From dcf2e45814ef3348b49750740fa82fb30c65f2e1 Mon Sep 17 00:00:00 2001 From: Ray Arndorfer Date: Sun, 1 Nov 2020 21:03:29 -0600 Subject: [PATCH] M e r g e (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Performance fixes (#5134) * Further fixes to profile data fetches, change the client to not create excessive amount of date objects * Sort entries once on load and then rely on the sorting to find out the largest and smallest value * Make the renderer reuse Date objects instead of instantiating a huge amount of dates all the time * Don't load retro data if not needed. Merge retro data faster if it's present. (#5141) * Updated Alexa template to comply with new requirements * Load data in one pass (#5142) * Fix layouting and load data in one go. Bonus points if you can fix the rendering for this * fix focus target lines and high and low lines * Remove debug logging. Change profile functions to clear the cache on new data, instead of calculating heavy hashes * Remove unused code * Fix tests * Have Travis run tests on Node 10 and 12, not 13 * D3 Upgrade Residual Cleanup (#5140) * fix forecast limit logic (cherry picked from commit e88ce7e5d1f5fb12c71ae67da4131ff1a50940c9) * fix minimum focus forecast time * make min forecast 30 min * fix forecast time max to client focusRangeMS instead of 30 min (#5143) * Fix initial forecast point display * Fix forecast time randomly resetting to 30 minutes for openaps users * Stop Focus Circles from Eating Entries (#5145) * stop focus circles from eating entries * fix forecastCircles remove * resolve possible key collision * Fixes a major bug where plugins were ran against a sandbox twice, causing issues with predictions. Fixes the look ahead timings. * Fix forecast behaviour (#5160) * stop focus circles from eating entries * fix forecastCircles remove * resolve possible key collision * Fixes a major bug where plugins were ran against a sandbox twice, causing issues with predictions. Fixes the look ahead timings. * Refactor of how prediction data is collected * Update node pushover library to fix crash in case Pushover service is down * adding list of supported APIs versions (GET /api/versions) * Fixed a few missing definitions * API3: add locking documents using isReadOnly flag * Translation improvements * Defined `translate()` in upbat plugin * Migrated titles to use translate() * Migrated static intent titles to use translate() * Misc fixes for tests (even though they didn't fail 🤔) * API3: testing isReadOnly feature * API3: more readonly field name variants * Added instructions on how to update skills/agents * fix devicestatus retro merge (#5193) * Turkish language updates (#5192) * Turkish language updates * in to Languages Part Turkish added * in to Languages Part Turkish added * Fix falsely suspended timeago alarms (#5170) * Fix continuous suspension of the timeago alerts The default hearbeat-setting is 60 seconds, so the delta between two timeago-checks will always be >15 seconds and the timeago-alarms will always be suspended (that's what Papertrail also shows). To fix this, make the delta-check heartbeat-setting-dependant and also simplify the code by using just 1 variable. * Add test that verifies the hibernation detection behaviour The real-world test would be to actually wait for a couple of minutes (with 2 * heartbeat of default settings) in the unit test, but this is not feasible, so just modify the heartbeat-setting to a lower value. I tested it by only running tests inside `tests/timeago.test.js` and by actually deploying this code and testing the alarms with Pushover and reading the logs in Papertrail. Before this change, I saw a 'Hibernation detected' log every minute. After this change, I didn't see it anymore, probably because the app wasn't actually hibernated (yet). * Take sulkaharo's feedback into account and differentiate between client and server (by introducing it in sandbox.js). On the client the behaviour is different from the server: > On client, the issue is browsers stop the execution of JS if the window is not > visible and the alarm is falsely triggered immediately when the execution is > resumed, so we need to suspend the alarm for ~10 seconds after the execution has > resumed to give the client time to update the data to prevent false alarms. While on the server, the default heartbeat from 60s needs to be taken into account to prevent the timeago alarm from falsely triggering. So detect hibernation there if the last check was more than 2 heartbeats ago. * Fix the tests by adding settings to the context, which is now required by timeago. Also, change the timeago test a bit so that it both succeeds when testing in isolation as when testing it along with the other tests. * Adding to #5121 (#5126) * changed gitter shield to discord * changed discord shield text to discord chat instead of just chat * changed gitter shield to discord * need to fix my md error * Update CONTRIBUTING.md Few more tweaks * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Update README.md Change Discord URL to be an invite link instead of a direct link to the channel (which doesn't work if you're not already in it). * Extra info (delta and last measurement time) on clock view. (#5151) * Parametrized clock view * Parametrized clock view * Wording tweaks to go with #4810 (#4866) * mmol/L wording tweak in Readme * Various wording & clarity tweaks in the Readme * Heroku template update to reflect mmol allowance tweak * Update README.md * Update minimum device requirements Ran some proper browser test suites, determined actual minimum specs for the main site. * GitHub actions build (#5168) * Run tests using GitHub Actions * Node 10 * Test CI fails * Use npm to run tests * Fix package json to run tests * Enable v3 TEST api for both development env and CI * Run CI on both Node 10 and 12 * Allow downgrading Mongo * Move the units configuration detection to a place where it's guaranteed to catch all invocations * Fix the check for environments without the units setting * Rename xdrip-js to xdripjs (#4959) * rename xdrip-js to xdripjs * update readme for xdrip-js name change * update from CGM to XDRIPJS label * change label from all caps to mixed case for xDripJS * change pill label back to CGM * Added customization step for Alexa skill updates * Language Update: added missing German Translations (#5172) * language Update devicestatus collection * language Update Admin-Tools * changed wording for admin authorization * typo corrected (admin auth) * language Updates for Admin Tools database cleanups * Don't use dynamic scale unless entries in database (#5195) * don't use dynamic scale unless have entries * fix report crash when no entries * set forecastTime correctly when no entries present * Language Matters: change "Normal" to "In Range" in glucose distribution reports * Added/updated TOC's * set auth dialog width to client width if smaller than default width * More hebrew translations * Added note regarding the use of the Alexa online simulator * fix focus scale hour format * trip open-top size to fit * fix clearing alarming when clearning CurrentSGV * do not set direction if not current * Handled 0 for pump reservoir * rollback npm-shrinkwrap.json * Fix for distributionpage bug that caused hour 23 to always be active even after uncheck * Russian translation update edit * Use default entries count instead of distributed number literals * Handle mmol to mgdl conversions with a constant reference (#5274) * Added sanity check for the bridge interval value (#4717) * Added sanity check for the bridge interval value, setting minimum allowed value to 30 seconds and maximum to 5 minutes. This is to avoid input of very low values which might overload the dexcom servers. * Added missin ';' * Fixed typoe in comment * Added test for default interval (not set in config) * Set lower limit to 1 second * Use auth tokens in api-secret (#5214) * Allow auth tokens to be passed in the api-secret header and used in the client in place of the API secret * Fix unit test * Fix admin test * Reload page when token is used as a secret * Fix dependency vulnerabilities * Added basal and bolus averages besides TDD and carbs (#4216) * Remove the leading 0 from the version, so we can comply to how version numbering should really work * Removed unsused var ic (#5288) * Use constants instead of number literals for http status codes (#5276) * Updated release name and number (#5280) * Allow use cgm-remote-monitor as npm package. (#4847) To use cgm-remote-monitor as npm package, the main attribute is required. * Add 4 hour option to view (#5289) * Revert "change default back to 3HR" This reverts commit e60ff3aa9d6c6cb18fe8b414d95cef09e76350b6. * shorten HR to H to avoid line-wrapping on mobile * try larger label font: 35 instead of 25 (vs. 40) * add comma * deviceInfo.recent null check * default back to 3, w/ 4 as an option * re-add H to single-digit hours * * Don't use cache manifest all all for development * Add 4 hours option to view and change the labelling to remove language issues * Have page scale the minor pill widths more gracefully * [Clock] Fix incorrect delta calculation (#5286) * Fix !delta check for delta == 0 * Fixed delta comparison * Hide unit suffix for SMBs issued by AndroidAPS (#4771) * Fix issues from `npm audit` * Read BG targets in using mmol when server is in mmol mode (#5291) * Read BG targets in using mmol when server is in mmol mode * Change the Heroku template to match the new logic * Deal with mmol targets being set using commas * Unbreak JSON serialization failing on the /properties API due to circular data reference * Bump version to 13.0.0 * Release ref update v2 (#5301) * Updated release name and number * Added missing version number * Added missing version number * Fix auth dialog sizing error (#5315) * Fix auth dialog sizing error (#5314) * Fix auth dialog sizing error * Fix Client Init After Auth (cherry picked from commit 1bf416c3bb964ed555850c3fc55977d69980b8e5) * update NS minor version * Release 13.0.1 (#5329) * Release ref update v2 (#5301) * Updated release name and number * Added missing version number * Added missing version number * Fix auth dialog sizing error (#5315) * Fix auth dialog sizing error (#5314) * Fix auth dialog sizing error * Fix Client Init After Auth (cherry picked from commit 1bf416c3bb964ed555850c3fc55977d69980b8e5) * update NS minor version * Bump version to 13.0.2-dev * fix some random test failures (#5341) * fix some random test failures * use typeof to test for existence * Added support for asking delta * Added time reference * Made delta reading more informative * Corrected time reference * Try shortening the response * Revert attempt * Updated templates and documentation * Minor improvement in response when delta is 0 * German translations improved and added (#5360) * Fix Alexa Launch and SessionEnded Requests (#5377) * Fix Alexa Launch Request * Allow LaunchRequest to handle intent if set, change shouldEndSession from string to bool * Fix SessionEndedRequest * Fix #5149 - discord link on CONTRIBUTING.md doesn't do anything (#5380) * Fix #5146 - display of treatment with only Fat and Protein set (#5381) * Basic report page style fixes and updates (#5387) * Add missing Polish translations and improve existing (#5382) * Add missing Polish translations and improve existing * Improve translations * Addressed virtual assistant usage with Authentication Roles (#5388) * Fix #5347 - Addressed auth roles * Unification * Fix timeago alarms (again). The changes to the rendered had an unintended consequence of triggering the timeago detection less frequently, causing the code to think the client has been hibernating, thus suppressing the alarms. The update changes the clock to be updated more frequently (this updating the hibernation detection), fixing the issue * Remove the server side test * Fix Graph Display Without Recent Devicestatus (#5409) * Update Russian (#5415) Corrected major and minor misprints and mistakes - particularly in variables, new translations added * Add server teardown ability (#5410) * adding teardown event to ctx.bus * adding teardown support for mmconnect and bridge plugins * Additional German Translations and corrections (#5405) * Cleaned up changed translations * Added Virtual Assistant Translations * Fix #5188 - move predictions to bundle and don't render chart on predictions offset change (#5395) * Fix #5188 - move predictions to bundle and don't render chart on predictions offset change * Fix predictions show for day to day * Move toolbar and authentication status to partial and include on each page (#5393) * Move authentication status to partial and include on each page * Move toolbar to partial Co-authored-by: Sulka Haro * Virtual assistant code simplification (#5400) * Moved duplicate plugin code to single location, and small improvements * Defined _each() * Pr/5379 (#5441) * Release 13.0.1 (#5329) * Release ref update v2 (#5301) * Updated release name and number * Added missing version number * Added missing version number * Fix auth dialog sizing error (#5315) * Fix auth dialog sizing error (#5314) * Fix auth dialog sizing error * Fix Client Init After Auth (cherry picked from commit 1bf416c3bb964ed555850c3fc55977d69980b8e5) * update NS minor version * Added handlers and translations for CGM info * Defined translate() * Fixed sensor state reference * Improved wording for tx age response * Improved wording for session duration response * Updated documentation and templates * Updated README.md TOC and a reference to it * Added CGM battery info * Added unit reference to CGM battery levels * Added handlers and translations for CGM info * Defined translate() * Fixed sensor state reference * Improved wording for tx age response * Improved wording for session duration response * Updated documentation and templates * Updated README.md TOC and a reference to it * Added CGM battery info * Added unit reference to CGM battery levels * Updated API reference in Google Home template Co-authored-by: Caleb * Volunteer for Polish translations (#5396) * Api3 output renderers (#5425) * APIv3: adding framework for various output renderers * APIv3: adding xml output renderer * APIv3: adding csv output renderer * APIv3: documenting supported output renderers * APIv3: testing output renderers * Restore glucose distribution test (#5434) * Increase coverage of tests on units utility and clean up timeago test (#5435) * Increase coverage of tests on units utility * clean up timeago test * Refactor to encapsulate duplicated settings logic (#5426) * Encapsulate duplicate settings checks inside functions * Simplify settings::isAlarmEventEnabled() * Send coverage reports to Codacy (#5444) * Send coverage reports to Codacy * Action secrets may not be read from forks :( * Let's see if this fixes the path & key passing * Ok one more try for the Actions env * Run tests and coverage separately * Try increasing the admintools test runtime for Node 10 * Run tests only once * Resolve unnecessary uses of eslint-ignore (#5436) * Resolve unnecessary disabling of no-undef eslint rule * Resolve unnecessary disabling of no-prototype-builtins eslint rule * Resolve unnecessary disabling of no-unused-vars eslint rule * Resolve unnecessary disabling of no-empty eslint rule * Fix eslint errors and add npm script for eslint (#5427) * re-enable auth check for device status routes * Resolve eslint errors * Add npm script for eslint * Correct regex for express extension middleware and add tests for expected behaviour * Resolve lint error in virtual assistant base * Update index.js * Update index.js * Remove redundant checks on entry ID (#5440) * Run CI Action for Pull Requests * Update DayToDay report for Loop Overrides (#5452) Add Loop override name/reason as text to the grey bar at the top of the graph. * Fix earlier merge error with predictions having moved to the report bundle * fix brushing loop (#5499) * Update README.md (#5480) Clarified that the "bridge" plugin is for Dexcom Share ("Dexcom" didn't appear at all in the read me), fixed a few typos, and added line 444 about the BRIDGE_SERVER variable. * Added period of days into headline of glucose distribution and percen… (#5428) * Added period of days into headline of glucose distribution and percentil chart report I make screencopies of the glucose distribution and percential chart report for my diabtes consultant and had to manually add the period of days to the report everytime because it was not shown in the report itself. I added the period of days this with a small number of lines of code and think this is helpful for other too. * removed comments as requested removed comments as requested * Camelcase for new variables reportPlugins, firstDay, lastDay, countDays * forget to save the change of reportPlugins in percentile.js * Move app caching to a service worker (#5504) * Move app caching to a webworker * Code cleanup * Code cleanup * Make Codacy happy * More parentheses * Added indexes to 'entries' and 'treatments' along with other updates (#5463) * Added compound indexes for treatments and entries collections. Updated ensureIndex to createIndex in mongo-strage.js as ensureIndex has been deprecated. Finally, updated testing/populate.js to be compatible with more recent versions of the node driver, as well as fixing a path issue. * Fixed missing end quote in lib/server/treatments.js. Changed all newly added double quotes to single quote to match style guide. * Removed indexes that referenced key600. * Fix: Round interporlated mg/dL value to an integer * Plugin to show database size (% of available space or in MiB) (#5496) * Database size plugin - pill that displays current mongoDB database size * Enabled dbsize by default * Fixed bug with dbsize not shownig when size is (rounded) 0% but real bytes > 0 * Cleanup & update to iconfont generation manual * Changed how warning/urgent levels are configured - from absolute MiB to percentage of DBSIZE_MAX * Trying to fix random fail of APIv3 tests (#5519) * APIv3: isolating documents from tests (not allowing clashes of calculated identifiers) * removing unused async keyword * client - mobile - Axis label font is scaled on small screens (#5512) On phones or other small screens, the axis labels (especially x axis) were previously an unreadable jumble of too-close letters * Split view (#5518) * Adds a 2, 3, 4 and 8 way split view option * Updated description * Generate the table on demand, so any number of sites from 1 to 8 generates a sensible layout * Update readme & don't crash if a name is missing * Allow flagging specific settings to be not exposed the /properties and /status APIs (#5525) * Add more variables to the special list * [DEV][FIX] Fix dbsize plugin to make it hideable again (#5529) * [FIX] Making dbsize plugin default but hideable - removed its forced show state * Removed unused lodash dependency * Removed duplicate if statement (#5531) Removed the additional if statement checking for the loop specific developer team id. This since the exact same if statement existed just above it. * Update Dutch translations (#5532) * Fix settings test * Update version to 13.1.0 * Fix topbar collapse on small screens (#5562) * Fix top bar collapsing on small screens * Fix wide button array overflowing the button bar * Option to switch off bolus amount outputs (#5522) * Option to switch off bolus amount outputs (#5514) * Fixing issue with carb value not be output when set to "none" (#5514) * Adding additional output options (#5514) * Adding environment variable option for 'x U and Over' option. This option is BOLUS_RENDER_OVER with a default value of 1 and the value can be an integer or a float, e.g. 0.3, 1.5, 2, etc... * Adding change to change the font size depending on the bolus value. * Merge two "all" options to create an option that displays as SMB had. * Fix d3 portrait to landscape brush failure (#5638) * fix d3 portrait to landscape brush failure * fix client.renderer.test for highlighBrushPoints function prototype change * fix highlightBrush * move brush reset inside check for valid brush * Sanitize data from manual careportal entries so only fields with actual data are sent to the server (#5619) * do not redirect to profile editor by default (#5671) In a default configuration, there is no treatment data. The code to redirect the UI to the profile editor is buried deep within the chart rendering code for basals. This plugin is only supposed to go into action when enabled via ENABLE=basal. This commit fixes first-use experience for the default configuration intended to draw real-time CGM traces and no basal information is expected. Since no basal information is expected unless plugin is enabled via ENABLE=basal, this allows skipping instead of redirecting to the profile editor. * Fix Issue #5486 - Device Status Days Feature (#5651) * Device Status Days Feature * Edits per review from @sulkaharo * Configurable clock views (#5625) * Configurable clock views * Configurable clock views * Configurable clock views * Configurable clock views * Configurable clock views * Configurable clock views * Update README.md * Update README.md * Configurable clock views * Add remote bolus/carbs + otp entry for loop (#5598) Add a remote bolus entry field for users on Loop, along with support for an OTP field * Sometimes at the very start no profiles provided. (#5591) At the first start the nightscout is crashed if profile not full. * Fixes #4991 - Now selects basal profile based day by day (#5572) * Test profile settings * NPM Version change * Use console log * Use console log for client * Use console log for client.sbx * Use console log for client.ddata * Use console log for client again * revert * Added multi-profile reporting capabilities Co-authored-by: VIS TFS Admin * prepare hotfix for MMConnect/EU - 2020-06-28 Prepare to handle issue #5719. This patch adjusts dependencies to target a community edition fork of minimed-connect-to-nightscout. The new dependency targets information provided to begin addressing changes to Medtronic Carelink services starting late June 2020. * bump pointing at right tag * prepare hotfix for MMConnect/EU - 2020-06-28 Prepare to handle issue #5719. This patch adjusts dependencies to target a community edition fork of minimed-connect-to-nightscout. The new dependency targets information provided to begin addressing changes to Medtronic Carelink services starting late June 2020. * bump pointing at right tag * hotfix attempt 2 - carelink 2020 sso updates * hotfix 3 - attempt 3 at carelink 2020 sso updates * attempt 4 at hotfix for nightscout Based on more reliable version of minimed-connect-to-nightscout.... This is testing the hotfix. When it works, we will bump this one more time with the newly released version. * hotfix 5 - incorporates latest axios changes * Fix for boluscalc issue (#5729) Boluscalc fix & remove integer validation from BG input field * npm install --save minimed-connect-to-nightscout@^1.4.0 * Change DISPLAY_UNITS to use readENV's default value and added several tests for DISPLAY_UNITS (#5708) readENV() was hard coding the default value of DISPLAY_UNITS, and ignoring the default value for no particular reason. Fixed that. Also, I added a full test suite for DISPLAY_UNITS environment settings to make sure that it works as intended. Finally, I've added the console log for the DISPLAY_UNITS since there's no particular reason why we log that setting and not others. * Fix audit errors, button order (#5694) * * Update packages giving npm audit errors * Fix Mongo test that broke after update (now uses async/await on getting the connection) * Restore original navigation bar button order * Disable Node 10 tests * Fix temporary target cancellation * Added documentation on the use of Alpha Testing (#5784) * Wip/bewest/mmconnect eu (#5731) * prepare hotfix for MMConnect/EU - 2020-06-28 Prepare to handle issue #5719. This patch adjusts dependencies to target a community edition fork of minimed-connect-to-nightscout. The new dependency targets information provided to begin addressing changes to Medtronic Carelink services starting late June 2020. * bump pointing at right tag * hotfix attempt 2 - carelink 2020 sso updates * hotfix 3 - attempt 3 at carelink 2020 sso updates * attempt 4 at hotfix for nightscout Based on more reliable version of minimed-connect-to-nightscout.... This is testing the hotfix. When it works, we will bump this one more time with the newly released version. Co-authored-by: Sulka Haro * * Updated several packages to remove issues from NPM audit * Fix excess logging in settings.js * Removed heapdump package, which was causing issues for some users * * Limit the default number of profiles returned by the API to fix issues with users who are syncing a large amount of profiles from an APS. * Fix profile reporting * Upgrade Helmet to 4.0.0 * CSP and service worker improvements (#5856) * Add connectSrc for Safari compatibiity * Add new font URL for Google Fonts compatibility * Reload on re-registering the worker * Change Mongo version in github to fix builds * Serve the CSP to everyone * Split view CSP support (#5864) * Parse split view frames into frameAncestors * Re-enable CSP configuration (#5867) * Re-enable CSP configuration * Bump share2nightscout-bridge plugin Use latest update from share2nightscout bridge to make things easier. It contains an updated default for OUS users. * feat Add robots.txt to disallow site crawling * Fix dbsize for usage with Atlas (#5962) * Openaps performance improvement (#5945) * improve openaps plugin performance * more performance improvements (cherry picked from commit 076a2dd3b69093149994258eb0244ae61d68f44e) * correct setting notEnacted variable * remove unnecessary debug logging * Adapt to MongoDB Atlas (#5893) * Fix Issue #5763 - Change $.ajax to /api/v1/profile (#5704) * Bug fixes for Service Worker (#5970) * Fix service worker refresh for Firefox * Improve logging for service worker initialisation * Skip browser cache for service worker precaching * Prevent `Partial Content` responses in `fetch` by stripping `Range` header * Prevent calling `filter` on an undefined property when service worker cache is empty * Bug fixes for Service Worker * Fixes https://github.com/nightscout/cgm-remote-monitor/issues/5920 * Fixes https://github.com/nightscout/cgm-remote-monitor/issues/5943 * Update service-worker.js Removed the MP3 files from the preload to fix issues with webkit-based browsers Co-authored-by: p5nbTgip0r Co-authored-by: Sulka Haro * Optimize Nightscout database usage (#5947) * On periodic polls, only load delta for CGM data, treatments and device statuses * Serve basic CGM data API queries from in-memory cache. Increases the Nightscout memory footprint but should significantly reduce the Mongo load * Serve device status, CGM and treatment records from memory cache * Support count parameter in /profile * Bumping version to 14.0.0 given this is a relatively large update and we want people on Atlas to all migrate to this version * Remove logging statements * fix brushing recursion * Normalize treatments to support absolute doses from the amount field * Add Loop Carbs foodType, absorptionTime to Reports for ISSUE #5701 (#5702) * add carb foodType, absorptionTime to reports * Update README to point to new docs * Fix `Treatments` report for `mmol/L` users Fixed a typo to a client function name * Bump version to 14.0.1 * Bump dev to 14.0.2 * Serve Last-Modified header for the service worker to help with some browsers misdetecting caching * Cleaned old appcache references * * Remove unnecessary logging to help with Papertrail limits * Load more treatments and entries to fix caching issues (need to add better cache invalidation to really fix this) * When merging inserts and updates from the database, ensure we always use the new data over the old data * Bump treatment delta loading to four hours * One more increase to treatment load time to cover long overrides * * Bump version to 14.0.3 * Fix a bug in the REST API query for profiles * Bumped up the number of treatments loaded / day for reporting * Fix reports unit test * Bump treatments default limit default to 1000 objects for queries that include other query parameters * * Require created_at for Treatments on insert * Refactoring the cache: moved to another file and support flushing from bus events * Add support for CGM data and device statuses. Refactor dataloader to use the new model and reduce queries back down * Fix data order for REST API * * Add cache update events to websocket API * Remove the validation for created_at in REST API ;( * Remove excess logging * Bump version to .4 * Bump version to 14.0.5 and fix #6050 * Fix Pebble API detecting mmol units from settings * Make settings parsing whitespace tolerant * * Fix security test * Change boot and caching to expose Mongo connection errors * Don't report an error on every reconnect * Less intimidating error page on start, which also checks for api_secret * Add file missing from last commit * Fix tests * Remove require statement breaking deploys * Update --feature-request--.md * Update --bug-report.md * Update --individual-troubleshooting-help.md * Fix memory leak and cache update issues in 14.0.4 (#6133) * Fix a memory leak in 14.0.4 * Fix linter error in ddata.js * Move data retention periods to caching * Update _id of inserted entries and device status so merging to cache works correctly * Reset the data in ddata before updating * Fix typo on entry cache retention period * Have device status cache retention period follow configuration * Fix _id injection in treatments * Add error cat to error page * Fix MONGODB_URI reference in error message * Bump version to 14.0.6 * Support uploading device statuses in batches (#6147) * Support uploading device statuses in batches * Correctly report batch insertion results * Make empty cache detection a bit more aggressive to account for cache flush and data insert happening concurrently * Update README.md Explicitly link to the application template in Github * Create config.yml * Delete --individual-troubleshooting-help.md * Update config.yml Co-authored-by: Sulka Haro Co-authored-by: inventor96 Co-authored-by: Jeremy Cunningham <34543464+jpcunningh@users.noreply.github.com> Co-authored-by: PetrOndrusek Co-authored-by: Caleb Co-authored-by: Diabetlum <44998140+diabetlum@users.noreply.github.com> Co-authored-by: Willem Fibbe Co-authored-by: John Weston Co-authored-by: ireneusz-ptak <31506973+ireneusz-ptak@users.noreply.github.com> Co-authored-by: Lukas Herzog Co-authored-by: Jeremy Cunningham Co-authored-by: Jake Bloom Co-authored-by: David Jansson Co-authored-by: fedor apanasenko Co-authored-by: Jakob Sandberg Co-authored-by: Jakob Co-authored-by: Asbjørn Aarrestad Co-authored-by: lixgbg Co-authored-by: Harmjan Greving Co-authored-by: Bluefox Co-authored-by: Cas Eliëns Co-authored-by: Tanja <7403966+tanja3981@users.noreply.github.com> Co-authored-by: Bartłomiej Szubert Co-authored-by: Andrew Dixon Co-authored-by: Petr Ondrusek <34578008+PetrOndrusek@users.noreply.github.com> Co-authored-by: Lukas Herzog Co-authored-by: jonfawcett <38429455+jonfawcett@users.noreply.github.com> Co-authored-by: Jonas Hummelstrand Co-authored-by: peterleimbach Co-authored-by: Adam Harrison Co-authored-by: Dominik Dzienia Co-authored-by: stephencmorton <17858976+stephencmorton@users.noreply.github.com> Co-authored-by: Simon Persson Co-authored-by: Cas Eliëns Co-authored-by: Ben West Co-authored-by: Matthew Co-authored-by: josep1972 Co-authored-by: fromorbonia <3193811+fromorbonia@users.noreply.github.com> Co-authored-by: VIS TFS Admin Co-authored-by: joracine Co-authored-by: Alexandre Viau Co-authored-by: Lennart Goedhart Co-authored-by: p5nbTgip0r Co-authored-by: kskandispersonal --- .eslintrc.js | 9 + .github/ISSUE_TEMPLATE/--bug-report.md | 4 + .github/ISSUE_TEMPLATE/--feature-request--.md | 4 + .../--individual-troubleshooting-help.md | 16 - .github/ISSUE_TEMPLATE/config.yml | 11 + .github/workflows/main.yml | 34 + .gitignore | 8 +- .travis.yml | 2 +- CONTRIBUTING.md | 65 +- Makefile | 4 +- README.md | 241 +- app.js | 215 +- app.json | 25 +- assets/fonts/Nightscout Plugin Icons.json | 87 + assets/fonts/README.md | 29 + azuredeploy.json | 4 +- bundle/bundle.reports.source.js | 3 +- bundle/bundle.source.js | 2 +- ci.test.env | 8 + ...add-virtual-assistant-support-to-plugin.md | 62 + docs/plugins/alexa-plugin.md | 341 +- docs/plugins/alexa-templates/en-us.json | 293 + docs/plugins/google-home-templates/en-us.zip | Bin 0 -> 6367 bytes docs/plugins/googlehome-plugin.md | 171 + .../interacting-with-virtual-assistants.md | 77 + env.js | 60 +- lib/api/activity/index.js | 8 +- lib/api/alexa/index.js | 246 +- lib/api/const.json | 4 + lib/api/devicestatus/index.js | 48 +- lib/api/entries/index.js | 60 +- lib/api/googlehome/index.js | 57 + lib/api/index.js | 6 +- lib/api/notifications-api.js | 8 +- lib/api/notifications-v2.js | 23 + lib/api/profile/index.js | 28 +- lib/api/properties.js | 2 + lib/api/root.js | 23 + lib/api/status.js | 12 +- lib/api/treatments/index.js | 142 +- lib/api/verifyauth.js | 9 +- lib/api3/const.json | 55 + lib/api3/doc/formats.md | 88 + lib/api3/doc/security.md | 48 + lib/api3/doc/socket.md | 142 + lib/api3/doc/tutorial.md | 329 + lib/api3/generic/collection.js | 193 + lib/api3/generic/create/insert.js | 46 + lib/api3/generic/create/operation.js | 63 + lib/api3/generic/create/validate.js | 26 + lib/api3/generic/delete/operation.js | 122 + lib/api3/generic/history/operation.js | 153 + lib/api3/generic/patch/operation.js | 118 + lib/api3/generic/patch/validate.js | 19 + lib/api3/generic/read/operation.js | 77 + lib/api3/generic/search/input.js | 140 + lib/api3/generic/search/operation.js | 79 + lib/api3/generic/setup.js | 103 + lib/api3/generic/update/operation.js | 86 + lib/api3/generic/update/replace.js | 53 + lib/api3/generic/update/validate.js | 48 + lib/api3/index.js | 110 + lib/api3/security.js | 122 + lib/api3/shared/dateTools.js | 78 + lib/api3/shared/fieldsProjector.js | 82 + lib/api3/shared/operationTools.js | 111 + lib/api3/shared/renderer.js | 99 + lib/api3/shared/storageTools.js | 63 + lib/api3/shared/stringTools.js | 28 + lib/api3/specific/lastModified.js | 101 + lib/api3/specific/status.js | 71 + lib/api3/specific/version.js | 28 + lib/api3/storage/mongoCollection/find.js | 93 + lib/api3/storage/mongoCollection/index.js | 90 + lib/api3/storage/mongoCollection/modify.js | 123 + lib/api3/storage/mongoCollection/utils.js | 178 + lib/api3/storageSocket.js | 145 + lib/api3/swagger.js | 41 + lib/api3/swagger.yaml | 1647 ++++ lib/authorization/index.js | 52 +- lib/authorization/storage.js | 24 +- lib/bus.js | 10 +- lib/client/browser-settings.js | 33 +- lib/client/browser-utils.js | 2 +- lib/client/careportal.js | 136 +- lib/client/chart.js | 565 +- lib/client/clock-client.js | 191 +- lib/client/d3locales.js | 16 + lib/client/index.js | 191 +- lib/client/receiveddata.js | 3 + lib/client/renderer.js | 548 +- lib/constants.json | 13 +- lib/data/calcdelta.js | 12 +- lib/data/dataloader.js | 252 +- lib/data/ddata.js | 122 +- lib/data/treatmenttocurve.js | 5 +- lib/hashauth.js | 58 +- lib/language.js | 2116 +++-- lib/middleware/express-extension-to-accept.js | 2 +- lib/middleware/index.js | 2 +- lib/notifications.js | 3 +- lib/plugins/alexa.js | 69 +- lib/plugins/ar2.js | 33 +- lib/plugins/basalprofile.js | 36 +- lib/plugins/bridge.js | 28 +- lib/plugins/careportal.js | 3 +- lib/plugins/cob.js | 45 +- lib/plugins/dbsize.js | 152 + lib/plugins/direction.js | 4 +- lib/plugins/googlehome.js | 97 + lib/plugins/index.js | 5 +- lib/plugins/iob.js | 25 +- lib/plugins/loop.js | 152 +- lib/plugins/mmconnect.js | 14 +- lib/plugins/openaps.js | 104 +- lib/plugins/pluginbase.js | 12 +- lib/plugins/pump.js | 54 +- lib/plugins/rawbg.js | 21 +- lib/plugins/timeago.js | 20 +- lib/plugins/upbat.js | 35 +- lib/plugins/virtAsstBase.js | 111 + lib/plugins/{xdrip-js.js => xdripjs.js} | 126 +- lib/profilefunctions.js | 146 +- lib/report/predictions.js | 33 + lib/report_plugins/calibrations.js | 14 +- lib/report_plugins/daytoday.js | 162 +- lib/report_plugins/glucosedistribution.js | 36 +- lib/report_plugins/loopalyzer.js | 13 +- lib/report_plugins/percentile.js | 31 +- lib/report_plugins/profiles.js | 15 +- lib/report_plugins/treatments.js | 26 +- lib/report_plugins/utils.js | 4 +- lib/report_plugins/weektoweek.js | 24 +- lib/sandbox.js | 4 +- lib/server/booterror.js | 40 +- lib/server/bootevent.js | 55 +- lib/server/cache.js | 103 + lib/server/clocks.js | 5 +- lib/server/devicestatus.js | 103 +- lib/server/entries.js | 116 +- lib/server/loop.js | 135 + lib/server/pebble.js | 2 +- lib/server/profile.js | 51 +- lib/server/pushnotify.js | 2 +- lib/server/treatments.js | 67 +- lib/server/websocket.js | 259 +- lib/settings.js | 125 +- lib/storage/mongo-storage.js | 114 +- lib/units.js | 6 +- lib/utils.js | 3 +- npm-shrinkwrap.json | 8249 +++++++++-------- package.json | 85 +- server.js | 8 +- static/css/drawer.css | 50 +- static/css/main.css | 74 +- static/css/report.css | 59 +- static/css/translations.css | 13 +- static/images/browserstack-logo.png | Bin 0 -> 14657 bytes static/images/errorcat.jpg | Bin 0 -> 48882 bytes static/profile/js/profileeditor.js | 9 +- static/report/js/predictions.js | 40 - static/report/js/report.js | 74 +- static/robots.txt | 4 +- swagger.json | 2 +- swagger.yaml | 2 +- test | 0 testing/populate.js | 7 +- tests/admintools.test.js | 8 +- tests/api.alexa.test.js | 97 + tests/api.devicestatus.test.js | 1 + tests/api.root.test.js | 42 + tests/api.treatments.test.js | 30 +- tests/api.verifyauth.test.js | 4 +- tests/api3.basic.test.js | 49 + tests/api3.create.test.js | 487 + tests/api3.delete.test.js | 53 + tests/api3.generic.workflow.test.js | 293 + tests/api3.patch.test.js | 219 + tests/api3.read.test.js | 180 + tests/api3.renderer.test.js | 268 + tests/api3.search.test.js | 261 + tests/api3.security.test.js | 189 + tests/api3.socket.test.js | 178 + tests/api3.update.test.js | 290 + tests/ar2.test.js | 8 +- tests/basalprofileplugin.test.js | 10 +- tests/bgnow.test.js | 1 + tests/bridge.test.js | 41 + tests/careportal.test.js | 2 +- tests/client.renderer.test.js | 9 +- tests/cob.test.js | 6 +- tests/dbsize.test.js | 320 + tests/ddata.test.js | 13 - tests/env.test.js | 86 +- tests/expressextensions.test.js | 33 + tests/fail.test.js | 14 + tests/fixtures/api/instance.js | 98 + tests/fixtures/api3/authSubject.js | 94 + tests/fixtures/api3/const.json | 138 + tests/fixtures/api3/instance.js | 163 + tests/fixtures/api3/localhost.crt | 18 + tests/fixtures/api3/localhost.key | 27 + tests/fixtures/api3/utils.js | 21 + tests/iob.test.js | 11 +- tests/loop.test.js | 11 +- tests/mongo-storage.test.js | 18 +- tests/openaps.test.js | 13 +- tests/profile.test.js | 200 +- tests/profileeditor.test.js | 6 +- tests/pump.test.js | 27 +- tests/rawbg.test.js | 6 +- tests/reports.test.js | 24 +- tests/security.test.js | 21 +- tests/settings.test.js | 2 +- tests/timeago.test.js | 6 +- tests/units.test.js | 16 + tests/upbat.test.js | 18 +- views/adminindex.html | 29 +- views/clockviews/bgclock.css | 79 - views/clockviews/clock-color.css | 80 - views/clockviews/clock-config.css | 39 + views/clockviews/clock-config.html | 65 + views/clockviews/clock-shared.css | 69 + views/clockviews/clock.css | 71 - views/clockviews/{shared.html => clock.html} | 27 +- views/error.html | 64 + views/foodindex.html | 33 +- views/frame.html | 54 + views/index.html | 1400 +-- views/nightscout.appcache | 37 - views/partials/authentication-status.ejs | 3 + views/partials/toolbar.ejs | 34 + views/profileindex.html | 30 +- views/reportindex.html | 166 +- views/service-worker.js | 175 + views/translationsindex.html | 9 +- 236 files changed, 21995 insertions(+), 8120 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/--individual-troubleshooting-help.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/main.yml create mode 100644 assets/fonts/Nightscout Plugin Icons.json create mode 100644 assets/fonts/README.md create mode 100644 ci.test.env create mode 100644 docs/plugins/add-virtual-assistant-support-to-plugin.md create mode 100644 docs/plugins/alexa-templates/en-us.json create mode 100644 docs/plugins/google-home-templates/en-us.zip create mode 100644 docs/plugins/googlehome-plugin.md create mode 100644 docs/plugins/interacting-with-virtual-assistants.md create mode 100644 lib/api/const.json create mode 100644 lib/api/googlehome/index.js create mode 100644 lib/api/notifications-v2.js create mode 100644 lib/api/root.js create mode 100644 lib/api3/const.json create mode 100644 lib/api3/doc/formats.md create mode 100644 lib/api3/doc/security.md create mode 100644 lib/api3/doc/socket.md create mode 100644 lib/api3/doc/tutorial.md create mode 100644 lib/api3/generic/collection.js create mode 100644 lib/api3/generic/create/insert.js create mode 100644 lib/api3/generic/create/operation.js create mode 100644 lib/api3/generic/create/validate.js create mode 100644 lib/api3/generic/delete/operation.js create mode 100644 lib/api3/generic/history/operation.js create mode 100644 lib/api3/generic/patch/operation.js create mode 100644 lib/api3/generic/patch/validate.js create mode 100644 lib/api3/generic/read/operation.js create mode 100644 lib/api3/generic/search/input.js create mode 100644 lib/api3/generic/search/operation.js create mode 100644 lib/api3/generic/setup.js create mode 100644 lib/api3/generic/update/operation.js create mode 100644 lib/api3/generic/update/replace.js create mode 100644 lib/api3/generic/update/validate.js create mode 100644 lib/api3/index.js create mode 100644 lib/api3/security.js create mode 100644 lib/api3/shared/dateTools.js create mode 100644 lib/api3/shared/fieldsProjector.js create mode 100644 lib/api3/shared/operationTools.js create mode 100644 lib/api3/shared/renderer.js create mode 100644 lib/api3/shared/storageTools.js create mode 100644 lib/api3/shared/stringTools.js create mode 100644 lib/api3/specific/lastModified.js create mode 100644 lib/api3/specific/status.js create mode 100644 lib/api3/specific/version.js create mode 100644 lib/api3/storage/mongoCollection/find.js create mode 100644 lib/api3/storage/mongoCollection/index.js create mode 100644 lib/api3/storage/mongoCollection/modify.js create mode 100644 lib/api3/storage/mongoCollection/utils.js create mode 100644 lib/api3/storageSocket.js create mode 100644 lib/api3/swagger.js create mode 100644 lib/api3/swagger.yaml create mode 100644 lib/plugins/dbsize.js create mode 100644 lib/plugins/googlehome.js create mode 100644 lib/plugins/virtAsstBase.js rename lib/plugins/{xdrip-js.js => xdripjs.js} (69%) create mode 100644 lib/report/predictions.js create mode 100644 lib/server/cache.js create mode 100644 lib/server/loop.js create mode 100644 static/images/browserstack-logo.png create mode 100644 static/images/errorcat.jpg delete mode 100644 static/report/js/predictions.js create mode 100644 test create mode 100644 tests/api.alexa.test.js create mode 100644 tests/api.root.test.js create mode 100644 tests/api3.basic.test.js create mode 100644 tests/api3.create.test.js create mode 100644 tests/api3.delete.test.js create mode 100644 tests/api3.generic.workflow.test.js create mode 100644 tests/api3.patch.test.js create mode 100644 tests/api3.read.test.js create mode 100644 tests/api3.renderer.test.js create mode 100644 tests/api3.search.test.js create mode 100644 tests/api3.security.test.js create mode 100644 tests/api3.socket.test.js create mode 100644 tests/api3.update.test.js create mode 100644 tests/dbsize.test.js create mode 100644 tests/expressextensions.test.js create mode 100644 tests/fail.test.js create mode 100644 tests/fixtures/api/instance.js create mode 100644 tests/fixtures/api3/authSubject.js create mode 100644 tests/fixtures/api3/const.json create mode 100644 tests/fixtures/api3/instance.js create mode 100644 tests/fixtures/api3/localhost.crt create mode 100644 tests/fixtures/api3/localhost.key create mode 100644 tests/fixtures/api3/utils.js delete mode 100644 views/clockviews/bgclock.css delete mode 100644 views/clockviews/clock-color.css create mode 100644 views/clockviews/clock-config.css create mode 100644 views/clockviews/clock-config.html create mode 100644 views/clockviews/clock-shared.css delete mode 100644 views/clockviews/clock.css rename views/clockviews/{shared.html => clock.html} (83%) create mode 100644 views/error.html create mode 100644 views/frame.html delete mode 100644 views/nightscout.appcache create mode 100644 views/partials/authentication-status.ejs create mode 100644 views/partials/toolbar.ejs create mode 100644 views/service-worker.js diff --git a/.eslintrc.js b/.eslintrc.js index 4bb1e696f5a..974a562c7c6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,15 @@ module.exports = { "commonjs": true, "es6": true, "node": true, + "mocha": true, "jquery": true + }, + "rules": { + "no-unused-vars": [ + "error", + { + "varsIgnorePattern": "should|expect" + } + ] } }; \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/--bug-report.md b/.github/ISSUE_TEMPLATE/--bug-report.md index b0d61217880..417f1eee1f8 100644 --- a/.github/ISSUE_TEMPLATE/--bug-report.md +++ b/.github/ISSUE_TEMPLATE/--bug-report.md @@ -5,6 +5,10 @@ label: bug --- +**If you need support for Nightscout, PLEASE DO NOT FILE A TICKET HERE** +For support, please post a question to the "CGM in The Cloud" group in Facebook +(https://www.facebook.com/groups/cgminthecloud) or visit the WeAreNotWaiting Discord at https://discord.gg/zg7CvCQ + **Describe the bug** A clear and concise description of what the bug is. diff --git a/.github/ISSUE_TEMPLATE/--feature-request--.md b/.github/ISSUE_TEMPLATE/--feature-request--.md index a94a261abf8..293efbc9c1e 100644 --- a/.github/ISSUE_TEMPLATE/--feature-request--.md +++ b/.github/ISSUE_TEMPLATE/--feature-request--.md @@ -4,6 +4,10 @@ about: Suggest an idea for this project --- +**If you need support for Nightscout, PLEASE DO NOT FILE A TICKET HERE** +For support, please post a question to the "CGM in The Cloud" group in Facebook +(https://www.facebook.com/groups/cgminthecloud) or visit the WeAreNotWaiting Discord at https://discord.gg/zg7CvCQ + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] diff --git a/.github/ISSUE_TEMPLATE/--individual-troubleshooting-help.md b/.github/ISSUE_TEMPLATE/--individual-troubleshooting-help.md deleted file mode 100644 index ff6e27b7682..00000000000 --- a/.github/ISSUE_TEMPLATE/--individual-troubleshooting-help.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: "\U0001F198Individual troubleshooting help" -about: Getting help with your own individual setup of Nightscout - ---- - -Having issues getting Nightscout up and running? Instead of creating an issue here, please use one of the existing support channels for Nightscout. - -The main support channel is on Facebook: please join the CGM In The Cloud Facebook group (https://www.facebook.com/groups/cgminthecloud) and start a post there. - -**Suggestions to include in your post when you are asking for help:** -1. Include what you are trying to do: ("*I am trying to set up Nightscout for the first time.*") -2. Include which step you are on and what the problem is: ("*I deployed on Heroku, but I'm not seeing any BG data.*") -3. If possible, include a link to the version of documentation you are following ("*I'm following the OpenAPS Nightscout setup docs (https://openaps.readthedocs.io/en/latest/docs/While%20You%20Wait%20For%20Gear/nightscout-setup.html#nightscout-setup-with-heroku)*") - -Other places you can find support and assistance for Nightscout include Gitter's [nightscout/public](https://gitter.im/nightscout/public) channel. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..659a213d692 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Troubleshooting Documentation + url: http://nightscout.github.io/troubleshoot/troublehoot/ + about: Having trouble with Nightscout? Please check our step by step troubleshooting instructions. + - name: Nightscout Community Support in Facebook + url: https://www.facebook.com/groups/cgminthecloud + about: If you're a Nightscout user and have trouble with your site, please post questions here. We don't have the resources to answer support questions posted here as tickets. + - name: Nightscout Community Support in Discord + url: https://discord.gg/zg7CvCQ + about: If you're a Nightscout user and have trouble with your site, please post questions here. We don't have the resources to answer support questions posted here as tickets. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000000..e661a61ab7b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,34 @@ +name: CI test + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-16.04 + + strategy: + matrix: + node-version: [12.x] + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + - name: Install MongoDB + run: | + wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add - + echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/4.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list + sudo apt-get update + sudo apt-get install -y mongodb-org + sudo apt-get install -y --allow-downgrades mongodb-org=4.4.0 mongodb-org-server=4.4.0 mongodb-org-shell=4.4.0 mongodb-org-mongos=4.4.0 mongodb-org-tools=4.4.0 + - name: Start MongoDB + run: sudo systemctl start mongod + - name: Run Tests + run: npm run-script test-ci + - name: Send Coverage + run: npm run-script coverage diff --git a/.gitignore b/.gitignore index 2cb28800650..1cf7ab06f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,8 @@ bundle/bundle.out.js .idea/ *.iml my.env +my.*.env -*.env static/bower_components/ .*.sw? .DS_Store @@ -24,3 +24,9 @@ npm-debug.log *.heapsnapshot /tmp +/.vs +/cgm-remote-monitor.njsproj +/cgm-remote-monitor.sln +/obj/Debug +/bin +/*.bat diff --git a/.travis.yml b/.travis.yml index 90331521284..78b056b8dbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,5 +26,5 @@ matrix: include: - node_js: "10" <<: *node_js-steps - - node_js: "node" # Latest Node is not supported, and recommend, but we'll test it to know incompatibility issues + - node_js: "12" # Latest Node is not supported, and recommend, but we'll test it to know incompatibility issues <<: *node_js-steps diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2e71049f4c..7beea2d51a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,9 +25,7 @@ [![Build Status][build-img]][build-url] [![Dependency Status][dependency-img]][dependency-url] [![Coverage Status][coverage-img]][coverage-url] -[![Gitter chat][gitter-img]][gitter-url] -[![Stories in Ready][ready-img]][waffle] -[![Stories in Progress][progress-img]][waffle] +[![Discord chat][discord-img]][discord-url] [build-img]: https://img.shields.io/travis/nightscout/cgm-remote-monitor.svg [build-url]: https://travis-ci.org/nightscout/cgm-remote-monitor @@ -35,11 +33,8 @@ [dependency-url]: https://david-dm.org/nightscout/cgm-remote-monitor [coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg [coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master -[gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg -[gitter-url]: https://gitter.im/nightscout/public -[ready-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=ready&title=Ready -[waffle]: https://waffle.io/nightscout/cgm-remote-monitor -[progress-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=in+progress&title=In+Progress +[discord-img]: https://img.shields.io/discord/629952586895851530?label=discord%20chat +[discord-url]: https://discord.gg/rTKhrqz ## Installation for development @@ -47,11 +42,11 @@ Nightscout is a Node.js application. The basic installation of the software for 1. Clone the software to your local machine using git 2. Install Node from https://nodejs.org/en/download/ -2. Use `npm` to install Nightscout dependencies by invokin `npm install` in the project directory. Note the - dependency installation has to be done usign a non-root user - _do not use root_ for development and hosting +2. Use `npm` to install Nightscout dependencies by invoking `npm install` in the project directory. Note the + dependency installation has to be done using a non-root user - _do not use root_ for development and hosting the software! -3. Get a Mongo database by either installing Mongo locally, or get a free cloud account from mLab or Mongodb Atlas. -4. Configure nightscout by copying `my.env.template` to `my.env` and run it - see the next chapter in the instructions +3. Get a Mongo database by either installing Mongo locally, or get a free cloud account from mLab or MongoDB Atlas. +4. Configure Nightscout by copying `my.env.template` to `my.env` and run it - see the next chapter in the instructions ## Develop on `dev` @@ -59,7 +54,7 @@ We develop on the `dev` branch. All new pull requests should be targeted to `dev You can get the `dev` branch checked out using `git checkout dev`. -Once checked out, install the dependencies using `npm install`, then copy the included `my.env.template`file to `my.env` and edit the file to include your settings (like the Mongo URL). Leave the `NODE_ENV=development` line intact. Once set, run the site using `npm run dev`. This will start Nigthscout in the development mode, with different code packaging rules and automatic restarting of the server using nodemon, when you save changed files on disk. The client also hot-reloads new code in, but it's recommended to reload the the website after changes due to the way the plugin sandbox works. +Once checked out, install the dependencies using `npm install`, then copy the included `my.env.template`file to `my.env` and edit the file to include your settings (like the Mongo URL). Leave the `NODE_ENV=development` line intact. Once set, run the site using `npm run dev`. This will start Nightscout in the development mode, with different code packaging rules and automatic restarting of the server using nodemon, when you save changed files on disk. The client also hot-reloads new code in, but it's recommended to reload the website after changes due to the way the plugin sandbox works. Note the template sets `INSECURE_USE_HTTP` to `true` to enable the site to work over HTTP in local development. @@ -67,20 +62,15 @@ If you want to additionaly test the site in production mode, create a file calle ## REST API -Nightscout implements a REST API for data syncronization. The API is documented using Swagger. To access the documentation -for the API, run Nightscout locally and load the documentation from /api-docs (or read the associated swagger.json and swagger.yaml -files locally). +Nightscout implements a REST API for data syncronization. The API is documented using Swagger. To access the documentation for the API, run Nightscout locally and load the documentation from /api-docs (or read the associated swagger.json and swagger.yaml files locally). -Note all dates used to access the API and dates stored in the objects are expected to comply with the ISO-8601 format and -be deserializable by the Javascript Date class. Of note here is the dates can contain a plus sign which has a special meaning in -URL encoding, so when issuing requests that place dates to the URL, take special care to ensure the data is properly URL -encoded. +Note all dates used to access the API and dates stored in the objects are expected to comply with the ISO-8601 format and be deserializable by the Javascript Date class. Of note here is the dates can contain a plus sign which has a special meaning in URL encoding, so when issuing requests that place dates to the URL, take special care to ensure the data is properly URL encoded. ## Design & new features If you intend to add a new feature, please allow the community to participate in the design process by creating an issue to discuss your design. For new features, the issue should describe what use cases the new feature intends to solve, or which existing use cases are being improved. -Note Nighscout has a plugin architecture for adding new features. We expect most code for new features live inside a Plugin, so the code retains a clear separation of concerns. If the Plugin API doesn't implement all features you need to implement your feature, please discuss with us on adding those features to the API. Note new features should under almost no circumstances require changes to the existing plugins. +Note Nightscout has a plugin architecture for adding new features. We expect most code for new features live inside a Plugin, so the code retains a clear separation of concerns. If the Plugin API doesn't implement all features you need to implement your feature, please discuss with us on adding those features to the API. Note new features should under almost no circumstances require changes to the existing plugins. ## Style Guide @@ -105,7 +95,7 @@ If in doubt, format your code with `js-beautify --indent-size 2 --comma-first - ## Create a prototype -Fork cgm-remote-monitor and create a branch. You can create a branch using `git checkout -b wip/add-my-widget`. This creates a new branch called `wip/add-my-widget`. The `wip` stands for work in progress and is a common prefix so that when know what to expect when reviewing many branches. +Fork cgm-remote-monitor and create a branch. You can create a branch using `git checkout -b wip/add-my-widget`. This creates a new branch called `wip/add-my-widget`. The "`wip`" stands for work-in-progress and is a common prefix so that we know what to expect when reviewing many branches. ## Submit a pull request @@ -115,11 +105,9 @@ This can be done by checking your code `git commit -avm 'my improvements are her Now that the commits are available on github, you can click on the compare buttons on your fork to create a pull request. Make sure to select [Nightscout's `dev` branch](https://github.com/nightscout/cgm-remote-monitor/tree/dev). -We assume all new Pull Requests are at least smoke tested by the author and all code in the PR actually works. -Please include a description of what the features do and rationalize why the changes are needed. +We assume all new Pull Requests are at least smoke tested by the author and all code in the PR actually works. Please include a description of what the features do and rationalize why the changes are needed. -If you add any new NPM module dependencies, you have to rationalize why there are needed - we prefer pull requests that reduce dependencies, not add them. -Before releasing a a new version, we check with `npm audit` if our dependencies don't have known security issues. +If you add any new NPM module dependencies, you have to rationalize why they are needed - we prefer pull requests that reduce dependencies, not add them. Before releasing a a new version, we check with `npm audit` if our dependencies don't have known security issues. When adding new features that add configuration options, please ensure the `README` document is amended with information on the new configuration. @@ -142,7 +130,7 @@ We encourage liberal use of the comments, including images where appropriate. ## Co-ordination -Most cgm-remote-monitor hackers use github's ticketing system, along with Facebook cgm-in-the-cloud, and gitter. +We primarily use GitHub's ticketing system for discussing PRs and bugs, and [Discord][discord-url] for general development chatter. We use git-flow, with `master` as our production, stable branch, and `dev` is used to queue up for upcoming releases. Everything else is done on branches, hopefully with names that indicate what to expect. @@ -152,7 +140,7 @@ Every commit is tested by travis. We encourage adding tests to validate your de ## Other Dev Tips -* Join the [Gitter chat][gitter-url] +* Join the [Discord chat][discord-url]. * Get a local dev environment setup if you haven't already. * Try breaking up big features/improvements into small parts. It's much easier to accept small PR's. * Create tests for your new code as well as the old code. We are aiming for a full test coverage. @@ -193,6 +181,7 @@ Also if you can't code, it's possible to contribute by improving the documentati [@unsoluble]: https://github.com/unsoluble [@viderehh]: https://github.com/viderehh [@OpossumGit]: https://github.com/OpossumGit +[@Bartlomiejsz]: https://github.com/Bartlomiejsz | Contribution area | List of contributors | | ------------------------------------- | ---------------------------------- | @@ -203,13 +192,13 @@ Also if you can't code, it's possible to contribute by improving the documentati | Release coordination 0.11.x: | [@PieterGit] | | Issue/Pull request coordination: | Please volunteer | | Cleaning up git fork spam: | Please volunteer | -| Documentation writers: | [@andrew-warrington][@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | +| Documentation writers: | [@andrew-warrington] [@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | ### Plugin contributors | Contribution area | List of developers | List of testers | ------------------------------------- | -------------------- | -------------------- | -| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| Please volunteer | Please volunteer | +| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| [@inventor96] | Please volunteer | | [`ar2` (AR2 Forecasting)](README.md#ar2-ar2-forecasting)| Please volunteer | Please volunteer | | [`basal` (Basal Profile)](README.md#basal-basal-profile)| Please volunteer | Please volunteer | | [`boluscalc` (Bolus Wizard)](README.md#boluscalc-bolus-wizard)| Please volunteer | Please volunteer | @@ -224,7 +213,7 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`direction` (BG Direction)](README.md#direction-bg-direction)| Please volunteer | Please volunteer | | [`errorcodes` (CGM Error Codes)](README.md#errorcodes-cgm-error-codes)| Please volunteer | Please volunteer | | [`food` (Custom Foods)](README.md#food-custom-foods)| Please volunteer | Please volunteer | -| [`googlehome` (Google Home)](README.md#google-home) |[@mdomox] [@rickfriele] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | +| [`googlehome` (Google Home/DialogFlow)](README.md#googlehome-google-homedialogflow)| [@mdomox] [@rickfriele] [@inventor96] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | | [`iage` (Insulin Age)](README.md#iage-insulin-age)| Please volunteer | Please volunteer | | [`iob` (Insulin-on-Board)](README.md#iob-insulin-on-board)| Please volunteer | Please volunteer | | [`loop` (Loop)](README.md#loop-loop)| Please volunteer | Please volunteer | @@ -233,9 +222,9 @@ Also if you can't code, it's possible to contribute by improving the documentati | [`profile` (Treatment Profile)](README.md#profile-treatment-profile)| Please volunteer | Please volunteer | | [`pump` (Pump Monitoring)](README.md#pump-pump-monitoring)| Please volunteer | Please volunteer | | [`rawbg` (Raw BG)](README.md#rawbg-raw-bg)| [@jpcunningh] | Please volunteer | -| [`sage` (Sensor Age)](README.md#sage-sensor-age)| @jpcunningh | Please volunteer | +| [`sage` (Sensor Age)](README.md#sage-sensor-age)| [@jpcunningh] | Please volunteer | | [`simplealarms` (Simple BG Alarms)](README.md#simplealarms-simple-bg-alarms)| Please volunteer | Please volunteer | -| [`speech` (Speech)](README.md#speech-speech) | [@sulkaharo] | Please volunteer | +| [`speech` (Speech)](README.md#speech-speech)| [@sulkaharo] | Please volunteer | | [`timeago` (Time Ago)](README.md#timeago-time-ago)| Please volunteer | Please volunteer | | [`treatmentnotify` (Treatment Notifications)](README.md#treatmentnotify-treatment-notifications)| Please volunteer | Please volunteer | | [`upbat` (Uploader Battery)](README.md#upbat-uploader-battery)| [@jpcunningh] | Please volunteer | @@ -252,19 +241,19 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver | Čeština (`cs`) |Please volunteer|OK | | Deutsch (`de`) |[@viderehh] [@herzogmedia] |OK | | Dansk (`dk`) | [@janrpn] |OK | -| Ελληνικά `(el`)|Please volunteer|Needs attention: 68.5%| +| Ελληνικά (`el`)|Please volunteer|Needs attention: 68.5%| | English (`en`)|Please volunteer|OK| | Español (`es`) |Please volunteer|OK| | Suomi (`fi`)|[@sulkaharo] |OK| | Français (`fr`)|Please volunteer|OK| -| עברית (`he`)|Please volunteer|OK| -| Hrvatski (`hr`)|[@OpossumGit]|Needs attention: 47.8% - committed 100% to dev| +| עברית (`he`)| [@jakebloom] |OK| +| Hrvatski (`hr`)|[@OpossumGit]|OK| | Italiano (`it`)|Please volunteer|OK| | 日本語 (`ja`)|[@LuminaryXion]|Working on this| | 한국어 (`ko`)|Please volunteer|Needs attention: 80.6%| | Norsk (Bokmål) (`nb`)|Please volunteer|OK| | Nederlands (`nl`)|[@PieterGit]|OK| -| Polski (`pl`)|Please volunteer|OK| +| Polski (`pl`)|[@Bartlomiejsz]|OK| | Português (Brasil) (`pt`)|Please volunteer|OK| | Română (`ro`)|Please volunteer|OK| | Русский (`ru`)|[@apanasef]|OK| @@ -279,7 +268,7 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver ### List of all contributors | Contribution area | List of contributors | | ------------------------------------- | -------------------- | -| All active developers: | [@jasoncalabrese] [@jpcunningh] [@jweismann] [@komarserjio] [@mdomox] [@MilosKozak] [@PieterGit] [@rickfriele] [@sulkaharo] +| All active developers: | [@jasoncalabrese] [@jpcunningh] [@jweismann] [@komarserjio] [@mdomox] [@MilosKozak] [@PieterGit] [@rickfriele] [@sulkaharo] [@unsoluble] | All active testers/documentors: | [@danamlewis] [@jamieowendexcom] [@mcdafydd] [@oteroos] [@rarneson] [@tynbendad] [@unsoluble] | All active translators: | [@apanasef] [@jizhongwen] [@viderehh] [@herzogmedia] [@LuminaryXion] [@OpossumGit] diff --git a/Makefile b/Makefile index bf87aaed1c1..1ca626ab88c 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ report: test_onebyone: python -c 'import os,sys,fcntl; flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL); fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK);' - $(foreach var,$(wildcard tests/*.js),${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap $(var);) + for var in tests/*.js; do ${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap $$var; done | tap-set-exit test: ${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap ${TESTS} @@ -52,7 +52,7 @@ travis: python -c 'import os,sys,fcntl; flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL); fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK);' # NODE_ENV=test ${MONGO_SETTINGS} \ # ${ISTANBUL} cover ${MOCHA} --report lcovonly -- --timeout 5000 -R tap ${TESTS} - $(foreach var,$(wildcard tests/*.js),${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap $(var);) + for var in tests/*.js; do ${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap $$var; done docker_release: # Get the version from the package.json file diff --git a/README.md b/README.md index 917be293303..fce55097f9f 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Nightscout Web Monitor (a.k.a. cgm-remote-monitor) [![Dependency Status][dependency-img]][dependency-url] [![Coverage Status][coverage-img]][coverage-url] [![Codacy Badge][codacy-img]][codacy-url] -[![Gitter chat][gitter-img]][gitter-url] +[![Discord chat][discord-img]][discord-url] -[![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/) [![Deploy to Heroku][heroku-img]][heroku-url] [![Update your site][update-img]][update-fork] +[![Deploy to Heroku][heroku-img]][heroku-url] [![Update your site][update-img]][update-fork] This acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patient's glucose data in @@ -20,10 +20,21 @@ and blood glucose values are predicted 0.5 hours ahead using an autoregressive second order model. Alarms are generated for high and low values, which can be cleared by any watcher of the data. -# [#WeAreNotWaiting](https://twitter.com/hashtag/wearenotwaiting?src=hash&vertical=default&f=images) and [this](https://vimeo.com/109767890) is why. +# Looking for documentation? -Community maintained fork of the -[original cgm-remote-monitor][original]. +## End user? + +Nightscout documentation is currently split to two locations. This page lists all the configuration options in +Nightscout and is useful for users who've already gone through the installation process. IF you're looking +for the documentation that looks like it's written for non-programmers, that's located at [nightscout.github.io](https://nightscout.github.io/). + +Older documentation is available at [nightscout.info](http://nightscout.info). + +## Developer? + +See [CONTRIBUTING.md](CONTRIBUTING.md) + +## [#WeAreNotWaiting](https://twitter.com/hashtag/wearenotwaiting?src=hash&vertical=default&f=images) and [this](https://vimeo.com/109767890) is why. [![Coverage Status](https://coveralls.io/repos/github/nightscout/cgm-remote-monitor/badge.svg?branch=master)](https://coveralls.io/github/nightscout/cgm-remote-monitor?branch=master) @@ -35,37 +46,39 @@ Community maintained fork of the [coverage-url]: https://coveralls.io/github/nightscout/cgm-remote-monitor?branch=master [codacy-img]: https://www.codacy.com/project/badge/f79327216860472dad9afda07de39d3b [codacy-url]: https://www.codacy.com/app/Nightscout/cgm-remote-monitor -[gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg -[gitter-url]: https://gitter.im/nightscout/public +[discord-img]: https://img.shields.io/discord/629952586895851530?label=discord%20chat +[discord-url]: https://discord.gg/rTKhrqz [heroku-img]: https://www.herokucdn.com/deploy/button.png -[heroku-url]: https://heroku.com/deploy +[heroku-url]: https://heroku.com/deploy?template=https://github.com/nightscout/cgm-remote-monitor [update-img]: update.png [update-fork]: http://nightscout.github.io/pages/update-fork/ [original]: https://github.com/rnpenguin/cgm-remote-monitor + **Table of Contents** - [Install](#install) - [Supported configurations:](#supported-configurations) - - [Minimum browser requirements for viewing the site:](#minimum-browser-requirements-for-viewing-the-site) + - [Recommended minimum browser versions for using Nightscout:](#recommended-minimum-browser-versions-for-using-nightscout) - [Windows installation software requirements:](#windows-installation-software-requirements) - [Installation notes for users with nginx or Apache reverse proxy for SSL/TLS offloading:](#installation-notes-for-users-with-nginx-or-apache-reverse-proxy-for-ssltls-offloading) - [Installation notes for Microsoft Azure, Windows:](#installation-notes-for-microsoft-azure-windows) +- [Development](#development) - [Usage](#usage) - [Updating my version?](#updating-my-version) - - [What is my mongo string?](#what-is-my-mongo-string) - [Configure my uploader to match](#configure-my-uploader-to-match) - [Nightscout API](#nightscout-api) - [Example Queries](#example-queries) - [Environment](#environment) - [Required](#required) - - [Features/Labs](#featureslabs) + - [Features](#features) - [Alarms](#alarms) - [Core](#core) - [Predefined values for your browser settings (optional)](#predefined-values-for-your-browser-settings-optional) - [Predefined values for your server settings (optional)](#predefined-values-for-your-server-settings-optional) + - [Views](#views) - [Plugins](#plugins) - [Default Plugins](#default-plugins) - [`delta` (BG Delta)](#delta-bg-delta) @@ -97,8 +110,9 @@ Community maintained fork of the - [`openaps` (OpenAPS)](#openaps-openaps) - [`loop` (Loop)](#loop-loop) - [`override` (Override Mode)](#override-override-mode) - - [`xdrip-js` (xDrip-js)](#xdrip-js-xdrip-js) + - [`xdripjs` (xDrip-js)](#xdripjs-xdrip-js) - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) + - [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow) - [`speech` (Speech)](#speech-speech) - [`cors` (CORS)](#cors-cors) - [Extended Settings](#extended-settings) @@ -108,6 +122,7 @@ Community maintained fork of the - [Setting environment variables](#setting-environment-variables) - [Vagrant install](#vagrant-install) - [More questions?](#more-questions) + - [Browser testing suite provided by](#browser-testing-suite-provided-by) - [License](#license) @@ -116,30 +131,39 @@ Community maintained fork of the ## Supported configurations: -If you plan to use Nightscout, we recommend using [Heroku](http://www.nightscout.info/wiki/welcome/set-up-nightscout-using-heroku), as Nightscout can reach the usage limits of the free Azure plan and cause it to shut down for hours or days. If you end up needing a paid tier, the $7/mo Heroku plan is also much cheaper than the first paid tier of Azure. Currently, the only added benefit to choosing the $7/mo Heroku plan vs the free Heroku plan is a section showing site use metrics for performance (such as response time). This has limited benefit to the average Nightscout user. In short, Heroku is the free and best option for Nightscout hosting. +If you plan to use Nightscout, we recommend using [Heroku](https://nightscout.github.io/nightscout/new_user/) as this is free and easy to use. +We used to recommend hostig at Azure, but the resource needs of Nightscout have grown over the years and Azure won't comfortably run Nightscout +anymore in the free tier. If you're hosting in Azure and looking to update your site, we recommend you +[switch from Azure to Heroku](http://openaps.readthedocs.io/en/latest/docs/While%20You%20Wait%20For%20Gear/nightscout-setup.html#switching-from-azure-to-heroku) +as you're likely to hit issues in the process of updating the site. -- [Nightscout Setup with Heroku](http://www.nightscout.info/wiki/welcome/set-up-nightscout-using-heroku) (recommended) -- [Nightscout Setup with Microsoft Azure](http://www.nightscout.info/wiki/faqs-2/azure-2) (not recommended, please -[switch from Azure to Heroku](http://openaps.readthedocs.io/en/latest/docs/While%20You%20Wait%20For%20Gear/nightscout-setup.html#switching-from-azure-to-heroku) ) -- Linux based install (Debian, Ubuntu, Raspbian) install with own Node.JS and MongoDB install (see software requirements below) -- Windows based install with own Node.JS and MongoDB install (see software requirements below) +- [Nightscout Setup with Heroku](https://nightscout.github.io/nightscout/new_user/) (recommended) + +While you can install Nightscout on a virtual server or a Raspberry Pi, we do not recommend this unless you have at least some +experience hosting Node applications and development using the toolchain in use with Nightscout. Heroku automates all of the +hosting for you and even many of the dvelopers run their production sites in Heroku due to convenience. + +If you're a hosting provider and want to provide our users additional free hosting options, +you're welcome to issue a documentation pull request with instructions on how to setup Nightscout on your system. ## Recommended minimum browser versions for using Nightscout: Older versions of the browsers might work, but are untested. - Android 4 -- Chrome 68 +- iOS 6 +- Chrome 35 - Edge 17 - Firefox 61 +- Opera 12.1 +- Safari 6 (macOS 10.7) - Internet Explorer: not supported -- iOS 11 -- Opera 54 -- Safari 10 (macOS 10.12) + +Some features may not work with devices/browsers on the older end of these requirements. ## Windows installation software requirements: -- [Node.js](http://nodejs.org/) Latest Node 8 LTS (Node 8.15.1 or later) or Node 10 LTS (Node 10.16.0 or later; Node 10.15.2 works for Azure). Node versions that do not have the latest security patches will not work. Use [Install instructions for Node](https://nodejs.org/en/download/package-manager/) or use `setup.sh`) +- [Node.js](http://nodejs.org/) Latest Node 12 LTS. Node versions that do not have the latest security patches will not work. Use [Install instructions for Node](https://nodejs.org/en/download/package-manager/) or use `setup.sh`) - [MongoDB](https://www.mongodb.com/download-center?jmp=nav#community) 3.x or later. MongoDB 2.4 is only supported for Raspberry Pi. As a non-root user clone this repo then install dependencies into the root of the project: @@ -155,7 +179,7 @@ $ npm install - HTTP Strict Transport Security (HSTS) headers are enabled by default, use settings `SECURE_HSTS_HEADER` and `SECURE_HSTS_HEADER_*` - See [Predefined values for your server settings](#predefined-values-for-your-server-settings-optional) for more details -## Installation notes for Microsoft Azure, Windows: +## Installation notes for Microsoft Azure, Windows: - If deploying the software to Microsoft Azure, you must set ** in the app settings for *WEBSITE_NODE_DEFAULT_VERSION* and *SCM_COMMAND_IDLE_TIMEOUT* **before** you deploy the latest Nightscout or the site deployment will likely fail. Other hosting environments do not require this setting. Additionally, if using the Azure free hosting tier, the installation might fail due to resource constraints imposed by Azure on the free hosting. Please set the following settings to the environment in Azure: ``` @@ -163,31 +187,23 @@ WEBSITE_NODE_DEFAULT_VERSION=10.15.2 SCM_COMMAND_IDLE_TIMEOUT=300 ``` - See [install MongoDB, Node.js, and Nightscouton a single Windows system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). if you want to host your Nightscout outside of the cloud. Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10. -- If you deploy to Windows and want to develop or test you need to install [Cygwin](https://www.cygwin.com/) (use [setup-x86_64.exe](https://www.cygwin.com/setup-x86_64.exe) and make sure to install `build-essential` package. Test your configuration by executing `make` and check if all tests are ok. +- If you deploy to Windows and want to develop or test you need to install [Cygwin](https://www.cygwin.com/) (use [setup-x86_64.exe](https://www.cygwin.com/setup-x86_64.exe) and make sure to install `build-essential` package. Test your configuration by executing `make` and check if all tests are ok. # Development -Wanna help with development, or just see how Nigthscout works? Great! See [CONTRIBUTING.md](CONTRIBUTING.md) for development related documentation. +Want to help with development, or just see how Nightscout works? Great! See [CONTRIBUTING.md](CONTRIBUTING.md) for development-related documentation. # Usage -The data being uploaded from the server to the client is from a -MongoDB server such as [mLab][mLab]. +The data being uploaded from the server to the client is from a MongoDB server such as [MongoDB Atlas][https://www.mongodb.com]. -[mLab]: https://mlab.com/ [autoconfigure]: https://nightscout.github.io/pages/configure/ [mongostring]: https://nightscout.github.io/pages/mongostring/ ## Updating my version? -The easiest way to update your version of cgm-remote-monitor to our latest -recommended version is to use the [update my fork tool][update-fork]. It even -gives out stars if you are up to date. -## What is my mongo string? - -Try the [what is my mongo string tool][mongostring] to get a good idea of your -mongo string. You can copy and paste the text in the gray box into your -`MONGO_CONNECTION` environment variable. +The easiest way to update your version of cgm-remote-monitor to the latest version is to use the [update tool][update-fork]. A step-by-step guide is available [here][http://www.nightscout.info/wiki/welcome/how-to-update-to-latest-cgm-remote-monitor-aka-cookie]. +To downgrade to an older version, follow [this guide][http://www.nightscout.info/wiki/welcome/how-to-deploy-an-older-version-of-nightscout]. ## Configure my uploader to match @@ -195,7 +211,7 @@ Use the [autoconfigure tool][autoconfigure] to sync an uploader to your config. ## Nightscout API -The Nightscout API enables direct access to your DData without the need for direct Mongo access. +The Nightscout API enables direct access to your data without the need for Mongo access. You can find CGM data in `/api/v1/entries`, Care Portal Treatments in `/api/v1/treatments`, and Treatment Profiles in `/api/v1/profile`. The server status and settings are available from `/api/v1/status.json`. @@ -206,7 +222,7 @@ Once you've installed Nightscout, you can access API documentation by loading `/ #### Example Queries -(replace `http://localhost:1337` with your base url, YOUR-SITE) +(replace `http://localhost:1337` with your own URL) * 100's: `http://localhost:1337/api/v1/entries.json?find[sgv]=100` * Count of 100's in a month: `http://localhost:1337/api/v1/count/entries/where?find[dateString][$gte]=2016-09&find[dateString][$lte]=2016-10&find[sgv]=100` @@ -223,15 +239,16 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or ### Required - * `MONGO_CONNECTION` - Your mongo uri, for example: `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout` - * `DISPLAY_UNITS` (`mg/dl`) - Choices: `mg/dl` and `mmol`. Setting to `mmol` puts the entire server into `mmol` mode by default, no further settings needed. - * `BASE_URL` - Used for building links to your sites api, ie pushover callbacks, usually the URL of your Nightscout site you may want https instead of http + * `MONGODB_URI` - The connection string for your Mongo database. Something like `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout`. + * `API_SECRET` - A secret passphrase that must be at least 12 characters long. + * `MONGODB_COLLECTION` (`entries`) - The Mongo collection where CGM entries are stored. + * `DISPLAY_UNITS` (`mg/dl`) - Options are `mg/dl` or `mmol/L` (or just `mmol`). Setting to `mmol/L` puts the entire server into `mmol/L` mode by default, no further settings needed. -### Features/Labs +### Features * `ENABLE` - Used to enable optional features, expects a space delimited list, such as: `careportal rawbg iob`, see [plugins](#plugins) below * `DISABLE` - Used to disable default features, expects a space delimited list, such as: `direction upbat`, see [plugins](#plugins) below - * `API_SECRET` - A secret passphrase that must be at least 12 characters long, required to enable `POST` and `PUT`; also required for the Care Portal + * `BASE_URL` - Used for building links to your site's API, i.e. Pushover callbacks, usually the URL of your Nightscout site. * `AUTH_DEFAULT_ROLES` (`readable`) - possible values `readable`, `denied`, or any valid role name. When `readable`, anyone can view Nightscout without a token. Setting it to `denied` will require a token from every visit, using `status-only` will enable api-secret based login. @@ -240,13 +257,13 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or ### Alarms - These alarm setting effect all delivery methods (browser, pushover, maker, etc), some settings can be overridden per client (web browser) + These alarm setting affect all delivery methods (browser, Pushover, IFTTT, etc.). Values and settings entered here will be the defaults for new browser views, but will be overridden if different choices are made in the settings UI. * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's - * `BG_HIGH` (`260`) - must be set using mg/dl units; the high BG outside the target range that is considered urgent - * `BG_TARGET_TOP` (`180`) - must be set using mg/dl units; the top of the target range, also used to draw the line on the chart - * `BG_TARGET_BOTTOM` (`80`) - must be set using mg/dl units; the bottom of the target range, also used to draw the line on the chart - * `BG_LOW` (`55`) - must be set using mg/dl units; the low BG outside the target range that is considered urgent + * `BG_HIGH` (`260`) - the high BG outside the target range that is considered urgent (interprets units based on DISPLAY_UNITS setting) + * `BG_TARGET_TOP` (`180`) - the top of the target range, also used to draw the line on the chart (interprets units based on DISPLAY_UNITS setting) + * `BG_TARGET_BOTTOM` (`80`) - the bottom of the target range, also used to draw the line on the chart (interprets units based on DISPLAY_UNITS setting) + * `BG_LOW` (`55`) - the low BG outside the target range that is considered urgent (interprets units based on DISPLAY_UNITS setting) * `ALARM_URGENT_HIGH` (`on`) - possible values `on` or `off` * `ALARM_URGENT_HIGH_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent high alarms, space separated for options in browser, first used for pushover * `ALARM_HIGH` (`on`) - possible values `on` or `off` @@ -258,10 +275,8 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `ALARM_URGENT_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover * `ALARM_WARN_MINS` (`30 60 90 120`) - Number of minutes to snooze warning alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover - ### Core - * `MONGO_COLLECTION` (`entries`) - The collection used to store SGV, MBG, and CAL records from your CGM device * `MONGO_TREATMENTS_COLLECTION` (`treatments`) -The collection used to store treatments entered in the Care Portal, see the `ENABLE` env var above * `MONGO_DEVICESTATUS_COLLECTION`(`devicestatus`) - The collection used to store device status information such as uploader battery * `MONGO_PROFILE_COLLECTION`(`profile`) - The collection used to store your profiles @@ -276,13 +291,13 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `DEBUG_MINIFY` (`true`) - Debug option, setting to `false` will disable bundle minification to help tracking down error and speed up development * `DE_NORMALIZE_DATES`(`true`) - The Nightscout REST API normalizes all entered dates to UTC zone. Some Nightscout clients have broken date deserialization logic and expect to received back dates in zoned formats. Setting this variable to `true` causes the REST API to serialize dates sent to Nightscout in zoned format back to zoned format when served to clients over REST. - ### Predefined values for your browser settings (optional) + * `TIME_FORMAT` (`12`)- possible values `12` or `24` * `NIGHT_MODE` (`off`) - possible values `on` or `off` * `SHOW_RAWBG` (`never`) - possible values `always`, `never` or `noise` - * `CUSTOM_TITLE` (`Nightscout`) - Usually name of T1 - * `THEME` (`default`) - possible values `default`, `colors`, or `colorblindfriendly` + * `CUSTOM_TITLE` (`Nightscout`) - Title for the main view + * `THEME` (`colors`) - possible values `default`, `colors`, or `colorblindfriendly` * `ALARM_TIMEAGO_WARN` (`on`) - possible values `on` or `off` * `ALARM_TIMEAGO_WARN_MINS` (`15`) - minutes since the last reading to trigger a warning * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` @@ -290,12 +305,13 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `SHOW_PLUGINS` - enabled plugins that should have their visualizations shown, defaults to all enabled * `SHOW_FORECAST` (`ar2`) - plugin forecasts that should be shown by default, supports space delimited values such as `"ar2 openaps"` * `LANGUAGE` (`en`) - language of Nightscout. If not available english is used - * Currently supported language codes are: bg (Български), cs (Čeština), de (Deutsch), dk (Dansk), el (Ελληνικά), en (English), es (Español), fi (Suomi), fr (Français), he (עברית), hr (Hrvatski), it (Italiano), ko (한국어), nb (Norsk (Bokmål)), nl (Nederlands), pl (Polski), pt (Português (Brasil)), ro (Română), ru (Русский), sk (Slovenčina), sv (Svenska), zh_cn (中文(简体)), zh_tw (中文(繁體)) + * Currently supported language codes are: bg (Български), cs (Čeština), de (Deutsch), dk (Dansk), el (Ελληνικά), en (English), es (Español), fi (Suomi), fr (Français), he (עברית), hr (Hrvatski), it (Italiano), ko (한국어), nb (Norsk (Bokmål)), nl (Nederlands), pl (Polski), pt (Português (Brasil)), ro (Română), ru (Русский), sk (Slovenčina), sv (Svenska), tr (Turkish), zh_cn (中文(简体)), zh_tw (中文(繁體)) * `SCALE_Y` (`log`) - The type of scaling used for the Y axis of the charts system wide. * The default `log` (logarithmic) option will let you see more detail towards the lower range, while still showing the full CGM range. - * The `linear` option has equidistant tick marks, the range used is dynamic so that space at the top of chart isn't wasted. + * The `linear` option has equidistant tick marks; the range used is dynamic so that space at the top of chart isn't wasted. * The `log-dynamic` is similar to the default `log` options, but uses the same dynamic range and the `linear` scale. - * `EDIT_MODE` (`on`) - possible values `on` or `off`. Enable or disable icon allowing enter treatments edit mode + * `EDIT_MODE` (`on`) - possible values `on` or `off`. Enables the icon allowing for editing of treatments in the main view. + * `BOLUS_RENDER_OVER` (1) - U value over which the bolus values are rendered on the chart if the 'x U and Over' option is selected. This value can be an integer or a float, e.g. 0.3, 1.5, 2, etc... ### Predefined values for your server settings (optional) * `INSECURE_USE_HTTP` (`false`) - Redirect unsafe http traffic to https. Possible values `false`, or `true`. Your site redirects to `https` by default. If you don't want that from Nightscout, but want to implement that with a Nginx or Apache proxy, set `INSECURE_USE_HTTP` to `true`. Note: This will allow (unsafe) http traffic to your Nightscout instance and is not recommended. @@ -304,14 +320,37 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `SECURE_HSTS_HEADER_PRELOAD` (`false`) - ask for preload in browsers for HSTS. Possible values `false`, or `true`. * `SECURE_CSP` (`false`) - Add Content Security Policy headers. Possible values `false`, or `true`. * `SECURE_CSP_REPORT_ONLY` (`false`) - If set to `true` allows to experiment with policies by monitoring (but not enforcing) their effects. Possible values `false`, or `true`. - + ### Views - There are a few alternate web views available from the main menu that display a simplified BG stream. (If you launch one of these in a fullscreen view in iOS, you can use a left-to-right swipe gesture to exit the view.) + Nightscout allows to create custom, simplified views using a predefined set of elements. This option is available under `[+]` link in the main menu. + + List of available items: + * `SGV` - Sensor Glucose Value + * `SGV age` - time since the last SGV read + * `SGV delta` - change of SGV in the last 5 minutes + * `Trend arrow` - icon of the SG trend + * `Time` - current time + * `Line break` - invisible item that will move following items to the next line (by default all are showing on the same level) + + All visible items have `Size` property which allows to customize the view even more. Also, all items may appear multiple times on the view. + + Apart from adding items, it is possible to customize other aspects of the views, like selecting `Color` or `Black` background. The first one will indicate current BG threshold (green = in range; blue = below range; yellow = above range; red = urgent below/above). + `Show SGV age` option will make `SGV age` item appear `Always` or only if the predefined threshold is reached: `Only after threshold`. Breaching `SGV age threshold` will also make `Color` background turn grey and strike through `SGV`. + `Clock view configurator` will generate an URL (available under `Open my clock view!` link) that could be bookmarked. + + There are a few default views available from the main menu: * `Clock` - Shows current BG, trend arrow, and time of day. Grey text on a black background. - * `Color` - Shows current BG and trend arrow. White text on a background that changes color to indicate current BG threshold (green = in range; blue = below range; yellow = above range; red = urgent below/above). + * `Color` - Shows current BG and trend arrow. White text on a color background. * `Simple` - Shows current BG. Grey text on a black background. - * Optional configuration: set `SHOW_CLOCK_CLOSEBUTTON` to `false` to never show the small X button in clock views. For bookmarking a clock view without the close box but have it appear when navigating to a clock from the Nightscout menu, don't change the settng, but remove the `showClockClosebutton=true` parameter from the clock view URL. + + If you launch one of these views in a fullscreen view in iOS, you can use a left-to-right swipe gesture to exit the view. + +### Split View + + Some users will need easy access to multiple Nightscout views at the same time. We have a special view for this case, accessed on /split path on your Nightscout URL. The view supports any number of sites between 1 to 8 way split, where the content for the screen can be loaded from multiple Nightscout instances. Note you still need to host separate instances for each Nightscout being monitored including the one that hosts the split view page - these variables only add the ability to load multiple views into one browser page. To set the URLs from which the content is loaded, set: + * `FRAME_URL_1` - URL where content is loaded, for the first view (increment the number up to 8 to get more views) + * `FRAME_NAME_1` - Name for the first split view portion of the screen (increment the number to name more views) ### Plugins @@ -321,7 +360,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or #### Default Plugins - These can be disabled by setting the `DISABLE` env var, for example `DISABLE="direction upbat"` + These can be disabled by adding them to the `DISABLE` variable, for example `DISABLE="direction upbat"` ##### `delta` (BG Delta) Calculates and displays the change between the last 2 BG values. @@ -343,7 +382,6 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm - ##### `devicestatus` (Device Status) Used by `upbat` and other plugins to display device status info. Supports the `DEVICESTATUS_ADVANCED="true"` [extended setting](#extended-settings) to send all device statuses to the client for retrospective use and to support other plugins. @@ -437,14 +475,15 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `BASAL_RENDER` (`none`) - Possible values are `none`, `default`, or `icicle` (inverted) ##### `bridge` (Share2Nightscout bridge) - Glucose reading directly from the Share service, uses these extended settings: - * `BRIDGE_USER_NAME` - Your user name for the Share service. + Glucose reading directly from the Dexcom Share service, uses these extended settings: + * `BRIDGE_USER_NAME` - Your username for the Share service. * `BRIDGE_PASSWORD` - Your password for the Share service. - * `BRIDGE_INTERVAL` (`150000` *2.5 minutes*) - The time to wait between each update. - * `BRIDGE_MAX_COUNT` (`1`) - The maximum number of records to fetch per update. + * `BRIDGE_INTERVAL` (`150000` *2.5 minutes*) - The time (in milliseconds) to wait between each update. + * `BRIDGE_MAX_COUNT` (`1`) - The number of records to attempt to fetch per update. * `BRIDGE_FIRST_FETCH_COUNT` (`3`) - Changes max count during the very first update only. * `BRIDGE_MAX_FAILURES` (`3`) - How many failures before giving up. - * `BRIDGE_MINUTES` (`1400`) - The time window to search for new data per update (default is one day in minutes). + * `BRIDGE_MINUTES` (`1400`) - The time window to search for new data per update (the default value is one day in minutes). + * `BRIDGE_SERVER` (``) - The default blank value is used to fetch data from Dexcom servers in the US. Set to (`EU`) to fetch from European servers instead. ##### `mmconnect` (MiniMed Connect bridge) Transfer real-time MiniMed Connect data from the Medtronic CareLink server into Nightscout ([read more](https://github.com/mddub/minimed-connect-to-nightscout)) @@ -481,14 +520,13 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `OPENAPS_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceed before an urgent alarm is triggered * `OPENAPS_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display by default. Any of the following fields: `status-symbol`, `status-label`, `iob`, `meal-assist`, `freq`, and `rssi` * `OPENAPS_RETRO_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display in retro mode. Any of the above fields. - * `OPENAPS_PRED_IOB_COLOR` (`#1e88e5`) - The color to use for IOB prediction lines. Colors can be in either `#RRGGBB` or `#RRGGBBAA` format. - * `OPENAPS_PRED_COB_COLOR` (`#FB8C00FF`) - The color to use for COB prediction lines. Same format as above. - * `OPENAPS_PRED_ACOB_COLOR` (`#FB8C0080`) - The color to use for ACOB prediction lines. Same format as above. + * `OPENAPS_PRED_IOB_COLOR` (`#1e88e5`) - The color to use for IOB prediction lines. Colors can be in `#RRGGBB` format, but [other CSS color units](https://www.w3.org/TR/css-color-3/#colorunits) may be used as well. + * `OPENAPS_PRED_COB_COLOR` (`#FB8C00`) - The color to use for COB prediction lines. Same format as above. + * `OPENAPS_PRED_ACOB_COLOR` (`#FB8C00`) - The color to use for ACOB prediction lines. Same format as above. * `OPENAPS_PRED_ZT_COLOR` (`#00d2d2`) - The color to use for ZT prediction lines. Same format as above. * `OPENAPS_PRED_UAM_COLOR` (`#c9bd60`) - The color to use for UAM prediction lines. Same format as above. * `OPENAPS_COLOR_PREDICTION_LINES` (`true`) - Enables / disables the colored lines vs the classic purple color. - Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker). ##### `loop` (Loop) @@ -499,20 +537,29 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `LOOP_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceeded before an urgent alarm is triggered * Add `loop` to `SHOW_FORECAST` to show forecasted BG. +For remote overrides, the following extended settings must be configured: + * `LOOP_APNS_KEY` - Apple Push Notifications service (APNs) Key, created in the Apple Developer website. + * `LOOP_APNS_KEY_ID` - The Key ID for the above key. + * `LOOP_DEVELOPER_TEAM_ID` - Your Apple developer team ID. + * `LOOP_PUSH_SERVER_ENVIRONMENT` - (optional) Set this to `production` if you are using a provisioning profile that specifies production aps-environment, such as when distributing builds via TestFlight. + ##### `override` (Override Mode) Additional monitoring for DIY automated insulin delivery systems to display real-time overrides such as Eating Soon or Exercise Mode: * Requires `DEVICESTATUS_ADVANCED="true"` to be set -##### `xdrip-js` (xDrip-js) +##### `xdripjs` (xDrip-js) Integrated xDrip-js monitoring, uses these extended settings: * Requires `DEVICESTATUS_ADVANCED="true"` to be set - * `XDRIP-JS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when CGM state is not OK or battery voltages fall below threshold. - * `XDRIP-JS_STATE_NOTIFY_INTRVL` (`0.5`) - Set to number of hours between CGM state notifications - * `XDRIP-JS_WARN_BAT_V` (`300`) - The voltage of either transmitter battery, a warning will be triggered when dropping below this threshold. + * `XDRIPJS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when CGM state is not OK or battery voltages fall below threshold. + * `XDRIPJS_STATE_NOTIFY_INTRVL` (`0.5`) - Set to number of hours between CGM state notifications + * `XDRIPJS_WARN_BAT_V` (`300`) - The voltage of either transmitter battery, a warning will be triggered when dropping below this threshold. ##### `alexa` (Amazon Alexa) Integration with Amazon Alexa, [detailed setup instructions](docs/plugins/alexa-plugin.md) +##### `googlehome` (Google Home/DialogFLow) + Integration with Google Home (via DialogFlow), [detailed setup instructions](docs/plugins/googlehome-plugin.md) + ##### `speech` (Speech) Speech synthesis plugin. When enabled, speaks out the blood glucose values, IOB and alarms. Note you have to set the LANGUAGE setting on the server to get all translated alarms. @@ -520,12 +567,34 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or Enabled [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) so other websites can make request to your Nightscout site, uses these extended settings: * `CORS_ALLOW_ORIGIN` (`*`) - The list of sites that are allow to make requests +##### `dbsize` (Database Size) + Show size of Nightscout Database, as a percentage of declared available space or in MiB. + + Many deployments of Nightscout use free tier of MongoDB Atlas on Heroku, which is limited in size. After some time, as volume of stored data grows, it may happen that this limit is reached and system is unable to store new data. This plugin provides pill that indicates size of Database and shows (when configured) alarms regarding reaching space limit. + + **IMPORTANT:** This plugin can only check how much space database already takes, _but cannot infer_ max size available on server for it. To have correct alarms and realistic percentage, `DBSIZE_MAX` need to be properly set - according to your mongoDB hosting configuration. + + **NOTE:** This plugin rely on db.stats() for reporting _logical_ size of database, which may be different than _physical_ size of database on server. It may work for free tier of MongoDB on Atlas, since it calculate quota according to logical size too, but may fail for other hostings or self-hosted database with quota based on physical size. + + **NOTE:** MongoDB Atlas quota is for **all** databases in cluster, while each instance will get only size of **its own database only**. It is ok when you only have **one** database in cluster (most common scenario) but will not work for multiple parallel databases. In such case, spliting known quota equally beetween databases and setting `DBSIZE_MAX` to that fraction may help, but wont be precise. + + All sizes are expressed as integers, in _Mebibytes_ `1 MiB == 1024 KiB == 1024*1024 B` + + * `DBSIZE_MAX` (`496`) - Maximal allowed size of database on your mongoDB server, in MiB. You need to adjust that value to match your database hosting limits - default value is for standard Heroku mongoDB free tier. + * `DBSIZE_WARN_PERCENTAGE` (`60`) - Threshold to show first warning about database size. When database reach this percentage of `DBSIZE_MAX` size - pill will show size in yellow. + * `DBSIZE_URGENT_PERCENTAGE` (`75`) - Threshold to show urgent warning about database size. When database reach this percentage of `DBSIZE_MAX` size, it is urgent to do backup and clean up old data. At this percentage info pill turns red. + * `DBSIZE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications about database size. + * `DBSIZE_IN_MIB` (`false`) - Set to `true` to display size of database in MiB-s instead of default percentage. + + This plugin should be enabled by default, if needed can be diasabled by adding `dbsize` to the list of disabled plugins, for example: `DISABLE="dbsize"`. + #### Extended Settings Some plugins support additional configuration using extra environment variables. These are prefixed with the name of the plugin and a `_`. For example setting `MYPLUGIN_EXAMPLE_VALUE=1234` would make `extendedSettings.exampleValue` available to the `MYPLUGIN` plugin. Plugins only have access to their own extended settings, all the extended settings of client plugins will be sent to the browser. * `DEVICESTATUS_ADVANCED` (`true`) - Defaults to true. Users who only have a single device uploading data to Nightscout can set this to false to reduce the data use of the site. + * `DEVICESTATUS_DAYS` (`1`) - Defaults to 1, can optionally be set to 2. Users can use this to show 48 hours of device status data for in retro mode, rather than the default 24 hours. Setting this value to 2 will roughly double the bandwidth usage of nightscout, so users with a data cap may not want to update this setting. #### Pushover In addition to the normal web based alarms, there is also support for [Pushover](https://pushover.net/) based alarms and notifications. @@ -545,13 +614,13 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or * `PUSHOVER_ANNOUNCEMENT_KEY` - An optional Pushover user/group key, will be used for system wide user generated announcements. If not defined this will fallback to `PUSHOVER_USER_KEY` or `PUSHOVER_ALARM_KEY`. This also support a space delimited list of keys. To disable Announcement pushes set this to `off`. * `BASE_URL` - Used for pushover callbacks, usually the URL of your Nightscout site, use https when possible. * `API_SECRET` - Used for signing the pushover callback request for acknowledgments. - + If you never want to get info level notifications (treatments) use `PUSHOVER_USER_KEY="off"` If you never want to get an alarm via pushover use `PUSHOVER_ALARM_KEY="off"` If you never want to get an announcement via pushover use `PUSHOVER_ANNOUNCEMENT_KEY="off"` - + If only `PUSHOVER_USER_KEY` is set it will be used for all info notifications, alarms, and announcements - + For testing/development try [localtunnel](http://localtunnel.me/). #### IFTTT Maker @@ -581,7 +650,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or Treatment Profile Fields: * `timezone` (Time Zone) - time zone local to the patient. *Should be set.* - * `units` (Profile Units) - blood glucose units used in the profile, either "mgdl" or "mmol" + * `units` (Profile Units) - blood glucose units used in the profile, either "mg/dl" or "mmol" * `dia` (Insulin duration) - value should be the duration of insulin action to use in calculating how much insulin is left active. Defaults to 3 hours. * `carbs_hr` (Carbs per Hour) - The number of carbs that are processed per hour, for more information see [#DIYPS](http://diyps.org/2014/05/29/determining-your-carbohydrate-absorption-rate-diyps-lessons-learned/). * `carbratio` (Carb Ratio) - grams per unit of insulin. @@ -632,6 +701,12 @@ Feel free to [post an issue][issues], but read the [wiki][wiki] first. [issues]: https://github.com/nightscout/cgm-remote-monitor/issues [wiki]: https://github.com/nightscout/cgm-remote-monitor/wiki +### Browser testing suite provided by +[![BrowserStack][browserstack-img]][browserstack-url] + +[browserstack-img]: /static/images/browserstack-logo.png +[browserstack-url]: https://www.browserstack.com/ + License --------------- @@ -641,16 +716,16 @@ License Copyright (C) 2017 Nightscout contributors. See the COPYRIGHT file at the root directory of this distribution and at https://github.com/nightscout/cgm-remote-monitor/blob/master/COPYRIGHT - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . diff --git a/app.js b/app.js index 5b6f49b9708..c6528819bb9 100644 --- a/app.js +++ b/app.js @@ -7,6 +7,7 @@ const bodyParser = require('body-parser'); const path = require('path'); const fs = require('fs'); +const ejs = require('ejs'); function create (env, ctx) { var app = express(); @@ -25,6 +26,9 @@ function create (env, ctx) { } }); if (secureHstsHeader) { // Add HSTS (HTTP Strict Transport Security) header + + const enableCSP = env.secureCsp ? true : false; + console.info('Enabled SECURE_HSTS_HEADER (HTTP Strict Transport Security)'); const helmet = require('helmet'); var includeSubDomainsValue = env.secureHstsHeaderIncludeSubdomains; @@ -36,39 +40,52 @@ function create (env, ctx) { , preload: preloadValue } , frameguard: false + , contentSecurityPolicy: enableCSP })); - if (env.secureCsp) { + + if (enableCSP) { var secureCspReportOnly = env.secureCspReportOnly; if (secureCspReportOnly) { console.info('Enabled SECURE_CSP (Content Security Policy header). Not enforcing. Report only.'); } else { console.info('Enabled SECURE_CSP (Content Security Policy header). Enforcing.'); } + + let frameAncestors = ["'self'"]; + + for (let i = 0; i <= 8; i++) { + let u = env.settings['frameUrl' + i]; + if (u) { + frameAncestors.push(u); + } + } + app.use(helmet.contentSecurityPolicy({ //TODO make NS work without 'unsafe-inline' directives: { defaultSrc: ["'self'"] - , styleSrc: ["'self'", 'https://fonts.googleapis.com/', "'unsafe-inline'"] + , styleSrc: ["'self'", 'https://fonts.googleapis.com/', 'https://fonts.gstatic.com/', "'unsafe-inline'"] , scriptSrc: ["'self'", "'unsafe-inline'"] - , fontSrc: ["'self'", 'https://fonts.gstatic.com/', 'data:'] + , fontSrc: ["'self'", 'https://fonts.googleapis.com/', 'https://fonts.gstatic.com/', 'data:'] , imgSrc: ["'self'", 'data:'] - , objectSrc: ["'none'"], // Restricts , , and elements - reportUri: '/report-violation' - , frameAncestors: ["'none'"], // Clickjacking protection, using frame-ancestors - baseUri: ["'none'"], // Restricts use of the tag - formAction: ["'self'"], // Restricts where
contents may be submitted + , objectSrc: ["'none'"] // Restricts , , and elements + , reportUri: '/report-violation' + , baseUri: ["'none'"] // Restricts use of the tag + , formAction: ["'self'"] // Restricts where contents may be submitted + , connectSrc: ["'self'", "ws:", "wss:", 'https://fonts.googleapis.com/', 'https://fonts.gstatic.com/'] + , frameSrc: ["'self'"] + , frameAncestors: frameAncestors } , reportOnly: secureCspReportOnly })); app.use(helmet.referrerPolicy({ policy: 'no-referrer' })); - app.use(helmet.featurePolicy({ features: { payment: ["'none'"], } })); app.use(bodyParser.json({ type: ['json', 'application/csp-report'] })); app.post('/report-violation', (req, res) => { if (req.body) { - console.log('CSP Violation: ', req.body) + console.log('CSP Violation: ', req.body); } else { - console.log('CSP Violation: No data received!') + console.log('CSP Violation: No data received!'); } - res.status(204).end() + res.status(204).end(); }) } } @@ -79,21 +96,61 @@ function create (env, ctx) { app.set('view engine', 'ejs'); // this allows you to render .html files as templates in addition to .ejs app.engine('html', require('ejs').renderFile); - app.engine('appcache', require('ejs').renderFile); app.set("views", path.join(__dirname, "views/")); let cacheBuster = 'developmentMode'; + let lastModified = new Date(); + let busterPath = '/tmp/cacheBusterToken'; + if (process.env.NODE_ENV !== 'development') { - if (fs.existsSync(process.cwd() + '/tmp/cacheBusterToken')) { - cacheBuster = fs.readFileSync(process.cwd() + '/tmp/cacheBusterToken').toString().trim(); - } else { - cacheBuster = fs.readFileSync(__dirname + '/tmp/cacheBusterToken').toString().trim(); - } + busterPath = process.cwd() + busterPath; + } else { + busterPath = __dirname + busterPath; + } + + if (fs.existsSync(busterPath)) { + cacheBuster = fs.readFileSync(busterPath).toString().trim(); + var stats = fs.statSync(busterPath); + lastModified = stats.mtime; } app.locals.cachebuster = cacheBuster; + app.get("/robots.txt", (req, res) => { + res.setHeader('Content-Type', 'text/plain'); + res.send(['User-agent: *','Disallow: /'].join('\n')); + }); + + app.get("/sw.js", (req, res) => { + res.setHeader('Content-Type', 'application/javascript'); + if (process.env.NODE_ENV !== 'development') { + res.setHeader('Last-Modified', lastModified.toUTCString()); + } + res.send(ejs.render(fs.readFileSync( + require.resolve(`${__dirname}/views/service-worker.js`), + { encoding: 'utf-8' }), + { locals: app.locals} + )); + }); + + // Allow static resources to be cached for week + var maxAge = 7 * 24 * 60 * 60 * 1000; + + if (process.env.NODE_ENV === 'development') { + maxAge = 1; + console.log('Development environment detected, setting static file cache age to 1 second'); + } + + var staticFiles = express.static(env.static_files, { + maxAge + }); + + // serve the static content + app.use(staticFiles); + if (ctx.bootErrors && ctx.bootErrors.length > 0) { - app.get('*', require('./lib/server/booterror')(ctx)); + const bootErrorView = require('./lib/server/booterror')(env, ctx); + bootErrorView.setLocals(app.locals); + app.get('*', bootErrorView); return app; } @@ -117,8 +174,11 @@ function create (env, ctx) { /////////////////////////////////////////////////// // api and json object variables /////////////////////////////////////////////////// + const apiRoot = require('./lib/api/root')(env, ctx); var api = require('./lib/api/')(env, ctx); + var api3 = require('./lib/api3/')(env, ctx); var ddata = require('./lib/data/endpoints')(env, ctx); + var notificationsV2 = require('./lib/api/notifications-v2')(app, ctx); app.use(compression({ filter: function shouldCompress (req, res) { @@ -128,49 +188,77 @@ function create (env, ctx) { } })); - const clockviews = require('./lib/server/clocks.js')(env, ctx); - clockviews.setLocals(app.locals); - - app.use("/clock", clockviews); - - app.get("/", (req, res) => { - res.render("index.html", { - locals: app.locals - }); - }); - var appPages = { - "/clock-color.html": "clock-color.html" - , "/admin": "adminindex.html" - , "/profile": "profileindex.html" - , "/food": "foodindex.html" - , "/bgclock.html": "bgclock.html" - , "/report": "reportindex.html" - , "/translations": "translationsindex.html" - , "/clock.html": "clock.html" + "/": { + file: "index.html" + , type: "index" + } + , "/admin": { + file: "adminindex.html" + , title: 'Admin Tools' + , type: 'admin' + } + , "/food": { + file: "foodindex.html" + , title: 'Food Editor' + , type: 'food' + } + , "/profile": { + file: "profileindex.html" + , title: 'Profile Editor' + , type: 'profile' + } + , "/report": { + file: "reportindex.html" + , title: 'Nightscout reporting' + , type: 'report' + } + , "/translations": { + file: "translationsindex.html" + , title: 'Nightscout translations' + , type: 'translations' + } + , "/split": { + file: "frame.html" + , title: '8-user view' + , type: 'index' + } }; Object.keys(appPages).forEach(function(page) { app.get(page, (req, res) => { - res.render(appPages[page], { - locals: app.locals + res.render(appPages[page].file, { + locals: app.locals, + title: appPages[page].title ? appPages[page].title : '', + type: appPages[page].type ? appPages[page].type : '', + settings: env.settings }); }); }); - app.get("/appcache/*", (req, res) => { - res.render("nightscout.appcache", { - locals: app.locals - }); - }); + const clockviews = require('./lib/server/clocks.js')(env, ctx); + clockviews.setLocals(app.locals); + + app.use("/clock", clockviews); + + app.use('/api', bodyParser({ + limit: 1048576 * 50 + }), apiRoot); app.use('/api/v1', bodyParser({ limit: 1048576 * 50 }), api); + app.use('/api/v2', bodyParser({ + limit: 1048576 * 50 + }), api); + app.use('/api/v2/properties', ctx.properties); app.use('/api/v2/authorization', ctx.authorization.endpoints); app.use('/api/v2/ddata', ddata); + app.use('/api/v2/notifications', notificationsV2); + + app.use('/api/v3', api3); // pebble data app.get('/pebble', ctx.pebble); @@ -185,38 +273,6 @@ function create (env, ctx) { res.sendFile(__dirname + '/swagger.yaml'); }); - if (env.settings.isEnabled('dumps')) { - var heapdump = require('heapdump'); - app.get('/api/v2/dumps/start', function(req, res) { - var path = new Date().toISOString() + '.heapsnapshot'; - path = path.replace(/:/g, '-'); - console.info('writing dump to', path); - heapdump.writeSnapshot(path); - res.send('wrote dump to ' + path); - }); - } - - // app.get('/package.json', software); - - // Allow static resources to be cached for week - var maxAge = 7 * 24 * 60 * 60 * 1000; - - if (process.env.NODE_ENV === 'development') { - maxAge = 1; - console.log('Development environment detected, setting static file cache age to 1 second'); - - app.get('/nightscout.appcache', function(req, res) { - res.sendStatus(404); - }); - } - - var staticFiles = express.static(env.static_files, { - maxAge - }); - - // serve the static content - app.use(staticFiles); - // API docs const swaggerUi = require('swagger-ui-express'); @@ -224,7 +280,7 @@ function create (env, ctx) { app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); - app.use('/swagger-ui-dist', (req, res, next) => { + app.use('/swagger-ui-dist', (req, res) => { res.redirect(307, '/api-docs'); }); @@ -233,10 +289,13 @@ function create (env, ctx) { app.locals.bundle = '/bundle'; + app.locals.mode = 'production'; + if (process.env.NODE_ENV === 'development') { console.log('Development mode'); + app.locals.mode = 'development'; app.locals.bundle = '/devbundle'; const webpack = require('webpack'); diff --git a/app.json b/app.json index 8fea5103f88..717e783dea3 100644 --- a/app.json +++ b/app.json @@ -53,25 +53,30 @@ "required": true }, "BG_HIGH": { - "description": "Urgent High BG threshold, triggers the ALARM_URGENT_HIGH alarm. Must be set in mg/dL, even if you use mmol/L (multiply a mmol/L value by 18 to change it to mg/dl).", + "description": "Urgent High BG threshold, triggers the ALARM_URGENT_HIGH alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", "value": "260", "required": false }, "BG_LOW": { - "description": "Urgent Low BG threshold, triggers the ALARM_URGENT_LOW alarm. Must be set in mg/dL, even if you use mmol/L (multiply a mmol/L value by 18 to change it to mg/dl).", + "description": "Urgent Low BG threshold, triggers the ALARM_URGENT_LOW alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", "value": "55", "required": false }, "BG_TARGET_BOTTOM": { - "description": "Low BG threshold, triggers the ALARM_LOW alarm. Must be set in mg/dL, even if you use mmol/L (multiply a mmol/L value by 18 to change it to mg/dl).", + "description": "Low BG threshold, triggers the ALARM_LOW alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", "value": "80", "required": false }, "BG_TARGET_TOP": { - "description": "High BG threshold, triggers the ALARM_HIGH alarm. Must be set in mg/dL, even if you use mmol/L (multiply a mmol/L value by 18 to change it to mg/dl).", + "description": "High BG threshold, triggers the ALARM_HIGH alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", "value": "180", "required": false }, + "BOLUS_RENDER_OVER": { + "description": "U value over which the bolus values are rendered on the chart if the 'x U and Over' option is selected.", + "value": "1", + "required": false + }, "BRIDGE_PASSWORD": { "description": "Your Dexcom account password, to receive CGM data from the Dexcom Share service. Also make sure to include 'bridge' in your ENABLE line.", "value": "", @@ -93,13 +98,13 @@ "required": false }, "DISPLAY_UNITS": { - "description": "Preferred BG units for the site:'mg/dl' or 'mmol'. (Note that it is *not* 'mmol/L')", + "description": "Preferred BG units for the site: 'mg/dl' or 'mmol/L' (or just 'mmol').", "value": "mg/dl", "required": true }, "ENABLE": { "description": "Plugins to enable for your site. Must be a space-delimited, lower-case list. Include the word 'bridge' here if you are receiving data from the Dexcom Share service. Include 'mmconnect' if you are bridging from the MiniMed CareLink service.", - "value": "careportal basal", + "value": "careportal basal dbsize", "required": false }, "MMCONNECT_USER_NAME": { @@ -117,6 +122,11 @@ "value": "US", "required": false }, + "MONGODB_URI": { + "description": "The MongoDB Connection String to connect to your MongoDB cluster", + "value": "", + "required": true + }, "MONGO_COLLECTION": { "description": "The Mongo collection where CGM data is stored.", "value": "entries", @@ -129,7 +139,7 @@ }, "SHOW_PLUGINS": { "description": "Default setting for whether or not these plugins are checked (active) by default, not merely enabled. Include plugins here as in the ENABLE line; space-separated and lower-case.", - "value": "careportal", + "value": "careportal dbsize", "required": false }, "SHOW_RAWBG": { @@ -149,7 +159,6 @@ } }, "addons": [ - "mongolab:sandbox", "papertrail" ] } diff --git a/assets/fonts/Nightscout Plugin Icons.json b/assets/fonts/Nightscout Plugin Icons.json new file mode 100644 index 00000000000..65874c15679 --- /dev/null +++ b/assets/fonts/Nightscout Plugin Icons.json @@ -0,0 +1,87 @@ +{ + "metadata": { + "name": "Nightscout Plugin Icons", + "lastOpened": 0, + "created": 1580075608590 + }, + "iconSets": [ + { + "selection": [ + { + "order": 2, + "id": 0, + "name": "database", + "prevSize": 32, + "code": 59649, + "tempChar": "" + } + ], + "id": 2, + "metadata": { + "name": "Plugin Icons", + "importSize": { + "width": 16, + "height": 18 + } + }, + "height": 1024, + "prevSize": 32, + "icons": [ + { + "id": 0, + "paths": [ + "M455.111 0c-251.449 0-455.111 101.831-455.111 227.556s203.662 227.556 455.111 227.556 455.111-101.831 455.111-227.556-203.662-227.556-455.111-227.556zM0 341.333v170.667c0 125.724 203.662 227.556 455.111 227.556s455.111-101.831 455.111-227.556v-170.667c0 125.724-203.662 227.556-455.111 227.556s-455.111-101.831-455.111-227.556zM0 625.778v170.667c0 125.724 203.662 227.556 455.111 227.556s455.111-101.831 455.111-227.556v-170.667c0 125.724-203.662 227.556-455.111 227.556s-455.111-101.831-455.111-227.556z" + ], + "attrs": [ + {} + ], + "width": 910, + "isMulticolor": false, + "isMulticolor2": false, + "grid": 0, + "tags": [ + "plugins" + ] + } + ], + "invisible": false, + "colorThemes": [] + } + ], + "preferences": { + "showGlyphs": true, + "showQuickUse": true, + "showQuickUse2": true, + "showSVGs": true, + "fontPref": { + "prefix": "plugicon-", + "metadata": { + "fontFamily": "pluginicons", + "majorVersion": 1, + "minorVersion": 0 + }, + "metrics": { + "emSize": 1024, + "baseline": 6.25, + "whitespace": 50 + }, + "embed": false, + "showSelector": false, + "showMetrics": false, + "showMetadata": false, + "showVersion": false + }, + "imagePref": { + "prefix": "icon-", + "png": true, + "useClassSelector": true, + "color": 0, + "bgColor": 16777215, + "classSelector": ".icon" + }, + "historySize": 50, + "showCodes": true, + "gridSize": 16 + }, + "uid": -1 +} \ No newline at end of file diff --git a/assets/fonts/README.md b/assets/fonts/README.md new file mode 100644 index 00000000000..b6b98ea4322 --- /dev/null +++ b/assets/fonts/README.md @@ -0,0 +1,29 @@ +How to upgrade icons in icon-fonts on Nightscout +================================================ + +This guide is fol developers regarding how to add new icon to Nightscout. + +Nightscout use icon fonts to render icons. Each icon is glyph (like - letter, or more like emoji character) inside custom made font file. +That way we have nice, vector icons, that are small, scalable, looks good on each platform, and are easy to embed inside CSS. + +To extend existing icon set.: + +1. Prepare minimalist, black & white icon in SVG tool of choice, and optimize it (you can use Inkscape) to be small in size and render good at small sizes. +2. Use https://icomoon.io/app and import accompanied JSON project file (`Nightscout Plugin Icons.json`) +3. Add SVG as new glyph. Remember to take care to set proper character code and CSS name +4. Save new version of JSON project file and store in this folder +5. Generate font, download zip file and unpack it to get `fonts/pluginicons.svg` and `fonts/pluginicons.woff` +6. Update `statc/css/main.css` file + * In section of `@font-face` with `font-family: 'pluginicons'` + * update part after `data:application/font-woff;charset=utf-8;base64,` with Base64-encoded content of just generated `pluginicons.woff` font + * update part after `data:application/font-svg;charset=utf-8;base64,` with Base64-encoded content of just generated `pluginicons.svg` font + * copy/update all entries `.plugicon-****:before { content: "****"; }` from generated font `style.css` into `statc/css/main.css` +7. Do not forget to update `Nightscout Plugin Icons.json` in this repo (´download updated project from icomoon.io) + +Hints +----- + +* You can find many useful online tools to encode file into Base64, like: https://base64.guru/converter/encode/file +* Do not split Base64 output - it should be one LONG line +* Since update process is **manual** and generated fonts & updated CSS sections are **binary** - try to avoid **git merge conflicts** by speaking with other developers if you plan to add new icon +* When in doubt - check `git log` and reach last contributor for guidelines :) diff --git a/azuredeploy.json b/azuredeploy.json index dc89c0ad956..bff5e3c41f7 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -173,7 +173,7 @@ }, "enable": { "type": "string", - "defaultValue": "basal bwp cage careportal iob cob rawbg sage iage treatmentnotify boluscalc profile food" + "defaultValue": "basal bwp cage careportal iob cob rawbg sage iage treatmentnotify boluscalc profile food dbsize" }, "night_mode": { "type": "string", @@ -185,7 +185,7 @@ }, "show_plugins": { "type": "string", - "defaultValue": "careportal" + "defaultValue": "careportal dbsize" }, "show_rawbg": { "type": "string", diff --git a/bundle/bundle.reports.source.js b/bundle/bundle.reports.source.js index c07368543b4..27d67e9fb82 100644 --- a/bundle/bundle.reports.source.js +++ b/bundle/bundle.reports.source.js @@ -1,10 +1,11 @@ import './bundle.source'; window.Nightscout.report_plugins = require('../lib/report_plugins/')(); +window.Nightscout.predictions = require('../lib/report/predictions'); console.info('Nightscout report bundle ready'); // Needed for Hot Module Replacement if(typeof(module.hot) !== 'undefined') { - module.hot.accept() // eslint-disable-line no-undef + module.hot.accept() } diff --git a/bundle/bundle.source.js b/bundle/bundle.source.js index d554744e6e4..db61af947bf 100644 --- a/bundle/bundle.source.js +++ b/bundle/bundle.source.js @@ -32,5 +32,5 @@ console.info('Nightscout bundle ready'); // Needed for Hot Module Replacement if(typeof(module.hot) !== 'undefined') { - module.hot.accept() // eslint-disable-line no-undef + module.hot.accept() } diff --git a/ci.test.env b/ci.test.env new file mode 100644 index 00000000000..f5e240f8381 --- /dev/null +++ b/ci.test.env @@ -0,0 +1,8 @@ +CUSTOMCONNSTR_mongo=mongodb://127.0.0.1:27017/testdb +API_SECRET=abcdefghij123 +HOSTNAME=localhost +INSECURE_USE_HTTP=true +PORT=1337 +NODE_ENV=production +CI=true +CODACY_PROJECT_TOKEN=cff7ab3377d6434a9355fd051dbb4595 \ No newline at end of file diff --git a/docs/plugins/add-virtual-assistant-support-to-plugin.md b/docs/plugins/add-virtual-assistant-support-to-plugin.md new file mode 100644 index 00000000000..60ac1d1957b --- /dev/null +++ b/docs/plugins/add-virtual-assistant-support-to-plugin.md @@ -0,0 +1,62 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Adding Virtual Assistant Support to a Plugin](#adding-virtual-assistant-support-to-a-plugin) + - [Intent Handlers](#intent-handlers) + - [Rollup handlers](#rollup-handlers) + + + +Adding Virtual Assistant Support to a Plugin +========================================= + +To add virtual assistant support to a plugin, the `init` method of the plugin should return an object that contains a `virtAsst` key. Here is an example: + +```javascript +iob.virtAsst = { + intentHandlers: [{ + intent: "MetricNow" + , metrics: ["iob"] + , intentHandler: virtAsstIOBIntentHandler + }] + , rollupHandlers: [{ + rollupGroup: "Status" + , rollupName: "current iob" + , rollupHandler: virtAsstIOBRollupHandler + }] +}; +``` + +There are 2 types of handlers that you can supply: +* Intent handler - Enables you to "teach" the virtual assistant how to respond to a user's question. +* A rollup handler - Enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. + +### Intent Handlers + +A plugin can expose multiple intent handlers (e.g. useful when it can supply multiple kinds of metrics). Each intent handler should be structured as follows: ++ `intent` - This is the intent this handler is built for. Right now, the templates used by both Alexa and Google Home use only the `"MetricNow"` intent (used for getting the present value of the requested metric) ++ `metrics` - An array of metric name(s) the handler will supply. e.g. "What is my `metric`" - iob, bg, cob, etc. Make sure to add the metric name and its synonyms to the list of metrics used by the virtual assistant(s). + - **IMPORTANT NOTE:** There is no protection against overlapping metric names, so PLEASE make sure your metric name is unique! + - Note: Although this value *is* an array, you really should only supply one (unique) value, and then add aliases or synonyms to that value in the list of metrics for the virtual assistant. We keep this value as an array for backwards compatibility. ++ `intenthandler` - This is a callback function that receives 3 arguments: + - `callback` Call this at the end of your function. It requires 2 arguments: + - `title` - Title of the handler. This is the value that will be displayed on the Alexa card (for devices with a screen). The Google Home response doesn't currently display a card, so it doesn't use this value. + - `text` - This is text that the virtual assistant should speak (and show, for devices with a screen). + - `slots` - These are the slots (Alexa) or parameters (Google Home) that the virtual assistant detected (e.g. `pwd` as seen in the templates is a slot/parameter. `metric` is technically a slot, too). + - `sandbox` - This is the Nightscout sandbox that allows access to various functions. + +### Rollup handlers + +A plugin can also expose multiple rollup handlers ++ `rollupGroup` - This is the key that is used to aggregate the responses when the intent is invoked ++ `rollupName` - This is the name of the handler. Primarily used for debugging ++ `rollupHandler` - This is a callback function that receives 3 arguments + - `slots` - These are the values of the slots. Make sure to add these values to the appropriate custom slot + - `sandbox` - This is the nightscout sandbox that allows access to various functions. + - `callback` - + - `error` - This would be an error message + - `response` - A simple object that expects a `results` string and a `priority` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: + ```javascript + callback(null, {results: "Hello world", priority: 1}); + ``` diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index a5dcb886e9c..87117affd46 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -10,12 +10,13 @@ - [Create a new Alexa skill](#create-a-new-alexa-skill) - [Define the interaction model](#define-the-interaction-model) - [Point your skill at your site](#point-your-skill-at-your-site) + - [Do you use Authentication Roles?](#do-you-use-authentication-roles) - [Test your skill out with the test tool](#test-your-skill-out-with-the-test-tool) - [What questions can you ask it?](#what-questions-can-you-ask-it) - [Activate the skill on your Echo or other device](#activate-the-skill-on-your-echo-or-other-device) + - [Updating your skill with new features](#updating-your-skill-with-new-features) + - [Adding support for additional languages](#adding-support-for-additional-languages) - [Adding Alexa support to a plugin](#adding-alexa-support-to-a-plugin) - - [Intent Handlers](#intent-handlers) - - [Rollup handlers](#rollup-handlers) @@ -41,9 +42,9 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p ### Get an Amazon Developer account -- Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. -- [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. -- Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa). +1. Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. +1. [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. +1. Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa/console/ask). ### Create a new Alexa skill @@ -58,164 +59,11 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p Your Alexa skill's "interaction model" defines how your spoken questions get translated into requests to your Nightscout site, and how your Nightscout site's responses get translated into the audio responses that Alexa says back to you. -To get up and running with a basic interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code below. - -```json -{ - "interactionModel": { - "languageModel": { - "invocationName": "nightscout", - "intents": [ - { - "name": "NSStatus", - "slots": [], - "samples": [ - "How am I doing" - ] - }, - { - "name": "UploaderBattery", - "slots": [], - "samples": [ - "How is my uploader battery" - ] - }, - { - "name": "PumpBattery", - "slots": [], - "samples": [ - "How is my pump battery" - ] - }, - { - "name": "LastLoop", - "slots": [], - "samples": [ - "When was my last loop" - ] - }, - { - "name": "MetricNow", - "slots": [ - { - "name": "metric", - "type": "LIST_OF_METRICS" - }, - { - "name": "pwd", - "type": "AMAZON.US_FIRST_NAME" - } - ], - "samples": [ - "What is my {metric}", - "What my {metric} is", - "What is {pwd} {metric}" - ] - }, - { - "name": "InsulinRemaining", - "slots": [ - { - "name": "pwd", - "type": "AMAZON.US_FIRST_NAME" - } - ], - "samples": [ - "How much insulin do I have left", - "How much insulin do I have remaining", - "How much insulin does {pwd} have left", - "How much insulin does {pwd} have remaining" - ] - } - ], - "types": [ - { - "name": "LIST_OF_METRICS", - "values": [ - { - "name": { - "value": "bg" - } - }, - { - "name": { - "value": "blood glucose" - } - }, - { - "name": { - "value": "number" - } - }, - { - "name": { - "value": "iob" - } - }, - { - "name": { - "value": "insulin on board" - } - }, - { - "name": { - "value": "current basal" - } - }, - { - "name": { - "value": "basal" - } - }, - { - "name": { - "value": "cob" - } - }, - { - "name": { - "value": "carbs on board" - } - }, - { - "name": { - "value": "carbohydrates on board" - } - }, - { - "name": { - "value": "loop forecast" - } - }, - { - "name": { - "value": "ar2 forecast" - } - }, - { - "name": { - "value": "forecast" - } - }, - { - "name": { - "value": "raw bg" - } - }, - { - "name": { - "value": "raw blood glucose" - } - } - ] - } - ] - } - } -} -``` - -Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the above code. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. +To get up and running with an interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code for your language from [the list of templates](alexa-templates/). + +- If you're language doesn't have a template, please consider starting with [the en-us template](alexa-templates/en-us.json), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. + +Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the code from your chosen template. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. Next you need to build your custom model. Click "Build Model" at the top of the same page. It'll take a minute to build, and then you should see another success message, "Build Successful". @@ -228,122 +76,91 @@ Now you need to point your skill at *your* Nightscout site. 1. In the left-hand menu for your skill, there's an option called "Endpoint". Click it. 1. Under "Service Endpoint Type", select "HTTPS". 1. You only need to set up the Default Region. In the box that says "Enter URI...", put in `https://{yourdomain}/api/v1/alexa`. (So if your Nightscout site is at `mynightscoutsite.herokuapp.com`, you'll enter `https://mynightscoutsite.herokuapp.com/api/v1/alexa` in the box.) + - If you use Authentication Roles, you'll need to add a bit to the end of your URL. See [the section](#do-you-use-authentication-roles) below. 1. In the dropdown under the previous box, select "My development endpoint is a sub-domain of a domain that has a wildcard certificate from a certificate authority". 1. Click the "Save Endpoints" button at the top. 1. You should see a success message pop up when the save succeeds. +### Do you use Authentication Roles? ### + +If you use Authentication Roles, you will need to add a token to the end of your Nightscout URL when configuring your Endpoint. + +1. In your Nightscout Admin Tools, add a new subject and give it the "readable" role. + - If you **really** would like to be super specific, you could create a new role and set the permissions to `api:*:read`. +1. After the new subject is created, copy the "Access Token" value for the new row in your subject table (**don't** copy the link, just copy the text). +1. At the end of your Nighscout URL, add `?token={yourtoken}`, where `{yourtoken}` is the Access Token you just copied. Your new URL should look like `https://{yourdomain}/api/v1/googlehome?token={yourtoken}`. + ### Test your skill out with the test tool Click on the "Test" tab on the top menu. This will take you to the page where you can test your new skill. Enable testing for your skill (click the toggle). As indicated on this page, when testing is enabled, you can interact with the development version of your skill in the Alexa simulator and on all devices linked to your Alexa developer account. (Your skill will always be a development version. There's no need to publish it to the public.) -After you enable testing, you can also use the Alexa Simulator in the left column, to try out the skill. You can type in questions and see the text your skill would reply with. You can also hold the microphone icon to ask questions using your microphone, and you'll get the audio and text responses back. +After you enable testing, you can also use the Alexa Simulator in the left column, to try out the skill. You can type in questions and see the text your skill would reply with. When typing your test question, only type what you would verbally say to an Alexa device after the wake word. (e.g. you would verbally say "Alexa, ask Nightscout how am I doing", so you would type only "ask Nightscout how am I doing") You can also hold the microphone icon to ask questions using your microphone, and you'll get the audio and text responses back. ##### What questions can you ask it? -*Forecast:* - -- "Alexa, ask Nightscout how am I doing" -- "Alexa, ask Nightscout how I'm doing" - -*Uploader Battery:* - -- "Alexa, ask Nightscout how is my uploader battery" - -*Pump Battery:* - -- "Alexa, ask Nightscout how is my pump battery" - -*Metrics:* - -- "Alexa, ask Nightscout what my bg is" -- "Alexa, ask Nightscout what my blood glucose is" -- "Alexa, ask Nightscout what my number is" -- "Alexa, ask Nightscout what is my insulin on board" -- "Alexa, ask Nightscout what is my basal" -- "Alexa, ask Nightscout what is my current basal" -- "Alexa, ask Nightscout what is my cob" -- "Alexa, ask Nightscout what is Charlie's carbs on board" -- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" -- "Alexa, ask Nightscout what is Harper's loop forecast" -- "Alexa, ask Nightscout what is Alicia's ar2 forecast" -- "Alexa, ask Nightscout what is Peter's forecast" -- "Alexa, ask Nightscout what is Arden's raw bg" -- "Alexa, ask Nightscout what is Dana's raw blood glucose" - -*Insulin Remaining:* - -- "Alexa, ask Nightscout how much insulin do I have left" -- "Alexa, ask Nightscout how much insulin do I have remaining" -- "Alexa, ask Nightscout how much insulin does Dana have left? -- "Alexa, ask Nightscout how much insulin does Arden have remaining? - -*Last Loop:* - -- "Alexa, ask Nightscout when was my last loop" - -(Note: all the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name.) +See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. ### Activate the skill on your Echo or other device If your device is [registered](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) with your developer account, you should be able to use your skill right away. Try it by asking Alexa one of the above questions using your device. +## Updating your skill with new features + +As more work is done on Nightscout, new ways to interact with Nighscout via Alexa may be made available. To be able to use these new features, you first will need to [update your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version), and then you can follow the steps below to update your Alexa skill. + +1. Make sure you've [updated your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) first. +1. Open [the latest skill template](alexa-templates/) in your language. You'll be copying the contents of the file later. + - If your language doesn't include the latest features you're looking for, you're help [translating those new features](#adding-support-for-additional-languages) would be greatly appreciated! +1. Sign in to the [Alexa developer portal](https://developer.amazon.com/alexa/console/ask). +1. Open your Nightscout skill. +1. Open the "JSON Editor" in the left navigation pane. +1. Select everything in the text box (Ctrl + A on Windows, Cmd + A on Mac) and delete it. +1. Copy the contents of the updated template and paste it in the text box in the JSON Editor page. +1. Click the "Save Model" button near the top of the page, and then click the "Build Model" button. +1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. If you gave your skill name something other than "night scout," you will need to go to the "Invocation" page in the left navigation pane and change the Skill Invocation Name back to your preferred name. Make sure to click the "Save Model" button followed by the "Build Model" button after you change the name. +1. Enjoy the new features! + +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. + +If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the "JSON Editor" in the left navigation pane. + +1. Open the Build tab of your Alexa Skill. + - Get to your list of Alexa Skills at https://developer.amazon.com/alexa/console/ask and click on the name of the skill. +1. Click on the language drop-down box in the upper right corner of the window. +1. Click "Language settings". +1. Add your desired language. +1. Click the "Save" button. +1. Navigate to "CUSTOM" in the left navigation pane. +1. Select your new language in the language drop-down box. +1. Go to "JSON Editor" (just above "Interfaces" in the left navigation pane). +1. Remove the existing contents in the text box, and copy and paste the configuration code from a familiar language in [the list of templates](alexa-templates/). +1. Click "Save Model". +1. Click the "Add" button next to the "Slot Types" section in the left pane. +1. Click the radio button for "Use an existing slot type from Alexa's built-in library" +1. In the search box just below that option, search for "first name" +1. If your language has an option, click the "Add Slot Type" button for that option. + - If your language doesn't have an option, you won't be able to ask Nightscout a question that includes a name. +1. For each Intent listed in the left navigation pane (e.g. "NSStatus" and "MetricNow"): + 1. Click on the Intent name. + 1. Scroll down to the "Slots" section + 1. If there's a slot with the name "pwd", change the Slot Type to the one found above. + - If you didn't find one above, you'll have to see if another language gets close enough for you, or delete the slot. + 1. If there's a slot with the name "metric", click the "Edit Dialog" link on the right. This is where you set Alexa's questions and your answers if you happen to ask a question about metrics but don't include which metric you want to know. + 1. Set the "Alexa speech prompts" in your language, and remove the old ones. + 1. Under "User utterances", set the phrases you would say in response to the questions Alexa would pose from the previous step. MAKE SURE that your example phrases include where you would say the name of the metric. You do this by typing the left brace (`{`) and then selecting `metric` in the popup. + 1. Click on the Intent name (just to the left of "metric") to return to the previous screen. + 1. For each Sample Utterance, add an equivalent phrase in your language. If the phrase you're replacing has a `metric` slot, make sure to include that in your replacement phrase. Same goes for the `pwd` slot, unless you had to delete that slot a couple steps ago, in which case you need to modify the phrase to not use a first name, or not make a replacement phrase. After you've entered your replacement phrase, delete the phrase you're replacing. +1. Navigate to the "LIST_OF_METRICS" under the Slot Types section. +1. For each metric listed, add synonyms in your language, and delete the old synonyms. + - What ever you do, **DO NOT** change the text in the "VALUE" column! Nightscout will be looking for these exact values. Only change the synonyms. +1. Click "Save Model" at the top, and then click on "Build Model". +1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. + ## Adding Alexa support to a plugin -This document assumes some familiarity with the Alexa interface. You can find more information [here](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/getting-started-guide). - -To add alexa support to a plugin the ``init`` should return an object that contains an "alexa" key. Here is an example: - -```javascript -var iob = { - name: 'iob' - , label: 'Insulin-on-Board' - , pluginType: 'pill-major' - , alexa : { - rollupHandlers: [{ - rollupGroup: "Status" - , rollupName: "current iob" - , rollupHandler: alexaIOBRollupHandler - }] - , intentHandlers: [{ - intent: "MetricNow" - , routableSlot: "metric" - , slots: ["iob", "insulin on board"] - , intentHandler: alexaIOBIntentHandler - }] - } -}; -``` - -There are 2 types of handlers that you will need to supply: -* Intent handler - enables you to "teach" Alexa how to respond to a user's question. -* A rollup handler - enables you to create a command that aggregates information from multiple plugins. This would be akin to the Alexa "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. - -### Intent Handlers - -A plugin can expose multiple intent handlers. -+ ``intent`` - this is the intent in the "intent schema" above -+ ``routeableSlot`` - This enables routing by a slot name to the appropriate intent handler for overloaded intents e.g. "What is my " - iob, bg, cob, etc. This value should match the slot named in the "intent schema" -+ ``slots`` - These are the values of the slots. Make sure to add these values to the appropriate custom slot -+ ``intenthandler`` - this is a callback function that receives 3 arguments - - ``callback`` Call this at the end of your function. It requires 2 arguments - - ``title`` - Title of the handler. This is the value that will be displayed on the Alexa card - - ``text`` - This is text that Alexa should speak. - - ``slots`` - these are the slots that Alexa detected - - ``sandbox`` - This is the nightscout sandbox that allows access to various functions. - -### Rollup handlers - -A plugin can also expose multiple rollup handlers -+ ``rollupGroup`` - This is the key that is used to aggregate the responses when the intent is invoked -+ ``rollupName`` - This is the name of the handler. Primarily used for debugging -+ ``rollupHandler`` - this is a callback function that receives 3 arguments - - ``slots`` - These are the values of the slots. Make sure to add these values to the appropriate custom slot - - ``sandbox`` - This is the nightscout sandbox that allows access to various functions. - - ``callback`` - - - ``error`` - This would be an error message - - ``response`` - A simple object that expects a ``results`` string and a ``priority`` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: - ```javascript - callback(null, {results: "Hello world", priority: 1}); - ``` +See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/alexa-templates/en-us.json b/docs/plugins/alexa-templates/en-us.json new file mode 100644 index 00000000000..79cc1baa977 --- /dev/null +++ b/docs/plugins/alexa-templates/en-us.json @@ -0,0 +1,293 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "night scout", + "intents": [ + { + "name": "NSStatus", + "slots": [], + "samples": [ + "How am I doing" + ] + }, + { + "name": "LastLoop", + "slots": [], + "samples": [ + "When was my last loop" + ] + }, + { + "name": "MetricNow", + "slots": [ + { + "name": "metric", + "type": "LIST_OF_METRICS", + "samples": [ + "what {pwd} {metric} is", + "what my {metric} is", + "how {pwd} {metric} is", + "how my {metric} is", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "how much {metric}", + "{pwd} {metric}", + "{metric}", + "my {metric}" + ] + }, + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME" + } + ], + "samples": [ + "how much {metric} does {pwd} have left", + "what's {metric}", + "what's my {metric}", + "how much {metric} is left", + "what's {pwd} {metric}", + "how much {metric}", + "how is {metric}", + "how is my {metric}", + "how is {pwd} {metric}", + "how my {metric} is", + "what is {metric}", + "how much {metric} do I have", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "what is my {metric}", + "what my {metric} is", + "what is {pwd} {metric}" + ] + }, + { + "name": "AMAZON.NavigateHomeIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + } + ], + "types": [ + { + "name": "LIST_OF_METRICS", + "values": [ + { + "name": { + "value": "delta", + "synonyms": [ + "blood glucose delta", + "blood sugar delta", + "bg delta" + ] + } + }, + { + "name": { + "value": "uploader battery", + "synonyms": [ + "uploader battery remaining", + "uploader battery power" + ] + } + }, + { + "name": { + "value": "pump reservoir", + "synonyms": [ + "remaining insulin", + "insulin remaining", + "insulin is left", + "insulin left", + "insulin in my pump", + "insulin" + ] + } + }, + { + "name": { + "value": "pump battery", + "synonyms": [ + "pump battery remaining", + "pump battery power" + ] + } + }, + { + "name": { + "value": "bg", + "synonyms": [ + "number", + "blood sugar", + "blood glucose" + ] + } + }, + { + "name": { + "value": "iob", + "synonyms": [ + "insulin on board" + ] + } + }, + { + "name": { + "value": "basal", + "synonyms": [ + "current basil", + "basil", + "current basal" + ] + } + }, + { + "name": { + "value": "cob", + "synonyms": [ + "carbs", + "carbs on board", + "carboydrates", + "carbohydrates on board" + ] + } + }, + { + "name": { + "value": "forecast", + "synonyms": [ + "ar2 forecast", + "loop forecast" + ] + } + }, + { + "name": { + "value": "raw bg", + "synonyms": [ + "raw number", + "raw blood sugar", + "raw blood glucose" + ] + } + }, + { + "name": { + "value": "cgm noise" + } + }, + { + "name": { + "value": "cgm tx age", + "synonyms": [ + "tx age", + "transmitter age", + "cgm transmitter age" + ] + } + }, + { + "name": { + "value": "cgm tx status", + "synonyms": [ + "tx status", + "transmitter status", + "cgm transmitter status" + ] + } + }, + { + "name": { + "value": "cgm battery", + "synonyms": [ + "cgm battery level", + "cgm battery levels", + "cgm batteries", + "cgm transmitter battery", + "cgm transmitter battery level", + "cgm transmitter battery levels", + "cgm transmitter batteries", + "transmitter battery", + "transmitter battery level", + "transmitter battery levels", + "transmitter batteries" + ] + } + }, + { + "name": { + "value": "cgm session age", + "synonyms": [ + "session age" + ] + } + }, + { + "name": { + "value": "cgm status" + } + }, + { + "name": { + "value": "cgm mode" + } + } + ] + } + ] + }, + "dialog": { + "intents": [ + { + "name": "MetricNow", + "confirmationRequired": false, + "prompts": {}, + "slots": [ + { + "name": "metric", + "type": "LIST_OF_METRICS", + "confirmationRequired": false, + "elicitationRequired": true, + "prompts": { + "elicitation": "Elicit.Slot.1421281086569.34001419564" + } + }, + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME", + "confirmationRequired": false, + "elicitationRequired": false, + "prompts": {} + } + ] + } + ], + "delegationStrategy": "ALWAYS" + }, + "prompts": [ + { + "id": "Elicit.Slot.1421281086569.34001419564", + "variations": [ + { + "type": "PlainText", + "value": "What metric are you looking for?" + }, + { + "type": "PlainText", + "value": "What value are you looking for?" + }, + { + "type": "PlainText", + "value": "What metric do you want to know?" + }, + { + "type": "PlainText", + "value": "What value do you want to know?" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/docs/plugins/google-home-templates/en-us.zip b/docs/plugins/google-home-templates/en-us.zip new file mode 100644 index 0000000000000000000000000000000000000000..d8ada2a834a29cb73802fb364902d86034ca3421 GIT binary patch literal 6367 zcmaJ_1z1#D*B-i&?v@ge?ohf0hDKT`iD76MkPs9BfuTV<1VQOWkO7eplu$rokdW>W zq~jmC-@S18{Qj9|&pFRLd%b6`v(|c7XamvE$p8QVHXx=UL+d~y@W%}_06-240Ki55 zYiVce?8)os;o@9s0MQ!fCpzmZmp#-tN=x>h62vtXp`yR7Iw+hQ8K9;~FIAl>n|iS6 zF%weoR8s-L0fa8TTg>u1_I34H(iu{$?BT!F1P#lA>f=g;vxcDqad0^X!EbujM8N(* z!n3bU&r?TB<}~7yqXY?vKQ{2c4oYO`v(;U(o~9XR2cp?1kaKH)V^^N|LFL$6 zgJM~-Qb^vSpV1|Z)>cwI>;g`{_>qsFyMz?{)RjEYqM?1oa0|B^TQr&S$Kf)hC6y7CS}{tfZL2oOlheU`g$W?9dJGavMY69 zd)iT8aMcHG2$%pH#@3_0Z&!-44{kP2l)o6ccjn$Kxn82vtx0G+T$!GWN4-jMS~kuP ze#b>K)S%49)Zv{5MmGgb@CLq`v?j%UKxe_jvAU^Il&TSob2`4kY__n=L-}oHNGkr! zNrvYiT~|czJVi!A(qwPmz1A)?4bN`$P})w z$*!aVl=&x5wHWHMZ@?Yz)9JBB9`O{TBH%Gl*u;U~!ArnsZWdF<%Rg>dIb zP?!vSOJ3mbb<^{SsZnLXFc1B()*MGD{z1*5czu*4EH;i8q^B^bRvvS!y`QYDT^=L4 zb-Hkh)OI)Z(YOHv|4fUnIANz%U27|?{~NrBBDf+0=Np&U{vGA*YToQs+HamwZB7|9 zu_oTb!FBKFq?|t1Z&abc?&%ijS6KMQ-t4C>(~x<%&&2&=QoMBzGm+%fapl!0H{)Bw zwOmKeupUW^+m0oe-+?YO=q2wC!PaMH2WSaZ(#zw4quVS{NVe*bD86+bSN?eH$M#w` z*I|dUhw4i8EpuMR$gk%<0F3=|H9%1J#vH}n24VJkSSy;VXHL3@w#8nKKTGa zO@Q+y^obP=dhez3MBsT-gFxRixypI@)==S+M>s%M#>}l|>x^BZH_8647yY2{2K|zv?}>t{n=28{xb&0kp6SOeLA7nR zFBT3F3sgBVQ+*q2DF55Tv5}{DaP~whaNWV*By0@CAsyTnnL=(}*`;Xv(9+Avlj)wV zleG)PmPzeG`a)%hZmTnm01B(5W$ddv1C6BMogphL`ubR{v7j*6l|(R(n9sw{H2| z|LFIkUc&x;SsrCg2L=Og$?n(^+dNpYpOe_d`8@`l%x8o~ai^F+quwf%lU`-mNdIJFlOM?NOM%JUID*;+qN1>ff` zi#8*EjJ`Rdt+VU!Kn#HkyB#L6oXtAE&_X%fr^)!_xO3(h`Xl!27^Y^lbkm;%0Sl3I$;% zy(g5LeqfFzSoF52lHAEAy8wOktj}h^rX=Q;8rpmKyxlYj?ehYqV0G%RCW8@{@8vSE zVNzW4`U)poT1NCS|-eS97 z+g1)uOV&|AKn(bIDBQzJ_UrPYxEfXX%VXCfu_ZQj-wNra!>9lN`5$z;@JsXfR_71= z_@SEt5wGoJ>8^l8sL~ zXV`cW3OhU{1N*9%eb{{@L5q<#8GTj0opzDv?K}*Jr)l@5h3bxF+#J_nKSRIh2oi+U z%4%)_hb!BlOqfnEguE;vzc5XpCcmo<4aK#mk$?;)#8`I9XvE1Srr7NW>ZuG zc-jK;4}&7(A+|d4s)Bg0Rh{^Tcsi$D3bOC8gHcwvmQV1-8!?K64TurzrorMm}J|#ilUWSlPj=dyDcT zEIp+_3MY_`S~{|<(ri7HZ%%FxPCeaA7%h@@_KrvkyLXqLBsK`;TH+4z;GakQUZnfM&LSA zjh#v)YRI;;G_F$c5Q~)G%0Qlo{k{studgms^&`vT6gZjk%El?}ZY#+Wc$Bf?pYTL8 zdl!78bVxFG*M$SYZLT0IWjl&??*kmK6 zjeNujB))~X3ff5Du!bj$sZKXs_wiLD0621wuc*{!V)Vt3zIEoX6rp@l-sxQJ+r#&; z39%WJ<#|XxyntWJt{VSCNWnAH2=N|fb$OFHs3qwn z2A?;(xHyxzFgDn?LH?Xwv)Pm1L3=ulc7?-c4$k$2z`VVk0fGgvSlpfTR@bS>{-iU` zM43f-*X$YvUrc8X_Z%oU6gEJ}lc(g~1e&bb`W_7Fl z(Ni@Y;;9MNf-qa-hYRWJTtQlN08tum%!f%NX{_!8Cfg>o-KNZ@Wr>dMY7F2({IxAD zDtsy-^JZ_5Au3${<-~??8>x(ji|Z2FTU%qGG_9Cc4BnVq!I1M;U#=Ok&1QYrnfl-g z{}*TO4;4-xMI`f+!v_YT&$;A|RFr&kC+Z)ROr&kg(JJ2~17UR)7ac=6`^(+N*{i=rL_MzIeK zW;L}EOMZ|!Cbn4;FclUqdI(0^a5zNE4=LvFi^J!dH(b-!09NGv~RLGWS zUSWpeKUiBiD@CtqwzDy_zt{Oj`ZE)=>PSCZriq6wuVPM?4~EH(zbj%(vHmGM&Ux)u9^ ze5xFyM0%n&B>mPce15Ah-(`~tPGTdV<|jlMg8E6npwRin@P;5~BKqcEB1h>A(Ua%5 z)?FP9UUDs4)%e=qRp}?93@_v(Saz+WpUgQTQ%%JOm|@+-kGZrAx+q2YK>uj zYWGzvIr4Ptne>~0@h}`(GSq&}Q}6QSiKe^%wu&a+2zM{iDkJCs0P7VJ{JVZqwsdl` zva|+YEudifmCneLjL561ltT*3^i(9M!bryjcawL%j;jw-}AXP6G+Tjc>uGaxYI}Q5pF&aMv zTHMvKpWGQfnBAQ!qmVZ1{j?cqe<$zEQt#LnUfmtmqPd@a$E@^h@rOBlIuI+&Xy6H> z^oM!P0N3+V$oaO1c+ebd)RpF%l_Z1D z-@+}-MeUt3esp^)?8`3Dgwqkri-JUAl|nqkbs}*rUtTu`Dd;85a2braYIs!uKZ31HjxQ- zfuL$o2-_n2v@*-AEX@dzg60``fcNQMetHTBJzo!FkvsiUoYD4yyG(4;8f!ksBFEip zJ?fr`YXCJeOnxtX;ZKQBKEZUC+}5CxS1 z?XnKNJ^@AI0FaCf$PfN~cR3IJOYVPG zVt*bGT?e~9Vz>k|#kg{YD+7oBPmAAe{$HQpT2NzNZTkQH&aSupzoq_C;_elxzmwP> zh5*+Ux!!eN0um!n_4n-IfA*l);jTA)mvHlcfV=RQ>k!x5lS_z4NFVt7ME?-;A2F^M zlb0Ao1eX|pS786e?bnZVy@b1jTmA#wA5V0>-nfK#eB<&&e;e*k)yMTiT~Aq;h|$EC pi2uB&|Doh%SY40lOFTF-X0JxLHV^~pYybcu^4pD!or~5U@PCO>nezYu literal 0 HcmV?d00001 diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md new file mode 100644 index 00000000000..5b82bdfb053 --- /dev/null +++ b/docs/plugins/googlehome-plugin.md @@ -0,0 +1,171 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Nightscout Google Home/DialogFlow Plugin](#nightscout-google-homedialogflow-plugin) + - [Overview](#overview) + - [Activate the Nightscout Google Home Plugin](#activate-the-nightscout-google-home-plugin) + - [Create Your DialogFlow Agent](#create-your-dialogflow-agent) + - [Do you use Authentication Roles?](#do-you-use-authentication-roles) + - [What questions can you ask it?](#what-questions-can-you-ask-it) + - [Using the Alpha Tester feature](#using-the-alpha-tester-feature) + - [Updating your agent with new features](#updating-your-agent-with-new-features) + - [Adding support for additional languages](#adding-support-for-additional-languages) + - [Adding Google Home support to a plugin](#adding-google-home-support-to-a-plugin) + + + +Nightscout Google Home/DialogFlow Plugin +======================================== + +## Overview + +To add Google Home support for your Nightscout site, here's what you need to do: + +1. [Activate the `googlehome` plugin](#activate-the-nightscout-google-home-plugin) on your Nightscout site, so your site will respond correctly to Google's requests. +1. [Create a custom DialogFlow agent](#create-your-dialogflow-agent) that points at your site and defines certain questions you want to be able to ask. + +## Activate the Nightscout Google Home Plugin + +1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. It needs to be [version 13.0.0 (Ketchup)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/13.0.0) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version. +1. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) + +## Create Your DialogFlow Agent + +**BEFORE YOU GET STARTED:** Please read [the section below on the **highly-recommended** use of the Alpha tester feature](#using-the-alpha-tester-feature). + +1. Download the agent template in your language for Google Home [here](google-home-templates/). + - If you're language doesn't have a template, please consider starting with [the en-us template](google-home-templates/en-us.zip), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. +1. [Sign in to Google's Action Console](https://console.actions.google.com) + - Make sure to use the same account that is connected to your Google Home device, Android smartphone, Android tablet, etc. +1. Click on the "New Project" button. +1. If prompted, agree to the Terms of Service. +1. Give your project a name (e.g. "Nightscout") and then click "Create project". +1. For the "development experience", select "Conversational" at the bottom of the list. +1. Click on the "Develop" tab at the top of the sreen. +1. Click on "Invocation" in the left navigation pane. +1. Set the display name (e.g. "Night Scout") of your Action and set your Google Assistant voice. + - Unfortunately, the Action name needs to be two words, and is required to be unique across all of Google, even though you won't be publishing this for everyone on Google to use. So you'll have to be creative with the name since "Night Scout" is already taken. +1. Click "Save" in the upper right corner. +1. Navigate to "Actions" in the left nagivation pane, then click on the "Add your first action" button. +1. Make sure you're on "Cutom intent" and then click "Build" to open DialogFlow in a new tab. +1. Sign in with the same Google account you used to sign in to the Actions Console. + - You'll have to go through the account setup steps if this is your first time using DialogFlow. +1. Verify the name for your agent (e.g. "Nightscout") and click "CREATE". +1. In the navigation pane on the left, click the gear icon next to your agent name. +1. Click on the "Export and Import" tab in the main area of the page. +1. Click the "IMPORT FROM ZIP" button. +1. Select the template file downloaded in step 1. +1. Type "IMPORT" where requested and then click the "IMPORT" button. +1. After the import finishes, click the "DONE" button followed by the "SAVE" button. +1. In the navigation pane on the left, click on "Fulfillment". +1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` + - If you use Authentication Roles, you'll need to add a bit to the end of your URL. See [the section](#do-you-use-authentication-roles) below. +1. Scroll down to the bottom of the page and click the "SAVE" button. +1. Click on "Integrations" in the navigation pane. +1. Click on "INTEGRATION SETTINGS" for "Google Assistant". +1. Under "Implicit invocation", add every intent listed. +1. Turn on the toggle for "Auto-preview changes". +1. Click "CLOSE". + +That's it! Now try asking Google "Hey Google, ask *your Action's name* how am I doing?" + +### Do you use Authentication Roles? ### + +If you use Authentication Roles, you will need to add a token to the end of your Nightscout URL when configuring your Webhook. + +1. In your Nightscout Admin Tools, add a new subject and give it the "readable" role. + - If you **really** would like to be super specific, you could create a new role and set the permissions to `api:*:read`. +1. After the new subject is created, copy the "Access Token" value for the new row in your subject table (**don't** copy the link, just copy the text). +1. At the end of your Nighscout URL, add `?token=YOUR-TOKEN`, where `YOUR-TOKEN` is the Access Token you just copied. Your new URL should look like `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome?token=YOUR-TOKEN`. + +### What questions can you ask it? + +See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. + +### Using the Alpha Tester feature + +If you use your Google Action on the same account as the one you used to create it, you will find that Google disables Test Mode for the action after some period of time, and you have to log in to the Actions console, and open the testing tab to re-enable testing for you to continue to use it. To overcome this limitation, you can use the Alpa Testers feature of Google Actions. To do so, you need to follow a few extra steps: + +1. Figure out which Google account you use for your Google Assistant. +1. Use or create a different account to follow the instructions to create your Google Action and DialogFlow agent. +1. Once you verify your Action is working, navigate to the [Actions Console](https://console.actions.google.com/), and open your project. +1. Navigate to the "Deploy" tab, then open the "Release" page in the left navigation pane. +1. Expand the "Alpha" section, then click on the "Create a release" button, and then follow the directions to create a release. + - You may need to fill out some extra information, such as a Privacy Policy ([example](https://docs.google.com/document/d/1RP32ooEol97UyPiJ9vUskhLb6XC6PHhtVTwh0siUZV0/view)), descriptions (e.g. both could be "Tools and stuff to help my partner and I do things better"), and testing instructions (e.g. "Since this is built entirely for myself and my partner, don't expect too much out of this one."). Because this will be for personal use, it's recommended that you keep all of these as vague as possible. +1. Click the "Manage Alpha Testers" button. +1. Enter the email address of the account you use for your Google Assistant (found in step 1). Enter the emails of any other people you would like to have access to talk to your Action (e.g. a spouse). +1. Click the "Save" button. +1. Copy the "Opt-in link" and open it on a device logged in with your account from step 1. Repeat for any other emails you added two steps ago. If the link doesn't work right away, try again in a couple hours. +1. Follow any directions to setup the account as an Alpha Tester. + +## Updating your agent with new features + +As more work is done on Nightscout, new ways to interact with Nighscout via Google Home may be made available. To be able to use these new features, you first will need to [update your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version), and then you can follow the steps below to update your DialogFlow agent. + +1. Make sure you've [updated your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) first. +1. Download [the latest skill template](google-home-templates/) in your language. + - If your language doesn't include the latest features you're looking for, you're help [translating those new features](#adding-support-for-additional-languages) would be greatly appreciated! +1. Sign in to the [DialogFlow developer portal](https://dialogflow.cloud.google.com/). +1. Make sure you're viewing your Nightscout agent (there's a drop-down box immediately below the DialogFlow logo where you can select your agent). +1. Click on the gear icon next to your agent name, then click on the "Export and Import" tab. +1. Click the "RESTORE FROM ZIP" button. +1. Select the template file you downloaded earlier, then type "RESTORE" in the text box as requested, and click the "RESTORE" button. +1. After the import is completed, click the "DONE" button. +1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. If you use the Alpha Testers feature (see [Using the Alpha Tester feature](#using-the-alpha-tester-feature) above), create a new release. +1. Enjoy the new features! + +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. + +If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the settings of your DialogFlow agent (the gear icon next to the project's name in the left nagivation pane), going to the "Export and Import" tab, and clicking "EXPORT AS ZIP". + +1. Open your DialogFlow agent. + - Get to your list of agents at https://console.dialogflow.com/api-client/#/agents and click on the name of your Nightscout agent. +1. Click on the "Languages" tab. +1. Click the "Add Additional Language" drop-down box. +1. Select your desired language. +1. Click the "SAVE" button. + - Note the new language code below the agent's name. e.g. if you're using the English template and you added Spanish, you would see two buttons: "en" and "es". +1. Click on "Intents" in the left navigation pane. +1. For each intent in the list (NOT including those that start with "Default" in the name): + 1. Click on the intent name. + 1. Note the phrases used in the "Training phrases" section. + - If the phrase has a colored block (e.g. `metric` or `pwd`), click the phrase (but NOT the colored block) and note the "PARAMETER NAME" of the item with the same-colored "ENTITY". + 1. Click on the new language code (beneath the agent name near the top of the navigation pane). + 1. Add equivalent or similar training phrases as those you noted a couple steps ago. + - If the phrase in the orginal language has a colored block with a word in it, that needs to be included. When adding the phrase to the new language, follow these steps to add the colored block: + 1. When typing that part of the training phrase, don't translate the word in the block; just keep it as-is. + 1. After typing the phrase (DON'T push the Enter key yet!) highlight/select the word. + 1. A box will pop up with a list of parameter types, some of which end with a colon (`:`) and a parameter name. Click the option that has the same parameter name as the one you determined just a few steps ago. + 1. Press the Enter key to add the phrase. + 1. Click the "SAVE" button. + 1. Go back and forth between your starting language and your new language, adding equivalent phrase(s) to the new language. Continue once you've added all the equivalent phrases you can think of. + 1. Scroll down to the "Action and parameters" section. + 1. If any of the items in that list have the "REQUIRED" option checked: + 1. Click the "Define prompts..." link on the right side of that item. + 1. Add phrases that Google will ask if you happen to say something similar to a training phrase, but don't include this parameter (e.g. if you ask about a metric but don't say what metric you want to know about). + 1. Click "CLOSE". + 1. Scroll down to the "Responses" section. + 1. Set just one phrase here. This will be what Google says if it has technical difficulties getting a response from your Nightscout website. + 1. Click the "SAVE" button at the top of the window. +1. Click on the "Entities" section in the navigation pane. +1. For each entity listed: + 1. Click the entity name. + 1. Switch to the starting language (beneath the agent name near the top of the left navigation pane). + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". + 1. Select all the text in the text box and copy it. + 1. Switch back to your new language. + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". + 1. In the text box, paste the text you just copied. + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to editor mode". + 1. For each item in the list, replace the items on the RIGHT side of the list with equivalent words and phrases in your language. + - What ever you do, **DO NOT** change the values on the left side of the list. Nightscout will be looking for these exact values. Only change the items on the right side of the list. + 1. Click the "SAVE" button. +1. You should be good to go! Feel free to try it out by click the "See how it works in Google Assistant" link in the right navigation pane, or start asking your Google-Home-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. + +## Adding Google Home support to a plugin + +See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/interacting-with-virtual-assistants.md b/docs/plugins/interacting-with-virtual-assistants.md new file mode 100644 index 00000000000..3fe67bee2fb --- /dev/null +++ b/docs/plugins/interacting-with-virtual-assistants.md @@ -0,0 +1,77 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Interacting with Virtual Assistants](#interacting-with-virtual-assistants) +- [Alexa vs. Google Home](#alexa-vs-google-home) +- [What questions can you ask it?](#what-questions-can-you-ask-it) + - [A note about names](#a-note-about-names) + + + +Interacting with Virtual Assistants +=================================== + +# Alexa vs. Google Home + +Although these example phrases reference Alexa, the exact same questions could be asked of Google. +Just replace "Alexa, ask Nightscout ..." with "Hey Google, ask *your action's name* ..." + +# What questions can you ask it? + +This list is not meant to be comprehensive, nor does it include every way you can ask the questions. To get the full picture, in the respective console for your virtual assistant, check the example phrases for each `intent`, and the values (including synonyms) of the "metric" `slot` (Alexa) or `entity` (Google Home). You can also just experiement with asking different questions to see what works. + +*Forecast:* + +- "Alexa, ask Nightscout how am I doing" +- "Alexa, ask Nightscout how I'm doing" + +*Uploader Battery:* + +- "Alexa, ask Nightscout how is my uploader battery" + +*Pump Battery:* + +- "Alexa, ask Nightscout how is my pump battery" + +*Metrics:* + +- "Alexa, ask Nightscout what my bg is" +- "Alexa, ask Nightscout what my blood glucose is" +- "Alexa, ask Nightscout what my number is" +- "Alexa, ask Nightscout what is my insulin on board" +- "Alexa, ask Nightscout what is my basal" +- "Alexa, ask Nightscout what is my current basal" +- "Alexa, ask Nightscout what is my cob" +- "Alexa, ask Nightscout what is my delta" +- "Alexa, ask Nightscout what is Charlie's carbs on board" +- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" +- "Alexa, ask Nightscout what is Harper's loop forecast" +- "Alexa, ask Nightscout what is Alicia's ar2 forecast" +- "Alexa, ask Nightscout what is Peter's forecast" +- "Alexa, ask Nightscout what is Arden's raw bg" +- "Alexa, ask Nightscout what is Dana's raw blood glucose" + +*CGM Info:* (when using the [`xdripjs` plugin](/README.md#xdripjs-xdrip-js)) + +- "Alexa, ask Nightscout what's my CGM status" +- "Alexa, ask Nightscout what's my CGM session age" +- "Alexa, ask Nightscout what's my CGM transmitter age" +- "Alexa, ask Nightscout what's my CGM mode" +- "Alexa, ask Nightscout what's my CGM noise" +- "Alexa, ask Nightscout what's my CGM battery" + +*Insulin Remaining:* + +- "Alexa, ask Nightscout how much insulin do I have left" +- "Alexa, ask Nightscout how much insulin do I have remaining" +- "Alexa, ask Nightscout how much insulin does Dana have left? +- "Alexa, ask Nightscout how much insulin does Arden have remaining? + +*Last Loop:* + +- "Alexa, ask Nightscout when was my last loop" + +## A note about names + +All the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name. \ No newline at end of file diff --git a/env.js b/env.js index 9114e7297fc..275271bdf26 100644 --- a/env.js +++ b/env.js @@ -14,23 +14,19 @@ var env = { settings: require('./lib/settings')() }; +var shadowEnv; + // Module to constrain all config and environment parsing to one spot. -// See the +// See README.md for info about all the supported ENV VARs function config ( ) { - /* - * See README.md for info about all the supported ENV VARs - */ - env.DISPLAY_UNITS = readENV('DISPLAY_UNITS', 'mg/dl'); - - // be lenient at accepting the mmol input - if (env.DISPLAY_UNITS.toLowerCase().includes('mmol')) { - env.DISPLAY_UNITS = 'mmol'; - } else { - // also ensure the mg/dl is set with expected case - env.DISPLAY_UNITS = 'mg/dl'; - } - console.log('Units set to', env.DISPLAY_UNITS ); + // Assume users will typo whitespaces into keys and values + + shadowEnv = {}; + + Object.keys(process.env).forEach((key, index) => { + shadowEnv[_trim(key)] = _trim(process.env[key]); + }); env.PORT = readENV('PORT', 1337); env.HOSTNAME = readENV('HOSTNAME', null); @@ -90,12 +86,6 @@ function setAPISecret() { var shasum = crypto.createHash('sha1'); shasum.update(readENV('API_SECRET')); env.api_secret = shasum.digest('hex'); - - if (!readENV('TREATMENTS_AUTH', true)) { - - } - - } } } @@ -112,6 +102,7 @@ function setStorage() { env.authentication_collections_prefix = readENV('MONGO_AUTHENTICATION_COLLECTIONS_PREFIX', 'auth_'); env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); env.profile_collection = readENV('MONGO_PROFILE_COLLECTION', 'profile'); + env.settings_collection = readENV('MONGO_SETTINGS_COLLECTION', 'settings'); env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); env.food_collection = readENV('MONGO_FOOD_COLLECTION', 'food'); env.activity_collection = readENV('MONGO_ACTIVITY_COLLECTION', 'activity'); @@ -132,29 +123,38 @@ function updateSettings() { UNITS: 'DISPLAY_UNITS' }; + var envDefaultOverrides = { + DISPLAY_UNITS: 'mg/dl' + }; + env.settings.eachSettingAsEnv(function settingFromEnv (name) { var envName = envNameOverrides[name] || name; - return readENV(envName); + return readENV(envName, envDefaultOverrides[envName]); }); //should always find extended settings last - env.extendedSettings = findExtendedSettings(process.env); + env.extendedSettings = findExtendedSettings(shadowEnv); if (!readENVTruthy('TREATMENTS_AUTH', true)) { env.settings.authDefaultRoles = env.settings.authDefaultRoles || ""; env.settings.authDefaultRoles += ' careportal'; } - - } function readENV(varName, defaultValue) { //for some reason Azure uses this prefix, maybe there is a good reason - var value = process.env['CUSTOMCONNSTR_' + varName] - || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] - || process.env[varName] - || process.env[varName.toLowerCase()]; - + var value = shadowEnv['CUSTOMCONNSTR_' + varName] + || shadowEnv['CUSTOMCONNSTR_' + varName.toLowerCase()] + || shadowEnv[varName] + || shadowEnv[varName.toLowerCase()]; + + if (varName == 'DISPLAY_UNITS') { + if (value && value.toLowerCase().includes('mmol')) { + value = 'mmol'; + } else { + value = defaultValue; + } + } return value != null ? value : defaultValue; } @@ -172,6 +172,8 @@ function findExtendedSettings (envs) { extended.devicestatus = {}; extended.devicestatus.advanced = true; + extended.devicestatus.days = 1; + if(shadowEnv['DEVICESTATUS_DAYS'] && shadowEnv['DEVICESTATUS_DAYS'] == '2') extended.devicestatus.days = 1; function normalizeEnv (key) { return key.toUpperCase().replace('CUSTOMCONNSTR_', ''); diff --git a/lib/api/activity/index.js b/lib/api/activity/index.js index d88019ab936..c42e73570c6 100644 --- a/lib/api/activity/index.js +++ b/lib/api/activity/index.js @@ -43,17 +43,17 @@ function configure(app, wares, ctx) { var d2 = null; - if (t.hasOwnProperty('created_at')) { + if (Object.prototype.hasOwnProperty.call(t, 'created_at')) { d2 = new Date(t.created_at); } else { - if (t.hasOwnProperty('timestamp')) { + if (Object.prototype.hasOwnProperty.call(t, 'timestamp')) { d2 = new Date(t.timestamp); } } if (d2 == null) { return; } - if (d1 == null || d2.getTime() > d1.getTime()) { + if (d1 == null || d2.getTime() > d1.getTime()) { d1 = d2; } }); @@ -80,7 +80,7 @@ function configure(app, wares, ctx) { if (!_isArray(activity)) { activity = [activity]; - }; + } ctx.activity.create(activity, function(err, created) { if (err) { diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index 65f477ad85d..2a5fd4ef6cd 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -1,159 +1,121 @@ 'use strict'; var moment = require('moment'); -var _each = require('lodash/each'); function configure (app, wares, ctx, env) { - var entries = ctx.entries; - var express = require('express') - , api = express.Router( ); - var translate = ctx.language.translate; - - // invoke common middleware - api.use(wares.sendJSONStatus); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw()); - // json body types get handled as parsed json - api.use(wares.bodyParser.json()); - - ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.alexa) { - if (plugin.alexa.intentHandlers) { - console.log(plugin.name + ' is Alexa enabled'); - _each(plugin.alexa.intentHandlers, function (route) { - if (route) { - ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.routableSlot, route.slots); - } - }); - } - if (plugin.alexa.rollupHandlers) { - console.log(plugin.name + ' is Alexa rollup enabled'); - _each(plugin.alexa.rollupHandlers, function (route) { - console.log('Route'); - console.log(route); - if (route) { - ctx.alexa.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); - } - }); - } - } else { - console.log('Plugin ' + plugin.name + ' is not Alexa enabled'); - } - }); - - api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { - console.log('Incoming request from Alexa'); - var locale = req.body.request.locale; - if(locale){ - if(locale.length > 2) { - locale = locale.substr(0, 2); - } - ctx.language.set(locale); - moment.locale(locale); - } - - switch (req.body.request.type) { - case 'IntentRequest': - onIntent(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'LaunchRequest': - onLaunch(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'SessionEndedRequest': - onSessionEnded(req.body.request.intent, function (alexaResponse) { - res.json(alexaResponse); - next( ); - }); - break; - } - }); - - ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { - entries.list({count: 1}, function (err, records) { - var direction; - if (translate(records[0].direction)) { - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('alexaStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time)) - ] - }); - //var status = sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)) + '.'; - callback(null, {results: status, priority: -1}); - }); - // console.log('BG results called'); - // callback(null, 'BG results'); - }, 'BG Status'); + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); + + ctx.virtAsstBase.setupVirtAsstHandlers(ctx.alexa); + + api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Alexa'); + var locale = req.body.request.locale; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } - ctx.alexa.configureIntentHandler('MetricNow', function ( callback, slots, sbx, locale) { - entries.list({count: 1}, function(err, records) { - var direction; - if(translate(records[0].direction)){ - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('alexaStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time))] - }); - //var status = sbx.scaleMgdl(records[0].sgv) + direction + ' as of ' + moment(records[0].date).from(moment(sbx.time)); - callback('Current blood glucose', status); + switch (req.body.request.type) { + case 'SessionEndedRequest': + onSessionEnded(function () { + res.json(''); + next( ); }); - }, 'metric', ['bg', 'blood glucose', 'number']); - - ctx.alexa.configureIntentHandler('NSStatus', function(callback, slots, sbx, locale) { - ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { - callback('Full status', status); + break; + case 'LaunchRequest': + if (!req.body.request.intent) { + onLaunch(function () { + res.json(ctx.alexa.buildSpeechletResponse( + translate('virtAsstTitleLaunch'), + translate('virtAsstLaunch'), + translate('virtAsstLaunch'), + false + )); + next( ); + }); + break; + } + // if intent is set then fallback to IntentRequest + case 'IntentRequest': // eslint-disable-line no-fallthrough + onIntent(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', true)); + next( ); }); - }); - - - function onLaunch() { - console.log('Session launched'); + break; } - - function onIntent(intent, next) { - console.log('Received intent request'); - console.log(JSON.stringify(intent)); - handleIntent(intent.name, intent.slots, next); + }); + + ctx.virtAsstBase.setupMutualIntents(ctx.alexa); + + function onLaunch(next) { + console.log('Session launched'); + next( ); + } + + function onIntent(intent, next) { + console.log('Received intent request'); + console.log(JSON.stringify(intent)); + handleIntent(intent.name, intent.slots, next); + } + + function onSessionEnded(next) { + console.log('Session ended'); + next( ); + } + + function handleIntent(intentName, slots, next) { + var metric; + if (slots) { + if (slots.metric + && slots.metric.resolutions + && slots.metric.resolutions.resolutionsPerAuthority + && slots.metric.resolutions.resolutionsPerAuthority.length + && slots.metric.resolutions.resolutionsPerAuthority[0].status + && slots.metric.resolutions.resolutionsPerAuthority[0].status.code + && slots.metric.resolutions.resolutionsPerAuthority[0].status.code == "ER_SUCCESS_MATCH" + && slots.metric.resolutions.resolutionsPerAuthority[0].values + && slots.metric.resolutions.resolutionsPerAuthority[0].values.length + && slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value + && slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value.name + ){ + metric = slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value.name; + } else { + next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); + return; + } } - function onSessionEnded() { - console.log('Session ended'); + var handler = ctx.alexa.getIntentHandler(intentName, metric); + if (handler){ + var sbx = initializeSandbox(); + handler(next, slots, sbx); + return; + } else { + next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); + return; } + } - function handleIntent(intentName, slots, next) { - var handler = ctx.alexa.getIntentHandler(intentName, slots); - if (handler){ - var sbx = initializeSandbox(); - handler(next, slots, sbx); - } else { - next('Unknown Intent', 'I\'m sorry I don\'t know what you\'re asking for'); - } - } - - function initializeSandbox() { - var sbx = require('../../sandbox')(); - sbx.serverInit(env, ctx); - ctx.plugins.setProperties(sbx); - return sbx; - } + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } - return api; + return api; } module.exports = configure; diff --git a/lib/api/const.json b/lib/api/const.json new file mode 100644 index 00000000000..cb1421d8520 --- /dev/null +++ b/lib/api/const.json @@ -0,0 +1,4 @@ +{ + "API1_VERSION": "1.0.0", + "API2_VERSION": "2.0.0" +} \ No newline at end of file diff --git a/lib/api/devicestatus/index.js b/lib/api/devicestatus/index.js index 91702902fa3..94dfb1c9385 100644 --- a/lib/api/devicestatus/index.js +++ b/lib/api/devicestatus/index.js @@ -2,6 +2,9 @@ const consts = require('../../constants'); const moment = require('moment'); +const { query } = require('express'); +const _take = require('lodash/take'); +const _ = require('lodash'); function configure (app, wares, ctx, env) { var express = require('express') @@ -18,6 +21,25 @@ function configure (app, wares, ctx, env) { api.use(ctx.authorization.isPermitted('api:devicestatus:read')); + function processDates(results) { + // Support date de-normalization for older clients + + if (env.settings.deNormalizeDates) { + const r = []; + results.forEach(function(e) { + if (e.created_at && Object.prototype.hasOwnProperty.call(e, 'utcOffset')) { + const d = moment(e.created_at).utcOffset(e.utcOffset); + e.created_at = d.toISOString(true); + delete e.utcOffset; + } + r.push(e); + }); + return r; + } else { + return results; + } + } + // List settings available api.get('/devicestatus/', function(req, res) { var q = req.query; @@ -25,21 +47,19 @@ function configure (app, wares, ctx, env) { q.count = 10; } - ctx.devicestatus.list(q, function(err, results) { + const inMemoryData = ctx.cache.devicestatus ? ctx.cache.devicestatus : []; + const canServeFromMemory = inMemoryData.length >= q.count && Object.keys(q).length == 1 ? true : false; - // Support date de-normalization for older clients - if (env.settings.deNormalizeDates) { - results.forEach(function(e) { - // eslint-disable-next-line no-prototype-builtins - if (e.created_at && e.hasOwnProperty('utcOffset')) { - const d = moment(e.created_at).utcOffset(e.utcOffset); - e.created_at = d.toISOString(true); - delete e.utcOffset; - } - }); - } + if (canServeFromMemory) { + const sorted = _.sortBy(inMemoryData, function(item) { + return -item.mills; + }); - return res.json(results); + return res.json(processDates(_take(sorted, q.count))); + } + + ctx.devicestatus.list(q, function(err, results) { + return res.json(processDates(results)); }); }); @@ -106,7 +126,7 @@ function configure (app, wares, ctx, env) { api.delete('/devicestatus/', ctx.authorization.isPermitted('api:devicestatus:delete'), delete_records); } - if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/ ) { + if (app.enabled('api')) { config_authed(app, api, wares, ctx); } diff --git a/lib/api/entries/index.js b/lib/api/entries/index.js index 0c8b8fc1ef7..5075df6eded 100644 --- a/lib/api/entries/index.js +++ b/lib/api/entries/index.js @@ -4,6 +4,7 @@ const _last = require('lodash/last'); const _isNil = require('lodash/isNil'); const _first = require('lodash/first'); const _includes = require('lodash/includes'); +const _ = require('lodash'); const moment = require('moment'); const consts = require('../../constants'); @@ -14,8 +15,7 @@ const expand = braces.expand; const ID_PATTERN = /^[a-f\d]{24}$/; function isId (value) { - //TODO: why did we need tht length check? - return value && ID_PATTERN.test(value) && value.length === 24; + return ID_PATTERN.test(value); } /** @@ -69,13 +69,13 @@ function configure (app, wares, ctx, env) { // if element has no data type, but we know what the type should be if (!data.type && opts.type) { // bless absence with known type + // TODO: this doesn't work, it assigns the query rather tham the type data.type = opts.type; } // Support date de-normalization for older clients if (env.settings.deNormalizeDates) { - // eslint-disable-next-line no-prototype-builtins - if (data.dateString && data.hasOwnProperty('utcOffset')) { + if (data.dateString && Object.prototype.hasOwnProperty.call(data, 'utcOffset')) { const d = moment(data.dateString).utcOffset(data.utcOffset); data.dateString = d.toISOString(true); delete data.utcOffset; @@ -106,9 +106,9 @@ function configure (app, wares, ctx, env) { return next(); } - console.log('CGM Entry request with If-Modified-Since: ', ifModifiedSince); + // console.log('CGM Entry request with If-Modified-Since: ', ifModifiedSince); - if (lastEntryDate.getTime() <= Date.parse(ifModifiedSince)) { + if (lastEntryDate && lastEntryDate.getTime() <= Date.parse(ifModifiedSince)) { console.log('Sending Not Modified'); res.status(304).send({ status: 304 @@ -315,6 +315,7 @@ function configure (app, wares, ctx, env) { function prepReqModel (req, model) { var type = model || 'sgv'; if (!req.query.find) { + req.query.find = { type: type }; @@ -408,7 +409,7 @@ function configure (app, wares, ctx, env) { // If "?count=" is present, use that number to decided how many to return. if (!query.count) { - query.count = 10; + query.count = consts.ENTRIES_DEFAULT_COUNT; } // bias towards entries, but allow expressing preference of storage layer var storage = req.params.echo || 'entries'; @@ -428,15 +429,56 @@ function configure (app, wares, ctx, env) { * db queries in a fairly regimented manner. * This middleware executes the query, assigning the payload to results on * `res.entries`. + * If the query can be served from data in the runtime ddata, use the cached + * data and don't query the database. */ function query_models (req, res, next) { var query = req.query; // If "?count=" is present, use that number to decided how many to return. if (!query.count) { - query.count = 10; + query.count = consts.ENTRIES_DEFAULT_COUNT; + } + + // Check if we can process the query using in-memory data only + let inMemoryPossible = true; + let typeQuery; + + if (query.find) { + Object.keys(query.find).forEach(function(key) { + if (key == 'type') { + typeQuery = query.find[key]["$eq"]; + if (!typeQuery) typeQuery = query.find.type; + } else { + inMemoryPossible = false; + } + }); + } + + let inMemoryCollection; + + if (typeQuery) { + inMemoryCollection= _.filter(ctx.cache.entries, function checkType (object) { + if (typeQuery == 'sgv') return 'sgv' in object; + if (typeQuery == 'mbg') return 'mbg' in object; + if (typeQuery == 'cal') return object.type === 'cal'; + return false; + }); + } else { + inMemoryCollection = ctx.cache.getData('entries'); + + inMemoryCollection = _.sortBy(inMemoryCollection, function(item) { + return item.mills; + }).reverse(); + } + + if (inMemoryPossible && query.count <= inMemoryCollection.length) { + res.entries = _.cloneDeep(_.take(inMemoryCollection,query.count)); + res.entries_err = null; + return next(); } + // If we get this far, query the database // bias to entries, but allow expressing a preference var storage = req.storage || ctx.entries; // perform the query @@ -476,7 +518,7 @@ function configure (app, wares, ctx, env) { } var query = req.query; if (!query.count) { - query.count = 10 + query.count = consts.ENTRIES_DEFAULT_COUNT; } // remove using the query req.model.remove(query, function(err, stat) { diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js new file mode 100644 index 00000000000..b44715b25eb --- /dev/null +++ b/lib/api/googlehome/index.js @@ -0,0 +1,57 @@ +'use strict'; + +var moment = require('moment'); + +function configure (app, wares, ctx, env) { + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + api.use(wares.bodyParser.json()); + + ctx.virtAsstBase.setupVirtAsstHandlers(ctx.googleHome); + + api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Google Home'); + var locale = req.body.queryResult.languageCode; + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } + + var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters.metric); + if (handler){ + var sbx = initializeSandbox(); + handler(function (title, response) { + res.json(ctx.googleHome.buildSpeechletResponse(response, false)); + next( ); + return; + }, req.body.queryResult.parameters, sbx); + } else { + res.json(ctx.googleHome.buildSpeechletResponse(translate('virtAsstUnknownIntentText'), true)); + next( ); + return; + } + }); + + ctx.virtAsstBase.setupMutualIntents(ctx.googleHome); + + function initializeSandbox() { + var sbx = require('../../sandbox')(); + sbx.serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + return sbx; + } + + return api; +} + +module.exports = configure; diff --git a/lib/api/index.js b/lib/api/index.js index 47a8a7bac3d..4b3d6a4fcb6 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -54,7 +54,7 @@ function create (env, ctx) { app.all('/notifications*', require('./notifications-api')(app, wares, ctx)); app.all('/activity*', require('./activity/')(app, wares, ctx)); - + app.use('/', wares.sendJSONStatus, require('./verifyauth')(ctx)); app.all('/food*', require('./food/')(app, wares, ctx)); @@ -65,6 +65,10 @@ function create (env, ctx) { app.all('/alexa*', require('./alexa/')(app, wares, ctx, env)); } + if (ctx.googleHome) { + app.all('/googlehome*', require('./googlehome/')(app, wares, ctx, env)); + } + return app; } diff --git a/lib/api/notifications-api.js b/lib/api/notifications-api.js index f9810256ad8..d08a03a7ead 100644 --- a/lib/api/notifications-api.js +++ b/lib/api/notifications-api.js @@ -1,5 +1,7 @@ 'use strict'; +var consts = require('../constants'); + function configure (app, wares, ctx) { var express = require('express') , api = express.Router( ) @@ -7,9 +9,9 @@ function configure (app, wares, ctx) { api.post('/notifications/pushovercallback', function (req, res) { if (ctx.pushnotify.pushoverAck(req.body)) { - res.sendStatus(200); + res.sendStatus(consts.HTTP_OK); } else { - res.sendStatus(500); + res.sendStatus(consts.HTTP_INTERNAL_ERROR); } }); @@ -21,7 +23,7 @@ function configure (app, wares, ctx) { var time = req.query.time && Number(req.query.time); console.info('got api ack, level: ', level, ', time: ', time, ', query: ', req.query); ctx.notifications.ack(level, group, time, true); - res.sendStatus(200); + res.sendStatus(consts.HTTP_OK); }); } diff --git a/lib/api/notifications-v2.js b/lib/api/notifications-v2.js new file mode 100644 index 00000000000..16eac1de975 --- /dev/null +++ b/lib/api/notifications-v2.js @@ -0,0 +1,23 @@ +'use strict'; + +var consts = require('../constants'); + +function configure (app, ctx) { + var express = require('express') + , api = express.Router( ) + ; + + api.post('/loop', ctx.authorization.isPermitted('notifications:loop:push'), function (req, res) { + ctx.loop.sendNotification(req.body, req.connection.remoteAddress, function (error) { + if (error) { + res.status(consts.HTTP_INTERNAL_ERROR).send(error) + console.log("error sending notification to Loop: ", error); + } else { + res.sendStatus(consts.HTTP_OK); + } + }); + }); + + return api; +} +module.exports = configure; diff --git a/lib/api/profile/index.js b/lib/api/profile/index.js index 30f8fe240ca..d00122f16bd 100644 --- a/lib/api/profile/index.js +++ b/lib/api/profile/index.js @@ -16,11 +16,37 @@ function configure (app, wares, ctx) { api.use(wares.bodyParser.urlencoded({ extended: true })); api.use(ctx.authorization.isPermitted('api:profile:read')); + + + /** + * @function query_models + * Perform the standard query logic, translating API parameters into mongo + * db queries in a fairly regimented manner. + * This middleware executes the query, returning the results as JSON + */ + function query_models (req, res, next) { + var query = req.query; + + // If "?count=" is present, use that number to decide how many to return. + if (!query.count) { + query.count = consts.PROFILES_DEFAULT_COUNT; + } + + // perform the query + ctx.profile.list_query(query, function payload(err, profiles) { + return res.json(profiles); + }); + } + + // List profiles available + api.get('/profiles/', query_models); + // List profiles available api.get('/profile/', function(req, res) { + const limit = req.query && req.query.count ? Number(req.query.count) : consts.PROFILES_DEFAULT_COUNT; ctx.profile.list(function (err, attribute) { return res.json(attribute); - }); + }, limit); }); // List current active record (in current state LAST record is current active) diff --git a/lib/api/properties.js b/lib/api/properties.js index 981f3c31328..7e9fd88ebab 100644 --- a/lib/api/properties.js +++ b/lib/api/properties.js @@ -42,6 +42,8 @@ function create (env, ctx) { result = _pick(sbx.properties, selected); } + result = env.settings.filteredSettings(result); + if (req.query && req.query.pretty) { res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(result, null, 2)); diff --git a/lib/api/root.js b/lib/api/root.js new file mode 100644 index 00000000000..275660eeae8 --- /dev/null +++ b/lib/api/root.js @@ -0,0 +1,23 @@ +'use strict'; + +function configure () { + const express = require('express') + , api = express.Router( ) + , apiConst = require('./const') + , api3Const = require('../api3/const') + ; + + api.get('/versions', function getVersion (req, res) { + + const versions = [ + { version: apiConst.API1_VERSION, url: '/api/v1' }, + { version: apiConst.API2_VERSION, url: '/api/v2' }, + { version: api3Const.API3_VERSION, url: '/api/v3' } + ]; + + res.json(versions); + }); + + return api; +} +module.exports = configure; diff --git a/lib/api/status.js b/lib/api/status.js index 9d18b524ec3..b630d629593 100644 --- a/lib/api/status.js +++ b/lib/api/status.js @@ -14,6 +14,12 @@ function configure (app, wares, env, ctx) { // Status badge/text/json api.get('/status', function (req, res) { + + let extended = env.settings.filteredSettings(app.extendedClientSettings); + let settings = env.settings.filteredSettings(env.settings); + + var authToken = req.query.token || req.query.secret || ''; + var date = new Date(); var info = { status: 'ok' , name: app.get('name') @@ -23,9 +29,9 @@ function configure (app, wares, env, ctx) { , apiEnabled: app.enabled('api') , careportalEnabled: app.enabled('api') && env.settings.enable.indexOf('careportal') > -1 , boluscalcEnabled: app.enabled('api') && env.settings.enable.indexOf('boluscalc') > -1 - , settings: env.settings - , extendedSettings: app.extendedClientSettings - , authorized: ctx.authorization.authorize(req.query.token || '') + , settings: settings + , extendedSettings: extended + , authorized: ctx.authorization.authorize(authToken) }; var badge = 'http://img.shields.io/badge/Nightscout-OK-green'; diff --git a/lib/api/treatments/index.js b/lib/api/treatments/index.js index 5d527fce6ac..0e1d8a0ede8 100644 --- a/lib/api/treatments/index.js +++ b/lib/api/treatments/index.js @@ -1,11 +1,12 @@ 'use strict'; -var _forEach = require('lodash/forEach'); -var _isNil = require('lodash/isNil'); -var _isArray = require('lodash/isArray'); +const _forEach = require('lodash/forEach'); +const _isNil = require('lodash/isNil'); +const _isArray = require('lodash/isArray'); +const _take = require('lodash/take'); -var consts = require('../../constants'); -var moment = require('moment'); +const constants = require('../../constants'); +const moment = require('moment'); function configure (app, wares, ctx, env) { var express = require('express') @@ -33,55 +34,73 @@ function configure (app, wares, ctx, env) { api.use(ctx.authorization.isPermitted('api:treatments:read')); - // List treatments available - api.get('/treatments', function(req, res) { - var ifModifiedSince = req.get('If-Modified-Since'); - ctx.treatments.list(req.query, function(err, results) { - var d1 = null; + function serveTreatments(req,res, err, results) { - const deNormalizeDates = env.settings.deNormalizeDates; + var ifModifiedSince = req.get('If-Modified-Since'); - _forEach(results, function clean (t) { - t.carbs = Number(t.carbs); - t.insulin = Number(t.insulin); + var d1 = null; - // eslint-disable-next-line no-prototype-builtins - if (deNormalizeDates && t.hasOwnProperty('utcOffset')) { - const d = moment(t.created_at).utcOffset(t.utcOffset); - t.created_at = d.toISOString(true); - delete t.utcOffset; - } + const deNormalizeDates = env.settings.deNormalizeDates; - var d2 = null; + _forEach(results, function clean (t) { + t.carbs = Number(t.carbs); + t.insulin = Number(t.insulin); - if (t.hasOwnProperty('created_at')) { - d2 = new Date(t.created_at); - } else { - if (t.hasOwnProperty('timestamp')) { - d2 = new Date(t.timestamp); - } - } + if (deNormalizeDates && Object.prototype.hasOwnProperty.call(t, 'utcOffset')) { + const d = moment(t.created_at).utcOffset(t.utcOffset); + t.created_at = d.toISOString(true); + delete t.utcOffset; + } - if (d2 == null) { return; } + var d2 = null; - if (d1 == null || d2.getTime() > d1.getTime()) { - d1 = d2; + if (Object.prototype.hasOwnProperty.call(t, 'created_at')) { + d2 = new Date(t.created_at); + } else { + if (Object.prototype.hasOwnProperty.call(t, 'timestamp')) { + d2 = new Date(t.timestamp); } - }); + } - if (!_isNil(d1)) res.setHeader('Last-Modified', d1.toUTCString()); + if (d2 == null) { return; } - if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) { - res.status(304).send({ - status: 304 - , message: 'Not modified' - , type: 'internal' - }); - return; - } else { - return res.json(results); + if (d1 == null || d2.getTime() > d1.getTime()) { + d1 = d2; } }); + + if (!_isNil(d1)) res.setHeader('Last-Modified', d1.toUTCString()); + + if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) { + res.status(304).send({ + status: 304 + , message: 'Not modified' + , type: 'internal' + }); + return; + } else { + return res.json(results); + } + } + + // List treatments available + api.get('/treatments', function(req, res) { + var query = req.query; + if (!query.count) { + // If there's a date search involved, default to a higher number of objects + query.count = query.find ? 1000 : 100; + } + + const inMemoryData = ctx.cache.treatments; + const canServeFromMemory = inMemoryData && inMemoryData.length >= query.count && Object.keys(query).length == 1 ? true : false; + + if (canServeFromMemory) { + serveTreatments(req, res, null, _take(inMemoryData,query.count)); + } else { + ctx.treatments.list(query, function(err, results) { + serveTreatments(req,res,err,results); + }); + } }); function config_authed (app, api, wares, ctx) { @@ -91,14 +110,37 @@ function configure (app, wares, ctx, env) { if (!_isArray(treatments)) { treatments = [treatments]; - }; + } + + for (let i = 0; i < treatments.length; i++) { + const t = treatments[i]; + + if (!t.created_at) { + t.created_at = new Date().toISOString(); + } + + /* + if (!t.created_at) { + console.log('Trying to create treatment without created_at field', t); + res.sendJSONStatus(res, constants.HTTP_VALIDATION_ERROR, 'Treatments must contain created_at'); + return; + } + const d = moment(t.created_at); + if (!d.isValid()) { + console.log('Trying to insert date with invalid created_at', t); + res.sendJSONStatus(res, constants.HTTP_VALIDATION_ERROR, 'Treatments created_at must be an ISO-8601 date'); + return; + } + */ + + } ctx.treatments.create(treatments, function(err, created) { if (err) { console.log('Error adding treatment', err); - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + res.sendJSONStatus(res, constants.HTTP_INTERNAL_ERROR, 'Mongo Error', err); } else { - console.log('Treatment created'); + console.log('REST API treatment created', created); res.json(created); } }); @@ -119,19 +161,16 @@ function configure (app, wares, ctx, env) { query.count = 10 } - console.log('Delete records with query: ', query); - // remove using the query ctx.treatments.remove(query, function(err, stat) { if (err) { console.log('treatments delete error: ', err); return next(err); } + // yield some information about success of operation res.json(stat); - console.log('treatments records deleted'); - return next(); }); } @@ -160,9 +199,8 @@ function configure (app, wares, ctx, env) { var data = req.body; ctx.treatments.save(data, function(err, created) { if (err) { - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); - console.log('Error saving treatment'); - console.log(err); + res.sendJSONStatus(res, constants.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + console.log('Error saving treatment', err); } else { res.json(created); console.log('Treatment saved', data); diff --git a/lib/api/verifyauth.js b/lib/api/verifyauth.js index b3c67433d5b..a4eb2edf4ee 100644 --- a/lib/api/verifyauth.js +++ b/lib/api/verifyauth.js @@ -10,11 +10,15 @@ function configure (ctx) { ctx.authorization.resolveWithRequest(req, function resolved (err, result) { // this is used to see if req has api-secret equivalent authorization - var authorized = !err && + var authorized = !err && ctx.authorization.checkMultiple('*:*:create,update,delete', result.shiros) && //can write to everything ctx.authorization.checkMultiple('admin:*:*:*', result.shiros); //full admin permissions too + var response = { + message: authorized ? 'OK' : 'UNAUTHORIZED', + rolefound: result.subject ? 'FOUND' : 'NOTFOUND' + } - res.sendJSONStatus(res, consts.HTTP_OK, authorized ? 'OK' : 'UNAUTHORIZED'); + res.sendJSONStatus(res, consts.HTTP_OK, response); }); }); @@ -22,4 +26,3 @@ function configure (ctx) { } module.exports = configure; - diff --git a/lib/api3/const.json b/lib/api3/const.json new file mode 100644 index 00000000000..fd874a1175a --- /dev/null +++ b/lib/api3/const.json @@ -0,0 +1,55 @@ +{ + "API3_VERSION": "3.0.0-alpha", + "API3_SECURITY_ENABLE": true, + "API3_TIME_SKEW_TOLERANCE": 5, + "API3_DEDUP_FALLBACK_ENABLED": true, + "API3_CREATED_AT_FALLBACK_ENABLED": true, + "API3_MAX_LIMIT": 1000, + + "HTTP": { + "OK": 200, + "CREATED": 201, + "NO_CONTENT": 204, + "NOT_MODIFIED": 304, + "BAD_REQUEST": 400, + "UNAUTHORIZED": 401, + "FORBIDDEN": 403, + "NOT_FOUND": 404, + "NOT_ACCEPTABLE": 406, + "GONE": 410, + "PRECONDITION_FAILED": 412, + "UNPROCESSABLE_ENTITY": 422, + "INTERNAL_ERROR": 500 + }, + + "MSG": { + "HTTP_400_BAD_LAST_MODIFIED": "Bad or missing Last-Modified header/parameter", + "HTTP_400_BAD_LIMIT": "Parameter limit out of tolerance", + "HTTP_400_BAD_REQUEST_BODY": "Bad or missing request body", + "HTTP_400_BAD_FIELD_IDENTIFIER": "Bad or missing identifier field", + "HTTP_400_BAD_FIELD_DATE": "Bad or missing date field", + "HTTP_400_BAD_FIELD_UTC": "Bad or missing utcOffset field", + "HTTP_400_BAD_FIELD_APP": "Bad or missing app field", + "HTTP_400_BAD_SKIP": "Parameter skip out of tolerance", + "HTTP_400_SORT_SORT_DESC": "Parameters sort and sort_desc cannot be combined", + "HTTP_400_UNSUPPORTED_FILTER_OPERATOR": "Unsupported filter operator {0}", + "HTTP_400_IMMUTABLE_FIELD": "Field {0} cannot be modified by the client", + "HTTP_401_BAD_DATE": "Bad Date header", + "HTTP_401_BAD_TOKEN": "Bad access token or JWT", + "HTTP_401_DATE_OUT_OF_TOLERANCE": "Date header out of tolerance", + "HTTP_401_MISSING_DATE": "Missing Date header", + "HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT", + "HTTP_403_MISSING_PERMISSION": "Missing permission {0}", + "HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS", + "HTTP_406_UNSUPPORTED_FORMAT": "Unsupported output format requested", + "HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document", + "HTTP_500_INTERNAL_ERROR": "Internal Server Error", + "STORAGE_ERROR": "Database error", + "SOCKET_MISSING_OR_BAD_ACCESS_TOKEN": "Missing or bad accessToken", + "SOCKET_UNAUTHORIZED_TO_ANY": "Unauthorized to receive any collection" + }, + + "MIN_TIMESTAMP": 946684800000, + "MIN_UTC_OFFSET": -1440, + "MAX_UTC_OFFSET": 1440 +} \ No newline at end of file diff --git a/lib/api3/doc/formats.md b/lib/api3/doc/formats.md new file mode 100644 index 00000000000..5a01a802f3e --- /dev/null +++ b/lib/api3/doc/formats.md @@ -0,0 +1,88 @@ +# APIv3: Output formats + +### Choosing output format +In APIv3, the standard content type is JSON for both HTTP request and HTTP response. +However, in HTTP response, the response content type can be changed to XML or CSV +for READ, SEARCH, and HISTORY operations. + +The response content type can be requested in one of the following ways: +- add a file type extension to the URL, eg. + `/api/v3/entries.csv?...` + or `/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895.xml?...` +- set `Accept` HTTP request header to `text/csv` or `application/xml` + +The server replies with `406 Not Acceptable` HTTP status in case of not supported content type. + + +### JSON + +Default content type is JSON, output can look like this: + +``` +[ + { + "type":"sgv", + "sgv":"171", + "dateString":"2014-07-19T02:44:15.000-07:00", + "date":1405763055000, + "device":"dexcom", + "direction":"Flat", + "identifier":"5c5a2404e0196f4d3d9a718a", + "srvModified":1405763055000, + "srvCreated":1405763055000 + }, + { + "type":"sgv", + "sgv":"176", + "dateString":"2014-07-19T03:09:15.000-07:00", + "date":1405764555000, + "device":"dexcom", + "direction":"Flat", + "identifier":"5c5a2404e0196f4d3d9a7187", + "srvModified":1405764555000, + "srvCreated":1405764555000 + } +] +``` + +### XML + +Sample output: + +``` + + + + sgv + 171 + 2014-07-19T02:44:15.000-07:00 + 1405763055000 + dexcom + Flat + 5c5a2404e0196f4d3d9a718a + 1405763055000 + 1405763055000 + + + sgv + 176 + 2014-07-19T03:09:15.000-07:00 + 1405764555000 + dexcom + Flat + 5c5a2404e0196f4d3d9a7187 + 1405764555000 + 1405764555000 + + +``` + +### CSV + +Sample output: + +``` +type,sgv,dateString,date,device,direction,identifier,srvModified,srvCreated +sgv,171,2014-07-19T02:44:15.000-07:00,1405763055000,dexcom,Flat,5c5a2404e0196f4d3d9a718a,1405763055000,1405763055000 +sgv,176,2014-07-19T03:09:15.000-07:00,1405764555000,dexcom,Flat,5c5a2404e0196f4d3d9a7187,1405764555000,1405764555000 +``` diff --git a/lib/api3/doc/security.md b/lib/api3/doc/security.md new file mode 100644 index 00000000000..0fdf4c7d2aa --- /dev/null +++ b/lib/api3/doc/security.md @@ -0,0 +1,48 @@ +# APIv3: Security + +### Enforcing HTTPS +APIv3 is ment to run only under SSL version of HTTP protocol, which provides: +- **message secrecy** - once the secure channel between client and server is closed the communication cannot be eavesdropped by any third party +- **message consistency** - each request/response is protected against modification by any third party (any forgery would be detected) +- **authenticity of identities** - once the client and server establish the secured channel, it is guaranteed that the identity of the client or server does not change during the whole session + +HTTPS (in use with APIv3) does not address the true identity of the client, but ensures the correct identity of the server. Furthermore, HTTPS does not prevent the resending of previously intercepted encrypted messages by an attacker. + + +--- +### Authentication and authorization +In APIv3, *API_SECRET* can no longer be used for authentication or authorization. Instead, a roles/permissions security model is used, which is managed in the *Admin tools* section of the web application. + + +The identity of the client is represented by the *subject* to whom the access level is set by assigning security *roles*. One or more *permissions* can be assigned to each role. Permissions are used in an [Apache Shiro-like style](http://shiro.apache.org/permissions.html "Apache Shiro-like style"). + + +For each security *subject*, the system automatically generates an *access token* that is difficult to guess since it is derived from the secret *API_SECRET*. The *access token* must be included in every secured API operation to decode the client's identity and determine its authorization level. In this way, it is then possible to resolve whether the client has the permission required by a particular API operation. + + +There are two ways to authorize API calls: +- use `token` query parameter to pass the *access token*, eg. `token=testreadab-76eaff2418bfb7e0` +- use so-called [JSON Web Tokens](https://jwt.io "JSON Web Tokens") + - at first let the `/api/v2/authorization/request` generates you a particular JWT, eg. `GET https://nsapiv3.herokuapp.com/api/v2/authorization/request/testreadab-76eaff2418bfb7e0` + - then, to each secure API operation attach a JWT token in the HTTP header, eg. `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NUb2tlbiI6InRlc3RyZWFkYWItNzZlYWZmMjQxOGJmYjdlMCIsImlhdCI6MTU2NTAzOTczMSwiZXhwIjoxNTY1MDQzMzMxfQ.Y-OFtFJ-gZNJcnZfm9r4S7085Z7YKVPiaQxuMMnraVk` (until the JWT expires) + + + +--- +### Client timestamps +As previously mentioned, a potential attacker cannot decrypt the captured messages, but he can send them back to the client/server at any later time. APIv3 is partially preventing this by the temporal validity of each secured API call. + + +The client must include his current timestamp to each call so that the server can compare it against its clock. If the timestamp difference is not within the limit, the request is considered invalid. The tolerance limit is set in minutes in the `API3_TIME_SKEW_TOLERANCE` environment variable. + +There are two ways to include the client timestamp to the call: +- use `now` query parameter with UNIX epoch millisecond timestamp, eg. `now=1565041446908` +- add HTTP `Date` header to the request, eg. `Date: Sun, 12 May 2019 07:49:58 GMT` + + +The client can check each server response in the same way, because each response contains a server timestamp in the HTTP *Date* header as well. + + +--- +APIv3 security is enabled by default, but it can be completely disabled for development and debugging purposes by setting the web environment variable `API3_SECURITY_ENABLE=false`. +This setting is hazardous and it is strongly discouraged to be used for production purposes! diff --git a/lib/api3/doc/socket.md b/lib/api3/doc/socket.md new file mode 100644 index 00000000000..802a85e0235 --- /dev/null +++ b/lib/api3/doc/socket.md @@ -0,0 +1,142 @@ +# APIv3: Socket.IO storage modifications channel + +APIv3 has the ability to broadcast events about all created, edited and deleted documents, using Socket.IO library. + +This provides a real-time data exchange experience in combination with API REST operations. + +### Complete sample client code +```html + + + + + + + + APIv3 Socket.IO sample + + + + + + + + + + +``` + +**Important notice: Only changes made via APIv3 are being broadcasted. All direct database or APIv1 modifications are not included by this channel.** + +### Subscription (authorization) +The client must first subscribe to the channel that is exposed at `storage` namespace, ie the `/storage` subadress of the base Nightscout's web address (without `/api/v3` subaddress). +```javascript +const socket = io('https://nsapiv3.herokuapp.com/storage'); +``` + + +Subscription is requested by emitting `subscribe` event to the server, while including document with parameters: +* `accessToken`: required valid accessToken of the security subject, which has been prepared in *Admin Tools* of Nightscout. +* `collections`: optional array of collections which the client wants to subscribe to, by default all collections are requested) + +```javascript +socket.on('connect', function () { + socket.emit('subscribe', { + accessToken: 'testadmin-ad3b1f9d7b3f59d5', + collections: [ 'entries', 'treatments' ] + }, +``` + + +On the server, the subject is first identified and authenticated (by the accessToken) and then a verification takes place, if the subject has read access to each required collection. + +An exception is the `settings` collection for which `api:settings:admin` permission is required, for all other collections `api::read` permission is required. + + +If the authentication was successful and the client has read access to at least one collection, `success` = `true` is set in the response object and the field `collections` contains an array of collections which were actually subscribed (granted). +In other case `success` = `false` is set in the response object and the field `message` contains an error message. + +```javascript +function (data) { + if (data.success) { + console.log('subscribed for collections', data.collections); + } + else { + console.error(data.message); + } + }); +}); +``` + +### Receiving events +After the successful subscription the client can start listening to `create`, `update` and/or `delete` events of the socket. + + +##### create +`create` event fires each time a new document is inserted into the storage, regardless of whether it was CREATE or UPDATE operation of APIv3 (both of these operations are upserting/deduplicating, so they are "insert capable"). If the document already existed in the storage, the `update` event would be fired instead. + +The received object contains: +* `colName` field with the name of the affected collection +* the inserted document in `doc` field + +```javascript +socket.on('create', function (data) { + console.log(`${data.colName}:created document`, data.doc); +}); +``` + + +##### update +`update` event fires each time an existing document is modified in the storage, regardless of whether it was CREATE, UPDATE or PATCH operation of APIv3 (all of these operations are "update capable"). If the document did not yet exist in the storage, the `create` event would be fired instead. + +The received object contains: +* `colName` field with the name of the affected collection +* the new version of the modified document in `doc` field + +```javascript +socket.on('update', function (data) { + console.log(`${data.colName}:updated document`, data.doc); +}); +``` + + +##### delete +`delete` event fires each time an existing document is deleted in the storage, regardless of whether it was "soft" (marking as invalid) or permanent deleting. + +The received object contains: +* `colName` field with the name of the affected collection +* the identifier of the deleted document in the `identifier` field + +```javascript +socket.on('delete', function (data) { + console.log(`${data.colName}:deleted document with identifier`, data.identifier); +}); +``` \ No newline at end of file diff --git a/lib/api3/doc/tutorial.md b/lib/api3/doc/tutorial.md new file mode 100644 index 00000000000..3d8c656dfbd --- /dev/null +++ b/lib/api3/doc/tutorial.md @@ -0,0 +1,329 @@ +# APIv3: Basics tutorial + +Nightscout API v3 is a component of [cgm-remote-monitor](https://github.com/nightscout/cgm-remote-monitor) project. +It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange. + +There is a list of REST operations that the API v3 offers (inside `/api/v3` relative URL namespace), we will briefly introduce them in this file. + +Each NS instance with API v3 contains self-included OpenAPI specification at [/api/v3/swagger-ui-dist/](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/) relative URL. + + +--- +### VERSION + +[VERSION](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/#/other/get_version) operation gets you basic information about software packages versions. +It is public (there is no need to add authorization parameters/headers). + +Sample GET `/version` client code (to get actual versions): +```javascript +const request = require('request'); + +request('https://nsapiv3.herokuapp.com/api/v3/version', + (error, response, body) => console.log(body)); +``` +Sample result: +```javascript +{ + "version":"0.12.2", + "apiVersion":"3.0.0-alpha", + "srvDate":1564386001772, + "storage":{ + "storage":"mongodb", + "version":"3.6.12" + } +} +``` + + +--- +### STATUS + +[STATUS](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/#/other/get_status) operation gets you basic information about software packages versions. +It is public (there is no need to add authorization parameters/headers). + +Sample GET `/status` client code (to get my actual permissions): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; + +request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`, + (error, response, body) => console.log(body)); +``` +Sample result: +```javascript +{ + "version":"0.12.2", + "apiVersion":"3.0.0-alpha", + "srvDate":1564391740738, + "storage":{ + "storage":"mongodb", + "version":"3.6.12" + }, + "apiPermissions":{ + "devicestatus":"crud", + "entries":"crud", + "food":"crud", + "profile":"crud", + "settings":"crud", + "treatments":"crud" + } +} +``` +`"crud"` represents create + read + update + delete permissions for the collection. + + +--- +### SEARCH + +[SEARCH](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/SEARCH) operation filters, sorts, paginates and projects documents from the collection. + +Sample GET `/entries` client code (to retrieve last 3 BG values): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; + +request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&limit=3&fields=dateString,sgv,direction`, + (error, response, body) => console.log(body)); +``` +Sample result: +``` +[ + { + "dateString":"2019-07-30T02:24:50.434+0200", + "sgv":115, + "direction":"FortyFiveDown" + }, + { + "dateString":"2019-07-30T02:19:50.374+0200", + "sgv":121, + "direction":"FortyFiveDown" + }, + { + "dateString":"2019-07-30T02:14:50.450+0200", + "sgv":129, + "direction":"FortyFiveDown" + } +] +``` + + +--- +### CREATE + +[CREATE](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/#/generic/post__collection_) operation inserts a new document into the collection. + +Sample POST `/treatments` client code: +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const doc = { + date: 1564591511232, // (new Date()).getTime(), + app: 'AndroidAPS', + device: 'Samsung XCover 4-861536030196001', + eventType: 'Correction Bolus', + insulin: 0.3 +}; +request({ + method: 'post', + body: doc, + json: true, + url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}` + }, + (error, response, body) => console.log(response.headers.location)); +``` +Sample result: +``` +/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895 +``` + + +--- +### READ + +[READ](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/#/generic/get__collection___identifier_) operation retrieves you a single document from the collection by its identifier. + +Sample GET `/treatments/{identifier}` client code: +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; + +request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`, + (error, response, body) => console.log(body)); +``` +Sample result: +``` +{ + "date":1564591511232, + "app":"AndroidAPS", + "device":"Samsung XCover 4-861536030196001", + "eventType":"Correction Bolus", + "insulin":0.3, + "identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895", + "utcOffset":0, + "created_at":"2019-07-31T16:45:11.232Z", + "srvModified":1564591627732, + "srvCreated":1564591511711, + "subject":"test-admin" +} +``` + + +--- +### LAST MODIFIED + +[LAST MODIFIED](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/other/LAST-MODIFIED) operation finds the date of last modification for each collection. + +Sample GET `/lastModified` client code (to get latest modification dates): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; + +request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`, + (error, response, body) => console.log(body)); +``` +Sample result: +```javascript +{ + "srvDate":1564591783202, + "collections":{ + "devicestatus":1564591490074, + "entries":1564591486801, + "profile":1548524042744, + "treatments":1564591627732 + } +} +``` + + +--- +### UPDATE + +[UPDATE](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/put__collection___identifier_) operation updates existing document in the collection. + +Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; +const doc = { + date: 1564591511232, + app: 'AndroidAPS', + device: 'Samsung XCover 4-861536030196001', + eventType: 'Correction Bolus', + insulin: 0.4 +}; + +request({ + method: 'put', + body: doc, + json: true, + url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` + }, + (error, response, body) => console.log(response.statusCode)); +``` +Sample result: +``` +204 +``` + + +--- +### PATCH + +[PATCH](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/patch__collection___identifier_) operation partially updates existing document in the collection. + +Sample PATCH `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; +const doc = { + insulin: 0.5 +}; + +request({ + method: 'patch', + body: doc, + json: true, + url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` + }, + (error, response, body) => console.log(response.statusCode)); +``` +Sample result: +``` +204 +``` + + +--- +### DELETE + +[DELETE](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/delete__collection___identifier_) operation deletes existing document from the collection. + +Sample DELETE `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5): +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895'; + +request({ + method: 'delete', + url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}` + }, + (error, response, body) => console.log(response.statusCode)); +``` +Sample result: +``` +204 +``` + + +--- +### HISTORY + +[HISTORY](https://nsapiv3insecure.herokuapp.com/api/v3/swagger-ui-dist/index.html#/generic/HISTORY2) operation queries all changes since the timestamp. + +Sample HISTORY `/treatments/history/{lastModified}` client code: +```javascript +const request = require('request'); +const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`; +const lastModified = 1564521267421; + +request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}?${auth}`, + (error, response, body) => console.log(response.body)); +``` +Sample result: +``` +[ + { + "date":1564521267421, + "app":"AndroidAPS", + "device":"Samsung XCover 4-861536030196001", + "eventType":"Correction Bolus", + "insulin":0.5, + "utcOffset":0, + "created_at":"2019-07-30T21:14:27.421Z", + "identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895", + "srvModified":1564592440416, + "srvCreated":1564592334853, + "subject":"test-admin", + "modifiedBy":"test-admin", + "isValid":false + }, + { + "date":1564592545299, + "app":"AndroidAPS", + "device":"Samsung XCover 4-861536030196001", + "eventType":"Snack Bolus", + "carbs":10, + "identifier":"267c43c2-f629-5191-a542-4f410c69e486", + "utcOffset":0, + "created_at":"2019-07-31T17:02:25.299Z", + "srvModified":1564592545781, + "srvCreated":1564592545781, + "subject":"test-admin" + } +] +``` +Notice the `"isValid":false` field marking the deletion of the document. \ No newline at end of file diff --git a/lib/api3/generic/collection.js b/lib/api3/generic/collection.js new file mode 100644 index 00000000000..0a1a29b3915 --- /dev/null +++ b/lib/api3/generic/collection.js @@ -0,0 +1,193 @@ +'use strict'; + +const apiConst = require('../const.json') + , _ = require('lodash') + , dateTools = require('../shared/dateTools') + , opTools = require('../shared/operationTools') + , stringTools = require('../shared/stringTools') + , CollectionStorage = require('../storage/mongoCollection') + , searchOperation = require('./search/operation') + , createOperation = require('./create/operation') + , readOperation = require('./read/operation') + , updateOperation = require('./update/operation') + , patchOperation = require('./patch/operation') + , deleteOperation = require('./delete/operation') + , historyOperation = require('./history/operation') + ; + +/** + * Generic collection (abstraction over each collection specifics) + * @param {string} colName - name of the collection inside the storage system + * @param {function} fallbackGetDate - function that tries to create srvModified virtually from other fields of document + * @param {Array} dedupFallbackFields - fields that all need to be matched to identify document via fallback deduplication + * @param {function} fallbackHistoryFilter - function that creates storage filter for all newer records (than the timestamp from first function parameter) + */ +function Collection ({ ctx, env, app, colName, storageColName, fallbackGetDate, dedupFallbackFields, + fallbackDateField }) { + + const self = this; + + self.colName = colName; + self.fallbackGetDate = fallbackGetDate; + self.dedupFallbackFields = app.get('API3_DEDUP_FALLBACK_ENABLED') ? dedupFallbackFields : []; + self.autoPruneDays = app.setENVTruthy('API3_AUTOPRUNE_' + colName.toUpperCase()); + self.nextAutoPrune = new Date(); + self.storage = new CollectionStorage(ctx, env, storageColName); + self.fallbackDateField = fallbackDateField; + + self.mapRoutes = function mapRoutes () { + const prefix = '/' + colName + , prefixId = prefix + '/:identifier' + , prefixHistory = prefix + '/history' + ; + + + // GET /{collection} + app.get(prefix, searchOperation(ctx, env, app, self)); + + // POST /{collection} + app.post(prefix, createOperation(ctx, env, app, self)); + + // GET /{collection}/history + app.get(prefixHistory, historyOperation(ctx, env, app, self)); + + // GET /{collection}/history + app.get(prefixHistory + '/:lastModified', historyOperation(ctx, env, app, self)); + + // GET /{collection}/{identifier} + app.get(prefixId, readOperation(ctx, env, app, self)); + + // PUT /{collection}/{identifier} + app.put(prefixId, updateOperation(ctx, env, app, self)); + + // PATCH /{collection}/{identifier} + app.patch(prefixId, patchOperation(ctx, env, app, self)); + + // DELETE /{collection}/{identifier} + app.delete(prefixId, deleteOperation(ctx, env, app, self)); + }; + + + /** + * Parse limit (max document count) from query string + */ + self.parseLimit = function parseLimit (req, res) { + const maxLimit = app.get('API3_MAX_LIMIT'); + let limit = maxLimit; + + if (req.query.limit) { + if (!isNaN(req.query.limit) && req.query.limit > 0 && req.query.limit <= maxLimit) { + limit = parseInt(req.query.limit); + } + else { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LIMIT); + return null; + } + } + + return limit; + }; + + + + /** + * Fetch modified date from document (with possible fallback and back-fill to srvModified/srvCreated) + * @param {Object} doc - document loaded from database + */ + self.resolveDates = function resolveDates (doc) { + let modifiedDate; + try { + if (doc.srvModified) { + modifiedDate = new Date(doc.srvModified); + } + else { + if (typeof (self.fallbackGetDate) === 'function') { + modifiedDate = self.fallbackGetDate(doc); + if (modifiedDate) { + doc.srvModified = modifiedDate.getTime(); + } + } + } + + if (doc.srvModified && !doc.srvCreated) { + doc.srvCreated = modifiedDate.getTime(); + } + } + catch (error) { + console.warn(error); + } + return modifiedDate; + }; + + + /** + * Deletes old documents from the collection if enabled (for this collection) + * in the background (asynchronously) + * */ + self.autoPrune = function autoPrune () { + + if (!stringTools.isNumberInString(self.autoPruneDays)) + return; + + const autoPruneDays = parseFloat(self.autoPruneDays); + if (autoPruneDays <= 0) + return; + + if (new Date() > self.nextAutoPrune) { + + const deleteBefore = new Date(new Date().getTime() - (autoPruneDays * 24 * 3600 * 1000)); + + const filter = [ + { field: 'srvCreated', operator: 'lt', value: deleteBefore.getTime() }, + { field: 'created_at', operator: 'lt', value: deleteBefore.toISOString() }, + { field: 'date', operator: 'lt', value: deleteBefore.getTime() } + ]; + + // let's autoprune asynchronously (we won't wait for the result) + self.storage.deleteManyOr(filter, function deleteDone (err, result) { + if (err || !result) { + console.error(err); + } + + if (result.deleted) { + console.info('Auto-pruned ' + result.deleted + ' documents from ' + self.colName + ' collection '); + } + }); + + self.nextAutoPrune = new Date(new Date().getTime() + (3600 * 1000)); + } + }; + + + /** + * Parse date and utcOffset + optional created_at fallback + * @param {Object} doc + */ + self.parseDate = function parseDate (doc) { + if (!_.isEmpty(doc)) { + + let values = app.get('API3_CREATED_AT_FALLBACK_ENABLED') + ? [doc.date, doc.created_at] + : [doc.date]; + + let m = dateTools.parseToMoment(values); + if (m && m.isValid()) { + doc.date = m.valueOf(); + + if (typeof doc.utcOffset === 'undefined') { + doc.utcOffset = m.utcOffset(); + } + + if (app.get('API3_CREATED_AT_FALLBACK_ENABLED')) { + doc.created_at = m.toISOString(); + } + else { + if (doc.created_at) + delete doc.created_at; + } + } + } + } +} + +module.exports = Collection; \ No newline at end of file diff --git a/lib/api3/generic/create/insert.js b/lib/api3/generic/create/insert.js new file mode 100644 index 00000000000..b643818569a --- /dev/null +++ b/lib/api3/generic/create/insert.js @@ -0,0 +1,46 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + , path = require('path') + ; + +/** + * Insert new document into the collection + * @param {Object} opCtx + * @param {Object} doc + */ +async function insert (opCtx, doc) { + + const { ctx, auth, col, req, res } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:create`); + + if (validate(opCtx, doc) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + doc.srvCreated = doc.srvModified; + + if (auth && auth.subject && auth.subject.name) { + doc.subject = auth.subject.name; + } + + const identifier = await col.storage.insertOne(doc); + + if (!identifier) + throw new Error('empty identifier'); + + res.setHeader('Last-Modified', now.toUTCString()); + res.setHeader('Location', path.posix.join(req.baseUrl, req.path, identifier)); + res.status(apiConst.HTTP.CREATED).send({ }); + + ctx.bus.emit('storage-socket-create', { colName: col.colName, doc }); + col.autoPrune(); + ctx.bus.emit('data-received'); +} + + +module.exports = insert; \ No newline at end of file diff --git a/lib/api3/generic/create/operation.js b/lib/api3/generic/create/operation.js new file mode 100644 index 00000000000..39986a87ebd --- /dev/null +++ b/lib/api3/generic/create/operation.js @@ -0,0 +1,63 @@ +'use strict'; + +const _ = require('lodash') + , apiConst = require('../../const.json') + , security = require('../../security') + , insert = require('./insert') + , replace = require('../update/replace') + , opTools = require('../../shared/operationTools') + ; + + +/** + * CREATE: Inserts a new document into the collection + */ +async function create (opCtx) { + + const { col, req, res } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + col.parseDate(doc); + opTools.resolveIdentifier(doc); + const identifyingFilter = col.storage.identifyingFilter(doc.identifier, doc, col.dedupFallbackFields); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('empty result'); + + if (result.length > 0) { + const storageDoc = result[0]; + await replace(opCtx, doc, storageDoc, { isDeduplication: true }); + } + else { + await insert(opCtx, doc); + } +} + + +function createOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await create(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = createOperation; \ No newline at end of file diff --git a/lib/api3/generic/create/validate.js b/lib/api3/generic/create/validate.js new file mode 100644 index 00000000000..e978a3955e5 --- /dev/null +++ b/lib/api3/generic/create/validate.js @@ -0,0 +1,26 @@ +'use strict'; + +const apiConst = require('../../const.json') + , stringTools = require('../../shared/stringTools') + , opTools = require('../../shared/operationTools') + ; + + +/** + * Validation of document to create + * @param {Object} opCtx + * @param {Object} doc + * @returns string with error message if validation fails, true in case of success + */ +function validate (opCtx, doc) { + + const { res } = opCtx; + + if (typeof(doc.identifier) !== 'string' || stringTools.isNullOrWhitespace(doc.identifier)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_IDENTIFIER); + } + + return opTools.validateCommon(doc, res); +} + +module.exports = validate; \ No newline at end of file diff --git a/lib/api3/generic/delete/operation.js b/lib/api3/generic/delete/operation.js new file mode 100644 index 00000000000..8f9463f3ef9 --- /dev/null +++ b/lib/api3/generic/delete/operation.js @@ -0,0 +1,122 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + ; + +/** + * DELETE: Deletes a document from the collection + */ +async function doDelete (opCtx) { + + const { col, req } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:delete`); + + if (await validateDelete(opCtx) !== true) + return; + + if (req.query.permanent && req.query.permanent === "true") { + await deletePermanently(opCtx); + } else { + await markAsDeleted(opCtx); + } +} + + +async function validateDelete (opCtx) { + + const { col, req, res } = opCtx; + + const identifier = req.params.identifier; + const result = await col.storage.findOne(identifier); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } + else { + const storageDoc = result[0]; + + if (storageDoc.isReadOnly === true || storageDoc.readOnly === true || storageDoc.readonly === true) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, + apiConst.MSG.HTTP_422_READONLY_MODIFICATION); + } + } + + return true; +} + + +async function deletePermanently (opCtx) { + + const { ctx, col, req, res } = opCtx; + + const identifier = req.params.identifier; + const result = await col.storage.deleteOne(identifier); + + if (!result) + throw new Error('empty result'); + + if (!result.deleted) { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } + + col.autoPrune(); + ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier }); + ctx.bus.emit('data-received'); + return res.status(apiConst.HTTP.NO_CONTENT).end(); +} + + +async function markAsDeleted (opCtx) { + + const { ctx, col, req, res, auth } = opCtx; + + const identifier = req.params.identifier; + const setFields = { 'isValid': false, 'srvModified': (new Date).getTime() }; + + if (auth && auth.subject && auth.subject.name) { + setFields.modifiedBy = auth.subject.name; + } + + const result = await col.storage.updateOne(identifier, setFields); + + if (!result) + throw new Error('empty result'); + + if (!result.updated) { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } + + ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier }); + col.autoPrune(); + ctx.bus.emit('data-received'); + return res.status(apiConst.HTTP.NO_CONTENT).end(); +} + + +function deleteOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await doDelete(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = deleteOperation; \ No newline at end of file diff --git a/lib/api3/generic/history/operation.js b/lib/api3/generic/history/operation.js new file mode 100644 index 00000000000..5151c8b2749 --- /dev/null +++ b/lib/api3/generic/history/operation.js @@ -0,0 +1,153 @@ +'use strict'; + +const dateTools = require('../../shared/dateTools') + , renderer = require('../../shared/renderer') + , apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , FieldsProjector = require('../../shared/fieldsProjector') + , _ = require('lodash') + ; + +/** + * HISTORY: Retrieves incremental changes since timestamp + */ +async function history (opCtx, fieldsProjector) { + + const { req, res, col } = opCtx; + + let filter = parseFilter(opCtx) + , limit = col.parseLimit(req, res) + , projection = fieldsProjector.storageProjection() + , sort = prepareSort() + , skip = 0 + , onlyValid = false + , logicalOperator = 'or' + ; + + if (filter !== null && limit !== null && projection !== null) { + + const result = await col.storage.findMany(filter + , sort + , limit + , skip + , projection + , onlyValid + , logicalOperator); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + return res.status(apiConst.HTTP.NO_CONTENT).end(); + } + + _.each(result, col.resolveDates); + + const srvModifiedValues = _.map(result, function mapSrvModified (item) { + return item.srvModified; + }) + , maxSrvModified = _.max(srvModifiedValues); + + res.setHeader('Last-Modified', (new Date(maxSrvModified)).toUTCString()); + res.setHeader('ETag', 'W/"' + maxSrvModified + '"'); + + _.each(result, fieldsProjector.applyProjection); + + res.status(apiConst.HTTP.OK); + renderer.render(res, result); + } +} + + +/** + * Parse history filtering criteria from Last-Modified header + */ +function parseFilter (opCtx) { + + const { req, res } = opCtx; + + let lastModified = null + , lastModifiedParam = req.params.lastModified + , operator = null; + + if (lastModifiedParam) { + + // using param in URL as a source of timestamp + const m = dateTools.parseToMoment(lastModifiedParam); + + if (m === null || !m.isValid()) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + + lastModified = m.toDate(); + operator = 'gt'; + } + else { + // using request HTTP header as a source of timestamp + const lastModifiedHeader = req.get('Last-Modified'); + if (!lastModifiedHeader) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + + try { + lastModified = dateTools.floorSeconds(new Date(lastModifiedHeader)); + } catch (err) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + operator = 'gte'; + } + + return [ + { field: 'srvModified', operator: operator, value: lastModified.getTime() }, + { field: 'created_at', operator: operator, value: lastModified.toISOString() }, + { field: 'date', operator: operator, value: lastModified.getTime() } + ]; +} + + + +/** + * Prepare sorting for storage query + */ +function prepareSort () { + return { + srvModified: 1, + created_at: 1, + date: 1 + }; +} + + +function historyOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + if (col.colName === 'settings') { + await security.demandPermission(opCtx, `api:${col.colName}:admin`); + } else { + await security.demandPermission(opCtx, `api:${col.colName}:read`); + } + + const fieldsProjector = new FieldsProjector(req.query.fields); + + await history(opCtx, fieldsProjector); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = historyOperation; \ No newline at end of file diff --git a/lib/api3/generic/patch/operation.js b/lib/api3/generic/patch/operation.js new file mode 100644 index 00000000000..d7bb5fc2b4d --- /dev/null +++ b/lib/api3/generic/patch/operation.js @@ -0,0 +1,118 @@ +'use strict'; + +const _ = require('lodash') + , apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + , opTools = require('../../shared/operationTools') + , dateTools = require('../../shared/dateTools') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + +/** + * PATCH: Partially updates document in the collection + */ +async function patch (opCtx) { + + const { req, res, col } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + await security.demandPermission(opCtx, `api:${col.colName}:update`); + + col.parseDate(doc); + const identifier = req.params.identifier + , identifyingFilter = col.storage.identifyingFilter(identifier); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('result empty'); + + if (result.length > 0) { + + const storageDoc = result[0]; + if (storageDoc.isValid === false) { + return res.status(apiConst.HTTP.GONE).end(); + } + + const modifiedDate = col.resolveDates(storageDoc) + , ifUnmodifiedSince = req.get('If-Unmodified-Since'); + + if (ifUnmodifiedSince + && dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) { + return res.status(apiConst.HTTP.PRECONDITION_FAILED).end(); + } + + await applyPatch(opCtx, identifier, doc, storageDoc); + } + else { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } +} + + +/** + * Patch existing document in the collection + * @param {Object} opCtx + * @param {string} identifier + * @param {Object} doc - fields and values to patch + * @param {Object} storageDoc - original (database) version of document + */ +async function applyPatch (opCtx, identifier, doc, storageDoc) { + + const { ctx, res, col, auth } = opCtx; + + if (validate(opCtx, doc, storageDoc) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + + if (auth && auth.subject && auth.subject.name) { + doc.modifiedBy = auth.subject.name; + } + + const matchedCount = await col.storage.updateOne(identifier, doc); + + if (!matchedCount) + throw new Error('matchedCount empty'); + + res.setHeader('Last-Modified', now.toUTCString()); + res.status(apiConst.HTTP.NO_CONTENT).send({ }); + + const fieldsProjector = new FieldsProjector('_all'); + const patchedDocs = await col.storage.findOne(identifier, fieldsProjector); + const patchedDoc = patchedDocs[0]; + fieldsProjector.applyProjection(patchedDoc); + ctx.bus.emit('storage-socket-update', { colName: col.colName, doc: patchedDoc }); + + col.autoPrune(); + ctx.bus.emit('data-received'); +} + + +function patchOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await patch(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = patchOperation; \ No newline at end of file diff --git a/lib/api3/generic/patch/validate.js b/lib/api3/generic/patch/validate.js new file mode 100644 index 00000000000..057bb5c39e8 --- /dev/null +++ b/lib/api3/generic/patch/validate.js @@ -0,0 +1,19 @@ +'use strict'; + +const updateValidate = require('../update/validate') + ; + + +/** + * Validate document to patch + * @param {Object} opCtx + * @param {Object} doc + * @param {Object} storageDoc + * @returns string - null if validation fails + */ +function validate (opCtx, doc, storageDoc) { + + return updateValidate(opCtx, doc, storageDoc, { isPatching: true }); +} + +module.exports = validate; \ No newline at end of file diff --git a/lib/api3/generic/read/operation.js b/lib/api3/generic/read/operation.js new file mode 100644 index 00000000000..c2e65a4afcc --- /dev/null +++ b/lib/api3/generic/read/operation.js @@ -0,0 +1,77 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , dateTools = require('../../shared/dateTools') + , renderer = require('../../shared/renderer') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + +/** + * READ: Retrieves a single document from the collection + */ +async function read (opCtx) { + + const { req, res, col } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:read`); + + const fieldsProjector = new FieldsProjector(req.query.fields); + + const result = await col.storage.findOne(req.params.identifier + , fieldsProjector.storageProjection()); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + return res.status(apiConst.HTTP.NOT_FOUND).end(); + } + + const doc = result[0]; + if (doc.isValid === false) { + return res.status(apiConst.HTTP.GONE).end(); + } + + + const modifiedDate = col.resolveDates(doc); + if (modifiedDate) { + res.setHeader('Last-Modified', modifiedDate.toUTCString()); + + const ifModifiedSince = req.get('If-Modified-Since'); + + if (ifModifiedSince + && dateTools.floorSeconds(modifiedDate) <= dateTools.floorSeconds(new Date(ifModifiedSince))) { + return res.status(apiConst.HTTP.NOT_MODIFIED).end(); + } + } + + fieldsProjector.applyProjection(doc); + + res.status(apiConst.HTTP.OK); + renderer.render(res, doc); +} + + +function readOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await read(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = readOperation; \ No newline at end of file diff --git a/lib/api3/generic/search/input.js b/lib/api3/generic/search/input.js new file mode 100644 index 00000000000..dbd37356760 --- /dev/null +++ b/lib/api3/generic/search/input.js @@ -0,0 +1,140 @@ +'use strict'; + +const apiConst = require('../../const.json') + , dateTools = require('../../shared/dateTools') + , stringTools = require('../../shared/stringTools') + , opTools = require('../../shared/operationTools') + ; + +const filterRegex = /(.*)\$([a-zA-Z]+)/; + + +/** + * Parse value of the parameter (to the correct data type) + */ +function parseValue(param, value) { + + value = stringTools.isNumberInString(value) ? parseFloat(value) : value; // convert number from string + + // convert boolean from string + if (value === 'true') + value = true; + + if (value === 'false') + value = false; + + // unwrap string in single quotes + if (typeof(value) === 'string' && value.startsWith('\'') && value.endsWith('\'')) { + value = value.substr(1, value.length - 2); + } + + if (['date', 'srvModified', 'srvCreated'].includes(param)) { + let m = dateTools.parseToMoment(value); + if (m && m.isValid()) { + value = m.valueOf(); + } + } + + if (param === 'created_at') { + let m = dateTools.parseToMoment(value); + if (m && m.isValid()) { + value = m.toISOString(); + } + } + + return value; +} + + +/** + * Parse filtering criteria from query string + */ +function parseFilter (req, res) { + const filter = [] + , reservedParams = ['token', 'sort', 'sort$desc', 'limit', 'skip', 'fields', 'now'] + , operators = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 're'] + ; + + for (let param in req.query) { + if (!Object.prototype.hasOwnProperty.call(req.query, param) + || reservedParams.includes(param)) continue; + + let field = param + , operator = 'eq' + ; + + const match = filterRegex.exec(param); + if (match != null) { + operator = match[2]; + field = match[1]; + + if (!operators.includes(operator)) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, + apiConst.MSG.HTTP_400_UNSUPPORTED_FILTER_OPERATOR.replace('{0}', operator)); + return null; + } + } + const value = parseValue(field, req.query[param]); + + filter.push({ field, operator, value }); + } + + return filter; +} + + +/** + * Parse sorting from query string + */ +function parseSort (req, res) { + let sort = {} + , sortDirection = 1; + + if (req.query.sort && req.query.sort$desc) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_SORT_SORT_DESC); + return null; + } + + if (req.query.sort$desc) { + sortDirection = -1; + sort[req.query.sort$desc] = sortDirection; + } + else { + if (req.query.sort) { + sort[req.query.sort] = sortDirection; + } + } + + sort.identifier = sortDirection; + sort.created_at = sortDirection; + sort.date = sortDirection; + + return sort; +} + + +/** + * Parse skip (offset) from query string + */ +function parseSkip (req, res) { + let skip = 0; + + if (req.query.skip) { + if (!isNaN(req.query.skip) && req.query.skip >= 0) { + skip = parseInt(req.query.skip); + } + else { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_SKIP); + return null; + } + } + + return skip; +} + + +module.exports = { + parseFilter, + parseSort, + parseSkip +}; \ No newline at end of file diff --git a/lib/api3/generic/search/operation.js b/lib/api3/generic/search/operation.js new file mode 100644 index 00000000000..c24f978dc27 --- /dev/null +++ b/lib/api3/generic/search/operation.js @@ -0,0 +1,79 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , renderer = require('../../shared/renderer') + , input = require('./input') + , _each = require('lodash/each') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + + +/** + * SEARCH: Search documents from the collection + */ +async function search (opCtx) { + + const { req, res, col } = opCtx; + + if (col.colName === 'settings') { + await security.demandPermission(opCtx, `api:${col.colName}:admin`); + } else { + await security.demandPermission(opCtx, `api:${col.colName}:read`); + } + + const fieldsProjector = new FieldsProjector(req.query.fields); + + const filter = input.parseFilter(req, res) + , sort = input.parseSort(req, res) + , limit = col.parseLimit(req, res) + , skip = input.parseSkip(req, res) + , projection = fieldsProjector.storageProjection() + , onlyValid = true + ; + + + if (filter !== null && sort !== null && limit !== null && skip !== null && projection !== null) { + + const result = await col.storage.findMany(filter + , sort + , limit + , skip + , projection + , onlyValid); + + if (!result) + throw new Error('empty result'); + + _each(result, col.resolveDates); + + _each(result, fieldsProjector.applyProjection); + + res.status(apiConst.HTTP.OK); + renderer.render(res, result); + } +} + + +function searchOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await search(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = searchOperation; \ No newline at end of file diff --git a/lib/api3/generic/setup.js b/lib/api3/generic/setup.js new file mode 100644 index 00000000000..17e118658dd --- /dev/null +++ b/lib/api3/generic/setup.js @@ -0,0 +1,103 @@ +'use strict'; + +const _ = require('lodash') + , dateTools = require('../shared/dateTools') + , Collection = require('./collection') + ; + + +function fallbackDate (doc) { + const m = dateTools.parseToMoment(doc.date); + return m == null || !m.isValid() + ? null + : m.toDate(); +} + + +function fallbackCreatedAt (doc) { + const m = dateTools.parseToMoment(doc.created_at); + return m == null || !m.isValid() + ? null + : m.toDate(); +} + + +function setupGenericCollections (ctx, env, app) { + const cols = { } + , enabledCols = app.get('enabledCollections'); + + if (_.includes(enabledCols, 'devicestatus')) { + cols.devicestatus = new Collection({ + ctx, env, app, + colName: 'devicestatus', + storageColName: env.devicestatus_collection || 'devicestatus', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at', 'device'], + fallbackDateField: 'created_at' + }); + } + + const entriesCollection = new Collection({ + ctx, env, app, + colName: 'entries', + storageColName: env.entries_collection || 'entries', + fallbackGetDate: fallbackDate, + dedupFallbackFields: ['date', 'type'], + fallbackDateField: 'date' + }); + app.set('entriesCollection', entriesCollection); + + if (_.includes(enabledCols, 'entries')) { + cols.entries = entriesCollection; + } + + if (_.includes(enabledCols, 'food')) { + cols.food = new Collection({ + ctx, env, app, + colName: 'food', + storageColName: env.food_collection || 'food', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at'], + fallbackDateField: 'created_at' + }); + } + + if (_.includes(enabledCols, 'profile')) { + cols.profile = new Collection({ + ctx, env, app, + colName: 'profile', + storageColName: env.profile_collection || 'profile', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at'], + fallbackDateField: 'created_at' + }); + } + + if (_.includes(enabledCols, 'settings')) { + cols.settings = new Collection({ + ctx, env, app, + colName: 'settings', + storageColName: env.settings_collection || 'settings' + }); + } + + if (_.includes(enabledCols, 'treatments')) { + cols.treatments = new Collection({ + ctx, env, app, + colName: 'treatments', + storageColName: env.treatments_collection || 'treatments', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at', 'eventType'], + fallbackDateField: 'created_at' + }); + } + + _.forOwn(cols, function forMember (col) { + col.mapRoutes(); + }); + + app.set('collections', cols); +} + + +module.exports = setupGenericCollections; diff --git a/lib/api3/generic/update/operation.js b/lib/api3/generic/update/operation.js new file mode 100644 index 00000000000..3e517a32d11 --- /dev/null +++ b/lib/api3/generic/update/operation.js @@ -0,0 +1,86 @@ +'use strict'; + +const _ = require('lodash') + , dateTools = require('../../shared/dateTools') + , apiConst = require('../../const.json') + , security = require('../../security') + , insert = require('../create/insert') + , replace = require('./replace') + , opTools = require('../../shared/operationTools') + ; + +/** + * UPDATE: Updates a document in the collection + */ +async function update (opCtx) { + + const { col, req, res } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + col.parseDate(doc); + opTools.resolveIdentifier(doc); + + const identifier = req.params.identifier + , identifyingFilter = col.storage.identifyingFilter(identifier); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('empty result'); + + doc.identifier = identifier; + + if (result.length > 0) { + await updateConditional(opCtx, doc, result[0]); + } + else { + await insert(opCtx, doc); + } +} + + +async function updateConditional (opCtx, doc, storageDoc) { + + const { col, req, res } = opCtx; + + if (storageDoc.isValid === false) { + return res.status(apiConst.HTTP.GONE).end(); + } + + const modifiedDate = col.resolveDates(storageDoc) + , ifUnmodifiedSince = req.get('If-Unmodified-Since'); + + if (ifUnmodifiedSince + && dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) { + return res.status(apiConst.HTTP.PRECONDITION_FAILED).end(); + } + + await replace(opCtx, doc, storageDoc); +} + + +function updateOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await update(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = updateOperation; \ No newline at end of file diff --git a/lib/api3/generic/update/replace.js b/lib/api3/generic/update/replace.js new file mode 100644 index 00000000000..fdf803ed16f --- /dev/null +++ b/lib/api3/generic/update/replace.js @@ -0,0 +1,53 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + , path = require('path') + ; + +/** + * Replace existing document in the collection + * @param {Object} opCtx + * @param {any} doc - new version of document to set + * @param {any} storageDoc - old version of document (existing in the storage) + * @param {Object} options + */ +async function replace (opCtx, doc, storageDoc, options) { + + const { ctx, auth, col, req, res } = opCtx; + const { isDeduplication } = options || {}; + + await security.demandPermission(opCtx, `api:${col.colName}:update`); + + if (validate(opCtx, doc, storageDoc, { isDeduplication }) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + doc.srvCreated = storageDoc.srvCreated || doc.srvModified; + + if (auth && auth.subject && auth.subject.name) { + doc.subject = auth.subject.name; + } + + const matchedCount = await col.storage.replaceOne(storageDoc.identifier, doc); + + if (!matchedCount) + throw new Error('empty matchedCount'); + + res.setHeader('Last-Modified', now.toUTCString()); + + if (storageDoc.identifier !== doc.identifier || isDeduplication) { + res.setHeader('Location', path.posix.join(req.baseUrl, req.path, doc.identifier)); + } + + res.status(apiConst.HTTP.NO_CONTENT).send({ }); + + ctx.bus.emit('storage-socket-update', { colName: col.colName, doc }); + col.autoPrune(); + ctx.bus.emit('data-received'); +} + + +module.exports = replace; \ No newline at end of file diff --git a/lib/api3/generic/update/validate.js b/lib/api3/generic/update/validate.js new file mode 100644 index 00000000000..b36e1410067 --- /dev/null +++ b/lib/api3/generic/update/validate.js @@ -0,0 +1,48 @@ +'use strict'; + +const apiConst = require('../../const.json') + , opTools = require('../../shared/operationTools') + ; + + +/** + * Validation of document to update + * @param {Object} opCtx + * @param {Object} doc + * @param {Object} storageDoc + * @param {Object} options + * @returns string with error message if validation fails, true in case of success + */ +function validate (opCtx, doc, storageDoc, options) { + + const { res } = opCtx; + const { isPatching, isDeduplication } = options || {}; + + const immutable = ['identifier', 'date', 'utcOffset', 'eventType', 'device', 'app', + 'srvCreated', 'subject', 'srvModified', 'modifiedBy', 'isValid']; + + if (storageDoc.isReadOnly === true || storageDoc.readOnly === true || storageDoc.readonly === true) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, + apiConst.MSG.HTTP_422_READONLY_MODIFICATION); + } + + for (const field of immutable) { + + // change of identifier is allowed in deduplication (for APIv1 documents) + if (field === 'identifier' && isDeduplication) + continue; + + // changing deleted document is without restrictions + if (storageDoc.isValid === false) + continue; + + if (typeof(doc[field]) !== 'undefined' && doc[field] !== storageDoc[field]) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, + apiConst.MSG.HTTP_400_IMMUTABLE_FIELD.replace('{0}', field)); + } + } + + return opTools.validateCommon(doc, res, { isPatching }); +} + +module.exports = validate; \ No newline at end of file diff --git a/lib/api3/index.js b/lib/api3/index.js new file mode 100644 index 00000000000..4bfe07a35fe --- /dev/null +++ b/lib/api3/index.js @@ -0,0 +1,110 @@ +'use strict'; + +const express = require('express') + , bodyParser = require('body-parser') + , renderer = require('./shared/renderer') + , StorageSocket = require('./storageSocket') + , apiConst = require('./const.json') + , security = require('./security') + , genericSetup = require('./generic/setup') + , swaggerSetup = require('./swagger') + ; + +function configure (env, ctx) { + + const self = { } + , app = express() + ; + + self.setENVTruthy = function setENVTruthy (varName, defaultValue) { + //for some reason Azure uses this prefix, maybe there is a good reason + let value = process.env['CUSTOMCONNSTR_' + varName] + || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] + || process.env[varName] + || process.env[varName.toLowerCase()]; + + value = value != null ? value : defaultValue; + + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + + app.set(varName, value); + return value; + }; + app.setENVTruthy = self.setENVTruthy; + + + self.setupApiEnvironment = function setupApiEnvironment () { + + app.use(bodyParser.json({ + limit: 1048576 * 50 + }), function errorHandler (err, req, res, next) { + console.error(err); + res.status(apiConst.HTTP.INTERNAL_ERROR).json({ + status: apiConst.HTTP.INTERNAL_ERROR, + message: apiConst.MSG.HTTP_500_INTERNAL_ERROR + }); + if (next) { // we need 4th parameter next to behave like error handler, but we have to use it to prevent "unused variable" message + } + }); + + app.use(renderer.extension2accept); + + // we don't need these here + app.set('etag', false); + app.set('x-powered-by', false); // this seems to be unreliable + app.use(function (req, res, next) { + res.removeHeader('x-powered-by'); + next(); + }); + + app.set('name', env.name); + app.set('version', env.version); + app.set('apiVersion', apiConst.API3_VERSION); + app.set('units', env.DISPLAY_UNITS); + app.set('ci', process.env['CI'] ? true: false); + app.set('enabledCollections', ['devicestatus', 'entries', 'food', 'profile', 'settings', 'treatments']); + + self.setENVTruthy('API3_SECURITY_ENABLE', apiConst.API3_SECURITY_ENABLE); + self.setENVTruthy('API3_TIME_SKEW_TOLERANCE', apiConst.API3_TIME_SKEW_TOLERANCE); + self.setENVTruthy('API3_DEDUP_FALLBACK_ENABLED', apiConst.API3_DEDUP_FALLBACK_ENABLED); + self.setENVTruthy('API3_CREATED_AT_FALLBACK_ENABLED', apiConst.API3_CREATED_AT_FALLBACK_ENABLED); + self.setENVTruthy('API3_MAX_LIMIT', apiConst.API3_MAX_LIMIT); + }; + + + self.setupApiRoutes = function setupApiRoutes () { + + app.get('/version', require('./specific/version')(app, ctx, env)); + + if (app.get('env') === 'development' || app.get('ci')) { // for development and testing purposes only + app.get('/test', async function test (req, res) { + + try { + const opCtx = {app, ctx, env, req, res}; + opCtx.auth = await security.authenticate(opCtx); + await security.demandPermission(opCtx, 'api:entries:read'); + res.status(apiConst.HTTP.OK).end(); + } catch (error) { + console.error(error); + } + }); + } + + app.get('/lastModified', require('./specific/lastModified')(app, ctx, env)); + + app.get('/status', require('./specific/status')(app, ctx, env)); + }; + + + self.setupApiEnvironment(); + genericSetup(ctx, env, app); + self.setupApiRoutes(); + swaggerSetup(app); + + ctx.storageSocket = new StorageSocket(app, env, ctx); + + return app; +} + +module.exports = configure; diff --git a/lib/api3/security.js b/lib/api3/security.js new file mode 100644 index 00000000000..33099d88f12 --- /dev/null +++ b/lib/api3/security.js @@ -0,0 +1,122 @@ +'use strict'; + +const moment = require('moment') + , apiConst = require('./const.json') + , _ = require('lodash') + , shiroTrie = require('shiro-trie') + , dateTools = require('./shared/dateTools') + , opTools = require('./shared/operationTools') + ; + + +/** + * Check if Date header in HTTP request (or 'now' query parameter) is present and valid (with error response sending) + */ +function checkDateHeader (opCtx) { + + const { app, req, res } = opCtx; + + let dateString = req.header('Date'); + if (!dateString) { + dateString = req.query.now; + } + + if (!dateString) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_DATE); + } + + let dateMoment = dateTools.parseToMoment(dateString); + if (!dateMoment) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_DATE); + } + + let nowMoment = moment(new Date()); + let diffMinutes = moment.duration(nowMoment.diff(dateMoment)).asMinutes(); + + if (Math.abs(diffMinutes) > app.get('API3_TIME_SKEW_TOLERANCE')) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); + } + + return true; +} + + +function authenticate (opCtx) { + return new Promise(function promise (resolve, reject) { + + let { app, ctx, req, res } = opCtx; + + if (!app.get('API3_SECURITY_ENABLE')) { + const adminShiro = shiroTrie.new(); + adminShiro.add('*'); + return resolve({ shiros: [ adminShiro ] }); + } + + if (req.protocol !== 'https') { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.FORBIDDEN, apiConst.MSG.HTTP_403_NOT_USING_HTTPS)); + } + + const checkDateResult = checkDateHeader(opCtx); + if (checkDateResult !== true) { + return checkDateResult; + } + + let token = ctx.authorization.extractToken(req); + if (!token) { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN)); + } + + ctx.authorization.resolve({ token }, function resolveFinish (err, result) { + if (err) { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_TOKEN)); + } + else { + return resolve(result); + } + }); + }); +} + + +/** + * Checks for the permission from the authorization without error response sending + * @param {any} auth + * @param {any} permission + */ +function checkPermission (auth, permission) { + + if (auth) { + const found = _.find(auth.shiros, function checkEach (shiro) { + return shiro && shiro.check(permission); + }); + return _.isObject(found); + } + else { + return false; + } +} + + + +function demandPermission (opCtx, permission) { + return new Promise(function promise (resolve, reject) { + const { auth, res } = opCtx; + + if (checkPermission(auth, permission)) { + return resolve(true); + } else { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.FORBIDDEN, apiConst.MSG.HTTP_403_MISSING_PERMISSION.replace('{0}', permission))); + } + }); +} + + +module.exports = { + authenticate, + checkPermission, + demandPermission +}; \ No newline at end of file diff --git a/lib/api3/shared/dateTools.js b/lib/api3/shared/dateTools.js new file mode 100644 index 00000000000..14b67f9e109 --- /dev/null +++ b/lib/api3/shared/dateTools.js @@ -0,0 +1,78 @@ +'use strict'; + +const moment = require('moment') + , stringTools = require('./stringTools') + , apiConst = require('../const.json') + ; + + +/** + * Floor date to whole seconds (cut off milliseconds) + * @param {Date} date + */ +function floorSeconds (date) { + let ms = date.getTime(); + ms -= ms % 1000; + return new Date(ms); +} + + +/** + * Parse date as moment object from value or array of values. + * @param {any} value + */ +function parseToMoment (value) +{ + if (!value) + return null; + + if (Array.isArray(value)) { + for (let item of value) { + let m = parseToMoment(item); + + if (m !== null) + return m; + } + } + else { + + if (typeof value === 'string' && stringTools.isNumberInString(value)) { + value = parseFloat(value); + } + + if (typeof value === 'number') { + let m = moment(value); + + if (!m.isValid()) + return null; + + if (m.valueOf() < apiConst.MIN_TIMESTAMP) + m = moment.unix(m); + + if (!m.isValid() || m.valueOf() < apiConst.MIN_TIMESTAMP) + return null; + + return m; + } + + if (typeof value === 'string') { + let m = moment.parseZone(value, moment.ISO_8601); + + if (!m.isValid()) + m = moment.parseZone(value, moment.RFC_2822); + + if (!m.isValid() || m.valueOf() < apiConst.MIN_TIMESTAMP) + return null; + + return m; + } + } + + // no parsing option succeeded => failure + return null; +} + +module.exports = { + floorSeconds, + parseToMoment +}; diff --git a/lib/api3/shared/fieldsProjector.js b/lib/api3/shared/fieldsProjector.js new file mode 100644 index 00000000000..921c7cc6df8 --- /dev/null +++ b/lib/api3/shared/fieldsProjector.js @@ -0,0 +1,82 @@ +'use strict'; + +const _each = require('lodash/each'); + +/** + * Decoder of 'fields' parameter providing storage projections + * @param {string} fieldsString - fields parameter from user + */ +function FieldsProjector (fieldsString) { + + const self = this + , exclude = []; + let specific = null; + + switch (fieldsString) + { + case '_all': + break; + + default: + if (fieldsString) { + specific = fieldsString.split(','); + } + } + + const systemFields = ['identifier', 'srvCreated', 'created_at', 'date']; + + /** + * Prepare projection definition for storage query + * */ + self.storageProjection = function storageProjection () { + const projection = { }; + + if (specific) { + _each(specific, function include (field) { + projection[field] = 1; + }); + + _each(systemFields, function include (field) { + projection[field] = 1; + }); + } + else { + _each(exclude, function exclude (field) { + projection[field] = 0; + }); + + _each(exclude, function exclude (field) { + if (systemFields.indexOf(field) >= 0) { + delete projection[field]; + } + }); + } + + return projection; + }; + + + /** + * Cut off unwanted fields from given document + * @param {Object} doc + */ + self.applyProjection = function applyProjection (doc) { + + if (specific) { + for(const field in doc) { + if (specific.indexOf(field) === -1) { + delete doc[field]; + } + } + } + else { + _each(exclude, function include (field) { + if (typeof(doc[field]) !== 'undefined') { + delete doc[field]; + } + }); + } + }; +} + +module.exports = FieldsProjector; \ No newline at end of file diff --git a/lib/api3/shared/operationTools.js b/lib/api3/shared/operationTools.js new file mode 100644 index 00000000000..1955b9c2068 --- /dev/null +++ b/lib/api3/shared/operationTools.js @@ -0,0 +1,111 @@ +'use strict'; + +const apiConst = require('../const.json') + , stringTools = require('./stringTools') + , uuidv5 = require('uuid/v5') + , uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-) + ; + +function sendJSONStatus (res, status, title, description, warning) { + + const json = { + status: status, + message: title, + description: description + }; + + // Add optional warning message. + if (warning) { json.warning = warning; } + + res.status(status).json(json); + + return title; +} + + +/** + * Validate document's common fields + * @param {Object} doc + * @param {any} res + * @param {Object} options + * @returns {any} - string with error message if validation fails, true in case of success + */ +function validateCommon (doc, res, options) { + + const { isPatching } = options || {}; + + + if ((!isPatching || typeof(doc.date) !== 'undefined') + + && (typeof(doc.date) !== 'number' + || doc.date <= apiConst.MIN_TIMESTAMP) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_DATE); + } + + + if ((!isPatching || typeof(doc.utcOffset) !== 'undefined') + + && (typeof(doc.utcOffset) !== 'number' + || doc.utcOffset < apiConst.MIN_UTC_OFFSET + || doc.utcOffset > apiConst.MAX_UTC_OFFSET) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_UTC); + } + + + if ((!isPatching || typeof(doc.app) !== 'undefined') + + && (typeof(doc.app) !== 'string' + || stringTools.isNullOrWhitespace(doc.app)) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_APP); + } + + return true; +} + + +/** + * Calculate identifier for the document + * @param {Object} doc + * @returns string + */ +function calculateIdentifier (doc) { + if (!doc) + return undefined; + + let key = doc.device + '_' + doc.date; + if (doc.eventType) { + key += '_' + doc.eventType; + } + + return uuidv5(key, uuidNamespace); +} + + +/** + * Validate identifier in the document + * @param {Object} doc + */ +function resolveIdentifier (doc) { + + let identifier = calculateIdentifier(doc); + if (doc.identifier) { + if (doc.identifier !== identifier) { + console.warn(`APIv3: Identifier mismatch (expected: ${identifier}, received: ${doc.identifier})`); + console.log(doc); + } + } + else { + doc.identifier = identifier; + } +} + + +module.exports = { + sendJSONStatus, + validateCommon, + calculateIdentifier, + resolveIdentifier +}; \ No newline at end of file diff --git a/lib/api3/shared/renderer.js b/lib/api3/shared/renderer.js new file mode 100644 index 00000000000..a3588819a72 --- /dev/null +++ b/lib/api3/shared/renderer.js @@ -0,0 +1,99 @@ +'use strict'; + +const apiConst = require('../const.json') + , mime = require('mime') + , url = require('url') + , opTools = require('./operationTools') + , EasyXml = require('easyxml') + , csvStringify = require('csv-stringify') + ; + + +/** + * Middleware that converts url's extension to Accept HTTP request header + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +function extension2accept (req, res, next) { + + const pathSplit = req.path.split('.'); + + if (pathSplit.length < 2) + return next(); + + const pathBase = pathSplit[0] + , extension = pathSplit.slice(1).join('.'); + + if (!extension) + return next(); + + const mimeType = mime.getType(extension); + if (!mimeType) + return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_ACCEPTABLE, apiConst.MSG.HTTP_406_UNSUPPORTED_FORMAT); + + req.extToAccept = { + url: req.url, + accept: req.headers.accept + }; + + req.headers.accept = mimeType; + const parsed = url.parse(req.url); + parsed.pathname = pathBase; + req.url = url.format(parsed); + + next(); +} + + +/** + * Sends data to output using the client's desired format + * @param {Object} res + * @param {any} data + */ +function render (res, data) { + res.format({ + 'json': () => res.send(data), + 'csv': () => renderCsv(res, data), + 'xml': () => renderXml(res, data), + 'default': () => + opTools.sendJSONStatus(res, apiConst.HTTP.NOT_ACCEPTABLE, apiConst.MSG.HTTP_406_UNSUPPORTED_FORMAT) + }); +} + + +/** + * Format data to output as .csv + * @param {Object} res + * @param {any} data + */ +function renderCsv (res, data) { + const csvSource = Array.isArray(data) ? data : [data]; + csvStringify(csvSource, { + header: true + }, + function csvStringified (err, output) { + res.send(output); + }); +} + + +/** + * Format data to output as .xml + * @param {Object} res + * @param {any} data + */ +function renderXml (res, data) { + const serializer = new EasyXml({ + rootElement: 'item', + dateFormat: 'ISO', + manifest: true + }); + res.send(serializer.render(data)); +} + + +module.exports = { + extension2accept, + render +}; \ No newline at end of file diff --git a/lib/api3/shared/storageTools.js b/lib/api3/shared/storageTools.js new file mode 100644 index 00000000000..b7d9dca6776 --- /dev/null +++ b/lib/api3/shared/storageTools.js @@ -0,0 +1,63 @@ +'use strict'; + +function getStorageVersion (app) { + + return new Promise(function (resolve, reject) { + + try { + const storage = app.get('entriesCollection').storage; + let storageVersion = app.get('storageVersion'); + + if (storageVersion) { + process.nextTick(() => { + resolve(storageVersion); + }); + } else { + storage.version() + .then(storageVersion => { + + app.set('storageVersion', storageVersion); + resolve(storageVersion); + }, reject); + } + } catch (error) { + reject(error); + } + }); +} + + +function getVersionInfo(app) { + + return new Promise(function (resolve, reject) { + + try { + const srvDate = new Date() + , info = { version: app.get('version') + , apiVersion: app.get('apiVersion') + , srvDate: srvDate.getTime() + }; + + getStorageVersion(app) + .then(storageVersion => { + + if (!storageVersion) + throw new Error('empty storageVersion'); + + info.storage = storageVersion; + + resolve(info); + + }, reject); + + } catch(error) { + reject(error); + } + }); +} + + +module.exports = { + getStorageVersion, + getVersionInfo +}; diff --git a/lib/api3/shared/stringTools.js b/lib/api3/shared/stringTools.js new file mode 100644 index 00000000000..b71a4b4f1a6 --- /dev/null +++ b/lib/api3/shared/stringTools.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * Check the string for strictly valid number (no other characters present) + * @param {any} str + */ +function isNumberInString (str) { + return !isNaN(parseFloat(str)) && isFinite(str); +} + + +/** + * Check the string for non-whitespace characters presence + * @param {any} input + */ +function isNullOrWhitespace (input) { + + if (typeof input === 'undefined' || input == null) return true; + + return input.replace(/\s/g, '').length < 1; +} + + + +module.exports = { + isNumberInString, + isNullOrWhitespace +}; diff --git a/lib/api3/specific/lastModified.js b/lib/api3/specific/lastModified.js new file mode 100644 index 00000000000..b27ecaca852 --- /dev/null +++ b/lib/api3/specific/lastModified.js @@ -0,0 +1,101 @@ +'use strict'; + +function configure (app, ctx, env) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , security = require('../security') + , opTools = require('../shared/operationTools') + ; + + api.get('/lastModified', async function getLastModified (req, res) { + + async function getLastModified (col) { + + let result; + const lastModified = await col.storage.getLastModified('srvModified'); + + if (lastModified) { + result = lastModified.srvModified ? lastModified.srvModified : null; + } + + if (col.fallbackDateField) { + + const lastModified = await col.storage.getLastModified(col.fallbackDateField); + + if (lastModified && lastModified[col.fallbackDateField]) { + let timestamp = lastModified[col.fallbackDateField]; + if (typeof(timestamp) === 'string') { + timestamp = (new Date(timestamp)).getTime(); + } + + if (result === null || result < timestamp) { + result = timestamp; + } + } + } + + return { colName: col.colName, lastModified: result }; + } + + + async function collectionsAsync (auth) { + + const cols = app.get('collections') + , promises = [] + , output = {} + ; + + for (const colName in cols) { + const col = cols[colName]; + + if (security.checkPermission(auth, 'api:' + col.colName + ':read')) { + promises.push(getLastModified(col)); + } + } + + const results = await Promise.all(promises); + + for (const result of results) { + if (result.lastModified) + output[result.colName] = result.lastModified; + } + + return output; + } + + + async function operation (opCtx) { + + const { res, auth } = opCtx; + const srvDate = new Date(); + + let info = { + srvDate: srvDate.getTime(), + collections: { } + }; + + info.collections = await collectionsAsync(auth); + + res.json(info); + } + + + const opCtx = { app, ctx, env, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await operation(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/specific/status.js b/lib/api3/specific/status.js new file mode 100644 index 00000000000..7b70b24ab71 --- /dev/null +++ b/lib/api3/specific/status.js @@ -0,0 +1,71 @@ +'use strict'; + +function configure (app, ctx, env) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , storageTools = require('../shared/storageTools') + , security = require('../security') + , opTools = require('../shared/operationTools') + ; + + api.get('/status', async function getStatus (req, res) { + + function permsForCol (col, auth) { + let colPerms = ''; + + if (security.checkPermission(auth, 'api:' + col.colName + ':create')) { + colPerms += 'c'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':read')) { + colPerms += 'r'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':update')) { + colPerms += 'u'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':delete')) { + colPerms += 'd'; + } + + return colPerms; + } + + + async function operation (opCtx) { + const cols = app.get('collections'); + + let info = await storageTools.getVersionInfo(app); + + info.apiPermissions = {}; + for (let col in cols) { + const colPerms = permsForCol(col, opCtx.auth); + if (colPerms) { + info.apiPermissions[col] = colPerms; + } + } + + res.json(info); + } + + + const opCtx = { app, ctx, env, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await operation(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/specific/version.js b/lib/api3/specific/version.js new file mode 100644 index 00000000000..25392fe99d7 --- /dev/null +++ b/lib/api3/specific/version.js @@ -0,0 +1,28 @@ +'use strict'; + +function configure (app) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , storageTools = require('../shared/storageTools') + , opTools = require('../shared/operationTools') + ; + + api.get('/version', async function getVersion (req, res) { + + try { + const versionInfo = await storageTools.getVersionInfo(app); + + res.json(versionInfo); + + } catch(error) { + console.error(error); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/storage/mongoCollection/find.js b/lib/api3/storage/mongoCollection/find.js new file mode 100644 index 00000000000..bc399dbce98 --- /dev/null +++ b/lib/api3/storage/mongoCollection/find.js @@ -0,0 +1,93 @@ +'use strict'; + +const utils = require('./utils') + , _ = require('lodash') + ; + + +/** + * Find single document by identifier + * @param {Object} col + * @param {string} identifier + * @param {Object} projection + */ +function findOne (col, identifier, projection) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.find(filter) + .project(projection) + .sort({ identifier: -1 }) // document with identifier first (not the fallback one) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + _.each(result, utils.normalizeDoc); + resolve(result); + } + }); + }); +} + + +/** + * Find single document by query filter + * @param {Object} col + * @param {Object} filter specific filter + * @param {Object} projection + */ +function findOneFilter (col, filter, projection) { + + return new Promise(function (resolve, reject) { + + col.find(filter) + .project(projection) + .sort({ identifier: -1 }) // document with identifier first (not the fallback one) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + _.each(result, utils.normalizeDoc); + resolve(result); + } + }); + }); +} + + +/** + * Find many documents matching the filtering criteria + */ +function findMany (col, filterDef, sort, limit, skip, projection, onlyValid, logicalOperator = 'and') { + + return new Promise(function (resolve, reject) { + + const filter = utils.parseFilter(filterDef, logicalOperator, onlyValid); + + col.find(filter) + .sort(sort) + .limit(limit) + .skip(skip) + .project(projection) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + _.each(result, utils.normalizeDoc); + resolve(result); + } + }); + }); +} + + +module.exports = { + findOne, + findOneFilter, + findMany +}; \ No newline at end of file diff --git a/lib/api3/storage/mongoCollection/index.js b/lib/api3/storage/mongoCollection/index.js new file mode 100644 index 00000000000..e6ad0a6cf8b --- /dev/null +++ b/lib/api3/storage/mongoCollection/index.js @@ -0,0 +1,90 @@ +'use strict'; + +/** + * Storage implementation using mongoDB + * @param {Object} ctx + * @param {Object} env + * @param {string} colName - name of the collection in mongo database + */ +function MongoCollection (ctx, env, colName) { + + const self = this + , utils = require('./utils') + , find = require('./find') + , modify = require('./modify') + ; + + self.colName = colName; + + self.col = ctx.store.collection(colName); + + ctx.store.ensureIndexes(self.col, [ 'identifier', + 'srvModified', + 'isValid' + ]); + + + self.identifyingFilter = utils.identifyingFilter; + + self.findOne = (...args) => find.findOne(self.col, ...args); + + self.findOneFilter = (...args) => find.findOneFilter(self.col, ...args); + + self.findMany = (...args) => find.findMany(self.col, ...args); + + self.insertOne = (...args) => modify.insertOne(self.col, ...args); + + self.replaceOne = (...args) => modify.replaceOne(self.col, ...args); + + self.updateOne = (...args) => modify.updateOne(self.col, ...args); + + self.deleteOne = (...args) => modify.deleteOne(self.col, ...args); + + self.deleteManyOr = (...args) => modify.deleteManyOr(self.col, ...args); + + + /** + * Get server version + */ + self.version = function version () { + + return new Promise(function (resolve, reject) { + + ctx.store.db.admin().buildInfo({}, function mongoDone (err, result) { + + err + ? reject(err) + : resolve({ + storage: 'mongodb', + version: result.version + }); + }); + }); + }; + + + /** + * Get timestamp (e.g. srvModified) of the last modified document + */ + self.getLastModified = function getLastModified (fieldName) { + + return new Promise(function (resolve, reject) { + + self.col.find() + + .sort({ [fieldName]: -1 }) + + .limit(1) + + .project({ [fieldName]: 1 }) + + .toArray(function mongoDone (err, [ result ]) { + err + ? reject(err) + : resolve(result); + }); + }); + } +} + +module.exports = MongoCollection; \ No newline at end of file diff --git a/lib/api3/storage/mongoCollection/modify.js b/lib/api3/storage/mongoCollection/modify.js new file mode 100644 index 00000000000..6552fe40e8c --- /dev/null +++ b/lib/api3/storage/mongoCollection/modify.js @@ -0,0 +1,123 @@ +'use strict'; + +const utils = require('./utils'); + + +/** + * Insert single document + * @param {Object} col + * @param {Object} doc + */ +function insertOne (col, doc) { + + return new Promise(function (resolve, reject) { + + col.insertOne(doc, function mongoDone(err, result) { + + if (err) { + reject(err); + } else { + const identifier = doc.identifier || result.insertedId.toString(); + delete doc._id; + resolve(identifier); + } + }); + }); +} + + +/** + * Replace single document + * @param {Object} col + * @param {string} identifier + * @param {Object} doc + */ +function replaceOne (col, identifier, doc) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.replaceOne(filter, doc, { }, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve(result.matchedCount); + } + }); + }); +} + + +/** + * Update single document by identifier + * @param {Object} col + * @param {string} identifier + * @param {object} setFields + */ +function updateOne (col, identifier, setFields) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.updateOne(filter, { $set: setFields }, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ updated: result.result.nModified }); + } + }); + }); +} + + +/** + * Permanently remove single document by identifier + * @param {Object} col + * @param {string} identifier + */ +function deleteOne (col, identifier) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.deleteOne(filter, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ deleted: result.result.n }); + } + }); + }); +} + + +/** + * Permanently remove many documents matching any of filtering criteria + */ +function deleteManyOr (col, filterDef) { + + return new Promise(function (resolve, reject) { + + const filter = utils.parseFilter(filterDef, 'or'); + + col.deleteMany(filter, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ deleted: result.deletedCount }); + } + }); + }); +} + + +module.exports = { + insertOne, + replaceOne, + updateOne, + deleteOne, + deleteManyOr +}; \ No newline at end of file diff --git a/lib/api3/storage/mongoCollection/utils.js b/lib/api3/storage/mongoCollection/utils.js new file mode 100644 index 00000000000..1b2ab5610d7 --- /dev/null +++ b/lib/api3/storage/mongoCollection/utils.js @@ -0,0 +1,178 @@ +'use strict'; + +const _ = require('lodash') + , checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$") + , ObjectID = require('mongodb').ObjectID +; + + +/** + * Normalize document (make it mongoDB independent) + * @param {Object} doc - document loaded from mongoDB + */ +function normalizeDoc (doc) { + if (!doc.identifier) { + doc.identifier = doc._id.toString(); + } + + delete doc._id; +} + + +/** + * Parse filter definition array into mongoDB filtering object + * @param {any} filterDef + * @param {string} logicalOperator + * @param {bool} onlyValid + */ +function parseFilter (filterDef, logicalOperator, onlyValid) { + + let filter = { }; + if (!filterDef) + return filter; + + if (!_.isArray(filterDef)) { + return filterDef; + } + + let clauses = []; + + for (const itemDef of filterDef) { + let item; + + switch (itemDef.operator) { + case 'eq': + item = itemDef.value; + break; + + case 'ne': + item = { $ne: itemDef.value }; + break; + + case 'gt': + item = { $gt: itemDef.value }; + break; + + case 'gte': + item = { $gte: itemDef.value }; + break; + + case 'lt': + item = { $lt: itemDef.value }; + break; + + case 'lte': + item = { $lte: itemDef.value }; + break; + + case 'in': + item = { $in: itemDef.value.toString().split('|') }; + break; + + case 'nin': + item = { $nin: itemDef.value.toString().split('|') }; + break; + + case 're': + item = { $regex: itemDef.value.toString() }; + break; + + default: + throw new Error('Unsupported or missing filter operator ' + itemDef.operator); + } + + if (logicalOperator === 'or') { + let clause = { }; + clause[itemDef.field] = item; + clauses.push(clause); + } + else { + filter[itemDef.field] = item; + } + } + + if (logicalOperator === 'or') { + filter = { $or: clauses }; + } + + if (onlyValid) { + filter.isValid = { $ne: false }; + } + + return filter; +} + + +/** + * Create query filter for single document with identifier fallback + * @param {string} identifier + */ +function filterForOne (identifier) { + + const filterOpts = [ { identifier } ]; + + // fallback to "identifier = _id" + if (checkForHexRegExp.test(identifier)) { + filterOpts.push({ _id: ObjectID(identifier) }); + } + + return { $or: filterOpts }; +} + + +/** + * Create query filter to check whether the document already exists in the storage. + * This function resolves eventual fallback deduplication. + * @param {string} identifier - identifier of document to check its existence in the storage + * @param {Object} doc - document to check its existence in the storage + * @param {Array} dedupFallbackFields - fields that all need to be matched to identify document via fallback deduplication + * @returns {Object} - query filter for mongo or null in case of no identifying possibility + */ +function identifyingFilter (identifier, doc, dedupFallbackFields) { + + const filterItems = []; + + if (identifier) { + // standard identifier field (APIv3) + filterItems.push({ identifier: identifier }); + + // fallback to "identifier = _id" (APIv1) + if (checkForHexRegExp.test(identifier)) { + filterItems.push({ identifier: { $exists: false }, _id: ObjectID(identifier) }); + } + } + + // let's deal with eventual fallback deduplication + if (!_.isEmpty(doc) && _.isArray(dedupFallbackFields) && dedupFallbackFields.length > 0) { + let dedupFilterItems = []; + + _.each(dedupFallbackFields, function addDedupField (field) { + + if (doc[field] !== undefined) { + + let dedupFilterItem = { }; + dedupFilterItem[field] = doc[field]; + dedupFilterItems.push(dedupFilterItem); + } + }); + + if (dedupFilterItems.length === dedupFallbackFields.length) { // all dedup fields are present + + dedupFilterItems.push({ identifier: { $exists: false } }); // force not existing identifier for fallback deduplication + filterItems.push({ $and: dedupFilterItems }); + } + } + + if (filterItems.length > 0) + return { $or: filterItems }; + else + return null; // we don't have any filtering rule to identify the document in the storage +} + + +module.exports = { + normalizeDoc, + parseFilter, + filterForOne, + identifyingFilter +}; \ No newline at end of file diff --git a/lib/api3/storageSocket.js b/lib/api3/storageSocket.js new file mode 100644 index 00000000000..e8c08310d2b --- /dev/null +++ b/lib/api3/storageSocket.js @@ -0,0 +1,145 @@ +'use strict'; + +const apiConst = require('./const'); + +/** + * Socket.IO broadcaster of any storage change + */ +function StorageSocket (app, env, ctx) { + + const self = this; + + const LOG_GREEN = '\x1B[32m' + , LOG_MAGENTA = '\x1B[35m' + , LOG_RESET = '\x1B[0m' + , LOG = LOG_GREEN + 'STORAGE SOCKET: ' + LOG_RESET + , LOG_ERROR = LOG_MAGENTA + 'STORAGE SOCKET: ' + LOG_RESET + , NAMESPACE = '/storage' + ; + + + /** + * Initialize socket namespace and bind the events + * @param {Object} io Socket.IO object to multiplex namespaces + */ + self.init = function init (io) { + self.io = io; + + self.namespace = io.of(NAMESPACE); + self.namespace.on('connection', function onConnected (socket) { + + const remoteIP = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress; + console.log(LOG + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP); + + socket.on('disconnect', function onDisconnect () { + console.log(LOG + 'Disconnected client ID: ', socket.client.id); + }); + + socket.on('subscribe', function onSubscribe (message, returnCallback) { + self.subscribe(socket, message, returnCallback); + }); + }); + + ctx.bus.on('storage-socket-create', self.emitCreate); + ctx.bus.on('storage-socket-update', self.emitUpdate); + ctx.bus.on('storage-socket-delete', self.emitDelete); + }; + + + /** + * Authorize Socket.IO client and subscribe him to authorized rooms + * @param {Object} socket + * @param {Object} message input message from the client + * @param {Function} returnCallback function for returning a value back to the client + */ + self.subscribe = function subscribe (socket, message, returnCallback) { + const shouldCallBack = typeof(returnCallback) === 'function'; + + if (message && message.accessToken) { + return ctx.authorization.resolveAccessToken(message.accessToken, function resolveFinish (err, auth) { + if (err) { + console.log(`${LOG_ERROR} Authorization failed for accessToken:`, message.accessToken); + + if (shouldCallBack) { + returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN }); + } + return err; + } + else { + return self.subscribeAuthorized(socket, message, auth, returnCallback); + } + }); + } + + console.log(`${LOG_ERROR} Authorization failed for message:`, message); + if (shouldCallBack) { + returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN}); + } + }; + + + /** + * Subscribe already authorized Socket.IO client to his rooms + * @param {Object} socket + * @param {Object} message input message from the client + * @param {Object} auth authorization of the client + * @param {Function} returnCallback function for returning a value back to the client + */ + self.subscribeAuthorized = function subscribeAuthorized (socket, message, auth, returnCallback) { + const shouldCallBack = typeof(returnCallback) === 'function'; + const enabledCols = app.get('enabledCollections'); + const cols = Array.isArray(message.collections) ? message.collections : enabledCols; + const subscribed = []; + + for (const col of cols) { + if (enabledCols.includes(col)) { + const permission = (col === 'settings') ? `api:${col}:admin` : `api:${col}:read`; + + if (ctx.authorization.checkMultiple(permission, auth.shiros)) { + socket.join(col); + subscribed.push(col); + } + } + } + + const doc = subscribed.length > 0 + ? { success: true, collections: subscribed } + : { success: false, message: apiConst.MSG.SOCKET_UNAUTHORIZED_TO_ANY }; + if (shouldCallBack) { + returnCallback(doc); + } + return doc; + }; + + + /** + * Emit create event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitCreate = function emitCreate (event) { + self.namespace.to(event.colName) + .emit('create', event); + }; + + + /** + * Emit update event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitUpdate = function emitUpdate (event) { + self.namespace.to(event.colName) + .emit('update', event); + }; + + + /** + * Emit delete event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitDelete = function emitDelete (event) { + self.namespace.to(event.colName) + .emit('delete', event); + } +} + +module.exports = StorageSocket; \ No newline at end of file diff --git a/lib/api3/swagger.js b/lib/api3/swagger.js new file mode 100644 index 00000000000..ff965061c87 --- /dev/null +++ b/lib/api3/swagger.js @@ -0,0 +1,41 @@ +'use strict'; + +const express = require('express') + , fs = require('fs') + ; + + +function setupSwaggerUI (app) { + + const serveSwaggerDef = function serveSwaggerDef (req, res) { + res.sendFile(__dirname + '/swagger.yaml'); + }; + app.get('/swagger', serveSwaggerDef); + + const swaggerUiAssetPath = require('swagger-ui-dist').getAbsoluteFSPath(); + const swaggerFiles = express.static(swaggerUiAssetPath); + + const urlRegex = /url: "[^"]*",/; + + const patchIndex = function patchIndex (req, res) { + const indexContent = fs.readFileSync(`${swaggerUiAssetPath}/index.html`) + .toString() + .replace(urlRegex, 'url: "../swagger.yaml",'); + res.send(indexContent); + }; + + app.get('/swagger-ui-dist', function getSwaggerRoot (req, res) { + let targetUrl = req.originalUrl; + if (!targetUrl.endsWith('/')) { + targetUrl += '/'; + } + targetUrl += 'index.html'; + res.redirect(targetUrl); + }); + app.get('/swagger-ui-dist/index.html', patchIndex); + + app.use('/swagger-ui-dist', swaggerFiles); +} + + +module.exports = setupSwaggerUI; \ No newline at end of file diff --git a/lib/api3/swagger.yaml b/lib/api3/swagger.yaml new file mode 100644 index 00000000000..5cbb2a05544 --- /dev/null +++ b/lib/api3/swagger.yaml @@ -0,0 +1,1647 @@ +openapi: 3.0.0 +servers: + - url: '/api/v3' +info: + version: "3.0.1" + title: Nightscout API + contact: + name: NS development discussion channel + url: https://gitter.im/nightscout/public + license: + name: AGPL 3 + url: 'https://www.gnu.org/licenses/agpl.txt' + description: + Nightscout API v3 is a component of cgm-remote-monitor project. It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange. + + + API v3 uses these environment variables, among other things: + + - Security switch (optional, default = `true`) +
API3_SECURITY_ENABLE=true
+ You can turn the whole security mechanism off, e.g. for debugging or development purposes, + but this should never be set to false in production. + + + - Number of minutes of acceptable time skew between client's and server's clock (optional, default = 5) +
API3_TIME_SKEW_TOLERANCE=5
+ This security parameter is used for preventing anti-replay attacks, specifically when checking the time from `Date` header. + + + - Maximum limit count of documents retrieved from single query +
API3_MAX_LIMIT=1000
+ + + - Autopruning of obsolete documents (optional, default is only `DEVICESTATUS`=60) +
API3_AUTOPRUNE_DEVICESTATUS=60
+
+      API3_AUTOPRUNE_ENTRIES=365
+
+      API3_AUTOPRUNE_TREATMENTS=120
+      
+ You can specify for which collections autopruning will be activated and length of retention period in days, e.g. "Hold 60 days of devicestatus, automatically delete older documents, hold 365 days of treatments and entries, automatically delete older documents." + + + - Fallback deduplication switch (optional, default = true) +
API3_DEDUP_FALLBACK_ENABLED=true
+ API3 uses the `identifier` field for document identification and mutual distinction within a single collection. There is automatic deduplication implemented matching the equal `identifier` field. E.g. `CREATE` operation for document having the same `identifier` as another one existing in the database is automatically transformed into `UPDATE` operation of the document found in the database. + + Documents not created via API v3 usually does not have any `identifier` field, but we would like to have some form of deduplication for them, too. This fallback deduplication is turned on by having set `API3_DEDUP_FALLBACK_ENABLED` to `true`. + When searching the collection in database, the document is found to be a duplicate only when either he has equal `identifier` or he has no `identifier` and meets: +
`devicestatus` collection: equal combination of `created_at` and `device`
+
+      `entries` collection:      equal combination of `date` and `type`
+
+      `food` collection:         equal `created_at`
+
+      `profile` collection:      equal `created_at`
+
+      `treatments` collection:   equal combination of `created_at` and `eventType`
+      
+ + + - Fallback switch for adding `created_at` field along the `date` field (optional, default = true) +
API3_CREATED_AT_FALLBACK_ENABLED=true
+ Standard APIv3 document model uses only `date` field for storing a timestamp of the event recorded by the document. But there is a fallback option to fill `created_at` field as well automatically on each insert/update, just to keep all older components working. + +tags: + - name: generic + description: Generic operations with each database collection (devicestatus, entries, food, profile, settings, treatments) + - name: other + description: All other various operations + + +paths: + /{collection}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + ###################################################################################### + get: + tags: + - generic + summary: 'SEARCH: Search documents from the collection' + operationId: SEARCH + description: General search operation through documents of one collection, matching the specified filtering criteria. You can apply: + + + 1) filtering - combining any number of filtering parameters + + + 2) ordering - using `sort` or `sort$desc` parameter + + + 3) paging - using `limit` and `skip` parameters + + + When there is no document matching the filtering criteria, HTTP status 204 is returned with empty response content. Otherwise HTTP 200 code is returned with JSON array of matching documents as a response content. + + + This operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`). + + + The only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings. + + + parameters: + - $ref: '#/components/parameters/filterParams' + - $ref: '#/components/parameters/sortParam' + - $ref: '#/components/parameters/sortDescParam' + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/skipParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/search200' + 204: + $ref: '#/components/responses/search204' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' + + + ###################################################################################### + post: + tags: + - generic + summary: 'CREATE: Inserts a new document into the collection' + description: + Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified and with an empty response content. `identifier` can be parsed from the `Location` response header. + + + When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 204 HTTP status code along with `Last-Modified` and correct `Location` headers. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `create` (and/or `update` for deduplication) permission for the API and the collection (e.g. `api:treatments:create` and `api:treatments:update`) + + requestBody: + description: JSON with new document to insert + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - apiKeyAuth: [] + + responses: + 201: + $ref: '#/components/responses/201CreatedLocation' + 204: + $ref: '#/components/responses/204NoContentLocation' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 422: + $ref: '#/components/responses/422UnprocessableEntity' + + + #return HTTP STATUS 400 for all other verbs (PUT, PATCH, DELETE,...) + + + /{collection}/{identifier}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + - in: path + name: identifier + description: Identifier of the document to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramIdentifier' + + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + ###################################################################################### + get: + tags: + - generic + summary: 'READ: Retrieves a single document from the collection' + description: + Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code. + + + If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + + + When `If-Modified-Since` header is used and its value is greater than the timestamp of the document in the collection, 304 HTTP status code with empty response content is returned. It means that the document has not been modified on server since the last retrieval to client side. + With `If-Modified-Since` header and less or equal timestamp `srvModified` a normal 200 HTTP status with full response is returned. + + + This operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`) + + parameters: + - $ref: '#/components/parameters/ifModifiedSinceHeader' + - $ref: '#/components/parameters/fieldsParam' + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/read200' + 304: + $ref: '#/components/responses/304NotModified' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' + 410: + $ref: '#/components/responses/410Gone' + + + ###################################################################################### + put: + tags: + - generic + summary: 'UPDATE: Updates a document in the collection' + description: + Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 204 HTTP status code will be returned with empty response body. + + + If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + + + When no document with `identifier` has been found in the collection, then an insert operation takes place instead of updating. Finally 201 HTTP status code is returned with only `Last-Modified` header (`identifier` is already known from the path parameter). + + + You can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `update` (and/or `create`) permission for the API and the collection (e.g. `api:treatments:update` and `api:treatments:create`) + + parameters: + - $ref: '#/components/parameters/ifUnmodifiedSinceHeader' + + requestBody: + description: JSON of new version of document (`identifier` in JSON is ignored if present) + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - apiKeyAuth: [] + + responses: + 201: + $ref: '#/components/responses/201Created' + 204: + $ref: '#/components/responses/204NoContentLocation' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 412: + $ref: '#/components/responses/412PreconditionFailed' + 410: + $ref: '#/components/responses/410Gone' + 422: + $ref: '#/components/responses/422UnprocessableEntity' + + + ###################################################################################### + patch: + tags: + - generic + summary: 'PATCH: Partially updates document in the collection' + description: + Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 204 HTTP status code will be returned with empty response body. + + + If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned. + + + When no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code. + + + You can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems. + + + `PATCH` operation can save some bandwidth for incremental document updates in comparison with `GET` - `UPDATE` operation sequence. + + + While patching the document, the field `modifiedBy` is automatically set to the authorized subject's name. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `update` permission for the API and the collection (e.g. `api:treatments:update`) + + parameters: + - $ref: '#/components/parameters/ifUnmodifiedSinceHeader' + + requestBody: + description: JSON of new version of document (`identifier` in JSON is ignored if present) + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - apiKeyAuth: [] + + responses: + 204: + $ref: '#/components/responses/204NoContentLocation' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 412: + $ref: '#/components/responses/412PreconditionFailed' + 410: + $ref: '#/components/responses/410Gone' + 422: + $ref: '#/components/responses/422UnprocessableEntity' + + + ###################################################################################### + delete: + tags: + - generic + summary: 'DELETE: Deletes a document from the collection' + description: + If the document has already been deleted, the operation will succeed anyway. Normally, documents are not really deleted from the collection but they are only marked as deleted. For special cases the deletion can be irreversible using `permanent` parameter. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `delete` permission for the API and the collection (e.g. `api:treatments:delete`) + + + parameters: + - $ref: '#/components/parameters/permanentParam' + + security: + - apiKeyAuth: [] + + responses: + 204: + description: Successful operation - empty response + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 422: + $ref: '#/components/responses/422UnprocessableEntity' + + + ###################################################################################### + /{collection}/history: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + get: + tags: + - generic + summary: 'HISTORY: Retrieves incremental changes since timestamp' + operationId: HISTORY + description: + HISTORY operation is intended for continuous data synchronization with other systems. + + Every insertion, update and deletion will be included in the resulting JSON array of documents (since timestamp in `Last-Modified` request header value). All changes are listed chronologically in response with 200 HTTP status code. The maximum listed `srvModified` timestamp is also stored in `Last-Modified` and `ETag` response headers that you can use for future, directly following synchronization. You can also limit the array's length using `limit` parameter. + + + Deleted documents will appear with `isValid` = `false` field. + + + When there is no change detected since the timestamp the operation ends with 204 HTTP status code and empty response content. + + + HISTORY operation has a fallback mechanism in place for documents, which were not created by API v3. For such documents `srvModified` is virtually assigned from the `date` field (for `entries` collection) or from the `created_at` field (for other collections). + + + This operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`) + + + The only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings. + + + parameters: + - $ref: '#/components/parameters/lastModifiedRequiredHeader' + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/history200' + 204: + $ref: '#/components/responses/history204' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' + + + ###################################################################################### + /{collection}/history/{lastModified}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + - in: path + name: lastModified + description: Starting timestamp (in UNIX epoch format, defined with respect to server's clock) since which the changes in documents are to be listed. Query for modified documents is made using "greater than" operator (not including equal timestamps). + required: true + schema: + type: integer + format: int64 + + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + get: + tags: + - generic + summary: 'HISTORY: Retrieves incremental changes since timestamp' + operationId: HISTORY2 + description: + This HISTORY operation variant is more precise than the previous one with `Last-Modified` request HTTP header), because it does not loose milliseconds precision. + + + Since this variant queries for changed documents by timestamp precisely and exclusively, the last modified document does not repeat itself in following calls. That is the reason why is this variant more suitable for continuous synchronization with other systems. + + + This variant behaves quite the same as the previous one in all other aspects. + + + parameters: + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/history200' + 204: + $ref: '#/components/responses/history204' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' + + + ###################################################################################### + /version: + + get: + tags: + - other + summary: 'VERSION: Returns actual version information' + description: No authentication is needed for this commnad (it is public) + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + + + ###################################################################################### + /status: + + get: + tags: + - other + summary: 'STATUS: Returns actual version information and all permissions granted for API' + description: + This operation requires authorization in contrast with VERSION operation. + + security: + - apiKeyAuth: [] + + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + + ###################################################################################### + /lastModified: + parameters: + - $ref: '#/components/parameters/dateHeader' + - $ref: '#/components/parameters/nowParam' + - $ref: '#/components/parameters/tokenParam' + + get: + tags: + - other + summary: 'LAST MODIFIED: Retrieves timestamp of the last modification of every collection' + operationId: LAST-MODIFIED + description: + LAST MODIFIED operation inspects collections separately (in parallel) and for each of them it finds the date of any last modification (insertion, update, deletion). + + Not only `srvModified`, but also `date` and `created_at` fields are inspected (as a fallback to previous API). + + + This operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to. + + security: + - apiKeyAuth: [] + + responses: + 200: + $ref: '#/components/responses/lastModified200' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + +###################################################################################### +components: + + parameters: + + dateHeader: + in: header + name: Date + schema: + type: string + required: false + description: + Timestamp (defined by client's clock) when the HTTP request was constructed on client. + This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. + This can be set alternatively in `now` query parameter. + + Example: + + +
Date: Wed, 17 Oct 2018 05:13:00 GMT
+ + + nowParam: + in: query + name: now + schema: + type: integer + format: int64 + required: false + description: + Timestamp (defined by client's clock) when the HTTP request was constructed on client. + This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code. + This can be set alternatively in `Date` header. + + + Example: + + +
now=1525383610088
+ + + tokenParam: + in: query + name: token + schema: + type: string + required: false + description: + An alternative way of authorization - passing accessToken in a query parameter. + + + Example: + + +
token=testadmin-bf2591231bd2c042
+ + + limitParam: + in: query + name: limit + schema: + type: integer + minimum: 1 + default: stored in API3_MAX_LIMIT environment variable (usually 1000) + example: 100 + description: Maximum number of documents to get in result array + + skipParam: + in: query + name: skip + schema: + type: integer + minimum: 0 + default: 0 + example: 0 + description: + Number of documents to skip from collection query before + loading them into result array (used for pagination) + + sortParam: + in: query + name: sort + schema: + type: string + required: false + description: + Field name by which the sorting of documents is performed. This parameter cannot be combined with `sort$desc` parameter. + + sortDescParam: + in: query + name: sort$desc + schema: + type: string + required: false + description: + Field name by which the descending (reverse) sorting of documents is performed. This parameter cannot be combined with `sort` parameter. + + permanentParam: + in: query + name: permanent + schema: + type: boolean + required: false + description: + If true, the deletion will be irreversible and it will not appear in `HISTORY` operation. Normally there is no reason for setting this flag. + + + fieldsParam: + in: query + name: fields + schema: + type: string + default: '_all' + required: false + examples: + all: + value: '_all' + summary: All fields will be returned (default behaviour) + customSet: + value: 'date,insulin' + summary: Only fields date and insulin will be returned + description: A chosen set of fields to return in response. Either you can enumerate specific fields of interest or use the predefined set. Sample parameter values: + + + _all: All fields will be returned (default value) + + + date,insulin: Only fields `date` and `insulin` will be returned + + + filterParams: + in: query + name: filter_parameters + schema: + type: string + description: + Any number of filtering operators. + + + Each filtering operator has name like `$`, e.g. `carbs$gt=2` which represents filtering rule "The field carbs must be present and greater than 2". + + + You can choose from operators: + + + `eq`=equals, `insulin$eq=1.5` + + + `ne`=not equals, `insulin$ne=1.5` + + + `gt`=greater than, `carbs$gt=30` + + + `gte`=greater than or equal, `carbs$gte=30` + + + `lt`=less than, `carbs$lt=30` + + + `lte`=less than or equal, `carbs$lte=30` + + + `in`=in specified set, `type$in=sgv|mbg|cal` + + + `nin`=not in specified set, `eventType$nin=Temp%20Basal|Temporary%20Target` + + + `re`=regex pattern, `eventType$re=Temp.%2A` + + + When filtering by field `date`, `created_at`, `srvModified` or `srvCreated`, you can choose from three input formats + + - Unix epoch in milliseconds (1525383610088) + + - Unix epoch in seconds (1525383610) + + - ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + + + The date is always queried in a normalized form - UTC with zero offset and with the correct format (1525383610088 for `date`, '2018-05-03T21:40:10.088Z' for `created_at`). + + lastModifiedRequiredHeader: + in: header + name: Last-Modified + schema: + type: string + required: true + description: + Starting timestamp (defined with respect to server's clock) since which the changes in documents are to be listed, formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + Example: + + +
Last-Modified: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ifModifiedSinceHeader: + in: header + name: If-Modified-Since + schema: + type: string + required: false + description: + Timestamp (defined with respect to server's clock) of the last document modification formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + If this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock. + + + Example: + + +
If-Modified-Since: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ifUnmodifiedSinceHeader: + in: header + name: If-Unmodified-Since + schema: + type: string + required: false + description: + Timestamp (defined with respect to server's clock) of the last document modification formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + If this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock. + + + Example: + + +
If-Unmodified-Since: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ###################################################################################### + responses: + + 201Created: + description: Successfully created a new document in collection + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + 201CreatedLocation: + description: Successfully created a new document in collection + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + 'Location': + $ref: '#/components/schemas/headerLocation' + + 204NoContent: + description: Successfully finished operation + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + 204NoContentLocation: + description: Successfully finished operation + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + 'Location': + $ref: '#/components/schemas/headerLocation' + + 304NotModified: + description: The document has not been modified on the server since timestamp specified in If-Modified-Since header + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + 400BadRequest: + description: The request is malformed. There may be some required parameters missing or there are unrecognized parameters present. + + 401Unauthorized: + description: The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy. + + 403Forbidden: + description: Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation. + + 404NotFound: + description: The collection or document specified was not found. + + 406NotAcceptable: + description: The requested content type (in `Accept` header) is not supported. + + 412PreconditionFailed: + description: The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header). + + 410Gone: + description: The requested document has already been deleted. + + 422UnprocessableEntity: + description: The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`). + + search200: + description: Successful operation returning array of documents matching the filtering criteria + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentArray' + text/csv: + schema: + $ref: '#/components/schemas/DocumentArray' + application/xml: + schema: + $ref: '#/components/schemas/DocumentArray' + + search204: + description: Successful operation - no documents matching the filtering criteria + + read200: + description: The document has been succesfully found and its JSON form returned in the response content. + content: + application/json: + schema: + $ref: '#/components/schemas/Document' + text/csv: + schema: + $ref: '#/components/schemas/Document' + application/xml: + schema: + $ref: '#/components/schemas/Document' + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + history200: + description: + Changed documents since specified timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentArray' + text/csv: + schema: + $ref: '#/components/schemas/DocumentArray' + application/xml: + schema: + $ref: '#/components/schemas/DocumentArray' + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModifiedMaximum' + 'ETag': + $ref: '#/components/schemas/headerEtagLastModifiedMaximum' + + history204: + description: No changes detected + + lastModified200: + description: Successful operation returning the timestamps + content: + application/json: + schema: + $ref: '#/components/schemas/LastModifiedResult' + + ###################################################################################### + schemas: + + headerLocation: + type: string + description: + Location of document - the relative part of URL. This can be used to parse the identifier + of just created document. + + Example=/api/v3/treatments/53409478-105f-11e9-ab14-d663bd873d93 + + headerLastModified: + type: string + description: + Timestamp of the last document modification on the server, formatted as + + ', :: GMT'. + + This field is relevant only for documents which were somehow modified by API v3 + (inserted, updated or deleted) and it was generated using server's clock. + + Example='Wed, 17 Oct 2018 05:13:00 GMT' + + headerLastModifiedMaximum: + type: string + description: + The latest (maximum) `srvModified` field of all returning documents, formatted as + + ', :: GMT'. + + Example='Wed, 17 Oct 2018 05:13:00 GMT' + + headerEtagLastModifiedMaximum: + type: string + description: + The latest (maximum) `srvModified` field of all returning documents. + This header does not loose milliseconds from the date (unlike the `Last-Modified` header). + + Example='W/"1525383610088"' + + paramCollection: + type: string + enum: + - devicestatus + - entries + - food + - profile + - settings + - treatments + example: 'treatments' + + paramIdentifier: + type: string + example: '53409478-105f-11e9-ab14-d663bd873d93' + + + DocumentBase: + description: Shared base for all documents + properties: + identifier: + description: + Main addressing, required field that identifies document in the collection. + + + The client should not create the identifier, the server automatically assigns it when the document is inserted. + + + The server calculates the identifier in such a way that duplicate records are automatically merged (deduplicating is made by `date`, `device` and `eventType` fields). + + + The best practise for all applications is not to loose identifiers from received documents, but save them carefully for other consumer applications/systems. + + + API v3 has a fallback mechanism in place, for documents without `identifier` field the `identifier` is set to internal `_id`, when reading or addressing these documents. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + type: string + example: '53409478-105f-11e9-ab14-d663bd873d93' + + date: + type: integer + format: int64 + description: + Required timestamp when the record or event occured, you can choose from three input formats + + - Unix epoch in milliseconds (1525383610088) + + - Unix epoch in seconds (1525383610) + + - ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + + + The date is always stored in a normalized form - UTC with zero offset. If UTC offset was present, it is going to be set in the `utcOffset` field. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + utcOffset: + type: integer + description: + Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the `date` field. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 120 + + app: + type: string + description: + Application or system in which the record was entered by human or device for the first time. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: xdrip + + device: + type: string + description: + The device from which the data originated (including serial number of the device, if it is relevant and safe). + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 'dexcom G5' + + _id: + description: Internally assigned database id. This field is for internal server purposes only, clients communicate with API by using identifier field. + type: string + example: '58e9dfbc166d88cc18683aac' + + srvCreated: + type: integer + format: int64 + description: + The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + subject: + type: string + description: + Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 'uploader' + + srvModified: + type: integer + format: int64 + description: + The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + modifiedBy: + type: string + description: + Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: admin + + isValid: + type: boolean + description: + A flag set by the server only for deleted documents. This field appears + only within history operation and for documents which were deleted by API v3 (and they always have a false value) + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: false + + + isReadOnly: + type: boolean + description: + A flag set by client that locks the document from any changes. Every document marked with `isReadOnly=true` is forever immutable and cannot even be deleted. + + + Any attempt to modify the read-only document will end with status 422 UNPROCESSABLE ENTITY. + + + example: true + + required: + - date + - app + + + DeviceStatus: + description: State of physical device, which is a technical part of the whole T1D compensation system + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Entry: + description: Blood glucose measurements and CGM calibrations + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + + type: + type: string + description: 'sgv, mbg, cal, etc' + + sgv: + type: number + description: The glucose reading. (only available for sgv types) + + direction: + type: string + description: Direction of glucose trend reported by CGM. (only available for sgv types) + example: '"DoubleDown", "SingleDown", "FortyFiveDown", "Flat", "FortyFiveUp", "SingleUp", "DoubleUp", "NOT COMPUTABLE", "RATE OUT OF RANGE" for xdrip' + + noise: + type: number + description: Noise level at time of reading. (only available for sgv types) + example: 'xdrip: 0, 1, 2=high, 3=high_for_predict, 4=very high, 5=extreme' + + filtered: + type: number + description: The raw filtered value directly from CGM transmitter. (only available for sgv types) + + unfiltered: + type: number + description: The raw unfiltered value directly from CGM transmitter. (only available for sgv types) + + rssi: + type: number + description: The signal strength from CGM transmitter. (only available for sgv types) + + units: + type: string + example: '"mg", "mmol"' + description: The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. + + + Food: + description: Nutritional values of food + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + + food: + type: string + description: 'food, quickpick' + + category: + type: string + description: Name for a group of related records + + subcategory: + type: string + description: Name for a second level of groupping + + name: + type: string + description: Name of the food described + + portion: + type: number + description: Number of units (e.g. grams) of the whole portion described + + unit: + type: string + example: '"g", "ml", "oz"' + description: Unit for the portion + + carbs: + type: number + description: Amount of carbs in the portion in grams + + fat: + type: number + description: Amount of fat in the portion in grams + + protein: + type: number + description: Amount of proteins in the portion in grams + + energy: + type: number + description: Amount of energy in the portion in kJ + + gi: + type: number + description: 'Glycemic index (1=low, 2=medium, 3=high)' + + hideafteruse: + type: boolean + description: Flag used for quickpick + + hidden: + type: boolean + description: Flag used for quickpick + + position: + type: number + description: Ordering field for quickpick + + portions: + type: number + description: component multiplier if defined inside quickpick compound + + foods: + type: array + description: Neighbour documents (from food collection) that together make a quickpick compound + items: + $ref: '#/components/schemas/Food' + + + Profile: + description: Parameters describing body functioning relative to T1D + compensation parameters + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Settings: + description: + A document representing persisted settings of some application or system (it could by Nightscout itself as well). This pack of options serves as a backup or as a shared centralized storage for multiple client instances. It is a probably good idea to `PATCH` the document instead of `UPDATE` operation, e.g. when changing one settings option in a client application. + + + `identifier` represents a client application name here, e.g. `xdrip` or `aaps`. + + + `Settings` collection has a more specific authorization required. For the `SEARCH` operation within this collection, you need an `admin` permission, such as `api:settings:admin`. The goal is to isolate individual client application settings. + + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Treatment: + description: T1D compensation action + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + eventType: + type: string + example: '"BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard"' + description: The type of treatment event. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + # created_at: + # type: string + # description: The date of the event, might be set retroactively. + glucose: + type: string + description: Current glucose. + glucoseType: + type: string + example: '"Sensor", "Finger", "Manual"' + description: Method used to obtain glucose, Finger or Sensor. + units: + type: string + example: '"mg/dl", "mmol/l"' + description: The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field when `glucose` is entered. + carbs: + type: number + description: Amount of carbs given. + protein: + type: number + description: Amount of protein given. + fat: + type: number + description: Amount of fat given. + insulin: + type: number + description: Amount of insulin, if any. + duration: + type: number + description: Duration in minutes. + preBolus: + type: number + description: How many minutes the bolus was given before the meal started. + splitNow: + type: number + description: Immediate part of combo bolus (in percent). + splitExt: + type: number + description: Extended part of combo bolus (in percent). + percent: + type: number + description: Eventual basal change in percent. + absolute: + type: number + description: Eventual basal change in absolute value (insulin units per hour). + targetTop: + type: number + description: Top limit of temporary target. + targetBottom: + type: number + description: Bottom limit of temporary target. + profile: + type: string + description: Name of the profile to which the pump has been switched. + reason: + type: string + description: For example the reason why the profile has been switched or why the temporary target has been set. + notes: + type: string + description: Description/notes of treatment. + enteredBy: + type: string + description: Who entered the treatment. + + + DocumentToPost: + description: Single document + type: object + oneOf: + - $ref: '#/components/schemas/DeviceStatus' + - $ref: '#/components/schemas/Entry' + - $ref: '#/components/schemas/Food' + - $ref: '#/components/schemas/Profile' + - $ref: '#/components/schemas/Settings' + - $ref: '#/components/schemas/Treatment' + example: + 'identifier': '53409478-105f-11e9-ab14-d663bd873d93' + 'date': 1532936118000 + 'utcOffset': 120 + 'carbs': 10 + 'insulin': 1 + 'eventType': 'Snack Bolus' + 'app': 'xdrip' + 'subject': 'uploader' + + + Document: + description: Single document + xml: + name: 'item' + type: object + oneOf: + - $ref: '#/components/schemas/DeviceStatus' + - $ref: '#/components/schemas/Entry' + - $ref: '#/components/schemas/Food' + - $ref: '#/components/schemas/Profile' + - $ref: '#/components/schemas/Settings' + - $ref: '#/components/schemas/Treatment' + example: + 'identifier': '53409478-105f-11e9-ab14-d663bd873d93' + 'date': 1532936118000 + 'utcOffset': 120 + 'carbs': 10 + 'insulin': 1 + 'eventType': 'Snack Bolus' + 'srvCreated': 1532936218000 + 'srvModified': 1532936218000 + 'app': 'xdrip' + 'subject': 'uploader' + 'modifiedBy': 'admin' + + + DeviceStatusArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/DeviceStatus' + + + EntryArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Entry' + + + FoodArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Food' + + + ProfileArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Profile' + + + SettingsArray: + description: Array of settings + type: array + items: + $ref: '#/components/schemas/Settings' + + + TreatmentArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Treatment' + + + DocumentArray: + type: object + xml: + name: 'items' + oneOf: + - $ref: '#/components/schemas/DeviceStatusArray' + - $ref: '#/components/schemas/EntryArray' + - $ref: '#/components/schemas/FoodArray' + - $ref: '#/components/schemas/ProfileArray' + - $ref: '#/components/schemas/SettingsArray' + - $ref: '#/components/schemas/TreatmentArray' + + + Version: + description: Information about versions + type: object + properties: + + version: + description: The whole Nightscout instance version + type: string + example: '0.10.2-release-20171201' + + apiVersion: + description: API v3 subsystem version + type: string + example: '3.0.0' + + srvDate: + description: Actual server date and time in UNIX epoch format + type: number + example: 1532936118000 + + storage: + type: object + properties: + + type: + description: Type of storage engine used + type: string + example: 'mongodb' + + version: + description: Version of the storage engine + type: string + example: '4.0.6' + + + Status: + description: Information about versions and API permissions + allOf: + - $ref: '#/components/schemas/Version' + - type: object + properties: + + apiPermissions: + type: object + properties: + devicestatus: + type: string + example: 'crud' + entries: + type: string + example: 'r' + food: + type: string + example: 'crud' + profile: + type: string + example: 'r' + treatments: + type: string + example: 'crud' + + + LastModifiedResult: + description: Result of LAST MODIFIED operation + properties: + srvDate: + description: + Actual storage server date (Unix epoch in ms). + type: integer + format: int64 + example: 1556260878776 + + collections: + type: object + description: + Collections which the user have read access to. + properties: + devicestatus: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1556260760974 + treatments: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1553374184169 + entries: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1556260758768 + profile: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1548524042744 + + ###################################################################################### + securitySchemes: + + accessToken: + type: apiKey + name: token + in: query + description: >- + Add token as query item in the URL or as HTTP header. You can manage access token in + `/admin`. + + Each operation requires a specific permission that has to be granted (via security role) to the security subject, which was authenticated by `token` parameter/header or `JWT`. E.g. for creating new `devicestatus` document via API you need `api:devicestatus:create` permission. + + jwtoken: + type: http + scheme: bearer + description: Use this if you know the temporary json webtoken. + bearerFormat: JWT \ No newline at end of file diff --git a/lib/authorization/index.js b/lib/authorization/index.js index 0c0c6f5f2a7..b81578e0dca 100644 --- a/lib/authorization/index.js +++ b/lib/authorization/index.js @@ -55,6 +55,8 @@ function init (env, ctx) { return token; } + authorization.extractToken = extractToken; + function authorizeAccessToken (req) { var accessToken = req.query.token; @@ -139,22 +141,25 @@ function init (env, ctx) { authorization.resolve = function resolve (data, callback) { + var defaultShiros = storage.rolesToShiros(defaultRoles); + + if (storage.doesAccessTokenExist(data.api_secret)) { + authorization.resolveAccessToken (data.api_secret, callback, defaultShiros); + return; + } + if (authorizeAdminSecret(data.api_secret)) { var admin = shiroTrie.new(); admin.add(['*']); return callback(null, { shiros: [ admin ] }); } - var defaultShiros = storage.rolesToShiros(defaultRoles); - if (data.token) { jwt.verify(data.token, env.api_secret, function result(err, verified) { if (err) { return callback(err, { shiros: [ ] }); } else { - var resolved = storage.resolveSubjectAndPermissions(verified.accessToken); - var shiros = resolved.shiros.concat(defaultShiros); - return callback(null, { shiros: shiros, subject: resolved.subject }); + authorization.resolveAccessToken (verified.accessToken, callback, defaultShiros); } }); } else { @@ -163,10 +168,25 @@ function init (env, ctx) { }; + authorization.resolveAccessToken = function resolveAccessToken (accessToken, callback, defaultShiros) { + + if (!defaultShiros) { + defaultShiros = storage.rolesToShiros(defaultRoles); + } + + let resolved = storage.resolveSubjectAndPermissions(accessToken); + if (!resolved || !resolved.subject) { + return callback('Subject not found', null); + } + + let shiros = resolved.shiros.concat(defaultShiros); + return callback(null, { shiros: shiros, subject: resolved.subject }); + }; + authorization.isPermitted = function isPermitted (permission, opts) { - opts = mkopts(opts); + mkopts(opts); authorization.seenPermissions = _.chain(authorization.seenPermissions) .push(permission) .sort() @@ -177,6 +197,25 @@ function init (env, ctx) { var remoteIP = getRemoteIP(req); + var secret = adminSecretFromRequest(req); + var defaultShiros = storage.rolesToShiros(defaultRoles); + + if (storage.doesAccessTokenExist(secret)) { + var resolved = storage.resolveSubjectAndPermissions (secret); + + if (authorization.checkMultiple(permission, resolved.shiros)) { + console.log(LOG_GRANTED, remoteIP, resolved.accessToken , permission); + next(); + } else if (authorization.checkMultiple(permission, defaultShiros)) { + console.log(LOG_GRANTED, remoteIP, resolved.accessToken, permission, 'default'); + next( ); + } else { + console.log(LOG_DENIED, remoteIP, resolved.accessToken, permission); + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); + } + return; + } + if (authorizeAdminSecretWithRequest(req)) { console.log(LOG_GRANTED, remoteIP, 'api-secret', permission); next( ); @@ -184,7 +223,6 @@ function init (env, ctx) { } var token = extractToken(req); - var defaultShiros = storage.rolesToShiros(defaultRoles); if (token) { jwt.verify(token, env.api_secret, function result(err, verified) { diff --git a/lib/authorization/storage.js b/lib/authorization/storage.js index 9999b9f3178..c032018d170 100644 --- a/lib/authorization/storage.js +++ b/lib/authorization/storage.js @@ -24,7 +24,7 @@ function init (env, ctx) { function create (collection) { function doCreate(obj, fn) { - if (!obj.hasOwnProperty('created_at')) { + if (!Object.prototype.hasOwnProperty.call(obj, 'created_at')) { obj.created_at = (new Date()).toISOString(); } collection.insert(obj, function (err, doc) { @@ -122,6 +122,12 @@ function init (env, ctx) { , { name: 'activity', permissions: [ 'api:activity:create' ] } ]; + storage.getSHA1 = function getSHA1 (message) { + var shasum = crypto.createHash('sha1'); + shasum.update(message); + return shasum.digest('hex'); + } + storage.reload = function reload (callback) { storage.listRoles({sort: {name: 1}}, function listResults (err, results) { @@ -152,6 +158,7 @@ function init (env, ctx) { var abbrev = subject.name.toLowerCase().replace(/[\W]/g, '').substring(0, 10); subject.digest = shasum.digest('hex'); subject.accessToken = abbrev + '-' + subject.digest.substring(0, 16); + subject.accessTokenDigest = storage.getSHA1(subject.accessToken); } return subject; @@ -200,17 +207,28 @@ function init (env, ctx) { }; storage.findSubject = function findSubject (accessToken) { - var prefix = _.last(accessToken.split('-')); + + if (!accessToken) return null; + + var split_token = accessToken.split('-'); + var prefix = split_token ? _.last(split_token) : ''; if (prefix.length < 16) { return null; } return _.find(storage.subjects, function matches (subject) { - return subject.digest.indexOf(prefix) === 0; + return subject.accessTokenDigest.indexOf(accessToken) === 0 || subject.digest.indexOf(prefix) === 0; }); }; + storage.doesAccessTokenExist = function doesAccessTokenExist(accessToken) { + if (storage.findSubject(accessToken)) { + return true; + } + return false; + } + storage.resolveSubjectAndPermissions = function resolveSubjectAndPermissions (accessToken) { var shiros = []; diff --git a/lib/bus.js b/lib/bus.js index 5828130fa92..65faeea21a6 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -1,11 +1,11 @@ 'use strict'; - var Stream = require('stream'); function init (settings) { var beats = 0; var started = new Date( ); var interval = settings.heartbeat * 1000; + let busInterval; var stream = new Stream; @@ -24,9 +24,15 @@ function init (settings) { stream.emit('tick', ictus( )); } + stream.teardown = function ( ) { + console.log('Initiating server teardown'); + clearInterval(busInterval); + stream.emit('teardown'); + }; + stream.readable = true; stream.uptime = repeat; - setInterval(repeat, interval); + busInterval = setInterval(repeat, interval); return stream; } module.exports = init; diff --git a/lib/client/browser-settings.js b/lib/client/browser-settings.js index 4f990eb57f3..0a8e1469a11 100644 --- a/lib/client/browser-settings.js +++ b/lib/client/browser-settings.js @@ -71,6 +71,8 @@ function init (client, serverSettings, $) { $('#basalrender').val(settings.extendedSettings.basal ? settings.extendedSettings.basal.render : 'none'); + $('#bolusrender').val(settings.extendedSettings.bolus ? settings.extendedSettings.bolus.render : 'all'); + if (settings.timeFormat === 24) { $('#24-browser').prop('checked', true); } else { @@ -135,7 +137,24 @@ function init (client, serverSettings, $) { }); $('#editprofilelink').toggle(settings.isEnabled('iob') || settings.isEnabled('cob') || settings.isEnabled('bwp') || settings.isEnabled('basal')); + + //fetches token from url + var parts = (location.search || '?').substring(1).split('&'); + var token = ''; + parts.forEach(function (val) { + if (val.startsWith('token=')) { + token = val.substring('token='.length); + } + }); + //if there is a token, append it to each of the links in the hamburger menu + if (token != '') { + token = '?token=' + token; + $('#reportlink').attr('href', 'report' + token); + $('#editprofilelink').attr('href', 'profile' + token); + $('#admintoolslink').attr('href', 'admin' + token); + $('#editfoodlink').attr('href', 'food' + token); + } } function wireForm () { @@ -144,6 +163,7 @@ function init (client, serverSettings, $) { storage.remove(name); }); storage.remove('basalrender'); + storage.remove('bolusrender'); event.preventDefault(); client.browserUtils.reload(); }); @@ -196,6 +216,7 @@ function init (client, serverSettings, $) { , language: $('#language').val() , scaleY: $('#scaleY').val() , basalrender: $('#basalrender').val() + , bolusrender: $('#bolusrender').val() , showPlugins: checkedPluginNames() , storageVersion: STORAGE_VERSION }); @@ -239,6 +260,7 @@ function init (client, serverSettings, $) { settings.thresholds = serverSettings.settings.thresholds; } + if (serverSettings.settings.enable) { settings.enable = serverSettings.settings.enable; } @@ -251,8 +273,15 @@ function init (client, serverSettings, $) { settings.extendedSettings.basal = {}; } - var stored = storage.get('basalrender'); - settings.extendedSettings.basal.render = stored !== null ? stored : settings.extendedSettings.basal.render; + var basalStored = storage.get('basalrender'); + settings.extendedSettings.basal.render = basalStored !== null ? basalStored : settings.extendedSettings.basal.render; + + if (!settings.extendedSettings.bolus) { + settings.extendedSettings.bolus = {}; + } + + var bolusStored = storage.get('bolusrender'); + settings.extendedSettings.bolus.render = bolusStored !== null ? bolusStored : settings.extendedSettings.bolus.render; } catch (err) { console.error(err); diff --git a/lib/client/browser-utils.js b/lib/client/browser-utils.js index 4f920588f80..634afb2ab40 100644 --- a/lib/client/browser-utils.js +++ b/lib/client/browser-utils.js @@ -51,7 +51,7 @@ function init ($) { function queryParms () { var params = {}; - if (location.search) { + if ((typeof location !== 'undefined') && location.search) { location.search.substr(1).split('&').forEach(function(item) { // eslint-disable-next-line no-useless-escape params[item.split('=')[0]] = item.split('=')[1].replace(/[_\+]/g, ' '); diff --git a/lib/client/careportal.js b/lib/client/careportal.js index 4a89ffcebc8..a6edfe3ec22 100644 --- a/lib/client/careportal.js +++ b/lib/client/careportal.js @@ -4,6 +4,7 @@ var moment = require('moment-timezone'); var _ = require('lodash'); var parse_duration = require('parse-duration'); // https://www.npmjs.com/package/parse-duration var times = require('../times'); +var consts = require('../constants'); var Storages = require('js-storage'); function init (client, $) { @@ -13,12 +14,6 @@ function init (client, $) { var storage = Storages.localStorage; var units = client.settings.units; - careportal.allEventTypes = client.plugins.getAllEventTypes(client.sbx); - - careportal.events = _.map(careportal.allEventTypes, function each (event) { - return _.pick(event, ['val', 'name']); - }); - var eventTime = $('#eventTimeValue'); var eventDate = $('#eventDateValue'); @@ -44,10 +39,25 @@ function init (client, $) { } var inputMatrix = {}; + var submitHooks = {}; + + function refreshEventTypes() { + careportal.allEventTypes = client.plugins.getAllEventTypes(client.sbx); + + careportal.events = _.map(careportal.allEventTypes, function each (event) { + return _.pick(event, ['val', 'name']); + }); + + inputMatrix = {}; + submitHooks = {}; + + _.forEach(careportal.allEventTypes, function each (event) { + inputMatrix[event.val] = _.pick(event, ['otp','remoteCarbs', 'remoteAbsorption', 'remoteBolus', 'bg', 'insulin', 'carbs', 'protein', 'fat', 'prebolus', 'duration', 'percent', 'absolute', 'profile', 'split', 'reasons', 'targets']); + submitHooks[event.val] = event.submitHook; + }); + } - _.forEach(careportal.allEventTypes, function each (event) { - inputMatrix[event.val] = _.pick(event, ['bg', 'insulin', 'carbs', 'protein', 'fat', 'prebolus', 'duration', 'percent', 'absolute', 'profile', 'split', 'reasons', 'targets']); - }); + refreshEventTypes(); careportal.filterInputs = function filterInputs (event) { var eventType = $('#eventType').val(); @@ -70,11 +80,18 @@ function init (client, $) { $('#reasonLabel').css('display', displayType(reasons && reasons.length > 0)); $('#targets').css('display', displayType(inputMatrix[eventType]['targets'])); + $('#otpLabel').css('display', displayType(inputMatrix[eventType]['otp'])); + $('#remoteCarbsLabel').css('display', displayType(inputMatrix[eventType]['remoteCarbs'])); + $('#remoteAbsorptionLabel').css('display', displayType(inputMatrix[eventType]['remoteAbsorption'])); + $('#remoteBolusLabel').css('display', displayType(inputMatrix[eventType]['remoteBolus'])); + $('#bg').css('display', displayType(inputMatrix[eventType]['bg'])); $('#insulinGivenLabel').css('display', displayType(inputMatrix[eventType]['insulin'])); + $('#carbsGivenLabel').css('display', displayType(inputMatrix[eventType]['carbs'])); $('#proteinGivenLabel').css('display', displayType(inputMatrix[eventType]['protein'])); $('#fatGivenLabel').css('display', displayType(inputMatrix[eventType]['fat'])); + $('#durationLabel').css('display', displayType(inputMatrix[eventType]['duration'])); $('#percentLabel').css('display', displayType(inputMatrix[eventType]['percent'] && $('#absolute').val() === '')); $('#absoluteLabel').css('display', displayType(inputMatrix[eventType]['absolute'] && $('#percent').val() === '')); @@ -84,11 +101,16 @@ function init (client, $) { $('#reason').empty(); _.each(reasons, function eachReason (reason) { - $('#reason').append(''); + $('#reason').append(''); }); careportal.reasonable(); + resetIfHidden(inputMatrix[eventType]['otp'], '#otp'); + resetIfHidden(inputMatrix[eventType]['remoteCarbs'], '#remoteCarbs'); + resetIfHidden(inputMatrix[eventType]['remoteAbsorption'], '#remoteAbsorption'); + resetIfHidden(inputMatrix[eventType]['remoteBolus'], '#remoteBolus'); + resetIfHidden(inputMatrix[eventType]['insulin'], '#insulinGiven'); resetIfHidden(inputMatrix[eventType]['carbs'], '#carbsGiven'); resetIfHidden(inputMatrix[eventType]['protein'], '#proteinGiven'); @@ -172,6 +194,8 @@ function init (client, $) { }; careportal.prepare = function prepare () { + refreshEventTypes(); + $('#profile').empty(); client.profilefunctions.listBasalProfiles().forEach(function(p) { $('#profile').append(''); @@ -180,6 +204,12 @@ function init (client, $) { $('#eventType').val(''); $('#glucoseValue').val('').attr('placeholder', translate('Value in') + ' ' + client.settings.units); $('#meter').prop('checked', true); + + $('#otp').val(''); + $('#remoteCarbs').val(''); + $('#remoteAbsorption').val(''); + $('#remoteBolus').val(''); + $('#carbsGiven').val(''); $('#proteinGiven').val(''); $('#fatGiven').val(''); @@ -196,11 +226,18 @@ function init (client, $) { }; function gatherData () { + var eventType = $('#eventType').val(); + var selectedReason = $('#reason').val(); + var data = { enteredBy: $('#enteredBy').val() - , eventType: $('#eventType').val() + , eventType: eventType + , otp: $('#otp').val() + , remoteCarbs: $('#remoteCarbs').val() + , remoteAbsorption: $('#remoteAbsorption').val() + , remoteBolus: $('#remoteBolus').val() , glucose: $('#glucoseValue').val().replace(',', '.') - , reason: $('#reason').val() + , reason: selectedReason , targetTop: $('#targetTop').val().replace(',', '.') , targetBottom: $('#targetBottom').val().replace(',', '.') , glucoseType: $('#treatment-form').find('input[name=glucoseType]:checked').val() @@ -211,14 +248,29 @@ function init (client, $) { , duration: times.msecs(parse_duration($('#duration').val())).mins < 1 ? $('#duration').val() : times.msecs(parse_duration($('#duration').val())).mins , percent: $('#percent').val() , profile: $('#profile').val() - , preBolus: parseInt($('#preBolus').val()) + , preBolus: $('#preBolus').val() , notes: $('#notes').val() , units: client.settings.units }; + data.preBolus = parseInt(data.preBolus); + + if (isNaN(data.preBolus)) { + delete data.preBolus; + } + + var reasons = inputMatrix[eventType]['reasons']; + var reason = _.find(reasons, function matches (r) { + return r.name === selectedReason; + }); + + if (reason) { + data.reasonDisplay = reason.displayName; + } + if (units == "mmol") { - data.targetTop = data.targetTop * 18; - data.targetBottom = data.targetBottom * 18; + data.targetTop = data.targetTop * consts.MMOL_TO_MGDL; + data.targetBottom = data.targetBottom * consts.MMOL_TO_MGDL; } //special handling for absolute to support temp to 0 @@ -231,6 +283,8 @@ function init (client, $) { data.eventTime = mergeDateAndTime().toDate(); } + data.created_at = data.eventTime ? data.eventTime.toISOString() : new Date().toISOString(); + if (!inputMatrix[data.eventType].profile) { delete data.profile; } @@ -242,6 +296,8 @@ function init (client, $) { if (data.eventType.indexOf('Temporary Target Cancel') > -1) { data.duration = 0; data.eventType = 'Temporary Target'; + data.targetBottom = ""; + data.targetTop = ""; } if (data.eventType.indexOf('Combo Bolus') > -1) { @@ -249,7 +305,14 @@ function init (client, $) { data.splitExt = parseInt($('#insulinSplitExt').val()) || 0; } - return data; + let d = {}; + Object.keys(data).forEach(function(key) { + if (data[key] !== "" && data[key] !== null) { + d[key] = data[key] + } + }); + + return d; } careportal.save = function save (event) { @@ -272,17 +335,17 @@ function init (client, $) { messages.push("Please enter a valid value for both top and bottom target to save a Temporary Target"); } else { - let targetTop = data.targetTop; - let targetBottom = data.targetBottom; + let targetTop = parseInt(data.targetTop); + let targetBottom = parseInt(data.targetBottom); - let minTarget = 4 * 18; - let maxTarget = 18 * 18; + let minTarget = 4 * consts.MMOL_TO_MGDL; + let maxTarget = 18 * consts.MMOL_TO_MGDL; if (units == "mmol") { - targetTop = Math.round(targetTop / 18.0 * 10) / 10; - targetBottom = Math.round(targetBottom / 18.0 * 10) / 10; - minTarget = Math.round(minTarget / 18.0 * 10) / 10; - maxTarget = Math.round(maxTarget / 18.0 * 10) / 10; + targetTop = Math.round(targetTop / consts.MMOL_TO_MGDL * 10) / 10; + targetBottom = Math.round(targetBottom / consts.MMOL_TO_MGDL * 10) / 10; + minTarget = Math.round(minTarget / consts.MMOL_TO_MGDL * 10) / 10; + maxTarget = Math.round(maxTarget / consts.MMOL_TO_MGDL * 10) / 10; } if (targetTop > maxTarget) { @@ -303,6 +366,8 @@ function init (client, $) { } } + // TODO: add check for remote (Bolus, Carbs, Absorption) + return { allOk , messages @@ -326,6 +391,11 @@ function init (client, $) { text[text.length - 1] += ' ' + translate('Cancel'); } + pushIf(data.remoteCarbs, translate('Remote Carbs') + ': ' + data.remoteCarbs); + pushIf(data.remoteAbsorption, translate('Remote Absorption') + ': ' + data.remoteAbsorption); + pushIf(data.remoteBolus, translate('Remote Bolus') + ': ' + data.remoteBolus); + pushIf(data.otp, translate('One Time Pascode') + ': ' + data.otp); + pushIf(data.glucose, translate('Blood Glucose') + ': ' + data.glucose); pushIf(data.glucose, translate('Measurement Method') + ': ' + translate(data.glucoseType)); @@ -335,8 +405,8 @@ function init (client, $) { var targetBottom = data.targetBottom; if (units == "mmol") { - targetTop = Math.round(data.targetTop / 18.0 * 10) / 10; - targetBottom = Math.round(data.targetBottom / 18.0 * 10) / 10; + targetTop = Math.round(data.targetTop / consts.MMOL_TO_MGDL * 10) / 10; + targetBottom = Math.round(data.targetBottom / consts.MMOL_TO_MGDL * 10) / 10; } pushIf(data.targetTop, translate('Target Top') + ': ' + targetTop); @@ -374,7 +444,19 @@ function init (client, $) { window.alert(messages); } else { if (window.confirm(buildConfirmText(data))) { - postTreatment(data); + var submitHook = submitHooks[data.eventType]; + if (submitHook) { + submitHook(client, data, function (error) { + if (error) { + console.log("submit error = ", error); + alert(translate('Error') + ': ' + error); + } else { + client.browserUtils.closeDrawer('#treatmentDrawer'); + } + }); + } else { + postTreatment(data); + } } } } diff --git a/lib/client/chart.js b/lib/client/chart.js index 32a15ba0155..d84dd0998b5 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -1,12 +1,24 @@ 'use strict'; -// var _ = require('lodash'); +var _ = require('lodash'); var times = require('../times'); var d3locales = require('./d3locales'); -var padding = { bottom: 30 }; +var scrolling = false + , scrollNow = 0 + , scrollBrushExtent = null + , scrollRange = null; + +var PADDING_BOTTOM = 30 + , OPEN_TOP_HEIGHT = 8 + , CONTEXT_MAX = 420 + , CONTEXT_MIN = 36 + , FOCUS_MAX = 510 + , FOCUS_MIN = 30; + +var loadTime = Date.now(); function init (client, d3, $) { - var chart = { }; + var chart = {}; var utils = client.utils; var renderer = client.renderer; @@ -23,146 +35,190 @@ function init (client, d3, $) { .attr('x', 0) .attr('y', 0) .append('g') - .style('fill', 'none') - .style('stroke', '#0099ff') - .style('stroke-width', 2) + .style('fill', 'none') + .style('stroke', '#0099ff') + .style('stroke-width', 2) .append('path').attr('d', 'M0,0 l' + dashWidth + ',' + dashWidth) .append('path').attr('d', 'M' + dashWidth + ',0 l-' + dashWidth + ',' + dashWidth); - + // arrow head defs.append('marker') - .attr({ - 'id': 'arrow', - 'viewBox': '0 -5 10 10', - 'refX': 5, - 'refY': 0, - 'markerWidth': 8, - 'markerHeight': 8, - 'orient': 'auto' - }) + .attr('id', 'arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 5) + .attr('refY', 0) + .attr('markerWidth', 8) + .attr('markerHeight', 8) + .attr('orient', 'auto') .append('path') - .attr('d', 'M0,-5L10,0L0,5') - .attr('class', 'arrowHead'); + .attr('d', 'M0,-5L10,0L0,5') + .attr('class', 'arrowHead'); + + var localeFormatter = d3.timeFormatLocale(d3locales.locale(client.settings.language)); + + function beforeBrushStarted () { + // go ahead and move the brush because + // a single click will not execute the brush event + var now = new Date(); + var dx = chart.xScale2(now) - chart.xScale2(new Date(now.getTime() - client.focusRangeMS)); + + var cx = d3.mouse(this)[0]; + var x0 = cx - dx / 2; + var x1 = cx + dx / 2; - var localeFormatter = d3.locale(d3locales.locale(client.settings.language)); - - function brushStarted ( ) { + var range = chart.xScale2.range(); + var X0 = range[0]; + var X1 = range[1]; + + var brush = x0 < X0 ? [X0, X0 + dx] : x1 > X1 ? [X1 - dx, X1] : [x0, x1]; + + chart.theBrush.call(chart.brush.move, brush); + } + + function brushStarted () { // update the opacity of the context data points to brush extent chart.context.selectAll('circle') .data(client.entries) .style('opacity', 1); } - function brushEnded ( ) { + function brushEnded () { // update the opacity of the context data points to brush extent + var selectedRange = chart.createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); + chart.context.selectAll('circle') .data(client.entries) - .style('opacity', function (d) { return renderer.highlightBrushPoints(d) }); + .style('opacity', function(d) { return renderer.highlightBrushPoints(d, from, to) }); } var extent = client.dataExtent(); var yScaleType; if (client.settings.scaleY === 'linear') { - yScaleType = d3.scale.linear; + yScaleType = d3.scaleLinear; } else { - yScaleType = d3.scale.log; + yScaleType = d3.scaleLog; } - var focusYDomain = [utils.scaleMgdl(30), utils.scaleMgdl(510)]; - var contextYDomain = [utils.scaleMgdl(36), utils.scaleMgdl(420)]; + var focusYDomain = [utils.scaleMgdl(FOCUS_MIN), utils.scaleMgdl(FOCUS_MAX)]; + var contextYDomain = [utils.scaleMgdl(CONTEXT_MIN), utils.scaleMgdl(CONTEXT_MAX)]; - function dynamicDomain() { + function dynamicDomain () { // allow y-axis to extend all the way to the top of the basal area, but leave room to display highest value var mult = 1.15 , targetTop = client.settings.thresholds.bgTargetTop // filter to only use actual SGV's (not rawbg's) to set the view window. // can switch to Logarithmic (non-dynamic) to see anything that doesn't fit in the dynamicDomain - , mgdlMax = d3.max(client.entries, function (d) { if ( d.type === 'sgv') { return d.mgdl; } }); - // use the 99th percentile instead of max to avoid rescaling for 1 flukey data point - // need to sort client.entries by mgdl first - //, mgdlMax = d3.quantile(client.entries, 0.99, function (d) { return d.mgdl; }); + , mgdlMax = d3.max(client.entries, function(d) { if (d.type === 'sgv') { return d.mgdl; } }); + // use the 99th percentile instead of max to avoid rescaling for 1 flukey data point + // need to sort client.entries by mgdl first + //, mgdlMax = d3.quantile(client.entries, 0.99, function (d) { return d.mgdl; }); return [ - utils.scaleMgdl(30) + utils.scaleMgdl(FOCUS_MIN) , Math.max(utils.scaleMgdl(mgdlMax * mult), utils.scaleMgdl(targetTop * mult)) ]; } - function dynamicDomainOrElse(defaultDomain) { - if (client.settings.scaleY === 'linear' || client.settings.scaleY === 'log-dynamic') { + function dynamicDomainOrElse (defaultDomain) { + if (client.entries && (client.entries.length > 0) && (client.settings.scaleY === 'linear' || client.settings.scaleY === 'log-dynamic')) { return dynamicDomain(); } else { return defaultDomain; } } - + // define the parts of the axis that aren't dependent on width or height - var xScale = chart.xScale = d3.time.scale().domain(extent); + var xScale = chart.xScale = d3.scaleTime().domain(extent); + focusYDomain = dynamicDomainOrElse(focusYDomain); var yScale = chart.yScale = yScaleType() - .domain(dynamicDomainOrElse(focusYDomain)); + .domain(focusYDomain); - var xScale2 = chart.xScale2 = d3.time.scale().domain(extent); + var xScale2 = chart.xScale2 = d3.scaleTime().domain(extent); + + contextYDomain = dynamicDomainOrElse(contextYDomain); var yScale2 = chart.yScale2 = yScaleType() - .domain(dynamicDomainOrElse(contextYDomain)); + .domain(contextYDomain); - chart.xScaleBasals = d3.time.scale().domain(extent); + chart.xScaleBasals = d3.scaleTime().domain(extent); - chart.yScaleBasals = d3.scale.linear() + chart.yScaleBasals = d3.scaleLinear() .domain([0, 5]); - var tickFormat = localeFormatter.timeFormat.multi( [ - ['.%L', function(d) { return d.getMilliseconds(); }], - [':%S', function(d) { return d.getSeconds(); }], - [client.settings.timeFormat === 24 ? '%H:%M' : '%I:%M', function(d) { return d.getMinutes(); }], - [client.settings.timeFormat === 24 ? '%H:%M' : '%-I %p', function(d) { return d.getHours(); }], - ['%a %d', function(d) { return d.getDay() && d.getDate() !== 1; }], - ['%b %d', function(d) { return d.getDate() !== 1; }], - ['%B', function(d) { return d.getMonth(); }], - ['%Y', function() { return true; }] - ]); + var formatMillisecond = localeFormatter.format('.%L') + , formatSecond = localeFormatter.format(':%S') + , formatMinute = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : + localeFormatter.format('%-I:%M') + , formatHour = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : + localeFormatter.format('%-I %p') + , formatDay = localeFormatter.format('%a %d') + , formatWeek = localeFormatter.format('%b %d') + , formatMonth = localeFormatter.format('%B') + , formatYear = localeFormatter.format('%Y'); + + var tickFormat = function(date) { + return (d3.timeSecond(date) < date ? formatMillisecond : + d3.timeMinute(date) < date ? formatSecond : + d3.timeHour(date) < date ? formatMinute : + d3.timeDay(date) < date ? formatHour : + d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek) : + d3.timeYear(date) < date ? formatMonth : + formatYear)(date); + }; var tickValues = client.ticks(client); - chart.xAxis = d3.svg.axis() - .scale(xScale) + chart.xAxis = d3.axisBottom(xScale) + + chart.xAxis = d3.axisBottom(xScale) .tickFormat(tickFormat) - .ticks(4) - .orient('bottom'); + .ticks(6); - chart.yAxis = d3.svg.axis() - .scale(yScale) + chart.yAxis = d3.axisLeft(yScale) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('left'); + .tickValues(tickValues); - chart.xAxis2 = d3.svg.axis() - .scale(xScale2) + chart.xAxis2 = d3.axisBottom(xScale2) .tickFormat(tickFormat) - .ticks(6) - .orient('bottom'); + .ticks(6); - chart.yAxis2 = d3.svg.axis() - .scale(yScale2) + chart.yAxis2 = d3.axisRight(yScale2) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('right'); + .tickValues(tickValues); + + d3.select('tick') + .style('z-index', '10000'); // setup a brush - chart.brush = d3.svg.brush() - .x(xScale2) - .on('brushstart', brushStarted) + chart.brush = d3.brushX() + .on('start', brushStarted) .on('brush', function brush (time) { - client.loadRetroIfNeeded(); + // layouting the graph causes a brushed event + // ignore retro data load the first two seconds + if (Date.now() - loadTime > 2000) client.loadRetroIfNeeded(); client.brushed(time); }) - .on('brushend', brushEnded); + .on('end', brushEnded); + + chart.theBrush = null; + + chart.futureOpacity = (function() { + var scale = d3.scaleLinear() + .domain([times.mins(25).msecs, times.mins(60).msecs]) + .range([0.8, 0.1]); - chart.futureOpacity = d3.scale.linear( ) - .domain([times.mins(25).msecs, times.mins(60).msecs]) - .range([0.8, 0.1]); + return function(delta) { + if (delta < 0) { + return null; + } else { + return scale(delta); + } + }; + })(); // create svg and g to contain the chart contents chart.charts = d3.select('#chartContainer').append('svg') @@ -176,54 +232,82 @@ function init (client, d3, $) { // create the x axis container chart.focus.append('g') - .attr('class', 'x axis'); + .attr('class', 'x axis') + .style("font-size", "16px"); // create the y axis container chart.focus.append('g') - .attr('class', 'y axis'); + .attr('class', 'y axis') + .style("font-size", "16px"); - chart.context = chart.charts.append('g').attr('class', 'chart-context'); + chart.context = chart.charts.append('g') + .attr('class', 'chart-context'); // create the x axis container chart.context.append('g') - .attr('class', 'x axis'); + .attr('class', 'x axis') + .style("font-size", "16px"); // create the y axis container chart.context.append('g') - .attr('class', 'y axis'); + .attr('class', 'y axis') + .style("font-size", "16px"); + + chart.createBrushedRange = function() { + var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null; - function createAdjustedRange() { - var range = chart.brush.extent().slice(); + var range = brushedRange && brushedRange.map(chart.xScale2.invert); + var dataExtent = client.dataExtent(); - var end = range[1].getTime() + client.forecastTime; + if (!brushedRange) { + // console.log('No current brushed range. Setting range to last focusRangeMS amount of available data'); + range = dataExtent; + range[0] = new Date(range[1].getTime() - client.focusRangeMS); + } + + var end = range[1].getTime(); if (!chart.inRetroMode()) { - var lastSGVMills = client.latestSGV ? client.latestSGV.mills : client.now; - end += (client.now - lastSGVMills); + end = client.now > dataExtent[1].getTime() ? client.now : dataExtent[1].getTime(); } range[1] = new Date(end); + range[0] = new Date(end - client.focusRangeMS); + + // console.log('createBrushedRange: ', brushedRange, range); return range; } - chart.inRetroMode = function inRetroMode() { - if (!chart.brush || !chart.xScale2) { + chart.createAdjustedRange = function() { + var adjustedRange = chart.createBrushedRange(); + + adjustedRange[1] = new Date(Math.max(adjustedRange[1].getTime(), client.forecastTime)); + + return adjustedRange; + } + + chart.inRetroMode = function inRetroMode () { + var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null; + + if (!brushedRange || !chart.xScale2) { return false; } - var brushTime = chart.brush.extent()[1].getTime(); var maxTime = chart.xScale2.domain()[1].getTime(); + var brushTime = chart.xScale2.invert(brushedRange[1]).getTime(); return brushTime < maxTime; }; // called for initial update and updates for resize - chart.update = function update(init) { + chart.update = function update (init) { if (client.documentHidden && !init) { console.info('Document Hidden, not updating - ' + (new Date())); return; } + chart.setForecastTime(); + var chartContainer = $('#chartContainer'); if (chartContainer.length < 1) { @@ -235,15 +319,16 @@ function init (client, d3, $) { var dataRange = client.dataExtent(); var chartContainerRect = chartContainer[0].getBoundingClientRect(); var chartWidth = chartContainerRect.width; - var chartHeight = chartContainerRect.height - padding.bottom; + var chartHeight = chartContainerRect.height - PADDING_BOTTOM; // get the height of each chart based on its container size ratio var focusHeight = chart.focusHeight = chartHeight * .7; - var contextHeight = chart.contextHeight = chartHeight * .2; + var contextHeight = chart.contextHeight = chartHeight * .3; chart.basalsHeight = focusHeight / 4; // get current brush extent - var currentBrushExtent = createAdjustedRange(); + var currentRange = chart.createAdjustedRange(); + var currentBrushExtent = chart.createBrushedRange(); // only redraw chart if chart size has changed var widthChanged = (chart.prevChartWidth !== chartWidth); @@ -259,14 +344,14 @@ function init (client, d3, $) { //set the width and height of the SVG element chart.charts.attr('width', chartWidth) - .attr('height', chartHeight + padding.bottom); + .attr('height', chartHeight + PADDING_BOTTOM); // ranges are based on the width and height available so reset chart.xScale.range([0, chartWidth]); chart.xScale2.range([0, chartWidth]); chart.xScaleBasals.range([0, chartWidth]); chart.yScale.range([focusHeight, 0]); - chart.yScale2.range([chartHeight, chartHeight - contextHeight]); + chart.yScale2.range([contextHeight, 0]); chart.yScaleBasals.range([0, focusHeight / 4]); if (init) { @@ -281,42 +366,49 @@ function init (client, d3, $) { .call(chart.yAxis); // if first run then just display axis with no transition + chart.context + .attr('transform', 'translate(0,' + focusHeight + ')') + chart.context.select('.x') - .attr('transform', 'translate(0,' + chartHeight + ')') + .attr('transform', 'translate(0,' + contextHeight + ')') .call(chart.xAxis2); -// chart.basals.select('.y') -// .attr('transform', 'translate(0,' + 0 + ')') -// .call(chart.yAxisBasals); - - chart.context.append('g') + chart.theBrush = chart.context.append('g') .attr('class', 'x brush') - .call(d3.svg.brush().x(chart.xScale2).on('brush', client.brushed)) - .selectAll('rect') - .attr('y', focusHeight) - .attr('height', chartHeight - focusHeight); + .call(chart.brush) + .call(g => g.select(".overlay") + .datum({ type: 'selection' }) + .on('mousedown touchstart', beforeBrushStarted)); + + chart.theBrush.selectAll('rect') + .attr('y', 0) + .attr('height', contextHeight) + .attr('width', '100%'); // disable resizing of brush - d3.select('.x.brush').select('.background').style('cursor', 'move'); - d3.select('.x.brush').select('.resize.e').style('cursor', 'move'); - d3.select('.x.brush').select('.resize.w').style('cursor', 'move'); + chart.context.select('.x.brush').select('.overlay').style('cursor', 'move'); + chart.context.select('.x.brush').selectAll('.handle') + .style('cursor', 'move'); + + chart.context.select('.x.brush').select('.selection') + .style('visibility', 'hidden'); // add a line that marks the current time chart.focus.append('line') .attr('class', 'now-line') .attr('x1', chart.xScale(new Date(client.now))) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) + .attr('y1', chart.yScale(focusYDomain[0])) .attr('x2', chart.xScale(new Date(client.now))) - .attr('y2', chart.yScale(utils.scaleMgdl(420))) + .attr('y2', chart.yScale(focusYDomain[1])) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); // add a y-axis line that shows the high bg threshold chart.focus.append('line') .attr('class', 'high-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) .style('stroke-dasharray', ('1, 6')) .attr('stroke', '#777'); @@ -324,9 +416,9 @@ function init (client, d3, $) { // add a y-axis line that shows the high bg threshold chart.focus.append('line') .attr('class', 'target-top-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -334,9 +426,9 @@ function init (client, d3, $) { // add a y-axis line that shows the low bg threshold chart.focus.append('line') .attr('class', 'target-bottom-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -344,9 +436,9 @@ function init (client, d3, $) { // add a y-axis line that shows the low bg threshold chart.focus.append('line') .attr('class', 'low-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) .style('stroke-dasharray', ('1, 6')) .attr('stroke', '#777'); @@ -355,7 +447,7 @@ function init (client, d3, $) { chart.context.append('line') .attr('class', 'open-top') .attr('stroke', '#111') - .attr('stroke-width', 12); + .attr('stroke-width', OPEN_TOP_HEIGHT); // add a x-axis line that closes the the brush container on left side chart.context.append('line') @@ -371,9 +463,9 @@ function init (client, d3, $) { chart.context.append('line') .attr('class', 'now-line') .attr('x1', chart.xScale(new Date(client.now))) - .attr('y1', chart.yScale2(utils.scaleMgdl(36))) + .attr('y1', chart.yScale2(contextYDomain[0])) .attr('x2', chart.xScale(new Date(client.now))) - .attr('y2', chart.yScale2(utils.scaleMgdl(420))) + .attr('y2', chart.yScale2(contextYDomain[1])) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -400,96 +492,82 @@ function init (client, d3, $) { } else { // for subsequent updates use a transition to animate the axis to the new position - var focusTransition = chart.focus.transition(); - focusTransition.select('.x') + chart.focus.select('.x') .attr('transform', 'translate(0,' + focusHeight + ')') .call(chart.xAxis); - focusTransition.select('.y') + chart.focus.select('.y') .attr('transform', 'translate(' + chartWidth + ', 0)') .call(chart.yAxis); - var contextTransition = chart.context.transition(); + chart.context + .attr('transform', 'translate(0,' + focusHeight + ')') - contextTransition.select('.x') - .attr('transform', 'translate(0,' + chartHeight + ')') + chart.context.select('.x') + .attr('transform', 'translate(0,' + contextHeight + ')') .call(chart.xAxis2); - chart.basals.transition(); - -// basalsTransition.select('.y') -// .attr('transform', 'translate(0,' + 0 + ')') -// .call(chart.yAxisBasals); + chart.basals; // reset brush location - chart.context.select('.x.brush') - .selectAll('rect') - .attr('y', focusHeight) - .attr('height', chartHeight - focusHeight); + chart.theBrush.selectAll('rect') + .attr('y', 0) + .attr('height', contextHeight); - // clear current brushs - d3.select('.brush').call(chart.brush.clear()); + // console.log('chart.update(): Redrawing old brush with new dimensions: ', currentBrushExtent); // redraw old brush with new dimensions - d3.select('.brush').transition().call(chart.brush.extent(currentBrushExtent)); + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); // transition lines to correct location chart.focus.select('.high-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))); chart.focus.select('.target-top-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))); chart.focus.select('.target-bottom-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))); chart.focus.select('.low-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))); // transition open-top line to correct location chart.context.select('.open-top') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[0])) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) - .attr('x2', chart.xScale2(currentBrushExtent[1])) - .attr('y2', chart.yScale(utils.scaleMgdl(30))); + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX)) + Math.floor(OPEN_TOP_HEIGHT/2.0)-1) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX)) + Math.floor(OPEN_TOP_HEIGHT/2.0)-1); // transition open-left line to correct location chart.context.select('.open-left') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[0])) - .attr('y1', focusHeight) - .attr('x2', chart.xScale2(currentBrushExtent[0])) - .attr('y2', chartHeight); + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[0])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition open-right line to correct location chart.context.select('.open-right') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[1])) - .attr('y1', focusHeight) - .attr('x2', chart.xScale2(currentBrushExtent[1])) - .attr('y2', chartHeight); + .attr('x1', chart.xScale2(currentRange[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition high line to correct location chart.context.select('.high-line') - .transition() .attr('x1', chart.xScale2(dataRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) .attr('x2', chart.xScale2(dataRange[1])) @@ -497,7 +575,6 @@ function init (client, d3, $) { // transition low line to correct location chart.context.select('.low-line') - .transition() .attr('x1', chart.xScale2(dataRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) .attr('x2', chart.xScale2(dataRange[1])) @@ -505,64 +582,83 @@ function init (client, d3, $) { } } - // update domain - chart.xScale2.domain(dataRange); + chart.updateContext(dataRange); + chart.xScaleBasals.domain(dataRange); - var updateBrush = d3.select('.brush').transition(); - updateBrush - .call(chart.brush.extent([new Date(dataRange[1].getTime() - client.focusRangeMS), dataRange[1]])); - client.brushed(true); + // console.log('chart.update(): Redrawing brush due to update: ', currentBrushExtent); + + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); + }; + + chart.updateContext = function(dataRange_) { + if (client.documentHidden) { + console.info('Document Hidden, not updating - ' + (new Date())); + return; + } + + // get current data range + var dataRange = dataRange_ || client.dataExtent(); + + // update domain + chart.xScale2.domain(dataRange); renderer.addContextCircles(); // update x axis domain chart.context.select('.x').call(chart.xAxis2); - }; - chart.scroll = function scroll (nowDate) { - chart.xScale.domain(createAdjustedRange()); - chart.yScale.domain(dynamicDomainOrElse(focusYDomain)); - chart.xScaleBasals.domain(createAdjustedRange()); + function scrollUpdate () { + var nowDate = scrollNow; + + var currentBrushExtent = scrollBrushExtent; + var currentRange = scrollRange; + + chart.setForecastTime(); + + chart.xScale.domain(currentRange); + + focusYDomain = dynamicDomainOrElse(focusYDomain); + + chart.yScale.domain(focusYDomain); + chart.xScaleBasals.domain(currentRange); // remove all insulin/carb treatment bubbles so that they can be redrawn to correct location d3.selectAll('.path').remove(); // transition open-top line to correct location chart.context.select('.open-top') - .attr('x1', chart.xScale2(chart.brush.extent()[0])) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) - .attr('x2', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y2', chart.yScale(utils.scaleMgdl(30))); + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[1]) + Math.floor(OPEN_TOP_HEIGHT / 2.0)-1) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1]) + Math.floor(OPEN_TOP_HEIGHT / 2.0)-1); // transition open-left line to correct location chart.context.select('.open-left') - .attr('x1', chart.xScale2(chart.brush.extent()[0])) - .attr('y1', chart.focusHeight) - .attr('x2', chart.xScale2(chart.brush.extent()[0])) - .attr('y2', chart.prevChartHeight); + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[0])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition open-right line to correct location chart.context.select('.open-right') - .attr('x1', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y1', chart.focusHeight) - .attr('x2', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y2', chart.prevChartHeight); + .attr('x1', chart.xScale2(currentRange[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); chart.focus.select('.now-line') - .transition() .attr('x1', chart.xScale(nowDate)) - .attr('y1', chart.yScale(utils.scaleMgdl(36))) + .attr('y1', chart.yScale(focusYDomain[0])) .attr('x2', chart.xScale(nowDate)) - .attr('y2', chart.yScale(utils.scaleMgdl(420))); + .attr('y2', chart.yScale(focusYDomain[1])); chart.context.select('.now-line') - .transition() - .attr('x1', chart.xScale2(chart.brush.extent()[1])) - .attr('y1', chart.yScale2(utils.scaleMgdl(36))) - .attr('x2', chart.xScale2(chart.brush.extent()[1])) - .attr('y2', chart.yScale2(utils.scaleMgdl(420))); + .attr('x1', chart.xScale2(currentBrushExtent[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentBrushExtent[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); // update x,y axis chart.focus.select('.x.axis').call(chart.xAxis); @@ -571,10 +667,79 @@ function init (client, d3, $) { renderer.addBasals(client); renderer.addFocusCircles(); - renderer.addTreatmentCircles(); + renderer.addTreatmentCircles(nowDate); renderer.addTreatmentProfiles(client); renderer.drawTreatments(client); + // console.log('scrollUpdate(): Redrawing brush due to update: ', currentBrushExtent); + + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); + + scrolling = false; + } + + chart.scroll = function scroll (nowDate) { + scrollNow = nowDate; + scrollBrushExtent = chart.createBrushedRange(); + scrollRange = chart.createAdjustedRange(); + + if (!scrolling) { + requestAnimationFrame(scrollUpdate); + } + + scrolling = true; + }; + + chart.getMaxForecastMills = function getMaxForecastMills () { + // limit lookahead to the same as lookback + var selectedRange = chart.createBrushedRange(); + var to = selectedRange[1].getTime(); + return to + client.focusRangeMS; + }; + + chart.getForecastData = function getForecastData () { + + var maxForecastAge = chart.getMaxForecastMills(); + var pointTypes = client.settings.showForecast.split(' '); + + var points = pointTypes.reduce( function (points, type) { + return points.concat(client.sbx.pluginBase.forecastPoints[type] || []); + }, [] ); + + return _.filter(points, function isShown (point) { + return point.mills < maxForecastAge; + }); + + }; + + chart.setForecastTime = function setForecastTime () { + + if (client.sbx.pluginBase.forecastPoints) { + var shownForecastPoints = chart.getForecastData(); + + // Get maximum time we will allow projected forward in time + // based on the number of hours the user has selected to show. + var maxForecastMills = chart.getMaxForecastMills(); + + var selectedRange = chart.createBrushedRange(); + var to = selectedRange[1].getTime(); + + // Default min forecast projection times to the default amount of time to forecast + var minForecastMills = to + client.defaultForecastTime; + var availForecastMills = 0; + + // Determine what the maximum forecast time is that is available in the forecast data + if (shownForecastPoints.length > 0) { + availForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); + } + + // Limit the amount shown to the maximum time allowed to be projected forward based + // on the number of hours the user has selected to show + var forecastMills = Math.min(availForecastMills, maxForecastMills); + + // Don't allow the forecast time to go below the minimum forecast time + client.forecastTime = Math.max(forecastMills, minForecastMills); + } }; return chart; diff --git a/lib/client/clock-client.js b/lib/client/clock-client.js index 18b5116f94d..891f10d57d5 100644 --- a/lib/client/clock-client.js +++ b/lib/client/clock-client.js @@ -6,7 +6,7 @@ var client = {}; client.settings = browserSettings(client, window.serverSettings, $); -// console.log('settings', client.settings); +//console.log('settings', client.settings); // client.settings now contains all settings client.query = function query () { @@ -20,7 +20,7 @@ client.query = function query () { }); var secret = localStorage.getItem('apisecrethash'); - var src = '/api/v1/entries.json?count=3&t=' + new Date().getTime(); + var src = '/api/v1/entries.json?find[type]=sgv&count=3&t=' + new Date().getTime(); if (secret) { src += '&secret=' + secret; @@ -37,52 +37,89 @@ client.render = function render (xhr) { console.log('got data', xhr); let rec; + let delta; - xhr.some(element => { - if (element.sgv) { + // Get SGV, calculate DELTA + xhr.forEach(element => { + if (element.sgv && !rec) { rec = element; - return true; + } + else if (element.sgv && rec && delta==null) { + delta = (rec.sgv - element.sgv)/((rec.date - element.date)/(5*60*1000)); } }); let $errorMessage = $('#errorMessage'); + let $inner = $('#inner'); // If no one measured value found => show "-?-" if (!rec) { if (!$errorMessage.length) { - $('#arrowDiv').append('
-?-
'); - $('#arrow').hide(); + $inner.after('
-?-
') } else { $errorMessage.show(); } + $inner.hide(); return; } else { $errorMessage.length && $errorMessage.hide(); - $('#arrow').show(); + $inner.show(); + } + + //Parse face parameters + let face = $inner.data('face').toLowerCase(); + + // Backward compatible + if (face === 'clock-color') { + face = 'c' + (window.serverSettings.settings.showClockLastTime ? 'y' : 'n') + '13-sg40-' + (window.serverSettings.settings.showClockDelta ? 'dt14-' : '') + 'nl-ar30-nl-ag6'; + } + else if (face === 'clock') { + face = 'bn0-sg40'; + } + else if (face === 'bgclock') { + face = 'bn0-sg30-ar18-nl-nl-tm26'; + } + else if (face === 'config') { + face = $inner.attr('data-face-config'); + $inner.empty(); + } + + let faceParams = face.split('-'); + let bgColor = false; + let staleMinutes = 13; + let alwaysShowTime = false; + + let clockCreated = ($inner.children().length > 0); + + for (let param in faceParams) { + if (param === '0') { + bgColor = (faceParams[param].substr(0, 1) === 'c'); // do we want colorful background? + alwaysShowTime = (faceParams[param].substr(1, 1) === 'y'); // always show "stale time" text? + staleMinutes = (faceParams[param].substr(2,2) - 0 >= 0) ? faceParams[param].substr(2,2) : 13; // threshold value (0=never) + } else if (!clockCreated){ + let div = '
0) ? ' style="' + ((faceParams[param].substr(0,2) === 'ar') ? 'height' : 'font-size') + ':' + faceParams[param].substr(2,2) + 'vmin"' : '') + '>
'; + $inner.append(div); + } } - - let last = new Date(rec.date); - let now = new Date(); // Convert BG to mmol/L if necessary. + let displayValue; + let deltaDisplayValue; + if (window.serverSettings.settings.units === 'mmol') { - var displayValue = window.Nightscout.units.mgdlToMMOL(rec.sgv); + displayValue = window.Nightscout.units.mgdlToMMOL(rec.sgv); + deltaDisplayValue = window.Nightscout.units.mgdlToMMOL(delta); } else { displayValue = rec.sgv; + deltaDisplayValue = Math.round(delta); } - // Insert the BG value text. - $('#bgnow').html(displayValue); - - // Insert the trend arrow. - $('#arrow').attr('src', '/images/' + (!rec.direction || rec.direction === 'NOT COMPUTABLE' ? 'NONE' : rec.direction) + '.svg'); - - // Time before data considered stale. - let staleMinutes = 13; - let threshold = 1000 * 60 * staleMinutes; + if (deltaDisplayValue > 0) { + deltaDisplayValue = '+' + deltaDisplayValue; + } - // Toggle stale if necessary. - $('#bgnow').toggleClass('stale', (now - last > threshold)); + // Insert the delta value text. + $('.dt').html(deltaDisplayValue); // Generate and insert the clock. let timeDivisor = parseInt(client.settings.timeFormat ? client.settings.timeFormat : 12, 10); @@ -96,80 +133,88 @@ client.render = function render (xhr) { } let m = today.getMinutes(); if (m < 10) m = "0" + m; - $('#clock').text(h + ":" + m); - - var queryDict = {}; - location.search.substr(1).split("&").forEach(function(item) { queryDict[item.split("=")[0]] = item.split("=")[1] }); - - if (!window.serverSettings.settings.showClockClosebutton || !queryDict['showClockClosebutton']) { - $('#close').css('display', 'none'); - } - - // defined in the template this is loaded into - // eslint-disable-next-line no-undef - if (clockFace === 'clock-color') { + $('.tm').html(h + ":" + m); - var bgHigh = window.serverSettings.settings.thresholds.bgHigh; - var bgLow = window.serverSettings.settings.thresholds.bgLow; - var bgTargetBottom = window.serverSettings.settings.thresholds.bgTargetBottom; - var bgTargetTop = window.serverSettings.settings.thresholds.bgTargetTop; - - var bgNum = parseFloat(rec.sgv); + // Color background + if (bgColor) { // These are the particular shades of red, yellow, green, and blue. - var red = 'rgba(213,9,21,1)'; - var yellow = 'rgba(234,168,0,1)'; - var green = 'rgba(134,207,70,1)'; - var blue = 'rgba(78,143,207,1)'; - - var darkRed = 'rgba(183,9,21,1)'; - var darkYellow = 'rgba(214,168,0,1)'; - var darkGreen = 'rgba(110,192,70,1)'; - var darkBlue = 'rgba(78,143,187,1)'; + let red = 'rgba(213,9,21,1)'; + let yellow = 'rgba(234,168,0,1)'; + let green = 'rgba(134,207,70,1)'; + let blue = 'rgba(78,143,207,1)'; - var elapsedMins = Math.round(((now - last) / 1000) / 60); + // Threshold values + let bgHigh = client.settings.thresholds.bgHigh; + let bgLow = client.settings.thresholds.bgLow; + let bgTargetBottom = client.settings.thresholds.bgTargetBottom; + let bgTargetTop = client.settings.thresholds.bgTargetTop; - // Insert the BG stale time text. - $('#staleTime').text(elapsedMins + ' minutes ago'); + let bgNum = parseFloat(rec.sgv); // Threshold background coloring. if (bgNum < bgLow) { $('body').css('background-color', red); - $('#close').css('border-color', darkRed); - $('#close').css('color', darkRed); } if ((bgLow <= bgNum) && (bgNum < bgTargetBottom)) { $('body').css('background-color', blue); - $('#close').css('border-color', darkBlue); - $('#close').css('color', darkBlue); } if ((bgTargetBottom <= bgNum) && (bgNum < bgTargetTop)) { $('body').css('background-color', green); - $('#close').css('border-color', darkGreen); - $('#close').css('color', darkGreen); } if ((bgTargetTop <= bgNum) && (bgNum < bgHigh)) { $('body').css('background-color', yellow); - $('#close').css('border-color', darkYellow); - $('#close').css('color', darkYellow); } if (bgNum >= bgHigh) { $('body').css('background-color', red); - $('#close').css('border-color', darkRed); - $('#close').css('color', darkRed); } - // Restyle body bg, and make the "x minutes ago" visible too. - if (now - last > threshold) { - $('body').css('background-color', 'grey'); - $('body').css('color', 'black'); - $('#staleTime').css('display', 'block'); - $('#arrow').css('filter', 'brightness(0%)'); - } else { - $('#staleTime').css('display', 'none'); - $('body').css('color', 'white'); - $('#arrow').css('filter', 'brightness(100%)'); + } + else { + $('body').css('background-color', 'black'); + } + + // Time before data considered stale. + let threshold = 1000 * 60 * staleMinutes; + + let last = new Date(rec.date); + let now = new Date(); + + let elapsedMins = Math.round(((now - last) / 1000) / 60); + + let thresholdReached = (now - last > threshold) && threshold > 0; + + // Insert the BG value text, toggle stale if necessary. + $('.sg').toggleClass('stale', thresholdReached).html(displayValue); + + if (thresholdReached || alwaysShowTime) { + let staleTimeText; + if (elapsedMins === 0) { + staleTimeText = 'Just now'; } + else if (elapsedMins === 1) { + staleTimeText = '1 minute ago'; + } + else { + staleTimeText = elapsedMins + ' minutes ago'; + } + + $('.ag').html(staleTimeText); + } + else { + $('.ag').html(''); + } + + // Insert the trend arrow. + let arrow = $('arrow').attr('src', '/images/' + (!rec.direction || rec.direction === 'NOT COMPUTABLE' ? 'NONE' : rec.direction) + '.svg'); + + // Restyle body bg + if (thresholdReached) { + $('body').css('background-color', 'grey').css('color', 'black'); + $('.ar').css('filter', 'brightness(0%)').html(arrow); + } else { + $('body').css('color', bgColor ? 'white' : 'grey'); + $('.ar').css('filter', bgColor ? 'brightness(100%)' : 'brightness(50%)').html(arrow); } }; diff --git a/lib/client/d3locales.js b/lib/client/d3locales.js index afb04cbad5c..1af15ba3fb6 100644 --- a/lib/client/d3locales.js +++ b/lib/client/d3locales.js @@ -142,6 +142,21 @@ d3locales.it_IT = { shortMonths: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'] }; +d3locales.pl_PL = { + decimal: '.', + thousands: ',', + grouping: [3], + currency: ['', 'zł'], + dateTime: '%a %b %e %X %Y', + date: '%d.%m.%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], // unused + days: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'], + shortDays: ['Nie', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'So'], + months: ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'], + shortMonths: ['Sty', 'Lu', 'Mar', 'Kw', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Pa', 'Lis', 'Gru'] +}; + d3locales.pt_BR = { decimal: ',', thousands: '.', @@ -212,6 +227,7 @@ d3locales.locale = function locale (language) { , fr: 'fr_FR' , he: 'he_IL' , it: 'it_IT' + , pl: 'pl_PL' , pt: 'pt_BR' , ro: 'ro_RO' , ru: 'ru_RU' diff --git a/lib/client/index.js b/lib/client/index.js index 979348c0cb5..c329c5cec0b 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -15,6 +15,8 @@ var levels = require('../levels'); var times = require('../times'); var receiveDData = require('./receiveddata'); +var brushing = false; + var client = {}; $('#loadingMessageText').html('Connecting to server'); @@ -35,6 +37,11 @@ client.headers = function headers () { } }; +client.crashed = function crashed () { + $('#centerMessagePanel').show(); + $('#loadingMessageText').html('It appears the server has crashed. Please go to Heroku or Azure and reboot the server.'); +} + client.init = function init (callback) { client.browserUtils = require('./browser-utils')($); @@ -59,8 +66,7 @@ client.init = function init (callback) { console.log('Application appears to be online'); $('#centerMessagePanel').hide(); client.load(serverSettings, callback); - // eslint-disable-next-line no-unused-vars - }).fail(function fail (jqXHR, textStatus, errorThrown) { + }).fail(function fail (jqXHR) { // check if we couldn't reach the server at all, show offline message if (!jqXHR.readyState) { @@ -93,7 +99,7 @@ client.init = function init (callback) { // auth failed, hide loader and request for key $('#centerMessagePanel').hide(); client.hashauth.requestAuthentication(function afterRequest () { - client.init(null, callback); + client.init(callback); }); } }); @@ -102,8 +108,7 @@ client.init = function init (callback) { client.load = function load (serverSettings, callback) { - var UPDATE_TRANS_MS = 750 // milliseconds - , FORMAT_TIME_12 = '%-I:%M %p' + var FORMAT_TIME_12 = '%-I:%M %p' , FORMAT_TIME_12_COMPACT = '%-I:%M' , FORMAT_TIME_24 = '%H:%M%' , FORMAT_TIME_12_SCALE = '%-I %p' @@ -124,11 +129,16 @@ client.load = function load (serverSettings, callback) { , urgentAlarmSound = 'alarm2.mp3' , previousNotifyTimestamp; - client.entryToDate = function entryToDate (entry) { return new Date(entry.mills); }; + client.entryToDate = function entryToDate (entry) { + if (entry.date) return entry.date; + entry.date = new Date(entry.mills); + return entry.date; + }; client.now = Date.now(); client.ddata = require('../data/ddata')(); - client.forecastTime = times.mins(30).msecs; + client.defaultForecastTime = times.mins(30).msecs; + client.forecastTime = client.now + client.defaultForecastTime; client.entries = []; client.ticks = require('./ticks'); @@ -261,9 +271,11 @@ client.load = function load (serverSettings, callback) { //client.ctx.bus.uptime( ); client.dataExtent = function dataExtent () { - return client.entries.length > 0 ? - d3.extent(client.entries, client.entryToDate) : - d3.extent([new Date(client.now - times.hours(history).msecs), new Date(client.now)]); + if (client.entries.length > 0) { + return [client.entryToDate(client.entries[0]), client.entryToDate(client.entries[client.entries.length - 1])]; + } else { + return [new Date(client.now - times.hours(history).msecs), new Date(client.now)]; + } }; client.bottomOfPills = function bottomOfPills () { @@ -276,7 +288,7 @@ client.load = function load (serverSettings, callback) { function formatTime (time, compact) { var timeFormat = getTimeFormat(false, compact); - time = d3.time.format(timeFormat)(time); + time = d3.timeFormat(timeFormat)(time); if (client.settings.timeFormat !== 24) { time = time.toLowerCase(); } @@ -375,20 +387,28 @@ client.load = function load (serverSettings, callback) { // clears the current user brush and resets to the current real time data function updateBrushToNow (skipBrushing) { - // get current time range - var dataRange = client.dataExtent(); - // update brush and focus chart with recent data - d3.select('.brush') - .transition() - .duration(UPDATE_TRANS_MS) - .call(chart.brush.extent([new Date(dataRange[1].getTime() - client.focusRangeMS), dataRange[1]])); + var brushExtent = client.dataExtent(); + + brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); + + // console.log('updateBrushToNow(): Resetting brush: ', brushExtent); + + if (chart.theBrush) { + chart.theBrush.call(chart.brush) + chart.theBrush.call(chart.brush.move, brushExtent.map(chart.xScale2)); + } if (!skipBrushing) { brushed(); } } + function updateBolusRenderOver () { + var bolusRenderOver = (client.settings.bolusRenderOver || 1) + ' U and Over'; + $('#bolusRenderOver').text(bolusRenderOver); + } + function alarmingNow () { return container.hasClass('alarming'); } @@ -398,21 +418,42 @@ client.load = function load (serverSettings, callback) { } function brushed () { + // Brush not initialized + if (!chart.theBrush) { + return; + } + + if (brushing) { + return; + } - var brushExtent = chart.brush.extent(); + brushing = true; - // ensure that brush extent is fixed at 3.5 hours - if (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.focusRangeMS) { + // default to most recent focus period + var brushExtent = client.dataExtent(); + brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); + + var brushedRange = d3.brushSelection(chart.theBrush.node()); + + // console.log("brushed(): coordinates: ", brushedRange); + + if (brushedRange) { + brushExtent = brushedRange.map(chart.xScale2.invert); + } + + // console.log('brushed(): Brushed to: ', brushExtent); + + if (!brushedRange || (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.focusRangeMS)) { // ensure that brush updating is with the time range if (brushExtent[0].getTime() + client.focusRangeMS > client.dataExtent()[1].getTime()) { brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); - d3.select('.brush') - .call(chart.brush.extent([brushExtent[0], brushExtent[1]])); } else { brushExtent[1] = new Date(brushExtent[0].getTime() + client.focusRangeMS); - d3.select('.brush') - .call(chart.brush.extent([brushExtent[0], brushExtent[1]])); } + + // console.log('brushed(): updating to: ', brushExtent); + + chart.theBrush.call(chart.brush.move, brushExtent.map(chart.xScale2)); } function adjustCurrentSGVClasses (value, isCurrent) { @@ -428,7 +469,6 @@ client.load = function load (serverSettings, callback) { currentBG.toggleClass('icon-hourglass', value === 9); currentBG.toggleClass('error-code', value < 39); currentBG.toggleClass('bg-limit', value === 39 || value > 400); - container.removeClass('loading'); } function updateCurrentSGV (entry) { @@ -450,6 +490,20 @@ client.load = function load (serverSettings, callback) { adjustCurrentSGVClasses(value, isCurrent); } + function mergeDeviceStatus (retro, ddata) { + if (!retro) { + return ddata; + } + + var result = retro.map(x => Object.assign(x, ddata.find(y => y._id == x._id))); + + var missingInRetro = ddata.filter(y => !retro.find(x => x._id == y._id)); + + result.push(...missingInRetro); + + return result; + } + function updatePlugins (time) { //TODO: doing a clone was slow, but ok to let plugins muck with data? @@ -458,10 +512,22 @@ client.load = function load (serverSettings, callback) { client.ddata.inRetroMode = inRetroMode(); client.ddata.profile = profile; + // retro data only ever contains device statuses + // Cleate a clone of the data for the sandbox given to plugins + + var mergedStatuses = client.ddata.devicestatus; + + if (client.retro.data) { + mergedStatuses = mergeDeviceStatus(client.retro.data.devicestatus, client.ddata.devicestatus); + } + + var clonedData = _.clone(client.ddata); + clonedData.devicestatus = mergedStatuses; + client.sbx = sandbox.clientInit( client.ctx , new Date(time).getTime() //make sure we send a timestamp - , _.merge({}, client.retro.data || {}, client.ddata) + , clonedData ); //all enabled plugins get a chance to set properties, even if they aren't shown @@ -505,7 +571,7 @@ client.load = function load (serverSettings, callback) { function clearCurrentSGV () { currentBG.text('---'); - container.removeClass('urgent warning inrange'); + container.removeClass('alarming urgent warning inrange'); } var nowDate = null; @@ -546,6 +612,9 @@ client.load = function load (serverSettings, callback) { var top = (client.bottomOfPills() + 5); $('#chartContainer').css({ top: top + 'px', height: $(window).height() - top - 10 }); + container.removeClass('loading'); + + brushing = false; } function sgvToColor (sgv) { @@ -724,7 +793,8 @@ client.load = function load (serverSettings, callback) { function updateClock () { updateClockDisplay(); - var interval = (60 - (new Date()).getSeconds()) * 1000 + 5; + // Update at least every 15 seconds + var interval = Math.min(15 * 1000, (60 - (new Date()).getSeconds()) * 1000 + 5); setTimeout(updateClock, interval); updateTimeAgo(); @@ -798,6 +868,12 @@ client.load = function load (serverSettings, callback) { container.toggleClass('alarming-timeago', status !== 'current'); + if (status === 'warn') { + container.addClass('warn'); + } else if (status === 'urgent') { + container.addClass('urgent'); + } + if (alarmingNow() && status === 'current' && isTimeAgoAlarmType()) { stopAlarm(true, times.min().msecs); } @@ -903,7 +979,7 @@ client.load = function load (serverSettings, callback) { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Client-side code to connect to server and handle incoming data //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // eslint-disable-next-line no-undef + /* global io */ client.socket = socket = io.connect(); socket.on('dataUpdate', dataUpdate); @@ -952,18 +1028,30 @@ client.load = function load (serverSettings, callback) { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Alarms and Text handling //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - socket.on('connect', function() { - console.log('Client connected to server.'); + + + client.authorizeSocket = function authorizeSocket() { + + console.log('Authorizing socket'); + var auth_data = { + client: 'web' + , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() + , token: client.authorized && client.authorized.token + , history: history + }; + socket.emit( 'authorize' - , { - client: 'web' - , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() - , token: client.authorized && client.authorized.token - , history: history - } + , auth_data , function authCallback (data) { - console.log('Client rights: ', data); + + console.log('Socket auth response', data); + + if (!data) { + console.log('Crashed!'); + client.crashed(); + } + if (!data.read || !hasRequiredPermission()) { client.hashauth.requestAuthentication(function afterRequest () { client.hashauth.updateSocketAuth(); @@ -976,6 +1064,11 @@ client.load = function load (serverSettings, callback) { } } ); + } + + socket.on('connect', function() { + console.log('Client connected to server.'); + client.authorizeSocket(); }); function hasRequiredPermission () { @@ -1127,9 +1220,14 @@ client.load = function load (serverSettings, callback) { point.color = 'transparent'; } }); + + client.entries.sort(function sorter (a, b) { + return a.mills - b.mills; + }); } - function dataUpdate (received) { + function dataUpdate (received, headless) { + console.info('got dataUpdate', new Date(client.now)); var lastUpdated = Date.now(); receiveDData(received, client.ddata, client.settings); @@ -1161,16 +1259,23 @@ client.load = function load (serverSettings, callback) { prepareEntries(); updateTitle(); + updateBolusRenderOver(); + + // Don't invoke D3 in headless mode + + if (headless) return; if (!isInitialData) { isInitialData = true; chart = client.chart = require('./chart')(client, d3, $); - brushed(); chart.update(true); - } else if (!inRetroMode()) { + brushed(); chart.update(false); - client.plugins.updateVisualisations(client.nowSBX); + } else if (!inRetroMode()) { brushed(); + chart.update(false); + } else { + chart.updateContext(); } } }; diff --git a/lib/client/receiveddata.js b/lib/client/receiveddata.js index e0adf13fb3b..6f542b89b03 100644 --- a/lib/client/receiveddata.js +++ b/lib/client/receiveddata.js @@ -128,6 +128,9 @@ function receiveDData (received, ddata, settings) { } } + if (received.dbstats && received.dbstats.dataSize) { + ddata.dbstats = received.dbstats; + } } //expose for tests diff --git a/lib/client/renderer.js b/lib/client/renderer.js index ccbac62d72b..a3841695060 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -2,14 +2,16 @@ var _ = require('lodash'); var times = require('../times'); +var consts = require('../constants'); var DEFAULT_FOCUS = times.hours(3).msecs , WIDTH_SMALL_DOTS = 420 , WIDTH_BIG_DOTS = 800 - , TOOLTIP_TRANS_MS = 100 // milliseconds , TOOLTIP_WIDTH = 150 //min-width + padding ; +const zeroDate = new Date(0); + function init (client, d3) { var renderer = {}; @@ -17,6 +19,12 @@ function init (client, d3) { var utils = client.utils; var translate = client.translate; + function getOrAddDate(entry) { + if (entry.date) return entry.date; + entry.date = new Date(entry.mills); + return entry.date; + } + //chart isn't created till the client gets data, so can grab the var at init function chart () { return client.chart; @@ -40,20 +48,18 @@ function init (client, d3) { }; function tooltipLeft () { - var windowWidth = $(client.tooltip).parent().parent().width(); + var windowWidth = $(client.tooltip.node()).parent().parent().width(); var left = d3.event.pageX + TOOLTIP_WIDTH < windowWidth ? d3.event.pageX : windowWidth - TOOLTIP_WIDTH - 10; return left + 'px'; } function hideTooltip () { - client.tooltip.transition() - .duration(TOOLTIP_TRANS_MS) - .style('opacity', 0); + client.tooltip.style('opacity', 0); } // get the desired opacity for context chart based on the brush extent - renderer.highlightBrushPoints = function highlightBrushPoints (data) { - if (client.latestSGV && data.mills >= chart().brush.extent()[0].getTime() && data.mills <= chart().brush.extent()[1].getTime()) { + renderer.highlightBrushPoints = function highlightBrushPoints (data, from, to) { + if (client.latestSGV && data.mills >= from && data.mills <= to) { return chart().futureOpacity(data.mills - client.latestSGV.mills); } else { return 0.5; @@ -66,36 +72,18 @@ function init (client, d3) { }; renderer.addFocusCircles = function addFocusCircles () { - // get slice of data so that concatenation of predictions do not interfere with subsequent updates - var focusData = client.entries.slice(); - - if (client.sbx.pluginBase.forecastPoints) { - var shownForecastPoints = _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) { - return client.settings.showForecast.indexOf(point.info.type) > -1; - }); - var maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); - // limit lookahead to the same as lookback - var focusHoursAheadMills = chart().brush.extent()[1].getTime() + client.focusRangeMS; - maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); - client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0; - focusData = focusData.concat(shownForecastPoints); - } - - // bind up the focus chart data to an array of circles - // selects all our data into data and uses date function to get current max date - var focusCircles = chart().focus.selectAll('circle').data(focusData, client.entryToDate); - function prepareFocusCircles (sel) { + function updateFocusCircles (sel) { var badData = []; sel.attr('cx', function(d) { if (!d) { console.error('Bad data', d); - return chart().xScale(new Date(0)); + return chart().xScale(zeroDate); } else if (!d.mills) { console.error('Bad data, no mills', d); - return chart().xScale(new Date(0)); + return chart().xScale(zeroDate); } else { - return chart().xScale(new Date(d.mills)); + return chart().xScale(getOrAddDate(d)); } }) .attr('cy', function(d) { @@ -107,17 +95,12 @@ function init (client, d3) { return chart().yScale(scaled); } }) - .attr('fill', function(d) { - return d.type === 'forecast' ? 'none' : d.color; - }) .attr('opacity', function(d) { - return d.noFade || !client.latestSGV ? 100 : chart().futureOpacity(d.mills - client.latestSGV.mills); - }) - .attr('stroke-width', function(d) { - return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 2 : 0; - }) - .attr('stroke', function(d) { - return (d.type === 'mbg' ? 'white' : d.color); + if (d.noFade) { + return null; + } else { + return !client.latestSGV ? 1 : chart().futureOpacity(d.mills - client.latestSGV.mills); + } }) .attr('r', function(d) { return dotRadius(d.type); @@ -130,6 +113,21 @@ function init (client, d3) { return sel; } + function prepareFocusCircles (sel) { + updateFocusCircles(sel) + .attr('fill', function(d) { + return d.type === 'forecast' ? 'none' : d.color; + }) + .attr('stroke-width', function(d) { + return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 2 : 0; + }) + .attr('stroke', function(d) { + return (d.type === 'mbg' ? 'white' : d.color); + }); + + return sel; + } + function focusCircleTooltip (d) { if (d.type !== 'sgv' && d.type !== 'mbg' && d.type !== 'forecast') { return; @@ -149,51 +147,119 @@ function init (client, d3) { var rawbgInfo = getRawbgInfo(); - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html('' + translate('BG') + ': ' + client.sbx.scaleEntry(d) + (d.type === 'mbg' ? '
' + translate('Device') + ': ' + d.device : '') + (d.type === 'forecast' && d.forecastType ? '
' + translate('Forecast Type') + ': ' + d.forecastType : '') + (rawbgInfo.value ? '
' + translate('Raw BG') + ': ' + rawbgInfo.value : '') + (rawbgInfo.noise ? '
' + translate('Noise') + ': ' + rawbgInfo.noise : '') + - '
' + translate('Time') + ': ' + client.formatTime(new Date(d.mills))) + '
' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d))) .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); } + // CGM data + + var focusData = client.entries; + + // bind up the focus chart data to an array of circles + // selects all our data into data and uses date function to get current max date + var focusCircles = chart().focus.selectAll('circle.entry-dot').data(focusData, function genKey (d) { + return "cgmreading." + d.mills; + }); + // if already existing then transition each circle to its new position - prepareFocusCircles(focusCircles.transition()); + updateFocusCircles(focusCircles); // if new circle then just display prepareFocusCircles(focusCircles.enter().append('circle')) + .attr('class', 'entry-dot') .on('mouseover', focusCircleTooltip) .on('mouseout', hideTooltip); focusCircles.exit().remove(); + + // Forecasts + + var shownForecastPoints = client.chart.getForecastData(); + + // bind up the focus chart data to an array of circles + // selects all our data into data and uses date function to get current max date + + var forecastCircles = chart().focus.selectAll('circle.forecast-dot').data(shownForecastPoints, function genKey (d) { + return d.forecastType + d.mills; + }); + + forecastCircles.exit().remove(); + + prepareFocusCircles(forecastCircles.enter().append('circle')) + .attr('class', 'forecast-dot') + .on('mouseover', focusCircleTooltip) + .on('mouseout', hideTooltip); + + updateFocusCircles(forecastCircles); + }; - renderer.addTreatmentCircles = function addTreatmentCircles () { + renderer.addTreatmentCircles = function addTreatmentCircles (nowDate) { function treatmentTooltip (d) { var targetBottom = d.targetBottom; var targetTop = d.targetTop; if (client.settings.units === 'mmol') { - targetBottom = Math.round(targetBottom / 18.0 * 10) / 10; - targetTop = Math.round(targetTop / 18.0 * 10) / 10; + targetBottom = Math.round(targetBottom / consts.MMOL_TO_MGDL * 10) / 10; + targetTop = Math.round(targetTop / consts.MMOL_TO_MGDL * 10) / 10; } - return '' + translate('Time') + ': ' + client.formatTime(new Date(d.mills)) + '
' + + var correctionRangeText; + if (d.correctionRange) { + var min = d.correctionRange[0]; + var max = d.correctionRange[1]; + + if (client.settings.units === 'mmol') { + max = client.sbx.roundBGToDisplayFormat(client.sbx.scaleMgdl(max)); + min = client.sbx.roundBGToDisplayFormat(client.sbx.scaleMgdl(min)); + } + + if (d.correctionRange[0] === d.correctionRange[1]) { + correctionRangeText = '' + min; + } else { + correctionRangeText = '' + min + ' - ' + max; + } + } + + var durationText; + if (d.durationType === "indefinite") { + durationText = translate("Indefinite"); + } else if (d.duration) { + var durationMinutes = Math.round(d.duration); + if (durationMinutes > 0 && durationMinutes % 60 == 0) { + var durationHours = durationMinutes / 60; + if (durationHours > 1) { + durationText = durationHours + ' hours'; + } else { + durationText = durationHours + ' hour'; + } + } else { + durationText = durationMinutes + ' min'; + } + } + + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
' + (d.eventType ? '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
' : '') + (d.reason ? '' + translate('Reason') + ': ' + translate(d.reason) + '
' : '') + (d.glucose ? '' + translate('BG') + ': ' + d.glucose + (d.glucoseType ? ' (' + translate(d.glucoseType) + ')' : '') + '
' : '') + (d.enteredBy ? '' + translate('Entered By') + ': ' + d.enteredBy + '
' : '') + (d.targetTop ? '' + translate('Target Top') + ': ' + targetTop + '
' : '') + (d.targetBottom ? '' + translate('Target Bottom') + ': ' + targetBottom + '
' : '') + - (d.duration ? '' + translate('Duration') + ': ' + Math.round(d.duration) + ' min
' : '') + + (durationText ? '' + translate('Duration') + ': ' + durationText + '
' : '') + + (d.insulinNeedsScaleFactor ? '' + translate('Insulin Scale Factor') + ': ' + d.insulinNeedsScaleFactor * 100 + '%
' : '') + + (correctionRangeText ? '' + translate('Correction Range') + ': ' + correctionRangeText + '
' : '') + (d.notes ? '' + translate('Notes') + ': ' + d.notes : ''); } function announcementTooltip (d) { - return '' + translate('Time') + ': ' + client.formatTime(new Date(d.mills)) + '
' + + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
' + (d.eventType ? '' + translate('Announcement') + '
' : '') + (d.notes && d.notes.length > 1 ? '' + translate('Message') + ': ' + d.notes + '
' : '') + (d.enteredBy ? '' + translate('Entered By') + ': ' + d.enteredBy + '
' : ''); @@ -204,7 +270,7 @@ function init (client, d3) { //NOTE: treatments with insulin or carbs are drawn by drawTreatment() // bind up the focus chart data to an array of circles - var treatCircles = chart().focus.selectAll('treatment-dot').data(client.ddata.treatments.filter(function(treatment) { + var treatCircles = chart().focus.selectAll('.treatment-dot').data(client.ddata.treatments.filter(function(treatment) { var notCarbsOrInsulin = !treatment.carbs && !treatment.insulin; var notTempOrProfile = !_.includes(['Temp Basal', 'Profile Switch', 'Combo Bolus', 'Temporary Target'], treatment.eventType); @@ -216,8 +282,23 @@ function init (client, d3) { return notes.indexOf(spam) === 0; })); - return notCarbsOrInsulin && !treatment.duration && notTempOrProfile && notOpenAPSSpam; - })); + return notCarbsOrInsulin && !treatment.duration && treatment.durationType !== 'indefinite' && notTempOrProfile && notOpenAPSSpam; + }), function (d) { return d._id; }); + + function updateTreatCircles (sel) { + + sel.attr('cx', function(d) { + return chart().xScale(getOrAddDate(d)); + }) + .attr('cy', function(d) { + return chart().yScale(client.sbx.scaleEntry(d)); + }) + .attr('r', function() { + return dotRadius('mbg'); + }); + + return sel; + } function prepareTreatCircles (sel) { function strokeColor (d) { @@ -240,15 +321,7 @@ function init (client, d3) { return color; } - sel.attr('cx', function(d) { - return chart().xScale(new Date(d.mills)); - }) - .attr('cy', function(d) { - return chart().yScale(client.sbx.scaleEntry(d)); - }) - .attr('r', function() { - return dotRadius('mbg'); - }) + updateTreatCircles(sel) .attr('stroke-width', 2) .attr('stroke', strokeColor) .attr('fill', fillColor); @@ -257,20 +330,23 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - prepareTreatCircles(treatCircles.transition()); + updateTreatCircles(treatCircles); // if new circle then just display prepareTreatCircles(treatCircles.enter().append('circle')) + .attr('class', 'treatment-dot') .on('mouseover', function(d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d)) .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); }) .on('mouseout', hideTooltip); + treatCircles.exit().remove(); + var durationTreatments = client.ddata.treatments.filter(function(treatment) { - return !treatment.carbs && !treatment.insulin && treatment.duration && + return !treatment.carbs && !treatment.insulin && (treatment.duration || treatment.durationType !== undefined) && !_.includes(['Temp Basal', 'Profile Switch', 'Combo Bolus', 'Temporary Target'], treatment.eventType); }); @@ -306,69 +382,95 @@ function init (client, d3) { if (d.eventType === 'Temporary Target') { top = d.targetTop === d.targetBottom ? d.targetTop + rectHeight(d) : d.targetTop; } - return 'translate(' + chart().xScale(new Date(d.mills)) + ',' + chart().yScale(utils.scaleMgdl(top)) + ')'; + return 'translate(' + chart().xScale(getOrAddDate(d)) + ',' + chart().yScale(utils.scaleMgdl(top)) + ')'; } - // if already existing then transition each rect to its new position - treatRects.transition() - .attr('transform', rectTranslate); - chart().focus.selectAll('.g-duration-rect').transition() - .attr('width', function(d) { - return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills)); - }); + function treatmentRectWidth (d) { + if (d.durationType === "indefinite") { + return chart().xScale(chart().xScale.domain()[1].getTime()) - chart().xScale(getOrAddDate(d)); + } else { + return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(getOrAddDate(d)); + } + } - chart().focus.selectAll('.g-duration-text').transition() - .attr('transform', function(d) { - return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills))) / 2 + ',' + 10 + ')'; - }); + function treatmentTextTransform (d) { + if (d.durationType === "indefinite") { + var offset = 0; + if (chart().xScale(getOrAddDate(d)) < chart().xScale(chart().xScale.domain()[0].getTime())) { + offset = chart().xScale(nowDate) - chart().xScale(getOrAddDate(d)); + } + return 'translate(' + offset + ',' + 10 + ')'; + } else { + return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(getOrAddDate(d))) / 2 + ',' + 10 + ')'; + } + } + + function treatmentText (d) { + if (d.eventType === 'Temporary Target') { + return ''; + } + return d.notes || d.reason || d.eventType; + } + + function treatmentTextAnchor (d) { + return d.durationType === "indefinite" ? 'left' : 'middle'; + } + + // if transitioning, update rect text, position, and width + var rectUpdates = treatRects; + rectUpdates.attr('transform', rectTranslate); + + rectUpdates.select('text') + .text(treatmentText) + .attr('text-anchor', treatmentTextAnchor) + .attr('transform', treatmentTextTransform); + + rectUpdates.select('rect') + .attr('width', treatmentRectWidth) - // if new rect then just display - var gs = treatRects.enter().append('g') + // if new rect then create new elements + var newRects = treatRects.enter().append('g') .attr('class', 'g-duration') .attr('transform', rectTranslate) .on('mouseover', function(d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d)) .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); }) .on('mouseout', hideTooltip); - gs.append('rect') + newRects.append('rect') .attr('class', 'g-duration-rect') - .attr('width', function(d) { - return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills)); - }) + .attr('width', treatmentRectWidth) .attr('height', rectHeight) .attr('rx', 5) .attr('ry', 5) .attr('opacity', .2) .attr('fill', fillColor); - gs.append('text') + newRects.append('text') .attr('class', 'g-duration-text') .style('font-size', 15) .attr('fill', 'white') - .attr('text-anchor', 'middle') + .attr('text-anchor', treatmentTextAnchor) .attr('dy', '.35em') - .attr('transform', function(d) { - return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills))) / 2 + ',' + 10 + ')'; - }) - .text(function(d) { - if (d.eventType === 'Temporary Target') { - return ''; - } - return d.notes || d.eventType; - }); + .attr('transform', treatmentTextTransform) + .text(treatmentText); + + // Remove any rects no longer needed + treatRects.exit().remove(); }; + + renderer.addContextCircles = function addContextCircles () { // bind up the context chart data to an array of circles var contextCircles = chart().context.selectAll('circle').data(client.entries); function prepareContextCircles (sel) { var badData = []; - sel.attr('cx', function(d) { return chart().xScale2(new Date(d.mills)); }) + sel.attr('cx', function(d) { return chart().xScale2(getOrAddDate(d)); }) .attr('cy', function(d) { var scaled = client.sbx.scaleEntry(d); if (isNaN(scaled)) { @@ -379,7 +481,7 @@ function init (client, d3) { } }) .attr('fill', function(d) { return d.color; }) - .style('opacity', function(d) { return renderer.highlightBrushPoints(d) }) + //.style('opacity', function(d) { return renderer.highlightBrushPoints(d) }) .attr('stroke-width', function(d) { return d.type === 'mbg' ? 2 : 0; }) .attr('stroke', function() { return 'white'; }) .attr('r', function(d) { return d.type === 'mbg' ? 4 : 2; }); @@ -392,7 +494,7 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - prepareContextCircles(contextCircles.transition()); + prepareContextCircles(contextCircles); // if new circle then just display prepareContextCircles(contextCircles.enter().append('circle')); @@ -425,7 +527,7 @@ function init (client, d3) { }; } - function prepareArc (treatment, radius) { + function prepareArc (treatment, radius, renderBasal) { var arc_data = [ // white carb half-circle on top { 'element': '', 'color': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': radius.R1 } @@ -436,39 +538,39 @@ function init (client, d3) { // these used to be semicircles from 1.5708 to 4.7124, but that made the tooltip target too big { 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R3 } , { 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R4 } - ]; + ] + , arc_data_1_elements = []; arc_data[0].outlineOnly = !treatment.carbs; arc_data[2].outlineOnly = !treatment.insulin; if (treatment.carbs > 0) { - arc_data[1].element = Math.round(treatment.carbs) + ' g'; + arc_data_1_elements.push(Math.round(treatment.carbs) + ' g'); } if (treatment.protein > 0) { - arc_data[1].element = arc_data[1].element + " / " + Math.round(treatment.protein) + ' g'; + arc_data_1_elements.push(Math.round(treatment.protein) + ' g'); } if (treatment.fat > 0) { - arc_data[1].element = arc_data[1].element + " / " + Math.round(treatment.fat) + ' g'; + arc_data_1_elements.push(Math.round(treatment.fat) + ' g'); } + arc_data[1].element = arc_data_1_elements.join(' / '); + if (treatment.foodType) { arc_data[1].element = arc_data[1].element + " " + treatment.foodType; } if (treatment.insulin > 0) { var dosage_units = '' + Math.round(treatment.insulin * 100) / 100; - - var unit_of_measurement = ' U'; // One international unit of insulin (1 IU) is shown as '1 U' - var enteredBy = '' + treatment.enteredBy; - - if (treatment.insulin < 1 && !treatment.carbs && enteredBy.indexOf('openaps') > -1) { // don't show the unit of measurement for insulin boluses < 1 without carbs (e.g. oref0 SMB's). Otherwise lot's of small insulin only dosages are often unreadable - unit_of_measurement = ''; - // remove leading zeros to avoid overlap with adjacent boluses + + if (renderBasal === 'all-remove-zero-u') { dosage_units = (dosage_units + "").replace(/^0/, ""); } + var unit_of_measurement = (renderBasal === 'all-remove-zero-u' ? '' : ' U'); // One international unit of insulin (1 IU) is shown as '1 U' + arc_data[3].element = dosage_units + unit_of_measurement; } @@ -476,7 +578,7 @@ function init (client, d3) { arc_data[4].element = translate(treatment.status); } - var arc = d3.svg.arc() + var arc = d3.arc() .innerRadius(function(d) { return 5 * d.inner; }) @@ -526,15 +628,15 @@ function init (client, d3) { function treatmentTooltip () { var glucose = treatment.glucose; - if (client.settings.units != client.ddata.profile.data[0].units) { - glucose *= (client.settings.units === 'mmol' ? 0.055 : 18); + if (client.settings.units != client.ddata.profile.getUnits()) { + glucose *= (client.settings.units === 'mmol' ? (1 / consts.MMOL_TO_MGDL) : consts.MMOL_TO_MGDL); var decimals = (client.settings.units === 'mmol' ? 10 : 1); glucose = Math.round(glucose * decimals) / decimals; } - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); - client.tooltip.html('' + translate('Time') + ': ' + client.formatTime(new Date(treatment.mills)) + '
' + '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '
' + + client.tooltip.style('opacity', .9); + client.tooltip.html('' + translate('Time') + ': ' + client.formatTime(getOrAddDate(treatment)) + '
' + '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '
' + (treatment.carbs ? '' + translate('Carbs') + ': ' + treatment.carbs + '
' : '') + (treatment.protein ? '' + translate('Protein') + ': ' + treatment.protein + '
' : '') + (treatment.fat ? '' + translate('Fat') + ': ' + treatment.fat + '
' : '') + @@ -555,12 +657,12 @@ function init (client, d3) { var insulinRect = { x: 0, y: 0, width: 0, height: 0 }; var carbsRect = { x: 0, y: 0, width: 0, height: 0 }; var operation; - renderer.drag = d3.behavior.drag() - .on('dragstart', function() { + renderer.drag = d3.drag() + .on('start', function() { //console.log(treatment); - var windowWidth = $(client.tooltip).parent().parent().width(); + var windowWidth = $(client.tooltip.node()).parent().parent().width(); var left = d3.event.x + TOOLTIP_WIDTH < windowWidth ? d3.event.x : windowWidth - TOOLTIP_WIDTH - 10; - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9) + client.tooltip.style('opacity', .9) .style('left', left + 'px') .style('top', (d3.event.pageY ? d3.event.pageY + 15 : 40) + 'px'); @@ -571,29 +673,25 @@ function init (client, d3) { , height: chart().yScale(chart().yScale.domain()[0]) }; chart().drag.append('rect') - .attr({ - class: 'drag-droparea' - , x: deleteRect.x - , y: deleteRect.y - , width: deleteRect.width - , height: deleteRect.height - , fill: 'red' - , opacity: 0.4 - , rx: 10 - , ry: 10 - }); + .attr('class', 'drag-droparea') + .attr('x', deleteRect.x) + .attr('y', deleteRect.y) + .attr('width', deleteRect.width) + .attr('height', deleteRect.height) + .attr('fill', 'red') + .attr('opacity', 0.4) + .attr('rx', 10) + .attr('ry', 10); chart().drag.append('text') - .attr({ - class: 'drag-droparea' - , x: deleteRect.x + deleteRect.width / 2 - , y: deleteRect.y + deleteRect.height / 2 - , 'font-size': 15 - , 'font-weight': 'bold' - , fill: 'red' - , 'text-anchor': 'middle' - , dy: '.35em' - , transform: 'rotate(-90 ' + (deleteRect.x + deleteRect.width / 2) + ',' + (deleteRect.y + deleteRect.height / 2) + ')' - }) + .attr('class', 'drag-droparea') + .attr('x', deleteRect.x + deleteRect.width / 2) + .attr('y', deleteRect.y + deleteRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', 'red') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .attr('transform', 'rotate(-90 ' + (deleteRect.x + deleteRect.width / 2) + ',' + (deleteRect.y + deleteRect.height / 2) + ')') .text(translate('Remove')); if (treatment.insulin && treatment.carbs) { @@ -610,52 +708,44 @@ function init (client, d3) { , height: 50 }; chart().drag.append('rect') - .attr({ - class: 'drag-droparea' - , x: carbsRect.x - , y: carbsRect.y - , width: carbsRect.width - , height: carbsRect.height - , fill: 'white' - , opacity: 0.4 - , rx: 10 - , ry: 10 - }); + .attr('class', 'drag-droparea') + .attr('x', carbsRect.x) + .attr('y', carbsRect.y) + .attr('width', carbsRect.width) + .attr('height', carbsRect.height) + .attr('fill', 'white') + .attr('opacitys', 0.4) + .attr('rx', 10) + .attr('ry', 10); chart().drag.append('text') - .attr({ - class: 'drag-droparea' - , x: carbsRect.x + carbsRect.width / 2 - , y: carbsRect.y + carbsRect.height / 2 - , 'font-size': 15 - , 'font-weight': 'bold' - , fill: 'white' - , 'text-anchor': 'middle' - , dy: '.35em' - }) + .attr('class', 'drag-droparea') + .attr('x', carbsRect.x + carbsRect.width / 2) + .attr('y', carbsRect.y + carbsRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', 'white') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') .text(translate('Move carbs')); chart().drag.append('rect') - .attr({ - class: 'drag-droparea' - , x: insulinRect.x - , y: insulinRect.y - , width: insulinRect.width - , height: insulinRect.height - , fill: '#0099ff' - , opacity: 0.4 - , rx: 10 - , ry: 10 - }); + .attr('class', 'drag-droparea') + .attr('x', insulinRect.x) + .attr('y', insulinRect.y) + .attr('width', insulinRect.width) + .attr('height', insulinRect.height) + .attr('fill', '#0099ff') + .attr('opacity', 0.4) + .attr('rx', 10) + .attr('ry', 10); chart().drag.append('text') - .attr({ - class: 'drag-droparea' - , x: insulinRect.x + insulinRect.width / 2 - , y: insulinRect.y + insulinRect.height / 2 - , 'font-size': 15 - , 'font-weight': 'bold' - , fill: '#0099ff' - , 'text-anchor': 'middle' - , dy: '.35em' - }) + .attr('class', 'drag-droparea') + .attr('x', insulinRect.x + insulinRect.width / 2) + .attr('y', insulinRect.y + insulinRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', '#0099ff') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') .text(translate('Move insulin')); } @@ -665,7 +755,7 @@ function init (client, d3) { }) .on('drag', function() { //console.log(d3.event); - client.tooltip.transition().style('opacity', .9); + client.tooltip.style('opacity', .9); var x = Math.min(Math.max(0, d3.event.x), chart().charts.attr('width')); var y = Math.min(Math.max(0, d3.event.y), chart().focusHeight); @@ -692,19 +782,16 @@ function init (client, d3) { chart().drag.selectAll('.arrow').remove(); chart().drag.append('line') - .attr({ - 'class': 'arrow' - , 'marker-end': 'url(#arrow)' - , 'x1': chart().xScale(new Date(treatment.mills)) - , 'y1': chart().yScale(client.sbx.scaleEntry(treatment)) - , 'x2': x - , 'y2': y - , 'stroke-width': 2 - , 'stroke': 'white' - }); - + .attr('class', 'arrow') + .attr('marker-end', 'url(#arrow)') + .attr('x1', chart().xScale(getOrAddDate(treatment))) + .attr('y1', chart().yScale(client.sbx.scaleEntry(treatment))) + .attr('x2', x) + .attr('y2', y) + .attr('stroke-width', 2) + .attr('stroke', 'white'); }) - .on('dragend', function() { + .on('end', function() { var newTreatment; chart().drag.selectAll('.drag-droparea').remove(); hideTooltip(); @@ -719,7 +806,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -736,7 +823,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -753,7 +840,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -769,7 +856,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -797,7 +884,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -825,7 +912,7 @@ function init (client, d3) { } , function callback (result) { console.log(result); - chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove(); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); } ); } else { @@ -841,7 +928,7 @@ function init (client, d3) { .enter() .append('g') .attr('class', 'draggable-treatment') - .attr('transform', 'translate(' + chart().xScale(new Date(treatment.mills)) + ', ' + chart().yScale(client.sbx.scaleEntry(treatment)) + ')') + .attr('transform', 'translate(' + chart().xScale(getOrAddDate(treatment)) + ', ' + chart().yScale(client.sbx.scaleEntry(treatment)) + ')') .on('mouseover', treatmentTooltip) .on('mouseout', hideTooltip); if (client.editMode) { @@ -877,11 +964,15 @@ function init (client, d3) { .attr('id', 'label') .style('fill', 'white'); - // reduce the treatment label font size to make it readable with SMB - var fontBaseSize = (opts.treatments >= 30) ? 40 : 50 - Math.floor((25 - opts.treatments) / 30 * 10); - label.append('text') - .style('font-size', fontBaseSize / opts.scale) + .style('font-size', function(d) { + var fontSize = ( (opts.treatments >= 30) ? 40 : 50 - Math.floor((25 - opts.treatments) / 30 * 10) ) / opts.scale; + var elementValue = parseFloat(d.element); + if (!isNaN(elementValue) && elementValue < 1) { + fontSize = (25 + Math.floor(elementValue * 10)) / opts.scale; + } + return fontSize; + }) .style('text-shadow', '0px 0px 10px rgba(0, 0, 0, 1)') .attr('text-anchor', 'middle') .attr('dy', '.35em') @@ -899,6 +990,7 @@ function init (client, d3) { renderer.drawTreatments = function drawTreatments (client) { var treatmentCount = 0; + var renderBasal = client.settings.extendedSettings.bolus.render; chart().focus.selectAll('.draggable-treatment').remove(); _.forEach(client.ddata.treatments, function eachTreatment (d) { @@ -907,23 +999,25 @@ function init (client, d3) { // add treatment bubbles _.forEach(client.ddata.treatments, function eachTreatment (d) { + var showLabels = ( !d.carbs && ( ( renderBasal == 'none') || ( renderBasal === 'over' && d.insulin < client.settings.bolusRenderOver) ) ) ? false : true; renderer.drawTreatment(d, { scale: renderer.bubbleScale() - , showLabels: true + , showLabels: showLabels , treatments: treatmentCount - }, client.sbx.data.profile.getCarbRatio(new Date())); + }, client.sbx.data.profile.getCarbRatio(new Date()), + renderBasal); }); }; - renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio) { - if (!treatment.carbs && !treatment.insulin) { + renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio, renderBasal) { + if (!treatment.carbs && !treatment.protein && !treatment.fat && !treatment.insulin) { return; } //when the tests are run window isn't available var innerWidth = window && window.innerWidth || -1; // don't render the treatment if it's not visible - if (Math.abs(chart().xScale(new Date(treatment.mills))) > innerWidth) { + if (Math.abs(chart().xScale(getOrAddDate(treatment))) > innerWidth) { return; } @@ -933,13 +1027,16 @@ function init (client, d3) { return; } - var arc = prepareArc(treatment, radius); + var arc = prepareArc(treatment, radius, renderBasal); var treatmentDots = appendTreatments(treatment, arc); appendLabels(treatmentDots, arc, opts); }; renderer.addBasals = function addBasals (client) { + if (!client.settings.isEnabled('basal')) { + return; + } var mode = client.settings.extendedSettings.basal.render; var profile = client.sbx.data.profile; var linedata = []; @@ -947,8 +1044,9 @@ function init (client, d3) { var basalareadata = []; var tempbasalareadata = []; var comboareadata = []; - var from = chart().brush.extent()[0].getTime(); - var to = Math.max(chart().brush.extent()[1].getTime(), client.sbx.time) + client.forecastTime; + var selectedRange = chart().createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); var date = from; var lastbasal = 0; @@ -1007,16 +1105,16 @@ function init (client, d3) { chart().basals.selectAll('.tempbasalarea').remove().data(tempbasalareadata); chart().basals.selectAll('.comboarea').remove().data(comboareadata); - var valueline = d3.svg.line() - .interpolate('step-after') + var valueline = d3.line() .x(function(d) { return chart().xScaleBasals(d.d); }) - .y(function(d) { return chart().yScaleBasals(d.b); }); + .y(function(d) { return chart().yScaleBasals(d.b); }) + .curve(d3.curveStepAfter); - var area = d3.svg.area() - .interpolate('step-after') + var area = d3.area() .x(function(d) { return chart().xScaleBasals(d.d); }) .y0(chart().yScaleBasals(0)) - .y1(function(d) { return chart().yScaleBasals(d.b); }); + .y1(function(d) { return chart().yScaleBasals(d.b); }) + .curve(d3.curveStepAfter); var g = chart().basals.append('g'); @@ -1087,7 +1185,7 @@ function init (client, d3) { } function profileTooltip (d) { - return '' + translate('Time') + ': ' + client.formatTime(new Date(d.mills)) + '
' + + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
' + (d.eventType ? '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
' : '') + (d.endprofile ? '' + translate('End of profile') + ': ' + d.endprofile + '
' : '') + (d.profile ? '' + translate('Profile') + ': ' + d.profile + '
' : '') + @@ -1097,8 +1195,9 @@ function init (client, d3) { } // calculate position of profile on left side - var from = chart().brush.extent()[0].getTime(); - var to = chart().brush.extent()[1].getTime(); + var selectedRange = chart().createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); var mult = (to - from) / times.hours(24).msecs; from += times.mins(20 * mult).msecs; @@ -1137,8 +1236,7 @@ function init (client, d3) { return ret; }; - treatProfiles.transition().duration(0) - .attr('transform', function(t) { + treatProfiles.attr('transform', function(t) { // change text of record on left side return 'rotate(-90,' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ') ' + 'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')'; @@ -1158,7 +1256,7 @@ function init (client, d3) { }) .text(generateText) .on('mouseover', function(d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); client.tooltip.html(profileTooltip(d)) .style('left', (d3.event.pageX) + 'px') .style('top', (d3.event.pageY + 15) + 'px'); diff --git a/lib/constants.json b/lib/constants.json index 31a4524b4d3..38e36040d42 100644 --- a/lib/constants.json +++ b/lib/constants.json @@ -4,5 +4,16 @@ "HTTP_UNAUTHORIZED" : 401, "HTTP_VALIDATION_ERROR" : 422, "HTTP_INTERNAL_ERROR" : 500, - "ENTRIES_DEFAULT_COUNT" : 10 + "HTTP_BAD_REQUEST": 400, + "ENTRIES_DEFAULT_COUNT" : 10, + "PROFILES_DEFAULT_COUNT" : 10, + "MMOL_TO_MGDL": 18, + "ONE_DAY" : 86400000, + "TWO_DAYS" : 172800000, + "FIFTEEN_MINUTES": 900000, + "THIRTY_MINUTES": 1800000, + "ONE_HOUR": 3600000, + "THREE_HOURS": 10800000, + "FOUR_HOURS": 14400000, + "SIX_HOURS": 21600000 } diff --git a/lib/data/calcdelta.js b/lib/data/calcdelta.js index e3e0fde7052..c991bd8d602 100644 --- a/lib/data/calcdelta.js +++ b/lib/data/calcdelta.js @@ -75,7 +75,7 @@ module.exports = function calcDelta (oldData, newData) { var result = []; l = newArray.length; for (var j = 0; j < l; j++) { - if (!seen.hasOwnProperty(newArray[j].mills)) { + if (!Object.prototype.hasOwnProperty.call(seen, newArray[j].mills)) { result.push(newArray[j]); } } @@ -94,12 +94,12 @@ module.exports = function calcDelta (oldData, newData) { var changesFound = false; for (var array in compressibleArrays) { - if (compressibleArrays.hasOwnProperty(array)) { + if (Object.prototype.hasOwnProperty.call(compressibleArrays, array)) { var a = compressibleArrays[array]; - if (newData.hasOwnProperty(a)) { + if (Object.prototype.hasOwnProperty.call(newData, a)) { // if previous data doesn't have the property (first time delta?), just assign data over - if (!oldData.hasOwnProperty(a)) { + if (!Object.prototype.hasOwnProperty.call(oldData, a)) { delta[a] = newData[a]; changesFound = true; continue; @@ -125,9 +125,9 @@ module.exports = function calcDelta (oldData, newData) { var changesFound = false; for (var object in skippableObjects) { - if (skippableObjects.hasOwnProperty(object)) { + if (Object.prototype.hasOwnProperty.call(skippableObjects, object)) { var o = skippableObjects[object]; - if (newData.hasOwnProperty(o)) { + if (Object.prototype.hasOwnProperty.call(newData, o)) { if (JSON.stringify(newData[o]) !== JSON.stringify(oldData[o])) { //console.log('delta changes found on', o); changesFound = true; diff --git a/lib/data/dataloader.js b/lib/data/dataloader.js index cc4426aa6ab..7074b18e1fb 100644 --- a/lib/data/dataloader.js +++ b/lib/data/dataloader.js @@ -1,29 +1,82 @@ 'use strict'; -var _ = require('lodash'); -var async = require('async'); -var times = require('../times'); -var fitTreatmentsToBGCurve = require('./treatmenttocurve'); +const _ = require('lodash'); +const async = require('async'); +const fitTreatmentsToBGCurve = require('./treatmenttocurve'); +const constants = require('../constants'); -var ONE_DAY = 86400000, - TWO_DAYS = 172800000; - -function uniq(a) { +function uniqBasedOnMills(a) { var seen = {}; return a.filter(function(item) { - // eslint-disable-next-line no-prototype-builtins - return seen.hasOwnProperty(item.mills) ? false : (seen[item.mills] = true); + return Object.prototype.hasOwnProperty.call(seen, item.mills) ? false : (seen[item.mills] = true); + }); +} + +const processForRuntime = (obj) => { + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'object' && obj[key]) { + if (obj[key].hasOwnProperty('_id')) { + obj[key]._id = obj[key]._id.toString(); + } + if (obj[key].hasOwnProperty('created_at') && !obj[key].hasOwnProperty('mills')) { + obj[key].mills = new Date(obj[key].created_at).getTime(); + } + } }); } +const findLatestMills = (data) => { + if (!data) return; + let max = data[0].mills; + for (let i = 0, len = data.length; i < len; i++) { + let o = data[i]; + max = o.mills > max ? o.mills : max; + } + return max; +} + +function mergeProcessSort(oldData, newData, ageLimit) { + + processForRuntime(newData); + + var filtered = _.filter(newData, function hasId(object) { + const hasId = !_.isEmpty(object._id); + const isFresh = (ageLimit && object.mills >= ageLimit) || (!ageLimit); + return isFresh && hasId; + }); + + // Merge old and new data, preferring the new objects + + let merged = []; + if (oldData && filtered) { + merged = filtered; // Start with the new / updated data + for (let i = 0; i < oldData.length; i++) { + const oldElement = oldData[i]; + let found = false; + for (let j = 0; j < filtered.length; j++) { + if (oldElement._id == filtered[j]._id) { + found = true; + break; + } + } + if (!found) merged.push(oldElement); // Merge old object in, if it wasn't found in the new data + } + } else { + merged = filtered; + } + + return _.sortBy(merged, function(item) { + return item.mills; + }); + +} + function init(env, ctx) { var dataloader = {}; dataloader.update = function update(ddata, opts, done) { - //console.log("Database is connected: " + ctx.store.client.isConnected()); - if (opts && done == null && opts.call) { done = opts; opts = { @@ -40,16 +93,38 @@ function init(env, ctx) { }; } ddata.lastUpdated = opts.lastUpdated; - // console.log('LOOKING SINCE', (new Date(ddata.lastUpdated))); + + const normalizeTreatments = (obj) => { + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'object' && obj[key]) { + const element = obj[key]; + if (element.hasOwnProperty('_id')) { + element._id = element._id.toString(); + } + if (element.hasOwnProperty('amount') && !element.hasOwnProperty('absolute')) { + element.absolute = Number(element.amount); + } + normalizeTreatments(obj[key]); + } + }); + } function loadComplete(err, result) { + + // convert all IDs to strings, as these are not used after load + + normalizeTreatments(ddata); + ddata.treatments = _.uniq(ddata.treatments, false, function(item) { - return item._id.toString(); + return item._id; }); + //sort treatments so the last is the most recent + ddata.treatments = _.sortBy(ddata.treatments, function(item) { return item.mills; }); + fitTreatmentsToBGCurve(ddata, env, ctx); if (err) { console.error(err); @@ -68,8 +143,13 @@ function init(env, ctx) { done(err, result); } - // clear treatments, we're going to merge from more queries + // clear data we'll get from the cache + ddata.treatments = []; + ddata.devicestatus = []; + ddata.entries = []; + + ddata.dbstats = {}; async.parallel([ loadEntries.bind(null, ddata, ctx) @@ -80,6 +160,7 @@ function init(env, ctx) { , loadFood.bind(null, ddata, ctx) , loadDeviceStatus.bind(null, ddata, env, ctx) , loadActivity.bind(null, ddata, ctx) + , loadDatabaseStats.bind(null, ddata, ctx) ], loadComplete); }; @@ -89,10 +170,15 @@ function init(env, ctx) { } function loadEntries(ddata, ctx, callback) { + + const withFrame = ddata.page && ddata.page.frame; + const longLoad = Math.round(constants.TWO_DAYS); + const loadTime = ctx.cache.isEmpty('entries') || withFrame ? longLoad : constants.FIFTEEN_MINUTES; + var dateRange = { - $gte: ddata.lastUpdated - TWO_DAYS + $gte: ddata.lastUpdated - loadTime }; - if (ddata.page && ddata.page.frame) { + if (withFrame) { dateRange['$lte'] = ddata.lastUpdated; } var q = { @@ -111,19 +197,28 @@ function loadEntries(ddata, ctx, callback) { } if (!err && results) { - var mbgs = []; - var sgvs = []; - var cals = []; - results.forEach(function(element) { + + const r = ctx.ddata.processRawDataForRuntime(results); + const currentData = ctx.cache.insertData('entries', r).reverse(); + + const mbgs = []; + const sgvs = []; + const cals = []; + + currentData.forEach(function(element) { if (element) { + if (!element.mills) element.mills = element.date; if (element.mbg) { mbgs.push({ + _id: element._id, mgdl: Number(element.mbg), mills: element.date, - device: element.device + device: element.device, + type: 'mbg' }); } else if (element.sgv) { sgvs.push({ + _id: element._id, mgdl: Number(element.sgv), mills: element.date, device: element.device, @@ -131,54 +226,34 @@ function loadEntries(ddata, ctx, callback) { filtered: element.filtered, unfiltered: element.unfiltered, noise: element.noise, - rssi: element.rssi + rssi: element.rssi, + type: 'sgv' }); } else if (element.type === 'cal') { cals.push({ + _id: element._id, mills: element.date, scale: element.scale, intercept: element.intercept, - slope: element.slope + slope: element.slope, + type: 'cal' }); } } }); - //stop using uniq for SGVs since we use buckets, also enables more detailed monitoring + const ageLimit = ddata.lastUpdated - constants.TWO_DAYS; ddata.sgvs = sgvs; - - ddata.mbgs = uniq(mbgs); - ddata.cals = uniq(cals); + ddata.mbgs = mbgs; + ddata.cals = cals; } callback(); }); } -function mergeToTreatments(ddata, results) { - var filtered = _.filter(results, function hasId(treatment) { - return !_.isEmpty(treatment._id); - }); - - var treatments = _.map(filtered, function update(treatment) { - treatment.mills = new Date(treatment.created_at).getTime(); - return treatment; - }); - - //filter out temps older than a day and an hour ago since we don't display them - var oldestAgo = ddata.lastUpdated - TWO_DAYS - times.hour().msecs; - treatments = _.filter(treatments, function noOldTemps(treatment) { - return !treatment.eventType || treatment.eventType.indexOf('Temp Basal') === -1 || treatment.mills > oldestAgo; - }); - - ddata.treatments = _.unionWith(ddata.treatments, treatments, function(a, b) { - return a._id.toString() == b._id.toString(); - }); -} - - function loadActivity(ddata, ctx, callback) { var dateRange = { - $gte: new Date(ddata.lastUpdated - (ONE_DAY * 2)).toISOString() + $gte: new Date(ddata.lastUpdated - (constants.ONE_DAY * 2)).toISOString() }; if (ddata.page && ddata.page.frame) { dateRange['$lte'] = new Date(ddata.lastUpdated).toISOString(); @@ -193,7 +268,6 @@ function loadActivity(ddata, ctx, callback) { } }; - var activity = []; ctx.activity.list(q, function(err, results) { if (err) { @@ -216,7 +290,7 @@ function loadActivity(ddata, ctx, callback) { } }); - ddata.activity = uniq(activity); + ddata.activity = uniqBasedOnMills(activity); } callback(); }); @@ -224,11 +298,18 @@ function loadActivity(ddata, ctx, callback) { function loadTreatments(ddata, ctx, callback) { - // Load 2.5 days to cover last 48 hours including overlapping temp boluses or temp targets + const withFrame = ddata.page && ddata.page.frame; + const longLoad = Math.round(constants.ONE_DAY * 2.5); //ONE_DAY * 2.5; + + // Load 2.5 days to cover last 48 hours including overlapping temp boluses or temp targets for first load + // Subsequently load at least 15 minutes of data + + const loadTime = ctx.cache.isEmpty('treatments') || withFrame ? longLoad : constants.FIFTEEN_MINUTES; + var dateRange = { - $gte: new Date(ddata.lastUpdated - (ONE_DAY * 2.5)).toISOString() + $gte: new Date(ddata.lastUpdated - loadTime).toISOString() }; - if (ddata.page && ddata.page.frame) { + if (withFrame) { dateRange['$lte'] = new Date(ddata.lastUpdated).toISOString(); } var tq = { @@ -242,7 +323,11 @@ function loadTreatments(ddata, ctx, callback) { ctx.treatments.list(tq, function(err, results) { if (!err && results) { - mergeToTreatments(ddata, results); + + // update cache and apply to runtime data + const r = ctx.ddata.processRawDataForRuntime(results); + const currentData = ctx.cache.insertData('treatments', r); + ddata.treatments = ctx.ddata.idMergePreferNew(ddata.treatments, currentData); } callback(); @@ -251,7 +336,7 @@ function loadTreatments(ddata, ctx, callback) { function loadProfileSwitchTreatments(ddata, ctx, callback) { var dateRange = { - $gte: new Date(ddata.lastUpdated - (ONE_DAY * 31 * 12)).toISOString() + $gte: new Date(ddata.lastUpdated - (constants.ONE_DAY * 31 * 12)).toISOString() }; if (ddata.page && ddata.page.frame) { @@ -273,7 +358,7 @@ function loadProfileSwitchTreatments(ddata, ctx, callback) { ctx.treatments.list(tq, function(err, results) { if (!err && results) { - mergeToTreatments(ddata, results); + ddata.treatments = mergeProcessSort(ddata.treatments, results); } // Store last profile switch @@ -307,7 +392,7 @@ function loadSensorAndInsulinTreatments(ddata, ctx, callback) { function loadLatestSingle(ddata, ctx, dataType, callback) { var dateRange = { - $gte: new Date(ddata.lastUpdated - (ONE_DAY * 32)).toISOString() + $gte: new Date(ddata.lastUpdated - (constants.ONE_DAY * 32)).toISOString() }; if (ddata.page && ddata.page.frame) { @@ -329,9 +414,8 @@ function loadLatestSingle(ddata, ctx, dataType, callback) { ctx.treatments.list(tq, function(err, results) { if (!err && results) { - mergeToTreatments(ddata, results); + ddata.treatments = mergeProcessSort(ddata.treatments, results); } - callback(); }); } @@ -361,12 +445,19 @@ function loadFood(ddata, ctx, callback) { } function loadDeviceStatus(ddata, env, ctx, callback) { + + const withFrame = ddata.page && ddata.page.frame; + const longLoad = env.extendedSettings.devicestatus && env.extendedSettings.devicestatus.days && env.extendedSettings.devicestatus.days == 2 ? constants.TWO_DAYS : constants.ONE_DAY; + const loadTime = ctx.cache.isEmpty('devicestatus') || withFrame ? longLoad : constants.FIFTEEN_MINUTES; + var dateRange = { - $gte: new Date(ddata.lastUpdated - ONE_DAY).toISOString() + $gte: new Date( ddata.lastUpdated - loadTime ).toISOString() }; - if (ddata.page && ddata.page.frame) { + + if (withFrame) { dateRange['$lte'] = new Date(ddata.lastUpdated).toISOString(); } + var opts = { find: { created_at: dateRange @@ -376,16 +467,14 @@ function loadDeviceStatus(ddata, env, ctx, callback) { } }; - if (env.extendedSettings.devicestatus && env.extendedSettings.devicestatus.advanced) { - //not adding count: 1 restriction - } else { - opts.count = 1; - } - ctx.devicestatus.list(opts, function(err, results) { if (!err && results) { - ddata.devicestatus = _.map(results, function eachStatus(result) { - result.mills = new Date(result.created_at).getTime(); + + // update cache and apply to runtime data + const r = ctx.ddata.processRawDataForRuntime(results); + const currentData = ctx.cache.insertData('devicestatus', r); + + const res2 = _.map(currentData, function eachStatus(result) { if ('uploaderBattery' in result) { result.uploader = { battery: result.uploaderBattery @@ -393,7 +482,9 @@ function loadDeviceStatus(ddata, env, ctx, callback) { delete result.uploaderBattery; } return result; - }).reverse(); + }); + + ddata.devicestatus = mergeProcessSort(ddata.devicestatus, res2); } else { ddata.devicestatus = []; } @@ -401,5 +492,20 @@ function loadDeviceStatus(ddata, env, ctx, callback) { }); } +function loadDatabaseStats(ddata, ctx, callback) { + ctx.store.db.stats(function mongoDone (err, result) { + if (err) { + console.log("Problem loading database stats"); + } + if (!err && result) { + ddata.dbstats = { + dataSize: result.dataSize + , indexSize: result.indexSize + }; + } + callback(); + }); +} + module.exports = init; diff --git a/lib/data/ddata.js b/lib/data/ddata.js index 1912ca4a6ae..9389f10d360 100644 --- a/lib/data/ddata.js +++ b/lib/data/ddata.js @@ -2,6 +2,7 @@ var _ = require('lodash'); var times = require('../times'); +var consts = require('../constants'); var DEVICE_TYPE_FIELDS = ['uploader', 'pump', 'openaps', 'loop', 'xdripjs']; @@ -16,9 +17,65 @@ function init () { , devicestatus: [] , food: [] , activity: [] + , dbstats: {} , lastUpdated: 0 }; + /** + * Convert Mongo ids to strings and ensure all objects have the mills property for + * significantly faster processing than constant date parsing, plus simplified + * logic + */ + ddata.processRawDataForRuntime = (data) => { + + let obj = _.cloneDeep(data); + + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'object' && obj[key]) { + if (Object.prototype.hasOwnProperty.call(obj[key], '_id')) { + obj[key]._id = obj[key]._id.toString(); + } + if (Object.prototype.hasOwnProperty.call(obj[key], 'created_at') + && !Object.prototype.hasOwnProperty.call(obj[key], 'mills')) { + obj[key].mills = new Date(obj[key].created_at).getTime(); + } + if (Object.prototype.hasOwnProperty.call(obj[key], 'sysTime') + && !Object.prototype.hasOwnProperty.call(obj[key], 'mills')) { + obj[key].mills = new Date(obj[key].sysTime).getTime(); + } + } + }); + + return obj; + }; + + /** + * Merge two arrays based on _id string, preferring new objects when a collision is found + * @param {array} oldData + * @param {array} newData + */ + ddata.idMergePreferNew = (oldData, newData) => { + + if (!newData && oldData) return oldData; + if (!oldData && newData) return newData; + + const merged = _.cloneDeep(newData); + + for (let i = 0; i < oldData.length; i++) { + const oldElement = oldData[i]; + let found = false; + for (let j = 0; j < newData.length; j++) { + if (oldElement._id == newData[j]._id) { + found = true; + break; + } + } + if (!found) merged.push(oldElement); // Merge old object in, if it wasn't found in the new data + } + + return merged; + }; + ddata.clone = function clone () { return _.clone(ddata, function(value) { //special handling of mongo ObjectID's @@ -32,59 +89,28 @@ function init () { }); }; - ddata.splitRecent = function splitRecent (time, cutoff, max, treatmentsToo) { - var result = { - first: {} - , rest: {} - }; - - function recent (item) { - return item.mills >= time - cutoff; - } - - function filterMax (item) { - return item.mills >= time - max; - } - - function partition (field, filter) { - var data; - if (filter) { - data = ddata[field].filter(filterMax); - } else { - data = ddata[field]; - } - - var parts = _.partition(data, recent); - result.first[field] = parts[0]; - result.rest[field] = parts[1]; - } - - partition('treatments', treatmentsToo ? filterMax : false); - - result.first.devicestatus = ddata.recentDeviceStatus(time); - - result.first.sgvs = ddata.sgvs.filter(filterMax); - result.first.cals = ddata.cals; + ddata.dataWithRecentStatuses = function dataWithRecentStatuses () { + var results = {}; + results.devicestatus = ddata.recentDeviceStatus(Date.now()); + results.sgvs = ddata.sgvs; + results.cals = ddata.cals; var profiles = _.cloneDeep(ddata.profiles); - if (profiles && profiles[0]) { + if (profiles && profiles[0] && profiles[0].store) { Object.keys(profiles[0].store).forEach(k => { if (k.indexOf('@@@@@') > 0) { delete profiles[0].store[k]; } }) } - result.first.profiles = profiles; + results.profiles = profiles; + results.mbgs = ddata.mbgs; + results.food = ddata.food; + results.treatments = ddata.treatments; + results.dbstats = ddata.dbstats; - result.rest.mbgs = ddata.mbgs.filter(filterMax); - result.rest.food = ddata.food; - result.rest.activity = ddata.activity; - - console.log('results.first size', JSON.stringify(result.first).length, 'bytes'); - console.log('results.rest size', JSON.stringify(result.rest).length, 'bytes'); - - return result; - }; + return results; + } ddata.recentDeviceStatus = function recentDeviceStatus (time) { @@ -244,18 +270,18 @@ function init () { if (t.units) { if (t.units == 'mmol') { //convert to mgdl - t.targetTop = t.targetTop * 18; - t.targetBottom = t.targetBottom * 18; + t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; + t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } } //if we have a temp target thats below 20, assume its mmol and convert to mgdl for safety. if (t.targetTop < 20) { - t.targetTop = t.targetTop * 18; + t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } if (t.targetBottom < 20) { - t.targetBottom = t.targetBottom * 18; + t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; t.units = 'mg/dl'; } return t.eventType && t.eventType.indexOf('Temporary Target') > -1; diff --git a/lib/data/treatmenttocurve.js b/lib/data/treatmenttocurve.js index 8bca564d091..714fb9049e6 100644 --- a/lib/data/treatmenttocurve.js +++ b/lib/data/treatmenttocurve.js @@ -1,9 +1,10 @@ 'use strict'; var _ = require('lodash'); +var consts = require('../constants'); const MAX_BG_MMOL = 22; -const MAX_BG_MGDL = MAX_BG_MMOL * 18; +const MAX_BG_MGDL = MAX_BG_MMOL * consts.MMOL_TO_MGDL; module.exports = function fitTreatmentsToBGCurve (ddata, env, ctx) { @@ -40,7 +41,7 @@ module.exports = function fitTreatmentsToBGCurve (ddata, env, ctx) { calcedBG = mgdlAfter; } - return calcedBG || 180; + return Math.round(calcedBG) || 180; } function mgdlValue (entry) { diff --git a/lib/hashauth.js b/lib/hashauth.js index 0840381a24c..8848ca08ef0 100644 --- a/lib/hashauth.js +++ b/lib/hashauth.js @@ -9,6 +9,7 @@ var hashauth = { , apisecrethash: null , authenticated: false , initialized: false + , tokenauthenticated: false }; hashauth.init = function init(client, $) { @@ -24,15 +25,27 @@ hashauth.init = function init(client, $) { , url: '/api/v1/verifyauth?t=' + Date.now() //cache buster , headers: client.headers() }).done(function verifysuccess (response) { - if (response.message === 'OK') { + + if (response.message.rolefound == 'FOUND') { + hashauth.tokenauthenticated = true; + console.log('Token Authentication passed.'); + client.authorizeSocket(); + next(true); + return; + } + + if (response.message.message === 'OK') { hashauth.authenticated = true; console.log('Authentication passed.'); next(true); - } else { - console.log('Authentication failed.', response); + return; + } + + console.log('Authentication failed.', response); hashauth.removeAuthentication(); next(false); - } + return; + }).fail(function verifyfail (err) { console.log('Authentication failed.', err); hashauth.removeAuthentication(); @@ -60,7 +73,7 @@ hashauth.init = function init(client, $) { Storages.localStorage.remove('apisecrethash'); - if (hashauth.authenticated) { + if (hashauth.authenticated || hashauth.tokenauthenticated) { client.browserUtils.reload(); } @@ -78,8 +91,15 @@ hashauth.init = function init(client, $) { hashauth.requestAuthentication = function requestAuthentication (eventOrNext) { var translate = client.translate; hashauth.injectHtml(); + + var clientWidth = window.innerWidth + || document.documentElement.clientWidth + || document.body.clientWidth; + + clientWidth = Math.min(400, clientWidth); + $( '#requestauthenticationdialog' ).dialog({ - width: 400 + width: clientWidth , height: 270 , closeText: '' , buttons: [ @@ -139,6 +159,8 @@ hashauth.init = function init(client, $) { if (isok) { if (hashauth.storeapisecret) { Storages.localStorage.set('apisecrethash',hashauth.apisecrethash); + // TODO show dialog first, then reload + if (hashauth.tokenauthenticated) client.browserUtils.reload(); } $('#authentication_placeholder').html(hashauth.inlineCode()); if (callback) { @@ -159,9 +181,18 @@ hashauth.init = function init(client, $) { var status = null; - if (client.authorized) { - status = translate('Authorized by token') + ' (' + translate('view without token') + ')' + - '
' + client.authorized.sub + ': ' + client.authorized.permissionGroups.join(', ') + ''; + if (client.authorized || hashauth.tokenauthenticated) { + status = translate('Authorized by token'); + if (client.authorized && client.authorized.sub) { + status += '
' + client.authorized.sub + ': ' + client.authorized.permissionGroups.join(', ') + ''; + } + if (hashauth.apisecrethash) + { + status += '
(' + translate('Remove stored token') + ')'; + } else { + status += '
(' + translate('view without token') + ')'; + } + } else if (hashauth.isAuthenticated()) { console.info('status isAuthenticated', hashauth); status = translate('Admin authorized') + ' (' + translate('Remove') + ')'; @@ -171,13 +202,10 @@ hashauth.init = function init(client, $) { var html = ''+ '
' + status + '
'; @@ -206,7 +234,7 @@ hashauth.init = function init(client, $) { }; hashauth.isAuthenticated = function isAuthenticated() { - return hashauth.authenticated; + return hashauth.authenticated || hashauth.tokenauthenticated; }; hashauth.initialized = true; diff --git a/lib/language.js b/lib/language.js index e2190e355de..86471de1972 100644 --- a/lib/language.js +++ b/lib/language.js @@ -535,7 +535,7 @@ function init() { ,nb: 'Siste 2 dager' ,he: 'יומיים אחרונים' ,pl: 'Ostatnie 2 dni' - ,ru: 'Последние 2 дня' + ,ru: 'Прошедшие 2 дня' ,sk: 'Posledné 2 dni' ,nl: 'Afgelopen 2 dagen' ,ko: '지난 2일' @@ -560,7 +560,7 @@ function init() { ,nb: 'Siste 3 dager' ,he: 'שלושה ימים אחרונים' ,pl: 'Ostatnie 3 dni' - ,ru: 'Последние 3 дня' + ,ru: 'Прошедшие 3 дня' ,sk: 'Posledné 3 dni' ,nl: 'Afgelopen 3 dagen' ,ko: '지난 3일' @@ -585,7 +585,7 @@ function init() { ,nb: 'Siste uke' ,he: 'שבוע אחרון' ,pl: 'Ostatni tydzień' - ,ru: 'Последняя неделя' + ,ru: 'Прошедшая неделя' ,sk: 'Posledný týždeň' ,nl: 'Afgelopen week' ,ko: '지난주' @@ -610,7 +610,7 @@ function init() { ,nb: 'Siste 2 uker' ,he: 'שבועיים אחרונים' ,pl: 'Ostatnie 2 tygodnie' - ,ru: 'Последние 2 недели' + ,ru: 'Прошедшие 2 недели' ,sk: 'Posledné 2 týždne' ,nl: 'Afgelopen 2 weken' ,ko: '지난 2주' @@ -635,7 +635,7 @@ function init() { ,nb: 'Siste måned' ,he: 'חודש אחרון' ,pl: 'Ostatni miesiąc' - ,ru: 'Последний месяц' + ,ru: 'Прошедший месяц' ,sk: 'Posledný mesiac' ,nl: 'Afgelopen maand' ,ko: '지난달' @@ -660,13 +660,88 @@ function init() { ,nb: 'Siste 3 måneder' ,he: 'שלושה חודשים אחרונים' ,pl: 'Ostatnie 3 miesiące' - ,ru: 'Последние 3 месяца' + ,ru: 'Прошедшие 3 месяца' ,sk: 'Posledné 3 mesiace' ,nl: 'Afgelopen 3 maanden' ,ko: '지난 3달' ,tr: 'Son 3 ay' ,zh_cn: '过去3个月' } + , 'between': { + cs: 'between' + ,de: 'between' + ,es: 'between' + ,fr: 'between' + ,el: 'between' + ,pt: 'between' + ,sv: 'between' + ,ro: 'between' + ,bg: 'between' + ,hr: 'between' + ,it: 'between' + ,ja: 'between' + ,dk: 'between' + ,fi: 'between' + ,nb: 'between' + ,he: 'between' + ,pl: 'between' + ,ru: 'между' + ,sk: 'between' + ,nl: 'tussen' + ,ko: 'between' + ,tr: 'between' + ,zh_cn: 'between' + } + , 'around': { + cs: 'around' + ,de: 'around' + ,es: 'around' + ,fr: 'around' + ,el: 'around' + ,pt: 'around' + ,sv: 'around' + ,ro: 'around' + ,bg: 'around' + ,hr: 'around' + ,it: 'around' + ,ja: 'around' + ,dk: 'around' + ,fi: 'around' + ,nb: 'around' + ,he: 'around' + ,pl: 'around' + ,ru: 'около' + ,sk: 'around' + ,nl: 'rond' + ,ko: 'around' + ,tr: 'around' + ,zh_cn: 'around' + } + , 'and': { + cs: 'and' + ,de: 'and' + ,es: 'and' + ,fr: 'and' + ,el: 'and' + ,pt: 'and' + ,sv: 'and' + ,ro: 'and' + ,bg: 'and' + ,hr: 'and' + ,it: 'and' + ,ja: 'and' + ,dk: 'and' + ,fi: 'and' + ,nb: 'and' + ,he: 'and' + ,pl: 'and' + ,ru: 'и' + ,sk: 'and' + ,nl: 'en' + ,ko: 'and' + ,tr: 'and' + ,zh_cn: 'and' + } ,'From' : { cs: 'Od' ,de: 'Von' @@ -819,7 +894,7 @@ function init() { } ,'Notes contain' : { cs: 'Poznámky obsahují' - ,de: 'Erläuterungen' + ,de: 'Notizen enthalten' ,he: 'ההערות מכילות' ,es: 'Contenido de las notas' ,fr: 'Notes contiennent' @@ -919,7 +994,7 @@ function init() { } ,'Display' : { cs: 'Zobraz' - ,de: 'Darstellen' + ,de: 'Anzeigen' ,es: 'Visualizar' ,fr: 'Afficher' ,el: 'Εμφάνιση' @@ -1060,7 +1135,7 @@ function init() { ,nb: 'Vises ikke' ,he: 'לא מוצג' ,pl: 'Nie jest wyświetlany' - ,ru: 'Не отражено' + ,ru: 'Не показано' ,sk: 'Nie je zobrazené' ,nl: 'Niet weergegeven' ,ko: '출력되지 않음' @@ -1144,7 +1219,7 @@ function init() { } ,'Portion' : { cs: 'Porce' - ,de: 'Portion' + ,de: 'Abschnitt' ,es: 'Porción' ,fr: 'Portion' ,el: 'Μερίδα' @@ -1185,7 +1260,7 @@ function init() { ,nb: 'Størrelse' ,he: 'גודל' ,pl: 'Rozmiar' - ,ru: 'Размер' + ,ru: 'Объем' ,sk: 'Veľkosť' ,nl: 'Grootte' ,ko: '크기' @@ -1321,7 +1396,7 @@ function init() { } ,'Week to week' : { cs: 'Week to week' - ,de: 'Week to week' + ,de: 'Woche zu Woche' ,es: 'Week to week' ,fr: 'Week to week' ,el: 'Week to week' @@ -1335,7 +1410,7 @@ function init() { ,fi: 'Week to week' ,nb: 'Week to week' ,he: 'Week to week' - ,pl: 'Week to week' + ,pl: 'Tydzień po tygodniu' ,ru: 'По неделям' ,sk: 'Week to week' ,nl: 'Week to week' @@ -1451,6 +1526,7 @@ function init() { ,fi: 'netIOB tilasto' ,bg: 'netIOB татистика' ,hr: 'netIOB statistika' + , pl: 'Statystyki netIOP' ,ru: 'статистика нетто активн инс netIOB' ,tr: 'netIOB istatistikleri' } @@ -1462,6 +1538,7 @@ function init() { ,bg: 'временните базали трябва да са показани за да се покаже тази това' ,hr: 'temp bazali moraju biti prikazani kako bi se vidio ovaj izvještaj' ,he: 'חובה לאפשר רמה בזלית זמנית כדי לרות דוח זה' + , pl: 'Tymczasowa dawka podstawowa jest wymagana aby wyświetlić ten raport' ,ru: 'для этого отчета требуется прорисовка врем базалов' ,tr: 'Bu raporu görüntülemek için geçici bazal oluşturulmalıdır' } @@ -1567,7 +1644,7 @@ function init() { } ,'Period' : { cs: 'Období' - ,de: 'Periode' + ,de: 'Zeitabschnitt' ,es: 'Periodo' ,fr: 'Période' ,el: 'Περίοδος' @@ -1942,7 +2019,7 @@ function init() { } ,'Total per day' : { cs: 'dní celkem' - ,de: 'Gesamttage' + ,de: 'Gesamt pro Tag' ,es: 'Total de días' ,fr: 'Total journalier' ,el: 'ημέρες συνολικά' @@ -1982,7 +2059,7 @@ function init() { ,nb: 'Generelt' ,he: 'סך הכל' ,pl: 'Ogółem' - ,ru: 'Всего' + ,ru: 'Суммарно' ,sk: 'Súhrn' ,nl: 'Totaal' ,ko: '전체' @@ -2182,7 +2259,7 @@ function init() { ,nb: 'Beregnet HbA1c' ,he: 'משוער A1c' ,pl: 'HbA1c przewidywany' - ,ru: 'Ожидаемый HbA1c' + ,ru: 'Ожидаемый HbA1c*' ,sk: 'Odhadované HbA1C*' ,nl: 'Geschatte HbA1C' ,ko: '예상 당화혈 색소' @@ -2335,7 +2412,7 @@ function init() { ,nb: 'Feil: Database kan ikke leses' ,he: 'שגיאה: לא ניתן לטעון בסיס נתונים' ,pl: 'Błąd, baza danych nie może być załadowana' - ,ru: 'Не удалось загрузить базу данных' + ,ru: 'Ошибка: Не удалось загрузить базу данных' ,sk: 'Chyba pri načítaní databázy' ,nl: 'FOUT: Database niet geladen' ,ko: '에러: 데이터베이스 로드 실패' @@ -2485,7 +2562,7 @@ function init() { ,fi: 'GI' ,nb: 'GI' ,pl: 'IG' - ,ru: 'ГИ' + ,ru: 'гл индекс ГИ' ,sk: 'GI' ,nl: 'Glycemische index ' ,ko: '혈당 지수' @@ -2635,7 +2712,7 @@ function init() { ,fi: 'API-avaimen tulee olla ainakin 12 merkin mittainen' ,nb: 'Din API nøkkel må være minst 12 tegn lang' ,pl: 'Twój poufny klucz API musi zawierać co majmniej 12 znaków' - ,ru: 'Ваш пароль API должен быть не менее 12 знаков' + ,ru: 'Ваш пароль API должен иметь не менее 12 знаков' ,sk: 'Vaše API heslo musí mať najmenej 12 znakov' ,nl: 'Uw API wachtwoord dient tenminste 12 karakters lang te zijn' ,ko: 'API secret는 최소 12자 이상이여야 합니다.' @@ -2763,7 +2840,7 @@ function init() { ,fi: 'Muokkaa ruokia' ,nb: 'Mat editor' ,pl: 'Edytor posiłków' - ,ru: 'Редактор епродуктов' + ,ru: 'Редактор продуктов' ,sk: 'Editor jedál' ,nl: 'Voeding beheer' ,ko: '음식 편집' @@ -2946,57 +3023,15 @@ function init() { ,tr: 'Gizli göster' ,zh_cn: '显示隐藏值' } - ,'Your API secret' : { - cs: 'Vaše API heslo' - ,he: 'הסיסמא הסודית שלך' - ,de: 'Deine API-Prüfsumme' - ,es: 'Su API secreto' - ,fr: 'Votre secret API' - ,el: 'Το συνθηματικό σας' - ,pt: 'Seu segredo de API' - ,sv: 'Din API-nyckel' - ,ro: 'Cheia API' - ,bg: 'Твоята API парола' - ,hr: 'Vaš tajni API' - ,it: 'Il tuo API secreto' - ,ja: 'あなたのAPI Secret' - ,dk: 'Din API-nøgle' - ,fi: 'Sinun API-avaimesi' - ,nb: 'Din API nøkkel' - ,pl: 'Twoje poufne hasło API' - ,ru: 'Ваш пароль API' - ,sk: 'Vaše API heslo' - ,nl: 'Uw API wachtwoord' - ,ko: 'API secret' - ,tr: 'API secret parolanız' - ,zh_cn: 'API密钥' - ,zh_tw: 'API密鑰' - } - ,'Remember the API Secret on this device. (Do not enable this on public computers.)' : { - cs: 'Ulož hash na tomto počítači (používejte pouze na soukromých počítačích)' - ,he: 'אחסן את הסיסמא הסודית שלך על מחשב זה.מומלץ לעשות כן רק אם המחשב בשימושך הפרטי' - ,de: 'Speichere Prüfsumme auf diesem Computer (nur auf privaten Computern verwenden)' - ,es: 'Guardar hash en este ordenador (Usar solo en ordenadores privados)' - ,fr: 'Sauvegarder le hash sur cet ordinateur (privé uniquement)' - ,el: 'Αποθήκευση συνθηματικού σε αυτό τον υπολογιστή (μόνο για υπολογιστές προσωπικής χρήσης)' - ,pt: 'Salvar hash nesse computador (Somente em computadores privados)' - ,ro: 'Salvează cheia pe acest PC (Folosiți doar PC de încredere)' - ,bg: 'Запамети данните на този компютър. ( Използвай само на собствен компютър)' - ,hr: 'Pohrani hash na ovom računalu (Koristiti samo na osobnom računalu)' - ,sv: 'Lagra hashvärde på denna dator (använd endast på privat dator)' - ,it: 'Conservare hash su questo computer (utilizzare solo su computer privati)' - ,ja: 'このコンピューターにハッシュ値を保存する(このコンピューターでのみ使用する)' - ,dk: 'Gemme hash på denne computer (brug kun på privat computer)' - ,fi: 'Tallenna avain tälle tietokoneelle (käytä vain omalla tietokoneellasi)' - ,nb: 'Lagre hash på denne pc (bruk kun på privat pc)' - ,pl: 'Zapisz na tym komputerze (korzystaj tylko na komputerach prywatnych)' - ,ru: 'Сохранить хеш на этом ПК (только для личных компьютеров)' - ,sk: 'Uložiť hash na tomto počítači (Používajte iba na súkromných počítačoch)' - ,nl: 'Sla wachtwoord op. (gebruik dit alleen op prive computers)' - ,ko: '이 컴퓨터에 hash를 저장하세요.(단, 개인 컴퓨터를 사용하세요.)' - ,tr: 'Bu bilgisayarda hash parolasını sakla (Yalnızca özel bilgisayarlarda kullanın)' - ,zh_cn: '在本机存储API密钥\n(请勿在公用电脑上使用本功能)' - ,zh_tw: '在本機存儲API密鑰\n(請勿在公用電腦上使用本功能)' + ,'Your API secret or token' : { + fi: 'API salaisuus tai avain' + , pl: 'Twój hash API lub token' + ,ru: 'Ваш пароль API или код доступа ' + } + ,'Remember this device. (Do not enable this on public computers.)' : { + fi: 'Muista tämä laite (Älä valitse julkisilla tietokoneilla)' + , pl: 'Zapamiętaj to urządzenie (Nie używaj tej opcji korzystając z publicznych komputerów.)' + ,ru: 'Запомнить это устройство (Не применяйте в общем доступе)' } ,'Treatments' : { cs: 'Ošetření' @@ -3117,7 +3152,7 @@ function init() { ,nb: 'Lagt inn av' ,he: 'הוזן על-ידי' ,pl: 'Wprowadzono przez' - ,ru: 'Введено от' + ,ru: 'Внесено через' ,sk: 'Zadal' ,nl: 'Ingevoerd door' ,ko: '입력 내용' @@ -3426,7 +3461,7 @@ function init() { } ,'Add from database' : { cs: 'Přidat z databáze' - ,de: 'Ergänzt aus Datenbank' + ,de: 'Ergänze aus Datenbank' ,es: 'Añadir desde la base de datos' ,fr: 'Ajouter à partir de la base de données' ,el: 'Επιλογή από τη Βάση Δεδομένων' @@ -3467,7 +3502,7 @@ function init() { ,fi: 'Käytä hiilihydraattikorjausta laskennassa' ,nb: 'Bruk karbohydratkorrigering i beregning' ,pl: 'Użyj wartość węglowodanów w obliczeniach korekty' - ,ru: 'Пользуйтесь коррекцией на углеводы при расчете' + ,ru: 'Пользоваться коррекцией на углеводы при расчете' ,sk: 'Použite korekciu na sacharidy' ,nl: 'Gebruik KH correctie in berekening' ,ko: '계산에 보정된 탄수화물을 사용하세요.' @@ -3492,7 +3527,7 @@ function init() { ,fi: 'Käytä aktiivisia hiilihydraatteja laskennassa' ,nb: 'Benytt aktive karbohydrater i beregning' ,pl: 'Użyj COB do obliczenia korekty' - ,ru: 'Учитывайте активные углеводы COB при расчете' + ,ru: 'Учитывать активные углеводы COB при расчете' ,sk: 'Použite korekciu na COB' ,nl: 'Gebruik ingenomen KH in berekening' ,ko: '계산에 보정된 COB를 사용하세요.' @@ -3517,7 +3552,7 @@ function init() { ,fi: 'Käytä aktiviivista insuliinia laskennassa' ,nb: 'Bruk aktivt insulin i beregningen' ,pl: 'Użyj IOB w obliczeniach' - ,ru: 'Учитывайте активный инсулин IOB при расчете' + ,ru: 'Учитывать активный инсулин IOB при расчете' ,sk: 'Použite IOB vo výpočte' ,nl: 'Gebruik IOB in berekening' ,ko: '계산에 IOB를 사용하세요.' @@ -3592,7 +3627,7 @@ function init() { ,fi: 'Syötä insuliinikorjaus' ,nb: 'Task inn insulinkorrigering' ,pl: 'Wprowadź wartość korekty w leczeniu' - ,ru: 'Введите коррекцию инсулина в лечение' + ,ru: 'Внести коррекцию инсулина в лечение' ,sk: 'Zadajte korekciu inzulínu do ošetrenia' ,nl: 'Voer insuline correctie toe aan behandeling' ,ko: '대처를 위해 보정된 인슐린을 입력하세요.' @@ -3702,7 +3737,7 @@ function init() { ,'60 minutes earlier' : { cs: '60 min předem' ,he: 'שישים דקות מוקדם יותר' - ,de: '60 Min. früher' + ,de: '60 Minuten früher' ,es: '60 min antes' ,fr: '60 min plus tôt' ,el: '60 λεπτά πριν' @@ -3727,7 +3762,7 @@ function init() { ,'45 minutes earlier' : { cs: '45 min předem' ,he: 'ארבעים דקות מוקדם יותר' - ,de: '45 Min. früher' + ,de: '45 Minuten früher' ,es: '45 min antes' ,fr: '45 min plus tôt' ,el: '45 λεπτά πριν' @@ -3752,7 +3787,7 @@ function init() { ,'30 minutes earlier' : { cs: '30 min předem' ,he: 'שלושים דקות מוקדם יותר' - ,de: '30 Min früher' + ,de: '30 Minuten früher' ,es: '30 min antes' ,fr: '30 min plus tôt' ,el: '30 λεπτά πριν' @@ -3777,7 +3812,7 @@ function init() { ,'20 minutes earlier' : { cs: '20 min předem' ,he: 'עשרים דקות מוקדם יותר' - ,de: '20 Min. früher' + ,de: '20 Minuten früher' ,es: '20 min antes' ,fr: '20 min plus tôt' ,el: '20 λεπτά πριν' @@ -3802,7 +3837,7 @@ function init() { ,'15 minutes earlier' : { cs: '15 min předem' ,he: 'חמש עשרה דקות מוקדם יותר' - ,de: '15 Min. früher' + ,de: '15 Minuten früher' ,es: '15 min antes' ,fr: '15 min plus tôt' ,el: '15 λεπτά πριν' @@ -3850,7 +3885,7 @@ function init() { } ,'15 minutes later' : { cs: '15 min po' - ,de: '15 Min. später' + ,de: '15 Minuten später' ,es: '15 min más tarde' ,fr: '15 min plus tard' ,el: '15 λεπτά αργότερα' @@ -3875,7 +3910,7 @@ function init() { } ,'20 minutes later' : { cs: '20 min po' - ,de: '20 Min. später' + ,de: '20 Minuten später' ,es: '20 min más tarde' ,fr: '20 min plus tard' ,el: '20 λεπτά αργότερα' @@ -3900,7 +3935,7 @@ function init() { } ,'30 minutes later' : { cs: '30 min po' - ,de: '30 Min. später' + ,de: '30 Minuten später' ,es: '30 min más tarde' ,fr: '30 min plus tard' ,el: '30 λεπτά αργότερα' @@ -3925,7 +3960,7 @@ function init() { } ,'45 minutes later' : { cs: '45 min po' - ,de: '45 Min. später' + ,de: '45 Minuten später' ,es: '45 min más tarde' ,fr: '45 min plus tard' ,el: '45 λεπτά αργότερα' @@ -3950,7 +3985,7 @@ function init() { } ,'60 minutes later' : { cs: '60 min po' - ,de: '60 Min. später' + ,de: '60 Minuten später' ,es: '60 min más tarde' ,fr: '60 min plus tard' ,el: '60 λεπτά αργότερα' @@ -4001,7 +4036,7 @@ function init() { ,'RETRO MODE' : { cs: 'V MINULOSTI' ,he: 'מצב רטרו' - ,de: 'RETRO MODUS' + ,de: 'Retro-Modus' ,es: 'Modo Retrospectivo' ,fr: 'MODE RETROSPECTIF' ,el: 'Αναδρομική Λειτουργία' @@ -4168,7 +4203,7 @@ function init() { ,fi: 'Lisää ruoka tietokannasta' ,nb: 'Legg til mat fra din database' ,pl: 'Dodaj posiłek z twojej bazy danych' - ,ru: 'Добавьте продукт из вашей базы данных' + ,ru: 'Добавить продукт из вашей базы данных' ,sk: 'Pridať jedlo z Vašej databázy' ,nl: 'Voeg voeding toe uit uw database' ,ko: '데이터베이스에서 음식을 추가하세요.' @@ -4193,7 +4228,7 @@ function init() { ,fi: 'Lataa tietokanta uudelleen' ,nb: 'Last inn databasen på nytt' ,pl: 'Odśwież bazę danych' - ,ru: 'Перезагрузите базу данных' + ,ru: 'Перезагрузить базу данных' ,sk: 'Obnoviť databázu' ,nl: 'Database opnieuw laden' ,ko: '데이터베이스 재로드' @@ -4218,7 +4253,7 @@ function init() { ,fi: 'Lisää' ,nb: 'Legg til' ,pl: 'Dodaj' - ,ru: 'Добавьте' + ,ru: 'Добавить' ,sk: 'Pridať' ,nl: 'Toevoegen' ,ko: '추가' @@ -4733,7 +4768,7 @@ function init() { ,sv: 'Kanylålder (CAGE)' ,pl: 'Czas wkłucia (CAGE)' ,pt: 'Idade da Cânula (ICAT)' - ,ru: 'Канюля отработала' + ,ru: 'Катетер проработал' ,sk: 'Zavedenie kanyly (CAGE)' ,nl: 'Canule leeftijd (CAGE)' ,ko: '캐뉼라 사용기간' @@ -4871,135 +4906,6 @@ function init() { ,zh_cn: '静音2小时' ,zh_tw: '靜音2小時' } - ,'2HR' : { - cs: '2hod' - ,de: '2h' - ,es: '2h' - ,fr: '2hr' - ,el: '2 ώρες' - ,pt: '2h' - ,sv: '2tim' - ,ro: '2h' - ,bg: '2часа' - ,hr: '2h' - ,it: '2ORE' - ,ja: '2時間' - ,dk: '2t' - ,fi: '2h' - ,nb: '2t' - ,pl: '2h' - ,ru: '2ч' - ,sk: '2 hod' - ,nl: '2uur' - ,ko: '2시간' - ,tr: '2sa.' - ,zh_cn: '2小时' - ,zh_tw: '2小時' - } - ,'3HR' : { - cs: '3hod' - ,he: 'שלוש שעות' - ,de: '3h' - ,es: '3h' - ,fr: '3hr' - ,el: '3 ώρες' - ,pt: '3h' - ,sv: '3tim' - ,ro: '3h' - ,bg: '3часа' - ,hr: '3h' - ,it: '3ORE' - ,ja: '3時間' - ,dk: '3t' - ,fi: '3h' - ,nb: '3t' - ,pl: '3h' - ,ru: '3ч' - ,sk: '3 hod' - ,nl: '3uur' - ,ko: '3시간' - ,tr: '3sa.' - ,zh_cn: '3小时' - ,zh_tw: '3小時' - } - ,'6HR' : { - cs: '6hod' - ,he: 'שש שעות' - ,de: '6h' - ,es: '6h' - ,fr: '6hr' - ,el: '6 ώρες' - ,pt: '6h' - ,sv: '6tim' - ,ro: '6h' - ,bg: '6часа' - ,hr: '6h' - ,it: '6ORE' - ,ja: '6時間' - ,dk: '6t' - ,fi: '6h' - ,nb: '6t' - ,pl: '6h' - ,ru: '6ч' - ,sk: '6 hod' - ,nl: '6uur' - ,ko: '6시간' - ,tr: '6sa.' - ,zh_cn: '6小时' - ,zh_tw: '6小時' - } - ,'12HR' : { - cs: '12hod' - ,he: 'שתים עשרה שעות' - ,de: '12h' - ,es: '12h' - ,fr: '12hr' - ,el: '12 ώρες' - ,pt: '12h' - ,sv: '12t' - ,ro: '12h' - ,bg: '12часа' - ,hr: '12h' - ,it: '12ORE' - ,ja: '12時間' - ,dk: '12t' - ,fi: '12h' - ,nb: '12t' - ,pl: '12h' - ,ru: '12ч' - ,sk: '12 hod' - ,nl: '12uur' - ,ko: '12시간' - ,tr: '12sa.' - ,zh_cn: '12小时' - ,zh_tw: '12小時' - } - ,'24HR' : { - cs: '24hod' - ,he: 'עשרים וארבע שעות' - ,de: '24h' - ,es: '24h' - ,fr: '24hr' - ,el: '24 ώρες' - ,pt: '24h' - ,sv: '24tim' - ,ro: '24h' - ,bg: '24часа' - ,hr: '24h' - ,it: '24ORE' - ,ja: '24時間' - ,dk: '24t' - ,fi: '24h' - ,nb: '24t' - ,pl: '24h' - ,ru: '24ч' - ,sk: '24 hod' - ,nl: '24uur' - ,ko: '24시간' - ,tr: '24sa.' - ,zh_cn: '24小时' - ,zh_tw: '24小時' - } ,'Settings' : { cs: 'Nastavení' ,he: 'הגדרות' @@ -5147,7 +5053,7 @@ function init() { ,nb: 'Logg en hendelse' ,he: 'הזן רשומה' ,pl: 'Wprowadź leczenie' - ,ru: 'Журнал лечения' + ,ru: 'Записать лечение' ,sk: 'Záznam ošetrenia' ,nl: 'Registreer een behandeling' ,ko: 'Treatment 로그' @@ -5156,7 +5062,7 @@ function init() { } ,'BG Check' : { cs: 'Kontrola glykémie' - ,de: 'BG-Prüfung' + ,de: 'BG-Messung' ,es: 'Control de glucemia' ,fr: 'Contrôle glycémie' ,el: 'Έλεγχος Γλυκόζης' @@ -5372,7 +5278,7 @@ function init() { ,nb: 'Pumpebytte' ,he: 'החלפת צינורית משאבה' ,pl: 'Zmiana miejsca wkłucia pompy' - ,ru: 'Смена места катетора помпы' + ,ru: 'Смена катетера помпы' ,sk: 'Výmena setu' ,nl: 'Nieuwe pomp infuus' ,ko: '펌프 위치 변경' @@ -5422,7 +5328,7 @@ function init() { ,nb: 'CGM Sensor Stop' ,he: 'CGM Sensor Stop' ,pl: 'CGM Sensor Stop' - ,ru: 'Остановка сенсора' + ,ru: 'Стоп сенсор' ,sk: 'CGM Sensor Stop' ,nl: 'CGM Sensor Stop' ,ko: 'CGM Sensor Stop' @@ -5472,7 +5378,7 @@ function init() { ,nb: 'Dexcom sensor start' ,he: 'אתחול חיישן סוכר של דקסקום' ,pl: 'Start sensora DEXCOM' - ,ru: 'Старт сенсора Декском' + ,ru: 'Старт сенсора' ,sk: 'Spustenie senzoru DEXCOM' ,nl: 'Dexcom sensor start' ,ko: 'Dexcom 센서 시작' @@ -5646,7 +5552,7 @@ function init() { ,nb: 'Insulin' ,he: 'אינסולין שניתן' ,pl: 'Podana insulina' - ,ru: 'Введенный инсулин' + ,ru: 'Введен инсулин' ,sk: 'Podaný inzulín' ,nl: 'Toegediende insuline' ,ko: '인슐린 요구량' @@ -5705,7 +5611,7 @@ function init() { } ,'View all treatments' : { cs: 'Zobraz všechny ošetření' - ,de: 'Zeige alle Eingaben' + ,de: 'Zeige alle Behandlungen' ,es: 'Visualizar todos los tratamientos' ,fr: 'Voir tous les traitements' ,el: 'Προβολή όλων των ενεργειών' @@ -5811,7 +5717,7 @@ function init() { ,nb: 'Når aktivert er alarmer aktive' ,he: 'כשמופעל התראות יכולות להישמע.' ,pl: 'Sygnalizacja dzwiękowa przy włączonym alarmie' - ,ru: 'При активации сигналы слышны' + ,ru: 'При активации может звучать сигнал' ,sk: 'Pri aktivovanom alarme znie zvuk ' ,nl: 'Als ingeschakeld kan alarm klinken' ,ko: '알림을 활성화 하면 알람이 울립니다.' @@ -5978,7 +5884,7 @@ function init() { ,'mins' : { cs: 'min' ,he: 'דקות' - ,de: 'min' + ,de: 'Minuten' ,es: 'min' ,fr: 'mins' ,el: 'λεπτά' @@ -6096,8 +6002,8 @@ function init() { ,dk: 'Vis rå BS data' ,fi: 'Näytä raaka VS tieto' ,nb: 'Vis rådata' - ,pl: 'Wyświetl surowe dane RAW' - ,ru: 'Показывать необработанные RAW данные' + ,pl: 'Wyświetl surowe dane BG' + ,ru: 'Показывать необработанные данные RAW' ,sk: 'Zobraziť RAW dáta' ,nl: 'Laat ruwe data zien' ,ko: 'Raw 혈당 데이터 보기' @@ -6238,7 +6144,7 @@ function init() { ,'Theme' : { cs: 'Téma' ,he: 'נושא' - ,de: 'Thema' + ,de: 'Aussehen' ,es: 'Tema' ,fr: 'Thème' ,el: 'Θέμα απεικόνισης' @@ -6564,9 +6470,9 @@ function init() { ,fi: 'minuutti sitten' ,nb: 'minutter siden' ,pl: 'minuta temu' - ,ru: 'мин. назад' + ,ru: 'мин назад' ,sk: 'min. pred' - ,nl: 'm geleden' + ,nl: 'minuut geleden' ,ko: '분 전' ,tr: 'dk. önce' ,zh_cn: '分钟前' @@ -6592,7 +6498,7 @@ function init() { ,pl: 'minut temu' ,ru: 'минут назад' ,sk: 'min. pred' - ,nl: 'm geleden' + ,nl: 'minuten geleden' ,ko: '분 전' ,tr: 'dakika önce' ,zh_cn: '分钟前' @@ -6679,7 +6585,7 @@ function init() { ,'Clean' : { cs: 'Čistý' ,he: 'נקה' - ,de: 'Rein' + ,de: 'Löschen' ,es: 'Limpio' ,fr: 'Propre' ,el: 'Καθαρισμός' @@ -6778,10 +6684,11 @@ function init() { ,tr: 'Ağır' ,zh_cn: '重度' ,zh_tw: '嚴重' + ,he: 'כבד' } ,'Treatment type' : { cs: 'Typ ošetření' - ,de: 'Eingabe-Typ' + ,de: 'Behandlungstyp' ,es: 'Tipo de tratamiento' ,fr: 'Type de traitement' ,el: 'Τύπος Ενέργειας' @@ -6802,6 +6709,7 @@ function init() { ,ko: 'Treatment 타입' ,tr: 'Tedavi tipi' ,zh_cn: '操作类型' + ,he: 'סוג הטיפול' } ,'Raw BG' : { cs: 'Glykémie z RAW dat' @@ -6849,6 +6757,7 @@ function init() { ,ko: '기기' ,tr: 'Cihaz' ,zh_cn: '设备' + ,he: 'התקן' } ,'Noise' : { cs: 'Šum' @@ -7463,7 +7372,7 @@ function init() { ,fi: 'Etsi ja poista tapahtumat' ,pl: 'Znajdź i usuń wpisy z przyszłości' ,pt: 'Encontrar e remover entradas futuras' - ,ru: 'Найти и удалить данные с сенсора из будущего' + ,ru: 'Найти и удалить данные сенсора из будущего' ,sk: 'Nájsť a odstrániť CGM dáta v budúcnosti' ,nl: 'Zoek en verwijder behandelingen met datum in de toekomst' ,ko: '미래에 입력을 검색하고 지우세요.' @@ -7488,7 +7397,7 @@ function init() { ,fi: 'Tämä työkalu etsii ja poistaa sensorimerkinnät joiden aikamerkintä sijaitsee tulevaisuudessa.' ,pl: 'To narzędzie odnajduje i usuwa dane CGM utworzone przez uploader w przyszłości - ze złą datą/czasem.' ,pt: 'Este comando procura e remove dados de sensor futuros criados por um uploader com data ou horário errados.' - ,ru: 'Эта опция найдет и удалит данные с сенсора созданные загрузчиком с неверными датой/временем' + ,ru: 'Эта опция найдет и удалит данные сенсора созданные загрузчиком с неверными датой/временем' ,sk: 'Táto úloha nájde a odstráni CGM dáta v budúcnosti vzniknuté zle nastaveným časom uploaderu.' ,nl: 'Dit commando zoekt en verwijdert behandelingen met datum in de toekomst' ,ko: '이 작업은 잘못된 날짜/시간으로 업로드 되어 생성된 미래의 CGM 데이터를 검색하고 지우는 것입니다.' @@ -7698,6 +7607,9 @@ function init() { } ,'%1 records deleted' : { hr: 'obrisano %1 zapisa' + ,de: '%1 Einträge gelöscht' + , pl: '%1 rekordów zostało usuniętych' + ,ru: '% записей удалено' } ,'Clean Mongo status database' : { cs: 'Vyčištění Mongo databáze statusů' @@ -7761,7 +7673,7 @@ function init() { ,hr: 'Ovo briše sve zapise o statusima. Korisno kada se status baterije uploadera ne osvježava ispravno.' ,it: 'Questa attività elimina tutti i documenti dalla collezione "devicestatus". Utile quando lo stato della batteria uploader/xdrip non si aggiorna.' ,fi: 'Tämä työkalu poistaa kaikki tiedot statustietokannasta, mikä korjaa tilanteen, jossa puhelimen akun lataustilanne ei näy oikein.' - ,pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji devicestatus. Potrzebne jest wtedy, gdy status baterii uploadera nie jest aktualizowany' + ,pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji devicestatus. Przydatne, gdy status baterii uploadera nie jest aktualizowany.' ,pt: 'Este comando remove todos os documentos da coleção devicestatus. Útil quando o status da bateria do uploader não é atualizado corretamente.' ,ru: 'Эта опция удаляет все документы из коллекции статус устройства. Полезно когда состояние батвреи загрузчика не обновляется' ,sk: 'Táto úloha vymaže všetky záznamy z kolekcie "devicestatus". Je to vhodné keď sa stav batérie nezobrazuje správne.' @@ -7787,7 +7699,7 @@ function init() { ,fi: 'Poista kaikki tiedot' ,pl: 'Usuń wszystkie dokumenty' ,pt: 'Apagar todos os documentos' - ,ru: 'Стереть все документы' + ,ru: 'Удалить все документы' ,sk: 'Zmazať všetky záznamy' ,nl: 'Verwijder alle documenten' ,ko: '모든 문서들을 지우세요' @@ -7811,7 +7723,7 @@ function init() { ,fi: 'Poista tiedot statustietokannasta?' ,pl: 'Czy na pewno usunąć wszystkie dokumenty z kolekcji devicestatus?' ,pt: 'Apagar todos os documentos da coleção devicestatus?' - ,ru: 'Стереть все документы коллекции статус устройства?' + ,ru: 'Удалить все документы коллекции статус устройства?' ,sk: 'Zmazať všetky záznamy z kolekcie "devicestatus"?' ,ko: 'devicestatus 수집의 모든 문서들을 지우세요.' ,tr: 'Tüm Devicestatus koleksiyon belgeleri silinsin mi?' @@ -7868,53 +7780,94 @@ function init() { } ,'Delete all documents from devicestatus collection older than 30 days' : { hr: 'Obriši sve statuse starije od 30 dana' - ,ru: 'Удалить все записи коллекции devicestatus' + ,ru: 'Удалить все записи коллекции devicestatus старше 30 дней' + ,de: 'Alle Dokumente der Gerätestatus-Sammlung löschen, die älter als 30 Tage sind' + , pl: 'Usuń wszystkie dokumenty z kolekcji devicestatus starsze niż 30 dni' } ,'Number of Days to Keep:' : { hr: 'Broj dana za sačuvati:' ,ru: 'Оставить дней' + ,de: 'Daten löschen, die älter sind (in Tagen) als:' + , pl: 'Ilość dni do zachowania:' } ,'This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo uklanja sve statuse starije od 30 dana. Korisno kada se status baterije uploadera ne osvježava ispravno.' + , pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji devicestatus starsze niż 30 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.' ,ru: 'Это удалит все документы коллекции devicestatus которым более 30 дней. Полезно, когда статус батареи не обновляется или обновляется неверно.' + ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Gerätestatus-Sammlung, die älter sind als 30 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' } ,'Delete old documents from devicestatus collection?' : { hr: 'Obriši stare statuse' - + ,de: 'Alte Dokumente aus der Gerätestatus-Sammlung entfernen?' + , pl: 'Czy na pewno chcesz usunąć stare dokumenty z kolekcji devicestatus?' + ,ru: 'Удалить старыые документы коллекции devicestatus' } ,'Clean Mongo entries (glucose entries) database' : { hr: 'Obriši GUK zapise iz baze' + ,de: 'Mongo-Einträge (Glukose-Einträge) Datenbank bereinigen' + , pl: 'Wyczyść bazę wpisów (wpisy glukozy) Mongo' + ,ru: 'Очистить записи данных в базе Mongo' } ,'Delete all documents from entries collection older than 180 days' : { hr: 'Obriši sve zapise starije od 180 dana' + ,de: 'Alle Dokumente aus der Einträge-Sammlung löschen, die älter sind als 180 Tage' + , pl: 'Usuń wszystkie dokumenty z kolekcji wpisów starsze niż 180 dni' + ,ru: 'Удалить все документы коллекции entries старше 180 дней ' } ,'This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo briše sve zapise starije od 180 dana. Korisno kada se status baterije uploadera ne osvježava.' + ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Einträge-Sammlung, die älter sind als 180 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' + , pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji wpisów starsze niż 180 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.' + ,ru: 'Это удалит все документы коллекции entries старше 180 дней. Полезно, когда статус батареи загрузчика должным образом не обновляется' } ,'Delete old documents' : { hr: 'Obriši stare zapise' + ,de: 'Alte Dokumente löschen' + , pl: 'Usuń stare dokumenty' + ,ru: 'Удалить старые документы' } ,'Delete old documents from entries collection?' : { hr: 'Obriši stare zapise?' + ,de: 'Alte Dokumente aus der Einträge-Sammlung entfernen?' + , pl: 'Czy na pewno chcesz usunąć stare dokumenty z kolekcji wpisów?' + ,ru: 'Удалить старые документы коллекции entries?' } ,'%1 is not a valid number' : { hr: '%1 nije valjan broj' + ,de: '%1 ist keine gültige Zahl' + ,he: 'זה לא מיספר %1' + , pl: '%1 nie jest poprawną liczbą' + ,ru: '% не является допустимым значением' } ,'%1 is not a valid number - must be more than 2' : { hr: '%1 nije valjan broj - mora biti veći od 2' + ,de: '%1 ist keine gültige Zahl - Eingabe muss größer als 2 sein' + , pl: '%1 nie jest poprawną liczbą - musi być większe od 2' + ,ru: '% не является допустимым значением - должно быть больше 2' } ,'Clean Mongo treatments database' : { hr: 'Obriši tretmane iz baze' + ,de: 'Mongo-Behandlungsdatenbank bereinigen' + , pl: 'Wyczyść bazę leczenia Mongo' + ,ru: 'Очистить базу лечения Mongo' } ,'Delete all documents from treatments collection older than 180 days' : { hr: 'Obriši tretmane starije od 180 dana iz baze' + ,de: 'Alle Dokumente aus der Behandlungs-Sammlung löschen, die älter sind als 180 Tage' + , pl: 'Usuń wszystkie dokumenty z kolekcji leczenia starsze niż 180 dni' + ,ru: 'Удалить все документы коллекции treatments старше 180 дней' } ,'This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo briše sve tretmane starije od 180 dana iz baze. Korisno kada se status baterije uploadera ne osvježava.' + ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Behandlungs-Sammlung, die älter sind als 180 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' + , pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji leczenia starsze niż 180 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.' + ,ru: 'Это удалит все документы коллекции treatments старше 180 дней. Полезно, когда статус батареи загрузчика не обновляется должным образом' } ,'Delete old documents from treatments collection?' : { hr: 'Obriši stare tretmane?' - ,ru: 'Удалить старые документы из коллекции лечения?' + ,ru: 'Удалить старые документы из коллекции treatments?' + ,de: 'Alte Dokumente aus der Behandlungs-Sammlung entfernen?' + , pl: 'Czy na pewno chcesz usunąć stare dokumenty z kolekcji leczenia?' } ,'Admin Tools' : { cs: 'Nástroje pro správu' @@ -8195,7 +8148,7 @@ function init() { ,fi: 'Basaalin määrä' ,pl: 'Dawka podstawowa' ,pt: 'Valor da basal' - ,ru: 'величина временного базалал' + ,ru: 'величина временного базала' ,sk: 'Hodnota bazálu' ,nl: 'Basaal snelheid' ,ko: 'Basal' @@ -8384,7 +8337,7 @@ function init() { ,hr: 'Naslov' ,pl: 'Nazwa' ,pt: 'Título' - ,ru: 'Наименование' + ,ru: 'Название' ,sk: 'Názov' ,nl: 'Titel' ,ko: '제목' @@ -8530,7 +8483,7 @@ function init() { ,hr: 'Pohranjeni profili' ,pl: 'Zachowane profile' ,pt: 'Perfis guardados' - ,ru: 'Запомненные профили' + ,ru: 'Сохраненные профили' ,sk: 'Uložené profily' ,nl: 'Opgeslagen profielen' ,ko: '저장된 프로파일' @@ -8602,7 +8555,7 @@ function init() { ,hr: 'Predstavlja uobičajeno trajanje djelovanje inzulina. Varira po vrstama inzulina i osobama. Tipično je to 3-4 sata za inzuline u pumpama za većinu osoba. Ponekad se naziva i vijek inzulina' ,pl: 'Odzwierciedla czas działania insuliny. Może różnić się w zależności od chorego i rodzaju insuliny. Zwykle są to 3-4 godziny dla insuliny podawanej pompą u większości chorych. Inna nazwa to czas trwania insuliny.' ,pt: 'Representa a tempo típico durante o qual a insulina tem efeito. Varia de acordo com o paciente e tipo de insulina. Tipicamente 3-4 horas para a maioria das insulinas usadas em bombas e dos pacientes. Algumas vezes chamada de tempo de vida da insulina' - ,ru: 'Представляет типичную продолжительность действия инсулина. Зависит от пациента и от типа инсулина. Обычно 3-4 часа для большинства помповых инсулинов и большинства пациентов' + ,ru: 'Отражает типичную продолжительность действия инсулина. Зависит от пациента и от типа инсулина. Обычно 3-4 часа для большинства помповых инсулинов и большинства пациентов' ,sk: 'Predstavuje typickú dobu počas ktorej inzulín pôsobí. Býva rôzna od pacienta a od typu inzulínu. Zvyčajne sa pohybuje medzi 3-4 hodinami u pacienta s pumpou.' ,nl: 'Geeft de werkingsduur van de insuline in het lichaam aan. Dit verschilt van patient tot patient er per soort insuline, algemeen gemiddelde is 3 tot 4 uur. ' ,ko: '인슐린이 작용하는 지속시간을 나타냅니다. 사람마다 그리고 인슐린 종류에 따라 다르고 일반적으로 3~4시간간 동안 지속되며 인슐린 작용 시간(Insulin lifetime)이라고 불리기도 합니다.' @@ -8634,6 +8587,30 @@ function init() { ,tr: 'İnsülin/Karbonhidrat oranı (I:C)' ,zh_cn: '碳水化合物系数(ICR)' } + ,'Hours:' : { + cs: 'Hodin:' + ,he: 'שעות:' + ,ro: 'Ore:' + ,el: 'ώρες:' + ,fr: 'Heures:' + ,de: 'Stunden:' + ,es: 'Horas:' + ,dk: 'Timer:' + ,sv: 'Timmar:' + ,nb: 'Timer:' + ,fi: 'Tunnit:' + ,bg: 'часове:' + ,hr: 'Sati:' + ,pl: 'Godziny:' + ,pt: 'Horas:' + ,ru: 'час:' + ,sk: 'Hodiny:' + ,nl: 'Uren:' + ,ko: '시간:' + ,it: 'Ore:' + ,tr: 'Saat:' + ,zh_cn: '小时:' + } ,'hours' : { cs: 'hodin' ,he: 'שעות ' @@ -8817,7 +8794,7 @@ function init() { ,hr: 'Bazali [jedinica/sat]' ,pl: 'Dawka podstawowa [j/h]' ,pt: 'Taxas de basal [unidades/hora]' - ,ru: 'Базал ед/час' + ,ru: 'Базал [unit/hour]' ,sk: 'Bazál [U/hod]' ,nl: 'Basaal snelheid [eenheden/uur]' ,ko: 'Basal 비율[unit/hour]' @@ -8914,7 +8891,7 @@ function init() { ,hr: 'Iscrtaj bazale' ,pl: 'Zmiana dawki bazowej' ,pt: 'Renderizar basal' - ,ru: 'показывать базал' + ,ru: 'Отображать базал' ,sk: 'Zobrazenie bazálu' ,nl: 'Toon basaal' ,ko: 'Basal 사용하기' @@ -8962,7 +8939,7 @@ function init() { ,hr: 'Izračun je u ciljanom rasponu.' ,pl: 'Obliczenie mieści się w zakresie docelowym' ,pt: 'O cálculo está dentro da meta' - ,ru: 'Расчет в целевых пределах ' + ,ru: 'Расчет в целевом диапазоне' ,sk: 'Výpočet je v cieľovom rozsahu.' ,nl: 'Berekening valt binnen doelwaards' ,ko: '계산은 목표 범위 안에 있습니다.' @@ -9082,7 +9059,7 @@ function init() { ,hr: 'Vremenski rasponi donje ciljane i gornje ciljane vrijednosti nisu ispravni. Vrijednosti vraćene na zadano.' ,pl: 'Zakres czasu w docelowo niskim i wysokim przedziale nie są dopasowane. Przywrócono wartości domyślne' ,pt: 'Os intervalos de tempo da meta inferior e da meta superior não conferem. Os valores padrão serão restaurados.' - ,ru: 'Диапазон времени нижних и верхних целевых значений не совпадают. Восстановлены значения по умолчанию' + ,ru: 'Диапазоны времени нижних и верхних целевых значений не совпадают. Восстановлены значения по умолчанию' ,sk: 'Časové rozsahy pre cieľové glykémie sa nezhodujú. Hodnoty nastavené na východzie.' ,ko: '설정한 저혈당과 고혈당의 시간 범위와 일치하지 않습니다. 값은 초기 설정값으로 다시 저장 될 것입니다.' ,it: 'Intervalli di tempo della glicemia obiettivo inferiore e superiore non corretti. Valori ripristinati a quelli standard.' @@ -9106,7 +9083,7 @@ function init() { ,hr: 'Vrijedi od:' ,pl: 'Ważne od:' ,pt: 'Válido desde:' - ,ru: 'Действует с' + ,ru: 'Действительно с' ,sk: 'Platné od:' ,nl: 'Geldig van:' ,ko: '유효' @@ -9192,7 +9169,7 @@ function init() { ,nb: 'IKH' ,fr: 'I:C' ,ro: 'ICR' - ,de: 'I:KH' + ,de: 'IE:KH' ,dk: 'I:C' ,es: 'I:C' ,sv: 'I:C' @@ -9225,7 +9202,7 @@ function init() { ,pl: 'ISF' ,pt: 'ISF' ,nl: 'ISF' - ,ru: 'Чувствительность к инсулину ISF' + ,ru: 'Чувств к инс ISF' ,sk: 'ISF' ,ko: 'ISF' ,it: 'ISF' @@ -9248,7 +9225,7 @@ function init() { ,hr: 'Dual bolus' ,fi: 'Yhdistelmäbolus' ,pt: 'Bolus duplo' - ,ru: 'Комбинированный болюс' + ,ru: 'Комбинир болюс' ,sk: 'Kombinovaný bolus' ,nl: 'Pizza Bolus' ,ko: 'Combo Bolus' @@ -9344,7 +9321,7 @@ function init() { ,bg: 'Когато е активно ,иконката за редактиране ще се вижда' ,hr: 'Kada je omogućeno, mod uređivanje je omogućen' ,fi: 'Muokkausmoodin ikoni tulee näkyviin kun laitat tämän päälle' - ,ru: 'При активации видна икона начать режим редактирования' + ,ru: 'При активации видна пиктограмма начать режим редактирования' ,sk: 'Keď je povolený, je zobrazená ikona editačného módu' ,pl: 'Po aktywacji, widoczne ikony, aby uruchomić tryb edycji' ,pt: 'Quando ativado, o ícone iniciar modo de edição estará visível' @@ -9538,7 +9515,7 @@ function init() { ,bg: 'Да променя ли времето на събитието с %1?' ,hr: 'Promijeni vrijeme tretmana na %1?' ,fi: 'Muuta hoidon aika? Uusi: %1' - ,ru: 'Изменить время события на %1?' + ,ru: 'Изменить время события на %1 ?' ,sk: 'Zmeniť čas ošetrenia na %1 ?' ,pl: 'Zmień czas zdarzenia na %1 ?' ,pt: 'Alterar horário do tratamento para %1 ?' @@ -9562,7 +9539,7 @@ function init() { ,bg: 'Да променя ли времето на ВХ с %1?' ,hr: 'Promijeni vrijeme UGH na %1?' ,fi: 'Muuta hiilihydraattien aika? Uusi: %1' - ,ru: 'Изменить время подачи углеводов на %?' + ,ru: 'Изменить время приема углеводов на % ?' ,sk: 'Zmeniť čas sacharidov na %1 ?' ,pl: 'Zmień czas węglowodanów na %1 ?' ,pt: 'Alterar horário do carboidrato para %1 ?' @@ -9586,7 +9563,7 @@ function init() { ,bg: 'Да променя ли времето на инсулина с %1?' ,hr: 'Promijeni vrijeme inzulina na %1?' ,fi: 'Muuta insuliinin aika? Uusi: %1' - ,ru: 'Изменить время подачи инсулина на %?' + ,ru: 'Изменить время подачи инсулина на % ?' ,sk: 'Zmeniť čas inzulínu na %1 ?' ,pl: 'Zmień czas insuliny na %1 ?' ,pt: 'Alterar horário da insulina para %1 ?' @@ -9610,7 +9587,7 @@ function init() { ,bg: 'Изтрий събитието' ,hr: 'Obriši tretman?' ,fi: 'Poista hoito?' - ,ru: 'Удалить событие?' + ,ru: 'Удалить событие ?' ,sk: 'Odstrániť ošetrenie?' ,pl: 'Usunąć wydarzenie?' ,pt: 'Remover tratamento?' @@ -9634,7 +9611,7 @@ function init() { ,bg: 'Да изтрия ли инсулина от събитието?' ,hr: 'Obriši inzulin iz tretmana?' ,fi: 'Poista insuliini hoidosta?' - ,ru: 'Удалить инсулин из событий?' + ,ru: 'Удалить инсулин из событий ?' ,sk: 'Odstrániť inzulín z ošetrenia?' ,pl: 'Usunąć insulinę z wydarzenia?' ,pt: 'Remover insulina do tratamento?' @@ -9658,7 +9635,7 @@ function init() { ,bg: 'Да изтрия ли ВХ от събитието?' ,hr: 'Obriši UGH iz tretmana?' ,fi: 'Poista hiilihydraatit hoidosta?' - ,ru: 'Удалить углеводы из событий?' + ,ru: 'Удалить углеводы из событий ?' ,sk: 'Odstrániť sacharidy z ošetrenia?' ,pl: 'Usunąć węglowodany z wydarzenia?' ,pt: 'Remover carboidratos do tratamento?' @@ -9754,7 +9731,7 @@ function init() { ,sv: 'Fel profilinställning.\nIngen profil vald för vald tid.\nOmdirigerar för att skapa ny profil.' ,nb: 'Feil profilinstilling.\nIngen profil valgt for valgt tid.\nVideresender for å lage ny profil.' ,fi: 'Väärä profiiliasetus tai profiilia ei löydy.\nSiirrytään profiilin muokkaamiseen uuden profiilin luontia varten.' - ,ru: 'Неверные настройки профиля. Для отображаемого времени не определен профиль. Переход к редактору профиля для создания нового' + ,ru: 'Переход к редактору профиля для создания нового' ,sk: 'Zle nastavený profil.\nK zobrazenému času nieje definovaný žiadny profil.\nPresmerovávam na vytvorenie profilu.' ,pl: 'Złe ustawienia profilu.\nDla podanego czasu nie zdefiniowano profilu.\nPrzekierowuję do edytora profili aby utworzyć nowy.' ,pt: 'Configuração de perfil incorreta. \nNão há perfil definido para mostrar o horário. \nRedirecionando para o editor de perfil para criar um perfil novo.' @@ -9782,11 +9759,11 @@ function init() { ,nl: 'Pomp' ,ko: '펌프' ,fi: 'Pumppu' + , pl: 'Pompa' ,pt: 'Bomba' ,it: 'Pompa' ,tr: 'Pompa' ,zh_cn: '胰岛素泵' - ,pl: 'Pompa' } ,'Sensor Age' : { cs: 'Stáří senzoru (SAGE)' @@ -9802,16 +9779,16 @@ function init() { ,bg: 'Възраст на сензора (ВС)' ,hr: 'Starost senzora' ,ro: 'Vechimea senzorului' - ,ru: 'Сенсор проработал' + ,ru: 'Сенсор работает' ,nl: 'Sensor leeftijd' ,ko: '센서 사용 기간' ,fi: 'Sensorin ikä' + , pl: 'Wiek sensora' ,pt: 'Idade do sensor' ,it: 'SAGE - Durata Sensore' ,tr: '(SAGE) Sensör yaşı ' ,zh_cn: '探头使用时间(SAGE)' ,zh_tw: '探頭使用時間(SAGE)' - ,pl: 'Wiek sensora' } ,'Insulin Age' : { cs: 'Stáří inzulínu (IAGE)' @@ -9827,16 +9804,16 @@ function init() { ,bg: 'Възраст на инсулина (ВИ)' ,hr: 'Starost inzulina' ,ro: 'Vechimea insulinei' - ,ru: 'инсулин проработал' + ,ru: 'Инсулин работает' ,ko: '인슐린 사용 기간' ,fi: 'Insuliinin ikä' + , pl: 'Wiek insuliny' ,pt: 'Idade da insulina' ,it: 'IAGE - Durata Insulina' ,nl: 'Insuline ouderdom (IAGE)' ,tr: '(IAGE) İnsülin yaşı' ,zh_cn: '胰岛素使用时间(IAGE)' ,zh_tw: '胰島素使用時間(IAGE)' - ,pj: 'Wiek insuliny' } ,'Temporary target' : { cs: 'Dočasný cíl' @@ -9856,6 +9833,7 @@ function init() { ,nl: 'Tijdelijk doel' ,ko: '임시 목표' ,fi: 'Tilapäinen tavoite' + , pl: 'Tymczasowy cel' ,pt: 'Meta temporária' ,it: 'Obiettivo Temporaneo' ,tr: 'Geçici hedef' @@ -9879,34 +9857,34 @@ function init() { ,ru: 'Причина' ,ko: '근거' ,fi: 'Syy' + , pl: 'Powód' ,pt: 'Razão' ,it: 'Ragionare' ,tr: 'Neden' // Gerekçe ,zh_cn: '原因' - ,pl: 'Powód' } ,'Eating soon' : { cs: 'Následuje jídlo' - ,he: 'אוכל בקרוב ' + ,he: 'אוכל בקרוב' ,sk: 'Jesť čoskoro' ,fr: 'Repas sous peu' ,sv: 'Snart matdags' ,nb: 'Snart tid for mat' - ,de: 'Bald essend' + ,de: 'Bald essen' ,dk: 'Spiser snart' ,es: 'Comer pronto' ,bg: 'Ядене скоро' ,hr: 'Uskoro jelo' ,ro: 'Mâncare în curând' - ,ru: 'Скоро еда' + ,ru: 'Ожидаемый прием пищи' ,nl: 'Binnenkort eten' ,ko: '편집 중' ,fi: 'Syödään pian' + , pl: 'Zjedz wkrótce' ,pt: 'Refeição em breve' ,it: 'Mangiare prossimamente' ,tr: 'Yakında yemek' // Kısa zamanda yemek yenecek ,zh_cn: '接近用餐时间' - ,pl: 'Zjedz wkrótce' } ,'Top' : { cs: 'Horní' @@ -10012,7 +9990,7 @@ function init() { ,ro: 'Insulină bolusată:' ,el: 'Ινσουλίνη' ,es: 'Bolo de Insulina' - ,ru: 'Болюс инсулин' + ,ru: 'Болюсный инсулин' ,sv: 'Bolusinsulin:' ,nb: 'Bolusinsulin:' ,hr: 'Bolus:' @@ -10036,7 +10014,7 @@ function init() { ,ro: 'Bazala obișnuită:' ,el: 'Βασική Ινσουλίνη' ,es: 'Insulina basal básica' - ,ru: 'Основной базал инсулин' + ,ru: 'Профильный базальный инсулин' ,sv: 'Basalinsulin:' ,nb: 'Basalinsulin:' ,hr: 'Osnovni bazal:' @@ -10068,13 +10046,13 @@ function init() { ,fi: 'Positiivinen tilapäisbasaali:' ,de: 'Positives temporäres Basal Insulin:' ,dk: 'Positiv midlertidig basalinsulin:' + , pl: 'Zwiększona bazowa dawka insuliny' ,pt: 'Insulina basal temporária positiva:' ,sk: 'Pozitívny dočasný bazálny inzulín:' ,it: 'Insulina basale temp positiva:' ,nl: 'Extra tijdelijke basaal insuline' ,tr: 'Pozitif geçici bazal insülin:' ,zh_cn: '实际临时基础率胰岛素' - , pl: 'Zwiększona bazowa dawka insuliny' } ,'Negative temp basal insulin:' : { cs:'Negativní dočasný bazální inzulín:' @@ -10092,13 +10070,13 @@ function init() { ,fi: 'Negatiivinen tilapäisbasaali:' ,de: 'Negatives temporäres Basal Insulin:' ,dk: 'Negativ midlertidig basalinsulin:' + , pl: 'Zmniejszona bazowa dawka insuliny' ,pt: 'Insulina basal temporária negativa:' ,sk: 'Negatívny dočasný bazálny inzulín:' ,it: 'Insulina basale Temp negativa:' ,nl: 'Negatieve tijdelijke basaal insuline' ,tr: 'Negatif geçici bazal insülin:' ,zh_cn: '其余临时基础率胰岛素' - , pl: 'Zmniejszona bazowa dawka insuliny' } ,'Total basal insulin:' : { cs: 'Celkový bazální inzulín:' @@ -10116,13 +10094,13 @@ function init() { ,fi: 'Basaali yhteensä:' ,de: 'Gesamt Basal Insulin:' ,dk: 'Total daglig basalinsulin:' + , pl: 'Całkowita ilość bazowej dawki insuliny' ,pt: 'Insulina basal total:' ,sk: 'Celkový bazálny inzulín:' ,it: 'Insulina Basale Totale:' ,nl: 'Totaal basaal insuline' ,tr: 'Toplam bazal insülin:' ,zh_cn: '基础率胰岛素合计' - , pl: 'Całkowita ilość bazowej dawki insuliny' } ,'Total daily insulin:' : { cs:'Celkový denní inzulín:' @@ -10140,13 +10118,13 @@ function init() { ,de: 'Gesamtes tägliches Insulin:' ,dk: 'Total dagsdosis insulin' ,es: 'Total Insulina diaria:' + ,pl: 'Całkowita dzienna ilość insuliny' ,pt: 'Insulina diária total:' ,sk: 'Celkový denný inzulín:' ,it: 'Totale giornaliero d\'insulina:' ,nl: 'Totaal dagelijkse insuline' ,tr: 'Günlük toplam insülin:' ,zh_cn: '每日胰岛素合计' - ,pl: 'Całkowita dzienna ilość insuliny' } ,'Unable to %1 Role' : { // PUT or POST cs: 'Chyba volání %1 Role:' @@ -10156,12 +10134,13 @@ function init() { ,fr: 'Incapable de %1 rôle' ,ro: 'Imposibil de %1 Rolul' ,es: 'Incapaz de %1 Rol' - ,ru: 'Невозможно %1 Роль' + ,ru: 'Невозможно назначить %1 Роль' ,sv: 'Kan inte ta bort roll %1' ,nb: 'Kan ikke %1 rolle' ,fi: '%1 operaatio roolille opäonnistui' ,de: 'Unpassend zu %1 Rolle' ,dk: 'Kan ikke slette %1 rolle' + ,pl: 'Nie można %1 roli' ,pt: 'Função %1 não foi possível' ,sk: 'Chyba volania %1 Role' ,ko: '%1로 비활성' @@ -10169,7 +10148,6 @@ function init() { ,nl: 'Kan %1 rol niet verwijderen' ,tr: '%1 Rolü yapılandırılamadı' ,zh_cn: '%1角色不可用' - ,pl: 'Nie można %1 roli' } ,'Unable to delete Role' : { cs: 'Nelze odstranit Roli:' @@ -10208,6 +10186,7 @@ function init() { ,fi: 'Tietokanta sisältää %1 roolia' ,de: 'Datenbank enthält %1 Rollen' ,dk: 'Databasen indeholder %1 roller' + , pl: 'Baza danych zawiera %1 ról' ,pt: 'Banco de dados contém %1 Funções' ,sk: 'Databáza obsahuje %1 rolí' ,ko: '데이터베이스가 %1 포함' @@ -10215,7 +10194,6 @@ function init() { ,nl: 'Database bevat %1 rollen' ,tr: 'Veritabanı %1 rol içerir' ,zh_cn: '数据库包含%1个角色' - , pl: 'Baza danych zawiera %1 ról' } ,'Edit Role' : { cs:'Editovat roli' @@ -10231,6 +10209,7 @@ function init() { ,de: 'Rolle editieren' ,dk: 'Rediger rolle' ,es: 'Editar Rol' + ,pl: 'Edycja roli' ,pt: 'Editar Função' ,sk: 'Editovať rolu' ,ko: '편집 모드' @@ -10238,7 +10217,6 @@ function init() { ,nl: 'Pas rol aan' ,tr: 'Rolü düzenle' ,zh_cn: '编辑角色' - ,pl: 'Edycja roli' } ,'admin, school, family, etc' : { cs: 'administrátor, škola, rodina atd...' @@ -10254,6 +10232,7 @@ function init() { ,fi: 'ylläpitäjä, koulu, perhe jne' ,de: 'Administrator, Schule, Familie, etc' ,dk: 'Administrator, skole, familie, etc' + ,pl: 'administrator, szkoła, rodzina, itp' ,pt: 'Administrador, escola, família, etc' ,sk: 'administrátor, škola, rodina atď...' ,ko: '관리자, 학교, 가족 등' @@ -10261,7 +10240,6 @@ function init() { ,nl: 'admin, school, familie, etc' ,tr: 'yönetici, okul, aile, vb' ,zh_cn: '政府、学校、家庭等' - ,pl: 'administrator, szkoła, rodzina, itp' } ,'Permissions' : { cs: 'Oprávnění' @@ -10277,6 +10255,7 @@ function init() { ,de: 'Berechtigungen' ,dk: 'Rettigheder' ,es: 'Permisos' + ,pl: 'Uprawnienia' ,pt: 'Permissões' ,sk: 'Oprávnenia' ,ko: '허가' @@ -10284,7 +10263,6 @@ function init() { ,nl: 'Rechten' ,tr: 'İzinler' ,zh_cn: '权限' - ,pl: 'Uprawnienia' } ,'Are you sure you want to delete: ' : { cs: 'Opravdu vymazat: ' @@ -10293,13 +10271,14 @@ function init() { ,hr: 'Sigurno želite obrisati?' ,ro: 'Confirmați ștergerea: ' ,fr: 'Êtes-vous sûr de vouloir effacer:' - ,ru: 'Вы уверены, что хотите удалить' + ,ru: 'Подтвердите удаление' ,sv: 'Är du säker att du vill ta bort:' ,nb: 'Er du sikker på at du vil slette:' ,fi: 'Oletko varmat että haluat tuhota: ' - ,de: ' Sind sie sicher, das Sie löschen wollen:' + ,de: 'Sind sie sicher, das Sie löschen wollen:' ,dk: 'Er du sikker på at du vil slette:' ,es: 'Seguro que quieres eliminarlo:' + ,pl: 'Jesteś pewien, że chcesz usunąć:' ,pt: 'Tem certeza de que deseja apagar:' ,sk: 'Naozaj zmazať:' ,ko: '정말로 삭제하시겠습니까: ' @@ -10307,7 +10286,6 @@ function init() { ,nl: 'Weet u het zeker dat u wilt verwijderen?' ,tr: 'Silmek istediğinizden emin misiniz:' ,zh_cn: '你确定要删除:' - ,pl: 'Jesteś pewien, że chcesz usunąć:' } ,'Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.' : { cs: 'Každá role má 1 nebo více oprávnění. Oprávnění * je zástupný znak, oprávnění jsou hiearchie používající : jako oddělovač.' @@ -10322,6 +10300,7 @@ function init() { ,fi: 'Jokaisella roolilla on yksi tai useampia oikeuksia. * on jokeri (tunnistuu kaikkina oikeuksina), oikeudet ovat hierarkia joka käyttää : merkkiä erottimena.' ,de: 'Jede Rolle hat eine oder mehrere Berechtigungen. Die * Berechtigung ist ein Platzhalter, Berechtigungen sind hierachrchisch mit : als Separator.' ,es: 'Cada Rol tiene uno o más permisos. El permiso * es un marcador de posición y los permisos son jerárquicos con : como separador.' + , pl: 'Każda rola będzie mieć 1 lub więcej uprawnień. Symbol * uprawnia do wszystkiego. Uprawnienia są hierarchiczne, używając : jako separatora.' ,pt: 'Cada função terá uma ou mais permissões. A permissão * é um wildcard, permissões são uma hierarquia utilizando * como um separador.' ,sk: 'Každá rola má 1 alebo viac oprávnení. Oprávnenie * je zástupný znak, oprávnenia sú hierarchie používajúce : ako oddelovač.' ,ko: '각각은 1 또는 그 이상의 허가를 가지고 있습니다. 허가는 예측이 안되고 구분자로 : 사용한 계층이 있습니다' @@ -10329,7 +10308,6 @@ function init() { ,nl: 'Elke rol heeft mintens 1 machtiging. De * machtiging is een wildcard, machtigingen hebben een hyrarchie door gebruik te maken van : als scheidingsteken.' ,tr: 'Her rolün bir veya daha fazla izni vardır.*izni bir yer tutucudur ve izinler ayırıcı olarak : ile hiyerarşiktir.' ,zh_cn: '每个角色都具有一个或多个权限。权限设置时使用*作为通配符,层次结构使用:作为分隔符。' - , pl: 'Każda rola będzie mieć 1 lub więcej uprawnień. Symbol * uprawnia do wszystkiego. Uprawnienia są hierarchiczne, używając : jako separatora.' } ,'Add new Role' : { cs: 'Přidat novou roli' @@ -10412,7 +10390,7 @@ function init() { ,sv: 'Administratorgodkänt' ,nb: 'Administratorgodkjent' ,fi: 'Ylläpitäjä valtuutettu' - ,de: 'Admin Authorisierung' + ,de: 'als Administrator autorisiert' ,dk: 'Administrator godkendt' ,pt: 'Administrador autorizado' ,sk: 'Admin autorizovaný' @@ -10500,7 +10478,7 @@ function init() { ,hr: 'Ne mogu %1 subjekt' ,ro: 'Imposibil de %1 Subiectul' ,fr: 'Impossible de créer l\'Utilisateur %1' - ,ru: 'Невозможно %1 субъекта' + ,ru: 'Невозможно создать %1 субъект' ,sv: 'Kan ej %1 ämne' ,nb: 'Kan ikke %1 ressurs' ,fi: '%1 operaatio käyttäjälle epäonnistui' @@ -10523,7 +10501,7 @@ function init() { ,hr: 'Ne mogu obrisati subjekt' ,ro: 'Imposibil de șters Subiectul' ,fr: 'Impossible d\'effacer l\'Utilisateur' - ,ru: 'Невозможно удалить ' + ,ru: 'Невозможно удалить Субъект ' ,sv: 'Kan ej ta bort ämne' ,nb: 'Kan ikke slette ressurs' ,fi: 'Käyttäjän poistaminen epäonnistui' @@ -10546,7 +10524,7 @@ function init() { ,hr: 'Baza sadrži %1 subjekata' ,fr: 'La base de données contient %1 utilisateurs' ,fi: 'Tietokanta sisältää %1 käyttäjää' - ,ru: 'База данных содержит %1 субъект(а/ов)' + ,ru: 'База данных содержит %1 субъекта/ов' ,ro: 'Baza de date are %1 subiecți' ,sv: 'Databasen innehåller %1 ämnen' ,nb: 'Databasen inneholder %1 ressurser' @@ -10570,7 +10548,7 @@ function init() { ,ro: 'Editează Subiectul' ,es: 'Editar sujeto' ,fr: 'Éditer l\'Utilisateur' - ,ru: 'Редактировать субъекта' + ,ru: 'Редактировать Субъект' ,sv: 'Editera ämne' ,nb: 'Editer ressurs' ,fi: 'Muokkaa käyttäjää' @@ -10638,7 +10616,7 @@ function init() { ,hr: 'Uredi ovaj subjekt' ,ro: 'Editează acest subiect' ,fr: 'Éditer cet utilisateur' - ,ru: 'Редактировать этого субъекта' + ,ru: 'Редактировать этот субъект' ,sv: 'Editera ämnet' ,es: 'Editar este sujeto' ,nb: 'Editer ressurs' @@ -10661,7 +10639,7 @@ function init() { ,hr: 'Obriši ovaj subjekt' ,ro: 'Șterge acest subiect' ,fr: 'Effacer cet utilisateur:' - ,ru: 'Удалить этого субъекта' + ,ru: 'Удалить этот субъект' ,sv: 'Ta bort ämnet' ,nb: 'Slett ressurs' ,fi: 'Poista tämä käyttäjä' @@ -10780,7 +10758,7 @@ function init() { ,sv: 'Tyst i %1 minuter' ,nb: 'Stille i %1 minutter' ,fi: 'Hiljennä %1 minuutiksi' - ,de: 'Inaktivität für %1 Minuten' + ,de: 'Ruhe für %1 Minuten' ,dk: 'Stilhed i %1 minutter' ,pt: 'Silencir por %1 minutos' ,es: 'Silenciado por %1 minutos' @@ -10870,7 +10848,7 @@ function init() { ,dk: 'Insulinfølsomhed (ISF)' ,ro: 'Sensibilitate la insulină (ISF)' ,fr: 'Sensibilité à l\'insuline (ISF)' - ,ru: 'Чуствительность к инсулину' + ,ru: 'Чуствительность к инсулину ISF' ,sk: 'Citlivosť (ISF)' ,sv: 'Insulinkönslighet (ISF)' ,nb: 'Insulinsensitivitet (ISF)' @@ -10893,7 +10871,7 @@ function init() { ,dk: 'Nuværende kulhydratratio' ,fr: 'Rapport Insuline-glucides actuel (I:C)' ,ro: 'Raport Insulină:Carbohidrați (ICR)' - ,ru: 'Актуальное соотношение инсулин:углеводы' + ,ru: 'Актуальное соотношение инсулин:углеводы I:C' ,sk: 'Aktuálny sacharidový pomer (I"C)' ,sv: 'Gällande kolhydratkvot' ,nb: 'Gjeldende karbohydratforhold' @@ -10985,7 +10963,7 @@ function init() { ,dk: 'Aktiv midlertidig basal start' ,ro: 'Start bazală temporară activă' ,fr: 'Début du débit basal temporaire' - ,ru: 'Старт активного временного базала' + ,ru: 'Старт актуального временного базала' ,sk: 'Štart dočasného bazálu' ,sv: 'Aktiv tempbasal start' ,nb: 'Aktiv midlertidig basal start' @@ -11008,7 +10986,7 @@ function init() { ,dk: 'Aktiv midlertidig basal varighed' ,ro: 'Durata bazalei temporare active' ,fr: 'Durée du débit basal temporaire' - ,ru: 'Длительность активного временного базала' + ,ru: 'Длительность актуального временного базала' ,sk: 'Trvanie dočasného bazálu' ,sv: 'Aktiv tempbasal varaktighetstid' ,nb: 'Aktiv midlertidig basal varighet' @@ -11031,7 +11009,7 @@ function init() { ,dk: 'Resterende tid for aktiv midlertidig basal' ,ro: 'Rest de bazală temporară activă' ,fr: 'Durée restante de débit basal temporaire' - ,ru: 'Остаток акивного временного базала' + ,ru: 'Остается актуального временного базала' ,sk: 'Zostatok dočasného bazálu' ,sv: 'Återstående tempbasaltid' ,nb: 'Gjenstående midlertidig basal tid' @@ -11053,7 +11031,7 @@ function init() { ,de: 'Basal-Profil Wert' ,dk: 'Basalprofil værdi' ,ro: 'Valoarea profilului bazalei' - ,ru: 'значение профиля базала' + ,ru: 'Величина профильного базала' ,fr: 'Valeur du débit basal' ,sv: 'Basalprofil värde' ,es: 'Valor perfil Basal' @@ -11100,7 +11078,7 @@ function init() { ,dk: 'Aktiv kombibolus start' ,ro: 'Start bolus combinat activ' ,fr: 'Début de Bolus Duo/Combo' - ,ru: 'Старт активного комбо болюса' + ,ru: 'Старт активного комбинир болюса' ,sv: 'Aktiv kombobolus start' ,nb: 'Kombinasjonsbolus start' ,fi: 'Aktiivisen yhdistelmäboluksen alku' @@ -11123,7 +11101,7 @@ function init() { ,dk: 'Aktiv kombibolus varighed' ,ro: 'Durată bolus combinat activ' ,fr: 'Durée du Bolus Duo/Combo' - ,ru: 'Длительность активного комбо болюса' + ,ru: 'Длительность активного комбинир болюса' ,sv: 'Aktiv kombibolus varaktighet' ,es: 'Duración del Combo-Bolo activo' ,nb: 'Kombinasjonsbolus varighet' @@ -11146,7 +11124,7 @@ function init() { ,dk: 'Resterende aktiv kombibolus' ,ro: 'Rest de bolus combinat activ' ,fr: 'Activité restante du Bolus Duo/Combo' - ,ru: 'Остаток активного комбо болюса' + ,ru: 'Остается активного комбинир болюса' ,sv: 'Återstående aktiv kombibolus' ,es: 'Restante Combo-Bolo activo' ,nb: 'Gjenstående kombinasjonsbolus' @@ -11176,7 +11154,7 @@ function init() { ,pt: 'Diferença de glicemia' ,es: 'Diferencia de glucemia' ,sk: 'Zmena glykémie' - ,bg: 'Изменение КЗ' + ,bg: 'Дельта ГК' ,hr: 'GUK razlika' ,ko: '혈당 차이' ,it: 'BG Delta' @@ -11264,7 +11242,7 @@ function init() { ,de: 'BWP' ,dk: 'Bolusberegner (BWP)' ,ro: 'Ajutor bolusare (BWP)' - ,ru: 'Предпросмотр калькулятора болюса' + ,ru: 'Калькулятор болюса' ,fr: 'Calculateur de bolus (BWP)' ,sv: 'Boluskalkylator' ,es: 'VistaPreviaCalculoBolo (BWP)' @@ -11411,7 +11389,7 @@ function init() { ,fr: 'Vérifier la glycémie, bolus nécessaire ?' ,bg: 'Провери КЗ, не е ли време за болус?' ,hr: 'Provjeri GUK, vrijeme je za bolus?' - ,ru: 'Проверьте СК, не пора ли ввести болюс?' + ,ru: 'Проверьте ГК, дать болюс?' ,sv: 'Kontrollera BS, dags att ge bolus?' ,nb: 'Sjekk blodsukker, på tide med bolus?' ,fi: 'Tarkista VS, aika bolustaa?' @@ -11596,18 +11574,18 @@ function init() { ,fr: 'Insuline en excès: %1U de plus que nécessaire pour atteindre la cible inférieure, sans prendre en compte les glucides' ,bg: 'Излишният инсулин %1U е повече от необходимия за достигане до долната граница, ВХ не се вземат под внимание' ,hr: 'Višak inzulina je %1U više nego li je potrebno da se postigne donja ciljana granica, ne uzevši u obzir UGH' - ,ru: 'Избыток инсулина равного %1U, необходимого для достижения нижнего целевого значения, углеводы не учитываются' + ,ru: 'Избыток инсулина равного %1U, необходимого для достижения нижнего целевого значения, углеводы не приняты в расчет' ,sv: 'Överskott av insulin motsvarande %1U mer än nödvändigt för att nå lågt målvärde, kolhydrater ej medräknade' ,nb: 'Insulin tilsvarende %1U mer enn det trengs for å nå lavt mål, karbohydrater ikke medregnet' ,nl: 'Insulineoverschot van %1U om laag doel te behalen (excl. koolhydraten)' ,fi: 'Liikaa insuliinia: %1U enemmän kuin tarvitaan tavoitteeseen pääsyyn (huomioimatta hiilihydraatteja)' + , pl: 'Nadmiar insuliny, %1J więcej niż potrzeba, aby osiągnąć cel dolnej granicy, nie biorąc pod uwagę węglowodanów' ,pt: 'Excesso de insulina equivalente a %1U além do necessário para atingir a meta inferior, sem levar em conta carboidratos' ,sk: 'Nadbytok inzulínu o %1U viac ako je potrebné na dosiahnutie spodnej cieľovej hranice. Neráta sa so sacharidmi.' ,ko: '낮은 혈당 목표에 도달하기 위해 필요한 인슐린양보다 %1U의 인슐린 양이 초과 되었고 탄수화물 양이 초과되지 않았습니다.' ,it: 'L\'eccesso d\'insulina equivalente %1U più che necessari per raggiungere l\'obiettivo basso, non rappresentano i carboidrati.' ,tr: 'Fazla insülin: Karbonhidratları dikkate alınmadan, alt hedefe ulaşmak için gerekenden %1U\'den daha fazla' //??? ,zh_cn: '胰岛素超过至血糖下限目标所需剂量%1单位,不计算碳水化合物' - , pl: 'Nadmiar insuliny, %1J więcej niż potrzeba, aby osiągnąć cel dolnej granicy, nie biorąc pod uwagę węglowodanów' } ,'Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS' : { cs:'Nadbytek inzulínu: o %1U více, než na dosažení spodní hranice cíle. UJISTĚTE SE, ŽE JE TO POKRYTO SACHARIDY' @@ -11618,7 +11596,7 @@ function init() { ,fr: 'Insuline en excès: %1U de plus que nécessaire pour atteindre la cible inférieure, ASSUREZ UN APPORT SUFFISANT DE GLUCIDES' ,bg: 'Излишният инсулин %1U е повече от необходимия за достигане до долната граница, ПРОВЕРИ ДАЛИ IOB СЕ ПОКРИВА ОТ ВЪГЛЕХИДРАТИТЕ' ,hr: 'Višak inzulina je %1U više nego li je potrebno da se postigne donja ciljana granica, OBAVEZNO POKRIJTE SA UGH' - ,ru: 'Избыток инсулина, равного %1U, необходимого для достижения нижнего целевого значения, ПОКРОЙТЕ IOB ИНСУЛИН В ОРГАНИЗМЕ УГЛЕВОДАМИ' + ,ru: 'Избыток инсулина, равного %1U, необходимого для достижения нижнего целевого значения, ПОКРОЙТЕ АКТИВН IOB ИНСУЛИН УГЛЕВОДАМИ' ,sv: 'Överskott av insulin motsvarande %1U mer än nödvändigt för att nå lågt målvärde, SÄKERSTÄLL ATT IOB TÄCKS AV KOLHYDRATER' ,es: 'Exceso de insulina en %1U más de la necesaria para alcanzar objetivo inferior. ASEGÚRESE QUE LA INSULINA ACTIVA IOB ESTA CUBIERTA POR CARBOHIDRATOS' ,nb: 'Insulin tilsvarende %1U mer enn det trengs for å nå lavt mål, PASS PÅ AT AKTIVT INSULIN ER DEKKET OPP MED KARBOHYDRATER' @@ -11653,7 +11631,7 @@ function init() { ,it: 'Riduzione 1U% necessaria d\'insulina attiva per raggiungere l\'obiettivo basso, troppa basale?' ,tr: 'Alt KŞ hedefi için %1U aktif insülin azaltılmalı, bazal oranı çok mu yüksek?' ,zh_cn: '活性胰岛素已可至血糖下限目标,需减少%1单位,基础率过高?' - , pl: '%1J potrzebnej redukcji w aktywnej insulinie, aby osiągnąć niski cel dolnej granicy, Za duża dawka podstawowa ?' + ,pl: '%1J potrzebnej redukcji w aktywnej insulinie, aby osiągnąć niski cel dolnej granicy, Za duża dawka podstawowa ?' } ,'basal adjustment out of range, give carbs?' : { cs:'úprava změnou bazálu není možná. Podat sacharidy?' @@ -11664,7 +11642,7 @@ function init() { ,fr: 'ajustement de débit basal hors de limites, prenez des glucides?' ,bg: 'Корекция на базала не е възможна, добавка на въглехидрати? ' ,hr: 'prilagodba bazala je izvan raspona, dodati UGH?' - ,ru: 'Корректировка базы вне диапазона, добавить углеводов?' + ,ru: 'Корректировка базала вне диапазона, добавить углеводов?' ,sv: 'basaländring utanför gräns, ge kolhydrater?' ,es: 'ajuste basal fuera de rango, dar carbohidratos?' ,nb: 'basaljustering utenfor tillatt område, gi karbohydrater?' @@ -11687,7 +11665,7 @@ function init() { ,fr: 'ajustement de débit basal hors de limites, prenez un bolus?' ,bg: 'Корекция на базала не е възможна, добавка на болус? ' ,hr: 'prilagodna bazala je izvan raspona, dati bolus?' - ,ru: 'Корректировка базы вне диапазона, добавить болюс?' + ,ru: 'Корректировка базала вне диапазона, добавить болюс?' ,sv: 'basaländring utanför gräns, ge bolus?' ,nb: 'basaljustering utenfor tillatt område, gi bolus?' ,fi: 'säätö liian suuri, anna bolus?' @@ -11708,7 +11686,7 @@ function init() { ,dk: 'over højt grænse' ,ro: 'peste ținta superioară' ,fr: 'plus haut que la limite supérieure' - ,ru: 'Выше верхнего' + ,ru: 'Выше верхней границы' ,bg: 'над горната' ,hr: 'iznad gornje granice' ,sv: 'över hög nivå' @@ -11734,7 +11712,7 @@ function init() { ,fr: 'plus bas que la limite inférieure' ,bg: 'под долната' ,hr: 'ispod donje granice' - ,ru: 'Ниже нижнего' + ,ru: 'Ниже нижней границы' ,sv: 'under låg nivå' ,nb: 'under lav grense' ,fi: 'alle matalan' @@ -11758,7 +11736,7 @@ function init() { ,fr: 'Glycémie cible projetée %1 ' ,bg: 'Предполагаемата КЗ %1 в граници' ,hr: 'Procjena GUK %1 cilja' - ,ru: 'Расчетная гликемия %1' + ,ru: 'Расчетная целевая гликемия %1' ,sv: 'Önskat BS %1 mål' ,nb: 'Ønsket BS %1 mål' ,fi: 'Laskettu VS %1 tavoitteen' @@ -11827,7 +11805,7 @@ function init() { ,fr: 'ou ajuster le débit basal' ,bg: 'или корекция на базала' ,hr: 'ili prilagodba bazala' - ,ru: 'или корректировать базу' + ,ru: 'или корректировать базал' ,sv: 'eller justera basal' ,nb: 'eller justere basal' ,fi: 'tai säädä basaalia' @@ -11850,7 +11828,7 @@ function init() { ,fr: 'Vérifier la glycémie avec un glucomètre avant de corriger!' ,bg: 'Провери КЗ с глюкомер, преди кореция!' ,hr: 'Provjeri GUK glukometrom prije korekcije!' - ,ru: 'Перед корректировкой сверьте СК с глюкометром' + ,ru: 'Перед корректировкой сверьте ГК с глюкометром' ,sv: 'Kontrollera blodglukos med fingerstick före korrigering!' ,nb: 'Sjekk blodsukker før korrigering!' ,fi: 'Tarkista VS mittarilla ennen korjaamista!' @@ -11873,7 +11851,7 @@ function init() { ,fr: 'Réduction du débit basal pour obtenir l\'effet d\' %1 unité' ,bg: 'Намаляне на базала с %1 единици' ,hr: 'Smanjeni bazal da uračuna %1 jedinica:' - ,ru: 'Снижение базы на %1 единиц' + ,ru: 'Снижение базы из-за %1 единиц болюса' ,sv: 'Basalsänkning för att nå %1 enheter' ,nb: 'Basalredusering for å nå %1 enheter' ,fi: 'Basaalin vähennys saadaksesi %1 yksikön vaikutuksen:' @@ -11942,7 +11920,7 @@ function init() { ,bg: 'Времето за смяна на сет просрочено' ,hr: 'Prošao rok za zamjenu kanile!' ,fr: 'Dépassement de date de changement de canule!' - ,ru: 'Срок работы катеттера истек' + ,ru: 'Срок замены катетера помпы истек' ,sv: 'Infusionsset, bytestid överskriden' ,nb: 'Byttetid for infusjonssett overskredet' ,fi: 'Kanyylin ikä yli määräajan!' @@ -11965,7 +11943,7 @@ function init() { ,ro: 'Este vremea să schimbați canula' ,bg: 'Време за смяна на сет' ,hr: 'Vrijeme za zamjenu kanile' - ,ru: 'Пора заменить катеттер' + ,ru: 'Пора заменить катетер помпы' ,sv: 'Dags att byta infusionsset' ,nb: 'På tide å bytte infusjonssett' ,fi: 'Aika vaihtaa kanyyli' @@ -11988,7 +11966,7 @@ function init() { ,fr: 'Changement de canule bientòt' ,bg: 'Смени сета скоро' ,hr: 'Zamijena kanile uskoro' - ,ru: 'Подходит время замены катеттера' + ,ru: 'Приближается время замены катетера помпы' ,sv: 'Byt infusionsset snart' ,nb: 'Bytt infusjonssett snart' ,fi: 'Vaihda kanyyli pian' @@ -12010,7 +11988,7 @@ function init() { ,ro: 'Vechimea canulei în ore: %1' ,bg: 'Сетът е на %1 часове' ,hr: 'Staros kanile %1 sati' - ,ru: 'Возраст катеттера %1 час' + ,ru: 'Катетер помпы работает %1 час' ,sv: 'Infusionsset tid %1 timmar' ,nb: 'infusjonssett alder %1 timer' ,fi: 'Kanyylin ikä %1 tuntia' @@ -12033,7 +12011,7 @@ function init() { ,fr: 'Insérée' ,bg: 'Поставен' ,hr: 'Postavljanje' - ,ru: 'Введено' + ,ru: 'Установлен' ,sv: 'Applicerad' ,nb: 'Satt inn' ,fi: 'Asetettu' @@ -12058,7 +12036,7 @@ function init() { ,bg: 'ВС' ,hr: 'Starost kanile' ,fr: 'CAGE' - ,ru: 'ВозрКат' + ,ru: 'КатПомп' ,sv: 'Nål' ,nb: 'Nål alder' ,fi: 'KIKÄ' @@ -12082,7 +12060,7 @@ function init() { ,bg: 'АВХ' ,hr: 'Aktivni UGH' ,fr: 'COB' - ,ru: 'Активн углеводы' + ,ru: 'АктУгл COB' ,sv: 'COB' ,nb: 'Aktive karbohydrater' ,fi: 'AH' @@ -12105,7 +12083,7 @@ function init() { ,fr: 'Derniers glucides' ,bg: 'Последни ВХ' ,hr: 'Posljednji UGH' - ,ru: 'Новые углеводы' + ,ru: 'Прошлые углеводы' ,sv: 'Senaste kolhydrater' ,nb: 'Siste karbohydrater' ,fi: 'Viimeisimmät hiilihydraatit' @@ -12126,9 +12104,9 @@ function init() { ,dk: 'Insulinalder' ,ro: 'VI' ,fr: 'IAGE' - ,bg: 'ВИнс' + ,bg: 'ИнсСрок' ,hr: 'Starost inzulina' - ,ru: 'ВозрИнс' + ,ru: 'ИнсСрок' ,sv: 'Insulinålder' ,nb: 'Insulinalder' ,fi: 'IIKÄ' @@ -12264,7 +12242,7 @@ function init() { ,de: 'IOB' ,dk: 'IOB' ,ro: 'IOB' - ,ru: 'Активный Инсулин IOB' + ,ru: 'АктИнс IOB' ,fr: 'IOB' ,bg: 'АИ' ,hr: 'Aktivni inzulin' @@ -12287,7 +12265,7 @@ function init() { ,de: 'Careportal IOB' ,dk: 'IOB i Careportal' ,ro: 'IOB în Careportal' - ,ru: 'Активн Инс на портале назначений' + ,ru: 'АктИнс на портале лечения' ,fr: 'Careportal IOB' ,bg: 'АИ от Кеърпортал' ,hr: 'Careportal IOB' @@ -12333,7 +12311,7 @@ function init() { ,de: 'Basal IOB' ,dk: 'Basal IOB' ,ro: 'IOB bazală' - ,ru: 'Активн Базал IOB' + ,ru: 'АктуальнБазал IOB' ,fr: 'IOB du débit basal' ,bg: 'Базален АИ' ,hr: 'Bazalni aktivni inzulin' @@ -12380,20 +12358,20 @@ function init() { ,dk: 'Gammel data, kontrollere uploader?' ,ro: 'Date învechite, verificați uploaderul!' ,fr: 'Valeurs trop anciennes, vérifier l\'uploadeur' - ,ru: 'Устаревшие данные, проверьте загрузчик' + ,ru: 'Старые данные, проверьте загрузчик' ,bg: 'Стари данни, провери телефона' ,hr: 'Nedostaju podaci, provjera opreme?' ,es: 'Datos desactualizados, controlar la subida?' ,sv: 'Gammal data, kontrollera rigg?' ,nb: 'Gamle data, sjekk rigg?' ,fi: 'Tiedot vanhoja, tarkista lähetin?' + , pl: 'Dane są nieaktualne, sprawdź urządzenie transmisyjne.' ,pt: 'Dados antigos, verificar uploader?' ,sk: 'Zastaralé dáta, skontrolujte uploader' ,ko: '오래된 데이터입니다. 확인해 보시겠습니까?' ,it: 'dati non aggiornati, controllare il telefono?' ,nl: 'Geen data, controleer uploader' ,zh_cn: '数据过期,检查一下设备?' - , pl: 'Dane są nieaktualne, sprawdź urządzenie transmisyjne.' ,tr: 'Veri güncel değil, vericiyi kontrol et?' } ,'Last received:' : { @@ -12403,7 +12381,7 @@ function init() { ,dk: 'Senest modtaget:' ,fr: 'Dernière réception:' ,ro: 'Ultimile date:' - ,ru: 'Предыдущий полученный' + ,ru: 'Получено:' ,bg: 'Последно получени' ,hr: 'Zadnji podaci od:' ,sv: 'Senast mottagen:' @@ -12426,7 +12404,7 @@ function init() { ,dk: '%1m siden' ,ro: 'acum %1 minute' ,fr: 'il y a %1 min' - ,ru: 'мин назад' + ,ru: '% мин назад' ,bg: 'преди %1 мин.' ,hr: 'prije %1m' ,sv: '%1m sedan' @@ -12449,7 +12427,7 @@ function init() { ,dk: '%1t siden' ,ro: 'acum %1 ore' ,fr: '%1 heures plus tôt' - ,ru: 'час назад' + ,ru: '% час назад' ,bg: 'преди %1 час' ,hr: 'prije %1 sati' ,sv: '%1h sedan' @@ -12472,7 +12450,7 @@ function init() { ,dk: '%1d siden' ,ro: 'acum %1 zile' ,fr: '%1 jours plus tôt' - ,ru: 'дн назад' + ,ru: '% дн назад' ,bg: 'преди %1 ден' ,hr: 'prije %1 dana' ,sv: '%1d sedan' @@ -12494,7 +12472,7 @@ function init() { ,de: 'RETRO' ,dk: 'RETRO' ,ro: 'VECHI' - ,ru: 'РЕТРО' + ,ru: 'ПРОШЛОЕ' ,bg: 'РЕТРО' ,hr: 'RETRO' ,fr: 'RETRO' @@ -12517,7 +12495,7 @@ function init() { ,de:'SAGE' ,dk: 'Sensoralder' ,ro: 'VS' - ,ru: 'Сенсор проработал' + ,ru: 'Сенсор работает' ,fr: 'SAGE' ,bg: 'ВС' ,hr: 'Starost senzora' @@ -12542,7 +12520,7 @@ function init() { ,dk: 'Sensor skift/genstart overskredet!' ,ro: 'Depășire termen schimbare/restart senzor!' ,fr: 'Changement/Redémarrage du senseur dépassé!' - ,ru: 'Рестарт сенсора просрочен' + ,ru: 'Рестарт сенсора пропущен' ,bg: 'Смяната/рестартът на сензора са пресрочени' ,hr: 'Prošao rok za zamjenu/restart senzora!' ,sv: 'Sensor byte/omstart överskriden!' @@ -12611,7 +12589,7 @@ function init() { ,dk: 'Sensoralder %1 dage %2 timer' ,ro: 'Senzori vechi de %1 zile și %2 ore' ,fr: 'Âge su senseur %1 jours et %2 heures' - ,ru: 'Сенсор проработал % дн % час' + ,ru: 'Сенсор работает %1 дн %2 час' ,bg: 'Сензорът е на %1 дни %2 часа ' ,hr: 'Starost senzora %1 dana i %2 sati' ,sv: 'Sensorålder %1 dagar %2 timmar' @@ -12634,7 +12612,7 @@ function init() { ,dk: 'Sensor isat' ,ro: 'Inserția senzorului' ,fr: 'Insertion du senseur' - ,ru: 'Установка сенсора' + ,ru: 'Сенсор установлен' ,bg: 'Поставяне на сензора' ,hr: 'Postavljanje senzora' ,sv: 'Sensor insättning' @@ -12656,13 +12634,14 @@ function init() { ,de: 'Sensorstart' ,dk: 'Sensor start' ,ro: 'Pornirea senzorului' - ,ru: 'Запуск сенсора' + ,ru: 'Старт сенсора' ,fr: 'Démarrage du senseur' ,bg: 'Стартиране на сензора' ,hr: 'Pokretanje senzora' ,sv: 'Sensorstart' ,nb: 'Sensorstart' ,fi: 'Sensorin Aloitus' + ,pl: 'Uruchomienie sensora' ,pt: 'Início de sensor' ,es: 'Inicio del sensor' ,sk: 'Štart senzoru' @@ -12670,7 +12649,6 @@ function init() { ,it: 'SAGE - partenza sensore' ,nl: 'Sensor start' ,zh_cn: '启动探头' - ,pl: 'Uruchom sensor' ,tr: 'Sensör başlatma' } ,'days' : { @@ -12733,7 +12711,7 @@ function init() { ,sv: 'För att se denna rapport, klicka på "Visa"' ,bg: 'За да видите тази статистика, натиснете ПОКАЖИ' ,hr: 'Za prikaz ovog izvješća, pritisnite PRIKAŽI na ovom prozoru' - , pl: 'Aby wyświetlić ten raport, naciśnij przycisk POKAŻ w tym widoku' + ,pl: 'Aby wyświetlić ten raport, naciśnij przycisk POKAŻ w tym widoku' ,tr: 'Bu raporu görmek için bu görünümde GÖSTER düğmesine basın.' } ,'AR2 Forecast' : { @@ -12783,7 +12761,7 @@ function init() { ,dk: 'Midlertidigt mål' ,fr: 'Cible temporaire' ,ro: 'Țintă temporară' - ,ru: 'промежуточная цель' + ,ru: 'Временная цель' ,fi: 'Tilapäinen tavoite' ,es: 'Objetivo temporal' ,ko: '임시목표' @@ -12803,7 +12781,7 @@ function init() { ,dk: 'Afslut midlertidigt mål' ,fr: 'Effacer la cible temporaire' ,ro: 'Renunțare la ținta temporară' - ,ru: 'отмена промежуточной цели' + ,ru: 'Отмена временной цели' ,fi: 'Peruuta tilapäinen tavoite' ,es: 'Objetivo temporal cancelado' ,ko: '임시목표취소' @@ -12843,7 +12821,7 @@ function init() { ,dk: 'Profiler' ,fr: 'Profils' ,ro: 'Profile' - ,ru: 'профили' + ,ru: 'Профили' ,fi: 'Profiilit' ,ko: '프로파일' ,it: 'Profili' @@ -12865,7 +12843,7 @@ function init() { ,it: 'Tempo in fluttuazione' ,ro: 'Timp în fluctuație' ,es: 'Tiempo fluctuando' - ,ru: 'время флуктуаций' + ,ru: 'Время флуктуаций' ,nl: 'Tijd met fluctuaties' ,zh_cn: '波动时间' ,sv: 'Tid i fluktation' @@ -12885,7 +12863,7 @@ function init() { ,it: 'Tempo in rapida fluttuazione' ,ro: 'Timp în fluctuație rapidă' ,es: 'Tiempo fluctuando rápido' - ,ru: 'время быстрых флуктуаций' + ,ru: 'Время быстрых флуктуаций' ,nl: 'Tijd met grote fluctuaties' ,zh_cn: '快速波动时间' ,sv: 'Tid i snabb fluktation' @@ -12924,7 +12902,7 @@ function init() { ,fr: 'Filtrer par heures' ,ro: 'Filtrare pe ore' ,es: 'Filtrar por horas' - ,ru: 'почасовой фильтр' + ,ru: 'Почасовая фильтрация' ,nl: 'Filter op uren' ,zh_cn: '按小时过滤' ,sv: 'Filtrera per timme' @@ -12943,7 +12921,7 @@ function init() { ,it: 'Tempo in fluttuazione e Tempo in rapida fluttuazione misurano la % di tempo durante il periodo esaminato, durante il quale la glicemia stà variando velocemente o rapidamente. Bassi valori sono migliori.' ,ro: 'Timpul în fluctuație și timpul în fluctuație rapidă măsoară procentul de timp, din perioada examinată, în care glicemia din sânge a avut o variație relativ rapidă sau rapidă. Valorile mici sunt de preferat.' ,es: 'Tiempo en fluctuación y Tiempo en fluctuación rápida miden el % de tiempo del período exáminado, durante la cual la glucosa en sangre ha estado cambiando relativamente rápido o rápidamente. Valores más bajos son mejores.' - ,ru: 'время флуктуаций и время быстрых флуктуаций означает % времени в рассматриваемый период в течение которого СК менялся относительно быстро или просто быстро. Более низкие значения предпочтительней' + ,ru: 'Время флуктуаций и время быстрых флуктуаций означает % времени в рассматриваемый период в течение которого ГК менялась относительно быстро или просто быстро. Более низкие значения предпочтительней' ,nl: 'Tijd met fluctuaties of grote fluctuaties in % van de geevalueerde periode, waarbij de bloed glucose relatief snel wijzigde.Lagere waarden zijn beter.' ,zh_cn: '在检查期间血糖波动时间和快速波动时间占的时间百分比,在此期间血糖相对快速或快速地变化。百分比值越低越好。' ,sv: 'Tid i fluktuation och tid i snabb fluktuation mäter% av tiden under den undersökta perioden, under vilken blodsockret har förändrats relativt snabbt eller snabbt. Lägre värden är bättre' @@ -12961,7 +12939,7 @@ function init() { ,ko: '전체 일일 변동 평균은 조사된 기간동안 전체 혈당 절대값의 합을 전체 일수로 나눈 값입니다. 낮을수록 좋습니다.' ,it: 'Media Totale Giornaliera Variazioni è la somma dei valori assoluti di tutte le escursioni glicemiche per il periodo esaminato, diviso per il numero di giorni. Bassi valori sono migliori.' ,ro: 'Schimbarea medie totală zilnică este suma valorilor absolute ale tuturor excursiilor glicemice din perioada examinată, împărțite la numărul de zile. Valorile mici sunt de preferat.' - ,ru: 'усредненное ежедневное изменение это сумма абсолютных величин всех отклонений СК в рассматриваемый период, деленное на количество дней. Меньшая величина предпочтительней' + ,ru: 'Усредненное ежедневное изменение это сумма абсолютных величин всех отклонений ГК в рассматриваемый период, деленная на количество дней. Меньшая величина предпочтительней' ,es: 'El cambio medio diario total es la suma de los valores absolutos de todas las glucémias en el período examinado, dividido por el número de días. Mejor valores bajos.' ,nl: 'Gemiddelde veranderingen per dag is een som van alle waardes die uitschieten over de bekeken periode, gedeeld door het aantal dagen in deze periode. Lager is beter.' ,zh_cn: '平均每日总变化是检查期间所有血糖偏移的绝对值之和除以天数。越低越好' @@ -12980,7 +12958,7 @@ function init() { ,ko: '시간당 변동 평균은 조사된 기간 동안 전체 혈당 절대값의 합을 기간의 시간으로 나눈 값입니다.낮을수록 좋습니다.' ,it: 'Media Oraria Variazioni è la somma del valore assoluto di tutte le escursioni glicemiche per il periodo esaminato, diviso per il numero di ore. Bassi valori sono migliori.' ,ro: 'Variația media orară este suma valorilor absolute ale tuturor excursiilor glicemice din perioada examinată, împărțite la numărul de ore din aceeași perioadă. Valorile mici sunt de preferat.' - ,ru: 'усредненное часовое изменение это сумма абсолютных величин всех отклонений СК в рассматриваемый период, деленное на количество часов в этот период. Более низкое предпочтительней' + ,ru: 'Усредненное часовое изменение это сумма абсолютных величин всех отклонений ГК в рассматриваемый период, деленная на количество часов в этот период. Более низкое предпочтительней' ,es: 'El cambio medio por hora, es la suma del valor absoluto de todas las glucemias para el período examinado, dividido por el número de horas en el período. Más bajo es mejor.' ,nl: 'Gemiddelde veranderingen per uur is een som van alle waardes die uitschieten over de bekeken periode, gedeeld door het aantal uur in deze periode. Lager is beter.' ,zh_cn: '平均每小时变化是检查期间所有血糖偏移的绝对值之和除以该期间的小时数。 越低越好' @@ -13001,7 +12979,7 @@ function init() { ,es: 'Variabilidad de la glucosa en sangre y el estado glucémico del paciente es un valor diseñado por Dexcom, más detalles en here.' ,fr: '">ici.' ,ro: '">aici.' - ,ru: '">here.' + ,ru: '">здесь.' ,nl: '">is hier te vinden.' ,zh_cn: '">here.' ,sv: '">här.' @@ -13040,7 +13018,7 @@ function init() { ,ko: '전체 일일 변동 평균' ,it: 'Media Totale Giornaliera Variazioni' ,ro: 'Variația medie totală zilnică' - ,ru: 'усредненное изменение за день' + ,ru: 'Усредненное изменение за день' ,es: 'Variación media total diaria' ,nl: 'Gemiddelde veranderingen per dag' ,zh_cn: '平均每日总变化' @@ -13061,7 +13039,7 @@ function init() { ,it: 'Media Oraria Variazioni' ,ro: 'Variația medie orară' ,es: 'Variación media total por horas' - ,ru: 'усредненное изменение за час' + ,ru: 'Усредненное изменение за час' ,nl: 'Gemiddelde veranderingen per uur' ,zh_cn: '平均每小时变化' ,sv: 'Medelvärde per timme' @@ -13255,7 +13233,579 @@ function init() { , zh_cn: '快速上升' , zh_tw: 'rapidly rising' }, - 'alexaStatus': { + 'virtAsstUnknown': { + bg: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , cs: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , de: 'Dieser Wert ist momentan unbekannt. Prüfe deine Nightscout-Webseite für weitere Details!' + , dk: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , el: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , en: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , es: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , fi: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , fr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , he: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , hr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , it: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ko: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , nb: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , pl: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , pt: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ro: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , nl: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , ru: 'В настоящий момент величина неизвестна. Зайдите на сайт Nightscout.' + , sk: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , sv: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , tr: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , zh_cn: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + , zh_tw: 'That value is unknown at the moment. Please see your Nightscout site for more details.' + }, + 'virtAsstTitleAR2Forecast': { + bg: 'AR2 Forecast' + , cs: 'AR2 Forecast' + , de: 'AR2-Vorhersage' + , dk: 'AR2 Forecast' + , el: 'AR2 Forecast' + , en: 'AR2 Forecast' + , es: 'AR2 Forecast' + , fi: 'AR2 Forecast' + , fr: 'AR2 Forecast' + , he: 'AR2 Forecast' + , hr: 'AR2 Forecast' + , it: 'AR2 Forecast' + , ko: 'AR2 Forecast' + , nb: 'AR2 Forecast' + , pl: 'AR2 Forecast' + , pt: 'AR2 Forecast' + , ro: 'AR2 Forecast' + , nl: 'AR2 Forecast' + , ru: 'Прогноз AR2' + , sk: 'AR2 Forecast' + , sv: 'AR2 Forecast' + , tr: 'AR2 Forecast' + , zh_cn: 'AR2 Forecast' + , zh_tw: 'AR2 Forecast' + }, + 'virtAsstTitleCurrentBasal': { + bg: 'Current Basal' + , cs: 'Current Basal' + , de: 'Aktuelles Basalinsulin' + , dk: 'Current Basal' + , el: 'Current Basal' + , en: 'Current Basal' + , es: 'Current Basal' + , fi: 'Current Basal' + , fr: 'Current Basal' + , he: 'Current Basal' + , hr: 'Current Basal' + , it: 'Current Basal' + , ko: 'Current Basal' + , nb: 'Current Basal' + , pl: 'Current Basal' + , pt: 'Current Basal' + , ro: 'Current Basal' + , nl: 'Current Basal' + , ru: 'Актуальный Базал' + , sk: 'Current Basal' + , sv: 'Current Basal' + , tr: 'Current Basal' + , zh_cn: 'Current Basal' + , zh_tw: 'Current Basal' + }, + 'virtAsstTitleCurrentCOB': { + bg: 'Current COB' + , cs: 'Current COB' + , de: 'Aktuelle Kohlenhydrate' + , dk: 'Current COB' + , el: 'Current COB' + , en: 'Current COB' + , es: 'Current COB' + , fi: 'Current COB' + , fr: 'Current COB' + , he: 'Current COB' + , hr: 'Current COB' + , it: 'Current COB' + , ko: 'Current COB' + , nb: 'Current COB' + , pl: 'Current COB' + , pt: 'Current COB' + , ro: 'Current COB' + , nl: 'Current COB' + , ru: 'АктивнУгл COB' + , sk: 'Current COB' + , sv: 'Current COB' + , tr: 'Current COB' + , zh_cn: 'Current COB' + , zh_tw: 'Current COB' + }, + 'virtAsstTitleCurrentIOB': { + bg: 'Current IOB' + , cs: 'Current IOB' + , de: 'Aktuelles Restinsulin' + , dk: 'Current IOB' + , el: 'Current IOB' + , en: 'Current IOB' + , es: 'Current IOB' + , fi: 'Current IOB' + , fr: 'Current IOB' + , he: 'Current IOB' + , hr: 'Current IOB' + , it: 'Current IOB' + , ko: 'Current IOB' + , nb: 'Current IOB' + , pl: 'Current IOB' + , pt: 'Current IOB' + , ro: 'Current IOB' + , nl: 'Current IOB' + , ru: 'АктИнс IOB' + , sk: 'Current IOB' + , sv: 'Current IOB' + , tr: 'Current IOB' + , zh_cn: 'Current IOB' + , zh_tw: 'Current IOB' + }, + 'virtAsstTitleLaunch': { + bg: 'Welcome to Nightscout' + , cs: 'Welcome to Nightscout' + , de: 'Willkommen bei Nightscout' + , dk: 'Welcome to Nightscout' + , el: 'Welcome to Nightscout' + , en: 'Welcome to Nightscout' + , es: 'Welcome to Nightscout' + , fi: 'Welcome to Nightscout' + , fr: 'Welcome to Nightscout' + , he: 'Welcome to Nightscout' + , hr: 'Welcome to Nightscout' + , it: 'Welcome to Nightscout' + , ko: 'Welcome to Nightscout' + , nb: 'Welcome to Nightscout' + , pl: 'Welcome to Nightscout' + , pt: 'Welcome to Nightscout' + , ro: 'Welcome to Nightscout' + , nl: 'Welcome to Nightscout' + , ru: 'Добро пожаловать в Nightscout' + , sk: 'Welcome to Nightscout' + , sv: 'Welcome to Nightscout' + , tr: 'Welcome to Nightscout' + , zh_cn: 'Welcome to Nightscout' + , zh_tw: 'Welcome to Nightscout' + }, + 'virtAsstTitleLoopForecast': { + bg: 'Loop Forecast' + , cs: 'Loop Forecast' + , de: 'Loop-Vorhersage' + , dk: 'Loop Forecast' + , el: 'Loop Forecast' + , en: 'Loop Forecast' + , es: 'Loop Forecast' + , fi: 'Loop Forecast' + , fr: 'Loop Forecast' + , he: 'Loop Forecast' + , hr: 'Loop Forecast' + , it: 'Loop Forecast' + , ko: 'Loop Forecast' + , nb: 'Loop Forecast' + , pl: 'Loop Forecast' + , pt: 'Loop Forecast' + , ro: 'Loop Forecast' + , nl: 'Loop Forecast' + , ru: 'Прогноз Loop' + , sk: 'Loop Forecast' + , sv: 'Loop Forecast' + , tr: 'Loop Forecast' + , zh_cn: 'Loop Forecast' + , zh_tw: 'Loop Forecast' + }, + 'virtAsstTitleLastLoop': { + bg: 'Last Loop' + , cs: 'Last Loop' + , de: 'Letzter Loop' + , dk: 'Last Loop' + , el: 'Last Loop' + , en: 'Last Loop' + , es: 'Last Loop' + , fi: 'Last Loop' + , fr: 'Last Loop' + , he: 'Last Loop' + , hr: 'Last Loop' + , it: 'Last Loop' + , ko: 'Last Loop' + , nb: 'Last Loop' + , pl: 'Last Loop' + , pt: 'Last Loop' + , ro: 'Last Loop' + , nl: 'Last Loop' + , ru: 'Прошлый Loop' + , sk: 'Last Loop' + , sv: 'Last Loop' + , tr: 'Last Loop' + , zh_cn: 'Last Loop' + , zh_tw: 'Last Loop' + }, + 'virtAsstTitleOpenAPSForecast': { + bg: 'OpenAPS Forecast' + , cs: 'OpenAPS Forecast' + , de: 'OpenAPS-Vorhersage' + , dk: 'OpenAPS Forecast' + , el: 'OpenAPS Forecast' + , en: 'OpenAPS Forecast' + , es: 'OpenAPS Forecast' + , fi: 'OpenAPS Forecast' + , fr: 'OpenAPS Forecast' + , he: 'OpenAPS Forecast' + , hr: 'OpenAPS Forecast' + , it: 'OpenAPS Forecast' + , ko: 'OpenAPS Forecast' + , nb: 'OpenAPS Forecast' + , pl: 'OpenAPS Forecast' + , pt: 'OpenAPS Forecast' + , ro: 'OpenAPS Forecast' + , nl: 'OpenAPS Forecast' + , ru: 'Прогноз OpenAPS' + , sk: 'OpenAPS Forecast' + , sv: 'OpenAPS Forecast' + , tr: 'OpenAPS Forecast' + , zh_cn: 'OpenAPS Forecast' + , zh_tw: 'OpenAPS Forecast' + }, + 'virtAsstTitlePumpReservoir': { + bg: 'Insulin Remaining' + , cs: 'Insulin Remaining' + , de: 'Verbleibendes Insulin' + , dk: 'Insulin Remaining' + , el: 'Insulin Remaining' + , en: 'Insulin Remaining' + , es: 'Insulin Remaining' + , fi: 'Insulin Remaining' + , fr: 'Insulin Remaining' + , he: 'Insulin Remaining' + , hr: 'Insulin Remaining' + , it: 'Insulin Remaining' + , ko: 'Insulin Remaining' + , nb: 'Insulin Remaining' + , pl: 'Insulin Remaining' + , pt: 'Insulin Remaining' + , ro: 'Insulin Remaining' + , nl: 'Insulin Remaining' + , ru: 'Осталось Инсулина' + , sk: 'Insulin Remaining' + , sv: 'Insulin Remaining' + , tr: 'Insulin Remaining' + , zh_cn: 'Insulin Remaining' + , zh_tw: 'Insulin Remaining' + }, + 'virtAsstTitlePumpBattery': { + bg: 'Pump Battery' + , cs: 'Pump Battery' + , de: 'Pumpenbatterie' + , dk: 'Pump Battery' + , el: 'Pump Battery' + , en: 'Pump Battery' + , es: 'Pump Battery' + , fi: 'Pump Battery' + , fr: 'Pump Battery' + , he: 'Pump Battery' + , hr: 'Pump Battery' + , it: 'Pump Battery' + , ko: 'Pump Battery' + , nb: 'Pump Battery' + , pl: 'Pump Battery' + , pt: 'Pump Battery' + , ro: 'Pump Battery' + , nl: 'Pump Battery' + , ru: 'Батарея помпы' + , sk: 'Pump Battery' + , sv: 'Pump Battery' + , tr: 'Pump Battery' + , zh_cn: 'Pump Battery' + , zh_tw: 'Pump Battery' + }, + 'virtAsstTitleRawBG': { + bg: 'Current Raw BG' + , cs: 'Current Raw BG' + , de: 'Aktueller Blutzucker-Rohwert' + , dk: 'Current Raw BG' + , el: 'Current Raw BG' + , en: 'Current Raw BG' + , es: 'Current Raw BG' + , fi: 'Current Raw BG' + , fr: 'Current Raw BG' + , he: 'Current Raw BG' + , hr: 'Current Raw BG' + , it: 'Current Raw BG' + , ko: 'Current Raw BG' + , nb: 'Current Raw BG' + , pl: 'Current Raw BG' + , pt: 'Current Raw BG' + , ro: 'Current Raw BG' + , nl: 'Current Raw BG' + , ru: 'Актуальн RAW ГК ' + , sk: 'Current Raw BG' + , sv: 'Current Raw BG' + , tr: 'Current Raw BG' + , zh_cn: 'Current Raw BG' + , zh_tw: 'Current Raw BG' + }, + 'virtAsstTitleUploaderBattery': { + bg: 'Uploader Battery' + , cs: 'Uploader Battery' + , de: 'Uploader Batterie' + , dk: 'Uploader Battery' + , el: 'Uploader Battery' + , en: 'Uploader Battery' + , es: 'Uploader Battery' + , fi: 'Uploader Battery' + , fr: 'Uploader Battery' + , he: 'Uploader Battery' + , hr: 'Uploader Battery' + , it: 'Uploader Battery' + , ko: 'Uploader Battery' + , nb: 'Uploader Battery' + , pl: 'Uploader Battery' + , pt: 'Uploader Battery' + , ro: 'Uploader Battery' + , nl: 'Uploader Battery' + , ru: 'Батарея загрузчика' + , sk: 'Uploader Battery' + , sv: 'Uploader Battery' + , tr: 'Uploader Battery' + , zh_cn: 'Uploader Battery' + , zh_tw: 'Uploader Battery' + }, + 'virtAsstTitleCurrentBG': { + bg: 'Current BG' + , cs: 'Current BG' + , de: 'Aktueller Blutzucker' + , dk: 'Current BG' + , el: 'Current BG' + , en: 'Current BG' + , es: 'Current BG' + , fi: 'Current BG' + , fr: 'Current BG' + , he: 'Current BG' + , hr: 'Current BG' + , it: 'Current BG' + , ko: 'Current BG' + , nb: 'Current BG' + , pl: 'Current BG' + , pt: 'Current BG' + , ro: 'Current BG' + , nl: 'Current BG' + , ru: 'Актуальная ГК' + , sk: 'Current BG' + , sv: 'Current BG' + , tr: 'Current BG' + , zh_cn: 'Current BG' + , zh_tw: 'Current BG' + }, + 'virtAsstTitleFullStatus': { + bg: 'Full Status' + , cs: 'Full Status' + , de: 'Gesamtstatus' + , dk: 'Full Status' + , el: 'Full Status' + , en: 'Full Status' + , es: 'Full Status' + , fi: 'Full Status' + , fr: 'Full Status' + , he: 'Full Status' + , hr: 'Full Status' + , it: 'Full Status' + , ko: 'Full Status' + , nb: 'Full Status' + , pl: 'Full Status' + , pt: 'Full Status' + , ro: 'Full Status' + , nl: 'Full Status' + , ru: 'Полная информация о статусе' + , sk: 'Full Status' + , sv: 'Full Status' + , tr: 'Full Status' + , zh_cn: 'Full Status' + , zh_tw: 'Full Status' + }, + 'virtAsstTitleCGMMode': { + bg: 'CGM Mode' + , cs: 'CGM Mode' + , de: 'CGM Mode' + , dk: 'CGM Mode' + , el: 'CGM Mode' + , en: 'CGM Mode' + , es: 'CGM Mode' + , fi: 'CGM Mode' + , fr: 'CGM Mode' + , he: 'CGM Mode' + , hr: 'CGM Mode' + , it: 'CGM Mode' + , ko: 'CGM Mode' + , nb: 'CGM Mode' + , pl: 'CGM Mode' + , pt: 'CGM Mode' + , ro: 'CGM Mode' + , nl: 'CGM Mode' + , ru: 'CGM Mode' + , sk: 'CGM Mode' + , sv: 'CGM Mode' + , tr: 'CGM Mode' + , zh_cn: 'CGM Mode' + , zh_tw: 'CGM Mode' + }, + 'virtAsstTitleCGMStatus': { + bg: 'CGM Status' + , cs: 'CGM Status' + , de: 'CGM Status' + , dk: 'CGM Status' + , el: 'CGM Status' + , en: 'CGM Status' + , es: 'CGM Status' + , fi: 'CGM Status' + , fr: 'CGM Status' + , he: 'CGM Status' + , hr: 'CGM Status' + , it: 'CGM Status' + , ko: 'CGM Status' + , nb: 'CGM Status' + , pl: 'CGM Status' + , pt: 'CGM Status' + , ro: 'CGM Status' + , nl: 'CGM Status' + , ru: 'CGM Status' + , sk: 'CGM Status' + , sv: 'CGM Status' + , tr: 'CGM Status' + , zh_cn: 'CGM Status' + , zh_tw: 'CGM Status' + }, + 'virtAsstTitleCGMSessionAge': { + bg: 'CGM Session Age' + , cs: 'CGM Session Age' + , de: 'CGM Session Age' + , dk: 'CGM Session Age' + , el: 'CGM Session Age' + , en: 'CGM Session Age' + , es: 'CGM Session Age' + , fi: 'CGM Session Age' + , fr: 'CGM Session Age' + , he: 'CGM Session Age' + , hr: 'CGM Session Age' + , it: 'CGM Session Age' + , ko: 'CGM Session Age' + , nb: 'CGM Session Age' + , pl: 'CGM Session Age' + , pt: 'CGM Session Age' + , ro: 'CGM Session Age' + , nl: 'CGM Session Age' + , ru: 'CGM Session Age' + , sk: 'CGM Session Age' + , sv: 'CGM Session Age' + , tr: 'CGM Session Age' + , zh_cn: 'CGM Session Age' + , zh_tw: 'CGM Session Age' + }, + 'virtAsstTitleCGMTxStatus': { + bg: 'CGM Transmitter Status' + , cs: 'CGM Transmitter Status' + , de: 'CGM Transmitter Status' + , dk: 'CGM Transmitter Status' + , el: 'CGM Transmitter Status' + , en: 'CGM Transmitter Status' + , es: 'CGM Transmitter Status' + , fi: 'CGM Transmitter Status' + , fr: 'CGM Transmitter Status' + , he: 'CGM Transmitter Status' + , hr: 'CGM Transmitter Status' + , it: 'CGM Transmitter Status' + , ko: 'CGM Transmitter Status' + , nb: 'CGM Transmitter Status' + , pl: 'CGM Transmitter Status' + , pt: 'CGM Transmitter Status' + , ro: 'CGM Transmitter Status' + , nl: 'CGM Transmitter Status' + , ru: 'CGM Transmitter Status' + , sk: 'CGM Transmitter Status' + , sv: 'CGM Transmitter Status' + , tr: 'CGM Transmitter Status' + , zh_cn: 'CGM Transmitter Status' + , zh_tw: 'CGM Transmitter Status' + }, + 'virtAsstTitleCGMTxAge': { + bg: 'CGM Transmitter Age' + , cs: 'CGM Transmitter Age' + , de: 'CGM Transmitter Age' + , dk: 'CGM Transmitter Age' + , el: 'CGM Transmitter Age' + , en: 'CGM Transmitter Age' + , es: 'CGM Transmitter Age' + , fi: 'CGM Transmitter Age' + , fr: 'CGM Transmitter Age' + , he: 'CGM Transmitter Age' + , hr: 'CGM Transmitter Age' + , it: 'CGM Transmitter Age' + , ko: 'CGM Transmitter Age' + , nb: 'CGM Transmitter Age' + , pl: 'CGM Transmitter Age' + , pt: 'CGM Transmitter Age' + , ro: 'CGM Transmitter Age' + , nl: 'CGM Transmitter Age' + , ru: 'CGM Transmitter Age' + , sk: 'CGM Transmitter Age' + , sv: 'CGM Transmitter Age' + , tr: 'CGM Transmitter Age' + , zh_cn: 'CGM Transmitter Age' + , zh_tw: 'CGM Transmitter Age' + }, + 'virtAsstTitleCGMNoise': { + bg: 'CGM Noise' + , cs: 'CGM Noise' + , de: 'CGM Noise' + , dk: 'CGM Noise' + , el: 'CGM Noise' + , en: 'CGM Noise' + , es: 'CGM Noise' + , fi: 'CGM Noise' + , fr: 'CGM Noise' + , he: 'CGM Noise' + , hr: 'CGM Noise' + , it: 'CGM Noise' + , ko: 'CGM Noise' + , nb: 'CGM Noise' + , pl: 'CGM Noise' + , pt: 'CGM Noise' + , ro: 'CGM Noise' + , nl: 'CGM Noise' + , ru: 'CGM Noise' + , sk: 'CGM Noise' + , sv: 'CGM Noise' + , tr: 'CGM Noise' + , zh_cn: 'CGM Noise' + , zh_tw: 'CGM Noise' + }, + 'virtAsstTitleDelta': { + bg: 'Blood Glucose Delta' + , cs: 'Blood Glucose Delta' + , de: 'Blutzucker-Delta' + , dk: 'Blood Glucose Delta' + , el: 'Blood Glucose Delta' + , en: 'Blood Glucose Delta' + , es: 'Blood Glucose Delta' + , fi: 'Blood Glucose Delta' + , fr: 'Blood Glucose Delta' + , he: 'Blood Glucose Delta' + , hr: 'Blood Glucose Delta' + , it: 'Blood Glucose Delta' + , ko: 'Blood Glucose Delta' + , nb: 'Blood Glucose Delta' + , pl: 'Blood Glucose Delta' + , pt: 'Blood Glucose Delta' + , ro: 'Blood Glucose Delta' + , nl: 'Blood Glucose Delta' + , ru: 'Дельта ГК' + , sk: 'Blood Glucose Delta' + , sv: 'Blood Glucose Delta' + , tr: 'Blood Glucose Delta' + , zh_cn: 'Blood Glucose Delta' + , zh_tw: 'Blood Glucose Delta' + }, + 'virtAsstStatus': { bg: '%1 and %2 as of %3.' , cs: '%1 %2 čas %3.' , de: '%1 und bis %3 %2.' @@ -13281,7 +13831,7 @@ function init() { , zh_cn: '%1 和 %2 到 %3.' , zh_tw: '%1 and %2 as of %3.' }, - 'alexaBasal': { + 'virtAsstBasal': { bg: '%1 současný bazál je %2 jednotek za hodinu' , cs: '%1 current basal is %2 units per hour' , de: '%1 aktuelle Basalrate ist %2 Einheiten je Stunde' @@ -13307,7 +13857,7 @@ function init() { , zh_cn: '%1 当前基础率是 %2 U/小时' , zh_tw: '%1 current basal is %2 units per hour' }, - 'alexaBasalTemp': { + 'virtAsstBasalTemp': { bg: '%1 dočasný bazál %2 jednotek za hodinu skončí %3' , cs: '%1 temp basal of %2 units per hour will end %3' , de: '%1 temporäre Basalrate von %2 Einheiten endet %3' @@ -13333,7 +13883,7 @@ function init() { , zh_cn: '%1 临时基础率 %2 U/小时将会在 %3结束' , zh_tw: '%1 temp basal of %2 units per hour will end %3' }, - 'alexaIob': { + 'virtAsstIob': { bg: 'a máte %1 jednotek aktivního inzulínu.' , cs: 'and you have %1 insulin on board.' , de: 'und du hast %1 Insulin wirkend.' @@ -13359,33 +13909,33 @@ function init() { , zh_cn: '并且你有 %1 的活性胰岛素.' , zh_tw: 'and you have %1 insulin on board.' }, - 'alexaIobIntent': { - bg: 'Máte %1 jednotek aktivního inzulínu' - , cs: 'You have %1 insulin on board' - , de: 'Du hast noch %1 Insulin wirkend' - , dk: 'Du har %1 insulin i kroppen' - , el: 'You have %1 insulin on board' - , en: 'You have %1 insulin on board' - , es: 'Tienes %1 insulina activa' - , fi: 'Sinulla on %1 aktiivista insuliinia' - , fr: 'You have %1 insulin on board' - , he: 'You have %1 insulin on board' - , hr: 'You have %1 insulin on board' - , it: 'Tu hai %1 insulina attiva' - , ko: 'You have %1 insulin on board' - , nb: 'You have %1 insulin on board' - , pl: 'Masz %1 aktywnej insuliny' - , pt: 'You have %1 insulin on board' - , ro: 'Aveți %1 insulină activă' - , ru: 'вы имеете %1 инсулина в организме' - , sk: 'You have %1 insulin on board' - , sv: 'You have %1 insulin on board' - , nl: 'You have %1 insulin on board' - , tr: 'Sizde %1 aktif insülin var' - , zh_cn: '你有 %1 的活性胰岛素' - , zh_tw: 'You have %1 insulin on board' - }, - 'alexaIobUnits': { + 'virtAsstIobIntent': { + bg: 'Máte %1 jednotek aktivního inzulínu' + , cs: 'You have %1 insulin on board' + , de: 'Du hast noch %1 Insulin wirkend' + , dk: 'Du har %1 insulin i kroppen' + , el: 'You have %1 insulin on board' + , en: 'You have %1 insulin on board' + , es: 'Tienes %1 insulina activa' + , fi: 'Sinulla on %1 aktiivista insuliinia' + , fr: 'You have %1 insulin on board' + , he: 'You have %1 insulin on board' + , hr: 'You have %1 insulin on board' + , it: 'Tu hai %1 insulina attiva' + , ko: 'You have %1 insulin on board' + , nb: 'You have %1 insulin on board' + , pl: 'Masz %1 aktywnej insuliny' + , pt: 'You have %1 insulin on board' + , ro: 'Aveți %1 insulină activă' + , ru: 'вы имеете %1 инсулина в организме' + , sk: 'You have %1 insulin on board' + , sv: 'You have %1 insulin on board' + , nl: 'You have %1 insulin on board' + , tr: 'Sizde %1 aktif insülin var' + , zh_cn: '你有 %1 的活性胰岛素' + , zh_tw: 'You have %1 insulin on board' + }, + 'virtAsstIobUnits': { bg: '%1 units of' , cs: '%1 jednotek' , de: 'noch %1 Einheiten' @@ -13411,7 +13961,33 @@ function init() { , zh_cn: '%1 单位' , zh_tw: '%1 units of' }, - 'alexaPreamble': { + 'virtAsstLaunch': { + bg: 'What would you like to check on Nightscout?' + , cs: 'What would you like to check on Nightscout?' + , de: 'Was möchtest du von Nightscout wissen?' + , dk: 'What would you like to check on Nightscout?' + , el: 'What would you like to check on Nightscout?' + , en: 'What would you like to check on Nightscout?' + , es: 'What would you like to check on Nightscout?' + , fi: 'What would you like to check on Nightscout?' + , fr: 'What would you like to check on Nightscout?' + , he: 'What would you like to check on Nightscout?' + , hr: 'What would you like to check on Nightscout?' + , it: 'What would you like to check on Nightscout?' + , ko: 'What would you like to check on Nightscout?' + , nb: 'What would you like to check on Nightscout?' + , pl: 'What would you like to check on Nightscout?' + , pt: 'What would you like to check on Nightscout?' + , ro: 'What would you like to check on Nightscout?' + , nl: 'What would you like to check on Nightscout?' + , ru: 'Что проверить в Nightscout?' + , sk: 'What would you like to check on Nightscout?' + , sv: 'What would you like to check on Nightscout?' + , tr: 'What would you like to check on Nightscout?' + , zh_cn: 'What would you like to check on Nightscout?' + , zh_tw: 'What would you like to check on Nightscout?' + }, + 'virtAsstPreamble': { bg: 'Your' , cs: 'Vaše' , de: 'Deine' @@ -13437,7 +14013,7 @@ function init() { , zh_cn: '你的' , zh_tw: 'Your' }, - 'alexaPreamble3person': { + 'virtAsstPreamble3person': { bg: '%1 has a ' , cs: '%1 má ' , de: '%1 hat eine' @@ -13463,7 +14039,7 @@ function init() { , zh_cn: '%1 有一个 ' , zh_tw: '%1 has a ' }, - 'alexaNoInsulin': { + 'virtAsstNoInsulin': { bg: 'no' , cs: 'žádný' , de: 'kein' @@ -13489,118 +14065,186 @@ function init() { , zh_cn: '否' , zh_tw: 'no' }, - 'alexaUploadBattery': { - bg: 'Your uploader battery is at %1' - ,cs: 'Baterie mobilu má %1' - , en: 'Your uploader battery is at %1' - , hr: 'Your uploader battery is at %1' - , de: 'Der Akku deines Uploader Handys ist bei %1' - , dk: 'Din uploaders batteri er %1' - , ko: 'Your uploader battery is at %1' - , nl: 'De batterij van je mobiel is bij %l' - ,zh_cn: '你的手机电池电量是 %1 ' - , sv: 'Din uppladdares batteri är %1' - , fi: 'Lähettimen paristoa jäljellä %1' - , ro: 'Bateria uploaderului este la %1' - , pl: 'Twoja bateria ma %1' - , ru: 'батарея загрузчика %1' - , tr: 'Yükleyici piliniz %1' - }, - 'alexaReservoir': { - bg: 'You have %1 units remaining' - , cs: 'V zásobníku zbývá %1 jednotek' - , en: 'You have %1 units remaining' - , hr: 'You have %1 units remaining' - , de: 'Du hast %1 Einheiten übrig' - , dk: 'Du har %1 enheder tilbage' - , ko: 'You have %1 units remaining' - , nl: 'Je hebt nog %l eenheden in je reservoir' - ,zh_cn: '你剩余%1 U的胰岛素' - , sv: 'Du har %1 enheter kvar' - , fi: '%1 yksikköä insuliinia jäljellä' - , ro: 'Mai aveți %1 unități rămase' - , pl: 'W zbiorniku pozostało %1 jednostek' - , ru: 'остается %1 ед' - , tr: '%1 birim kaldı' - }, - 'alexaPumpBattery': { - bg: 'Your pump battery is at %1 %2' - , cs: 'Baterie v pumpě má %1 %2' - , en: 'Your pump battery is at %1 %2' - , hr: 'Your pump battery is at %1 %2' - , de: 'Der Batteriestand deiner Pumpe ist bei %1 %2' - , dk: 'Din pumpes batteri er %1 %2' - , ko: 'Your pump battery is at %1 %2' - , nl: 'Je pomp batterij is bij %1 %2' - ,zh_cn: '你的泵电池电量是%1 %2' - , sv: 'Din pumps batteri är %1 %2' - , fi: 'Pumppu on %1 %2' - , ro: 'Bateria pompei este la %1 %2' - , pl: 'Bateria pompy jest w %1 %2' - , ru: 'батарея помпы %1 %2' - , tr: 'Pompa piliniz %1 %2' - }, - 'alexaLastLoop': { - bg: 'The last successful loop was %1' - , cs: 'Poslední úšpěšné provedení smyčky %1' - , en: 'The last successful loop was %1' - , hr: 'The last successful loop was %1' - , de: 'Der letzte erfolgreiche Loop war %1' - , dk: 'Seneste successfulde loop var %1' - , ko: 'The last successful loop was %1' - , nl: 'De meest recente goede loop was %1' - ,zh_cn: '最后一次成功闭环的是在%1' - , sv: 'Senaste lyckade loop var %1' - , fi: 'Viimeisin onnistunut loop oli %1' - , ro: 'Ultima decizie loop implementată cu succes a fost %1' - , pl: 'Ostatnia pomyślna pętla była %1' - , ru: 'недавний успешный цикл был %1' - , tr: 'Son başarılı döngü %1 oldu' - }, - 'alexaLoopNotAvailable': { + 'virtAsstUploadBattery': { + bg: 'Your uploader battery is at %1' + , cs: 'Baterie mobilu má %1' + , en: 'Your uploader battery is at %1' + , hr: 'Your uploader battery is at %1' + , de: 'Der Akku deines Uploader-Handys ist bei %1' + , dk: 'Din uploaders batteri er %1' + , ko: 'Your uploader battery is at %1' + , nl: 'De batterij van je mobiel is bij %l' + , zh_cn: '你的手机电池电量是 %1 ' + , sv: 'Din uppladdares batteri är %1' + , fi: 'Lähettimen paristoa jäljellä %1' + , ro: 'Bateria uploaderului este la %1' + , pl: 'Twoja bateria ma %1' + , ru: 'батарея загрузчика %1' + , tr: 'Yükleyici piliniz %1' + }, + 'virtAsstReservoir': { + bg: 'You have %1 units remaining' + , cs: 'V zásobníku zbývá %1 jednotek' + , en: 'You have %1 units remaining' + , hr: 'You have %1 units remaining' + , de: 'Du hast %1 Einheiten übrig' + , dk: 'Du har %1 enheder tilbage' + , ko: 'You have %1 units remaining' + , nl: 'Je hebt nog %l eenheden in je reservoir' + , zh_cn: '你剩余%1 U的胰岛素' + , sv: 'Du har %1 enheter kvar' + , fi: '%1 yksikköä insuliinia jäljellä' + , ro: 'Mai aveți %1 unități rămase' + , pl: 'W zbiorniku pozostało %1 jednostek' + , ru: 'остается %1 ед' + , tr: '%1 birim kaldı' + }, + 'virtAsstPumpBattery': { + bg: 'Your pump battery is at %1 %2' + , cs: 'Baterie v pumpě má %1 %2' + , en: 'Your pump battery is at %1 %2' + , hr: 'Your pump battery is at %1 %2' + , de: 'Der Batteriestand deiner Pumpe ist bei %1 %2' + , dk: 'Din pumpes batteri er %1 %2' + , ko: 'Your pump battery is at %1 %2' + , nl: 'Je pomp batterij is bij %1 %2' + , zh_cn: '你的泵电池电量是%1 %2' + , sv: 'Din pumps batteri är %1 %2' + , fi: 'Pumppu on %1 %2' + , ro: 'Bateria pompei este la %1 %2' + , pl: 'Bateria pompy jest w %1 %2' + , ru: 'батарея помпы %1 %2' + , tr: 'Pompa piliniz %1 %2' + }, + 'virtAsstUploaderBattery': { + bg: 'Your uploader battery is at %1' + , cs: 'Your uploader battery is at %1' + , en: 'Your uploader battery is at %1' + , hr: 'Your uploader battery is at %1' + , de: 'Der Akku deines Uploader-Handys ist bei %1' + , dk: 'Your uploader battery is at %1' + , ko: 'Your uploader battery is at %1' + , nl: 'Your uploader battery is at %1' + , zh_cn: 'Your uploader battery is at %1' + , sv: 'Your uploader battery is at %1' + , fi: 'Your uploader battery is at %1' + , ro: 'Your uploader battery is at %1' + , pl: 'Your uploader battery is at %1' + , ru: 'Батарея загрузчика %1' + , tr: 'Your uploader battery is at %1' + }, + 'virtAsstLastLoop': { + bg: 'The last successful loop was %1' + , cs: 'Poslední úšpěšné provedení smyčky %1' + , en: 'The last successful loop was %1' + , hr: 'The last successful loop was %1' + , de: 'Der letzte erfolgreiche Loop war %1' + , dk: 'Seneste successfulde loop var %1' + , ko: 'The last successful loop was %1' + , nl: 'De meest recente goede loop was %1' + , zh_cn: '最后一次成功闭环的是在%1' + , sv: 'Senaste lyckade loop var %1' + , fi: 'Viimeisin onnistunut loop oli %1' + , ro: 'Ultima decizie loop implementată cu succes a fost %1' + , pl: 'Ostatnia pomyślna pętla była %1' + , ru: 'недавний успешный цикл был %1' + , tr: 'Son başarılı döngü %1 oldu' + }, + 'virtAsstLoopNotAvailable': { bg: 'Loop plugin does not seem to be enabled' , cs: 'Plugin smyčka není patrně povolený' , en: 'Loop plugin does not seem to be enabled' , hr: 'Loop plugin does not seem to be enabled' - , de: 'Das Loop Plugin scheint nicht aktiviert zu sein' + , de: 'Das Loop-Plugin scheint nicht aktiviert zu sein' , dk: 'Loop plugin lader ikke til at være slået til' , ko: 'Loop plugin does not seem to be enabled' , nl: 'De Loop plugin is niet geactiveerd' - ,zh_cn: 'Loop插件看起来没有被启用' + , zh_cn: 'Loop插件看起来没有被启用' , sv: 'Loop plugin verkar inte vara aktiverad' , fi: 'Loop plugin ei ole aktivoitu' , ro: 'Extensia loop pare a fi dezactivată' , pl: 'Plugin Loop prawdopodobnie nie jest włączona' - , ru: 'плагин ЗЦ Loop не активирован ' + , ru: 'Расширение ЗЦ Loop не активировано' , tr: 'Döngü eklentisi etkin görünmüyor' }, - 'alexaLoopForecast': { - bg: 'According to the loop forecast you are expected to be %1 over the next %2' - , cs: 'Podle přepovědi smyčky je očekávána glykémie %1 během následujících %2' - , en: 'According to the loop forecast you are expected to be %1 over the next %2' - , hr: 'According to the loop forecast you are expected to be %1 over the next %2' - , de: 'Entsprechend der Loop Vorhersage landest du bei %1 während der nächsten %2' - , dk: 'Ifølge Loops forudsigelse forventes du at blive %1 i den næste %2' - , ko: 'According to the loop forecast you are expected to be %1 over the next %2' - , nl: 'Volgens de Loop voorspelling is je waarde %1 over de volgnede %2' - ,zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是%1' - , sv: 'Enligt Loops förutsägelse förväntas du bli %1 inom %2' - , fi: 'Ennusteen mukaan olet %1 seuraavan %2 ajan' - , ro: 'Potrivit previziunii date de loop se estiemază %1 pentru următoarele %2' - , pl: 'Zgodnie z prognozą pętli, glikemia %1 będzie podczas następnego %2' - , ru: 'по прогнозу алгоритма ЗЦ ожидается %1 за последующие %2' - , tr: 'Döngü tahminine göre sonraki %2 ye göre %1 olması bekleniyor' + 'virtAsstLoopForecastAround': { + bg: 'According to the loop forecast you are expected to be around %1 over the next %2' + , cs: 'Podle přepovědi smyčky je očekávána glykémie around %1 během následujících %2' + , en: 'According to the loop forecast you are expected to be around %1 over the next %2' + , hr: 'According to the loop forecast you are expected to be around %1 over the next %2' + , de: 'Entsprechend der Loop-Vorhersage landest in den nächsten %2 bei %1' + , dk: 'Ifølge Loops forudsigelse forventes du at blive around %1 i den næste %2' + , ko: 'According to the loop forecast you are expected to be around %1 over the next %2' + , nl: 'Volgens de Loop voorspelling is je waarde around %1 over de volgnede %2' + , zh_cn: '根据loop的预测,在接下来的%2你的血糖将会是around %1' + , sv: 'Enligt Loops förutsägelse förväntas du bli around %1 inom %2' + , fi: 'Ennusteen mukaan olet around %1 seuraavan %2 ajan' + , ro: 'Potrivit previziunii date de loop se estiemază around %1 pentru următoarele %2' + , pl: 'Zgodnie z prognozą pętli, glikemia around %1 będzie podczas następnego %2' + , ru: 'по прогнозу алгоритма ЗЦ ожидается около %1 за последующие %2' + , tr: 'Döngü tahminine göre sonraki %2 ye göre around %1 olması bekleniyor' + }, + 'virtAsstLoopForecastBetween': { + bg: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , cs: 'Podle přepovědi smyčky je očekávána glykémie between %1 and %2 během následujících %3' + , en: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , hr: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , de: 'Entsprechend der Loop-Vorhersage landest du zwischen %1 und %2 während der nächsten %3' + , dk: 'Ifølge Loops forudsigelse forventes du at blive between %1 and %2 i den næste %3' + , ko: 'According to the loop forecast you are expected to be between %1 and %2 over the next %3' + , nl: 'Volgens de Loop voorspelling is je waarde between %1 and %2 over de volgnede %3' + , zh_cn: '根据loop的预测,在接下来的%3你的血糖将会是between %1 and %2' + , sv: 'Enligt Loops förutsägelse förväntas du bli between %1 and %2 inom %3' + , fi: 'Ennusteen mukaan olet between %1 and %2 seuraavan %3 ajan' + , ro: 'Potrivit previziunii date de loop se estiemază between %1 and %2 pentru următoarele %3' + , pl: 'Zgodnie z prognozą pętli, glikemia between %1 and %2 będzie podczas następnego %3' + , ru: 'по прогнозу алгоритма ЗЦ ожидается между %1 и %2 за последующие %3' + , tr: 'Döngü tahminine göre sonraki %3 ye göre between %1 and %2 olması bekleniyor' }, - 'alexaForecastUnavailable': { + 'virtAsstAR2ForecastAround': { + bg: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , cs: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , en: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , hr: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , de: 'Entsprechend der AR2-Vorhersage landest du in %2 bei %1' + , dk: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ko: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , nl: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , zh_cn: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , sv: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , fi: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ro: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , pl: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + , ru: 'По прогнозу AR2 ожидается около %1 в следующие %2' + , tr: 'According to the AR2 forecast you are expected to be around %1 over the next %2' + }, + 'virtAsstAR2ForecastBetween': { + bg: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , cs: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , en: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , hr: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , de: 'Entsprechend der AR2-Vorhersage landest du in %3 zwischen %1 and %2' + , dk: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ko: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , nl: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , zh_cn: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , sv: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , fi: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ro: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , pl: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + , ru: 'По прогнозу AR2 ожидается между %1 и %2 в следующие %3' + , tr: 'According to the AR2 forecast you are expected to be between %1 and %2 over the next %3' + }, + 'virtAsstForecastUnavailable': { bg: 'Unable to forecast with the data that is available' , cs: 'S dostupnými daty přepověď není možná' , en: 'Unable to forecast with the data that is available' , hr: 'Unable to forecast with the data that is available' - , de: 'Mit den verfügbaren Daten ist eine Loop Vorhersage nicht möglich' + , de: 'Mit den verfügbaren Daten ist eine Loop-Vorhersage nicht möglich' , dk: 'Det er ikke muligt at forudsige md de tilgængelige data' , ko: 'Unable to forecast with the data that is available' , nl: 'Niet mogelijk om een voorspelling te doen met de data die beschikbaar is' - ,zh_cn: '血糖数据不可用,无法预测未来走势' + , zh_cn: '血糖数据不可用,无法预测未来走势' , sv: 'Förutsägelse ej möjlig med tillgänlig data' , fi: 'Ennusteet eivät ole toiminnassa puuttuvan tiedon vuoksi' , ro: 'Estimarea este imposibilă pe baza datelor disponibile' @@ -13608,14 +14252,14 @@ function init() { , ru: 'прогноз при таких данных невозможен' , tr: 'Mevcut verilerle tahmin edilemedi' }, - 'alexaRawBG': { - en: 'Your raw bg is %1' + 'virtAsstRawBG': { + en: 'Your raw bg is %1' , cs: 'Raw glykémie je %1' , de: 'Dein Rohblutzucker ist %1' , dk: 'Dit raw blodsukker er %1' , ko: 'Your raw bg is %1' , nl: 'Je raw bloedwaarde is %1' - ,zh_cn: '你的血糖是 %1' + , zh_cn: '你的血糖是 %1' , sv: 'Ditt raw blodsocker är %1' , fi: 'Suodattamaton verensokeriarvo on %1' , ro: 'Glicemia brută este %1' @@ -13625,39 +14269,368 @@ function init() { , ru: 'ваши необработанные данные RAW %1' , tr: 'Ham kan şekeriniz %1' }, - 'alexaOpenAPSForecast': { + 'virtAsstOpenAPSForecast': { en: 'The OpenAPS Eventual BG is %1' , cs: 'OpenAPS Eventual BG je %1' , de: 'Der von OpenAPS vorhergesagte Blutzucker ist %1' , dk: 'OpenAPS forventet blodsukker er %1' , ko: 'The OpenAPS Eventual BG is %1' , nl: 'OpenAPS uiteindelijke bloedglucose van %1' - ,zh_cn: 'OpenAPS 预测最终血糖是 %1' + , zh_cn: 'OpenAPS 预测最终血糖是 %1' , sv: 'OpenAPS slutgiltigt blodsocker är %1' , fi: 'OpenAPS verensokeriarvio on %1' , ro: 'Glicemia estimată de OpenAPS este %1' - ,bg: 'The OpenAPS Eventual BG is %1' - ,hr: 'The OpenAPS Eventual BG is %1' + , bg: 'The OpenAPS Eventual BG is %1' + , hr: 'The OpenAPS Eventual BG is %1' , pl: 'Glikemia prognozowana przez OpenAPS wynosi %1' , ru: 'OpenAPS прогнозирует ваш СК как %1 ' , tr: 'OpenAPS tarafından tahmin edilen kan şekeri %1' }, - 'alexaCOB': { - en: '%1 %2 carbohydrates on board' - , cs: '%1 %2 aktivních sachridů' - , de: '%1 %2 Gramm Kohlenhydrate wirkend.' - , dk: '%1 %2 gram aktive kulhydrater' - , ko: '%1 %2 carbohydrates on board' - , nl: '%1 %2 actieve koolhydraten' - ,zh_cn: '%1 %2 活性碳水化合物' - , sv: '%1 %2 gram aktiva kolhydrater' - , fi: '%1 %2 aktiivista hiilihydraattia' - , ro: '%1 %2 carbohidrați activi în corp' - ,bg: '%1 %2 carbohydrates on board' - ,hr: '%1 %2 carbohydrates on board' - , pl: '%1 %2 aktywnych węglowodanów' - , ru: '%1 $2 активных углеводов' - , tr: '%1 %2 aktif karbonhidrat' + 'virtAsstCob3person': { + bg: '%1 has %2 carbohydrates on board' + , cs: '%1 has %2 carbohydrates on board' + , de: '%1 hat %2 Kohlenhydrate wirkend' + , dk: '%1 has %2 carbohydrates on board' + , el: '%1 has %2 carbohydrates on board' + , en: '%1 has %2 carbohydrates on board' + , es: '%1 has %2 carbohydrates on board' + , fi: '%1 has %2 carbohydrates on board' + , fr: '%1 has %2 carbohydrates on board' + , he: '%1 has %2 carbohydrates on board' + , hr: '%1 has %2 carbohydrates on board' + , it: '%1 has %2 carbohydrates on board' + , ko: '%1 has %2 carbohydrates on board' + , nb: '%1 has %2 carbohydrates on board' + , nl: '%1 has %2 carbohydrates on board' + , pl: '%1 has %2 carbohydrates on board' + , pt: '%1 has %2 carbohydrates on board' + , ro: '%1 has %2 carbohydrates on board' + , ru: '%1 имеет %2 активных углеводов' + , sk: '%1 has %2 carbohydrates on board' + , sv: '%1 has %2 carbohydrates on board' + , tr: '%1 has %2 carbohydrates on board' + , zh_cn: '%1 has %2 carbohydrates on board' + , zh_tw: '%1 has %2 carbohydrates on board' + }, + 'virtAsstCob': { + bg: 'You have %1 carbohydrates on board' + , cs: 'You have %1 carbohydrates on board' + , de: 'Du hast noch %1 Kohlenhydrate wirkend.' + , dk: 'You have %1 carbohydrates on board' + , el: 'You have %1 carbohydrates on board' + , en: 'You have %1 carbohydrates on board' + , es: 'You have %1 carbohydrates on board' + , fi: 'You have %1 carbohydrates on board' + , fr: 'You have %1 carbohydrates on board' + , he: 'You have %1 carbohydrates on board' + , hr: 'You have %1 carbohydrates on board' + , it: 'You have %1 carbohydrates on board' + , ko: 'You have %1 carbohydrates on board' + , nb: 'You have %1 carbohydrates on board' + , nl: 'You have %1 carbohydrates on board' + , pl: 'You have %1 carbohydrates on board' + , pt: 'You have %1 carbohydrates on board' + , ro: 'You have %1 carbohydrates on board' + , ru: 'У вас %1 активных углеводов' + , sk: 'You have %1 carbohydrates on board' + , sv: 'You have %1 carbohydrates on board' + , tr: 'You have %1 carbohydrates on board' + , zh_cn: 'You have %1 carbohydrates on board' + , zh_tw: 'You have %1 carbohydrates on board' + }, + 'virtAsstCGMMode': { + bg: 'Your CGM mode was %1 as of %2.' + , cs: 'Your CGM mode was %1 as of %2.' + , de: 'Your CGM mode was %1 as of %2.' + , dk: 'Your CGM mode was %1 as of %2.' + , el: 'Your CGM mode was %1 as of %2.' + , en: 'Your CGM mode was %1 as of %2.' + , es: 'Your CGM mode was %1 as of %2.' + , fi: 'Your CGM mode was %1 as of %2.' + , fr: 'Your CGM mode was %1 as of %2.' + , he: 'Your CGM mode was %1 as of %2.' + , hr: 'Your CGM mode was %1 as of %2.' + , it: 'Your CGM mode was %1 as of %2.' + , ko: 'Your CGM mode was %1 as of %2.' + , nb: 'Your CGM mode was %1 as of %2.' + , nl: 'Your CGM mode was %1 as of %2.' + , pl: 'Your CGM mode was %1 as of %2.' + , pt: 'Your CGM mode was %1 as of %2.' + , ro: 'Your CGM mode was %1 as of %2.' + , ru: 'Your CGM mode was %1 as of %2.' + , sk: 'Your CGM mode was %1 as of %2.' + , sv: 'Your CGM mode was %1 as of %2.' + , tr: 'Your CGM mode was %1 as of %2.' + , zh_cn: 'Your CGM mode was %1 as of %2.' + , zh_tw: 'Your CGM mode was %1 as of %2.' + }, + 'virtAsstCGMStatus': { + bg: 'Your CGM status was %1 as of %2.' + , cs: 'Your CGM status was %1 as of %2.' + , de: 'Your CGM status was %1 as of %2.' + , dk: 'Your CGM status was %1 as of %2.' + , el: 'Your CGM status was %1 as of %2.' + , en: 'Your CGM status was %1 as of %2.' + , es: 'Your CGM status was %1 as of %2.' + , fi: 'Your CGM status was %1 as of %2.' + , fr: 'Your CGM status was %1 as of %2.' + , he: 'Your CGM status was %1 as of %2.' + , hr: 'Your CGM status was %1 as of %2.' + , it: 'Your CGM status was %1 as of %2.' + , ko: 'Your CGM status was %1 as of %2.' + , nb: 'Your CGM status was %1 as of %2.' + , nl: 'Your CGM status was %1 as of %2.' + , pl: 'Your CGM status was %1 as of %2.' + , pt: 'Your CGM status was %1 as of %2.' + , ro: 'Your CGM status was %1 as of %2.' + , ru: 'Your CGM status was %1 as of %2.' + , sk: 'Your CGM status was %1 as of %2.' + , sv: 'Your CGM status was %1 as of %2.' + , tr: 'Your CGM status was %1 as of %2.' + , zh_cn: 'Your CGM status was %1 as of %2.' + , zh_tw: 'Your CGM status was %1 as of %2.' + }, + 'virtAsstCGMSessAge': { + bg: 'Your CGM session has been active for %1 days and %2 hours.' + , cs: 'Your CGM session has been active for %1 days and %2 hours.' + , de: 'Your CGM session has been active for %1 days and %2 hours.' + , dk: 'Your CGM session has been active for %1 days and %2 hours.' + , el: 'Your CGM session has been active for %1 days and %2 hours.' + , en: 'Your CGM session has been active for %1 days and %2 hours.' + , es: 'Your CGM session has been active for %1 days and %2 hours.' + , fi: 'Your CGM session has been active for %1 days and %2 hours.' + , fr: 'Your CGM session has been active for %1 days and %2 hours.' + , he: 'Your CGM session has been active for %1 days and %2 hours.' + , hr: 'Your CGM session has been active for %1 days and %2 hours.' + , it: 'Your CGM session has been active for %1 days and %2 hours.' + , ko: 'Your CGM session has been active for %1 days and %2 hours.' + , nb: 'Your CGM session has been active for %1 days and %2 hours.' + , nl: 'Your CGM session has been active for %1 days and %2 hours.' + , pl: 'Your CGM session has been active for %1 days and %2 hours.' + , pt: 'Your CGM session has been active for %1 days and %2 hours.' + , ro: 'Your CGM session has been active for %1 days and %2 hours.' + , ru: 'Your CGM session has been active for %1 days and %2 hours.' + , sk: 'Your CGM session has been active for %1 days and %2 hours.' + , sv: 'Your CGM session has been active for %1 days and %2 hours.' + , tr: 'Your CGM session has been active for %1 days and %2 hours.' + , zh_cn: 'Your CGM session has been active for %1 days and %2 hours.' + , zh_tw: 'Your CGM session has been active for %1 days and %2 hours.' + }, + 'virtAsstCGMSessNotStarted': { + bg: 'There is no active CGM session at the moment.' + , cs: 'There is no active CGM session at the moment.' + , de: 'There is no active CGM session at the moment.' + , dk: 'There is no active CGM session at the moment.' + , el: 'There is no active CGM session at the moment.' + , en: 'There is no active CGM session at the moment.' + , es: 'There is no active CGM session at the moment.' + , fi: 'There is no active CGM session at the moment.' + , fr: 'There is no active CGM session at the moment.' + , he: 'There is no active CGM session at the moment.' + , hr: 'There is no active CGM session at the moment.' + , it: 'There is no active CGM session at the moment.' + , ko: 'There is no active CGM session at the moment.' + , nb: 'There is no active CGM session at the moment.' + , nl: 'There is no active CGM session at the moment.' + , pl: 'There is no active CGM session at the moment.' + , pt: 'There is no active CGM session at the moment.' + , ro: 'There is no active CGM session at the moment.' + , ru: 'There is no active CGM session at the moment.' + , sk: 'There is no active CGM session at the moment.' + , sv: 'There is no active CGM session at the moment.' + , tr: 'There is no active CGM session at the moment.' + , zh_cn: 'There is no active CGM session at the moment.' + , zh_tw: 'There is no active CGM session at the moment.' + }, + 'virtAsstCGMTxStatus': { + bg: 'Your CGM transmitter status was %1 as of %2.' + , cs: 'Your CGM transmitter status was %1 as of %2.' + , de: 'Your CGM transmitter status was %1 as of %2.' + , dk: 'Your CGM transmitter status was %1 as of %2.' + , el: 'Your CGM transmitter status was %1 as of %2.' + , en: 'Your CGM transmitter status was %1 as of %2.' + , es: 'Your CGM transmitter status was %1 as of %2.' + , fi: 'Your CGM transmitter status was %1 as of %2.' + , fr: 'Your CGM transmitter status was %1 as of %2.' + , he: 'Your CGM transmitter status was %1 as of %2.' + , hr: 'Your CGM transmitter status was %1 as of %2.' + , it: 'Your CGM transmitter status was %1 as of %2.' + , ko: 'Your CGM transmitter status was %1 as of %2.' + , nb: 'Your CGM transmitter status was %1 as of %2.' + , nl: 'Your CGM transmitter status was %1 as of %2.' + , pl: 'Your CGM transmitter status was %1 as of %2.' + , pt: 'Your CGM transmitter status was %1 as of %2.' + , ro: 'Your CGM transmitter status was %1 as of %2.' + , ru: 'Your CGM transmitter status was %1 as of %2.' + , sk: 'Your CGM transmitter status was %1 as of %2.' + , sv: 'Your CGM transmitter status was %1 as of %2.' + , tr: 'Your CGM transmitter status was %1 as of %2.' + , zh_cn: 'Your CGM transmitter status was %1 as of %2.' + , zh_tw: 'Your CGM transmitter status was %1 as of %2.' + }, + 'virtAsstCGMTxAge': { + bg: 'Your CGM transmitter is %1 days old.' + , cs: 'Your CGM transmitter is %1 days old.' + , de: 'Your CGM transmitter is %1 days old.' + , dk: 'Your CGM transmitter is %1 days old.' + , el: 'Your CGM transmitter is %1 days old.' + , en: 'Your CGM transmitter is %1 days old.' + , es: 'Your CGM transmitter is %1 days old.' + , fi: 'Your CGM transmitter is %1 days old.' + , fr: 'Your CGM transmitter is %1 days old.' + , he: 'Your CGM transmitter is %1 days old.' + , hr: 'Your CGM transmitter is %1 days old.' + , it: 'Your CGM transmitter is %1 days old.' + , ko: 'Your CGM transmitter is %1 days old.' + , nb: 'Your CGM transmitter is %1 days old.' + , nl: 'Your CGM transmitter is %1 days old.' + , pl: 'Your CGM transmitter is %1 days old.' + , pt: 'Your CGM transmitter is %1 days old.' + , ro: 'Your CGM transmitter is %1 days old.' + , ru: 'Your CGM transmitter is %1 days old.' + , sk: 'Your CGM transmitter is %1 days old.' + , sv: 'Your CGM transmitter is %1 days old.' + , tr: 'Your CGM transmitter is %1 days old.' + , zh_cn: 'Your CGM transmitter is %1 days old.' + , zh_tw: 'Your CGM transmitter is %1 days old.' + }, + 'virtAsstCGMNoise': { + bg: 'Your CGM noise was %1 as of %2.' + , cs: 'Your CGM noise was %1 as of %2.' + , de: 'Your CGM noise was %1 as of %2.' + , dk: 'Your CGM noise was %1 as of %2.' + , el: 'Your CGM noise was %1 as of %2.' + , en: 'Your CGM noise was %1 as of %2.' + , es: 'Your CGM noise was %1 as of %2.' + , fi: 'Your CGM noise was %1 as of %2.' + , fr: 'Your CGM noise was %1 as of %2.' + , he: 'Your CGM noise was %1 as of %2.' + , hr: 'Your CGM noise was %1 as of %2.' + , it: 'Your CGM noise was %1 as of %2.' + , ko: 'Your CGM noise was %1 as of %2.' + , nb: 'Your CGM noise was %1 as of %2.' + , nl: 'Your CGM noise was %1 as of %2.' + , pl: 'Your CGM noise was %1 as of %2.' + , pt: 'Your CGM noise was %1 as of %2.' + , ro: 'Your CGM noise was %1 as of %2.' + , ru: 'Your CGM noise was %1 as of %2.' + , sk: 'Your CGM noise was %1 as of %2.' + , sv: 'Your CGM noise was %1 as of %2.' + , tr: 'Your CGM noise was %1 as of %2.' + , zh_cn: 'Your CGM noise was %1 as of %2.' + , zh_tw: 'Your CGM noise was %1 as of %2.' + }, + 'virtAsstCGMBattOne': { + bg: 'Your CGM battery was %1 volts as of %2.' + , cs: 'Your CGM battery was %1 volts as of %2.' + , de: 'Your CGM battery was %1 volts as of %2.' + , dk: 'Your CGM battery was %1 volts as of %2.' + , el: 'Your CGM battery was %1 volts as of %2.' + , en: 'Your CGM battery was %1 volts as of %2.' + , es: 'Your CGM battery was %1 volts as of %2.' + , fi: 'Your CGM battery was %1 volts as of %2.' + , fr: 'Your CGM battery was %1 volts as of %2.' + , he: 'Your CGM battery was %1 volts as of %2.' + , hr: 'Your CGM battery was %1 volts as of %2.' + , it: 'Your CGM battery was %1 volts as of %2.' + , ko: 'Your CGM battery was %1 volts as of %2.' + , nb: 'Your CGM battery was %1 volts as of %2.' + , nl: 'Your CGM battery was %1 volts as of %2.' + , pl: 'Your CGM battery was %1 volts as of %2.' + , pt: 'Your CGM battery was %1 volts as of %2.' + , ro: 'Your CGM battery was %1 volts as of %2.' + , ru: 'Your CGM battery was %1 volts as of %2.' + , sk: 'Your CGM battery was %1 volts as of %2.' + , sv: 'Your CGM battery was %1 volts as of %2.' + , tr: 'Your CGM battery was %1 volts as of %2.' + , zh_cn: 'Your CGM battery was %1 volts as of %2.' + , zh_tw: 'Your CGM battery was %1 volts as of %2.' + }, + 'virtAsstCGMBattTwo': { + bg: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , cs: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , de: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , dk: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , el: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , en: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , es: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , fi: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , fr: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , he: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , hr: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , it: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , ko: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , nb: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , nl: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , pl: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , pt: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , ro: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , ru: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , sk: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , sv: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , tr: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , zh_cn: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + , zh_tw: 'Your CGM battery levels were %1 volts and %2 volts as of %3.' + }, + 'virtAsstDelta': { + bg: 'Your delta was %1 between %2 and %3.' + , cs: 'Your delta was %1 between %2 and %3.' + , de: 'Dein Delta war %1 zwischen %2 und %3.' + , dk: 'Your delta was %1 between %2 and %3.' + , el: 'Your delta was %1 between %2 and %3.' + , en: 'Your delta was %1 between %2 and %3.' + , es: 'Your delta was %1 between %2 and %3.' + , fi: 'Your delta was %1 between %2 and %3.' + , fr: 'Your delta was %1 between %2 and %3.' + , he: 'Your delta was %1 between %2 and %3.' + , hr: 'Your delta was %1 between %2 and %3.' + , it: 'Your delta was %1 between %2 and %3.' + , ko: 'Your delta was %1 between %2 and %3.' + , nb: 'Your delta was %1 between %2 and %3.' + , nl: 'Your delta was %1 between %2 and %3.' + , pl: 'Your delta was %1 between %2 and %3.' + , pt: 'Your delta was %1 between %2 and %3.' + , ro: 'Your delta was %1 between %2 and %3.' + , ru: 'Дельта была %1 между %2 и %3.' + , sk: 'Your delta was %1 between %2 and %3.' + , sv: 'Your delta was %1 between %2 and %3.' + , tr: 'Your delta was %1 between %2 and %3.' + , zh_cn: 'Your delta was %1 between %2 and %3.' + , zh_tw: 'Your delta was %1 between %2 and %3.' + }, + 'virtAsstUnknownIntentTitle': { + en: 'Unknown Intent' + , cs: 'Unknown Intent' + , de: 'Unbekannte Absicht' + , dk: 'Unknown Intent' + , ko: 'Unknown Intent' + , nl: 'Unknown Intent' + , zh_cn: 'Unknown Intent' + , sv: 'Unknown Intent' + , fi: 'Unknown Intent' + , ro: 'Unknown Intent' + , bg: 'Unknown Intent' + , hr: 'Unknown Intent' + , pl: 'Unknown Intent' + , ru: 'Неизвестное намерение' + , tr: 'Unknown Intent' + }, + 'virtAsstUnknownIntentText': { + en: 'I\'m sorry, I don\'t know what you\'re asking for.' + , cs: 'I\'m sorry, I don\'t know what you\'re asking for.' + , de: 'Tut mir leid, ich hab deine Frage nicht verstanden.' + , dk: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ko: 'I\'m sorry, I don\'t know what you\'re asking for.' + , nl: 'I\'m sorry, I don\'t know what you\'re asking for.' + , zh_cn: 'I\'m sorry, I don\'t know what you\'re asking for.' + , sv: 'I\'m sorry, I don\'t know what you\'re asking for.' + , fi: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ro: 'I\'m sorry, I don\'t know what you\'re asking for.' + , bg: 'I\'m sorry, I don\'t know what you\'re asking for.' + , hr: 'I\'m sorry, I don\'t know what you\'re asking for.' + , pl: 'I\'m sorry, I don\'t know what you\'re asking for.' + , ru: 'Ваш запрос непонятен' + , tr: 'I\'m sorry, I don\'t know what you\'re asking for.' }, 'Fat [g]': { cs: 'Tuk [g]' @@ -13677,6 +14650,7 @@ function init() { ,hr: 'Masnoće [g]' ,pl: 'Tłuszcz [g]' ,tr: 'Yağ [g]' + ,he: '[g] שמן' }, 'Protein [g]': { cs: 'Proteiny [g]' @@ -13696,6 +14670,7 @@ function init() { ,hr: 'Proteini [g]' ,pl: 'Białko [g]' ,tr: 'Protein [g]' + ,he: '[g] חלבון' }, 'Energy [kJ]': { cs: 'Energie [kJ]' @@ -13705,7 +14680,7 @@ function init() { ,es: 'Energía [Kj]' ,fr: 'Énergie [kJ]' ,ro: 'Energie [g]' - ,ru: 'энергия [kJ' + ,ru: 'энергия [kJ]' ,it: 'Energia [kJ]' ,zh_cn: '能量 [kJ]' ,ko: 'Energy [kJ]' @@ -13715,6 +14690,7 @@ function init() { ,hr: 'Energija [kJ]' ,pl: 'Energia [kJ}' ,tr: 'Enerji [kJ]' + ,he: '[kJ] אנרגיה' }, 'Clock Views:': { cs: 'Hodiny:' @@ -13734,6 +14710,7 @@ function init() { ,hr: 'Satovi:' ,pl: 'Widoki zegarów' ,tr: 'Saat Görünümü' + ,he: 'צגים השעון' }, 'Clock': { cs: 'Hodiny' @@ -13752,6 +14729,7 @@ function init() { ,pl: 'Zegar' ,ru: 'часы' ,tr: 'Saat' + ,he: 'שעון' }, 'Color': { cs: 'Barva' @@ -13770,6 +14748,7 @@ function init() { ,pl: 'Kolor' ,ru: 'цвет' ,tr: 'Renk' + ,he: 'צבע' }, 'Simple': { cs: 'Jednoduchý' @@ -13788,6 +14767,7 @@ function init() { ,pl: 'Prosty' ,ru: 'простой' ,tr: 'Basit' + ,he: 'פשוט' }, 'TDD average': { cs: 'Průměrná denní dávka' @@ -13822,6 +14802,7 @@ function init() { , pl: 'Średnia ilość węglowodanów' , ru: 'среднее кол-во углеводов за сутки' , tr: 'Günde ortalama karbonhidrat' + ,he: 'פחמימות ממוצע' }, 'Eating Soon': { cs: 'Blížící se jídlo' @@ -13837,8 +14818,9 @@ function init() { , bg: 'Преди хранене' , hr: 'Uskoro obrok' , pl: 'Przed jedzeniem' - , ru: 'скоро прием пищи' + , ru: 'Ожидаемый прием пищи' , tr: 'Yakında Yenecek' + , he: 'אוכל בקרוב' }, 'Last entry {0} minutes ago': { cs: 'Poslední hodnota {0} minut zpět' @@ -13873,6 +14855,7 @@ function init() { , pl: 'zmiana' , ru: 'замена' , tr: 'değişiklik' + , he: 'שינוי' }, 'Speech': { cs: 'Hlas' @@ -13890,6 +14873,7 @@ function init() { , pl: 'Głos' , ru: 'речь' , tr: 'Konuş' + , he: 'דיבור' }, 'Target Top': { cs: 'Horní cíl' @@ -13907,6 +14891,7 @@ function init() { , ru: 'верхняя граница цели' , de: 'Oberes Ziel' , tr: 'Hedef Üst' + , he: 'ראש היעד' }, 'Target Bottom': { cs: 'Dolní cíl' @@ -13924,6 +14909,7 @@ function init() { , ru: 'нижняя граница цели' , de: 'Unteres Ziel' , tr: 'Hedef Alt' + , he: 'תחתית היעד' }, 'Canceled': { cs: 'Zrušený' @@ -13941,6 +14927,7 @@ function init() { , ru: 'отменено' , de: 'Abgebrochen' , tr: 'İptal edildi' + , he: 'מבוטל' }, 'Meter BG': { cs: 'Hodnota z glukoměru' @@ -13955,9 +14942,10 @@ function init() { , bg: 'Измерена КЗ' , hr: 'GUK iz krvi' , pl: 'Glikemia z krwi' - , ru: 'СК по глюкометру' + , ru: 'ГК по глюкометру' , de: 'Wert Blutzuckermessgerät' , tr: 'Glikometre KŞ' + , he: 'סוכר הדם של מד' }, 'predicted': { cs: 'přepověď' @@ -13975,6 +14963,7 @@ function init() { , ru: 'прогноз' , de: 'vorhergesagt' , tr: 'tahmin' + , he: 'חזה' }, 'future': { cs: 'budoucnost' @@ -13992,6 +14981,7 @@ function init() { , ru: 'будущее' , de: 'Zukunft' , tr: 'gelecek' + , he: 'עתיד' }, 'ago': { cs: 'zpět' @@ -14005,9 +14995,10 @@ function init() { , bg: 'преди' , hr: 'prije' , pl: 'temu' - , ru: 'назад' + , ru: 'в прошлом' , de: 'vor' , tr: 'önce' + , he: 'לפני' }, 'Last data received': { cs: 'Poslední data přiajata' @@ -14022,9 +15013,10 @@ function init() { , bg: 'Последни данни преди' , hr: 'Podaci posljednji puta primljeni' , pl: 'Ostatnie otrzymane dane' - , ru: 'недавние данные получены' + , ru: 'прошлые данные получены' , de: 'Zuletzt Daten empfangen' , tr: 'Son veri alındı' + , he: 'הנתונים המקבל אחרונים' }, 'Clock View': { cs: 'Hodiny' @@ -14044,36 +15036,112 @@ function init() { ,de: 'Uhr-Anzeigen' ,pl: 'Widok zegara' ,tr: 'Saat Görünümü' + ,he: 'צג השעון' }, 'Protein': { fi: 'Proteiini' - , de: 'Protein' + ,de: 'Protein' + ,tr: 'Protein' + ,hr: 'Proteini' + , pl: 'Białko' + ,ru: 'Белки' + ,he: 'חלבון' + ,nl: 'Eiwit' }, 'Fat': { fi: 'Rasva' - , de: 'Fett' + ,de: 'Fett' + ,tr: 'Yağ' + ,hr: 'Masti' + , pl: 'Tłuszcz' + ,ru: 'Жиры' + ,he: 'שמן' + ,nl: 'Vet' }, 'Protein average': { fi: 'Proteiini keskiarvo' - , de: 'Proteine Durchschnitt' + ,de: 'Proteine Durchschnitt' + ,tr: 'Protein Ortalaması' + ,hr: 'Prosjek proteina' + , pl: 'Średnia białka' + ,ru: 'Средний белок' + ,he: 'חלבון ממוצע' + ,nl: 'eiwitgemiddelde' }, 'Fat average': { fi: 'Rasva keskiarvo' - , de: 'Fett Durchschnitt' - + ,de: 'Fett Durchschnitt' + ,tr: 'Yağ Ortalaması' + ,hr: 'Prosjek masti' + , pl: 'Średnia tłuszczu' + ,ru: 'Средний жир' + ,he: 'שמן ממוצע' + ,nl: 'Vetgemiddelde' }, 'Total carbs': { fi: 'Hiilihydraatit yhteensä' , de: 'Kohlenhydrate gesamt' + ,tr: 'Toplam Karbonhidrat' + ,hr: 'Ukupno ugh' + , pl: 'Węglowodany ogółem' + ,ru: 'Всего углеводов' + ,he: 'כל פחמימות' + ,nl: 'Totaal koolhydraten' }, 'Total protein': { fi: 'Proteiini yhteensä' , de: 'Protein gesamt' + ,tr: 'Toplam Protein' + ,hr: 'Ukupno proteini' + , pl: 'Białko ogółem' + ,ru: 'Всего белков' + ,he: 'כל חלבונים' + ,nl: 'Totaal eiwitten' }, 'Total fat': { fi: 'Rasva yhteensä' , de: 'Fett gesamt' - } + ,tr: 'Toplam Yağ' + ,hr: 'Ukupno masti' + , pl: 'Tłuszcz ogółem' + ,ru: 'Всего жиров' + ,he: 'כל שומנים' + ,nl: 'Totaal vetten' + }, + 'Database Size': { + pl: 'Rozmiar Bazy Danych' + ,nl: 'Grootte database' + }, + 'Database Size near its limits!': { + pl: 'Rozmiar bazy danych zbliża się do limitu!' + ,nl: 'Database grootte nadert limiet!' + }, + 'Database size is %1 MiB out of %2 MiB. Please backup and clean up database!': { + pl: 'Baza danych zajmuje %1 MiB z dozwolonych %2 MiB. Proszę zrób kopię zapasową i oczyść bazę danych!' + ,nl: 'Database grootte is %1 MiB van de %2 MiB. Maak een backup en verwijder oude data' + }, + 'Database file size': { + pl: 'Rozmiar pliku bazy danych' + ,nl: 'Database bestandsgrootte' + }, + '%1 MiB of %2 MiB (%3%)': { + pl: '%1 MiB z %2 MiB (%3%)' + ,nl: '%1 MiB van de %2 MiB (%3%)' + }, + 'Data size': { + pl: 'Rozmiar danych' + ,nl: 'Datagrootte' + }, + 'virtAsstDatabaseSize': { + en: '%1 MiB that is %2% of available database space' + ,pl: '%1 MiB co stanowi %2% przestrzeni dostępnej dla bazy danych' + ,nl: '%1 MiB dat is %2% van de beschikbaare database ruimte' + }, + 'virtAsstTitleDatabaseSize': { + en: 'Database file size' + ,pl: 'Rozmiar pliku bazy danych' + ,nl: 'Database bestandsgrootte' + } }; language.translations = translations; diff --git a/lib/middleware/express-extension-to-accept.js b/lib/middleware/express-extension-to-accept.js index 8f357e869a1..cdfe9a269fe 100644 --- a/lib/middleware/express-extension-to-accept.js +++ b/lib/middleware/express-extension-to-accept.js @@ -16,7 +16,7 @@ module.exports = function (formats) { throw new Error('Invalid format.') }) - var regexp = new RegExp('\.(' + formats.join('|') + ')$', 'i') + var regexp = new RegExp('\\.(' + formats.join('|') + ')$', 'i') return function (req, res, next) { var match = req.path.match(regexp) diff --git a/lib/middleware/index.js b/lib/middleware/index.js index c20eede6351..deb4fd5fb41 100644 --- a/lib/middleware/index.js +++ b/lib/middleware/index.js @@ -10,7 +10,7 @@ function extensions (list) { return require('./express-extension-to-accept')(list); } -function configure (env) { +function configure () { return { sendJSONStatus: wares.sendJSONStatus( ), bodyParser: wares.bodyParser, diff --git a/lib/notifications.js b/lib/notifications.js index 1a03ab87244..b4a8767afbe 100644 --- a/lib/notifications.js +++ b/lib/notifications.js @@ -108,8 +108,7 @@ function init (env, ctx) { }; notifications.requestNotify = function requestNotify (notify) { - // eslint-disable-next-line no-prototype-builtins - if (!notify.hasOwnProperty('level') || !notify.title || !notify.message || !notify.plugin) { + if (!Object.prototype.hasOwnProperty.call(notify, 'level') || !notify.title || !notify.message || !notify.plugin) { console.error(new Error('Unable to request notification, since the notify isn\'t complete: ' + JSON.stringify(notify))); return; } diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js index 38ae449c249..25b13d0596d 100644 --- a/lib/plugins/alexa.js +++ b/lib/plugins/alexa.js @@ -1,60 +1,58 @@ var _ = require('lodash'); var async = require('async'); -function init(env, ctx) { - console.log('Configuring Alexa.'); +function init () { + console.log('Configuring Alexa...'); function alexa() { return alexa; } var intentHandlers = {}; var rollup = {}; - // This configures a router/handler. A routable slot the name of a slot that you wish to route on and the slotValues - // are the values that determine the routing. This allows for specific intent handlers based on the value of a - // specific slot. Routing is only supported on one slot for now. - // There is no protection for a previously configured handler - one plugin can overwrite the handler of another - // plugin. - alexa.configureIntentHandler = function configureIntentHandler(intent, handler, routableSlot, slotValues) { - if (! intentHandlers[intent]) { + // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. + alexa.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { + if (!intentHandlers[intent]) { intentHandlers[intent] = {}; } - if (routableSlot && slotValues) { - for (var i = 0, len = slotValues.length; i < len; i++) { - if (! intentHandlers[intent][routableSlot]) { - intentHandlers[intent][routableSlot] = {}; + if (metrics) { + for (var i = 0, len = metrics.length; i < len; i++) { + if (!intentHandlers[intent][metrics[i]]) { + intentHandlers[intent][metrics[i]] = {}; } - if (!intentHandlers[intent][routableSlot][slotValues[i]]) { - intentHandlers[intent][routableSlot][slotValues[i]] = {}; - } - intentHandlers[intent][routableSlot][slotValues[i]].handler = handler; + console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); + intentHandlers[intent][metrics[i]].handler = handler; } } else { + console.log('Storing handler for intent \'' + intent + '\''); intentHandlers[intent].handler = handler; } }; - // This function retrieves a handler based on the intent name and slots requested. - alexa.getIntentHandler = function getIntentHandler(intentName, slots) { - if (intentName && intentHandlers[intentName]) { - if (slots) { - var slotKeys = Object.keys(slots); - for (var i = 0, len = slotKeys.length; i < len; i++) { - if (intentHandlers[intentName][slotKeys[i]] && slots[slotKeys[i]].value && - intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value] && - intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler) { - - return intentHandlers[intentName][slotKeys[i]][slots[slotKeys[i]].value].handler; - } - } - } - if (intentHandlers[intentName].handler) { + // This function retrieves a handler based on the intent name and metric requested. + alexa.getIntentHandler = function getIntentHandler(intentName, metric) { + if (metric === undefined) { + console.log('Looking for handler for intent \'' + intentName + '\''); + if (intentName + && intentHandlers[intentName] + && intentHandlers[intentName].handler + ) { + console.log('Found!'); return intentHandlers[intentName].handler; } - return null; } else { - return null; + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); + if (intentName + && intentHandlers[intentName] + && intentHandlers[intentName][metric] + && intentHandlers[intentName][metric].handler + ) { + console.log('Found!'); + return intentHandlers[intentName][metric].handler + } } + console.log('Not found!'); + return null; }; alexa.addToRollup = function(rollupGroup, handler, rollupName) { @@ -63,7 +61,6 @@ function init(env, ctx) { rollup[rollupGroup] = []; } rollup[rollupGroup].push({handler: handler, name: rollupName}); - // status = _.orderBy(status, ['priority'], ['asc']) }; alexa.getRollup = function(rollupGroup, sbx, slots, locale, callback) { @@ -110,4 +107,4 @@ function init(env, ctx) { return alexa; } -module.exports = init; +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index e25a2b36229..133c5b8d3c9 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -15,8 +15,8 @@ var AR = [-0.723, 1.716]; //TODO: move this to css var AR2_COLOR = 'cyan'; -// eslint-disable-next-line no-unused-vars function init (ctx) { + var translate = ctx.language.translate; var ar2 = { name: 'ar2' @@ -146,7 +146,7 @@ function init (ctx) { return result.points; }; - function alexaAr2Handler (next, slots, sbx) { + function virtAsstAr2Handler (next, slots, sbx) { if (sbx.properties.ar2.forecast.predicted) { var forecast = sbx.properties.ar2.forecast.predicted; var max = forecast[0].mgdl; @@ -163,19 +163,34 @@ function init (ctx) { maxForecastMills = forecast[i].mills; } } - var response = 'You are expected to be between ' + min + ' and ' + max + ' over the ' + moment(maxForecastMills).from(moment(sbx.time)); - next('AR2 Forecast', response); + var response = ''; + if (min === max) { + response = translate('virtAsstAR2ForecastAround', { + params: [ + max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); + } else { + response = translate('virtAsstAR2ForecastBetween', { + params: [ + min + , max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); + } + next(translate('virtAsstTitleAR2Forecast'), response); } else { - next('AR2 Forecast', 'AR2 plugin does not seem to be enabled'); + next(translate('virtAsstTitleAR2Forecast'), translate('virtAsstUnknown')); } } - ar2.alexa = { + ar2.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['ar2 forecast', 'forecast'] - , intentHandler: alexaAr2Handler + , metrics: ['ar2 forecast', 'forecast'] + , intentHandler: virtAsstAr2Handler }] }; diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index 05bc8c7248a..4347a7005ec 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -1,6 +1,7 @@ 'use strict'; var times = require('../times'); var moment = require('moment'); +var consts = require('../constants'); function init (ctx) { @@ -62,9 +63,10 @@ function init (ctx) { var tzMessage = profile.getTimezone() ? profile.getTimezone() : 'Timezone not set in profile'; var sensitivity = profile.getSensitivity(sbx.time); + var units = profile.getUnits(); - if (sbx.settings.units != profile.data[0].units) { - sensitivity *= (sbx.settings.units === 'mmol' ? 0.055 : 18); + if (sbx.settings.units != units) { + sensitivity *= (sbx.settings.units === 'mmol' ? (1 / consts.MMOL_TO_MGDL) : consts.MMOL_TO_MGDL); var decimals = (sbx.settings.units === 'mmol' ? 10 : 1); sensitivity = Math.round(sensitivity * decimals) / decimals; @@ -111,16 +113,16 @@ function init (ctx) { function basalMessage(slots, sbx) { var basalValue = sbx.data.profile.getTempBasal(sbx.time); - var response = 'Unable to determine current basal'; + var response = translate('virtAsstUnknown'); var preamble = ''; if (basalValue.treatment) { - preamble = (slots && slots.pwd && slots.pwd.value) ? translate('alexaPreamble3person', { + preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { params: [ slots.pwd.value ] - }) : translate('alexaPreamble'); + }) : translate('virtAsstPreamble'); var minutesLeft = moment(basalValue.treatment.endmills).from(moment(sbx.time)); - response = translate('alexaBasalTemp', { + response = translate('virtAsstBasalTemp', { params: [ preamble, basalValue.totalbasal, @@ -128,12 +130,12 @@ function init (ctx) { ] }); } else { - preamble = (slots && slots.pwd && slots.pwd.value) ? translate('alexaPreamble3person', { + preamble = (slots && slots.pwd && slots.pwd.value) ? translate('virtAsstPreamble3person', { params: [ slots.pwd.value ] - }) : translate('alexaPreamble'); - response = translate('alexaBasal', { + }) : translate('virtAsstPreamble'); + response = translate('virtAsstBasal', { params: [ preamble, basalValue.totalbasal @@ -143,30 +145,28 @@ function init (ctx) { return response; } - function alexaRollupCurrentBasalHandler (slots, sbx, callback) { + function virtAsstRollupCurrentBasalHandler (slots, sbx, callback) { callback(null, {results: basalMessage(slots, sbx), priority: 1}); } - function alexaCurrentBasalhandler (next, slots, sbx) { - next('Current Basal', basalMessage(slots, sbx)); + function virtAsstCurrentBasalhandler (next, slots, sbx) { + next(translate('virtAsstTitleCurrentBasal'), basalMessage(slots, sbx)); } - basal.alexa = { + basal.virtAsst = { rollupHandlers: [{ rollupGroup: 'Status' , rollupName: 'current basal' - , rollupHandler: alexaRollupCurrentBasalHandler + , rollupHandler: virtAsstRollupCurrentBasalHandler }], intentHandlers: [{ intent: 'MetricNow' - , routableSlot:'metric' - , slots:['basal', 'current basal'] - , intentHandler: alexaCurrentBasalhandler + , metrics: ['basal', 'current basal'] + , intentHandler: virtAsstCurrentBasalhandler }] }; return basal; } - module.exports = init; diff --git a/lib/plugins/bridge.js b/lib/plugins/bridge.js index dc47f13aa9b..50851e6df74 100644 --- a/lib/plugins/bridge.js +++ b/lib/plugins/bridge.js @@ -5,9 +5,9 @@ var engine = require('share2nightscout-bridge'); // Track the most recently seen record var mostRecentRecord; -function init (env) { +function init (env, bus) { if (env.extendedSettings.bridge && env.extendedSettings.bridge.userName && env.extendedSettings.bridge.password) { - return create(env); + return create(env, bus); } else { console.info('Dexcom bridge not enabled'); } @@ -46,9 +46,17 @@ function options (env) { , minutes: env.extendedSettings.bridge.minutes || 1440 }; + var interval = env.extendedSettings.bridge.interval || 60000 * 2.5; // Default: 2.5 minutes + + if (interval < 1000 || interval > 300000) { + // Invalid interval range. Revert to default + console.error("Invalid interval set: [" + interval + "ms]. Defaulting to 2.5 minutes.") + interval = 60000 * 2.5 // 2.5 minutes + } + return { login: config - , interval: env.extendedSettings.bridge.interval || 60000 * 2.5 + , interval: interval , fetch: fetch_config , nightscout: { } , maxFailures: env.extendedSettings.bridge.maxFailures || 3 @@ -56,26 +64,32 @@ function options (env) { }; } -function create (env) { +function create (env, bus) { var bridge = { }; var opts = options(env); var interval = opts.interval; - mostRecentRecord = new Date().getTime() - opts.fetch.minutes * 60000 + mostRecentRecord = new Date().getTime() - opts.fetch.minutes * 60000; bridge.startEngine = function startEngine (entries) { opts.callback = bridged(entries); - setInterval(function () { + let timer = setInterval(function () { opts.fetch.minutes = parseInt((new Date() - mostRecentRecord) / 60000); opts.fetch.maxCount = parseInt((opts.fetch.minutes / 5) + 1); - opts.firstFetchCount = opts.fetch.maxCount + opts.firstFetchCount = opts.fetch.maxCount; console.log("Fetching Share Data: ", 'minutes', opts.fetch.minutes, 'maxCount', opts.fetch.maxCount); engine(opts); }, interval); + + if (bus) { + bus.on('teardown', function serverTeardown () { + clearInterval(timer); + }); + } }; return bridge; diff --git a/lib/plugins/careportal.js b/lib/plugins/careportal.js index 2d65e1c9e9c..90f9bbd992a 100644 --- a/lib/plugins/careportal.js +++ b/lib/plugins/careportal.js @@ -8,8 +8,7 @@ function init() { , pluginType: 'drawer' }; - // eslint-disable-next-line no-unused-vars - careportal.getEventTypes = function getEventTypes (sbx) { + careportal.getEventTypes = function getEventTypes () { //TODO: use sbx and new CAREPORTAL_EVENTTYPE_GROUPS="core temps combo dad sensor site etc" diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index bc769197c2b..c6d4c4fdf8f 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -41,15 +41,17 @@ function init (ctx) { } var devicestatusCOB = cob.lastCOBDeviceStatus(devicestatus, time); + var result = devicestatusCOB; - var treatmentCOB = (treatments !== undefined && treatments.length) ? cob.fromTreatments(treatments, devicestatus, profile, time, spec_profile) : {}; + const TEN_MINUTES = 10 * 60 * 1000; - var result = devicestatusCOB; - if (_.isEmpty(result)) { - result = treatmentCOB; + if (_.isEmpty(result) || _.isNil(result.cob) || (Date.now() - result.mills) > TEN_MINUTES) { + + var treatmentCOB = (treatments !== undefined && treatments.length) ? cob.fromTreatments(treatments, devicestatus, profile, time, spec_profile) : {}; + + result = _.cloneDeep(treatmentCOB); result.source = 'Care Portal'; - } else if (treatmentCOB) { - result.treatmentCOB = treatmentCOB; + result.treatmentCOB = _.cloneDeep(treatmentCOB); } return addDisplay(result); @@ -289,22 +291,31 @@ function init (ctx) { }); }; - function alexaCOBHandler (next, slots, sbx) { - var preamble = (slots && slots.pwd && slots.pwd.value) ? slots.pwd.value.replace('\'s', '') + ' has' : 'You have'; - var value = 'no'; - if (sbx.properties.cob && sbx.properties.cob.cob !== 0) { - value = sbx.properties.cob.cob; + function virtAsstCOBHandler (next, slots, sbx) { + var response = ''; + var value = (sbx.properties.cob && sbx.properties.cob.cob) ? sbx.properties.cob.cob : 0; + if (slots && slots.pwd && slots.pwd.value) { + response = translate('virtAsstCob3person', { + params: [ + slots.pwd.value.replace('\'s', '') + , value + ] + }); + } else { + response = translate('virtAsstCob', { + params: [ + value + ] + }); } - var response = preamble + ' ' + value + ' carbohydrates on board'; - next('Current COB', response); + next(translate('virtAsstTitleCurrentCOB'), response); } - cob.alexa = { + cob.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['cob', 'carbs on board', 'carbohydrates on board'] - , intentHandler: alexaCOBHandler + , metrics: ['cob', 'carbs on board', 'carbohydrates on board'] + , intentHandler: virtAsstCOBHandler }] }; diff --git a/lib/plugins/dbsize.js b/lib/plugins/dbsize.js new file mode 100644 index 00000000000..1698fa050b3 --- /dev/null +++ b/lib/plugins/dbsize.js @@ -0,0 +1,152 @@ +'use strict'; + +var levels = require('../levels'); + +function init (ctx) { + var translate = ctx.language.translate; + + var dbsize = { + name: 'dbsize' + , label: translate('Database Size') + , pluginType: 'pill-status' + , pillFlip: true + }; + + dbsize.getPrefs = function getPrefs (sbx) { + return { + warnPercentage: sbx.extendedSettings.warnPercentage ? sbx.extendedSettings.warnPercentage : 60 + , urgentPercentage: sbx.extendedSettings.urgentPercentage ? sbx.extendedSettings.urgentPercentage : 75 + , max: sbx.extendedSettings.max ? sbx.extendedSettings.max : 496 + , enableAlerts: sbx.extendedSettings.enableAlerts + , inMib: sbx.extendedSettings.inMib + }; + }; + + dbsize.setProperties = function setProperties (sbx) { + sbx.offerProperty('dbsize', function setDbsize () { + return dbsize.analyzeData(sbx); + }); + }; + + dbsize.analyzeData = function analyzeData (sbx) { + + var prefs = dbsize.getPrefs(sbx); + + var recentData = sbx.data.dbstats; + + var result = { + level: undefined + , display: prefs.inMib ? '?MiB' : '?%' + , status: undefined + }; + + var maxSize = (prefs.max > 0) ? prefs.max : 100 * 1024; + var totalDataSize = (recentData && recentData.dataSize) ? recentData.dataSize : 0; + totalDataSize += (recentData && recentData.indexSize) ? recentData.indexSize : 0; + totalDataSize /= 1024 * 1024; + + var dataPercentage = Math.floor((totalDataSize * 100.0) / maxSize); + + result.totalDataSize = totalDataSize; + result.dataPercentage = dataPercentage; + result.notificationLevel = levels.INFO; + result.details = { + maxSize: parseFloat(maxSize.toFixed(2)) + , dataSize: parseFloat(totalDataSize.toFixed(2)) + }; + + // failsafe to have percentage in 0..100 range + var boundWarnPercentage = Math.max(0, Math.min(100, parseInt(prefs.warnPercentage))); + var boundUrgentPercentage = Math.max(0, Math.min(100, parseInt(prefs.urgentPercentage))); + + var warnSize = Math.floor((boundWarnPercentage/100) * maxSize); + var urgentSize = Math.floor((boundUrgentPercentage/100) * maxSize); + + if ((totalDataSize >= urgentSize)&&(boundUrgentPercentage > 0)) { + result.notificationLevel = levels.URGENT; + } else if ((totalDataSize >= warnSize)&&(boundWarnPercentage > 0)) { + result.notificationLevel = levels.WARN; + } + + result.display = prefs.inMib ? parseFloat(totalDataSize.toFixed(0)) + 'MiB' : dataPercentage + '%'; + result.status = levels.toStatusClass(result.notificationLevel); + + return result; + }; + + dbsize.checkNotifications = function checkNotifications (sbx) { + var prefs = dbsize.getPrefs(sbx); + + if (!prefs.enableAlerts) { return; } + + var prop = sbx.properties.dbsize; + + if (prop.dataPercentage && prop.notificationLevel && prop.notificationLevel >= levels.WARN) { + sbx.notifications.requestNotify({ + level: prop.notificationLevel + , title: levels.toDisplay(prop.notificationLevel) + ' ' + translate('Database Size near its limits!') + , message: translate('Database size is %1 MiB out of %2 MiB. Please backup and clean up database!', { + params: [prop.details.dataSize, prop.details.maxSize] + }) + , pushoverSound: 'echo' + , group: 'Database Size' + , plugin: dbsize + , debug: prop + }); + } + }; + + dbsize.updateVisualisation = function updateVisualisation (sbx) { + var prop = sbx.properties.dbsize; + + var infos = [{ + label: translate('Data size') + , value: translate('%1 MiB of %2 MiB (%3%)', { + params: [prop.details.dataSize, prop.details.maxSize, prop.dataPercentage] + }) + } + ]; + + sbx.pluginBase.updatePillText(dbsize, { + value: prop && prop.display + , labelClass: 'plugicon-database' + , pillClass: prop && prop.status + , info: infos + , hide: !(prop && prop.totalDataSize && prop.totalDataSize >= 0) + }); + }; + + function virtAsstDatabaseSizeHandler (next, slots, sbx) { + if (sbx.properties.dbsize.display) { + var response = translate('virtAsstDatabaseSize', { + params: [ + sbx.properties.dbsize.details.dataSize + , sbx.properties.dbsize.dataPercentage + ] + }); + next(translate('virtAsstTitleDatabaseSize'), response); + } else { + next(translate('virtAsstTitleDatabaseSize'), translate('virtAsstUnknown')); + } + } + + dbsize.virtAsst = { + intentHandlers: [ + { + // for backwards compatibility + intent: 'DatabaseSize' + , intentHandler: virtAsstDatabaseSizeHandler + } + , { + intent: 'MetricNow' + , metrics: ['database size', 'file size', 'db size', 'data size'] + , intentHandler: virtAsstDatabaseSizeHandler + } + ] + }; + + return dbsize; + +} + +module.exports = init; diff --git a/lib/plugins/direction.js b/lib/plugins/direction.js index 12a3511368b..6274fc9e537 100644 --- a/lib/plugins/direction.js +++ b/lib/plugins/direction.js @@ -10,7 +10,7 @@ function init() { direction.setProperties = function setProperties (sbx) { sbx.offerProperty('direction', function setDirection ( ) { - if (sbx.data.inRetroMode && !sbx.isCurrent(sbx.lastSGVEntry())) { + if (!sbx.isCurrent(sbx.lastSGVEntry())) { return undefined; } else { return direction.info(sbx.lastSGVEntry()); @@ -77,4 +77,4 @@ function init() { } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js new file mode 100644 index 00000000000..6b6d7b098f2 --- /dev/null +++ b/lib/plugins/googlehome.js @@ -0,0 +1,97 @@ +var _ = require('lodash'); +var async = require('async'); + +function init () { + console.log('Configuring Google Home...'); + function googleHome() { + return googleHome; + } + var intentHandlers = {}; + var rollup = {}; + + // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. + googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { + if (!intentHandlers[intent]) { + intentHandlers[intent] = {}; + } + if (metrics) { + for (var i = 0, len = metrics.length; i < len; i++) { + if (!intentHandlers[intent][metrics[i]]) { + intentHandlers[intent][metrics[i]] = {}; + } + console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); + intentHandlers[intent][metrics[i]].handler = handler; + } + } else { + console.log('Storing handler for intent \'' + intent + '\''); + intentHandlers[intent].handler = handler; + } + }; + + // This function retrieves a handler based on the intent name and metric requested. + googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); + if (intentName && intentHandlers[intentName]) { + if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { + console.log('Found!'); + return intentHandlers[intentName][metric].handler + } else if (intentHandlers[intentName].handler) { + console.log('Found!'); + return intentHandlers[intentName].handler; + } + console.log('Not found!'); + return null; + } else { + console.log('Not found!'); + return null; + } + }; + + googleHome.addToRollup = function(rollupGroup, handler, rollupName) { + if (!rollup[rollupGroup]) { + console.log('Creating the rollup group: ', rollupGroup); + rollup[rollupGroup] = []; + } + rollup[rollupGroup].push({handler: handler, name: rollupName}); + }; + + googleHome.getRollup = function(rollupGroup, sbx, slots, locale, callback) { + var handlers = _.map(rollup[rollupGroup], 'handler'); + console.log('Rollup array for ', rollupGroup); + console.log(rollup[rollupGroup]); + var nHandlers = []; + _.each(handlers, function (handler) { + nHandlers.push(handler.bind(null, slots, sbx)); + }); + async.parallelLimit(nHandlers, 10, function(err, results) { + if (err) { + console.error('Error: ', err); + } + callback(_.map(_.orderBy(results, ['priority'], ['asc']), 'results').join(' ')); + }); + }; + + // This creates the expected Google Home response + googleHome.buildSpeechletResponse = function buildSpeechletResponse(output, expectUserResponse) { + return { + payload: { + google: { + expectUserResponse: expectUserResponse, + richResponse: { + items: [ + { + simpleResponse: { + textToSpeech: output + } + } + ] + } + } + } + }; + }; + + return googleHome; +} + +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 5970c16836c..5a6cb2822da 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -37,7 +37,7 @@ function init (ctx) { , require('./careportal')(ctx) , require('./pump')(ctx) , require('./openaps')(ctx) - , require('./xdrip-js')(ctx) + , require('./xdripjs')(ctx) , require('./loop')(ctx) , require('./override')(ctx) , require('./boluswizardpreview')(ctx) @@ -49,6 +49,7 @@ function init (ctx) { , require('./boluscalc')(ctx) // fake plugin to show/hide , require('./profile')(ctx) // fake plugin to hold extended settings , require('./speech')(ctx) + , require('./dbsize')(ctx) ]; var serverDefaultPlugins = [ @@ -63,7 +64,7 @@ function init (ctx) { , require('./cob')(ctx) , require('./pump')(ctx) , require('./openaps')(ctx) - , require('./xdrip-js')(ctx) + , require('./xdripjs')(ctx) , require('./loop')(ctx) , require('./boluswizardpreview')(ctx) , require('./cannulaage')(ctx) diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index f9bf082d0f4..96bea03b3ff 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -243,21 +243,19 @@ function init(ctx) { }; - function alexaIOBIntentHandler (callback, slots, sbx) { + function virtAsstIOBIntentHandler (callback, slots, sbx) { - var message = translate('alexaIobIntent', { + var message = translate('virtAsstIobIntent', { params: [ - //preamble, getIob(sbx) ] }); - //preamble + + ' insulin on board'; - callback('Current IOB', message); + callback(translate('virtAsstTitleCurrentIOB'), message); } - function alexaIOBRollupHandler (slots, sbx, callback) { + function virtAsstIOBRollupHandler (slots, sbx, callback) { var iob = getIob(sbx); - var message = translate('alexaIob', { + var message = translate('virtAsstIob', { params: [iob] }); callback(null, {results: message, priority: 2}); @@ -265,26 +263,25 @@ function init(ctx) { function getIob(sbx) { if (sbx.properties.iob && sbx.properties.iob.iob !== 0) { - return translate('alexaIobUnits', { + return translate('virtAsstIobUnits', { params: [ utils.toFixed(sbx.properties.iob.iob) ] }); } - return translate('alexaNoInsulin'); + return translate('virtAsstNoInsulin'); } - iob.alexa = { + iob.virtAsst = { rollupHandlers: [{ rollupGroup: 'Status' , rollupName: 'current iob' - , rollupHandler: alexaIOBRollupHandler + , rollupHandler: virtAsstIOBRollupHandler }] , intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['iob', 'insulin on board'] - , intentHandler: alexaIOBIntentHandler + , metrics: ['iob', 'insulin on board'] + , intentHandler: virtAsstIOBIntentHandler }] }; diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js index fc8dcbb3282..46d16738787 100644 --- a/lib/plugins/loop.js +++ b/lib/plugins/loop.js @@ -9,6 +9,7 @@ var levels = require('../levels'); function init (ctx) { var utils = require('../utils')(ctx); + var translate = ctx.language.translate; var loop = { name: 'loop' @@ -173,6 +174,102 @@ function init (ctx) { } }; + loop.getEventTypes = function getEventTypes (sbx) { + + var units = sbx.settings.units; + console.log('units', units); + + var reasonconf = []; + + if (sbx.data === undefined || sbx.data.profile === undefined || sbx.data.profile.data.length == 0) { + return []; + } + + let profile = sbx.data.profile.data[0]; + + if (profile.loopSettings === undefined || profile.loopSettings.overridePresets == undefined) { + return []; + } + + let presets = profile.loopSettings.overridePresets; + + for (var i = 0; i < presets.length; i++) { + let preset = presets[i] + reasonconf.push({ name: preset.name, displayName: preset.symbol + " " + preset.name, duration: preset.duration / 60}); + } + + var postLoopNotification = function (client, data, callback) { + + $.ajax({ + method: "POST" + , headers: client.headers() + , url: '/api/v2/notifications/loop' + , data: data + }) + .done(function () { + callback(); + }) + .fail(function (jqXHR) { + callback(jqXHR.responseText); + }); + } + + // TODO: add OTP entry + + return [ + { + val: 'Temporary Override' + , name: 'Temporary Override' + , bg: false + , insulin: false + , carbs: false + , prebolus: false + , duration: true + , percent: false + , absolute: false + , profile: false + , split: false + , targets: false + , reasons: reasonconf + , otp: true + , submitHook: postLoopNotification + }, + { + val: 'Temporary Override Cancel' + , name: 'Temporary Override Cancel' + , bg: false + , insulin: false + , carbs: false + , prebolus: false + , duration: false + , percent: false + , absolute: false + , profile: false + , split: false + , targets: false + , submitHook: postLoopNotification + }, + { + val: 'Remote Carbs Entry' + , name: 'Remote Carbs Entry' + , remoteCarbs: true + , remoteAbsorption: true + , otp: true + , submitHook: postLoopNotification + }, + { + val: 'Remote Bolus Entry' + , name: 'Remote Bolus Entry' + , remoteBolus: true + , otp: true + , submitHook: postLoopNotification + } + ]; + }; + + // TODO: Add event listener to customize labels + + loop.updateVisualisation = function updateVisualisation (sbx) { var prop = sbx.properties.loop; @@ -284,9 +381,9 @@ function init (ctx) { var iob = prop.lastLoop.iob; valueParts = valueParts.concat([ ', IOB: ' - + , sbx.roundInsulinForDisplayFormat(iob.iob) + 'U' - + , iob.basaliob ? ', Basal IOB ' + sbx.roundInsulinForDisplayFormat(iob.basaliob) + 'U' : '' ]); } @@ -431,7 +528,7 @@ function init (ctx) { } }; - function alexaForecastHandler (next, slots, sbx) { + function virtAsstForecastHandler (next, slots, sbx) { if (sbx.properties.loop.lastLoop.predicted) { var forecast = sbx.properties.loop.lastLoop.predicted.values; var max = forecast[0]; @@ -441,7 +538,7 @@ function init (ctx) { var startPrediction = moment(sbx.properties.loop.lastLoop.predicted.startDate); var endPrediction = startPrediction.clone().add(maxForecastIndex * 5, 'minutes'); if (endPrediction.valueOf() < sbx.time) { - next('Loop Forecast', 'Unable to forecast with the data that is available'); + next(translate('virtAsstTitleLoopForecast'), translate('virtAsstForecastUnavailable')); } else { for (var i = 1, len = forecast.slice(0, maxForecastIndex).length; i < len; i++) { if (forecast[i] > max) { @@ -451,35 +548,52 @@ function init (ctx) { min = forecast[i]; } } - var value = ''; + var response = ''; if (min === max) { - value = 'around ' + max; + response = translate('virtAsstLoopForecastAround', { + params: [ + max + , moment(endPrediction).from(moment(sbx.time)) + ] + }); } else { - value = 'between ' + min + ' and ' + max; + response = translate('virtAsstLoopForecastBetween', { + params: [ + min + , max + , moment(endPrediction).from(moment(sbx.time)) + ] + }); } - var response = 'According to the loop forecast you are expected to be ' + value + ' over the next ' + moment(endPrediction).from(moment(sbx.time)); - next('Loop Forecast', response); + next(translate('virtAsstTitleLoopForecast'), response); } } else { - next('Loop forecast', 'Loop plugin does not seem to be enabled'); + next(translate('virtAsstTitleLoopForecast'), translate('virtAsstUnknown')); } } - function alexaLastLoopHandler (next, slots, sbx) { - console.log(JSON.stringify(sbx.properties.loop.lastLoop)); - var response = 'The last successful loop was ' + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)); - next('Last loop', response); + function virtAsstLastLoopHandler (next, slots, sbx) { + if (sbx.properties.loop.lastLoop) { + console.log(JSON.stringify(sbx.properties.loop.lastLoop)); + var response = translate('virtAsstLastLoop', { + params: [ + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)) + ] + }); + next(translate('virtAsstTitleLastLoop'), response); + } else { + next(translate('virtAsstTitleLastLoop'), translate('virtAsstUnknown')); + } } - loop.alexa = { + loop.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['loop forecast', 'forecast'] - , intentHandler: alexaForecastHandler + , metrics: ['loop forecast', 'forecast'] + , intentHandler: virtAsstForecastHandler }, { intent: 'LastLoop' - , intentHandler: alexaLastLoopHandler + , intentHandler: virtAsstLastLoopHandler }] }; diff --git a/lib/plugins/mmconnect.js b/lib/plugins/mmconnect.js index 342c68ef2ea..c6321e9f7f3 100644 --- a/lib/plugins/mmconnect.js +++ b/lib/plugins/mmconnect.js @@ -4,16 +4,16 @@ var _ = require('lodash'), connect = require('minimed-connect-to-nightscout'); -function init (env, entries, devicestatus) { +function init (env, entries, devicestatus, bus) { if (env.extendedSettings.mmconnect && env.extendedSettings.mmconnect.userName && env.extendedSettings.mmconnect.password) { - return {run: makeRunner(env, entries, devicestatus)}; + return {run: makeRunner(env, entries, devicestatus, bus)}; } else { console.info('MiniMed Connect not enabled'); return null; } } -function makeRunner (env, entries, devicestatus) { +function makeRunner (env, entries, devicestatus, bus) { var options = getOptions(env); var client = connect.carelink.Client(options); @@ -22,9 +22,15 @@ function makeRunner (env, entries, devicestatus) { var handleData = makeHandler_(entries, devicestatus, options.sgvLimit, options.storeRawData); return function run () { - setInterval(function() { + let timer = setInterval(function() { client.fetch(handleData); }, options.interval); + + if (bus) { + bus.on('teardown', function serverTeardown () { + clearInterval(timer); + }); + } }; } diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index a9c45db7e1b..037680960d2 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -4,6 +4,7 @@ var _ = require('lodash'); var moment = require('moment'); var times = require('../times'); var levels = require('../levels'); +var consts = require('../constants'); // var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'meal-assist', 'freq', 'rssi']; Unused variable @@ -54,8 +55,8 @@ function init (ctx) { , urgent: settings.urgent ? settings.urgent : 60 , enableAlerts: settings.enableAlerts , predIOBColor: settings.predIobColor ? settings.predIobColor : '#1e88e5' - , predCOBColor: settings.predCobColor ? settings.predCobColor : '#FB8C00FF' - , predACOBColor: settings.predAcobColor ? settings.predAcobColor : '#FB8C0080' + , predCOBColor: settings.predCobColor ? settings.predCobColor : '#FB8C00' + , predACOBColor: settings.predAcobColor ? settings.predAcobColor : '#FB8C00' , predZTColor: settings.predZtColor ? settings.predZtColor : '#00d2d2' , predUAMColor: settings.predUamColor ? settings.predUamColor : '#c9bd60' , colorPredictionLines: settings.colorPredictionLines @@ -122,12 +123,42 @@ function init (ctx) { } function toMoments (status) { + var enacted = false; + var notEnacted = false; + if (status.openaps.enacted && status.openaps.enacted.timestamp && (status.openaps.enacted.recieved || status.openaps.enacted.received)) { + if (status.openaps.enacted.mills) { + enacted = moment(status.openaps.enacted.mills); + } else { + enacted = moment(status.openaps.enacted.timestamp); + } + } else if (status.openaps.enacted && status.openaps.enacted.timestamp && !(status.openaps.enacted.recieved || status.openaps.enacted.received)) { + if (status.openaps.enacted.mills) { + notEnacted = moment(status.openaps.enacted.mills) + } else { + notEnacted = moment(status.openaps.enacted.timestamp) + } + } + + var suggested = false; + if (status.openaps.suggested && status.openaps.suggested.mills) { + suggested = moment(status.openaps.suggested.mills); + } else if (status.openaps.suggested && status.openaps.suggested.timestamp) { + suggested = moment(status.openaps.suggested.timestamp); + } + + var iob = false; + if (status.openaps.iob && status.openaps.iob.mills) { + iob = moment(status.openaps.iob.mills); + } else if (status.openaps.iob && status.openaps.iob.timestamp) { + iob = moment(status.openaps.iob.timestamp); + } + return { when: moment(status.mills) - , enacted: status.openaps.enacted && status.openaps.enacted.timestamp && (status.openaps.enacted.recieved || status.openaps.enacted.received) && moment(status.openaps.enacted.timestamp) - , notEnacted: status.openaps.enacted && status.openaps.enacted.timestamp && !(status.openaps.enacted.recieved || status.openaps.enacted.received) && moment(status.openaps.enacted.timestamp) - , suggested: status.openaps.suggested && status.openaps.suggested.timestamp && moment(status.openaps.suggested.timestamp) - , iob: status.openaps.iob && status.openaps.iob.timestamp && moment(status.openaps.iob.timestamp) + , enacted + , notEnacted + , suggested + , iob }; } @@ -174,7 +205,11 @@ function init (ctx) { var enacted = status.openaps && status.openaps.enacted; if (enacted && moments.enacted && (!result.lastEnacted || moments.enacted.isAfter(result.lastEnacted.moment))) { - enacted.moment = moment(enacted.timestamp); + if (enacted.mills) { + enacted.moment = moment(enacted.mills); + } else { + enacted.moment = moment(enacted.timestamp); + } result.lastEnacted = enacted; if (enacted.predBGs && (!result.lastPredBGs || enacted.moment.isAfter(result.lastPredBGs.moment))) { result.lastPredBGs = _.isArray(enacted.predBGs) ? { values: enacted.predBGs } : enacted.predBGs; @@ -183,13 +218,21 @@ function init (ctx) { } if (enacted && moments.notEnacted && (!result.lastNotEnacted || moments.notEnacted.isAfter(result.lastNotEnacted.moment))) { - enacted.moment = moment(enacted.timestamp); + if (enacted.mills) { + enacted.moment = moment(enacted.mills); + } else { + enacted.moment = moment(enacted.timestamp); + } result.lastNotEnacted = enacted; } var suggested = status.openaps && status.openaps.suggested; if (suggested && moments.suggested && (!result.lastSuggested || moments.suggested.isAfter(result.lastSuggested.moment))) { - suggested.moment = moment(suggested.timestamp); + if (suggested.mills) { + suggested.moment = moment(suggested.mills); + } else { + suggested.moment = moment(suggested.timestamp); + } result.lastSuggested = suggested; if (suggested.predBGs && (!result.lastPredBGs || suggested.moment.isAfter(result.lastPredBGs.moment))) { result.lastPredBGs = _.isArray(suggested.predBGs) ? { values: suggested.predBGs } : suggested.predBGs; @@ -349,8 +392,10 @@ function init (ctx) { function addSuggestion () { if (prop.lastSuggested) { var bg = prop.lastSuggested.bg; - if (sbx.data.profile.data[0].units === 'mmol') { - bg = Math.round(bg / 18 * 10) / 10; + var units = sbx.data.profile.getUnits(); + + if (units === 'mmol') { + bg = Math.round(bg / consts.MMOL_TO_MGDL * 10) / 10; } var valueParts = [ @@ -514,36 +559,41 @@ function init (ctx) { } }; - function alexaForecastHandler (next, slots, sbx) { + function virtAsstForecastHandler (next, slots, sbx) { if (sbx.properties.openaps && sbx.properties.openaps.lastEventualBG) { - var response = translate('alexaOpenAPSForecast', { + var response = translate('virtAsstOpenAPSForecast', { params: [ sbx.properties.openaps.lastEventualBG ] }); - next('Loop Forecast', response); + next(translate('virtAsstTitleOpenAPSForecast'), response); + } else { + next(translate('virtAsstTitleOpenAPSForecast'), translate('virtAsstUnknown')); } } - function alexaLastLoopHandler (next, slots, sbx) { - console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); - var response = translate('alexaLastLoop', { - params: [ - moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) - ] - }); - next('Last loop', response); + function virtAsstLastLoopHandler (next, slots, sbx) { + if (sbx.properties.openaps.lastLoopMoment) { + console.log(JSON.stringify(sbx.properties.openaps.lastLoopMoment)); + var response = translate('virtAsstLastLoop', { + params: [ + moment(sbx.properties.openaps.lastLoopMoment).from(moment(sbx.time)) + ] + }); + next(translate('virtAsstTitleLastLoop'), response); + } else { + next(translate('virtAsstTitleLastLoop'), translate('virtAsstUnknown')); + } } - openaps.alexa = { + openaps.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot: 'metric' - , slots: ['openaps forecast', 'forecast'] - , intentHandler: alexaForecastHandler + , metrics: ['openaps forecast', 'forecast'] + , intentHandler: virtAsstForecastHandler }, { intent: 'LastLoop' - , intentHandler: alexaLastLoopHandler + , intentHandler: virtAsstLastLoopHandler }] }; diff --git a/lib/plugins/pluginbase.js b/lib/plugins/pluginbase.js index a6f0e77e455..0e2adad3586 100644 --- a/lib/plugins/pluginbase.js +++ b/lib/plugins/pluginbase.js @@ -10,7 +10,7 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { var pluginBase = { }; pluginBase.forecastInfos = []; - pluginBase.forecastPoints = []; + pluginBase.forecastPoints = {}; function findOrCreatePill (plugin) { var container = null; @@ -84,9 +84,9 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { }).join('
\n'); pill.mouseover(function pillMouseover (event) { - tooltip.transition().duration(200).style('opacity', .9); + tooltip.style('opacity', .9); - var windowWidth = $(tooltip).parent().parent().width(); + var windowWidth = $(tooltip.node()).parent().parent().width(); var left = event.pageX + TOOLTIP_WIDTH < windowWidth ? event.pageX : windowWidth - TOOLTIP_WIDTH - 10; tooltip.html(html) .style('left', left + 'px') @@ -94,9 +94,7 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { }); pill.mouseout(function pillMouseout ( ) { - tooltip.transition() - .duration(200) - .style('opacity', 0); + tooltip.style('opacity', 0); }); } else { pill.off('mouseover'); @@ -113,7 +111,7 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { }); pluginBase.forecastInfos.push(info); - pluginBase.forecastPoints = pluginBase.forecastPoints.concat(points); + pluginBase.forecastPoints[info.type] = points; }; return pluginBase; diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js index 842d8536b6b..7e71c21e1b1 100644 --- a/lib/plugins/pump.js +++ b/lib/plugins/pump.js @@ -135,38 +135,58 @@ function init (ctx) { }); }; - function alexaReservoirHandler (next, slots, sbx) { - var response = translate('alexaReservoir', { + function virtAsstReservoirHandler (next, slots, sbx) { + var reservoir = sbx.properties.pump.pump.reservoir; + if (reservoir || reservoir === 0) { + var response = translate('virtAsstReservoir', { params: [ - sbx.properties.pump.pump.reservoir + reservoir ] - }); - next('Remaining insulin', response); + }); + next(translate('virtAsstTitlePumpReservoir'), response); + } else { + next(translate('virtAsstTitlePumpReservoir'), translate('virtAsstUnknown')); + } } - function alexaBatteryHandler (next, slots, sbx) { + function virtAsstBatteryHandler (next, slots, sbx) { var battery = _.get(sbx, 'properties.pump.data.battery'); if (battery) { - var response = translate('alexaPumpBattery', { + var response = translate('virtAsstPumpBattery', { params: [ battery.value, battery.unit ] }); - next('Pump battery', response); + next(translate('virtAsstTitlePumpBattery'), response); } else { - next(); + next(translate('virtAsstTitlePumpBattery'), translate('virtAsstUnknown')); } } - pump.alexa = { - intentHandlers:[{ - intent: 'InsulinRemaining', - intentHandler: alexaReservoirHandler - }, { - intent: 'PumpBattery', - intentHandler: alexaBatteryHandler - }] + pump.virtAsst = { + intentHandlers:[ + { + // backwards compatibility + intent: 'InsulinRemaining', + intentHandler: virtAsstReservoirHandler + } + , { + // backwards compatibility + intent: 'PumpBattery', + intentHandler: virtAsstBatteryHandler + } + , { + intent: 'MetricNow' + , metrics: ['pump reservoir'] + , intentHandler: virtAsstReservoirHandler + } + , { + intent: 'MetricNow' + , metrics: ['pump battery'] + , intentHandler: virtAsstBatteryHandler + } + ] }; function statusClass (level) { diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index f19e669f63b..3248126b046 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -106,17 +106,24 @@ function init (ctx) { return display; }; - function alexaRawBGHandler (next, slots, sbx) { - var response = 'Your raw bg is ' + sbx.properties.rawbg.mgdl; - next('Current Raw BG', response); + function virtAsstRawBGHandler (next, slots, sbx) { + if (sbx.properties.rawbg.mgdl) { + var response = translate('virtAsstRawBG', { + params: [ + sbx.properties.rawbg.mgdl + ] + }); + next(translate('virtAsstTitleRawBG'), response); + } else { + next(translate('virtAsstTitleRawBG'), translate('virtAsstUnknown')); + } } - rawbg.alexa = { + rawbg.virtAsst = { intentHandlers: [{ intent: 'MetricNow' - , routableSlot:'metric' - , slots:['raw bg', 'raw blood glucose'] - , intentHandler: alexaRawBGHandler + , metrics:['raw bg', 'raw blood glucose'] + , intentHandler: virtAsstRawBGHandler }] }; diff --git a/lib/plugins/timeago.js b/lib/plugins/timeago.js index 6f8989a4876..4b713a60217 100644 --- a/lib/plugins/timeago.js +++ b/lib/plugins/timeago.js @@ -3,7 +3,7 @@ var levels = require('../levels'); var times = require('../times'); var lastChecked = new Date(); -var lastSuspendTime = new Date("1900-01-01"); +var lastRecoveryTimeFromSuspend = new Date("1900-01-01"); function init(ctx) { var translate = ctx.language.translate; @@ -64,20 +64,26 @@ function init(ctx) { }; timeago.checkStatus = function checkStatus(sbx) { - // Check if the app has been suspended; if yes, snooze data missing alarmn for 15 seconds var now = new Date(); var delta = now.getTime() - lastChecked.getTime(); lastChecked = now; - if (delta > 15 * 1000) { // Looks like we've been hibernating - lastSuspendTime = now; - } + function isHibernationDetected() { + if (sbx.runtimeEnvironment === 'client') { + if (delta > 20 * 1000) { // Looks like we've been hibernating + lastRecoveryTimeFromSuspend = now; + } + var timeSinceLastRecovered = now.getTime() - lastRecoveryTimeFromSuspend.getTime(); + return timeSinceLastRecovered < (10 * 1000); + } - var timeSinceLastSuspended = now.getTime() - lastSuspendTime.getTime(); + // Assume server never hibernates, or if it does, it's alarm-worthy + return false; - if (timeSinceLastSuspended < (10 * 1000)) { + } + if (isHibernationDetected()) { console.log('Hibernation detected, suspending timeago alarm'); return 'current'; } diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index eda42a3901f..dc603054ecb 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -4,7 +4,8 @@ var _ = require('lodash'); var times = require('../times'); var levels = require('../levels'); -function init() { +function init(ctx) { + var translate = ctx.language.translate; var upbat = { name: 'upbat' @@ -221,16 +222,32 @@ function init() { }); }; - function alexaUploaderBatteryHandler (next, slots, sbx) { - var response = 'Your uploader battery is at ' + sbx.properties.upbat.display; - next('Uploader battery', response); + function virtAsstUploaderBatteryHandler (next, slots, sbx) { + if (sbx.properties.upbat.display) { + var response = translate('virtAsstUploaderBattery', { + params: [ + sbx.properties.upbat.display + ] + }); + next(translate('virtAsstTitleUploaderBattery'), response); + } else { + next(translate('virtAsstTitleUploaderBattery'), translate('virtAsstUnknown')); + } } - upbat.alexa = { - intentHandlers: [{ - intent: 'UploaderBattery' - , intentHandler: alexaUploaderBatteryHandler - }] + upbat.virtAsst = { + intentHandlers: [ + { + // for backwards compatibility + intent: 'UploaderBattery' + , intentHandler: virtAsstUploaderBatteryHandler + } + , { + intent: 'MetricNow' + , metrics: ['uploader battery'] + , intentHandler: virtAsstUploaderBatteryHandler + } + ] }; return upbat; diff --git a/lib/plugins/virtAsstBase.js b/lib/plugins/virtAsstBase.js new file mode 100644 index 00000000000..e0d103672a2 --- /dev/null +++ b/lib/plugins/virtAsstBase.js @@ -0,0 +1,111 @@ +'use strict'; + +var moment = require('moment'); +var _each = require('lodash/each'); + +function init(env, ctx) { + function virtAsstBase() { + return virtAsstBase; + } + + var entries = ctx.entries; + var translate = ctx.language.translate; + + virtAsstBase.setupMutualIntents = function (configuredPlugin) { + // full status + configuredPlugin.addToRollup('Status', function (slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time)) + ] + }); + + callback(null, {results: status, priority: -1}); + }); + }, 'BG Status'); + + configuredPlugin.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { + configuredPlugin.getRollup('Status', sbx, slots, locale, function (status) { + callback(translate('virtAsstTitleFullStatus'), status); + }); + }); + + // blood sugar and direction + configuredPlugin.configureIntentHandler('MetricNow', function (callback, slots, sbx) { + entries.list({count: 1}, function(err, records) { + var direction; + if(translate(records[0].direction)){ + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time))] + }); + + callback(translate('virtAsstTitleCurrentBG'), status); + }); + }, ['bg', 'blood glucose', 'number']); + + // blood sugar delta + configuredPlugin.configureIntentHandler('MetricNow', function (callback, slots, sbx) { + if (sbx.properties.delta && sbx.properties.delta.display) { + entries.list({count: 2}, function(err, records) { + callback( + translate('virtAsstTitleDelta'), + translate('virtAsstDelta', { + params: [ + sbx.properties.delta.display == '+0' ? '0' : sbx.properties.delta.display, + moment(records[0].date).from(moment(sbx.time)), + moment(records[1].date).from(moment(sbx.time)) + ] + }) + ); + }); + } else { + callback(translate('virtAsstTitleDelta'), translate('virtAsstUnknown')); + } + }, ['delta']); + }; + + virtAsstBase.setupVirtAsstHandlers = function (configuredPlugin) { + ctx.plugins.eachEnabledPlugin(function (plugin){ + if (plugin.virtAsst) { + if (plugin.virtAsst.intentHandlers) { + console.log('Plugin "' + plugin.name + '" supports Virtual Assistants'); + _each(plugin.virtAsst.intentHandlers, function (route) { + if (route) { + configuredPlugin.configureIntentHandler(route.intent, route.intentHandler, route.metrics); + } + }); + } + if (plugin.virtAsst.rollupHandlers) { + console.log('Plugin "' + plugin.name + '" supports rollups for Virtual Assistants'); + _each(plugin.virtAsst.rollupHandlers, function (route) { + if (route) { + configuredPlugin.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); + } + }); + } + } else { + console.log('Plugin "' + plugin.name + '" does not support Virtual Assistants'); + } + }); + }; + + return virtAsstBase; +} + +module.exports = init; diff --git a/lib/plugins/xdrip-js.js b/lib/plugins/xdripjs.js similarity index 69% rename from lib/plugins/xdrip-js.js rename to lib/plugins/xdripjs.js index d66f112cfcf..dc44aad2988 100644 --- a/lib/plugins/xdrip-js.js +++ b/lib/plugins/xdripjs.js @@ -9,9 +9,10 @@ function init(ctx) { var utils = require('../utils')(ctx); var firstPrefs = true; var lastStateNotification = null; + var translate = ctx.language.translate; var sensorState = { - name: 'xdrip-js' + name: 'xdripjs' , label: 'CGM Status' , pluginType: 'pill-status' }; @@ -25,7 +26,7 @@ function init(ctx) { if (firstPrefs) { firstPrefs = false; - console.info('xdrip-js Prefs:', prefs); + console.info('xdripjs Prefs:', prefs); } return prefs; @@ -154,8 +155,8 @@ function init(ctx) { }; } - message = 'CGM state: ' + sensorInfo.xdripjs.stateString; - title = 'CGM state: ' + sensorInfo.xdripjs.stateString; + message = 'CGM Transmitter state: ' + sensorInfo.xdripjs.stateString; + title = 'CGM Transmitter state: ' + sensorInfo.xdripjs.stateString; if (sensorInfo.xdripjs.state == 0x7) { // If it is a calibration request, only use INFO @@ -167,15 +168,15 @@ function init(ctx) { if (sensorInfo.xdripjs.voltagea && (sensorInfo.xdripjs.voltagea < prefs.warnBatV)) { sendNotification = true; - message = 'CGM Battery A Low Voltage: ' + sensorInfo.xdripjs.voltagea; - title = 'CGM Battery Low'; + message = 'CGM Transmitter Battery A Low Voltage: ' + sensorInfo.xdripjs.voltagea; + title = 'CGM Transmitter Battery Low'; result.level = levels.WARN; } if (sensorInfo.xdripjs.voltageb && (sensorInfo.xdripjs.voltageb < (prefs.warnBatV - 10))) { sendNotification = true; - message = 'CGM Battery B Low Voltage: ' + sensorInfo.xdripjs.voltageb; - title = 'CGM Battery Low'; + message = 'CGM Transmitter Battery B Low Voltage: ' + sensorInfo.xdripjs.voltageb; + title = 'CGM Transmitter Battery Low'; result.level = levels.WARN; } @@ -321,6 +322,115 @@ function init(ctx) { } }; + function virtAsstGenericCGMHandler(translateItem, field, next, sbx) { + var response; + if (sbx.properties.sensorState && sbx.properties.sensorState[field]) { + response = translate('virtAsstCGM'+translateItem, { + params:[ + sbx.properties.sensorState[field] + , moment(sbx.properties.sensorState.lastStateTime).from(moment(sbx.time)) + ] + }); + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGM'+translateItem), response); + } + + sensorState.virtAsst = { + intentHandlers: [ + { + intent: 'MetricNow' + , metrics: ['cgm mode'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Mode', 'lastMode', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm status'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Status', 'lastStateString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm session age'] + , intentHandler: function(next, slots, sbx){ + var response; + // session start is only valid if in a session + if (sbx.properties.sensorState && sbx.properties.sensorState.lastSessionStart) { + if (sbx.properties.sensorState.lastState != 0x1) { + var duration = moment.duration(moment().diff(moment(sbx.properties.sensorState.lastSessionStart))); + response = translate('virtAsstCGMSessAge', { + params: [ + duration.days(), + duration.hours() + ] + }); + } else { + response = translate('virtAsstCGMSessNotStarted'); + } + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGMSessAge'), response); + } + } + , { + intent: 'MetricNow' + , metrics: ['cgm tx status'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('TxStatus', 'lastTxStatusString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm tx age'] + , intentHandler: function(next, slots, sbx){ + next( + translate('virtAsstTitleCGMTxAge'), + (sbx.properties.sensorState && sbx.properties.sensorState.lastTxActivation) + ? translate('virtAsstCGMTxAge', {params:[moment().diff(moment(sbx.properties.sensorState.lastTxActivation), 'days')]}) + : translate('virtAsstUnknown') + ); + } + } + , { + intent: 'MetricNow' + , metrics: ['cgm noise'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Noise', 'lastNoiseString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm battery'] + , intentHandler: function(next, slots, sbx){ + var response; + var sensor = sbx.properties.sensorState; + if (sensor && (sensor.lastVoltageA || sensor.lastVoltageB)) { + if (sensor.lastVoltageA && sensor.lastVoltageB) { + response = translate('virtAsstCGMBattTwo', { + params:[ + (sensor.lastVoltageA / 100) + , (sensor.lastVoltageB / 100) + , moment(sensor.lastBatteryTimestamp).from(moment(sbx.time)) + ] + }); + } else { + var finalValue = sensor.lastVoltageA ? sensor.lastVoltageA : sensor.lastVoltageB; + response = translate('virtAsstCGMBattOne', { + params:[ + (finalValue / 100) + , moment(sensor.lastBatteryTimestamp).from(moment(sbx.time)) + ] + }); + } + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGMBatt'), response); + } + } + ] + }; + return sensorState; } diff --git a/lib/profilefunctions.js b/lib/profilefunctions.js index c15860bab26..9fac4984949 100644 --- a/lib/profilefunctions.js +++ b/lib/profilefunctions.js @@ -4,16 +4,20 @@ var _ = require('lodash'); var moment = require('moment-timezone'); var c = require('memory-cache'); var times = require('./times'); -var crypto = require('crypto'); - -var cacheTTL = 600; +var cacheTTL = 5000; var prevBasalTreatment = null; +var cache = new c.Cache(); function init (profileData) { var profile = {}; - var cache = new c.Cache(); + + profile.clear = function clear() { + cache.clear(); + profile.data = null; + prevBasalTreatment = null; + } profile.loadData = function loadData (profileData) { if (profileData && profileData.length) { @@ -57,6 +61,9 @@ function init (profileData) { // preprocess the timestamps to seconds for a couple orders of magnitude faster operation profile.preprocessProfileOnLoad = function preprocessProfileOnLoad (container) { _.each(container, function eachValue (value) { + + if (value === null) return; + if (Object.prototype.toString.call(value) === '[object Array]') { profile.preprocessProfileOnLoad(value); } @@ -71,6 +78,15 @@ function init (profileData) { profile.getValueByTime = function getValueByTime (time, valueType, spec_profile) { if (!time) { time = Date.now(); } + //round to the minute for better caching + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = (minuteTime + valueType + spec_profile); + var returnValue = cache.get(cacheKey); + + if (returnValue) { + return returnValue; + } + // CircadianPercentageProfile support var timeshift = 0; var percentage = 100; @@ -83,16 +99,6 @@ function init (profileData) { var offset = timeshift % 24; time = time + offset * times.hours(offset).msecs; - //round to the minute for better caching - var minuteTime = Math.round(time / 60000) * 60000; - - var cacheKey = (minuteTime + valueType + spec_profile + profile.profiletreatments_hash); - var returnValue = cache.get(cacheKey); - - if (returnValue) { - return returnValue; - } - var valueContainer = profile.getCurrentProfile(time, spec_profile)[valueType]; // Assumes the timestamps are in UTC @@ -139,14 +145,30 @@ function init (profileData) { }; profile.getCurrentProfile = function getCurrentProfile (time, spec_profile) { - time = time || new Date().getTime(); - var data = profile.hasData() ? profile.data[0] : null; - var timeprofile = spec_profile || profile.activeProfileToTime(time); - return data && data.store[timeprofile] ? data.store[timeprofile] : {}; + + time = time || Date.now(); + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = ("profile" + minuteTime + spec_profile); + var returnValue = cache.get(cacheKey); + + if (returnValue) { + return returnValue; + } + + var pdataActive = profile.profileFromTime(time); + var data = profile.hasData() ? pdataActive : null; + var timeprofile = profile.activeProfileToTime(time); + returnValue = data && data.store[timeprofile] ? data.store[timeprofile] : {}; + + cache.put(cacheKey, returnValue, cacheTTL); + return returnValue; + }; profile.getUnits = function getUnits (spec_profile) { - return profile.getCurrentProfile(null, spec_profile)['units']; + var pu = profile.getCurrentProfile(null, spec_profile)['units'] + ' '; + if (pu.toLowerCase().includes('mmol')) return 'mmol'; + return 'mg/dl'; }; profile.getTimezone = function getTimezone (spec_profile) { @@ -202,17 +224,19 @@ function init (profileData) { }); profile.combobolustreatments = combobolustreatments || []; - profile.profiletreatments_hash = crypto.createHash('sha1').update(JSON.stringify(profile.profiletreatments)).digest('hex'); - profile.tempbasaltreatments_hash = crypto.createHash('sha1').update(JSON.stringify(profile.tempbasaltreatments)).digest('hex'); - profile.combobolustreatments_hash = crypto.createHash('sha1').update(JSON.stringify(profile.combobolustreatments)).digest('hex'); + + cache.clear(); }; profile.activeProfileToTime = function activeProfileToTime (time) { if (profile.hasData()) { - var timeprofile = profile.data[0].defaultProfile; time = Number(time) || new Date().getTime(); + + var pdataActive = profile.profileFromTime(time); + var timeprofile = pdataActive.defaultProfile; var treatment = profile.activeProfileTreatmentToTime(time); - if (treatment && profile.data[0].store && profile.data[0].store[treatment.profile]) { + + if (treatment && pdataActive.store && pdataActive.store[treatment.profile]) { timeprofile = treatment.profile; } return timeprofile; @@ -221,9 +245,10 @@ function init (profileData) { }; profile.activeProfileTreatmentToTime = function activeProfileTreatmentToTime (time) { - var cacheKey = 'profile' + time + profile.profiletreatments_hash; - //var returnValue = profile.timeValueCache[cacheKey]; - var returnValue; + + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = 'profileCache' + minuteTime; + var returnValue = cache.get(cacheKey); if (returnValue) { return returnValue; @@ -231,30 +256,31 @@ function init (profileData) { var treatment = null; if (profile.hasData()) { - profile.profiletreatments.forEach(function eachTreatment (t) { - if (time >= t.mills && t.mills >= profile.data[0].mills) { - var duration = times.mins(t.duration || 0).msecs; - if (duration != 0 && time < t.mills + duration) { - treatment = t; - // if profile switch contains json of profile inject it in to store to be findable by profile name - if (treatment.profileJson && !profile.data[0].store[treatment.profile]) { - if (treatment.profile.indexOf("@@@@@") < 0) - treatment.profile += "@@@@@" + treatment.mills; - let json = JSON.parse(treatment.profileJson); - profile.data[0].store[treatment.profile] = json; - } + var pdataActive = profile.profileFromTime(time); + profile.profiletreatments.forEach(function eachTreatment(t) { + if (time >= t.mills && t.mills >= pdataActive.mills) { + var duration = times.mins(t.duration || 0).msecs; + if (duration != 0 && time < t.mills + duration) { + treatment = t; + // if profile switch contains json of profile inject it in to store to be findable by profile name + if (treatment.profileJson && !pdataActive.store[treatment.profile]) { + if (treatment.profile.indexOf("@@@@@") < 0) + treatment.profile += "@@@@@" + treatment.mills; + let json = JSON.parse(treatment.profileJson); + pdataActive.store[treatment.profile] = json; + } + } + if (duration == 0) { + treatment = t; + // if profile switch contains json of profile inject it in to store to be findable by profile name + if (treatment.profileJson && !pdataActive.store[treatment.profile]) { + if (treatment.profile.indexOf("@@@@@") < 0) + treatment.profile += "@@@@@" + treatment.mills; + let json = JSON.parse(treatment.profileJson); + pdataActive.store[treatment.profile] = json; + } + } } - if (duration == 0) { - treatment = t; - // if profile switch contains json of profile inject it in to store to be findable by profile name - if (treatment.profileJson && !profile.data[0].store[treatment.profile]) { - if (treatment.profile.indexOf("@@@@@") < 0) - treatment.profile += "@@@@@" + treatment.mills; - let json = JSON.parse(treatment.profileJson); - profile.data[0].store[treatment.profile] = json; - } - } - } }); } @@ -269,6 +295,23 @@ function init (profileData) { else return name.substring(0, index); } + profile.profileFromTime = function profileFromTime (time) { + var profileData = null; + + if (profile.hasData()) { + profileData = profile.data[0]; + for (var i = 0; i < profile.data.length; i++) + { + if (Number(time) >= Number(profile.data[i].mills)) { + profileData = profile.data[i]; + break; + } + } + } + + return profileData; + } + profile.tempBasalTreatment = function tempBasalTreatment (time) { // Most queries for the data in reporting will match the latest found value, caching that hugely improves performance @@ -310,7 +353,8 @@ function init (profileData) { profile.getTempBasal = function getTempBasal (time, spec_profile) { - var cacheKey = 'basal' + time + profile.tempbasaltreatments_hash + profile.combobolustreatments_hash + profile.profiletreatments_hash + spec_profile; + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = 'basalCache' + minuteTime + spec_profile; var returnValue = cache.get(cacheKey); if (returnValue) { diff --git a/lib/report/predictions.js b/lib/report/predictions.js new file mode 100644 index 00000000000..8e341d0b666 --- /dev/null +++ b/lib/report/predictions.js @@ -0,0 +1,33 @@ +var predictions = { + offset: 0, + backward: function () { + this.offset -= 5; + this.updateOffsetHtml(); + }, + forward: function () { + this.offset += 5; + this.updateOffsetHtml(); + }, + moreBackward: function () { + this.offset -= 30; + this.updateOffsetHtml(); + }, + moreForward: function () { + this.offset += 30; + this.updateOffsetHtml(); + }, + reset: function () { + this.offset = 0; + this.updateOffsetHtml(); + }, + updateOffsetHtml: function () { + $('#rp_predictedOffset').html(this.offset); + } +}; + +$(document).on('change', '#rp_optionspredicted', function() { + $('#rp_predictedSettings').toggle(this.checked); + predictions.reset(); +}); + +module.exports = predictions; diff --git a/lib/report_plugins/calibrations.js b/lib/report_plugins/calibrations.js index 958dd06c906..baf16a47a27 100644 --- a/lib/report_plugins/calibrations.js +++ b/lib/report_plugins/calibrations.js @@ -146,20 +146,16 @@ calibrations.report = function report_calibrations (datastorage, sorteddaystosho calibration_context = charts.append('g'); // define the parts of the axis that aren't dependent on width or height - xScale2 = d3.scale.linear() + xScale2 = d3.scaleLinear() .domain([0, maxBG]); - yScale2 = d3.scale.linear() + yScale2 = d3.scaleLinear() .domain([0, 400000]); - var xAxis2 = d3.svg.axis() - .scale(xScale2) - .ticks(10) - .orient('bottom'); + var xAxis2 = d3.axisBottom(xScale2) + .ticks(10); - var yAxis2 = d3.svg.axis() - .scale(yScale2) - .orient('left'); + var yAxis2 = d3.axisLeft(yScale2); // get current data range var dataRange = [0, maxBG]; diff --git a/lib/report_plugins/daytoday.js b/lib/report_plugins/daytoday.js index 5f7983480c5..47c16cf71a8 100644 --- a/lib/report_plugins/daytoday.js +++ b/lib/report_plugins/daytoday.js @@ -23,17 +23,17 @@ daytoday.html = function html (client) { '

' + translate('Day to day') + '

' + '' + translate('To see this report, press SHOW while in this view') + '
' + translate('Display') + ': ' + - '' + translate('Insulin') + '' + - '' + translate('Carbs') + '' + - '' + translate('Basal rate') + '' + - '' + translate('Notes') + - '' + translate('Food') + - '' + translate('Raw') + '' + - '' + translate('IOB') + '' + - '' + translate('COB') + '' + - '' + translate('Predictions') + '' + - '' + translate('OpenAPS') + '' + - '' + translate('Insulin distribution') + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ' ' + translate('Size') + ' ' + '
' + translate('Scale') + ': ' + - '' + - translate('Linear') + - '' + - translate('Logarithmic') + + '' + + '' + '' + '
' + '
' + @@ -82,11 +82,12 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio var report_plugins = Nightscout.report_plugins; var scaledTreatmentBG = report_plugins.utils.scaledTreatmentBG; - var TOOLTIP_TRANS_MS = 300; - var padding = { top: 15, right: 22, bottom: 30, left: 35 }; var tddSum = 0; + var basalSum = 0; + var baseBasalSum = 0; + var bolusSum = 0; var carbsSum = 0; var proteinSum = 0; var fatSum = 0; @@ -97,22 +98,28 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio }); var tddAverage = tddSum / datastorage.alldays; + var basalAveragePercent = Math.round( (basalSum / datastorage.alldays) / tddAverage * 100); + var baseBasalAveragePercent = Math.round( (baseBasalSum / datastorage.alldays) / tddAverage * 100); + var bolusAveragePercent = Math.round( (bolusSum / datastorage.alldays) / tddAverage * 100); var carbsAverage = carbsSum / datastorage.alldays; var proteinAverage = proteinSum / datastorage.alldays; var fatAverage = fatSum / datastorage.alldays; - if (options.insulindistribution) - $('#daytodaycharts').append('

' + - '' + translate('TDD average') + ': ' + tddAverage.toFixed(1) + 'U ' + - '' + translate('Carbs average') + ': ' + carbsAverage.toFixed(0) + 'g ' + - '' + translate('Protein average') + ': ' + proteinAverage.toFixed(0) + 'g ' + - '' + translate('Fat average') + ': ' + fatAverage.toFixed(0) + 'g' - ); - - function timeTicks (n, i) { - var t12 = [ - '12am', '', '2am', '', '4am', '', '6am', '', '8am', '', '10am', '' - , '12pm', '', '2pm', '', '4pm', '', '6pm', '', '8pm', '', '10pm', '', '12am' + if (options.insulindistribution) { + var html = '

' + translate('TDD average') + ': ' + tddAverage.toFixed(1) + 'U  '; + html += '' + translate('Bolus average') + ': ' + bolusAveragePercent + '%  '; + html += '' + translate('Basal average') + ': ' + basalAveragePercent + '%  '; + html += '(' + translate('Base basal average:') + ' ' + baseBasalAveragePercent + '%)  '; + html += '' + translate('Carbs average') + ': ' + carbsAverage.toFixed(0) + 'g'; + html += '' + translate('Protein average') + ': ' + proteinAverage.toFixed(0) + 'g'; + html += '' + translate('Fat average') + ': ' + fatAverage.toFixed(0) + 'g'; + $('#daytodaycharts').append(html); + } + + function timeTicks(n,i) { + var t12 = [ + '12am', '', '2am', '', '4am', '', '6am', '', '8am', '', '10am', '', + '12pm', '', '2pm', '', '4pm', '', '6pm', '', '8pm', '', '10pm', '', '12am' ]; if (Nightscout.client.settings.timeFormat === 24) { return ('00' + i).slice(-2); @@ -170,37 +177,33 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio context = charts.append('g'); // define the parts of the axis that aren't dependent on width or height - xScale2 = d3.time.scale() + xScale2 = d3.scaleTime() .domain(d3.extent(data.sgv, dateFn)); if (options.scale === report_plugins.consts.SCALE_LOG) { - yScale2 = d3.scale.log() + yScale2 = d3.scaleLog() .domain([client.utils.scaleMgdl(options.basal ? 30 : 36), client.utils.scaleMgdl(420)]); } else { - yScale2 = d3.scale.linear() + yScale2 = d3.scaleLinear() .domain([client.utils.scaleMgdl(options.basal ? -40 : 36), client.utils.scaleMgdl(420)]); } // allow insulin to be negative (when plotting negative IOB) - yInsulinScale = d3.scale.linear() + yInsulinScale = d3.scaleLinear() .domain([-2 * options.maxInsulinValue, 2 * options.maxInsulinValue]); - yCarbsScale = d3.scale.linear() + yCarbsScale = d3.scaleLinear() .domain([0, options.maxCarbsValue * 1.25]); - yScaleBasals = d3.scale.linear(); + yScaleBasals = d3.scaleLinear(); - xAxis2 = d3.svg.axis() - .scale(xScale2) + xAxis2 = d3.axisBottom(xScale2) .tickFormat(timeTicks) - .ticks(24) - .orient('bottom'); + .ticks(24); - yAxis2 = d3.svg.axis() - .scale(yScale2) + yAxis2 = d3.axisLeft(yScale2) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('left'); + .tickValues(tickValues); // get current data range var dataRange = d3.extent(data.sgv, dateFn); @@ -294,7 +297,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio }) .on('mouseover', function(d) { if (options.openAps && d.openaps) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + client.tooltip.style('opacity', .9); var text = 'BG: ' + d.openaps.suggested.bg + ', ' + d.openaps.suggested.reason + (d.openaps.suggested.mealAssist ? ' Meal Assist: ' + d.openaps.suggested.mealAssist : ''); @@ -359,16 +362,14 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio for (var treatmentsIndex = 0; treatmentsIndex < treatmentsTimestamps.length; treatmentsIndex++) { var timestamp = treatmentsTimestamps[treatmentsIndex]; // TODO refactor code so this is set here - now set as global in file loaded by the browser - // eslint-disable-next-line no-undef - var predictedIndex = findPredicted(predictions, timestamp, predictedOffset); // Find predictions offset before or after timestamp + var predictedIndex = findPredicted(predictions, timestamp, Nightscout.predictions.offset); // Find predictions offset before or after timestamp if (predictedIndex != null) { entry = predictions[predictedIndex]; // Start entry var d = moment(entry.startDate); var end = moment().endOf('day'); if (options.predictedTruncate) { - // eslint-disable-next-line no-undef - if (predictedOffset >= 0) { + if (Nightscout.predictions.offset >= 0) { // If we are looking forward we want to stop at the next treatment if (treatmentsIndex < treatmentsTimestamps.length - 1) { end = moment(treatmentsTimestamps[treatmentsIndex + 1]); @@ -451,6 +452,8 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio data.netBasalNegative[hour] = 0; }); + profile.loadData(datastorage.profiles); + profile.updateTreatments(datastorage.profileSwitchTreatments, datastorage.tempbasalTreatments, datastorage.combobolusTreatments); var bolusInsulin = 0; @@ -602,13 +605,13 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio yScaleBasals.domain([basalMax, 0]); - var valueline = d3.svg.line() - .interpolate('step-after') + var valueline = d3.line() + .curve(d3.curveStepAfter) .x(function(d) { return xScale2(d.d) + padding.left; }) .y(function(d) { return yScaleBasals(d.b) + padding.top; }); - var area = d3.svg.area() - .interpolate('step-after') + var area = d3.area() + .curve(d3.curveStepAfter) .x(function(d) { return xScale2(d.d) + padding.left; }) .y0(yScaleBasals(0) + padding.top) .y1(function(d) { return yScaleBasals(d.b) + padding.top; }); @@ -735,8 +738,9 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio } if (treatment.carbs && options.carbs) { - var ic = profile.getCarbRatio(new Date(treatment.mills)); var label = ' ' + treatment.carbs + ' g'; + label += treatment.foodType ? ' ' + treatment.foodType : '' + label += treatment.absorptionTime ? ' ' + (Math.round(treatment.absorptionTime / 60.0 * 10) / 10) + 'h' : '' if (treatment.protein) label += ' / ' + treatment.protein + ' g'; if (treatment.fat) label += ' / ' + treatment.fat + ' g'; @@ -848,6 +852,26 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio .attr('y', yScale2(client.utils.scaleMgdl(306)) + padding.top) .attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left) .text(treatment.notes); + } else if (treatment.eventType === 'Temporary Override' && treatment.duration ) { + // Loop Overrides with duration + context.append('rect') + .attr('x', xScale2(treatment.mills) + padding.left) + .attr('y', yScale2(client.utils.scaleMgdl(432)) + padding.top) + .attr('width', xScale2(treatment.mills + times.mins(treatment.duration).msecs) - xScale2(treatment.mills)) + .attr('height', yScale2(client.utils.scaleMgdl(396)) - yScale2(client.utils.scaleMgdl(432))) + .attr('stroke-width', 1) + .attr('opacity', .2) + .attr('stroke', 'white') + .attr('fill', 'black'); + context.append('text') + .style('font-size', '12px') + .style('font-weight', 'bold') + .attr('fill', 'black') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .attr('y', yScale2(client.utils.scaleMgdl(414)) + padding.top) + .attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left) + .text(treatment.reason); } else if (!treatment.duration) { // other treatments without duration context.append('circle') @@ -932,9 +956,9 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio var height = 120; var radius = Math.min(width, height) / 2; - var color = d3.scale.ordinal().range([basalcolor, boluscolor]); + var color = d3.scaleOrdinal().range([basalcolor, boluscolor]); - var labelArc = d3.svg.arc() + var labelArc = d3.arc() .outerRadius(radius / 2) .innerRadius(radius / 2); @@ -946,10 +970,11 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); - var arc = d3.svg.arc() + var arc = d3.arc() + .innerRadius(0) .outerRadius(radius); - var pie = d3.layout.pie() + var pie = d3.pie() .value(function(d) { return d.count; }) @@ -981,7 +1006,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio // Carbs pie chart - var carbscolor = d3.scale.ordinal().range(['red']); + var carbscolor = d3.scaleOrdinal().range(['red']); var carbsData = [ { label: translate('Carbs'), count: data.dailyCarbs } @@ -995,10 +1020,10 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')'); - var carbsarc = d3.svg.arc() + var carbsarc = d3.arc() .outerRadius(radius * data.dailyCarbs / options.maxDailyCarbsValue); - var carbspie = d3.layout.pie() + var carbspie = d3.pie() .value(function(d) { return d.count; }) @@ -1030,6 +1055,9 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio } tddSum += totalDailyInsulin; + basalSum += totalBasalInsulin; + baseBasalSum += baseBasalInsulin; + bolusSum += bolusInsulin; carbsSum += data.dailyCarbs; proteinSum += data.dailyProtein; fatSum += data.dailyFat; @@ -1067,8 +1095,6 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio } function hideTooltip () { - client.tooltip.transition() - .duration(TOOLTIP_TRANS_MS) - .style('opacity', 0); + client.tooltip.style('opacity', 0); } }; diff --git a/lib/report_plugins/glucosedistribution.js b/lib/report_plugins/glucosedistribution.js index 4a5e7dd31fb..21d5213e9c9 100644 --- a/lib/report_plugins/glucosedistribution.js +++ b/lib/report_plugins/glucosedistribution.js @@ -1,5 +1,7 @@ 'use strict'; +var consts = require('../constants'); + var glucosedistribution = { name: 'glucosedistribution' , label: 'Distribution' @@ -17,9 +19,9 @@ glucosedistribution.html = function html (client) { var ret = '

' + translate('Glucose distribution') + - ' (' + - ' ' + - ' )' + + ' (' + + '' + + ')' + '

' + '' + '' + @@ -120,9 +122,13 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s var data = datastorage.allstatsrecords; var days = datastorage.alldays; - $('#glucosedistribution-days').text(days + ' ' + translate('days total')); + var reportPlugins = Nightscout.report_plugins; + var firstDay = reportPlugins.utils.localeDate(sorteddaystoshow[sorteddaystoshow.length - 1]); + var lastDay = reportPlugins.utils.localeDate(sorteddaystoshow[0]); + + $('#glucosedistribution-days').text(days + ' ' + translate('days total') + ', ' + firstDay + ' - ' + lastDay); - for (var i = 0; i < 23; i++) { + for (var i = 0; i < 24; i++) { $('#glucosedistribution-' + i).unbind('click').click(onClick); enabledHours[i] = $('#glucosedistribution-' + i).is(':checked'); } @@ -146,6 +152,11 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s var glucose_data = [data[0]]; + if (data.length === 0) { + $('#glucosedistribution-days').text(translate('Result is empty')); + return; + } + // data cleaning pass 1 - add interpolated missing points for (i = 0; i <= data.length - 2; i++) { var entry = data[i]; @@ -169,7 +180,7 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s var bg = Math.floor(entry.bgValue + bgDelta * j); var t = new Date(entry.displayTime.getTime() + j * timePatch); var newEntry = { - sgv: displayUnits === 'mmol' ? bg / 18 : bg + sgv: displayUnits === 'mmol' ? bg / consts.MMOL_TO_MGDL : bg , bgValue: bg , displayTime: t }; @@ -212,7 +223,7 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s const interpolatedValue = prevEntry.bgValue + d; let newEntry = { - sgv: displayUnits === 'mmol' ? interpolatedValue / 18 : interpolatedValue + sgv: displayUnits === 'mmol' ? interpolatedValue / consts.MMOL_TO_MGDL : interpolatedValue , bgValue: interpolatedValue , displayTime: entry.displayTime }; @@ -292,7 +303,10 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s rangeExp = ' (>=' + options.targetHigh + ')'; } - $('').appendTo(tr); + var rangeLabel = range; + if (rangeLabel == 'Normal') rangeLabel = 'In Range'; + + $('').appendTo(tr); $('').appendTo(tr); $('').appendTo(tr); if (r.rangeRecords.length > 0) { @@ -425,11 +439,11 @@ glucosedistribution.report = function report_glucosedistribution (datastorage, s var unitString = ' mg/dl'; if (displayUnits == 'mmol') { - TDC = TDC / 18.0; - TDCHourly = TDCHourly / 18.0; + TDC = TDC / consts.MMOL_TO_MGDL; + TDCHourly = TDCHourly / consts.MMOL_TO_MGDL; unitString = ' mmol/L'; - RMS = Math.sqrt(RMSTotal / events) / 18; + RMS = Math.sqrt(RMSTotal / events) / consts.MMOL_TO_MGDL; } TDC = Math.round(TDC * 100) / 100; diff --git a/lib/report_plugins/loopalyzer.js b/lib/report_plugins/loopalyzer.js index 2d53fa251a3..c37c58f635f 100644 --- a/lib/report_plugins/loopalyzer.js +++ b/lib/report_plugins/loopalyzer.js @@ -318,10 +318,12 @@ loopalyzer.fillNanWithTreatments = function(array, treatments) { var stop = index; // Now move left and right until we find real numbers, so not NaN - // eslint-disable-next-line no-empty - while (start-- >= 0 && isNaN(array[start])) {} - // eslint-disable-next-line no-empty - while (stop++ < array.length && isNaN(array[stop])) {} + while (start >= 0 && isNaN(array[start])) { + start--; + } + while (stop < array.length && isNaN(array[stop])) { + stop++; + } // var gap = stop - start; // if (isNaN(array[start]) || isNaN(array[stop]) || gap > interpolationGap || (gap < interpolationGap && array[start]= interpolationGap || array[start]==0)) ) { @@ -732,8 +734,7 @@ loopalyzer.renderProfilesTable = function(datastoreProfiles, daysToShow, client) if (store) { for (var key in store) { if (laDebug) console.log('profile ' + key); - // eslint-disable-next-line no-prototype-builtins - if (store.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(store, key)) { var defaultProfile = store[key]; newEntry.profileName = key; if (defaultProfile.basal) newEntry.basal = defaultProfile.basal; diff --git a/lib/report_plugins/percentile.js b/lib/report_plugins/percentile.js index 803860d5e47..9694fd51faf 100644 --- a/lib/report_plugins/percentile.js +++ b/lib/report_plugins/percentile.js @@ -15,11 +15,17 @@ module.exports = init; percentile.html = function html(client) { var translate = client.translate; var ret = - '

' + translate('Glucose Percentile report') + '

' - + '
' - + '
' - + '
' - ; + '

' + + translate('Glucose Percentile report') + + ' (' + + '' + + ')' + + '

' + + '
' + + '
' + + '
' + ; + return ret; }; @@ -36,16 +42,23 @@ percentile.report = function report_percentile(datastorage, sorteddaystoshow, op var translate = client.translate; var ss = require('simple-statistics'); - var minutewindow = 30; //minute-window should be a divisor of 60 - + var minutewindow = 30; //minute-window should be a divisor of 60 + var data = datastorage.allstatsrecords; - + var bins = []; var filterFunc = function withinWindow(record) { var recdate = new Date(record.displayTime); return recdate.getHours() === hour && recdate.getMinutes() >= minute && recdate.getMinutes() < minute + minutewindow; }; - + + var reportPlugins = Nightscout.report_plugins; + var firstDay = reportPlugins.utils.localeDate(sorteddaystoshow[sorteddaystoshow.length - 1]); + var lastDay = reportPlugins.utils.localeDate(sorteddaystoshow[0]); + var countDays = sorteddaystoshow.length; + + $('#percentile-days').text(countDays + ' ' + translate('days total') + ', ' + firstDay + ' - ' + lastDay); + for (var hour = 0; hour < 24; hour++) { for (var minute = 0; minute < 60; minute = minute + minutewindow) { var date = new Date(); diff --git a/lib/report_plugins/profiles.js b/lib/report_plugins/profiles.js index 580367891de..49c41d30aa9 100644 --- a/lib/report_plugins/profiles.js +++ b/lib/report_plugins/profiles.js @@ -30,8 +30,7 @@ profiles.css = ' height: 100%;' + '}'; -// eslint-disable-next-line no-unused-vars -profiles.report = function report_profiles (datastorage, sorteddaystoshow, options) { +profiles.report = function report_profiles (datastorage) { var Nightscout = window.Nightscout; var client = Nightscout.client; var translate = client.translate; @@ -91,10 +90,16 @@ profiles.report = function report_profiles (datastorage, sorteddaystoshow, optio function displayRanges (array, array2) { var text = ''; - for (var i = 0; i < array.length; i++) { - text += array[i].time + ' : ' + array[i].value + (array2 ? ' - ' + array2[i].value : '') + '
'; - } + if (array && array2) { + for (let i = 0; i < array.length; i++) { + text += array[i].time + ' : ' + array[i].value + (array2 ? ' - ' + array2[i].value : '') + '
'; + } + } else { + for (let i = 0; i < array.length; i++) { + text += array[i].time + ' : ' + array[i].value + '
'; + } + } return text; } }; diff --git a/lib/report_plugins/treatments.js b/lib/report_plugins/treatments.js index c0215b59b4a..f07c84ad322 100644 --- a/lib/report_plugins/treatments.js +++ b/lib/report_plugins/treatments.js @@ -293,7 +293,7 @@ treatments.report = function report_treatments(datastorage, sorteddaystoshow, op .append($('
').should.be.greaterThan(-1); //dailystats - //TODO FIXME result.indexOf('td class="tdborder" style="background-color:#8f8">Normal: ').should.be.greaterThan(-1); // distribution + result.indexOf('').should.be.greaterThan(-1); // distribution result.indexOf('').should.be.greaterThan(-1); // hourlystats result.indexOf('
').should.be.greaterThan(-1); //success result.indexOf('CAL: Scale: 1.10 Intercept: 31102 Slope: 776.91').should.be.greaterThan(-1); //calibrations diff --git a/tests/security.test.js b/tests/security.test.js index 6b612f1bc6e..1f182970019 100644 --- a/tests/security.test.js +++ b/tests/security.test.js @@ -2,14 +2,14 @@ var request = require('supertest'); var should = require('should'); -var load = require('./fixtures/load'); var language = require('../lib/language')(); describe('API_SECRET', function ( ) { - var api = require('../lib/api/'); - + var api; var scope = this; + function setup_app (env, fn) { + api = require('../lib/api/'); require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { ctx.app = api(env, ctx); scope.app = ctx.app; @@ -18,21 +18,6 @@ describe('API_SECRET', function ( ) { }); } - it('should work fine absent', function (done) { - delete process.env.API_SECRET; - var env = require('../env')( ); - should.not.exist(env.api_secret); - setup_app(env, function (ctx) { - - ctx.app.enabled('api').should.equal(false); - ping_status(ctx.app, again); - function again ( ) { - ping_authorized_endpoint(ctx.app, 404, done); - } - }); - }); - - it('should work fail set unauthorized', function (done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; diff --git a/tests/settings.test.js b/tests/settings.test.js index 3e591cfac27..9c12b5bf483 100644 --- a/tests/settings.test.js +++ b/tests/settings.test.js @@ -28,7 +28,7 @@ describe('settings', function ( ) { settings.alarmTimeagoUrgent.should.equal(true); settings.alarmTimeagoUrgentMins.should.equal(30); settings.language.should.equal('en'); - settings.showPlugins.should.equal(''); + settings.showPlugins.should.equal('dbsize'); settings.insecureUseHttp.should.equal(false); settings.secureHstsHeader.should.equal(true); settings.secureCsp.should.equal(false); diff --git a/tests/timeago.test.js b/tests/timeago.test.js index 7b4a718ccd0..66306a3d154 100644 --- a/tests/timeago.test.js +++ b/tests/timeago.test.js @@ -7,6 +7,8 @@ describe('timeago', function() { ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); ctx.language = require('../lib/language')(); + ctx.settings = require('../lib/settings')(); + ctx.settings.heartbeat = 0.5; // short heartbeat to speedup tests var timeago = require('../lib/plugins/timeago')(ctx); @@ -41,6 +43,7 @@ describe('timeago', function() { done(); }); + it('should trigger a warning when data older than 15m', function(done) { ctx.notifications.initRequests(); ctx.ddata.sgvs = [{ mills: Date.now() - times.mins(16).msecs, mgdl: 100, type: 'sgv' }]; @@ -50,9 +53,6 @@ describe('timeago', function() { var currentTime = new Date().getTime(); - // eslint-disable-next-line no-empty - while (currentTime + 500 >= new Date().getTime()) {} - var highest = ctx.notifications.findHighestAlarm('Time Ago'); highest.level.should.equal(levels.WARN); highest.message.should.equal('Last received: 16 mins ago\nBG Now: 100 mg/dl'); diff --git a/tests/units.test.js b/tests/units.test.js index b6e8a9faa8f..2fbef0c4d3e 100644 --- a/tests/units.test.js +++ b/tests/units.test.js @@ -13,4 +13,20 @@ describe('units', function ( ) { units.mgdlToMMOL(180).should.equal('10.0'); }); + it('should convert 5.5 to 99', function () { + units.mmolToMgdl(5.5).should.equal(99); + }); + + it('should convert 10.0 to 180', function () { + units.mmolToMgdl(10.0).should.equal(180); + }); + + it('should convert 5.5 mmol and then convert back to 5.5 mmol', function () { + units.mgdlToMMOL(units.mmolToMgdl(5.5)).should.equal('5.5'); + }); + + it('should convert 99 mgdl and then convert back to 99 mgdl', function () { + units.mmolToMgdl(units.mgdlToMMOL(99)).should.equal(99); + }); + }); diff --git a/tests/upbat.test.js b/tests/upbat.test.js index 9b48c3b845e..42d18bb0854 100644 --- a/tests/upbat.test.js +++ b/tests/upbat.test.js @@ -93,7 +93,7 @@ describe('Uploader Battery', function ( ) { upbat.updateVisualisation(sbx); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: {} @@ -106,13 +106,19 @@ describe('Uploader Battery', function ( ) { var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); - upbat.alexa.intentHandlers.length.should.equal(1); + upbat.virtAsst.intentHandlers.length.should.equal(2); - upbat.alexa.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Uploader battery'); + upbat.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Uploader Battery'); response.should.equal('Your uploader battery is at 20%'); - - done(); + + upbat.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Uploader Battery'); + response.should.equal('Your uploader battery is at 20%'); + + done(); + }, [], sbx); + }, [], sbx); }); diff --git a/views/adminindex.html b/views/adminindex.html index 6108d1b3978..9b4ff750d2e 100644 --- a/views/adminindex.html +++ b/views/adminindex.html @@ -26,32 +26,21 @@ - + - + <% include preloadCSS %> - - -
X
- -
-

Nightscout

-
- -
-

Admin Tools

-
+ + <%- include('partials/toolbar') %>
- -
- Authentication status: - - - - + <%- include('partials/authentication-status') %> + + + + diff --git a/views/clockviews/bgclock.css b/views/clockviews/bgclock.css deleted file mode 100644 index 0a80dae7db0..00000000000 --- a/views/clockviews/bgclock.css +++ /dev/null @@ -1,79 +0,0 @@ -body { - text-align: center; - margin: 0 0; - padding: 0; - overflow: hidden; - font-family: 'Open Sans'; - color: grey; - background-color: black; -} - -main { - display: -webkit-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - height: 100vh; -} - -.inner { - width: 100%; - -webkit-transform: translateY(-2%); -} - -#trend { - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - justify-content: center; - -webkit-flex-direction: row; - flex-direction: row; -} - -#bgnow, #arrowDiv { - display: flex; - flex-grow: 0; - font-weight: 700; - font-size: 30vmin; - padding: 0 20px; - margin: 0; -} - -img#arrow { - height: 18vmin; - filter: brightness(50%); - -webkit-transform: translateY(5%); -} - -#clock { - font-weight: 700; - font-size: 25vmin; -} - -.stale { - text-decoration: line-through; -} - -.close { - color: white; - font: 4em 'Open Sans'; - position: absolute; - right: 20px; - text-decoration: none; -} - -.close:after { - content: '\00D7'; -} - -.hidden { - opacity: 0; - transition: opacity 0.5s linear; -} \ No newline at end of file diff --git a/views/clockviews/clock-color.css b/views/clockviews/clock-color.css deleted file mode 100644 index 36002c6b9ac..00000000000 --- a/views/clockviews/clock-color.css +++ /dev/null @@ -1,80 +0,0 @@ -body { - text-align: center; - margin: 0 0; - padding: 0; - overflow: hidden; - font-family: 'Open Sans'; - color: white; - background-color: white; -} - -main { - display: -webkit-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - height: 100vh; -} - -.inner { - width: 100%; - -webkit-transform: translateY(-5%); -} - -#bgnow { - font-weight: 700; - font-size: 40vmin; -} - -#trend { - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -ms-flex-align: center; - -webkit-align-items: center; - -webkit-transform: translateX(1%); - align-items: center; - justify-content: center; - -webkit-flex-direction: column; - flex-direction: column; -} - -#arrowDiv { - flex-grow: 1; - text-align: center; -} - -img#arrow { - height: 30vmin; -} - -#staleTime { - flex-grow: 1; - font-size: 6vmin; - display: none; -} - -#clock { - display: none; -} - -.close { - color: white; - font: 4em 'Open Sans'; - position: absolute; - right: 20px; - text-decoration: none; -} - -.close:after { - content: '\00D7'; -} - -.hidden { - opacity: 0; - transition: opacity 0.5s linear; -} \ No newline at end of file diff --git a/views/clockviews/clock-config.css b/views/clockviews/clock-config.css new file mode 100644 index 00000000000..0d5f2b8b912 --- /dev/null +++ b/views/clockviews/clock-config.css @@ -0,0 +1,39 @@ +#config-form { + position: fixed; + top: 10px; + left: 10px; + width: 250px; + min-width: 220px; + background: white; + color: black; + opacity: 0.8; + padding: 1%; + font-size: 10px; +} +#config-form p { + margin: 15px; + text-align: left; +} +input.elmt { + width: 120px; +} +select { + width: 100%; +} +#facename { + font-size: 7px; +} +#clocklink { + font-size: 18px; +} +#clocklink:link, #clocklink:visited { + background-color: #f44336; + color: white; + padding: 14px 25px; + text-align: center; + text-decoration: none; + display: inline-block; +} +#clocklink:hover, #clocklink:active { + background-color: red; +} \ No newline at end of file diff --git a/views/clockviews/clock-config.html b/views/clockviews/clock-config.html new file mode 100644 index 00000000000..fff92bb65cb --- /dev/null +++ b/views/clockviews/clock-config.html @@ -0,0 +1,65 @@ +
+

Clock view configurator

+ +

+ + +

+

+ + +

+

SGV age threshold: minutes

+

+

Size:

+

Size:

+

Size:

+

Size:

+

Size:

+

+ Open my clock view! +
cy10
+ +
+ \ No newline at end of file diff --git a/views/clockviews/clock-shared.css b/views/clockviews/clock-shared.css new file mode 100644 index 00000000000..fc624a52c00 --- /dev/null +++ b/views/clockviews/clock-shared.css @@ -0,0 +1,69 @@ +body { + text-align: center; + margin: 0 0; + padding: 0; + overflow: hidden; + font-family: 'Open Sans', Arial, Helvetica, sans-serif; + color: grey; + background-color: black; +} + +main { + height: 100vh; +} + +#inner { + display: -webkit-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + flex-flow: wrap; + height: 100%; + width: 100%; +} + +#inner div { + margin-right: 2vmin; + margin-left: 2vmin; + line-height: 1em; +} + +#inner div img { + height: 100%; +} + +#inner div.nl { + width: 100%; + margin: 0; + height: 3vmin; +} + +#errorMessage { + font-size: 25em; +} + +.stale { + text-decoration: line-through; +} + +.close { + color: white; + font: 4em 'Open Sans'; + position: absolute; + top: 0; + right: 20px; + text-decoration: none; + z-index: 10; +} + +.close:after { + content: '\00D7'; +} + +.hidden { + opacity: 0; + transition: opacity 0.5s linear; +} \ No newline at end of file diff --git a/views/clockviews/clock.css b/views/clockviews/clock.css deleted file mode 100644 index e73f715061f..00000000000 --- a/views/clockviews/clock.css +++ /dev/null @@ -1,71 +0,0 @@ -body { - text-align: center; - margin: 0 0; - padding: 0; - overflow: hidden; - font-family: 'Open Sans'; - color: grey; - background-color: black; -} - -main { - display: -webkit-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - height: 100vh; -} - -.inner { - width: 100%; - -webkit-transform: translateY(-5%); -} - -#bgnow { - font-weight: 700; - font-size: 40vmin; -} - -#trend { - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -ms-flex-align: center; - -webkit-align-items: center; - -webkit-transform: translateX(1%); - align-items: center; - justify-content: center; - -webkit-flex-direction: column; - flex-direction: column; -} - -#staleTime { - flex-grow: 1; - font-size: 6vmin; - display: none; -} - -#clock { - display: none; -} - -.close { - color: white; - font: 4em 'Open Sans'; - position: absolute; - right: 20px; - text-decoration: none; -} - -.close:after { - content: '\00D7'; -} - -.hidden { - opacity: 0; - transition: opacity 0.5s linear; -} \ No newline at end of file diff --git a/views/clockviews/shared.html b/views/clockviews/clock.html similarity index 83% rename from views/clockviews/shared.html rename to views/clockviews/clock.html index 401b5bb3c25..2893aec3612 100644 --- a/views/clockviews/shared.html +++ b/views/clockviews/clock.html @@ -1,5 +1,5 @@ - + @@ -20,24 +20,20 @@ -
-
-
-
-
arrow
-
-
-
+ +
>
- + + <%if (face == 'config') { %> + <%- include('clock-config.html', {}); %> + <% } %> diff --git a/views/error.html b/views/error.html new file mode 100644 index 00000000000..580618c4071 --- /dev/null +++ b/views/error.html @@ -0,0 +1,64 @@ + + + + + + + + Nightscout - Boot error + + + + + + + + + + + + + + + + + +
' + translate(range) + rangeExp + ': ' + translate(rangeLabel) + rangeExp + ': ' + r.readingspct + '%' + r.rangeRecords.length + '').css('width','150px').attr('align','left').append(translate('Event Type'))) .append($('').css('width','150px').attr('align','left').append(translate('Blood Glucose'))) .append($('').css('width','50px').attr('align','left').append(translate('Insulin'))) - .append($('').css('width','50px').attr('align','left').append(translate('Carbs'))) + .append($('').css('width','100px').attr('align','left').append(translate('Carbs/Food/Time'))) .append($('').css('width','50px').attr('align','left').append(translate('Protein'))) .append($('').css('width','50px').attr('align','left').append(translate('Fat'))) .append($('').css('width','50px').attr('align','left').append(translate('Duration'))) @@ -316,6 +316,25 @@ treatments.report = function report_treatments(datastorage, sorteddaystoshow, op } for (var t=0; t').addClass('border_bottom') .append($('') .append($('').addClass('deleteTreatment').css('cursor','pointer').attr('title',translate('Delete record')).attr('src',icon_remove).attr('data',JSON.stringify(tr)).attr('day',day)) @@ -323,10 +342,11 @@ treatments.report = function report_treatments(datastorage, sorteddaystoshow, op .append($('').addClass('editTreatment').css('cursor','pointer').attr('title',translate('Edit record')).attr('src',icon_edit).attr('data',JSON.stringify(tr)).attr('day',day)) ) .append($('').append(new Date(tr.created_at).toLocaleTimeString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'))) - .append($('').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) : '')) + .append($('').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) + (tr.reason ? '
' + tr.reason : '') + + (tr.insulinNeedsScaleFactor ? '
' + tr.insulinNeedsScaleFactor * 100 + '%' : '') + (tr.correctionRange ? ' ' + correctionRangeText : '') : '')) .append($('
').attr('align','center').append(tr.glucose ? tr.glucose + ' ('+translate(tr.glucoseType)+')' : '')) .append($('').attr('align','center').append(tr.insulin ? tr.insulin.toFixed(2) : '')) - .append($('').attr('align','center').append(tr.carbs ? tr.carbs : '')) + .append($('').attr('align','center').append(carbs)) .append($('').attr('align','center').append(tr.protein ? tr.protein : '')) .append($('').attr('align','center').append(tr.fat ? tr.fat : '')) .append($('').attr('align','center').append(tr.duration ? tr.duration.toFixed(0) : '')) diff --git a/lib/report_plugins/utils.js b/lib/report_plugins/utils.js index 10daec32304..be6ba940003 100644 --- a/lib/report_plugins/utils.js +++ b/lib/report_plugins/utils.js @@ -1,5 +1,7 @@ 'use strict'; +var consts = require('../constants'); + var moment = window.moment; var utils = { }; @@ -69,7 +71,7 @@ utils.scaledTreatmentBG = function scaledTreatmentBG(treatment,data) { console.info('found mismatched glucose units, converting ' + treatment.units + ' into ' + client.settings.units, treatment); if (treatment.units === 'mmol') { //BG is in mmol and display in mg/dl - treatmentGlucose = Math.round(treatment.glucose * 18); + treatmentGlucose = Math.round(treatment.glucose * consts.MMOL_TO_MGDL); } else { //BG is in mg/dl and display in mmol treatmentGlucose = client.utils.scaleMgdl(treatment.glucose); diff --git a/lib/report_plugins/weektoweek.js b/lib/report_plugins/weektoweek.js index 3ff84a78639..8719af81542 100644 --- a/lib/report_plugins/weektoweek.js +++ b/lib/report_plugins/weektoweek.js @@ -79,8 +79,6 @@ weektoweek.report = function report_weektoweek(datastorage, sorteddaystoshow, op var client = Nightscout.client; var report_plugins = Nightscout.report_plugins; - var TOOLTIP_TRANS_MS = 300; - var padding = { top: 15, right: 22, bottom: 30, left: 35 }; var weekstoshow = [ ]; @@ -196,28 +194,24 @@ weektoweek.report = function report_weektoweek(datastorage, sorteddaystoshow, op context = charts.append('g'); // define the parts of the axis that aren't dependent on width or height - xScale2 = d3.time.scale() + xScale2 = d3.scaleTime() .domain(d3.extent(sgvData, dateFn)); if (options.weekscale === report_plugins.consts.SCALE_LOG) { - yScale2 = d3.scale.log() + yScale2 = d3.scaleLog() .domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]); } else { - yScale2 = d3.scale.linear() + yScale2 = d3.scaleLinear() .domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]); } - xAxis2 = d3.svg.axis() - .scale(xScale2) + xAxis2 = d3.axisBottom(xScale2) .tickFormat(timeTicks) - .ticks(24) - .orient('bottom'); + .ticks(24); - yAxis2 = d3.svg.axis() - .scale(yScale2) + yAxis2 = d3.axisLeft(yScale2) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('left'); + .tickValues(tickValues); // get current data range var dataRange = d3.extent(sgvData, dateFn); @@ -326,8 +320,6 @@ weektoweek.report = function report_weektoweek(datastorage, sorteddaystoshow, op } function hideTooltip ( ) { - client.tooltip.transition() - .duration(TOOLTIP_TRANS_MS) - .style('opacity', 0); + client.tooltip.style('opacity', 0); } }; diff --git a/lib/sandbox.js b/lib/sandbox.js index ceac9a3fe29..4bcee3b40e4 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -44,6 +44,7 @@ function init () { sbx.serverInit = function serverInit (env, ctx) { reset(); + sbx.runtimeEnvironment = 'server'; sbx.time = Date.now(); sbx.settings = env.settings; sbx.data = ctx.ddata.clone(); @@ -83,6 +84,7 @@ function init () { sbx.clientInit = function clientInit (ctx, time, data) { reset(); + sbx.runtimeEnvironment = 'client'; sbx.settings = ctx.settings; sbx.showPlugins = ctx.settings.showPlugins; sbx.time = time; @@ -96,7 +98,7 @@ function init () { if (sbx.pluginBase) { sbx.pluginBase.forecastInfos = []; - sbx.pluginBase.forecastPoints = []; + sbx.pluginBase.forecastPoints = {}; } sbx.extendedSettings = { empty: true }; diff --git a/lib/server/booterror.js b/lib/server/booterror.js index f08eb06db39..d8735e94451 100644 --- a/lib/server/booterror.js +++ b/lib/server/booterror.js @@ -1,22 +1,46 @@ 'use strict'; +const express = require('express'); +const path = require('path'); var _ = require('lodash'); -var head = 'Nightscout - Boot Error

Nightscout - Boot Error

'; -var tail = '
'; +function bootError(env, ctx) { -function bootError(ctx) { + const app = new express(); + let locals = {}; + + app.set('view engine', 'ejs'); + app.engine('html', require('ejs').renderFile); + app.set("views", path.join(__dirname, "../../views/")); + + app.get('*', (req, res, next) => { + + if (req.url.includes('images')) return next(); - return function pageHandler (req, res) { var errors = _.map(ctx.bootErrors, function (obj) { - obj.err = _.pick(obj.err, Object.getOwnPropertyNames(obj.err)); - return '
' + obj.desc + '
' + JSON.stringify(obj.err).replace(/\\n/g, '
') + '
'; + + let message; + + if (typeof obj.err === 'string' || obj.err instanceof String) { + message = obj.err; + } else { + message = JSON.stringify(_.pick(obj.err, Object.getOwnPropertyNames(obj.err))); + } + return '
' + obj.desc + '
' + message.replace(/\\n/g, '
') + '
'; }).join(' '); - res.set('Content-Type', 'text/html'); - res.send(head + errors + tail); + res.render('error.html', { + errors, + locals + }); + + }); + app.setLocals = function (_locals) { + locals = _locals; } + + return app; } module.exports = bootError; \ No newline at end of file diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js index 9e53c4f77ac..6247645b3d2 100644 --- a/lib/server/bootevent.js +++ b/lib/server/bootevent.js @@ -7,8 +7,8 @@ var UPDATE_THROTTLE = 5000; function boot (env, language) { ////////////////////////////////////////////////// - // Check Node version. - // Latest Node 8 LTS and Latest Node 10 LTS are recommended and supported. + // Check Node version. + // Latest Node 8 LTS and Latest Node 10 LTS are recommended and supported. // Latest Node version on Azure is tolerated, but not recommended // Latest Node (non LTS) version works, but is not recommended // Older Node versions or Node versions with known security issues will not work. @@ -34,7 +34,7 @@ function boot (env, language) { else if ( semver.eq(nodeVersion, '10.15.2')) { //Latest Node version on Azure is tolerated, but not recommended console.log('WARNING: Node version v10.15.2 and Microsoft Azure are not recommended.'); - console.log('WARNING: Please migrate to another hosting provider. Your Node version is outdated and insecure'); + console.log('WARNING: Please migrate to another hosting provider. Your Node version is outdated and insecure'); next(); } else if ( semver.satisfies(nodeVersion, '^12.6.0')) { @@ -42,7 +42,7 @@ function boot (env, language) { console.debug('Node version ' + nodeVersion + ' is not a LTS version. Not recommended. Not supported'); next(); } else { - // Other versions will not start + // Other versions will not start console.log( 'ERROR: Node version ' + nodeVersion + ' is not supported. Please use a secure LTS version or upgrade your Node'); process.exit(1); } @@ -95,6 +95,25 @@ function boot (env, language) { } } + function checkSettings (ctx, next) { + + ctx.bootErrors = ctx.bootErrors || []; + + console.log('Checking settings'); + + if (!env.storageURI) { + ctx.bootErrors.push({'desc': 'Mandatory setting missing', + err: 'MONGODB_URI setting is missing, cannot connect to database'}); + } + + if (!env.api_secret) { + ctx.bootErrors.push({'desc': 'Mandatory setting missing', + err: 'API_SECRET setting is missing, cannot enable REST API'}); + } + + next(); + } + function setupStorage (ctx, next) { if (hasBootErrors(ctx)) { @@ -107,7 +126,6 @@ function boot (env, language) { if (err) { throw err; } - ctx.store = store; console.log('OpenAPS Storage system ready'); next(); @@ -115,15 +133,19 @@ function boot (env, language) { } else { //TODO assume mongo for now, when there are more storage options add a lookup require('../storage/mongo-storage')(env, function ready(err, store) { - // FIXME, error is always null, if there is an error, the storage.js will throw an exception + // FIXME, error is always null, if there is an error, the index.js will throw an exception + if (err) { + console.info('ERROR CONNECTING TO MONGO', err); + ctx.bootErrors = ctx.bootErrors || [ ]; + ctx.bootErrors.push({'desc': 'Unable to connect to Mongo', err: err}); + } console.log('Mongo Storage system ready'); ctx.store = store; - next(); }); } } catch (err) { - console.info('mongo err', err); + console.info('ERROR CONNECTING TO MONGO', err); ctx.bootErrors = ctx.bootErrors || [ ]; ctx.bootErrors.push({'desc': 'Unable to connect to Mongo', err: err}); next(); @@ -164,6 +186,7 @@ function boot (env, language) { ctx.pushover = require('../plugins/pushover')(env); ctx.maker = require('../plugins/maker')(env); ctx.pushnotify = require('./pushnotify')(env, ctx); + ctx.loop = require('./loop')(env, ctx); ctx.activity = require('./activity')(env, ctx); ctx.entries = require('./entries')(env, ctx); @@ -175,13 +198,22 @@ function boot (env, language) { ctx.properties = require('../api/properties')(env, ctx); ctx.bus = require('../bus')(env.settings, ctx); ctx.ddata = require('../data/ddata')(); + ctx.cache = require('./cache')(env,ctx); ctx.dataloader = require('../data/dataloader')(env, ctx); ctx.notifications = require('../notifications')(env, ctx); + if (env.settings.isEnabled('alexa') || env.settings.isEnabled('googlehome')) { + ctx.virtAsstBase = require('../plugins/virtAsstBase')(env, ctx); + } + if (env.settings.isEnabled('alexa')) { ctx.alexa = require('../plugins/alexa')(env, ctx); } + if (env.settings.isEnabled('googlehome')) { + ctx.googleHome = require('../plugins/googlehome')(env, ctx); + } + next( ); } @@ -223,7 +255,7 @@ function boot (env, language) { }); ctx.bus.on('data-loaded', function updatePlugins ( ) { - console.info('reloading sandbox data'); + // console.info('reloading sandbox data'); var sbx = require('../sandbox')().serverInit(env, ctx); ctx.plugins.setProperties(sbx); ctx.notifications.initRequests(); @@ -242,7 +274,7 @@ function boot (env, language) { return next(); } - ctx.bridge = require('../plugins/bridge')(env); + ctx.bridge = require('../plugins/bridge')(env, ctx.bus); if (ctx.bridge) { ctx.bridge.startEngine(ctx.entries); } @@ -254,7 +286,7 @@ function boot (env, language) { return next(); } - ctx.mmconnect = require('../plugins/mmconnect').init(env, ctx.entries, ctx.devicestatus); + ctx.mmconnect = require('../plugins/mmconnect').init(env, ctx.entries, ctx.devicestatus, ctx.bus); if (ctx.mmconnect) { ctx.mmconnect.run(); } @@ -275,6 +307,7 @@ function boot (env, language) { .acquire(checkNodeVersion) .acquire(checkEnv) .acquire(augmentSettings) + .acquire(checkSettings) .acquire(setupStorage) .acquire(setupAuthorization) .acquire(setupInternals) diff --git a/lib/server/cache.js b/lib/server/cache.js new file mode 100644 index 00000000000..88e5f77ca02 --- /dev/null +++ b/lib/server/cache.js @@ -0,0 +1,103 @@ +'use strict'; + +/* This is a simple cache intended to reduce the amount of load + * Nightscout puts on MongoDB. The cache is based on identifying + * elements based on the MongoDB _id field and implements simple + * semantics for adding data to the cache in the runtime, intended + * to be accessed by the persistence layer as data is inserted, updated + * or deleted, as well as the periodic dataloader, which polls Mongo + * for new inserts. + * + * Longer term, the cache is planned to allow skipping the Mongo polls + * altogether. + */ + +const _ = require('lodash'); +const constants = require('../constants'); + +function cache (env, ctx) { + + const data = { + treatments: [] + , devicestatus: [] + , entries: [] + }; + + const retentionPeriods = { + treatments: constants.ONE_HOUR * 60 + , devicestatus: env.extendedSettings.devicestatus && env.extendedSettings.devicestatus.days && env.extendedSettings.devicestatus.days == 2 ? constants.TWO_DAYS : constants.ONE_DAY + , entries: constants.TWO_DAYS + }; + + function mergeCacheArrays (oldData, newData, retentionPeriod) { + + const ageLimit = Date.now() - retentionPeriod; + + var filteredOld = filterForAge(oldData, ageLimit); + var filteredNew = filterForAge(newData, ageLimit); + + const merged = ctx.ddata.idMergePreferNew(filteredOld, filteredNew); + + return _.sortBy(merged, function(item) { + return -item.mills; + }); + + function filterForAge(data, ageLimit) { + return _.filter(data, function hasId(object) { + const hasId = !_.isEmpty(object._id); + const isFresh = object.mills >= ageLimit; + return isFresh && hasId; + }); + } + + } + + data.isEmpty = (datatype) => { + return data[datatype].length < 20; + } + + data.getData = (datatype) => { + return _.cloneDeep(data[datatype]); + } + + data.insertData = (datatype, newData) => { + data[datatype] = mergeCacheArrays(data[datatype], newData, retentionPeriods[datatype]); + return data.getData(datatype); + } + + function dataChanged (operation) { + if (!data[operation.type]) return; + + if (operation.op == 'remove') { + // if multiple items were deleted, flush entire cache + if (!operation.changes) { + data.treatments = []; + data.devicestatus = []; + data.entries = []; + } else { + removeFromArray(data[operation.type], operation.changes); + } + } + + if (operation.op == 'update') { + data[operation.type] = mergeCacheArrays(data[operation.type], operation.changes, retentionPeriods[operation.type]); + } + } + + ctx.bus.on('data-update', dataChanged); + + function removeFromArray (array, id) { + for (let i = 0; i < array.length; i++) { + const o = array[i]; + if (o._id == id) { + //console.log('Deleting object from cache', id); + array.splice(i, 1); + break; + } + } + } + + return data; +} + +module.exports = cache; diff --git a/lib/server/clocks.js b/lib/server/clocks.js index 282acd01951..9926eefcf82 100644 --- a/lib/server/clocks.js +++ b/lib/server/clocks.js @@ -3,8 +3,7 @@ const express = require('express'); const path = require('path'); -// eslint-disable-next-line no-unused-vars -function clockviews(env, ctx) { +function clockviews() { const app = new express(); let locals = {}; @@ -18,7 +17,7 @@ function clockviews(env, ctx) { const face = req.params.face; console.log('Clockface requested:', face); - res.render('shared.html', { + res.render('clock.html', { face, locals }); diff --git a/lib/server/devicestatus.js b/lib/server/devicestatus.js index d35c6be87cb..bf71437d646 100644 --- a/lib/server/devicestatus.js +++ b/lib/server/devicestatus.js @@ -5,26 +5,55 @@ var find_options = require('./query'); function storage (collection, ctx) { - function create(obj, fn) { - - // Normalize all dates to UTC - const d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment(); - obj.created_at = d.toISOString(); - obj.utcOffset = d.utcOffset(); - - api().insert(obj, function (err, doc) { - if (err != null && err.message) { - console.log('Error inserting the device status object', err.message); - fn(err.message, null); - return; - } - fn(null, doc.ops); - ctx.bus.emit('data-received'); - }); + function create (statuses, fn) { + + if (!Array.isArray(statuses)) { statuses = [statuses]; } + + const r = []; + let errorOccurred = false; + + for (let i = 0; i < statuses.length; i++) { + + const obj = statuses[i]; + + if (errorOccurred) return; + + // Normalize all dates to UTC + const d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment(); + obj.created_at = d.toISOString(); + obj.utcOffset = d.utcOffset(); + + api().insertOne(obj, function(err, results) { + if (err !== null && err.message) { + console.log('Error inserting the device status object', err.message); + errorOccurred = true; + fn(err.message, null); + return; + } + + if (!err) { + + if (!obj._id) obj._id = results.insertedIds[0]._id; + r.push(obj); + + ctx.bus.emit('data-update', { + type: 'devicestatus' + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime([obj]) + }); + + // Last object! Return results + if (i == statuses.length - 1) { + fn(null, r); + ctx.bus.emit('data-received'); + } + } + }); + }; } - function last(fn) { - return list({count: 1}, function (err, entries) { + function last (fn) { + return list({ count: 1 }, function(err, entries) { if (entries && entries.length > 0) { fn(err, entries[0]); } else { @@ -37,18 +66,18 @@ function storage (collection, ctx) { return find_options(opts, storage.queryOpts); } - function list(opts, fn) { + function list (opts, fn) { // these functions, find, sort, and limit, are used to // dynamically configure the request, based on the options we've // been given // determine sort options - function sort ( ) { - return opts && opts.sort || {created_at: -1}; + function sort () { + return opts && opts.sort || { created_at: -1 }; } // configure the limit portion of the current query - function limit ( ) { + function limit () { if (opts && opts.count) { return this.limit(parseInt(opts.count)); } @@ -61,17 +90,31 @@ function storage (collection, ctx) { } // now just stitch them all together - limit.call(api( ) - .find(query_for(opts)) - .sort(sort( )) + limit.call(api() + .find(query_for(opts)) + .sort(sort()) ).toArray(toArray); } function remove (opts, fn) { - return api( ).remove(query_for(opts), fn); + + function removed (err, stat) { + + ctx.bus.emit('data-update', { + type: 'devicestatus' + , op: 'remove' + , count: stat.result.n + , changes: opts.find._id + }); + + fn(err, stat); + } + + return api().remove( + query_for(opts), removed); } - function api() { + function api () { return ctx.store.collection(collection); } @@ -80,9 +123,13 @@ function storage (collection, ctx) { api.query_for = query_for; api.last = last; api.remove = remove; - api.aggregate = require('./aggregate')({ }, api); + api.aggregate = require('./aggregate')({}, api); api.indexedFields = [ 'created_at' + + + + , 'NSCLIENT_ID' ]; return api; diff --git a/lib/server/entries.js b/lib/server/entries.js index d7258f13b79..50c8e0cc41a 100644 --- a/lib/server/entries.js +++ b/lib/server/entries.js @@ -10,52 +10,60 @@ var moment = require('moment'); * Encapsulate persistent storage of sgv entries. \**********/ -function storage(env, ctx) { +function storage (env, ctx) { // TODO: Code is a little redundant. // query for entries from storage function list (opts, fn) { - // these functions, find, sort, and limit, are used to - // dynamically configure the request, based on the options we've - // been given + // these functions, find, sort, and limit, are used to + // dynamically configure the request, based on the options we've + // been given - // determine sort options - function sort ( ) { - return opts && opts.sort || {date: -1}; - } + // determine sort options + function sort () { + return opts && opts.sort || { date: -1 }; + } - // configure the limit portion of the current query - function limit ( ) { - if (opts && opts.count) { - return this.limit(parseInt(opts.count)); - } - return this; + // configure the limit portion of the current query + function limit () { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); } + return this; + } - // handle all the results - function toArray (err, entries) { - fn(err, entries); - } + // handle all the results + function toArray (err, entries) { + fn(err, entries); + } - // now just stitch them all together - limit.call(api( ) - .find(query_for(opts)) - .sort(sort( )) - ).toArray(toArray); + // now just stitch them all together + limit.call(api() + .find(query_for(opts)) + .sort(sort()) + ).toArray(toArray); } function remove (opts, fn) { - api( ).remove(query_for(opts), function (err, stat) { - //TODO: this is triggering a read from Mongo, we can do better - ctx.bus.emit('data-received'); - fn(err, stat); + api().remove(query_for(opts), function(err, stat) { + + ctx.bus.emit('data-update', { + type: 'entries' + , op: 'remove' + , count: stat.result.n + , changes: opts.find._id }); + + //TODO: this is triggering a read from Mongo, we can do better + ctx.bus.emit('data-received'); + fn(err, stat); + }); } // return writable stream to lint each sgv record passing through it // TODO: get rid of this? not doing anything now - function map ( ) { + function map () { return es.map(function iter (item, next) { return next(null, item); }); @@ -72,7 +80,7 @@ function storage(env, ctx) { create(result, fn); } // lint and store the entire list - return es.pipeline(map( ), es.writeArray(done)); + return es.pipeline(map(), es.writeArray(done)); } //TODO: implement @@ -83,9 +91,9 @@ function storage(env, ctx) { // store new documents using the storage mechanism function create (docs, fn) { // potentially a batch insert - var firstErr = null, - numDocs = docs.length, - totalCreated = 0; + var firstErr = null + , numDocs = docs.length + , totalCreated = 0; docs.forEach(function(doc) { @@ -98,9 +106,22 @@ function storage(env, ctx) { doc.sysTime = _sysTime.toISOString(); if (doc.dateString) doc.dateString = doc.sysTime; - var query = (doc.sysTime && doc.type) ? {sysTime: doc.sysTime, type: doc.type} : doc; - api( ).update(query, doc, {upsert: true}, function (err) { + var query = (doc.sysTime && doc.type) ? { sysTime: doc.sysTime, type: doc.type } : doc; + api().update(query, doc, { upsert: true }, function(err, updateResults) { firstErr = firstErr || err; + + if (!err) { + if (updateResults.result.upserted) { + doc._id = updateResults.result.upserted[0]._id + } + + ctx.bus.emit('data-update', { + type: 'entries' + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime([doc]) + }); + } + if (++totalCreated === numDocs) { //TODO: this is triggering a read from Mongo, we can do better ctx.bus.emit('data-received'); @@ -110,8 +131,8 @@ function storage(env, ctx) { }); } - function getEntry(id, fn) { - api( ).findOne({_id: ObjectID(id)}, function (err, entry) { + function getEntry (id, fn) { + api().findOne({ _id: ObjectID(id) }, function(err, entry) { if (err) { fn(err); } else { @@ -125,7 +146,7 @@ function storage(env, ctx) { } // closure to represent the API - function api ( ) { + function api () { // obtain handle usable for querying the collection associated // with these records return ctx.store.collection(env.entries_collection); @@ -139,8 +160,22 @@ function storage(env, ctx) { api.persist = persist; api.query_for = query_for; api.getEntry = getEntry; - api.aggregate = require('./aggregate')({ }, api); - api.indexedFields = [ 'date', 'type', 'sgv', 'mbg', 'sysTime', 'dateString' ]; + api.aggregate = require('./aggregate')({}, api); + api.indexedFields = [ + 'date' + + , 'type' + + , 'sgv' + + , 'mbg' + + , 'sysTime' + + , 'dateString' + + , { 'type': 1, 'date': -1, 'dateString': 1 } + ]; return api; } @@ -153,11 +188,10 @@ storage.queryOpts = { , rssi: parseInt , noise: parseInt , mbg: parseInt - } + } , useEpoch: true }; // expose module storage.storage = storage; module.exports = storage; - diff --git a/lib/server/loop.js b/lib/server/loop.js new file mode 100644 index 00000000000..ca87892bba6 --- /dev/null +++ b/lib/server/loop.js @@ -0,0 +1,135 @@ +//'use strict'; + +const apn = require('apn'); + +function init (env, ctx) { + + function loop () { + return loop; + } + + loop.sendNotification = function sendNotification (data, remoteAddress, completion) { + + // console.info("JAP"); + // console.info(data); + + if (env.extendedSettings.loop.apnsKey === undefined || env.extendedSettings.loop.apnsKey.length == 0) { + completion("Loop notification failed: LOOP_APNS_KEY not set."); + return; + } + + if (env.extendedSettings.loop.apnsKeyId === undefined || env.extendedSettings.loop.apnsKeyId.length == 0) { + completion("Loop notification failed: LOOP_APNS_KEY_ID not set."); + return; + } + + if (env.extendedSettings.loop.developerTeamId === undefined || env.extendedSettings.loop.developerTeamId.length != 10) { + completion("Loop notification failed: LOOP_DEVELOPER_TEAM_ID not set."); + return; + } + + if (ctx.ddata.profiles === undefined || ctx.ddata.profiles.length < 1 || ctx.ddata.profiles[0].loopSettings === undefined) { + completion("Loop notification failed: Could not find loopSettings in profile."); + return; + } + + let loopSettings = ctx.ddata.profiles[0].loopSettings; + + if (loopSettings.deviceToken === undefined) { + completion("Loop notification failed: Could not find deviceToken in loopSettings."); + return; + } + + if (loopSettings.bundleIdentifier === undefined) { + completion("Loop notification failed: Could not find bundleIdentifier in loopSettings."); + return; + } + + var options = { + token: { + key: env.extendedSettings.loop.apnsKey + , keyId: env.extendedSettings.loop.apnsKeyId + , teamId: env.extendedSettings.loop.developerTeamId + }, + production: env.extendedSettings.loop.pushServerEnvironment === "production" + }; + + var provider = new apn.Provider(options); + + var payload = { + 'remote-address': remoteAddress, + 'notes': data.notes, + 'entered-by': data.enteredBy + }; + var alert; + if (data.eventType === 'Temporary Override Cancel') { + payload["cancel-temporary-override"] = "true"; + alert = "Cancel Temporary Override"; + } else if (data.eventType === 'Temporary Override') { + payload["override-name"] = data.reason; + if (data.duration !== undefined && parseInt(data.duration) > 0) { + payload["override-duration-minutes"] = parseInt(data.duration); + } + alert = data.reasonDisplay + " Temporary Override"; + } else if (data.eventType === 'Remote Carbs Entry') { + payload["carbs-entry"] = parseFloat(data.remoteCarbs); + if(payload["carbs-entry"] > 0.0 ) { + payload["absorption-time"] = 3.0; + if (data.remoteAbsorption !== undefined && parseFloat(data.remoteAbsorption) > 0.0) { + payload["absorption-time"] = parseFloat(data.remoteAbsorption); + } + if (data.otp !== undefined && data.otp.length > 0) { + payload["otp"] = ""+data.otp + } + alert = "Remote Carbs Entry: "+payload["carbs-entry"]+" grams\n"; + alert += "Absorption Time: "+payload["absorption-time"]+" hours"; + } else { + completion("Loop remote carbs failed. Incorrect carbs entry: ", data.remoteCarbs); + return; + } + + } else if (data.eventType === 'Remote Bolus Entry') { + payload["bolus-entry"] = parseFloat(data.remoteBolus); + if(payload["bolus-entry"] > 0.0 ) { + alert = "Remote Bolus Entry: "+payload["bolus-entry"]+" U\n"; + if (data.otp !== undefined && data.otp.length > 0) { + payload["otp"] = ""+data.otp + } + } else { + completion("Loop remote bolus failed. Incorrect bolus entry: ", data.remoteBolus); + return; + } + } else { + completion("Loop notification failed: Unhandled event type:", data.eventType); + return; + } + + if (data.notes !== undefined && data.notes.length > 0) { + alert += " - " + data.notes + } + + if (data.enteredBy !== undefined && data.enteredBy.length > 0) { + alert += " - " + data.enteredBy + } + + let notification = new apn.Notification(); + notification.alert = alert; + notification.topic = loopSettings.bundleIdentifier; + notification.contentAvailable = 1; + notification.expiry = Math.round((Date.now() / 1000)) + 60 * 5; // Allow this to enact within 5 minutes. + notification.payload = payload; + + provider.send(notification, [loopSettings.deviceToken]).then( (response) => { + if (response.sent && response.sent.length > 0) { + completion(); + } else { + console.log("APNs delivery failed:", response.failed) + completion("APNs delivery failed: " + response.failed[0].response.reason); + } + }); + }; + + return loop(); +} + +module.exports = init; diff --git a/lib/server/pebble.js b/lib/server/pebble.js index 544aa878268..97b756a9b84 100644 --- a/lib/server/pebble.js +++ b/lib/server/pebble.js @@ -176,7 +176,7 @@ function configure (env, ctx) { req.rawbg = env.settings.isEnabled('rawbg'); req.iob = env.settings.isEnabled('iob'); req.cob = env.settings.isEnabled('cob'); - req.mmol = (req.query.units || env.DISPLAY_UNITS) === 'mmol'; + req.mmol = (req.query.units || env.settings.units) === 'mmol'; req.count = parseInt(req.query.count) || 1; next( ); diff --git a/lib/server/profile.js b/lib/server/profile.js index d456b590959..e49e366d15a 100644 --- a/lib/server/profile.js +++ b/lib/server/profile.js @@ -1,5 +1,8 @@ 'use strict'; +var find_options = require('./query'); +var consts = require('../constants'); + function storage (collection, ctx) { var ObjectID = require('mongodb').ObjectID; @@ -23,10 +26,53 @@ function storage (collection, ctx) { ctx.bus.emit('data-received'); } - function list (fn) { - return api( ).find({ }).sort({startDate: -1}).toArray(fn); + function list (fn, count) { + const limit = count !== null ? count : Number(consts.PROFILES_DEFAULT_COUNT); + return api( ).find({ }).limit(limit).sort({startDate: -1}).toArray(fn); + } + + function list_query (opts, fn) { + + storage.queryOpts = { + walker: {} + , dateField: 'startDate' + }; + + function limit () { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + return limit.call(api() + .find(query_for(opts)) + .sort(opts && opts.sort && query_sort(opts) || { startDate: -1 }), opts) + .toArray(fn); + } + + function query_for (opts) { + var retVal = find_options(opts, storage.queryOpts); + return retVal; } + function query_sort (opts) { + if (opts && opts.sort) { + var sortKeys = Object.keys(opts.sort); + + for (var i = 0; i < sortKeys.length; i++) { + if (opts.sort[sortKeys[i]] == '1') { + opts.sort[sortKeys[i]] = 1; + } + else { + opts.sort[sortKeys[i]] = -1; + } + } + return opts.sort; + } + } + + function last (fn) { return api().find().sort({startDate: -1}).limit(1).toArray(fn); } @@ -43,6 +89,7 @@ function storage (collection, ctx) { } api.list = list; + api.list_query = list_query; api.create = create; api.save = save; api.remove = remove; diff --git a/lib/server/pushnotify.js b/lib/server/pushnotify.js index ae30e67d7f0..48d5be11078 100644 --- a/lib/server/pushnotify.js +++ b/lib/server/pushnotify.js @@ -38,7 +38,7 @@ function init (env, ctx) { key = notifyToHash(notify); } } - + notify.key = key; if (recentlySent.get(key)) { diff --git a/lib/server/treatments.js b/lib/server/treatments.js index c02bd50f317..a9107ea99b2 100644 --- a/lib/server/treatments.js +++ b/lib/server/treatments.js @@ -3,7 +3,6 @@ var _ = require('lodash'); var async = require('async'); var moment = require('moment'); - var find_options = require('./query'); function storage (env, ctx) { @@ -25,8 +24,7 @@ function storage (env, ctx) { errs.push(err); callback(err, docs) }); - // eslint-disable-next-line no-unused-vars - }, function (err, docs) { + }, function () { errs = _.compact(errs); done(errs.length > 0 ? errs : null, allDocs); }); @@ -49,12 +47,16 @@ function storage (env, ctx) { }; api( ).update(query, obj, {upsert: true}, function complete (err, updateResults) { + + if (err) console.error('Problem upserting treatment', err); + if (!err) { if (updateResults.result.upserted) { obj._id = updateResults.result.upserted[0]._id } } + // TODO document this feature if (!err && obj.preBolus) { //create a new object to insert copying only the needed fields var pbTreat = { @@ -68,11 +70,32 @@ function storage (env, ctx) { } query.created_at = pbTreat.created_at; - api( ).update(query, pbTreat, {upsert: true}, function pbComplete (err) { + api( ).update(query, pbTreat, {upsert: true}, function pbComplete (err, updateResults) { + + if (!err) { + if (updateResults.result.upserted) { + pbTreat._id = updateResults.result.upserted[0]._id + } + } + var treatments = _.compact([obj, pbTreat]); + + ctx.bus.emit('data-update', { + type: 'treatments', + op: 'update', + changes: ctx.ddata.processRawDataForRuntime(treatments) + }); + fn(err, treatments); }); } else { + + ctx.bus.emit('data-update', { + type: 'treatments', + op: 'update', + changes: ctx.ddata.processRawDataForRuntime([obj]) + }); + fn(err, [obj]); } @@ -100,7 +123,16 @@ function storage (env, ctx) { function remove (opts, fn) { return api( ).remove(query_for(opts), function (err, stat) { - //TODO: this is triggering a read from Mongo, we can do better + //TODO: this is triggering a read from Mongo, we can do better + //console.log('Treatment removed', opts); // , stat); + + ctx.bus.emit('data-update', { + type: 'treatments', + op: 'remove', + count: stat.result.n, + changes: opts.find._id + }); + ctx.bus.emit('data-received'); fn(err, stat); }); @@ -109,7 +141,26 @@ function storage (env, ctx) { function save (obj, fn) { obj._id = new ObjectID(obj._id); prepareData(obj); - api().save(obj, fn); + + function saved (err, created) { + if (!err) { + // console.log('Treatment updated', created); + + ctx.ddata.processRawDataForRuntime(obj); + + ctx.bus.emit('data-update', { + type: 'treatments', + op: 'update', + changes: ctx.ddata.processRawDataForRuntime([obj]) + }); + + } + if (err) console.error('Problem saving treating', err); + + fn(err, created); + } + + api().save(obj, saved); ctx.bus.emit('data-received'); } @@ -134,6 +185,7 @@ function storage (env, ctx) { , 'percent' , 'absolute' , 'duration' + , { 'eventType' : 1, 'duration' : 1, 'created_at' : 1 } ]; api.remove = remove; @@ -147,6 +199,7 @@ function prepareData(obj) { // Convert all dates to UTC dates + // TODO remove this -> must not create new date if missing const d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment(); obj.created_at = d.toISOString(); @@ -170,7 +223,7 @@ function prepareData(obj) { obj.relative = Number(obj.relative); obj.preBolus = Number(obj.preBolus); - //NOTE: the eventTime is sent by the client, but deleted, we only store created_at right now + //NOTE: the eventTime is sent by the client, but deleted, we only store created_at var eventTime; if (obj.eventTime) { eventTime = new Date(obj.eventTime).toISOString(); diff --git a/lib/server/websocket.js b/lib/server/websocket.js index fb0aa38b349..7a685ccde05 100644 --- a/lib/server/websocket.js +++ b/lib/server/websocket.js @@ -7,7 +7,7 @@ var ObjectID = require('mongodb').ObjectID; function init (env, ctx, server) { - function websocket ( ) { + function websocket () { return websocket; } @@ -25,21 +25,20 @@ function init (env, ctx, server) { // TODO: this would be better to have somehow integrated/improved var supportedCollections = { - 'treatments' : env.treatments_collection, - 'entries': env.entries_collection, - 'devicestatus': env.devicestatus_collection, - 'profile': env.profile_collection, - 'food': env.food_collection, - 'activity': env.activity_collection + 'treatments': env.treatments_collection + , 'entries': env.entries_collection + , 'devicestatus': env.devicestatus_collection + , 'profile': env.profile_collection + , 'food': env.food_collection + , 'activity': env.activity_collection }; // This is little ugly copy but I was unable to pass testa after making module from status and share with /api/v1/status - // eslint-disable-next-line no-unused-vars - function status (profile) { + function status () { var versionNum = 0; var verParse = /(\d+)\.(\d+)\.(\d+)*/.exec(env.version); if (verParse) { - versionNum = 10000 * parseInt(verParse[1]) + 100 * parseInt(verParse[2]) + 1 * parseInt(verParse[3]) ; + versionNum = 10000 * parseInt(verParse[1]) + 100 * parseInt(verParse[2]) + 1 * parseInt(verParse[3]); } var apiEnabled = env.api_secret ? true : false; @@ -64,14 +63,22 @@ function init (env, ctx, server) { return info; } - function start ( ) { + function start () { io = require('socket.io')({ - 'transports': ['xhr-polling'], 'log level': 0 + 'transports': ['xhr-polling'] + , 'log level': 0 }).listen(server, { //these only effect the socket.io.js file that is sent to the client, but better than nothing - 'browser client minification': true, - 'browser client etag': true, - 'browser client gzip': false + 'browser client minification': true + , 'browser client etag': true + , 'browser client gzip': false + }); + + ctx.bus.on('teardown', function serverTeardown () { + Object.keys(io.sockets.sockets).forEach(function(s) { + io.sockets.sockets[s].disconnect(true); + }); + io.close(); }); } @@ -79,10 +86,11 @@ function init (env, ctx, server) { ctx.authorization.resolve({ api_secret: message.secret, token: message.token }, function resolved (err, result) { if (err) { - return callback( err, { + return callback(err, { read: false , write: false , write_treatment: false + , error: true }); } @@ -96,9 +104,9 @@ function init (env, ctx, server) { function emitData (delta) { if (lastData.cals) { - console.log(LOG_WS + 'running websocket.emitData', ctx.ddata.lastUpdated); + // console.log(LOG_WS + 'running websocket.emitData', ctx.ddata.lastUpdated); if (lastProfileSwitch !== ctx.ddata.lastProfileFromSwitch) { - console.log(LOG_WS + 'profile switch detected OLD: ' + lastProfileSwitch + ' NEW: ' + ctx.ddata.lastProfileFromSwitch); + // console.log(LOG_WS + 'profile switch detected OLD: ' + lastProfileSwitch + ' NEW: ' + ctx.ddata.lastProfileFromSwitch); delta.status = status(ctx.ddata.profiles); lastProfileSwitch = ctx.ddata.lastProfileFromSwitch; } @@ -106,7 +114,7 @@ function init (env, ctx, server) { } } - function listeners ( ) { + function listeners () { io.sockets.on('connection', function onConnection (socket) { var socketAuthorization = null; var clientType = null; @@ -117,16 +125,15 @@ function init (env, ctx, server) { console.log(LOG_WS + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP); io.emit('clients', ++watchers); - socket.on('ack', function onAck(level, group, silenceTime) { + socket.on('ack', function onAck (level, group, silenceTime) { ctx.notifications.ack(level, group, silenceTime, true); }); - socket.on('disconnect', function onDisconnect ( ) { + socket.on('disconnect', function onDisconnect () { io.emit('clients', --watchers); - console.log(LOG_WS + 'Disconnected client ID: ',socket.client.id); + console.log(LOG_WS + 'Disconnected client ID: ', socket.client.id); }); - function checkConditions (action, data) { var collection = supportedCollections[data.collection]; if (!collection) { @@ -161,10 +168,10 @@ function init (env, ctx, server) { socket.on('loadRetro', function loadRetro (opts, callback) { if (callback) { - callback( { result: 'success' } ); + callback({ result: 'success' }); } //TODO: use opts to only send delta for retro data - socket.emit('retroUpdate', {devicestatus: lastData.devicestatus}); + socket.emit('retroUpdate', { devicestatus: lastData.devicestatus }); console.info('sent retroUpdate', opts); }); @@ -178,30 +185,46 @@ function init (env, ctx, server) { // } // } socket.on('dbUpdate', function dbUpdate (data, callback) { - console.log(LOG_WS + 'dbUpdate client ID: ', socket.client.id, ' data: ', data); - var collection = supportedCollections[data.collection]; + console.log(LOG_WS + 'dbUpdate client ID: ', socket.client.id, ' data: ', data); + var collection = supportedCollections[data.collection]; var check = checkConditions('dbUpdate', data); if (check) { - if (callback) { - callback( check ); + if (callback) { + callback(check); } return; } - var id ; + var id; try { - id = new ObjectID(data._id); - } catch (err){ - console.error(err); + id = new ObjectID(data._id); + } catch (err) { + console.error(err); id = new ObjectID(); } - ctx.store.collection(collection).update( - { '_id': id }, - { $set: data.data } + + ctx.store.collection(collection).update({ '_id': id } + , { $set: data.data } + , function(err, results) { + + if (!err) { + ctx.store.collection(collection).findOne({ '_id': id } + , function(err, results) { + console.log('Got results', results); + if (!err) { + ctx.bus.emit('data-update', { + type: data.collection + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime([results]) + }); + } + }); + } + } ); if (callback) { - callback( { result: 'success' } ); + callback({ result: 'success' }); } ctx.bus.emit('data-received'); }); @@ -216,13 +239,13 @@ function init (env, ctx, server) { // } // } socket.on('dbUpdateUnset', function dbUpdateUnset (data, callback) { - console.log(LOG_WS + 'dbUpdateUnset client ID: ', socket.client.id, ' data: ', data); - var collection = supportedCollections[data.collection]; + console.log(LOG_WS + 'dbUpdateUnset client ID: ', socket.client.id, ' data: ', data); + var collection = supportedCollections[data.collection]; var check = checkConditions('dbUpdate', data); if (check) { - if (callback) { - callback( check ); + if (callback) { + callback(check); } return; } @@ -231,10 +254,25 @@ function init (env, ctx, server) { ctx.store.collection(collection).update( { '_id': objId }, { $unset: data.data } - ); + , function(err, results) { + + if (!err) { + ctx.store.collection(collection).findOne({ '_id': objId } + , function(err, results) { + console.log('Got results', results); + if (!err) { + ctx.bus.emit('data-update', { + type: data.collection + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime([results]) + }); + } + }); + } + }); if (callback) { - callback( { result: 'success' } ); + callback({ result: 'success' }); } ctx.bus.emit('data-received'); }); @@ -248,14 +286,14 @@ function init (env, ctx, server) { // } // } socket.on('dbAdd', function dbAdd (data, callback) { - console.log(LOG_WS + 'dbAdd client ID: ', socket.client.id, ' data: ', data); + console.log(LOG_WS + 'dbAdd client ID: ', socket.client.id, ' data: ', data); var collection = supportedCollections[data.collection]; var maxtimediff = times.mins(1).msecs; var check = checkConditions('dbAdd', data); if (check) { - if (callback) { - callback( check ); + if (callback) { + callback(check); } return; } @@ -271,7 +309,7 @@ function init (env, ctx, server) { if (data.collection === 'treatments') { var query; if (data.data.NSCLIENT_ID) { - query = { NSCLIENT_ID: data.data.NSCLIENT_ID }; + query = { NSCLIENT_ID: data.data.NSCLIENT_ID }; } else { query = { created_at: data.data.created_at @@ -279,19 +317,19 @@ function init (env, ctx, server) { }; } - // try to find exact match + // try to find exact match ctx.store.collection(collection).find(query).toArray(function findResult (err, array) { if (err || array.length > 0) { - console.log(LOG_DEDUP + 'Exact match'); - if (callback) { - callback([array[0]]); - } - return; + console.log(LOG_DEDUP + 'Exact match'); + if (callback) { + callback([array[0]]); + } + return; } - var selected = false; - var query_similiar = { - created_at: {$gte: new Date(new Date(data.data.created_at).getTime() - maxtimediff).toISOString(), $lte: new Date(new Date(data.data.created_at).getTime() + maxtimediff).toISOString()} + var selected = false; + var query_similiar = { + created_at: { $gte: new Date(new Date(data.data.created_at).getTime() - maxtimediff).toISOString(), $lte: new Date(new Date(data.data.created_at).getTime() + maxtimediff).toISOString() } }; if (data.data.insulin) { query_similiar.insulin = data.data.insulin; @@ -305,7 +343,7 @@ function init (env, ctx, server) { query_similiar.percent = data.data.percent; selected = true; } - if (data.data.absolute) { + if (data.data.absolute) { query_similiar.absolute = data.data.absolute; selected = true; } @@ -313,7 +351,7 @@ function init (env, ctx, server) { query_similiar.duration = data.data.duration; selected = true; } - if (data.data.NSCLIENT_ID) { + if (data.data.NSCLIENT_ID) { query_similiar.NSCLIENT_ID = data.data.NSCLIENT_ID; selected = true; } @@ -328,10 +366,7 @@ function init (env, ctx, server) { console.log(LOG_DEDUP + 'Found similiar', array[0]); array[0].created_at = data.data.created_at; var objId = new ObjectID(array[0]._id); - ctx.store.collection(collection).update( - { '_id': objId }, - { $set: {created_at: data.data.created_at} } - ); + ctx.store.collection(collection).update({ '_id': objId }, { $set: { created_at: data.data.created_at } }); if (callback) { callback([array[0]]); } @@ -345,6 +380,13 @@ function init (env, ctx, server) { console.log('treatments data insertion error: ', err.message); return; } + + ctx.bus.emit('data-update', { + type: data.collection + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime(doc.ops) + }); + if (callback) { callback(doc.ops); } @@ -352,7 +394,7 @@ function init (env, ctx, server) { }); }); }); - // devicestatus deduping + // devicestatus deduping } else if (data.collection === 'devicestatus') { var queryDev; if (data.data.NSCLIENT_ID) { @@ -378,8 +420,15 @@ function init (env, ctx, server) { console.log('devicestatus insertion error: ', err.message); return; } + + ctx.bus.emit('data-update', { + type: 'devicestatus' + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime(doc.ops) + }); + if (callback) { - callback(doc.ops); + callback(doc.ops); } ctx.bus.emit('data-received'); }); @@ -389,6 +438,13 @@ function init (env, ctx, server) { console.log(data.collection + ' insertion error: ', err.message); return; } + + ctx.bus.emit('data-update', { + type: data.collection + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime(doc.ops) + }); + if (callback) { callback(doc.ops); } @@ -402,24 +458,34 @@ function init (env, ctx, server) { // _id: 'some mongo record id' // } socket.on('dbRemove', function dbRemove (data, callback) { - console.log(LOG_WS + 'dbRemove client ID: ', socket.client.id, ' data: ', data); - var collection = supportedCollections[data.collection]; + console.log(LOG_WS + 'dbRemove client ID: ', socket.client.id, ' data: ', data); + var collection = supportedCollections[data.collection]; var check = checkConditions('dbUpdate', data); if (check) { - if (callback) { - callback( check ); + if (callback) { + callback(check); } return; } var objId = new ObjectID(data._id); - ctx.store.collection(collection).remove( - { '_id': objId } - ); + ctx.store.collection(collection).remove({ '_id': objId } + , function(err, stat) { + + if (!err) { + ctx.bus.emit('data-update', { + type: data.collection + , op: 'remove' + , count: stat.result.n + , changes: data._id + }); + + } + }); if (callback) { - callback( { result: 'success' } ); + callback({ result: 'success' }); } ctx.bus.emit('data-received'); }); @@ -436,34 +502,21 @@ function init (env, ctx, server) { socketAuthorization = authorization; clientType = message.client; history = message.history || 48; //default history is 48 hours - var from = message.from; if (socketAuthorization.read) { socket.join('DataReceivers'); - var filterTreatments = false; - var msecHistory = times.hours(history).msecs; - // if `from` is received, it's a reconnection and full data is not needed - if (from && from > 0) { - filterTreatments = true; - msecHistory = Math.min(new Date().getTime() - from, msecHistory); - } - // send all data upon new connection - if (lastData && lastData.splitRecent) { - var split = lastData.splitRecent(Date.now(), times.hours(3).msecs, msecHistory, filterTreatments); + + if (lastData && lastData.dataWithRecentStatuses) { + let data = lastData.dataWithRecentStatuses(); + if (message.status) { - split.first.status = status(split.first.profiles); + data.status = status(data.profiles); } - //send out first chunk - socket.emit('dataUpdate', split.first); - - //then send out the rest - setTimeout(function sendTheRest() { - split.rest.delta = true; - socket.emit('dataUpdate', split.rest); - }, 500); + + socket.emit('dataUpdate', data); } } - console.log(LOG_WS + 'Authetication ID: ', socket.client.id, ' client: ', clientType, ' history: ' + history); + // console.log(LOG_WS + 'Authetication ID: ', socket.client.id, ' client: ', clientType, ' history: ' + history); if (callback) { callback(socketAuthorization); } @@ -477,7 +530,7 @@ function init (env, ctx, server) { socket.on('nsping', function ping (message, callback) { var clientTime = message.mills; timeDiff = new Date().getTime() - clientTime; - console.log(LOG_WS + 'Ping from client ID: ',socket.client.id, ' client: ', clientType, ' timeDiff: ', (timeDiff/1000).toFixed(1) + 'sec'); + // console.log(LOG_WS + 'Ping from client ID: ',socket.client.id, ' client: ', clientType, ' timeDiff: ', (timeDiff/1000).toFixed(1) + 'sec'); if (callback) { callback({ result: 'pong', mills: new Date().getTime(), authorization: socketAuthorization }); } @@ -485,15 +538,15 @@ function init (env, ctx, server) { }); } - websocket.update = function update ( ) { - console.log(LOG_WS + 'running websocket.update'); + websocket.update = function update () { + // console.log(LOG_WS + 'running websocket.update'); if (lastData.sgvs) { var delta = calcData(lastData, ctx.ddata); if (delta.delta) { - console.log('lastData full size', JSON.stringify(lastData).length,'bytes'); - if (delta.sgvs) { console.log('patientData update size', JSON.stringify(delta).length,'bytes'); } + // console.log('lastData full size', JSON.stringify(lastData).length,'bytes'); + // if (delta.sgvs) { console.log('patientData update size', JSON.stringify(delta).length,'bytes'); } emitData(delta); - } else { console.log('delta calculation indicates no new data is present'); } + }; // else { console.log('delta calculation indicates no new data is present'); } } lastData = ctx.ddata.clone(); }; @@ -517,8 +570,12 @@ function init (env, ctx, server) { } }; - start( ); - listeners( ); + start(); + listeners(); + + if (ctx.storageSocket) { + ctx.storageSocket.init(io); + } return websocket(); } diff --git a/lib/settings.js b/lib/settings.js index 2d16abd0f2d..845cea07628 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -2,11 +2,12 @@ var _ = require('lodash'); var levels = require('./levels'); +var constants = require('./constants.json'); function init () { var settings = { - units: 'mg/dL' + units: 'mg/dl' , timeFormat: 12 , nightMode: false , editMode: true @@ -30,7 +31,7 @@ function init () { , alarmPumpBatteryLow: false , language: 'en' , scaleY: 'log' - , showPlugins: '' + , showPlugins: 'dbsize' , showForecast: 'ar2' , focusHours: 3 , heartbeat: 60 @@ -47,10 +48,36 @@ function init () { , secureHstsHeaderIncludeSubdomains: false , secureHstsHeaderPreload: false , secureCsp: false - , showClockClosebutton: true , deNormalizeDates: false + , showClockDelta: false + , showClockLastTime: false + , bolusRenderOver: 1 + , frameUrl1: '' + , frameUrl2: '' + , frameUrl3: '' + , frameUrl4: '' + , frameUrl5: '' + , frameUrl6: '' + , frameUrl7: '' + , frameUrl8: '' + , frameName1: '' + , frameName2: '' + , frameName3: '' + , frameName4: '' + , frameName5: '' + , frameName6: '' + , frameName7: '' + , frameName8: '' }; + var secureSettings = [ + 'apnsKey' + , 'apnsKeyId' + , 'developerTeamId' + , 'userName' + , 'password' + ]; + var valueMappers = { nightMode: mapTruthy , alarmUrgentHigh: mapTruthy @@ -69,10 +96,38 @@ function init () { , insecureUseHttp: mapTruthy , secureHstsHeader: mapTruthy , secureCsp: mapTruthy - , showClockClosebutton: mapTruthy , deNormalizeDates: mapTruthy + , showClockDelta: mapTruthy + , showClockLastTime: mapTruthy + , bgHigh: mapNumber + , bgLow: mapNumber + , bgTargetTop: mapNumber + , bgTargetBottom: mapNumber }; + function filterObj(obj, secureKeys) { + if (obj && typeof obj === 'object') { + var allKeys = Object.keys(obj); + for (var i = 0 ; i < allKeys.length ; i++) { + var k = allKeys[i]; + if (secureKeys.includes(k)) { + delete obj[k]; + } else { + var value = obj[k]; + if ( typeof value === 'object') { + filterObj(value, secureKeys); + } + } + } + } + return obj; + } + + function filteredSettings(settingsObject) { + let so = _.cloneDeep(settingsObject); + return filterObj(so, secureSettings); + } + function mapNumberArray (value) { if (!value || _.isArray(value)) { return value; @@ -93,6 +148,11 @@ function init () { return value; } + if (typeof value === 'string' && isNaN(value)) { + const decommaed = value.replace(',','.'); + if (!isNaN(decommaed)) { value = decommaed; } + } + if (isNaN(value)) { return value; } else { @@ -107,7 +167,7 @@ function init () { } //TODO: getting sent in status.json, shouldn't be - settings.DEFAULT_FEATURES = ['bgnow', 'delta', 'direction', 'timeago', 'devicestatus', 'upbat', 'errorcodes', 'profile']; + settings.DEFAULT_FEATURES = ['bgnow', 'delta', 'direction', 'timeago', 'devicestatus', 'upbat', 'errorcodes', 'profile', 'dbsize']; var wasSet = []; @@ -146,7 +206,9 @@ function init () { function getAndPrepare (key) { var raw = accessor(nameFromKey(key, nameType)) || ''; var cleaned = decodeURIComponent(raw).toLowerCase(); - return cleaned ? cleaned.split(' ') : []; + cleaned = cleaned ? cleaned.split(' ') : []; + cleaned = _.filter(cleaned, function(e) { return e !== ""; } ); + return cleaned; } function enableIf (feature, condition) { @@ -208,6 +270,14 @@ function init () { thresholds.bgTargetBottom = Number(thresholds.bgTargetBottom); thresholds.bgLow = Number(thresholds.bgLow); + // Do not convert for old installs that have these set in mg/dl + if (settings.units.toLowerCase().includes('mmol') && thresholds.bgHigh < 50) { + thresholds.bgHigh = Math.round(thresholds.bgHigh * constants.MMOL_TO_MGDL); + thresholds.bgTargetTop = Math.round(thresholds.bgTargetTop * constants.MMOL_TO_MGDL); + thresholds.bgTargetBottom = Math.round(thresholds.bgTargetBottom * constants.MMOL_TO_MGDL); + thresholds.bgLow = Math.round(thresholds.bgLow * constants.MMOL_TO_MGDL); + } + verifyThresholds(); adjustShownPlugins(); } @@ -270,34 +340,40 @@ function init () { return enabled; } - function isAlarmEventEnabled (notify) { - var enabled = false; + function isUrgentHighAlarmEnabled(notify) { + return notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh; + } - if ('high' !== notify.eventName && 'low' !== notify.eventName) { - enabled = true; - } else if (notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh) { - enabled = true; - } else if (notify.eventName === 'high' && settings.alarmHigh) { - enabled = true; - } else if (notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow) { - enabled = true; - } else if (notify.eventName === 'low' && settings.alarmLow) { - enabled = true; - } + function isHighAlarmEnabled(notify) { + return notify.eventName === 'high' && settings.alarmHigh; + } - return enabled; + function isUrgentLowAlarmEnabled(notify) { + return notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow; + } + + function isLowAlarmEnabled(notify) { + return notify.eventName === 'low' && settings.alarmLow; + } + + function isAlarmEventEnabled (notify) { + return ('high' !== notify.eventName && 'low' !== notify.eventName) + || isUrgentHighAlarmEnabled(notify) + || isHighAlarmEnabled(notify) + || isUrgentLowAlarmEnabled(notify) + || isLowAlarmEnabled(notify); } function snoozeMinsForAlarmEvent (notify) { var snoozeTime; - if (notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh) { + if (isUrgentHighAlarmEnabled(notify)) { snoozeTime = settings.alarmUrgentHighMins; - } else if (notify.eventName === 'high' && settings.alarmHigh) { + } else if (isHighAlarmEnabled(notify)) { snoozeTime = settings.alarmHighMins; - } else if (notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow) { + } else if (isUrgentLowAlarmEnabled(notify)) { snoozeTime = settings.alarmUrgentLowMins; - } else if (notify.eventName === 'low' && settings.alarmLow) { + } else if (isLowAlarmEnabled(notify)) { snoozeTime = settings.alarmLowMins; } else if (notify.level === levels.URGENT) { snoozeTime = settings.alarmUrgentMins; @@ -318,6 +394,7 @@ function init () { settings.isAlarmEventEnabled = isAlarmEventEnabled; settings.snoozeMinsForAlarmEvent = snoozeMinsForAlarmEvent; settings.snoozeFirstMinsForAlarmEvent = snoozeFirstMinsForAlarmEvent; + settings.filteredSettings = filteredSettings; return settings; diff --git a/lib/storage/mongo-storage.js b/lib/storage/mongo-storage.js index 275cde6eb8a..39c1a81e641 100644 --- a/lib/storage/mongo-storage.js +++ b/lib/storage/mongo-storage.js @@ -1,12 +1,11 @@ 'use strict'; var mongodb = require('mongodb'); - var connection = null; +var MongoClient = mongodb.MongoClient; +var mongo = {}; function init (env, cb, forceNewConnection) { - var MongoClient = mongodb.MongoClient; - var mongo = {}; function maybe_connect (cb) { @@ -20,61 +19,76 @@ function init (env, cb, forceNewConnection) { } } else { if (!env.storageURI) { - throw new Error('MongoDB connection string is missing. Please set MONGO_CONNECTION environment variable'); + throw new Error('MongoDB connection string is missing. Please set MONGODB_URI environment variable'); } console.log('Setting up new connection to MongoDB'); - var timeout = 30 * 1000; - var options = { reconnectInterval: 10000, reconnectTries: 500, connectTimeoutMS: timeout, - socketTimeoutMS: timeout, useNewUrlParser: true }; + var timeout = 30 * 1000; + var options = { + reconnectInterval: 10000 + , reconnectTries: 500 + , connectTimeoutMS: timeout + , socketTimeoutMS: timeout + , useNewUrlParser: true + }; var connect_with_retry = function(i) { - return MongoClient.connect(env.storageURI, options, function connected(err, client) { + + MongoClient.connect(env.storageURI, options) + .then(client => { + console.log('Successfully established a connected to MongoDB'); + + var dbName = env.storageURI.split('/').pop().split('?'); + dbName = dbName[0]; // drop Connection Options + mongo.db = client.db(dbName); + connection = mongo.db; + mongo.client = client; + // If there is a valid callback, then invoke the function to perform the callback + + if (cb && cb.call) { + cb(null, mongo); + } + }) + .catch(err => { + if (err.message && err.message.includes('AuthenticationFailed')) { + console.log('Authentication to Mongo failed'); + cb(new Error('MongoDB authentication failed! Double check the URL has the right username and password in MONGODB_URI.'), null); + return; + } + + if (err.name && err.name === "MongoNetworkError") { + var timeout = (i > 15) ? 60000 : i * 3000; + console.log('Error connecting to MongoDB: %j - retrying in ' + timeout / 1000 + ' sec', err); + setTimeout(connect_with_retry, timeout, i + 1); + if (i == 1) cb(new Error('MongoDB connection failed! Double check the MONGODB_URI setting in Heroku.'), null); + } else { + cb(new Error('MONGODB_URI ' + env.storageURI + ' seems invalid: ' + err.message)); + } + }); + + }; + + return connect_with_retry(1); + + } + } + + mongo.collection = function get_collection (name) { + return connection.collection(name); + }; + + mongo.ensureIndexes = function ensureIndexes (collection, fields) { + fields.forEach(function(field) { + console.info('ensuring index for: ' + field); + collection.createIndex(field, { 'background': true }, function(err) { if (err) { - //console.log('err=', err) - if (err.name && err.name === "MongoNetworkError") { - var timeout = (i > 15) ? 60000 : i*3000; - console.log('Error connecting to MongoDB: %j - retrying in ' + timeout/1000 + ' sec', err); - setTimeout(connect_with_retry, timeout, i+1); - } else if (err.message) { - throw new Error('MongoDB connection string '+env.storageURI+' seems invalid: '+err.message) ; - } - } else { - console.log('Successfully established a connected to MongoDB'); - - var dbName = env.storageURI.split('/').pop().split('?'); - dbName=dbName[0]; // drop Connection Options - mongo.db = client.db(dbName); - connection = mongo.db; - mongo.client = client; - // If there is a valid callback, then invoke the function to perform the callback - - if (cb && cb.call) { - cb(err, mongo); - } + console.error('unable to ensureIndex for: ' + field + ' - ' + err); } }); - }; - connect_with_retry(1); - } - } - - mongo.collection = function get_collection (name) { - return connection.collection(name); - }; - - mongo.ensureIndexes = function ensureIndexes (collection, fields) { - fields.forEach(function (field) { - console.info('ensuring index for: ' + field); - collection.ensureIndex(field, function (err) { - if (err) { - console.error('unable to ensureIndex for: ' + field + ' - ' + err); - } }); - }); - }; + }; - return maybe_connect(cb); -} + return maybe_connect(cb); + } -module.exports = init; + module.exports = init; diff --git a/lib/units.js b/lib/units.js index f548d55744e..5eb36c10950 100644 --- a/lib/units.js +++ b/lib/units.js @@ -1,11 +1,13 @@ 'use strict'; +var consts = require('./constants'); + function mgdlToMMOL(mgdl) { - return (Math.round((mgdl / 18) * 10) / 10).toFixed(1); + return (Math.round((mgdl / consts.MMOL_TO_MGDL) * 10) / 10).toFixed(1); } function mmolToMgdl(mgdl) { - return Math.round(mgdl * 18); + return Math.round(mgdl * consts.MMOL_TO_MGDL); } function configure() { diff --git a/lib/utils.js b/lib/utils.js index fe1778f8120..083c4284846 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -39,7 +39,8 @@ function init(ctx) { return '0'; } var mult = Math.pow(10,digits); - var fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult + var fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult; + if (isNaN(fixed)) return '0'; return String(fixed); }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 1a9c1dd716b..14b8d343cf4 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,104 +1,63 @@ { "name": "nightscout", - "version": "0.12.5", + "version": "14.0.6", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/compat-data": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz", + "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==", "requires": { - "@babel/highlight": "^7.0.0" + "browserslist": "^4.12.0", + "invariant": "^2.2.4", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "@babel/core": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.6.2.tgz", - "integrity": "sha512-l8zto/fuoZIbncm+01p8zPSDZu/VuuJhAfA7d/AbzM09WR7iVhavvfNDYCNpo1VvLk6E6xgAoP9P+/EMJHuRkQ==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.2", - "@babel/helpers": "^7.6.2", - "@babel/parser": "^7.6.2", - "@babel/template": "^7.6.0", - "@babel/traverse": "^7.6.2", - "@babel/types": "^7.6.0", - "convert-source-map": "^1.1.0", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.4.tgz", + "integrity": "sha512-5deljj5HlqRXN+5oJTY7Zs37iH3z3b++KjiKtIsJy1NrjOOVSEaJHEetLBhyu0aQOSNNZ/0IuEAan9GzRuDXHg==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.4", + "@babel/helper-module-transforms": "^7.11.0", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.11.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.11.0", + "@babel/types": "^7.11.0", + "convert-source-map": "^1.7.0", "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.2.tgz", - "integrity": "sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ==", - "requires": { - "@babel/types": "^7.6.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/parser": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.2.tgz", - "integrity": "sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==" - }, - "@babel/template": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", - "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.0" - } - }, - "@babel/traverse": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.2.tgz", - "integrity": "sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.2", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.6.2", - "@babel/types": "^7.6.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" + "safe-buffer": "~5.1.1" } }, "debug": { @@ -109,24 +68,19 @@ "ms": "^2.1.1" } }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json5": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", - "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", - "requires": { - "minimist": "^1.2.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "requires": { + "path-parse": "^1.0.6" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -140,22 +94,15 @@ } }, "@babel/generator": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.0.tgz", - "integrity": "sha512-/v5I+a1jhGSKLgZDcmAUZ4K/VePi43eRkUs3yePW1HB1iANOD5tqJXwGSG4BZhSksP8J9ejSlwGeTiiOFZOrXQ==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "requires": { - "@babel/types": "^7.4.0", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" + "source-map": "^0.5.0" }, "dependencies": { - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -164,1140 +111,821 @@ } }, "@babel/helper-annotate-as-pure": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", - "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "requires": { - "@babel/types": "^7.0.0" + "@babel/types": "^7.10.4" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", - "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", "requires": { - "@babel/helper-explode-assignable-expression": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" } }, - "@babel/helper-call-delegate": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", - "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", + "@babel/helper-compilation-targets": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", + "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", "requires": { - "@babel/helper-hoist-variables": "^7.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" + "@babel/compat-data": "^7.10.4", + "browserslist": "^4.12.0", + "invariant": "^2.2.4", + "levenary": "^1.1.1", + "semver": "^5.5.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.2.tgz", - "integrity": "sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ==", - "requires": { - "@babel/types": "^7.6.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/parser": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.2.tgz", - "integrity": "sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==" - }, - "@babel/traverse": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.2.tgz", - "integrity": "sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.2", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.6.2", - "@babel/types": "^7.6.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz", + "integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==", + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.10.5", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", + "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-regex": "^7.10.4", + "regexpu-core": "^4.7.0" + } + }, "@babel/helper-define-map": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz", - "integrity": "sha512-fTfxx7i0B5NJqvUOBBGREnrqbTxRh7zinBANpZXAVDlsZxYdclDp467G1sQ8VZYMnAURY3RpBUAgOYT9GfzHBg==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/types": "^7.5.5", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" } }, "@babel/helper-explode-assignable-expression": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", - "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz", + "integrity": "sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==", "requires": { - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/types": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "requires": { - "@babel/types": "^7.0.0" + "@babel/types": "^7.10.4" } }, "@babel/helper-hoist-variables": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", - "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", "requires": { - "@babel/types": "^7.4.4" - }, - "dependencies": { - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.10.4" } }, "@babel/helper-member-expression-to-functions": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz", - "integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", "requires": { - "@babel/types": "^7.5.5" - }, - "dependencies": { - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/types": "^7.11.0" } }, "@babel/helper-module-imports": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "requires": { - "@babel/types": "^7.0.0" + "@babel/types": "^7.10.4" } }, "@babel/helper-module-transforms": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz", - "integrity": "sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-simple-access": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/template": "^7.4.4", - "@babel/types": "^7.5.5", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/parser": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.2.tgz", - "integrity": "sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==" - }, - "@babel/template": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", - "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.0" - } - }, - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" } }, "@babel/helper-optimise-call-expression": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", - "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", "requires": { - "@babel/types": "^7.0.0" + "@babel/types": "^7.10.4" } }, "@babel/helper-plugin-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==" + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" }, "@babel/helper-regex": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz", - "integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", + "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", "requires": { - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/helper-remap-async-to-generator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", - "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", + "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-wrap-function": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-replace-supers": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz", - "integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", "requires": { - "@babel/helper-member-expression-to-functions": "^7.5.5", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/traverse": "^7.5.5", - "@babel/types": "^7.5.5" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.2.tgz", - "integrity": "sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ==", - "requires": { - "@babel/types": "^7.6.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/parser": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.2.tgz", - "integrity": "sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==" - }, - "@babel/traverse": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.2.tgz", - "integrity": "sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.2", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.6.2", - "@babel/types": "^7.6.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-simple-access": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", - "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", "requires": { - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", + "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", + "requires": { + "@babel/types": "^7.11.0" } }, "@babel/helper-split-export-declaration": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.0.tgz", - "integrity": "sha512-7Cuc6JZiYShaZnybDmfwhY4UYHzI6rlqhWjaIqbsJGsIqPimEYy5uh3akSRLMg65LSdSEnJ8a8/bWQN6u2oMGw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "requires": { - "@babel/types": "^7.4.0" + "@babel/types": "^7.11.0" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" + }, "@babel/helper-wrap-function": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", - "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", + "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.2.0" + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helpers": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.6.2.tgz", - "integrity": "sha512-3/bAUL8zZxYs1cdX2ilEE0WobqbCmKWr/889lf2SS0PpDcpEIY8pb1CCyz0pEcX3pEb+MCbks1jIokz2xLtGTA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", "requires": { - "@babel/template": "^7.6.0", - "@babel/traverse": "^7.6.2", - "@babel/types": "^7.6.0" - }, - "dependencies": { - "@babel/generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.2.tgz", - "integrity": "sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ==", - "requires": { - "@babel/types": "^7.6.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/parser": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.2.tgz", - "integrity": "sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==" - }, - "@babel/template": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", - "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.0" - } - }, - "@babel/traverse": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.2.tgz", - "integrity": "sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.2", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.6.2", - "@babel/types": "^7.6.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "requires": { - "@babel/highlight": "^7.0.0" - } - } - } - }, - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "requires": { + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@babel/parser": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.3.tgz", - "integrity": "sha512-gxpEUhTS1sGA63EGQGuA+WESPR/6tz6ng7tSHFCmaTJK/cGK8y37cBTspX+U2xCAue2IQVvF6Z0oigmjwD8YGQ==" + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==" }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", - "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", + "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0", - "@babel/plugin-syntax-async-generators": "^7.2.0" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4", + "@babel/plugin-syntax-async-generators": "^7.8.0" } }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", - "integrity": "sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==", + "@babel/plugin-proposal-class-properties": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", + "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0" + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, - "@babel/plugin-proposal-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", - "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "@babel/plugin-proposal-dynamic-import": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", + "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-json-strings": "^7.2.0" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.0" } }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.6.2.tgz", - "integrity": "sha512-LDBXlmADCsMZV1Y9OQwMc0MyGZ8Ta/zlD9N67BfQT8uYwkRswiu2hU6nJKrjrt/58aH/vqfQlR/9yId/7A2gWw==", + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", + "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", + "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.0" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz", + "integrity": "sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", + "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", + "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", + "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.10.4" } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", + "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", + "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", + "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.6.2.tgz", - "integrity": "sha512-NxHETdmpeSCtiatMRYWVJo7266rrvAC3DTeG5exQBIH/fMIUK7ejDNznBbn3HQl/o9peymRRg7Yqkx6PdUXmMw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", + "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.6.0" - }, - "dependencies": { - "regexpu-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", - "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.1.0", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.1.0" - } - }, - "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==" - }, - "regjsparser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "requires": { - "jsesc": "~0.5.0" - } - } + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-async-generators": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", - "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", + "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-dynamic-import": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", - "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" } }, "@babel/plugin-syntax-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", - "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-object-rest-spread": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.8.0" } }, "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", + "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", - "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", + "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", - "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", + "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0" + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", - "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", + "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.2.tgz", - "integrity": "sha512-zZT8ivau9LOQQaOGC7bQLQOT4XPkPXgN2ERfUgk1X8ql+mVkLc4E8eKk+FO3o0154kxzqenWCorfmEXpEZcrSQ==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz", + "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "lodash": "^4.17.13" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-classes": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.5.5.tgz", - "integrity": "sha512-U2htCNK/6e9K7jGyJ++1p5XRU+LJjrwtoiVn9SzRlDT2KubcZ11OOwy3s24TjHxPgxNwonCYP7U2K51uVYCMDg==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-define-map": "^7.5.5", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.5.5", - "@babel/helper-split-export-declaration": "^7.4.4", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", + "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" - }, - "dependencies": { - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/plugin-transform-computed-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", - "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", + "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-destructuring": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz", - "integrity": "sha512-2bGIS5P1v4+sWTCnKNDZDxbGvEqi0ijeqM/YqHtVGrvG2y0ySgnEEhXErvE9dA0bnIzY9bIzdFK0jFA46ASIIQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", + "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.6.2.tgz", - "integrity": "sha512-KGKT9aqKV+9YMZSkowzYoYEiHqgaDhGmPNZlZxX6UeHC4z30nC1J9IrZuGqbYFB1jaIGdv91ujpze0exiVK8bA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", + "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.6.0" - }, - "dependencies": { - "regexpu-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", - "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.1.0", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.1.0" - } - }, - "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==" - }, - "regjsparser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "requires": { - "jsesc": "~0.5.0" - } - } + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", - "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", + "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", - "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", + "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-for-of": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", - "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", + "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-function-name": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", - "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", + "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", - "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", + "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", - "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", + "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", - "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", + "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0", - "babel-plugin-dynamic-import-node": "^2.3.0" + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.6.0.tgz", - "integrity": "sha512-Ma93Ix95PNSEngqomy5LSBMAQvYKVe3dy+JlVJSHEXZR5ASL9lQBedMiCyVtmTLraIDVRE3ZjTZvmXXD2Ozw3g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", + "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", "requires": { - "@babel/helper-module-transforms": "^7.4.4", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-simple-access": "^7.1.0", - "babel-plugin-dynamic-import-node": "^2.3.0" + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz", - "integrity": "sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", + "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", "requires": { - "@babel/helper-hoist-variables": "^7.4.4", - "@babel/helper-plugin-utils": "^7.0.0", - "babel-plugin-dynamic-import-node": "^2.3.0" + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", - "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", + "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.6.2.tgz", - "integrity": "sha512-xBdB+XOs+lgbZc2/4F5BVDVcDNS4tcSKQc96KmlqLEAwz6tpYPEvPdmDfvVG0Ssn8lAhronaRs6Z6KSexIpK5g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", + "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", "requires": { - "regexpu-core": "^4.6.0" - }, - "dependencies": { - "regexpu-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", - "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.1.0", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.1.0" - } - }, - "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==" - }, - "regjsparser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "requires": { - "jsesc": "~0.5.0" - } - } + "@babel/helper-create-regexp-features-plugin": "^7.10.4" } }, "@babel/plugin-transform-new-target": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", - "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", + "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-object-super": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz", - "integrity": "sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", + "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.5.5" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4" } }, "@babel/plugin-transform-parameters": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", - "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", + "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", "requires": { - "@babel/helper-call-delegate": "^7.4.4", - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-property-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", - "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", + "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-regenerator": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", - "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", + "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", "requires": { - "regenerator-transform": "^0.14.0" + "regenerator-transform": "^0.14.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", - "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", + "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", - "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", + "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-spread": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.6.2.tgz", - "integrity": "sha512-DpSvPFryKdK1x+EDJYCy28nmAaIMdxmhot62jAXF/o99iA33Zj2Lmcp3vDmz+MUh0LNYVPvfj5iC3feb3/+PFg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz", + "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", - "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", + "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-regex": "^7.10.4" } }, "@babel/plugin-transform-template-literals": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", - "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz", + "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==", "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", - "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", + "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", + "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.6.2.tgz", - "integrity": "sha512-orZI6cWlR3nk2YmYdb0gImrgCUwb5cBUwjf6Ks6dvNVvXERkwtJWOQaEOjPiu0Gu1Tq6Yq/hruCZZOOi9F34Dw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", + "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.6.0" - }, - "dependencies": { - "regexpu-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", - "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.1.0", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.1.0" - } - }, - "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==" - }, - "regjsparser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "requires": { - "jsesc": "~0.5.0" - } - } + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/preset-env": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.6.2.tgz", - "integrity": "sha512-Ru7+mfzy9M1/YTEtlDS8CD45jd22ngb9tXnn64DvQK3ooyqSw9K4K9DUWmYknTTVk4TqygL9dqCrZgm1HMea/Q==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.2.0", - "@babel/plugin-proposal-dynamic-import": "^7.5.0", - "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-object-rest-spread": "^7.6.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.6.2", - "@babel/plugin-syntax-async-generators": "^7.2.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-syntax-json-strings": "^7.2.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", - "@babel/plugin-transform-arrow-functions": "^7.2.0", - "@babel/plugin-transform-async-to-generator": "^7.5.0", - "@babel/plugin-transform-block-scoped-functions": "^7.2.0", - "@babel/plugin-transform-block-scoping": "^7.6.2", - "@babel/plugin-transform-classes": "^7.5.5", - "@babel/plugin-transform-computed-properties": "^7.2.0", - "@babel/plugin-transform-destructuring": "^7.6.0", - "@babel/plugin-transform-dotall-regex": "^7.6.2", - "@babel/plugin-transform-duplicate-keys": "^7.5.0", - "@babel/plugin-transform-exponentiation-operator": "^7.2.0", - "@babel/plugin-transform-for-of": "^7.4.4", - "@babel/plugin-transform-function-name": "^7.4.4", - "@babel/plugin-transform-literals": "^7.2.0", - "@babel/plugin-transform-member-expression-literals": "^7.2.0", - "@babel/plugin-transform-modules-amd": "^7.5.0", - "@babel/plugin-transform-modules-commonjs": "^7.6.0", - "@babel/plugin-transform-modules-systemjs": "^7.5.0", - "@babel/plugin-transform-modules-umd": "^7.2.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.6.2", - "@babel/plugin-transform-new-target": "^7.4.4", - "@babel/plugin-transform-object-super": "^7.5.5", - "@babel/plugin-transform-parameters": "^7.4.4", - "@babel/plugin-transform-property-literals": "^7.2.0", - "@babel/plugin-transform-regenerator": "^7.4.5", - "@babel/plugin-transform-reserved-words": "^7.2.0", - "@babel/plugin-transform-shorthand-properties": "^7.2.0", - "@babel/plugin-transform-spread": "^7.6.2", - "@babel/plugin-transform-sticky-regex": "^7.2.0", - "@babel/plugin-transform-template-literals": "^7.4.4", - "@babel/plugin-transform-typeof-symbol": "^7.2.0", - "@babel/plugin-transform-unicode-regex": "^7.6.2", - "@babel/types": "^7.6.0", - "browserslist": "^4.6.0", - "core-js-compat": "^3.1.1", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz", + "integrity": "sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg==", + "requires": { + "@babel/compat-data": "^7.11.0", + "@babel/helper-compilation-targets": "^7.10.4", + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-proposal-async-generator-functions": "^7.10.4", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-dynamic-import": "^7.10.4", + "@babel/plugin-proposal-export-namespace-from": "^7.10.4", + "@babel/plugin-proposal-json-strings": "^7.10.4", + "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-numeric-separator": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.10.4", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", + "@babel/plugin-proposal-private-methods": "^7.10.4", + "@babel/plugin-proposal-unicode-property-regex": "^7.10.4", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.10.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.10.4", + "@babel/plugin-transform-arrow-functions": "^7.10.4", + "@babel/plugin-transform-async-to-generator": "^7.10.4", + "@babel/plugin-transform-block-scoped-functions": "^7.10.4", + "@babel/plugin-transform-block-scoping": "^7.10.4", + "@babel/plugin-transform-classes": "^7.10.4", + "@babel/plugin-transform-computed-properties": "^7.10.4", + "@babel/plugin-transform-destructuring": "^7.10.4", + "@babel/plugin-transform-dotall-regex": "^7.10.4", + "@babel/plugin-transform-duplicate-keys": "^7.10.4", + "@babel/plugin-transform-exponentiation-operator": "^7.10.4", + "@babel/plugin-transform-for-of": "^7.10.4", + "@babel/plugin-transform-function-name": "^7.10.4", + "@babel/plugin-transform-literals": "^7.10.4", + "@babel/plugin-transform-member-expression-literals": "^7.10.4", + "@babel/plugin-transform-modules-amd": "^7.10.4", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-modules-systemjs": "^7.10.4", + "@babel/plugin-transform-modules-umd": "^7.10.4", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4", + "@babel/plugin-transform-new-target": "^7.10.4", + "@babel/plugin-transform-object-super": "^7.10.4", + "@babel/plugin-transform-parameters": "^7.10.4", + "@babel/plugin-transform-property-literals": "^7.10.4", + "@babel/plugin-transform-regenerator": "^7.10.4", + "@babel/plugin-transform-reserved-words": "^7.10.4", + "@babel/plugin-transform-shorthand-properties": "^7.10.4", + "@babel/plugin-transform-spread": "^7.11.0", + "@babel/plugin-transform-sticky-regex": "^7.10.4", + "@babel/plugin-transform-template-literals": "^7.10.4", + "@babel/plugin-transform-typeof-symbol": "^7.10.4", + "@babel/plugin-transform-unicode-escapes": "^7.10.4", + "@babel/plugin-transform-unicode-regex": "^7.10.4", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.11.0", + "browserslist": "^4.12.0", + "core-js-compat": "^3.6.2", "invariant": "^2.2.2", - "js-levenshtein": "^1.1.3", + "levenary": "^1.1.1", "semver": "^5.5.0" }, "dependencies": { - "@babel/types": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", - "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -1305,30 +933,50 @@ } } }, + "@babel/preset-modules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "@babel/template": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.0.tgz", - "integrity": "sha512-SOWwxxClTTh5NdbbYZ0BmaBVzxzTh2tO/TeLTbF6MO6EzVhHTnff8CdBXx3mEtazFBoysmEM6GU/wF+SuSx4Fw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.0", - "@babel/types": "^7.4.0" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.3.tgz", - "integrity": "sha512-HmA01qrtaCwwJWpSKpA948cBvU5BrmviAief/b3AVw936DtcdsTexlbyzNuDnthwhOQ37xshn7hvQaEQk7ISYQ==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.0", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/types": "^7.4.0", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.11" + "lodash": "^4.17.19" }, "dependencies": { "debug": { @@ -1340,177 +988,187 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, "@babel/types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.0.tgz", - "integrity": "sha512-aPvkXyU2SPOnztlgo8n9cEiXW755mgyvueUPcpStqdzoSPm0fjO0vQBjLkt3JKJW7ufikfcnMTTPsN1xaTsBPA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==" + }, "@webassemblyjs/ast": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", - "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", "requires": { - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5" + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", - "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==" }, "@webassemblyjs/helper-api-error": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", - "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==" }, "@webassemblyjs/helper-buffer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", - "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==" }, "@webassemblyjs/helper-code-frame": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", - "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", "requires": { - "@webassemblyjs/wast-printer": "1.8.5" + "@webassemblyjs/wast-printer": "1.9.0" } }, "@webassemblyjs/helper-fsm": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", - "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==" }, "@webassemblyjs/helper-module-context": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", - "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", "requires": { - "@webassemblyjs/ast": "1.8.5", - "mamacro": "^0.0.3" + "@webassemblyjs/ast": "1.9.0" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", - "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==" }, "@webassemblyjs/helper-wasm-section": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", - "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" } }, "@webassemblyjs/ieee754": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", - "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", - "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", - "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==" }, "@webassemblyjs/wasm-edit": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", - "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/helper-wasm-section": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-opt": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "@webassemblyjs/wast-printer": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" } }, "@webassemblyjs/wasm-gen": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", - "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" } }, "@webassemblyjs/wasm-opt": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", - "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" } }, "@webassemblyjs/wasm-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", - "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" } }, "@webassemblyjs/wast-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", - "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/floating-point-hex-parser": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-code-frame": "1.8.5", - "@webassemblyjs/helper-fsm": "1.8.5", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/wast-printer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", - "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", "@xtuc/long": "4.2.2" } }, @@ -1536,45 +1194,45 @@ "dev": true }, "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" + "mime-types": "~2.1.24", + "negotiator": "0.6.2" } }, "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==" }, "acorn-globals": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", - "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", "requires": { "acorn": "^6.0.1", "acorn-walk": "^6.0.1" }, "dependencies": { "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==" + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" } } }, "acorn-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", - "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" }, "after": { "version": "0.8.2", @@ -1582,11 +1240,11 @@ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -1598,16 +1256,9 @@ "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" }, "ajv-keywords": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", - "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==" - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true, - "optional": true + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" }, "ansi-align": { "version": "2.0.0", @@ -1624,12 +1275,6 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -1652,18 +1297,26 @@ } }, "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, "ansi-escapes": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", - "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "type-fest": "^0.5.2" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, "ansi-html": { @@ -1678,26 +1331,46 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } }, "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "apn": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/apn/-/apn-2.2.0.tgz", + "integrity": "sha512-YIypYzPVJA9wzNBLKZ/mq2l1IZX/2FadPvwmSv4ZeR0VH7xdNITQ6Pucgh0Uw6ZZKC+XwheaJ57DFZAhJ0FvPg==", "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" + "debug": "^3.1.0", + "http2": "https://github.com/node-apn/node-http2/archive/apn-2.1.4.tar.gz", + "jsonwebtoken": "^8.1.0", + "node-forge": "^0.7.1", + "verror": "^1.10.0" }, "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { - "remove-trailing-separator": "^1.0.1" + "ms": "^2.1.1" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -1760,6 +1433,18 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "array.prototype.map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", + "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.4" + } + }, "arraybuffer.slice": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", @@ -1774,13 +1459,21 @@ } }, "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "assert": { @@ -1834,9 +1527,9 @@ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" }, "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, "asynckit": { "version": "0.4.0", @@ -1854,9 +1547,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==" }, "babel-code-frame": { "version": "6.26.0", @@ -1866,84 +1559,78 @@ "chalk": "^1.1.3", "esutils": "^2.0.2", "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } } }, "babel-eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz", - "integrity": "sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", "eslint-visitor-keys": "^1.0.0", "resolve": "^1.12.0" - } - }, - "babel-loader": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", - "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", - "requires": { - "find-cache-dir": "^2.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1", - "pify": "^4.0.1" }, "dependencies": { - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" + "path-parse": "^1.0.6" } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "requires": { - "find-up": "^3.0.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, + "babel-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", + "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "requires": { + "find-cache-dir": "^2.1.0", + "loader-utils": "^1.4.0", + "mkdirp": "^0.5.3", + "pify": "^4.0.1", + "schema-utils": "^2.6.5" + } + }, "babel-plugin-dynamic-import-node": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", - "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", "requires": { "object.assign": "^4.1.0" } @@ -1972,6 +1659,11 @@ "pascalcase": "^0.1.1" }, "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", @@ -2013,16 +1705,16 @@ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, "base64id": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2067,9 +1759,27 @@ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } }, "blob": { "version": "0.0.5", @@ -2077,14 +1787,14 @@ "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" }, "bluebird": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==" }, "body-parser": { "version": "1.19.0", @@ -2101,18 +1811,6 @@ "qs": "6.7.0", "raw-body": "2.4.0", "type-is": "~1.6.17" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - } } }, "bootevent": { @@ -2123,11 +1821,6 @@ "chainsaw": "~0.1.0" } }, - "bowser": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.7.0.tgz", - "integrity": "sha512-aIlMvstvu8x+34KEiOHD3AsBgdrzg6sxALYiukOWhFvGMbQI6TRP/iY0LMhUrHs56aD6P1G0Z7h45PUJaa5m9w==" - }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -2149,30 +1842,10 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, "string-width": { @@ -2193,15 +1866,6 @@ "requires": { "ansi-regex": "^3.0.0" } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -2228,9 +1892,9 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "browser-stdout": { "version": "1.3.1", @@ -2279,20 +1943,51 @@ "requires": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "browserify-zlib": { @@ -2304,30 +1999,36 @@ } }, "browserslist": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.0.tgz", - "integrity": "sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz", + "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==", "requires": { - "caniuse-lite": "^1.0.30000989", - "electron-to-chromium": "^1.3.247", - "node-releases": "^1.1.29" + "caniuse-lite": "^1.0.30001111", + "electron-to-chromium": "^1.3.523", + "escalade": "^3.0.2", + "node-releases": "^1.1.60" } }, "bson": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", - "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", + "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" }, "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" }, "dependencies": { + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -2356,14 +2057,14 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" }, "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "cacache": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", "requires": { "bluebird": "^3.5.5", "chownr": "^1.1.1", @@ -2382,19 +2083,6 @@ "y18n": "^4.0.0" }, "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2402,11 +2090,6 @@ "requires": { "yallist": "^3.0.2" } - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" } } }, @@ -2424,6 +2107,13 @@ "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + } } }, "caching-transform": { @@ -2436,30 +2126,6 @@ "make-dir": "^2.0.0", "package-hash": "^3.0.0", "write-file-atomic": "^2.4.2" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } } }, "callsite": { @@ -2474,20 +2140,14 @@ "dev": true }, "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caniuse-lite": { - "version": "1.0.30000999", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000999.tgz", - "integrity": "sha512-1CUyKyecPeksKwXZvYw0tEoaMCo/RwBlXmEtN5vVnabvO0KPd9RQLcaAuR9/1F+KDMv6esmOFWlsXuzDk+8rxg==" + "version": "1.0.30001119", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001119.tgz", + "integrity": "sha512-Hpwa4obv7EGP+TjkCh/wVvbtNJewxmtg4yVJBLFnxo35vbPapBr138bUWENkb5j5L9JZJ9RXLn4OrXRG/cecPQ==" }, "capture-stack-trace": { "version": "1.0.1", @@ -2516,15 +2176,28 @@ } }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "chardet": { @@ -2533,6 +2206,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "check-types": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", @@ -2540,163 +2218,84 @@ "dev": true }, "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" }, "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "is-descriptor": "^0.1.0" } } } }, - "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" - }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" + "source-map": "~0.6.0" }, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, - "clean-css": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", - "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", - "requires": { - "source-map": "~0.6.0" - } - }, "cli-boxes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", @@ -2713,9 +2312,9 @@ } }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, "cliui": { @@ -2733,26 +2332,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -2768,6 +2347,23 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" }, + "codacy-coverage": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/codacy-coverage/-/codacy-coverage-3.4.0.tgz", + "integrity": "sha512-A0ats3/gZtOw76muu++HZ6QrInztWjjLefkLJmmBpjPfyn6nNwNLoApmGmj3F3dfgl2+o6u5GwPnUBkKdfKXTQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.x", + "commander": "^2.x", + "jacoco-parse": "^2.x", + "joi": "^13.x", + "lcov-parse": "^1.x", + "lodash": "^4.17.4", + "log-driver": "^1.x", + "request": "^2.88.0", + "request-promise": "^4.x" + } + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -2791,22 +2387,18 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "component-bind": { "version": "1.0.0", @@ -2814,9 +2406,10 @@ "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" }, "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, "component-inherit": { "version": "0.0.3", @@ -2824,11 +2417,11 @@ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "compressible": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.16.tgz", - "integrity": "sha512-JQfEOdnI7dASwCuSPWIeVYwc/zMsu/+tRhoUvEfXz2gxOA2DNjmG5vhtFdBlhWPPGo+RdT9S3tgc/uH5qgDiiA==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "requires": { - "mime-db": ">= 1.38.0 < 2" + "mime-db": ">= 1.43.0 < 2" } }, "compression": { @@ -2843,6 +2436,13 @@ "on-headers": "~1.0.2", "safe-buffer": "5.1.2", "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + } } }, "concat-map": { @@ -2850,44 +2450,37 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz", + "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", "dev": true, "requires": { - "dot-prop": "^4.1.0", + "dot-prop": "^4.2.1", "graceful-fs": "^4.1.2", "make-dir": "^1.0.0", "unique-string": "^1.0.0", "write-file-atomic": "^2.0.0", "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "requires": { - "date-now": "^0.1.4" - } - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -2896,28 +2489,15 @@ "safe-buffer": "5.1.2" } }, - "content-security-policy-builder": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz", - "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==" - }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "requires": { - "safe-buffer": "~5.1.1" - } - }, "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, "cookie-signature": { "version": "1.0.6", @@ -2949,12 +2529,19 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-js-compat": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.2.1.tgz", - "integrity": "sha512-MwPZle5CF9dEaMYdDeWm73ao/IflDH+FjeJCWEADcEgFSE9TLimFKwJsfmkwzI8eC0Aj0mgvMDjeQjrElkz4/A==", + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", + "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", "requires": { - "browserslist": "^4.6.6", - "semver": "^6.3.0" + "browserslist": "^4.8.5", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" + } } }, "core-util-is": { @@ -2973,39 +2560,22 @@ "nested-error-stacks": "^2.0.0", "pify": "^4.0.1", "safe-buffer": "^5.0.1" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } } }, "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "requires": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "create-error-class": { @@ -3055,29 +2625,16 @@ }, "dependencies": { "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" }, "crypto-random-string": { "version": "1.0.0", @@ -3105,19 +2662,18 @@ } }, "css-selector-tokenizer": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", - "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", "requires": { - "cssesc": "^0.1.0", - "fastparse": "^1.1.1", - "regexpu-core": "^1.0.0" + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" } }, "cssesc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", - "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" }, "cssmin": { "version": "0.4.3", @@ -3125,9 +2681,9 @@ "integrity": "sha1-yRlAd+Dr2s1pHV9ZAVudgZ840BU=" }, "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==" + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" }, "cssstyle": { "version": "0.3.1", @@ -3137,76 +2693,336 @@ "cssom": "0.3.x" } }, + "csv-parse": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.12.0.tgz", + "integrity": "sha512-wPQl3H79vWLPI8cgKFcQXl0NBgYYEqVnT1i6/So7OjMpsI540oD7p93r3w6fDSyPvwkTepG05F69/7AViX2lXg==", + "dev": true + }, + "csv-stringify": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.5.1.tgz", + "integrity": "sha512-HM0/86Ks8OwFbaYLd495tqTs1NhscZL52dC4ieKYumy8+nawQYC0xZ63w1NqLf0M148T2YLYqowoImc1giPn0g==" + }, "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" }, "d3": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", - "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", "requires": { - "assert-plus": "^1.0.0" + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" } }, - "dasherize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", - "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==" - }, - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } + "d3-array": "^1.1.1" } }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", "requires": { - "ms": "2.0.0" + "d3-dispatch": "1", + "d3-selection": "1" } }, - "decamelize": { + "d3-dsv": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } }, - "decode-uri-component": { - "version": "0.2.0", + "d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + }, + "d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "abab": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", + "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==" + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, @@ -3278,7 +3094,13 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" }, "depd": { "version": "1.1.2", @@ -3286,9 +3108,9 @@ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", "requires": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" @@ -3318,13 +3140,15 @@ "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, - "dns-prefetch-control": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz", - "integrity": "sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q==" - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3347,25 +3171,15 @@ "webidl-conversions": "^4.0.2" } }, - "dont-sniff-mimetype": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz", - "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==" - }, "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", "dev": true, "requires": { "is-obj": "^1.0.0" } }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" - }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -3383,6 +3197,15 @@ "stream-shift": "^1.0.0" } }, + "easyxml": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/easyxml/-/easyxml-2.0.1.tgz", + "integrity": "sha1-7qCShCyREwCox4GRPL5b04pQcRw=", + "requires": { + "elementtree": "^0.1.6", + "inflect": "^0.3.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -3406,19 +3229,27 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.1.tgz", - "integrity": "sha512-kS/gEPzZs3Y1rRsbGX4UOSjtP/CeJP0CxSNZHYxGfVM/VgLcv0ZqM7C45YyTj2DI2g7+P9Dd24C+IMIg6D0nYQ==" + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" }, "electron-to-chromium": { - "version": "1.3.275", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.275.tgz", - "integrity": "sha512-/YWtW/VapMnuYA1lNOaa1F4GhR1LBf+CUTp60lzDPEEh0XOzyOAyULyYZVF9vziZ3qSbTqCwmKwsyRXp66STbw==" + "version": "1.3.554", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.554.tgz", + "integrity": "sha512-Vtz2dVH5nMtKK4brahmgScwFS8PBnpA4VObYXtlsqN8ZpT9IFelv0Rpflc1+NIILjGVaj6vEiXQbhrs3Pl8O7g==" + }, + "elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha1-mskb5uUvtuYkTE5UpKw+2K6OKcA=", + "requires": { + "sax": "1.1.4" + } }, "elliptic": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", - "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -3427,18 +3258,24 @@ "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" }, "encodeurl": { "version": "1.0.2", @@ -3446,9 +3283,9 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "requires": { "once": "^1.4.0" } @@ -3466,6 +3303,11 @@ "ws": "~3.3.1" }, "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -3504,6 +3346,11 @@ "yeast": "0.1.2" }, "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -3537,13 +3384,24 @@ } }, "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", "requires": { "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", + "memory-fs": "^0.5.0", "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } } }, "env-cmd": { @@ -3579,22 +3437,57 @@ "requires": { "accepts": "~1.3.7", "escape-html": "~1.0.3" - }, - "dependencies": { - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - } + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, "es6-error": { @@ -3603,6 +3496,16 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "es6-promisify": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.1.tgz", + "integrity": "sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==" + }, + "escalade": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", + "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3613,22 +3516,10 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, "eslint": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.2.1.tgz", - "integrity": "sha512-ES7BzEzr0Q6m5TK9i+/iTpKjclXitOdDK4vT07OqbkBT2/VcN/gO9EL1C4HlK3TAOXYv2ItcmbVR9jO1MR0fJg==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -3638,19 +3529,19 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.2", + "eslint-utils": "^1.4.3", "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.0", + "espree": "^6.1.2", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", - "globals": "^11.7.0", + "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", + "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -3659,7 +3550,7 @@ "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.2", + "optionator": "^0.8.3", "progress": "^2.0.0", "regexpp": "^2.0.1", "semver": "^6.1.2", @@ -3676,40 +3567,55 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "ms": "^2.1.1" } }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", "dev": true, "requires": { - "ms": "^2.1.1" + "type-fest": "^0.8.1" } }, - "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", - "dev": true + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } }, "ms": { "version": "2.1.2", @@ -3717,6 +3623,20 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -3725,15 +3645,6 @@ "requires": { "ansi-regex": "^4.1.0" } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -3751,67 +3662,70 @@ } }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", - "dev": true, + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "requires": { "esrecurse": "^4.1.0", "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + } } }, "eslint-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", - "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.0.0" + "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true }, "espree": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.0.tgz", - "integrity": "sha512-boA7CHRLlVWUSg3iL5Kmlt/xT3Q+sXnKoRYYzj1YeM10A76TEJBbotV5pKbnK42hEUIr121zTv+QLRM5LsCPXQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^7.0.0", - "acorn-jsx": "^5.0.0", + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" }, "dependencies": { "acorn": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.0.0.tgz", - "integrity": "sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==", - "dev": true - }, - "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true } } }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" - }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "esrecurse": { @@ -3820,17 +3734,19 @@ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "requires": { "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + } + } }, "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "etag": { "version": "1.8.1", @@ -3849,12 +3765,19 @@ "split": "0.3", "stream-combiner": "~0.0.4", "through": "~2.3.1" + }, + "dependencies": { + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + } } }, "events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", - "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" }, "evp_bytestokey": { "version": "1.0.3", @@ -3933,11 +3856,6 @@ "homedir-polyfill": "^1.0.1" } }, - "expect-ct": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz", - "integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g==" - }, "expose-loader": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-0.7.5.tgz", @@ -3978,32 +3896,6 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" - }, - "dependencies": { - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - } } }, "express-minify": { @@ -4014,12 +3906,20 @@ "clean-css": "^4.1.7", "on-headers": "^1.0.1", "uglify-js": "^3.0.28" + }, + "dependencies": { + "uglify-js": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.2.tgz", + "integrity": "sha512-GXCYNwqoo0MbLARghYjxVBxDCnU0tLqN7IPLdHHbibCb1NI5zBkU2EPcy/GaVxc0BtTjqyGXJCINe6JMR2Dpow==" + } } }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "extend-shallow": { "version": "3.0.2", @@ -4111,44 +4011,34 @@ } }, "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz", + "integrity": "sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=" }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==" }, - "feature-policy": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz", - "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==" - }, "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" }, "figures": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.0.0.tgz", - "integrity": "sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -4170,8 +4060,26 @@ "requires": { "loader-utils": "^1.0.2", "schema-utils": "^1.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "filesize": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", @@ -4201,24 +4109,28 @@ } }, "find-cache-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", - "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", "requires": { "commondir": "^1.0.1", - "mkdirp": "^0.5.1", - "pkg-dir": "^1.0.0" + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "dependencies": { + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + } } }, "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" + "locate-path": "^3.0.0" } }, "findup-sync": { @@ -4232,6 +4144,23 @@ "resolve-dir": "^1.0.1" } }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + } + } + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", @@ -4241,610 +4170,142 @@ "flatted": "^2.0.0", "rimraf": "2.6.3", "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", - "dev": true - }, - "flot": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/flot/-/flot-0.8.3.tgz", - "integrity": "sha512-xg2otcTJDvS+ERK+my4wxG/ASq90QURXtoM4LhacCq0jQW2jbyjdttbRNqU2cPykrpMvJ6b2uSp6SAgYAzj9tQ==" - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "foreground-child": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", - "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", - "dev": true, - "requires": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" }, "dependencies": { - "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" + "glob": "^7.1.3" } } } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", - "dev": true - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "frameguard": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.1.0.tgz", - "integrity": "sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "optional": true, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "flot": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/flot/-/flot-0.8.3.tgz", + "integrity": "sha512-xg2otcTJDvS+ERK+my4wxG/ASq90QURXtoM4LhacCq0jQW2jbyjdttbRNqU2cPykrpMvJ6b2uSp6SAgYAzj9tQ==" + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, "requires": { - "string-width": "^1.0.2 || 2" + "lru-cache": "^4.0.1", + "which": "^1.2.9" } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "optional": true } } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -4856,6 +4317,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4881,10 +4347,9 @@ } }, "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4895,10 +4360,9 @@ } }, "glob-parent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", - "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", - "dev": true, + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "requires": { "is-glob": "^4.0.1" } @@ -4969,9 +4433,9 @@ } }, "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, "growl": { "version": "1.10.5", @@ -4989,40 +4453,37 @@ "pify": "^4.0.1" }, "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true } } }, - "handlebars": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz", - "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==", - "dev": true, - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - } - }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -5037,6 +4498,13 @@ "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", "requires": { "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } } }, "has-cors": { @@ -5044,15 +4512,10 @@ "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-value": { "version": "1.0.0", @@ -5102,12 +4565,35 @@ } }, "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "hash.js": { @@ -5134,63 +4620,10 @@ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, - "heapdump": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz", - "integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==", - "requires": { - "nan": "^2.13.2" - } - }, "helmet": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.21.1.tgz", - "integrity": "sha512-IC/54Lxvvad2YiUdgLmPlNFKLhNuG++waTF5KPYq/Feo3NNhqMFbcLAlbVkai+9q0+4uxjxGPJ9bNykG+3zZNg==", - "requires": { - "depd": "2.0.0", - "dns-prefetch-control": "0.2.0", - "dont-sniff-mimetype": "1.1.0", - "expect-ct": "0.2.0", - "feature-policy": "0.3.0", - "frameguard": "3.1.0", - "helmet-crossdomain": "0.4.0", - "helmet-csp": "2.9.2", - "hide-powered-by": "1.1.0", - "hpkp": "2.0.0", - "hsts": "2.2.0", - "ienoopen": "1.1.0", - "nocache": "2.1.0", - "referrer-policy": "1.2.0", - "x-xss-protection": "1.3.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, - "helmet-crossdomain": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz", - "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==" - }, - "helmet-csp": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.9.2.tgz", - "integrity": "sha512-Lt5WqNfbNjEJ6ysD4UNpVktSyjEKfU9LVJ1LaFmPfYseg/xPealPfgHhtqdAdjPDopp5zbg/VWCyp4cluMIckw==", - "requires": { - "bowser": "^2.6.1", - "camelize": "1.0.0", - "content-security-policy-builder": "2.1.0", - "dasherize": "2.0.0" - } - }, - "hide-powered-by": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz", - "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.1.0.tgz", + "integrity": "sha512-KWy75fYN8hOG2Rhl8e5B3WhOzb0by1boQum85TiddIE9iu6gV+TXbUjVC17wfej0o/ZUpqB9kxM0NFCZRMzf+Q==" }, "hmac-drbg": { "version": "1.0.1", @@ -5202,6 +4635,12 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==", + "dev": true + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -5217,31 +4656,11 @@ "dev": true }, "hosted-git-info": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", - "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, - "hpkp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", - "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" - }, - "hsts": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz", - "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==", - "requires": { - "depd": "2.0.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, "html-encoding-sniffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", @@ -5251,9 +4670,15 @@ } }, "html-entities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", - "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", + "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, "http-errors": { @@ -5278,6 +4703,10 @@ "sshpk": "^1.7.0" } }, + "http2": { + "version": "https://github.com/node-apn/node-http2/archive/apn-2.1.4.tar.gz", + "integrity": "sha512-ad4u4I88X9AcUgxCRW3RLnbh7xHWQ1f5HbrXa7gEy2x4Xgq+rq+auGx5I+nUDE2YYuqteGIlbxrwQXkIaYTfnQ==" + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -5309,11 +4738,6 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, - "ienoopen": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.1.0.tgz", - "integrity": "sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ==" - }, "iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", @@ -5332,13 +4756,21 @@ "dev": true }, "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } } }, "import-lazy": { @@ -5354,24 +4786,6 @@ "requires": { "pkg-dir": "^3.0.0", "resolve-cwd": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "requires": { - "find-up": "^3.0.0" - } - } } }, "imurmurhash": { @@ -5389,6 +4803,11 @@ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" }, + "inflect": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/inflect/-/inflect-0.3.0.tgz", + "integrity": "sha1-gdDqqja1CmAjC3UQBIs5xBQv5So=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5409,76 +4828,120 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.1.tgz", - "integrity": "sha512-uxNHBeQhRXIoHWTSNYUFhQVrHYFThIt6IVo2fFmSe8aBwdR3/w6b58hJpiL/fMukFkvGzjg+hSxFtwvVmKZmXw==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", + "chalk": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "mute-stream": "0.0.8", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" } }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } } } }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" }, "invariant": { "version": "2.2.4", @@ -5488,15 +4951,10 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" - }, "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, "is-accessor-descriptor": { "version": "0.1.6", @@ -5516,6 +4974,12 @@ } } }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5523,11 +4987,11 @@ "dev": true }, "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "requires": { - "binary-extensions": "^1.0.0" + "binary-extensions": "^2.0.0" } }, "is-buffer": { @@ -5535,6 +4999,12 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, "is-ci": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", @@ -5562,6 +5032,12 @@ } } }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -5590,10 +5066,9 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "is-glob": { "version": "4.0.1", @@ -5613,6 +5088,12 @@ "is-path-inside": "^1.0.0" } }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", + "dev": true + }, "is-npm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", @@ -5639,6 +5120,12 @@ "path-is-inside": "^1.0.1" } }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -5647,28 +5134,53 @@ "isobject": "^3.0.1" } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", "dev": true }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", "dev": true }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } }, "is-typedarray": { "version": "1.0.0", @@ -5686,9 +5198,27 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" }, "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "dev": true, + "requires": { + "punycode": "2.x.x" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } }, "isexe": { "version": "2.0.0", @@ -5705,105 +5235,6 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - } - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, "istanbul-lib-coverage": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", @@ -5845,26 +5276,10 @@ "supports-color": "^6.1.0" }, "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, "supports-color": { @@ -5900,49 +5315,150 @@ "ms": "^2.1.1" } }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==", + "dev": true + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "dev": true, + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, + "jacoco-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jacoco-parse/-/jacoco-parse-2.0.1.tgz", + "integrity": "sha512-YGhIb2iXuQ4/zNh2zgHd6Z6dqlYwLYH1wfsxtTNQ+jnHH9PhhuMwqOFihXymSI41trxok48LdKkSeDIWs28tYg==", + "dev": true, + "requires": { + "mocha": "^5.2.0", + "xml2js": "^0.4.9" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", "dev": true }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, - "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "joi": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", + "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", "dev": true, "requires": { - "handlebars": "^4.1.2" + "hoek": "5.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" } }, "jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" }, "jquery-ui-bundle": { "version": "1.12.1-migrate", @@ -5954,20 +5470,15 @@ "resolved": "https://registry.npmjs.org/jquery.tooltips/-/jquery.tooltips-1.0.0.tgz", "integrity": "sha1-/Ko2Il0IXQ/NY71E4rAGtUGDHHI=" }, - "js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==" - }, "js-storage": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/js-storage/-/js-storage-1.1.0.tgz", "integrity": "sha512-XwkyTB3cjwBSaaKo+edR/n8ZbmX/mj5lJpW/O753NYvMpClQeurucceIvX3HeF4ZTTY2YRPXTVzgPByK4pA7aQ==" }, "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.13.1", @@ -6023,12 +5534,74 @@ "whatwg-url": "^6.4.1", "ws": "^4.0.0", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } } }, "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json-parse-better-errors": { "version": "1.0.2", @@ -6057,11 +5630,18 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", "requires": { - "minimist": "^1.2.0" + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + } } }, "jsonwebtoken": { @@ -6087,9 +5667,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -6102,6 +5682,13 @@ "extsprintf": "1.3.0", "json-schema": "0.2.3", "verror": "1.10.0" + }, + "dependencies": { + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + } } }, "jwa": { @@ -6124,9 +5711,9 @@ } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "latest-version": { "version": "3.1.0", @@ -6137,26 +5724,28 @@ "package-json": "^4.0.0" } }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "requires": { - "invert-kv": "^2.0.0" - } + "lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha1-6w1GtUER68VhrLTECO+TY73I9+A=", + "dev": true }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, + "levenary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", + "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "leven": "^3.1.0" } }, "load-json-file": { @@ -6169,16 +5758,71 @@ "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, "loader-fs-cache": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.2.tgz", - "integrity": "sha512-70IzT/0/L+M20jUlEqZhZyArTU6VKLRTYRDAYN26g4jfzpJqjipLL3/hgYpySqI9PwsVRHHFja0LfEmsx9X2Cw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz", + "integrity": "sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA==", "dev": true, "requires": { "find-cache-dir": "^0.1.1", - "mkdirp": "0.5.1" + "mkdirp": "^0.5.1" + }, + "dependencies": { + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "^1.0.0" + } + } } }, "loader-runner": { @@ -6187,13 +5831,28 @@ "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==" }, "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "requires": { "big.js": "^5.2.2", - "emojis-list": "^2.0.0", + "emojis-list": "^3.0.0", "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + } } }, "locate-path": { @@ -6203,19 +5862,12 @@ "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - } } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.difference": { "version": "4.5.0", @@ -6268,6 +5920,73 @@ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6290,28 +6009,30 @@ "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" + }, + "dependencies": { + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } } }, "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "mamacro": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", - "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==" - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "requires": { - "p-defer": "^1.0.0" + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } } }, "map-cache": { @@ -6332,6 +6053,16 @@ "object-visit": "^1.0.0" } }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -6347,16 +6078,6 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memory-cache": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", @@ -6371,6 +6092,12 @@ "readable-stream": "^2.0.1" } }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -6383,6 +6110,14 @@ "dev": true, "requires": { "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "methods": { @@ -6494,30 +6229,38 @@ "requires": { "bn.js": "^4.0.0", "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", "requires": { - "mime-db": "1.40.0" + "mime-db": "1.43.0" } }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true }, "minimalistic-assert": { "version": "1.0.1", @@ -6538,13 +6281,16 @@ } }, "minimed-connect-to-nightscout": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/minimed-connect-to-nightscout/-/minimed-connect-to-nightscout-1.3.2.tgz", - "integrity": "sha512-1fF1ekFafvzNB9VzoP5T74KVJ32UDi9s4IE8ezNGOJ1XUAAq3qgWYGC4tZgug51sbMvr7c77XQw0bIPl2RpU9Q==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/minimed-connect-to-nightscout/-/minimed-connect-to-nightscout-1.4.0.tgz", + "integrity": "sha512-hxbcncJPiQOoTtjp1VquoxA4ab/Lbn1LiwAymeaEUfyqUxaj9E7w8CHJ/kGbgEu2oYHsqUMvJ1orOlCuUA/g6g==", "requires": { + "axios": "^0.19.2", + "axios-cookiejar-support": "^1.0.0", "common": "^0.2.5", "lodash": "^4.17.15", - "request": "^2.88.0" + "request": "^2.88.0", + "tough-cookie": "^4.0.0" }, "dependencies": { "ajv": { @@ -6612,6 +6358,23 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "axios-cookiejar-support": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.0.tgz", + "integrity": "sha512-9pBlIU5jfrGZTnUQlt8symShviSTOSlOKGtryHx76lJPnKIXDqUT3JDAjJ1ywOQLyfiWrthIt4iJiVP2L2S4jA==", + "requires": { + "is-redirect": "^1.0.0", + "pify": "^5.0.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -6881,6 +6644,29 @@ "is-buffer": "~2.0.3" } }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -7031,6 +6817,11 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -7125,9 +6916,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "log-symbols": { "version": "2.2.0", @@ -7360,10 +7151,15 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" + }, "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "pump": { "version": "3.0.0", @@ -7414,6 +7210,20 @@ "node-uuid": { "version": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", "integrity": "sha1-baWhdmjEs91ZYjvaEc9/pMH2Cm8=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } } } }, @@ -7522,19 +7332,13 @@ } }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" } }, "tunnel-agent": { @@ -7550,6 +7354,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -7750,11 +7559,6 @@ } } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -7770,6 +7574,19 @@ "pumpify": "^1.3.3", "stream-each": "^1.1.0", "through2": "^2.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + } } }, "mixin-deep": { @@ -7792,102 +7609,219 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" }, "dependencies": { "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } }, "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.2.tgz", + "integrity": "sha512-I8FRAcuACNMLQn3lS4qeWLxXqLvGf6r2CaLstDpZmMUUSmvW6Cnm1AuHxgbc7ctZVRcfwspCRbDHymPsi3dkJw==", "dev": true, "requires": { + "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", + "chokidar": "3.4.2", + "debug": "4.1.1", + "diff": "4.0.2", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", "growl": "1.10.5", - "he": "1.1.1", + "he": "1.2.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" + "ms": "2.1.2", + "object.assign": "4.1.0", + "promise.allsettled": "1.0.2", + "serialize-javascript": "4.0.0", + "strip-json-comments": "3.0.1", + "supports-color": "7.1.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.0", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.1" }, "dependencies": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "requires": { - "ms": "2.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" } }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" } } } }, "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" }, "moment-locales-webpack-plugin": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.1.0.tgz", - "integrity": "sha512-0Hn+xdNmQt+XZgsWOlwXJcQ881nURSoDJY1o4hOLiyGaUVZbY475GrvyBXUOMc5mgjvPiQz/KU8ht/IoRnadMg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz", + "integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==", "requires": { "lodash.difference": "^4.5.0" } }, "moment-timezone": { - "version": "0.5.26", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.26.tgz", - "integrity": "sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==", + "version": "0.5.31", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", + "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", "requires": { "moment": ">= 2.9.0" } }, "moment-timezone-data-webpack-plugin": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/moment-timezone-data-webpack-plugin/-/moment-timezone-data-webpack-plugin-1.1.0.tgz", - "integrity": "sha512-szKf9rbRoY9u3WNcwK6D0tdCREk/OZkcF1k163Xc5m7GcqBh28LgNVWisb4sje6/qdG3WUkFD5hJ7/lmKOkBAA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/moment-timezone-data-webpack-plugin/-/moment-timezone-data-webpack-plugin-1.3.0.tgz", + "integrity": "sha512-0V0xnHZpdHLsSerIQ2yNEPBC3uJWfU/zNT3nB0PO+tjmGHuNeUWqNDiw7ZpLo54uER6/OAE75EJ7ThmlwkGuZw==", "requires": { "find-cache-dir": "^3.0.0", "make-dir": "^3.0.0" }, "dependencies": { + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + }, "find-cache-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz", - "integrity": "sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", "requires": { "commondir": "^1.0.1", - "make-dir": "^3.0.0", + "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, @@ -7909,9 +7843,9 @@ } }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "requires": { "semver": "^6.0.0" } @@ -7940,13 +7874,16 @@ } }, "mongodb": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.3.2.tgz", - "integrity": "sha512-fqJt3iywelk4yKu/lfwQg163Bjpo5zDKhXiohycvon4iQHbrfflSAz9AIlRE6496Pm/dQKQK5bMigdVo2s6gBg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.0.tgz", + "integrity": "sha512-/XWWub1mHZVoqEsUppE0GV7u9kanLvHxho6EvBxQbShXTKYF9trhZC2NzbulRGeG7xMJHD8IOWRcdKx5LPjAjQ==", "requires": { - "bson": "^1.1.1", + "bl": "^2.2.0", + "bson": "^1.1.4", + "denque": "^1.4.1", "require_optional": "^1.0.1", - "safe-buffer": "^5.1.2" + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" } }, "mongomock": { @@ -7982,9 +7919,10 @@ "dev": true }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "optional": true }, "nanomatch": { "version": "1.2.13", @@ -8011,9 +7949,9 @@ "dev": true }, "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "neo-async": { "version": "2.6.1", @@ -8031,11 +7969,6 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, - "nocache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", - "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==" - }, "node-cache": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-4.2.1.tgz", @@ -8045,6 +7978,11 @@ "lodash": "^4.17.15" } }, + "node-forge": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" + }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", @@ -8075,39 +8013,134 @@ "vm-browserify": "^1.0.1" }, "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" } } }, "node-releases": { - "version": "1.1.34", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.34.tgz", - "integrity": "sha512-fNn12JTEfniTuCqo0r9jXgl44+KxRH/huV7zM/KAGOKxDKrHr6EbT7SSs4B+DNxyBE2mks28AD+Jw6PkfY5uwA==", - "requires": { - "semver": "^6.3.0" - } + "version": "1.1.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz", + "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==" }, "nodemon": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.1.tgz", - "integrity": "sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg==", + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.4.tgz", + "integrity": "sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ==", "dev": true, "requires": { - "chokidar": "^2.1.5", - "debug": "^3.1.0", + "chokidar": "^2.1.8", + "debug": "^3.2.6", "ignore-by-default": "^1.0.1", "minimatch": "^3.0.4", - "pstree.remy": "^1.1.6", - "semver": "^5.5.0", - "supports-color": "^5.2.0", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.2", "update-notifier": "^2.5.0" }, "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -8117,12 +8150,109 @@ "ms": "^2.1.1" } }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -8137,18 +8267,19 @@ "requires": { "has-flag": "^3.0.0" } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } } } }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -8161,6 +8292,15 @@ "validate-npm-package-license": "^3.0.1" }, "dependencies": { + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -8178,14 +8318,15 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, "requires": { "path-key": "^2.0.0" } }, "nwsapi": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.3.tgz", - "integrity": "sha512-RowAaJGEgYXEZfQ7tvvdtAQUKPyTR6T6wNu0fwlNsGQYr/h3yQc6oI8WnVZh3Y/Sylwc+dtAlvPqfFZjhTyk3A==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" }, "nyc": { "version": "14.1.1", @@ -8220,69 +8361,19 @@ "yargs-parser": "^13.0.0" }, "dependencies": { - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "dev": true, "requires": { - "find-up": "^3.0.0" + "safe-buffer": "~5.1.1" } }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true } } @@ -8336,6 +8427,12 @@ "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", "dev": true }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -8390,9 +8487,9 @@ } }, "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "requires": { "mimic-fn": "^2.1.0" @@ -8404,43 +8501,6 @@ "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", "dev": true }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -8452,65 +8512,21 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "dependencies": { - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - } - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "requires": { "p-try": "^2.0.0" } @@ -8561,16 +8577,16 @@ } }, "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", "requires": { - "cyclist": "~0.2.2", + "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" } @@ -8585,22 +8601,21 @@ } }, "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", "requires": { - "asn1.js": "^4.0.0", + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", "pbkdf2": "^3.0.3", "safe-buffer": "^5.1.1" } }, "parse-duration": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.1.tgz", - "integrity": "sha1-ExFN3JiRwezSgANiRFVN5DZHoiY=" + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.3.tgz", + "integrity": "sha512-hMOZHfUmjxO5hMKn7Eft+ckP2M4nV4yzauLXiw3PndpkASnx5r8pDAMcOAiqxoemqWjMWmz4fOHQM6n6WwETXw==" }, "parse-json": { "version": "4.0.0", @@ -8659,13 +8674,9 @@ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" }, "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", @@ -8700,6 +8711,14 @@ "dev": true, "requires": { "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, "pause-stream": { @@ -8711,9 +8730,9 @@ } }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -8722,16 +8741,41 @@ "sha.js": "^2.4.8" } }, + "pem": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/pem/-/pem-1.14.4.tgz", + "integrity": "sha512-v8lH3NpirgiEmbOqhx0vwQTxwi0ExsiWBGYh0jYNq7K6mQuO4gI6UEFlr6fLAdv9TPXRt6GqiwE37puQdIDS8g==", + "requires": { + "es6-promisify": "^6.0.0", + "md5": "^2.2.1", + "os-tmpdir": "^1.0.1", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" }, "pinkie": { "version": "2.0.4", @@ -8749,12 +8793,11 @@ } }, "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", "requires": { - "find-up": "^1.0.0" + "find-up": "^3.0.0" } }, "pn": { @@ -8777,23 +8820,15 @@ "supports-color": "^5.4.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "supports-color": { "version": "5.5.0", @@ -8856,20 +8891,10 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -8882,13 +8907,26 @@ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" }, + "promise.allsettled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", + "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", + "dev": true, + "requires": { + "array.prototype.map": "^1.0.1", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "iterate-value": "^1.0.0" + } + }, "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" + "ipaddr.js": "1.9.1" } }, "prr": { @@ -8903,14 +8941,14 @@ "dev": true }, "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "pstree.remy": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", - "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, "public-encrypt": { @@ -8924,6 +8962,13 @@ "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "pump": { @@ -8957,19 +9002,19 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.2.4.tgz", + "integrity": "sha1-VACKyXKux0F13vnLpt9/qdORh0A=" }, "pushover-notifications": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-1.2.0.tgz", - "integrity": "sha512-Da2XgHDDq9ZU4idbIx5Y9N4kCsHVgeeHViHK2wxdtdkdP58OvrsKCqpLZnr5nS+I4/PphjTORGSVzwMV2UaPLg==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-1.2.2.tgz", + "integrity": "sha512-+3Xcj+kiMiouZK1Ws8yGBTyl8WMPZZdELgl/iVxYqNwDdlaObBHMhEGPRC6Zb9t0BE27ikOoOqSIO1cKZOtsDA==" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "querystring": { "version": "0.2.0", @@ -9017,13 +9062,6 @@ "http-errors": "1.7.2", "iconv-lite": "0.4.24", "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - } } }, "rc": { @@ -9038,6 +9076,12 @@ "strip-json-comments": "~2.0.1" }, "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -9065,23 +9109,12 @@ "requires": { "find-up": "^3.0.0", "read-pkg": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - } } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9100,39 +9133,37 @@ } }, "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" + "picomatch": "^2.2.1" } }, - "referrer-policy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", - "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==" - }, "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", + "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==" }, "regenerate-unicode-properties": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", - "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", + "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", "requires": { "regenerate": "^1.4.0" } }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, "regenerator-transform": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", - "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "requires": { - "private": "^0.1.6" + "@babel/runtime": "^7.8.4" } }, "regex-not": { @@ -9151,13 +9182,16 @@ "dev": true }, "regexpu-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", - "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", + "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" } }, "registry-auth-token": { @@ -9180,16 +9214,23 @@ } }, "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==" }, "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", + "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", "requires": { "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } } }, "release-zalgo": { @@ -9217,9 +9258,9 @@ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -9228,7 +9269,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -9238,41 +9279,72 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" } } }, + "request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", "requires": { - "lodash": "^4.17.11" + "lodash": "^4.17.19" } }, "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", "requires": { - "request-promise-core": "1.1.2", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" } @@ -9296,11 +9368,6 @@ "semver": "^5.1.0" }, "dependencies": { - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -9308,14 +9375,6 @@ } } }, - "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "requires": { - "path-parse": "^1.0.6" - } - }, "resolve-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", @@ -9353,10 +9412,9 @@ } }, "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" }, "resolve-url": { "version": "0.2.1", @@ -9385,26 +9443,11 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } } }, "ripemd160": { @@ -9417,13 +9460,10 @@ } }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true }, "run-queue": { "version": "1.0.3", @@ -9433,10 +9473,15 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rxjs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", - "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -9460,19 +9505,28 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha1-dLbTPJrh4AFRDxeakRaFiPGu2qk=" }, "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" } }, "semver": { @@ -9530,9 +9584,12 @@ } }, "serialize-javascript": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.8.0.tgz", - "integrity": "sha512-3tHgtF4OzDmeKYj6V9nSyceRS0UJ3C7VqyD2Yj28vC/z2j6jG5FmFGahOKMD9CrglxTm3tETr87jEypaYV8DUg==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } }, "serve-static": { "version": "1.14.1", @@ -9591,9 +9648,9 @@ } }, "share2nightscout-bridge": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/share2nightscout-bridge/-/share2nightscout-bridge-0.2.1.tgz", - "integrity": "sha512-tzMlJHYNysMsPgCwbG0y+hHA6XtYtpZ13YCoXQ6aQqxsn8fln/JUkJU8jCt1LfeMkvY2BAq+d4oek4r0aACQoQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/share2nightscout-bridge/-/share2nightscout-bridge-0.2.4.tgz", + "integrity": "sha512-GLDEMnIETcxUZj/dRgnhsHz33/tFSojZsP5FNHn8mUS/UHmAR6AE6GIbjrUxWO2RQCKqf+E/v8o+ORUfOKKvgA==", "requires": { "request": "^2.88.0" }, @@ -9952,9 +10009,9 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "shiro-trie": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/shiro-trie/-/shiro-trie-0.4.8.tgz", - "integrity": "sha512-CtBcRIbueg6dnSE1XuFf03RX67GdSUFcb5+zhbTL+cESTd8Vjr42YvtHMKlOZvsLt0RmtbuLzslLix8E4nKFwg==" + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/shiro-trie/-/shiro-trie-0.4.9.tgz", + "integrity": "sha512-iNdNhhYLb9BsTOp242Wmk359ZiRZqAcGbMsxP0GQns0K8HMeO5zhOTFdnQE7ewgFGEPnmZaXPVHNU6zyhG+kYQ==" }, "should": { "version": "13.2.3", @@ -10005,15 +10062,16 @@ } }, "should-util": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", - "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", "dev": true }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true }, "simple-statistics": { "version": "0.7.0", @@ -10029,23 +10087,6 @@ "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } } }, "snapdragon": { @@ -10174,9 +10215,9 @@ } }, "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" }, "socket.io-client": { "version": "2.1.1", @@ -10199,6 +10240,11 @@ "to-array": "0.1.4" }, "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -10219,6 +10265,11 @@ "isarray": "2.0.1" }, "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -10226,6 +10277,11 @@ "requires": { "ms": "2.0.0" } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" } } }, @@ -10234,17 +10290,12 @@ "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "requires": { - "atob": "^2.1.1", + "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", @@ -10252,12 +10303,19 @@ } }, "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "source-map-url": { @@ -10265,10 +10323,19 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, "spawn-wrap": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", - "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", "dev": true, "requires": { "foreground-child": "^1.5.6", @@ -10280,9 +10347,9 @@ } }, "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -10290,15 +10357,15 @@ } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -10401,6 +10468,13 @@ "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", "requires": { "duplexer": "~0.1.1" + }, + "dependencies": { + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + } } }, "stream-each": { @@ -10425,38 +10499,55 @@ } }, "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "string-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz", - "integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^5.2.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, "requires": { "ansi-regex": "^4.1.0" } } } }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -10482,12 +10573,13 @@ "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "style-loader": { @@ -10497,6 +10589,18 @@ "requires": { "loader-utils": "^1.1.0", "schema-utils": "^1.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } } }, "superagent": { @@ -10533,9 +10637,9 @@ "dev": true }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } @@ -10550,28 +10654,23 @@ "superagent": "^3.8.3" } }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, "swagger-ui-dist": { - "version": "3.23.11", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.23.11.tgz", - "integrity": "sha512-ipENHHH/sqpngTpHXUwg55eAOZ7b2UVayUwwuWPA6nQSPhjBVXX4zOPpNKUwQIFOl3oIwVvZF7mqoxH7pMgVzA==" + "version": "3.32.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.32.5.tgz", + "integrity": "sha512-3SKHv8UVqsKKknivtACHbFDGcn297jkoZN2h6zAZ7b2yoaJNMaRadQpC3qFw3GobZTGzqHCgHph4ZH9NkaCjrQ==" }, "swagger-ui-express": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.2.tgz", - "integrity": "sha512-bVT16qj6WdNlEKFkSLOoTeGuqEm2lfOFRq6mVHAx+viA/ikORE+n4CS3WpVcYmQzM4HE6+DUFgAWcMRBJNpjcw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz", + "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==", "requires": { "swagger-ui-dist": "^3.18.1" } }, "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "table": { "version": "5.4.6", @@ -10583,58 +10682,6 @@ "lodash": "^4.17.14", "slice-ansi": "^2.1.0", "string-width": "^3.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } } }, "tapable": { @@ -10659,73 +10706,50 @@ "commander": "^2.19.0", "source-map": "~0.6.1", "source-map-support": "~0.5.10" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "terser-webpack-plugin": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", - "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^1.7.0", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", "worker-farm": "^1.7.0" }, "dependencies": { - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", "requires": { - "find-up": "^3.0.0" + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" } }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "terser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.2.0.tgz", - "integrity": "sha512-6lPt7lZdZ/13icQJp8XasFOwZjFJkxFFIb/N1fhYEQNoNI3Ilo3KABZ9OocZvZoB39r6SiIk/0+v/bt8nZoSeA==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", @@ -10734,32 +10758,16 @@ } } }, - "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", - "dev": true, - "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^2.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" } }, "text-table": { @@ -10862,6 +10870,23 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "dev": true, + "requires": { + "hoek": "6.x.x" + }, + "dependencies": { + "hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==", + "dev": true + } + } + }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -10889,6 +10914,13 @@ "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } } }, "tr46": { @@ -10897,6 +10929,13 @@ "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", "requires": { "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } } }, "traverse": { @@ -10904,11 +10943,6 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" - }, "tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -10916,9 +10950,9 @@ "dev": true }, "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" }, "tty-browserify": { "version": "0.0.0", @@ -10947,9 +10981,9 @@ } }, "type-fest": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", - "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, "type-is": { @@ -10966,24 +11000,15 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, - "uglify-js": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.6.tgz", - "integrity": "sha512-YDKRX8F0Y+Jr7LhoVk0n4G7ltR3Y7qFAj+DtVBthlOgCcIj1hyMigCfousVfn9HKmvJ+qiFlLDwaHx44/e5ZKw==", - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - } - }, "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" }, "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", "dev": true, "requires": { "debug": "^2.2.0" @@ -11004,14 +11029,14 @@ } }, "unicode-match-property-value-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", - "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==" }, "unicode-property-aliases-ecmascript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", - "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==" }, "union-value": { "version": "1.0.1", @@ -11102,9 +11127,9 @@ "dev": true }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" }, "update-notifier": { "version": "2.5.0", @@ -11122,37 +11147,6 @@ "latest-version": "^3.0.0", "semver-diff": "^2.0.0", "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "uri-js": { @@ -11161,6 +11155,13 @@ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "requires": { "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } } }, "urix": { @@ -11217,15 +11218,14 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", - "dev": true + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==" }, "validate-npm-package-license": { "version": "3.0.4", @@ -11252,27 +11252,199 @@ "extsprintf": "^1.2.0" } }, - "vm-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", - "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==" - }, "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", "requires": { - "browser-process-hrtime": "^0.1.2" + "browser-process-hrtime": "^1.0.0" } }, "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", + "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "requires": { - "chokidar": "^2.0.2", + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.0" + } + }, + "watchpack-chokidar2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", + "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", + "optional": true, + "requires": { + "chokidar": "^2.1.8" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, "webidl-conversions": { @@ -11281,75 +11453,60 @@ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" }, "webpack": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.0.tgz", - "integrity": "sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g==", - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/wasm-edit": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.1.tgz", + "integrity": "sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ==", + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.1.0", + "enhanced-resolve": "^4.3.0", "eslint-scope": "^4.0.3", "json-parse-better-errors": "^1.0.2", "loader-runner": "^2.4.0", "loader-utils": "^1.2.3", "memory-fs": "^0.4.1", "micromatch": "^3.1.10", - "mkdirp": "^0.5.1", + "mkdirp": "^0.5.3", "neo-async": "^2.6.1", "node-libs-browser": "^2.2.1", "schema-utils": "^1.0.0", "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.1", - "watchpack": "^1.6.0", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", "webpack-sources": "^1.4.1" }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==" - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==" + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" } } } }, "webpack-bundle-analyzer": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.4.1.tgz", - "integrity": "sha512-Bs8D/1zF+17lhqj2OYmzi7HEVYqEVxu7lCO9Ff8BwajenOU0vAwEoV8e4ICCPNZAcqR1PCR/7o2SkW+cnCmF0A==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.8.0.tgz", + "integrity": "sha512-PODQhAYVEourCcOuU+NiYI7WdR8QyELZGgPvB1y2tjbUpbmcQOt5Q7jEK+ttd5se0KSBKD9SXHCEozS++Wllmw==", "dev": true, "requires": { - "acorn": "^6.0.7", - "acorn-walk": "^6.1.1", + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1", "bfj": "^6.1.1", "chalk": "^2.4.1", "commander": "^2.18.0", @@ -11364,39 +11521,16 @@ }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true }, "ws": { "version": "6.2.1", @@ -11410,91 +11544,27 @@ } }, "webpack-cli": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.9.tgz", - "integrity": "sha512-xwnSxWl8nZtBl/AFJCOn9pG7s5CYUYdZxmmukv+fAHLcBIHM36dImfpQg3WfShZXeArkWlf6QRw24Klcsv8a5A==", - "requires": { - "chalk": "2.4.2", - "cross-spawn": "6.0.5", - "enhanced-resolve": "4.1.0", - "findup-sync": "3.0.0", - "global-modules": "2.0.0", - "import-local": "2.0.0", - "interpret": "1.2.0", - "loader-utils": "1.2.3", - "supports-color": "6.1.0", - "v8-compile-cache": "2.0.3", - "yargs": "13.2.4" + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz", + "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==", + "requires": { + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.1.1", + "findup-sync": "^3.0.0", + "global-modules": "^2.0.0", + "import-local": "^2.0.0", + "interpret": "^1.4.0", + "loader-utils": "^1.4.0", + "supports-color": "^6.1.0", + "v8-compile-cache": "^2.1.1", + "yargs": "^13.3.2" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "find-up": { + "has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "supports-color": { "version": "6.1.0", @@ -11503,29 +11573,6 @@ "requires": { "has-flag": "^3.0.0" } - }, - "v8-compile-cache": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", - "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==" - }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } } } }, @@ -11562,6 +11609,14 @@ "requires": { "ansi-colors": "^3.0.0", "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + } } }, "webpack-sources": { @@ -11571,6 +11626,13 @@ "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "whatwg-encoding": { @@ -11579,16 +11641,6 @@ "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", "requires": { "iconv-lite": "0.4.24" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } } }, "whatwg-mimetype": { @@ -11619,6 +11671,42 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "widest-line": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", @@ -11634,12 +11722,6 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -11661,10 +11743,10 @@ } } }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, "worker-farm": { "version": "1.7.0", @@ -11674,6 +11756,12 @@ "errno": "~0.1.7" } }, + "workerpool": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", + "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", + "dev": true + }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", @@ -11689,34 +11777,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -11761,11 +11821,6 @@ "safe-buffer": "~5.1.0" } }, - "x-xss-protection": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz", - "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==" - }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", @@ -11777,6 +11832,22 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true + }, "xmlhttprequest-ssl": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", @@ -11793,16 +11864,14 @@ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", - "dev": true, + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "requires": { "cliui": "^5.0.0", "find-up": "^3.0.0", @@ -11813,74 +11882,62 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz", + "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "decamelize": "^1.2.0", + "flat": "^4.1.0", + "is-plain-obj": "^1.1.0", + "yargs": "^14.2.3" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" } }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - } - } - }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", diff --git a/package.json b/package.json index 39f4d2e2bfa..ec94894a060 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.12.5", + "version": "14.0.6", "description": "Nightscout acts as a web-based CGM (Continuous Glucose Montinor) to allow multiple caregivers to remotely view a patients glucose data in realtime.", "license": "AGPL-3.0", "author": "Nightscout Team", @@ -27,17 +27,21 @@ }, "scripts": { "start": "node server.js", - "test": "env-cmd ./test.env mocha --exit tests/*.test.js", + "test": "env-cmd ./my.test.env mocha --exit tests/*.test.js", + "test-single": "env-cmd ./my.test.env mocha --exit tests/$TEST.test.js", + "test-ci": "env-cmd ./ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --exit tests/*.test.js", "env": "env", "postinstall": "webpack --mode production --config webpack.config.js && npm run-script update-buster", "bundle": "webpack --mode production --config webpack.config.js && npm run-script update-buster", "bundle-dev": "webpack --mode development --config webpack.config.js && npm run-script update-buster", "bundle-analyzer": "webpack --mode development --config webpack.config.js --profile --json > stats.json && webpack-bundle-analyzer stats.json", "update-buster": "node bin/generateCacheBuster.js >tmp/cacheBusterToken", - "coverage": "env-cmd ./test.env nyc mocha --exit tests/*.test.js", + "coverage": "cat ./coverage/lcov.info | env-cmd ./ci.test.env codacy-coverage", "dev": "env-cmd ./my.env nodemon server.js 0.0.0.0", - "prod": "env-cmd ./my.prod.env node server.js 0.0.0.0" + "prod": "env-cmd ./my.prod.env node server.js 0.0.0.0", + "lint": "eslint lib" }, + "main": "server.js", "config": { "blanket": { "pattern": [ @@ -57,18 +61,22 @@ "npm": "^6.4.1" }, "dependencies": { - "@babel/core": "^7.5.5", - "@babel/preset-env": "^7.5.5", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "apn": "^2.2.0", "async": "^0.9.2", - "babel-loader": "^8.0.6", + "babel-loader": "^8.1.0", + "base64url": "^3.0.1", "body-parser": "^1.19.0", "bootevent": "0.0.1", "braces": "^3.0.2", "compression": "^1.7.4", "css-loader": "^1.0.1", "cssmin": "^0.4.3", - "d3": "^3.5.17", - "ejs": "^2.6.2", + "csv-stringify": "^5.5.1", + "d3": "^5.16.0", + "easyxml": "^2.0.1", + "ejs": "^2.7.4", "errorhandler": "^1.5.1", "event-stream": "3.3.4", "expose-loader": "^0.7.5", @@ -76,58 +84,61 @@ "express-minify": "^1.0.0", "file-loader": "^3.0.1", "flot": "^0.8.3", - "heapdump": "^0.3.15", - "helmet": "^3.20.0", - "jquery": "^3.4.1", + "helmet": "^4.0.0", + "jquery": "^3.5.1", "jquery-ui-bundle": "^1.12.1-migrate", "jquery.tooltips": "^1.0.0", "js-storage": "^1.1.0", "jsdom": "~11.11.0", "jsonwebtoken": "^8.5.1", - "lodash": "^4.17.15", + "lodash": "^4.17.20", "memory-cache": "^0.2.0", - "mime": "^2.4.4", - "minimed-connect-to-nightscout": "^1.3.2", - "moment": "^2.24.0", - "moment-locales-webpack-plugin": "^1.1.0", - "moment-timezone": "^0.5.26", - "moment-timezone-data-webpack-plugin": "^1.1.0", - "mongodb": "^3.3.0", + "mime": "^2.4.6", + "minimed-connect-to-nightscout": "^1.4.0", + "moment": "^2.27.0", + "moment-locales-webpack-plugin": "^1.2.0", + "moment-timezone": "^0.5.31", + "moment-timezone-data-webpack-plugin": "^1.3.0", + "mongodb": "^3.6.0", "mongomock": "^0.1.2", "node-cache": "^4.2.1", - "parse-duration": "^0.1.1", - "pushover-notifications": "^1.2.0", + "parse-duration": "^0.1.3", + "pem": "^1.14.4", + "pushover-notifications": "^1.2.2", "random-token": "0.0.8", - "request": "^2.88.0", + "request": "^2.88.2", "semver": "^6.3.0", - "share2nightscout-bridge": "^0.2.1", - "shiro-trie": "^0.4.8", + "share2nightscout-bridge": "^0.2.4", + "shiro-trie": "^0.4.9", "simple-statistics": "^0.7.0", "socket.io": "~2.1.1", "style-loader": "^0.23.1", - "swagger-ui-dist": "^3.23.5", - "swagger-ui-express": "^4.1.2", + "swagger-ui-dist": "^3.32.1", + "swagger-ui-express": "^4.1.4", "terser": "^3.17.0", "traverse": "^0.6.6", - "webpack": "^4.39.2", - "webpack-cli": "^3.3.7" + "uuid": "^3.4.0", + "webpack": "^4.44.1", + "webpack-cli": "^3.3.12" }, "devDependencies": { - "babel-eslint": "^10.0.3", + "babel-eslint": "^10.1.0", "benv": "^3.3.0", + "codacy-coverage": "^3.4.0", + "csv-parse": "^4.12.0", "env-cmd": "^8.0.2", - "eslint": "^6.2.1", + "eslint": "^6.8.0", "eslint-loader": "^2.2.1", - "istanbul": "^0.4.5", - "mocha": "^5.2.0", - "nodemon": "^1.19.1", + "mocha": "^8.1.1", + "nodemon": "^1.19.4", "nyc": "^14.1.1", "should": "^13.2.3", "supertest": "^3.4.2", - "terser-webpack-plugin": "^1.4.1", - "webpack-bundle-analyzer": "^3.4.1", + "terser-webpack-plugin": "^1.4.5", + "webpack-bundle-analyzer": "^3.8.0", "webpack-dev-middleware": "^3.7.2", - "webpack-hot-middleware": "^2.25.0" + "webpack-hot-middleware": "^2.25.0", + "xml2js": "^0.4.23" }, "browserslist": "> 0.25%, not dead" } diff --git a/server.js b/server.js index df99724747a..f4350bbbe00 100644 --- a/server.js +++ b/server.js @@ -54,6 +54,12 @@ require('./lib/server/bootevent')(env, language).boot(function booted (ctx) { return; } + ctx.bus.on('teardown', function serverTeardown () { + server.close(); + clearTimeout(sendStartupAllClearTimer); + ctx.store.client.close(); + }); + /////////////////////////////////////////////////// // setup socket io for data and message transmission /////////////////////////////////////////////////// @@ -68,7 +74,7 @@ require('./lib/server/bootevent')(env, language).boot(function booted (ctx) { }); //after startup if there are no alarms send all clear - setTimeout(function sendStartupAllClear () { + let sendStartupAllClearTimer = setTimeout(function sendStartupAllClear () { var alarm = ctx.notifications.findHighestAlarm(); if (!alarm) { ctx.bus.emit('notification', { diff --git a/static/css/drawer.css b/static/css/drawer.css index f9e6147fe2d..3e5e72542a4 100644 --- a/static/css/drawer.css +++ b/static/css/drawer.css @@ -207,34 +207,49 @@ h1, legend, } #toolbar { - background: url(/images/logo2.png) no-repeat 3px 3px #333; - border-bottom: 1px solid #999; - top: 0; - margin: 0; - height: 44px; text-shadow: 0 0 5px black; + display: flex; + height: 44px; + margin: 0 0 10px; + padding: 0 15px 0 40px; + position: relative; + align-items: center; + background: url(/images/logo2.png) no-repeat 3px center #333; + border-bottom: 1px solid #999; + justify-content: space-between; } #toolbar .customTitle { color: #ccc; font-size: 16px; - margin-top: 0; - margin-left: 42px; - padding-top: 10px; - padding-right: 150px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } + +#toolbar .button-close { + color: #404040; + text-align: center; + text-shadow: none; + height: 20px; + width: 20px; + padding: 5px; + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + background: grey; + border: 2px solid #404040; + border-radius: 5px; +} + +#toolbar .button-close + #buttonbar { + margin-right: 40px; +} + #buttonbar { - margin-right: 50px; - padding-right: 15px; - height: 44px; opacity: 0.75; vertical-align: middle; - position: absolute; - right: 0; - z-index: 500; } #buttonbar a, @@ -244,14 +259,17 @@ h1, legend, height: 44px; line-height: 44px; } + #buttonbar .selected { color: red; } + #buttonbar a { - float: left; + float: right; text-decoration: none; width: 34px; } + #buttonbar i { padding-left: 12px; } diff --git a/static/css/main.css b/static/css/main.css index 78cf7ae4c97..d6bee072d16 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -9,7 +9,21 @@ font-style: normal; } - [class^="icon-"]:before, [class*=" icon-"]:before { +/* + Icon font for additional plugin icons. + Please read assets/fonts/README.md about update process +*/ +@font-face { + font-family: 'pluginicons'; + /* Plugin Icons font files content (from WOFF and SVG icon files, base64 encoded) */ + src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAWAAAsAAAAABTQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgDxIE8mNtYXAAAAFoAAAAVAAAAFQXVdKJZ2FzcAAAAbwAAAAIAAAACAAAABBnbHlmAAABxAAAAUgAAAFIFA4eR2hlYWQAAAMMAAAANgAAADYXVLrVaGhlYQAAA0QAAAAkAAAAJAdQA8ZobXR4AAADaAAAABQAAAAUCY4AAGxvY2EAAAN8AAAADAAAAAwAKAC4bWF4cAAAA4gAAAAgAAAAIAAJAFxuYW1lAAADqAAAAbYAAAG2DBt7mXBvc3QAAAVgAAAAIAAAACAAAwAAAAMCxwGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA6QEDwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADgAAAAKAAgAAgACAAEAIOkB//3//wAAAAAAIOkB//3//wAB/+MXAwADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAwAA/8ADjgPAABsAOgBZAAABIgcOAQcGFRQXHgEXFjMyNz4BNzY1NCcuAScmARUUFx4BFxYzMjc+ATc2PQEUBw4BBwYjIicuAScmNREVFBceARcWMzI3PgE3Nj0BFAcOAQcGIyInLgEnJjUBx15TU3skJCQke1NTXl5TU3wjJCQjfFNT/dskJHtTU15eU1N8IyQkI3xTU15eU1N7JCQkJHtTU15eU1N8IyQkI3xTU15eU1N7JCQDwBISPikpMC8pKj0SEhISPSopLzApKT4SEv6rqy8qKT4SEhISPikqL6svKik+EhISEj4pKi/+46owKSk+EhISEj4pKTCqLykqPhESEhE+KikvAAAAAAEAAAABAABgRbaTXw889QALBAAAAAAA2lO7LAAAAADaU7ssAAD/wAOOA8AAAAAIAAIAAAAAAAAAAQAAA8D/wAAABAAAAAAAA44AAQAAAAAAAAAAAAAAAAAAAAUEAAAAAAAAAAAAAAACAAAAA44AAAAAAAAACgAUAB4ApAABAAAABQBaAAMAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEACwAAAAEAAAAAAAIABwCEAAEAAAAAAAMACwBCAAEAAAAAAAQACwCZAAEAAAAAAAUACwAhAAEAAAAAAAYACwBjAAEAAAAAAAoAGgC6AAMAAQQJAAEAFgALAAMAAQQJAAIADgCLAAMAAQQJAAMAFgBNAAMAAQQJAAQAFgCkAAMAAQQJAAUAFgAsAAMAAQQJAAYAFgBuAAMAAQQJAAoANADUcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzVmVyc2lvbiAxLjAAVgBlAHIAcwBpAG8AbgAgADEALgAwcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzUmVndWxhcgBSAGUAZwB1AGwAYQBycGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzRm9udCBnZW5lcmF0ZWQgYnkgSWNvTW9vbi4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('woff'), + url(data:application/font-svg;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIiA+DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+DQo8bWV0YWRhdGE+R2VuZXJhdGVkIGJ5IEljb01vb248L21ldGFkYXRhPg0KPGRlZnM+DQo8Zm9udCBpZD0icGx1Z2luaWNvbnMiIGhvcml6LWFkdi14PSIxMDI0Ij4NCjxmb250LWZhY2UgdW5pdHMtcGVyLWVtPSIxMDI0IiBhc2NlbnQ9Ijk2MCIgZGVzY2VudD0iLTY0IiAvPg0KPG1pc3NpbmctZ2x5cGggaG9yaXotYWR2LXg9IjEwMjQiIC8+DQo8Z2x5cGggdW5pY29kZT0iJiN4MjA7IiBob3Jpei1hZHYteD0iNTEyIiBkPSIiIC8+DQo8Z2x5cGggdW5pY29kZT0iJiN4ZTkwMTsiIGdseXBoLW5hbWU9ImRhdGFiYXNlIiBob3Jpei1hZHYteD0iOTEwIiBkPSJNNDU1LjExMSA5NjBjLTI1MS40NDkgMC00NTUuMTExLTEwMS44MzEtNDU1LjExMS0yMjcuNTU2czIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTYgNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1Ni0yMDMuNjYyIDIyNy41NTYtNDU1LjExMSAyMjcuNTU2ek0wIDYxOC42Njd2LTE3MC42NjdjMC0xMjUuNzI0IDIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTZzNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1NnYxNzAuNjY3YzAtMTI1LjcyNC0yMDMuNjYyLTIyNy41NTYtNDU1LjExMS0yMjcuNTU2cy00NTUuMTExIDEwMS44MzEtNDU1LjExMSAyMjcuNTU2ek0wIDMzNC4yMjJ2LTE3MC42NjdjMC0xMjUuNzI0IDIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTZzNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1NnYxNzAuNjY3YzAtMTI1LjcyNC0yMDMuNjYyLTIyNy41NTYtNDU1LjExMS0yMjcuNTU2cy00NTUuMTExIDEwMS44MzEtNDU1LjExMSAyMjcuNTU2eiIgLz4NCjwvZm9udD48L2RlZnM+PC9zdmc+) format('svg'); + font-weight: normal; + font-style: normal; +} + + [class^="icon-"]:before, [class*=" icon-"]:before, + [class^="plugicon-"]:before, [class*=" plugicon-"]:before { font-family: "nsicons"; font-style: normal; font-weight: normal; @@ -43,6 +57,10 @@ /* Uncomment for 3D effect */ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } + +[class^="plugicon-"]:before, [class*=" plugicon-"]:before { + font-family: "pluginicons"; +} .icon-volume:before { content: '\e800'; } .icon-plus:before { content: '\e801'; } @@ -65,6 +83,9 @@ .icon-chart-line:before { content: '\f201'; } .icon-hourglass:before { content: '\f254'; } +/* Plugin Icons id-s (copy from generated icon style.css) */ +.plugicon-database:before { content: "\e901"; } + html, body { margin: 0; padding: 0; @@ -427,18 +448,36 @@ a, a:visited, a:link { display: none; } -#authorizationstatus a { +.toolbar-title { + text-align: left; + padding: 0 10px; +} + +.toolbar-title h2 { + margin: 0; +} + +.page-content { + padding: 10px; +} + +.authentication-status { + padding: 10px; + border-top: 1px solid #bdbdbd; +} + +.authentication-status a { color: #2196f3; text-decoration: underline; } -#authorizationstatus .small { +.authentication-status .small { font-size: 12px; } -@media (max-width: 800px) { +@media (max-width: 1000px) { .bgStatus { - width: 300px; + width: 450px; } .bgButton { @@ -498,6 +537,11 @@ a, a:visited, a:link { } @media (max-width: 750px) { + .x.axis { + font-size: 2.5vmin !important; + } + + .bgStatus { width: 50%; padding: 0 0 20px 0; @@ -639,6 +683,10 @@ a, a:visited, a:link { #chartContainer { font-size: 14px; } + .y.axis { + font-size: 2.5vmin !important; + } + } @media (max-height: 600px) { @@ -651,6 +699,22 @@ a, a:visited, a:link { #container #toolbar { float: right; height: auto; + background: none; + border-bottom: none; + margin: 0px; + } + + #buttonbar { + margin-right: 0px; + padding-right: 15px; + margin-top: 15px; + height: 44px; + opacity: 0.75; + vertical-align: middle; + position: absolute; + right: 0; + z-index: 500; + width: 400px; } #toolbar .customTitle { diff --git a/static/css/report.css b/static/css/report.css index afe046f00d9..6994eb96b6c 100644 --- a/static/css/report.css +++ b/static/css/report.css @@ -24,26 +24,32 @@ body { #tabnav { text-align: left; - margin: 1em 0 1em 0; + margin: 1em 0 0; font: bold 11pt verdana, arial, sans-serif; border-bottom: 1px solid #6c6; list-style-type: none; - padding: 3px 10px 3px 10px; + padding: 0 0 0 15px; } -#tabnav li{ - display: inline; - padding: 3px 4px; - border: 1px solid #6c6; +#tabnav li { + display: inline-block; + padding: 10px 15px; + border-top: 1px solid #6c6; + border-right: 1px solid #6c6; + border-bottom: none; background-color: #cfc; color: #666; - margin-right: 0; + margin: 0 0 -1px 0; text-decoration: none; - border-bottom: none; +} + +#tabnav li:first-child { + border-left: 1px solid #6c6; } #tabnav .selected { background: #fff; + border-bottom: 1px solid #fff; } #tabnav li:hover { @@ -66,4 +72,41 @@ body { float: right; min-width: 150px; max-width: 400px; +} + +main { + padding: 15px; +} + +input[type=date], +input[type=text], +input[type=number], +select { + font: 13px verdana, arial, sans-serif; +} + +label { + display: inline-flex; + justify-content: flex-start; + align-items: center; + margin-right: 7px; +} + +#rp_to { + margin-right: 10px; +} + +.presetdates { + display: inline-block; + margin-right: 8px; +} + +#rp_show { + background-color: #cfc; + border: 1px solid #6c6; + color: #666; + padding: 10px; + font-weight: bold; + font-size: 12pt; + text-transform: uppercase; } \ No newline at end of file diff --git a/static/css/translations.css b/static/css/translations.css index 8408cf29f45..b1fae30e253 100644 --- a/static/css/translations.css +++ b/static/css/translations.css @@ -1,5 +1,16 @@ -td,th { +body { + color: black; + font-family: 'Open Sans', Helvetica, Arial, sans-serif; + background-color: white; +} + +th, +td { vertical-align: top; text-align: left; border: 1px solid } + +#translations { + padding: 15px; +} diff --git a/static/images/browserstack-logo.png b/static/images/browserstack-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..765fea8fe1f615f2fafd8448bc4e630f408278bc GIT binary patch literal 14657 zcmdVAWmFv7)-FsScyM=@;BG;Jy9DT*D8n0BmoR@e zq6$qS& zd2n*@To~;Wpau#;in{SuCzcRwLr=95iger*1Q-2F8KnkOe-36vwyGB<0q)kOl{way zw^cK}>+g-&Wraj1$+P493auQul8QOO>%OOC6`h zZ=ulL$Zmr7If)^}+C=uovA5N|M;b#(CJBT|T`j6SZSBLRchIJSjk-xDy^3kk6Pebx z0Daa+n0t@(9M#m->x4+|_|SW?F6Pigx}b3UUK}B2h9dW&??R1p?cA{92N+vh!K3@A zc$(bZ^GC$nH#o#USV7y}VaHm*EKd7mG`Ssf+o%=Fg{UNC`V4G-XhijH2Y@S2%vMjc zU4vwOvwWm*onf`BL1aDGm&t@^Z94`XXHL)<1s}}^ORv>oG7L^p$^mh&$OC5r4%@@? z-SdM`QG&v$v#1rXy)x&_E1Zskn}o;ysZqLgrwdc3`lT77>oimh#KBrwM4vPh{(R3 z#Ajkm#3K5=O!{#0-l!x1F>raW$H3%|!^CkB@SD8jUhfDkBIqgMbFyd@aacw2vT&n= zATp75xmmEZ{I@gjb|ETJi-iy};da$7sT?tmL_TK8v=5wZ~}W<3*-#rSB|lE$xY0D?Ed{y|`Vt z#eXDx{@}qsO00^Q49zX@jwqqbU+yC%ks?wk3LWAx@;wxRzfdOjnD?&OB}F%~LV#qz zK|q85RVTVGs%YF?3QDd8h!{esZ*MyKwl(;c)Yad$F`$mU6ne*h9%slAu0^ zP2}@8=fHE?bDMJp;Q(DBt6Xn6?`X2P}T9Ksexm(F0Jj@|#iGglkS#?nXw` z_X?$*%3*gpg&Ap;>;|Qq;A7Zos-yuK!?^O13dpO<%f~ByM>NMH$BujQ`-ppo`>Xpf zxO@m)FH)~!h++sY-+JGQcX3$L*t`sxm{!5^!PPibSS~DVjG=7WCW}Uk7MbSl%zdl{ ztPcA93}4x`jMRaSJ;6rZjFQX?0J{lX)*5zMMtb%Z9S?O5Reu^gHCrhXDVs#{RAtWM ziAeLls=*&cRfj+1D`3j|D~u~0d6q047cYk<7INKcs%vCwmJOdqMY_ma$wzYMwu_7F zid(A}uS4-VhG)!kw4#=hu+W*24G&Yt03(%YfR^dzspllGEU(FoVV%~oQO0GfJUt4( zF6gbI9ZhRu4hODcTduv!xw$sZg**R?&vdbLKsrdxd&9&*spL8fgVMrg>XoYYs)djv z^poofjhcqk4=YXMPX&G+exq6a8=N!2N&P&%E=Rzl_gCDUDOAO^B)Kn?&nQF6JlZhm9?db&V}CPUywl8Q)x=SKKU&FB5Ok3eFT>`)35 z)gHPURvU)LTf+w|pd_FxU?fU2DlqCkN-L^{6d}7dFFLys*&(1`$U^=};U?Qv3RZj} zjV%&CMLUgu*nfnBNEs;CnTV^9| zK=r^s9kmgwka_|9G?|$d6oV0j@mWK+S$`>Vr>?oKg{F%hjmAtXx5o6UJaubv3ynG) z4VqeA1-oeQ^IT28h0>Mr{(j}u%tg^f;Kg!jfc8XkX-W%K%MD};fiWy9Og4CmiPrS- z9nwqIZqM~sy92wT2W?84MtOZLMfuCGO<$g3(o9{co`Cv-TBj) zY`*@mwlcRmiB;3Ut}d@ZH+N|#V!dq}ic4#bn15_Pnn2jHwT|!-$v?5*=GO3WfI9n-lU2u~>(Z72? zrGO)=AiI->&l%-pkS{!bzHfRyw>%AAj4;xb++gE+eRmD(fkCHr*<@c5vS7Czd5{wo zmqMRb+g>+rKecAu!TMm*Q@Ze(Z~1t1*|OtIWI{SGX$X(`gUpI`%h&7bo1>s__TNgD z;hGn$=+CuPb{2<^>2y?Ho-Q9O@He?JU2FG|);_K2<@AurR;DUGn_oM*)Ck@b;Gd+L z$abXh9->_~Kaw1TmAc%GV2((n9`Kg#)g1A0u70q*=t{ZHp>Sn5yi-j-_Bj=S{$Ao< z{eI|BFUxP!se(s(^#rHqJO5 zrQku7XhE{F)K970VQ7MX$X$X!brH9N$$j|X@Tj0BZdbjbT`6M9@*WGz!EeZyl-b=P zg@mLdgoI?}dzk?NNToAo+K_0D*ORRO0B+7fwrJWa-z5SzBlx9Pr@6%I`W4zzOw9%a z1QzAz`wd89GUn@QmIm3*&$Ij&?4Q&Bl>lH5{4aWc&iq%{tCRn;MtdF0{|O{5{XfIZ%>JX9Hnzfc zua5gY5dUK09|~<09V`L#@&Fr)uhx11VLO1i?O)M&bvE};KXUz?H~soEAWG&yTV5va zU+(?q@L%!y4W;@QjF+8>;q^t&`1A93^xvod36-@5zMhHBPmR2czsvu9?4R;#|Ff#U zPyY@2XPmiSZ}NV>$Kw?O+E|+EIB?Vd$Jzgg7PK(6u$H#a2kZ>GwbiLzYYHr@k^K5U%FUd{rQ)w zzfJrN_?o9&(ywK&uVbqtq+<)l@W?2E#QTb&(+BEv zvFfogaB>3lUd?0ErDb7bXQ0(#WMiY%;b3NDWYp(0VAW^;rQtu*{F^EY%C0Hzv);5 z_zhnT;^qH&MIe;>qcak|KK6KxIx8Wiw2mIo{Aas-&rScIfPWeAr;gthd@};P=JqSe ze~J8WY`>)bI|5u9{Xfk7ee|C&Zu;L` z{{K`c^o(@O4FUSR^nV!l$G|_W`zt;EZ$*z;k4cwPpOt|Yps&kB%c8@Z@PweVj{S^VGg@_%R9zrp_s z!SB?6YcjvB!t^clUeo@s3zC7ZK9l~>9>c-L0-$ANWapsec=akRCp)LE9st0|!NKzC z!rv7AjqGM59^{1?17bIVfRH?j3-K%3zd1^QOjeqz zI-TLH8D8A;$zZHZO{51N*PsLHMmG6Hv# zBYfjyC>SUFCdcVW@0*R2srM!s@elQeGS%(fQq<;Cs{9q|@bwbK@DsF4xvCLD(&i+( z^+Qj{Rmy5)$5SN|GbVy3ZuJKzQ=zyixRAVkxVNLAh5?wxe921V%d{v`jJ)*7Y1Bkn z^Tsv-Q=908CSS7QO6SbpE{@dpJBQs-+$+ZhhfHC|@t{ADGSkjfzjDw(LXe}WO%~VL zXgT>FH%%fQ1nH*`A!YS)xrgHk&3gI~ipK)5bvuz%o^!m`m^(^{Zyay3J>wpkC7$K% z*PfEzQHk~R`Pkrc%yU{o?d>+0RIQ{i4*EB^deJ1q`O4O7A+H=GN%d(9=sUpeCGN%! z7nnnb+KOi<%aeacVzjQ(ST78ceBRDSEeZ%MWHwjv?+$v%#bDnWqIu#2$9OV`(4n-0 zx5mBQS#5O(H=k=~UPMtRHdNPL44B0&tQVE;u}^_CVd@I5T?rqxjH{Hr5$mxs4?xFn zh5-X(DkGv2J`w6{;h{QaBdyDo8?_y&r(Uc9H&}V8QG~0L3k4IUhlHnxyEGekyl`>N za!CcMzDQKcH<+;6lP<3!2jq%*LpEdst5vutFvPPhy@hfF)QMezFm>6mjhGY^+F1%P zT5$5~B^_OzUfnjW{(+lE{^ETmI`8fIh*S5Ssb3ZYr44)x`PUp;92VOK?l|Fbi*d!o z2Z$WYc0N=zlgEB#r*sH3V9lJf%bLS=mv~?aJA5+EdE0~`-pQk7{9&oQnvB?Njpb@* zbt4HmTGv6m`&Okt+p}!5K_NB^J0hplJ7>Kh-JSwXtS!fjQ82{h!f>U5qo!0+PnXVq z(rPsvw5O4Me`lm#PU_%|qw@Eriut*UBcWMh9%0&O%>gVD{SzNU$ugtdLxg&L7fD6z zdM5&w@4E%61@bmj*J8z}>ZM{;ZiR}F#PZu+Mr5BejL^7{GpLi*i5)8X;r%7esp4H$ zB2yO^S8yRidEq&18b)Gg+?=Lq2EzHWmZ(hMl9D$sQ7CJ)!*?#Y>4Y5e+`uCD;ShgK zi7f&KiW#!x6leOY3Ubp8oyEW9OsND9Rlk5!ppsVTmyWB=kpzX(DmbeSh7kg2@@p=x z?k<v}M+IzW>A^V-ZkTMF;}m-5V+*Ltw~oH8^=lgNwPbyQ z#3f{fqg;2h8I}qWyQfkMiNzzqtS-RavYafwn^Mw|U@w~pF>?7m8#P)TW=7%JZT6_Y znE0d4nOc&*tp}Mx;QG2mWy$4L**i6$SX-A@*Sp%pB7XrE?xQ<+Wlv|`^P`gpX z#>3vyTR^|F!(mxzNl3;Iq}?u|9q}I+ig&vv3NTO`(ln;HHv7J`PuHO7snl^OUzNfL zgua6MG>cyC&mzPbu~^4{uI+}dq(T89061A4HJDD%K<2u_-Ai@O)Z!IMC|(Uh3g^DS zeP52xzdKpP0zgibLDL#|SA*I-O|%>Uj@`sDg7Sd!>^Aq-+RYl5LxN*8ueFZ$0;1E< zv>ZXGNLfY{dDM3P9Wcqc2ycRCuDUvZcLT}Caj+LG#Z3P8JZoL++FSLgdHTtU@nzT+vUJhw zFZ37>R*KkL9vZFYOXZw?#$b2#BYl+ojX7nq!`@Jh05|V+9O*|IzR%Zb1z}Q->5Q0h z*J5Fn1)tOwDrI5fp8f2fA7ou%aX9e9M02H+$VqO@Vls1H=#Hn!lj7y)FwZQ#$Fn#E zd-p?!QyrCt@RGM8M(^!#;s$Q4Qp}cS79tkYvLeAc{18TPKG+@Bj%A4w__+=GBa!%~ zT6JT&Pr^cf$5Tn$0=%Q8dq-sV()on83Z*ml< z?`rqa=g8N>RiW8(E2i9dOQP;-T05G_R zjhb8;H+lQI(UkzOsi`Tl@O$**&$>V2kGqz}j3%w?G&52@d}CuU7>TOZ?+L|gIOY4q zU@#bu0=y`Vr>0vpZm&6BZc6eB@Zk4t<>}AbR4sCaSl^OW_2*GkTt&8?;&HAm-oiaE zj)-oZVrTb1naaDLGF3O|pDd^@Q&Lv$ueI99uB}y7uYP@wdh<-RM3MR)Y4U)-P>$x> zqEY&X^b{O!da^RoL~&N-mtE!Z&znCg%%MfAak_!wF6}_s>aen{j(8L?f~b_8PTKvq z!(A=-H!AaA;RI?tD&Mhj+;YAM+O&Gt^pI#uW-t(?!02rX^)Nwj<*o>ynv3qbiqGOh zzWvH(uPxl^_VwZVs2Ka1!=pOpQ;kc!7)L+o(TB);W~u%O$P?-;>Zn~Uk$TcW@qwdA zIVCiNaD}vRs!k;&G%MvLF}5 z@rf96F+1|q@UJ1vWXmy&80ZPHtR3pr40>z3@274)$LGLt$8Q_CS0po($FfF2|9Fc+ zQRR1(q)2xK#R(#r*SheM9Zzc->~k!jyvi5yg50c*#eG`gQAe4#5g93V`@Vap&-;K= zsO(c%DjyaKtC8?PldiHM`%2)|<=lECm!^Dj#F0#|cDw@0%|5$4GdmbH8soV4RozZ% z)b_T)hlxcMY7vz@|Cy0D61m1>Tg7C9WE{mITR7Z!6{cQM`ew6znk^4!Q#9N(>1VDX z4>4Z7t5~irZf{Z6FtYdhzsq?QSh77Ue~5j!xLI|FUZM9L-F(Z4cS|j%5fc}O;j;ok zhfr~D{|LSbr)%o!pT?Ad3?61cFSz97eC*bSo%0VIxQ+3Hq&rF0!J~{K&{qI*J1xlBhlcEV1P8zyct}NBpNoxfDN`j z22U;t?g#}7p-0DCqlGFHl_o8wD?^0b;V!5p4blRGi&46(s*2%2Nd_=Yg*=%d6Em}r zpSgd`&2$c+Bfo#G{zo`CI8TM-Y~$h1$DZy=yh4>yI~y@1jb>*Dx2HaHZ#5;Q7{92S z6}K0$1@ky1&r1!rAqzM>u1N&hJQTaZEYXXLi_cmut~`=J%qm_Phm4LD<&IFVo0opCI)$~kXS7%>;>WJ*+tx?_xyC-(NN@WyG zdUi<69xo^Cuc4Wtpwc}SWX&n7*7jweCdymg=~2WUXr2;4&5`tw$jsP*^b+I|Wf*&FKKqM&|gbm~v9>&v?JAwod5=+yC zD1h>MIT`OLjqWbGtm}LTyd`}K6$VwgwU%#zVN7)V5)JF#(DlXf^A{Er)_l=^a{0;= z0R@PLga??nm|j|KAlkC?ipeFdd703L8wUnz8kv>}Foe%}SlJuBGdz7?l5rB@LqF~q zFRH&?)Ag>11MMJ04G~zvN1^b4`EwZs$=x)RfefsDmG^way zpWB0?!}J`^wj`kmq@bWW8Vf*ai(Y{p<|bba0s_7x1sVo~b?o8rf;k&%wg_k*V+v49bOL#lTs^WtjHQ)PcWZ`0bg0>o zg7mW+@`9LY{BaUqjjnT{S4TsnwXohb!@FQ6$KC#Y?!)fI(B)l?cNDhnW^V+ByBh(J z%Yd%ko@>Qadnj+jO_#q3zisU71Opj%*JhHP+v5^3XeRO!v=2U;;u^*&8G_I6A?=t^QO|Ewtxo`p!6Q>duX`ThEvEjd~0lp#$ydDiv_(n8049FZ4_@VTpw z&NT*muEB&@OA0~|!?x_MRhduD?HZVyEr_em?+)rbT0O>0>bUhVm+)?F*s)n08MP*1z3$3_)kl;ru5gyL|P+pc~ z!JGuBqM2WrKZ7d3qI4;HU4SW{-8>LgBt70i4Jq&1TPaYk1b2`3UGVHmNk)J8c#9$B z&)z#To$C3(C_~YyI&R+iLPtKUwPty%-pe;Sv43kQmne zA+&ndrDp}cxW={|`8j!;8c|zFE2#GXq{aO@n4A|<{nKE|0wcBAn{>W3aFrWX7-NWK zh-2gVTrkE&*X2+*rmfouzdPx-FAE_dSktA`Q)0_EXDY^N8Jzgo4x@5II22Dv{Ub!q z5HkD@89`pg;^y8(2K|Rw#Tu(yULGG;9hNru4}{03*GeIJQY&s@^AEJfjbjj}{S)2! z2%n6TjT7){&37I4c7(=VeHE7B1?_o#T4aU{Z|?}akD@-Fe)iiAOIJfEIX@FPFX?*0 zdbX(@6c_gtkd~GPii-A>Qrdui(xy@m>o~X@%&@~DI+FEm4VclxiZEIX>{c9)`eM(S zEjo~(=_;CJ7l>pgnnkwbu}K8S3NQy2B@lIOm}GyoakF6+XJQV=2!Gu^6rr1@v90X8 z<;By$d=ao$g`-QZ-R!b1qbA0N|8A_0i_jelobbCOD6o&gV~j)E@+}jOEX5Qa!+Akv zC#y_+W8J8=eGTe_@DQKh@PQQ66wM2}hZ{Dv8JSb{kyHs<_8#eKpSqOKA=tR0UUKlj z@q+2N;%Yq3Hp}i@l+SIU6e`{-$PuOV!32y{FQ}fDmQ}Rci$uqKDvK<1Q zsgDvBfu~ltak`{Gq>zE<3+ZwEX>2WE&Gn|LVbe1%OVWuRrJfW}3jvd? zskI_s5%?hAoQ#TrPJgj_)byDX@4ZGcU~j)BwB^lm7*+2hzA)?7F80(8KP=mh2Y)td z%%Y|lQ>HkD2bkulE5FCwWCu)tDaDL)m0o^0Y7sB9vY@(< z%V^hy9&VDM@`H~l^g|c3d6+;B034%K+3U0y(S8r}|Fk~1N2=2!B~E^c;ZAhWyRf&} zK{>?>o-IpMG%qkD!fI3E4jjO1^d3PVqM==ZokV@hw>0SpPAWQU>I|_CPQDQ4zuQUY z6)RKr#IeLa)Cs(QFV#mQWK*W=PYCz-I)l@JjDAJPQv2nE)~0*KcI1)*2?C zGk>G-!EmbzY#vhU$7GQbzY4ztyW}j(+;p;CdKh2ydw@q{9G9sMm~ai0%2*HSOGOX> zMpIn7_nqH@-cp2=hn^S_tw@k$Kh8$^eyfJOntd|rof1~|7tGu(HRfFL<~ts>AGh#( zyk+z;b1M0j^7V9e>LP=;m71T>T3btEs*3ApZ+w|@GXsuPr=eaX%Fk&FN2W+&2SVGE zSD4h?b~dfl4Zoc4nxHPx(-Lb@OcSoFFrBUFe#BM0nrhrrs8utA#rilU!R9`*>y4Mz zHWrhOXDAC{37ROXW$~#7K2oL{m?4*s`{0JUCeAVBmi$QUNo85j5Uk5;PUs9HvjzfN z+*D3-$?t6r4WI*)?NMRvt}&&l#8{JAd{fzDeE$tLN>77$WQ{60Hfg+{wA`$sz4~2x z@sZ3ms|O+p&n_WBcNmF~F+>3~g-kN0JA2ou9@Le#_k+s#SE8}54TuudXKE*UA#Hlh z*G1d;4thT>EbUPkOf0QsC2d0!m3P0w2Z9#3Z$cmM#79$-m3w99!8=bRBnO}jppxC} zbyQSLR3xS9zjpP3WWDv+cs4n0&cD6AKiGZ>8?frw4(L&e_fUyAiGUqRCSvWOkH9#2 z$~km?U=28r^!B~PG{zN7G!Mhn%kzJLof+^!>FG@%cX+xR$mj~ZoDiI+eIj|Gd*VMz#NLsZsB^8_;r(4?na18ov0>ecP+YHK!1yyUj*yAhp5ZO}f)5uD$^t5|7qVa?iVTQ#X3yM}1 zDD$w@bWl7iGAJlLN1NOkQbK(veR8n#@^aMU5+S&dzK(FEx^$O8qmXMR*9MOAQs=KU zoY}<|ui08)8}k8;{3Q*W#5}W?yl)_QCx0uLkFC#(@MNyf4yH- zsqeRs*W{6r5K$ZuYYAFu`9UB2ZNSdxtM5(j)HKSZ3FyPj%Hfh^jbA5^xe-0CV*&M+ zD0ugPJpbeyd7V=tXP4pc%~b1k>Nub?(y3;6a?4n^q+9}99=RPfc%)qzOIc+-_C9Un zPf62G@0+=NZm=4wlHVQYHkPytuWEUgk*VO16A&&XsH%$~wxSvNMmh7YFjfj9@F;S8 zT~Ga}DNV>{o13|mhvFJx$-D>fuF|R*^EEtuc#9rIxn!Z|)zz)x{atm$GHUeEr{HU>rzB|iJestj75-SsDNj=P{LsfAOfxQI2j$l=f zIh;P6G0B<+ege_`(IMdt7AQsy_t0^F`%-s_$gdN?%13ihXo^XzY)<*5^U5Ncg^ zr3&Qm^|;b;R`5C)LWuga`Nj(Xd5Ny}AMll11UUsZB*9#{2buEnWQcN$Ih0W*`kjeB-cbuZ2VX5!)`ANcn z3NU%qcI10Oc^$8qek`OF-k=$UVkzmG=qp`&I9sQRrdf9*epFh#<8V6hSP6!(hvCCp zG&cEiHjI5ivpXDKmmsy#U;uQ~Ze?oe7{`F8yF{sl6C*!Fe zLsUTnGSLrd{Gqo}{YvGltd~MgPfMbK_~G2(soIh>bHD2eQ|eHsh1Xq zq%m1kU|c%tYVEf9?bZhElPwghArJH~JVbLyCN(PFSI1r1&e5NS{wAPuopzRfJG1zzFB1KqL`AW^N#v+W`;$=9hX|$+P zfwIcu$J6*ce_H>xFwdhFfdVCxgjBg?R0%geTNjJc_YBD4a!+sTA+o3xcPGeA>y27(!dMSwp&uHG7``BnEQ3)! z-^2_oYxWk!=r>}h7SiOV&F93*1P(M+pB7?DDqA*&z7>GLE}^Ux>E2ss7Nlw(maROa z2X+>L6m}+}QqUcjahwFPyX+L9*b3|74sOlD+UQSakHX0?${^fk12>hp4vw|y)L~6f zC@)BP9-sN`of4#0FQF%S;COd=Mfs(#_i9(7a#<3AS?6Ly+OcXl(KoX4E!729_ex@Wh%8<&uo!t>;h z>=9Kd(bU<=ONQfa{EZQfrjnl7_;Rp*;HwXn;;cucN3a0beyx>n6b}uYRmEKTMtV9MlFk(HL`=RR0+2_7PnTRXk`GLswPI8%PSQAs4{sF1o z5%Ux?Slb=C;zDfii(`=};kLrDjwYuJ#Xhi>^rIUd*dv-$971G30Ej?s@S6qUO6xG73m6xC|AE2yK z$Qgj_6_K9>R|tr|!}_*U_UB*Af|`vtZPRLw5={0C^#ymlo!#K|1Ud9vrcED~5xhad zk3^sOg82>O@m9G0I(ONL(t=*|_WRaV^yu_7)U5294;^`4M4q$Ck+-2&Gq;-PB9a(| z*6vFb)Im*eOO!L(Ez?&Z;Og=#W93UC&7`b5W3iySg}yTZm@r21NClZgYMJ``YKUVp z%kG35TiZsfan0|UboSB?VmhG@DSHv`9oBwHHJ+HEbg+4~)`M6dr#f{hy?(Vh(#Cmp?AKi( zmC5IBlO9%nueazW)xVH0OQ95A%@>enPE|neh{?rePGnR}Nl(90iqMWnxF~gNr@YW* zlx|}5;&Az?lvB}*ve;leRfrNJN7i6`V`1)MD4ucS*5%>a1#eMOe1cbD5(??FbRA@y zgAWT16%`e+COx<8GMy!tWnoIADW4VJ4cFcd#h|3iYEj(Sce9l0BhsRcJ1AIpUuJje ze$q0z*)1CE9q~VHikePWSx!X^XJzHp*kw8oBg`xn@75KO z*@x*H{hA^#hk?vILg5khy=(C1I+%S~Pr6E7#Uh^`?0BFUaRfoH@^nFvnN zD%ag?;2jALySXV*GEwK#RFNMsu4;S_zLV*UUf5H|^9XNBt+4<>!CeUe^?tpenlsFA zjnTV+AY@G8cH{Zr^`Dz2JbJBfB>VfrtFaIymBl0AoJziQqIU=%uDdTbw-VmWeFi4o z7d*c4zGNvUM~oW7;mR>ph^xKjh-jl9yfF3+Ff1F6GcDT(<(<)=>Yd}PvkwJJQ-se4 zj#HdU9#dpmZBV^k*SUHY`L%Sm`SfVBZf7ZH-Zqdv_Id$sCF9T=Pr`%wAx@ox zt~kwhie#`SnB)^?r4L#3Y>Oe?&rNRYUo`go4kocVS?rq_IZKMQLe-|;4>_Z(eMyz% zx{zYSbr#2JV`e0*J&ok{F7{U#)ou3K*=dN)eWGmgt6r|U>w!Z)5gW#v%?G?GH{r&( zen{(+7!kI?z3sF~pD~rO_;IzXsUoU#2R3+DsMz4cQKfhh8S;f|-o4!}K*+3m1_62@ zIGIb4Tabao2{vhZs$bT2_}yTg#g#kNFwxfQAGqtSMBp+Dz~axlNBK)PG(9JVNX#Tx z#mMX7#VZp72y>iZaJ>~wXJMttDr)I`R6~@#2j_+>aaK2z1lDvsJsz^yknq%&^Q+ew zJ9uF0FM2DsncW+BL;;7wr^ZQLFv2W*Bpz6&0j2eeyts^wPwI8F-15yiU#^=TU)Ehx zrChSvHeq%+POU7zd<4yd+jBI3KCOM-wfyBgava7czC7!bK*Q}J=n5gHJj0XrSx6!3 z2~?8__q$W-!`3>b#anvr^FVk(uDFzq)}AhpH?YZU`43wrBHg;%#4srX=rj(Dxw5U9 z(vA4Q<8_i^qJWFjgwG*2$;SJxVNLOg!S@7?N!nqxFfP?|yL4NwpJYGXgTb@EZ|c~j zIKHz`ep*(IN2z{RjgjQz;;<9~&C}vKZD$ocr@c^j)w*h7JaptU4?eG=b@3|ryi{I_ zbZILD9{L@TYr?B9f6c?4sPqghK_hUu>7;DL7(#%~FKVOp#RC4F<@Sxa!NJ7}M<1XT zJ&GA^=A^suTVF@cE>`~7@C%Kl%5%xAY>3Ip%WhI18h}EjzYn%@21Z&c7AG@fF z?RRl2XG6q#KU*I8(OKak=ZEkxvaAPx%DG&qZPzQclBv{ z;QV@h{EXY1I|hLl-|JK<>xqi9?1nt~{Ysuat@*aG7zq0VQLA|F_LH kf9|>o-)J4{^LTlK<>zsobc50VbFa9#u#8ZNz!%T|1D&J2rvLx| literal 0 HcmV?d00001 diff --git a/static/images/errorcat.jpg b/static/images/errorcat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..15c1fb058b0c27bb2cf313c8c9f858723cc930ae GIT binary patch literal 48882 zcmeFYXIxWX+a|hc(upX&MFBy&AVm-eihzI!id2E9C@s=kr~wgBdJ#}Wihu|R5vkHb zFCx-=Csb((H9!((|DSp1o!@Uhoip>ibH1ES)(1keS@&LR-Q~Kj`)vGd9yqV3t*Z@C zP*4C*z#rgj3eW_oC@KGWffqISqM@Uqp{Ax`prt)W$Hc(I#K^$N$jox#JTnU`3nSxs zj`OS++1T0HnIN289Bf<{*x1?rxd{ao_#J8*dKwyfHfBaM=?LKAc7gl?BLvp*cb315Ha@rs^-gOiJ!=Zc8vRWWgS1x2MB z$|{<7w6t~Z>gpLkGBGtXx3IK*{=&}Q!O_Xn%iG5n>E|EzIy@rs&D*G?z53s>>^}|rpLR_GOjH!$;!&{z zFo0zIIqE9#|BwIbXMm`Q^>CmVe#Gpi_z4s6MO9=6u?UA?t2-A}XH5T`V0pytA!Ceejf#Vc)5;KrKgV-4FU z?@7}Cl4_|7)>K%oT_v(To;Qq~8Fl6G%lFWYxZocwPqCjisehOKQC8ri*vnSW%{u%e ziR@|f>X~_^IYm~xu;Iz)2PdO<=5Xea&|i5S=}V#h+Z9y1t6AhqAgHbU!d;1|q(al zB3gl@g{-Np1*Iqsh!FfPj}@`}e4Z^|VwV4bHSMDPYodXR=un_$!E`}TwOHZ-cQ#8^ zRK9cJEu`T@>E)WH)gHIGUnPaY9r6(7IA)h z@b|dURVr=W$3C2cAEkLNuGHM&H_tHZKnbAc3!o5n&UPtu zR=kL6f16)jHvCa*$UNb4#t|KXqQfQ-!rBhqeua_lx$u+Uv*TCW`v{V- zaCs_u0p>wi)fH|=sgBzuB)V$`(XP@LC2eVK{Gg^jB~Jg|(n`7Mn2+0xO6D-dVOE7)`mMaR&~8XUZa zl?;&gO^L%^(A9MPAa%j!QLB&uEbc42rD}+I9H|jwshv=yI2c4)qr}?Gw0b;`n@^5; z;W;!|DM@r*G+<8Emoh~>EtR=7vZ`D_|`iq3sfFY)I8_Q*q2xAxWPCT zV??_>*P2BTGi!x#Yr496BVX>oU-5>fpCDSY&dWo&EIPgOol=ivZ*)y!6^I>ERtEMe zx~@VJVY~BVUx+YLe!V;H3^0m77}za8i;dhSwnhQuq%cZ~KaQU8WDEsnz9=nq@3A>D z$Mmil!wWUUKIZ~zOJP1fN)cD}q4@St7wvX*XfNP@z}B^*%jxl5Xqd+vkYanFLGyoY zJTwCCifZsnTp6M8~n6NHb(&V>gvIe0qp%;sFUTckG?!TbKiYze+) zeFo47;Fa2Wdm1EK#~=lc%Hq_{G4U-S>bs;|%zSPpJwXTIY5Vz}f`N(0PtDyhc*+^T zk8y#%IRkjmr583H*HjHz$4xoRF~;lRBuEc;%xfKYME#a*lm2)by%~W6OY#1|m85`m zOWhZae3=$gB3~lTH-s702t#VGcegVNJFKSgf2CNsectHF(auzfHe0FR)LeR$V>GPG zTwK$i#ee=Y>g4>FvG5QKuJMZ^L%OPV}i{e8V;M7!K$mGb2F(}us(7Y*Ip zWPj3@QYf5;&Ce08WVMvAaQ(@pa=IV`e}_^zkdis%Ajsxw7eZc_9LeBiS3+P}R|XFB zD@h|MZ69EJU1vbJh3h%Q-y-cSH?>qbW|Wrc8Sn$sa_aRczNO06vFpZ4N%iIsZmnuz zp;*gB{e=xO8?wqygrZv}oE6st0teDx?HLYB`h3{>#5r@9odmTZ|DIz(JKuvW)M;rF z^+I@BMqsw^*R>;2lQRU%S@XzM4d|bMyBmL6&ILqWuk@qw>=VQwy|V}+uolK1KhsV_ zgc)r1X2?pt#G)bbg2c1XgEQ@I#*UtPMU9{(IO%UaC>? zg_{T6hD|fh{_rE-zD3P)5}|h6^|JX3U%qD{usr^j)J6qT3d*&QnP@$oQVhK1m?d}WwhPj#SL9D!f$ zv{4&ryo@m8NiFmi-ql-f^brZUDHEGjq4YLbZsSPFKYYKBI!>>{9Lv6}m-{MUp!#?? zD;D6@Fz)Of9(3hzmyE3oxvwTMpnic!fwynpa$=N%-phO-M1gtQ4){&W_Ih55Vw00y zE4psei}O6x#qV_7B;*XqM~A=dB4IC6v7Z>9%AEnt()%#_sfoX{vZRBxR$l8|bVJa_ zLj1Y4Sx8yjn9fjkSvaS|ZUQBW2O#bfAmB-SKj`J~qz+%%`RVF$Y<*zHiP_W~%Nbxl zM;RzxeplXrVNwpWJ9xKMX~QOg?MOL)@+#$zj>{BJV{LDdMu10!bp>}nf9bIGc4JvkDQJ^;l@URC$@U{GSX!}ZbfAN zTNK@i5$*0?5dme?zbznfs(b0hjhoHv8INqk;EkvX*e2(EU(ADPs|gFOFHTJK=hzwY zutdae^PjA#I$_ zikWSqJ6suF2k3D)6>>ule;DRkK8&Mwm-999XXo=E99EHtfAh>gM&@vd-FCKZRQair{)XqVr~{ihkuk?s=Nn zsYf)x7sOOT`8TRnQgc+3btO`dr{pQ%?E>P%flrHjzdBv4AN)1-_L-q&7uRaFJTXMY z^PX)x!2Jg(bapNnmgZnYPU>>ai$|4xUQ3%592Wdh7P^|L&A@J^I`%hmQvuu^>!Hry zh-TQw-s0oPl04(olYZJh(DTEH7w6!FYCq>9MAgoVylv){yER>h=&`=naEd@toUc~( zkAaG=3l75)Rxf!!##PR&HA07oYF}Dy3oJVkhzsF&Vl}wfvx_*hv(KrdzF3X&D4jhn zFt+$gF&tiIG<`1>U-9E^+2@$lX8`*Gh7=1<*&JOdozwpJz}}{g9iNz6`O@Ko!ZhWsKBMvS#TWAe5c&5gGwvP~_ZBQ0;e>^O2YC>g~tD?r$s zPAVewKc2pQzqhAc1atWG^EwPI=|FXG27uYct5&4{py^{ZT8}W|os6-YGDHrau3v1( zr|cen-Ku8G)F0o6{4tL=PG{Gl-p_`N_R5eM`3t7?F1WJf^g)qf2a9!5oMeW91^uSa z8wTd)Rnx93815TZ&Rs6Ul7ofRcbg8gA!{hOa@X#PLVYu|VhAH1*$z8XT&lj|&OyQSm{0a{_EJel?l7R23orVL0QMkx~|cE>uQ5bvzW+KSp>wBwcf#45=98ua=W zmL=~zoe(w8aoFKEQdGS%9;=fXfN*%}9Uillc#${$Wk2oS88BAxauCZ0De;f&j0c@r zTNWkJ%)7v8RIZajnOh0`&lUa`Txt4J=cQ%BbFo*XvCRAb>d+(ntESV*~F;(VA{2ANN zv)X6dX_$)wxc2!;;)h|`HiR_eh>i}9bU&`EkGNGw5c=HS5?v)F{q?0jU)fJEpxeNb z-jsZMxqwfL`qrc0+RPLMQvud6PYlOJI$LU;v!Yck9E2MZ=r6xAS>Ndmj-ImAa-?i! zLh$KbE6N2J9tR+|a_%nP;&b}fuc;vyQlpB${yp#L^TTTT*Y{>F$?Z;nLW6_50VmN( zRv-`Aun;7^7$}x}y>oPx-kde`<=Agn6(^C+SC`=F>YYu{_NoPw4P7_o$sn3wy+A`k z`=U&gf7{VyRgOH_mSggFKtJs`yrO`}hHVR0PhY)<+mC%(u$$fHb{8EPsDI(D zdPx`5(t7%|pLQUN@(gf}=L_VhiG6v%ogZ?ZTi8SU=Gb5R%}(%ap2tCVKQCW^tpRj`-Q%S-v( zw=t~iW)OHEu@NG-xBbEn=Utcm&%YX9X8d6Jgg7W}f}O8`zvba?MCZ)u_#xyXc+D@U zS5OQa^BX=G{iLrUAa<%n{@ut+$_bQme_EuG2tU4a%cJ;L)s4Q&VD{j_xpLPxFnbKI z^?L26;*u|WOTE?0FAmOOa#yVOY$Q$%gL{Laq+C@9ShAk)NY8Vy9Nd7)Ro$Q+2T%69 ze2d4~Y}?;xZPY90h#mx`Z^ZNX<-^dTCOp^k%jR)(-Ro8iTQ5ChSY{k3CWCX_bN`;V zd3<-E@1l#T4GZE>lT82f^qD3$3(`DK3SQUFVF-Vv`#ALw-O=%&`3w-OjSoKqSQTU< z)UTC|%CBdNMwr-EoUm#@hlJNMBy2nTrap#@ZHgR#la6x$bXU;EuyWuw6D5ZT5KKUs5h7}Bk$ zJ!O;@zB+^06QsIPyoD@OKi-_2R51+CaNIj57GbI)*NgwqUL5TAoLqX0JEDWy2-r3ZUY_ zqXXxMV_HKi4fP+MRE6u;{cHk^sJvtC`fd%`H#U5d+RAzOXWI>6KQ=k_Mm66S85XX^ zVP3o3Gwpf5l&ZK~XKKH-3~tH`a8|et;bwDrxn;{ePb=+l>LYG+Z_k#cv)*6*=m^Jx zgrzzo%D?HKRvLn?j4bs^4k|AG)-9^jE96@FQa$^dO}p)|X(jK|^Vmo4-`Ws;vKh=( zf|hqyu(yh8@XN>@u_`gK~#We;g>)KYg9{+_?YZU_&>?JDVz^ zk1H>~Q=lTt#(`p*%)e%b{UU}z9_25&YtW}Zn9R74=K2d#30$L`gXX5)^{p#0|+I?07(t}S0sf6|B zz`V<*6Fs&!}$c%150f4bkxTDN~t9^b95(%2p4<6JI+1w%A zQwQi4==n9@M$R$m{T_l1W3eNXOWd_v=P+M;TSQ(KHlw@y2;$Y7;iPCUby1{UmsG|o z&+7*=(;l96aUCHrdCnCBf|}pgFV@X*EdDD=%I602=HtRD%~^?<&EyWxxdtEzM8&B0 zNMB2XnTdKo26sxi);WLBed4a+zGSxjDK6i&(0p?CaSc-RmZ01&odU6X!n*s*oTeuu zdlDzpx5f^P7i>Nwt3rd&fGZu-Y4~a3ZLRD>V|N@CssXi^ILFO5IBZmSE9_xEg%+F9 zziUulr+H|2s~g=TLlF1f0;VtvY!z8_xh!9G6izhphV6+2nWoyRVxiZ&m+YuX631B5 z7xk-#c+Exa0+f7sWJXgEg*?Sl?B&FsYT=A@*V)6B3~k!`V#OVX4>T@LOz3~L)}hwx z70RH1SHN)hFcU>kD&%b$_PHsF0FNvYe4oN`^C>%#w~kPPe{2x$JQ-k;axe4atrbkR z3@yQKtVM+;GtKVfz|h6*QlW>g_J zCL5=U<|Yp398Yx;p;$>(Ab&aoJ^h_vE`;FE5K$Iwajtw^d{tP%DEYR%k&IX^9E!BHZAqou-tX%Ai z0F%w2p4LLgp{XrV(lgdUOo3R*pu?}b(O}#)iSV>}iq|=OiBoDX77a5Wykz#0d?-v& zYec*EVk-u!_b{|pAZTNM>M$M5@hxrE@Y8=Y8AY}y-k>WW|h2A|B}4X zUF~rZtxNs1u&uGl7?)?_hV?ZMvi#YcSEGO!(cT!}0<1&xmbTAP{ww;OFXm5U^AqD4 zaz#Rf9F=@lO4n5ca!RKqITU|PcXd+OK16IlCc?<{8(5iGv#O)C!;%6=>gD_oZgl7o zIQTfeXSJfksz!H3GSf4&vpG_|X?%@nmqc3($FDCcwL_#92Un`z{+ts}0nA3{AgK9x zCCRehMSe>*ZkpVO2Ehdu1n5lM^Y7xZ@uBjNAV?DsWuC2cP z@^0;JD>Ge}-S^W z@H*#&!Ue-~A)+5lH6AN__7TPgq2J-1#po~?cSXnE-LhJgxRz)P&+()~$K3}zn@&>F zC)T3IRrPP)Av3PI=7UwoAy1K$siN<9#Wfc}3r^P@ZuYze2{1N}b7JL)HiV0|EBCHo zhRQ&yZYnhD#EIKq42%^2Hs~)YHTT2eWzjG6*^p@1eEuBg8L&V_cqzZOI)3Ez=&PEC z1a6kRHVZdY-VjPfH=yRfLOBhiRJ}#suDU)peZf#NR2fJ&-qa58V*ZHVwY{#F7^nZ; zMqD<|uXJxd>(vr|F1}($TDGQJJVgCV2+RyNp9-VW2`7q^uhc{`yJ6~{!7NEkXaxdf z{ykZMC_7Mr{LHG$rw5PIZTB}D4dM6~zA0804oHdYI4#zD;)|Q%EH9U9@U(Bfy`+AU z6n|B!voa8cmmo9j;d)v)lcpR-m2560((~pl+*bp^tl8npV<|Ma-w_JB`KV1ajSe-c zbybi4WRxFGWJt)E&>Gv!jqFXZK)i;Z@qudrvX^hD&qcIl<^xK69_d!E|awCV>3G&+Kx3iyM* zQXSV3j!snw$@7M9wu;hK8!VY`8isF6 zi#!Q^^n4d_5Pc{H`aNe02Ol5xtN4-ce&+PH<$=&IhyXJE>M7%TCS(80dcNK8iV@Wzg}>>W5s9U&>Gr<~V_C zA>l1&z?9*FQu~W2HPLp(#M!$r?C3(vclDGloRy9@PdDSY#P3}%l+T4+`G*NEG8+wk z{vuK`H{UwXwbp?u8E^P{OZ_sE$_#gybDrZQrFcrE`o^z$Xx}+Z20R|dR7z$cTH&GL zGLQ1ogQh;?M?5>%;e%}t>86|t8I#kAC~$V|f!rWjs5=-K;K+I&;kbOi$$jN&r}`Oy z)qrLh?3FIx1Pf3W@si=^(zk6#^D3V%HL9H0mnh!3!r~JX(@(nwC0;PZK1RAOKFWmD zq&YX{DSox>aBmOp)XNbu-9{XjkUkN;m0D5JWay}ELg&)7^va(bBA8rPdgNPGi}@K| zi}0|LnOgm#*0*3yf#x86SHNZm z(&B}GOGc!8rD4r|g!{sPv@N5QeMYVBAQ|eH;vgP=X#VIB!Yy*InoHVgq(K zkafDa`_bx!-)yw(?HN+-6GPVF@3N}Em~j2ku&Z>v0&P5c>9SYdQ3Rm8h=PvT%~>T zhdBt3%OcYq<4#*;HI<@wR`ASS%?`sUF;0>eJ?giPU%a~K$yPYR=S%xnPB|NL za{%+?3<#~|uhoQv$WKtlxO~vRXtqjz17c_+FQ8DZ;~H^pHfqY0)19fAQUzpjo|)htkyCJ=vm9b9%Tb-_T!;g_2S zQnyj(365M6lU9nWCTGC&bkaN0r!&BDhn74r%N=iw+gg14DoiRPhWbqa9^TvTn?)O} zNGPATkN!rywP3CH_~v`Q>zXyk9wDBUU}|9SEVVJ16o)}DUp)LmK(L@)Ovh|0Db;ig zXZExSr%nDmM>X4Q^U#!rtCUVSNDTZ3VT<6^7mjlSs1yj{r@_IqJZT^%#5zqtv?>`}5L5Vp8|nL~o0BVdJDcVvRu|D*JBzXRPstY?b}YNcwyM)h zCVXBiU3#;ZA>gLb$-c+zPunyXXCq0<48H70zJ?1wwfM_X@bY_^2=CXo$2lpew~#eA zZ8|oJlX6@&nQjWV(>jXl!SXvDwfR1w5_ZcHkvw4`po0fs^O-Q9f@@fFo>$pIXYSL7 zak5>l%!JutF|J_C8n+ES?{Fbn&2(DQzcgAXOd+Pt3pT6yXKY)I?oxnRN?n+Sfbd+I zSu<_5EUa~gf2g3B6ZXl^unj{Hfi!1vTmG(9j1~y>IKEuceF^G<*yOAG+2-00Ma=FAXtMu2*nvdS;@VP+{1z1_E>!` zF9X%>3%z0yMr{iZ{CZ@u!1Gb)W;>Vx$l-UIKi!@(#7q6oyn^e$Y@@84$ob{gj*b24 zGxCIu1gWIn%~gR)BxON@^J$&~y~snF^%dPX0aO0R)wep~nO~_+%m~b)BDwUO$~t=u*{|59oe1WHb<+5!o^U{28~>hx6pEq z0u{{_YlmINwo4uWRB0dm4L#;1y5mlot^5^ehC3m&F6cygz2$VP9}<0IKiuICQa*cP zc0RvNLOZS@R|?YA)$unC`Sy!>R^mk?>kUu^_kpBIpEs*zv?aIP>VhBip&kmxZ@f{wz64v zGS!MVIkw;#b2!s2_isee7l;+{aSf*WQIPCY=Zt&9Cx;M~*Jqpjs|w z&pO8ts|769ZjP;WZgJvOAg|8=#sg<3ANneeN5a^Zc9gn}d?ntn1I>mkXg4)gQmIhf zKDzjx?PnBUMwQAui(!GyJBpQ;?QeybGIjsSQ_$8Vu%LB<@CNr}szkS`Uufv94>vO! zl;-;Qm=JoJBx5_qqHH`=qpqa|pzcD;AzNnibCbDE?i;h5HM`z)lwa~cPv1B-hS<=`BQYPs9xO|hpyJm z_jRB-h*bEQ_~=01BHjGIRV5LfvQ|1->0-vYxnMN9rlH`A9qZiT0)4c49o{wO;G%DU zXt%4gfa&ZXNj#fwR*KGwp>VPW52_+-OBM^A3|nOHd`qsSq9^8oPSH7KH99S69brP)b@of9=93P*>j6 z{x8tWmoVEXPY^03FdSlo3l=m-gre`j`KH4~&kmahQNtic!_dbF8+xG+Z)hV0m(t!jD6$Eh)Eg zAd~h6E&IxI_8Y1^|Kh>-4MWZV0ac4r72=)m_{i5Xu_XsTrxfazntkbE@^War7_qsT zOAr%`d4ZbDZ9!Fw)N0SYjlu{) z>Q~pU0qmbn?*|W)~8LXF2tQm~;;n{vNxusj}+^z=DZFivds`B*I|Kt2=eEg-nyl zeSV4kY5D}ZZ;l`g>C~A zH;j5+q$QU^f3k!_7G;f-qvANHAA@4Sy|7aoq$imbI(fY#-NE|v&BG@^C-qp$w5!R( z;D?BHP)V2>=LMhKXbb@;X?O72$G}OPd~OANl?7+z69|n8KJv(2vU*`wV2maD@+4qK2Y*T=u68O%CR{E z@&!Ye182l+_bq|-V4lY%AlmR|{6*ja$gPnR$t-PEpc5r37G+wJqF`xcN{Qokf|9__ z8_WCt#~Z{w_@GSdGul=X5Z-jkXN79g0=(Ibu7c+YMvy3I8_FX}RN)T3ttgiY^B!hq zAC?-E>5gz6BSdI1A^q>nizHnhdFktkU7bVhD=?gStW`XpqpA1=bA-?kWn&%a-5)!$ zI%3Mhzm-Y1N-Tj|zg>{uQ6kfSIpKA}TnxV87{0$yyK0VWD0)$HlA1Z8^Jse@zOI|k z4JIpbk`=j{_~RU6?;@GuG6h*+N z$jXzl+x(5FHT;yyXyo>?^SfQ=t-S$SpTBKBT{NtcBJFaAr!)S5EAlY4 zyjt**->EA1vMp`l-=*jH5cRwdn%W*}YgEyM_W8%~w`Twy(PZJ{TB^CXfue(AO&62V z7A)+^*dn+u=Q=zC6-edsTP!Qad@A*3mUq@O&LvP9gKo-j6yt?`dgbS~EW8gMS!&pd znp&JRUovk`i;d$)QrIlTFAY)$=4gQo8Vr|L^!tdZ1LTg7O~+Ao0Cih#Yeu zJ(%Yu;?ajPn|pa{vR1D@nvB@)U8a7Kt8w3bjJs2?@p$gTA*eAGC2}OU@uU;);l4x* z|DLopCw;lGs~9nT3tJknx1}Kft~oWFU{4d-GRKCTL>S2^Fz(sYw6X+NJg9-RjVXiw zh6mgh&asdFX0eekI)>Q9j;YK=lw5pUSK0d`e(|rR>{wF$5;PBl)YDG9zhs?WM#47{ z)qni1ybC+ZNK7UNNo=rN$rNToz=xj8{=_7bTHtdM<}s*lh2sn=@#23`Y>jN|p$dt$ zSWDdU=O@;_h-hzq;q5NziCt-1gVlJqgQS5E2f|%Jj+fN?6Z%=w#}~t)Cx%MVeG4 zbq{Ayu6067f@iKfC-^E5_lehs<6B`<`u8(nNr{Np{Ecy)@uQgRzH8H(2)B@}=gb7w zbO3V{u5)3_r$BoO%N%0|(!%BrRJc7d!}=Em^p=`bkBI8&)imNOxU-6zA&}6+iX_X-??@z!hFFstE z2)a)3s!`}@hzk1k-bd6M|LZQ3>is`mJ1}|Z@I8q@W!$OHDIe;D86_V~pKKo^|Cyz4 z|8+MJaiUMSf|O~zfP7}M^FkoNC!6KqzM1?(5ycp<{Fk79sneAp>62uOKa30t+j~_{ zlGuG+?cCfUD?&^4^hoL!Izpv!@(+n zN%!i-Z<)_7tK$3qQQ4YU=cONCKm1E-@PbSjwxV;_rCdt_?>0s&`rB;{OcbuGb*MnL z3rr8W6~a#aQO&pZ<0q1JYH;ZAE7hawBGy{Fw-RvcdaZBm4gx*Oj^;B}$d{10?L3&+ zdd;th+QVG;Vg%3YUbT^}p|m73(Ij0^IuHV3M`k_57ra$4K$+K^+7uUBb^Cg&w;Sx< zw{^k9gkL(;=l!F|<`P?&aUC+l^%C+Q zPSZvRJ|j(Kwbc>+47iuZbKg*gC3rvRH!5l#rvY;pPM8|+5iGS3k^~dKK}<$G-`}zH z)h+L@@Hf>q)5sEwXAjC(lmPM2KQ}4hnDoweSEj$vbFTG>F9;6HtVkF6W6`R{2-M6y z20oxQ!LvgpYzO|QGtyhMhJbzgb-7xxzM6X$d?4_;9dDw4Kka@wpIm9YOH5XSdBU~e zyEnLrP6v(D;uKv}?3RD*Y6hRAaoNAMirM~4u`#z{`h>QJn@gU;z)tkxRpaW}_Fy#- zwQ-WKDIqH%Zy(sYU-=VfGUqVq)1?EqdWLy2sy3P6t=!aVKZp@V%~!G3fEc)$Z=~bU z((V9i)Yh$AeIgqI>dR96lm4Q9KqLQ<`e=UlU&F@cQJg$$#F#QCuz+1IWV!|{(v{4J z{MafZdSVb!vO32u?%dT?6#V$h2#j}q~d77yLlF4$2%VM>l72Q`WyPtJhF ztoVZ+PA8v3pW28rpCeDrr$7X?ofM%g+*boD(IsjyNX^j0pOJ06>OFR_Tf_W>zhxf9yj;j=j4d9<2I=D*g5}ov{jNDaw zLtBZugDQt_U0Hxm1}wA}L=h!C!`jSFp8r-Kc&aXEn}8DGyS5a7QdIa0s&lq3FYZi4 zE~=rURmD6zS>fwH%1+gY+}%;Ph#!kG!O~ApH2*o_^NUJcN3jrTq$(j}|MW`@bOHl9 zwfHS%+{#dU=Ed5s8&RhQA0j6uMLT=h!j6@+`J(K%l(0Q83L-C_d270dE_GtTx|91? zIzWWhmIb*UenXZln~7S|a8H#&M=_rbA;r22GuL!MSk5lqhsAI5=GfjbxDYn5H(3?E z8JUmudFDQbJhCVJkvL&4iv>a0gO6c$`lsf{5L#AC6ELoYw0peYzipmBFto13~{sBZ2!l z(#T|GO*6AxOc?}LcrA7d%T!d%ovRN@mAnh3*;;+&GIPovEUgOn)9kSH9HP#fcaOdN zg|Z~aV4jmq6yihGxf9_&Q{J>7X-Ib4d@dG56_?XYLL8lfd^4Kab6z^*W3?Ycl(v5$ z$XlJ1jkf8H%I zelT1wregUw?RovT%>Jr_Pv`(9bPigl4ZnQKQMY!f^UmDcwt)bQqcvu-hn7_%AbL&BlKHA~c zoE9PF1&fq7rDA7r(T!JFXcETc2rN?OE7^ZyU8xZmmOzt2q-$I4i;nn`^}+WpFSpGQ z9j(VrBJtma<-XhkX3$g~8I>oKH6@|xpJBFe#RiZ1h8i+-%sN7-*-WJ?`V$EN!j z#;FOXQqmc}FC%CAT4D3v?{kK;)YuIY;zOKbLPC@|R@F=wciIPkBOsvV&et16NgD{r z&D)E{JORV#dU>n3am$I(%}@@v_*tfF$FvVNEZtWf`YitpPEW2WUK!UjoxRoz)jrnf zIxM-`^_<$P9eAWQ6)=`zPl} zRr0w1t`e+g-j4+qlXlD4Y6KX!Y)pY(sYf+39NO}xft}5b z&5g$6TYH>tciw;JrsC=!z`xGn3@`*y?%|L32xWLP{r0a@p4kKsJZ$txzxXw*wg0m~ zJ&5Us1Y37sY!%WGly@m$`gZNszTiiA1r+xHLJ;N0bRZtjy}>ZQd{%*9y83)r{{16i z|CZ3BcU~4vI|C;IZU}t(Ne9-TK67xJUdp9p=5s22LT<-jw_m^fsLzAC-%B9oMm37E z%bI-LKKEm%4FP9IT4KHIS)F*Seh2rV&Ux4-_@4pZk>Y;*&8Iqt1X49o>GT$nt)Di` z<;yFHe2Cu**(rsJ1_4p3p|Uj{L5zZ<$s1})H~)A`;IxxW51k(w3s%-gxnX`*zxlCz zJ&8gMao8^{+d&iT0{TRuU=ZK3C)jMXUub%eZ=IOL;)fno0W4#udV2hshEviO5$Nw= zTSpDk`A({Y&gyd^FiOn)XUl{UjH7d~w`6couKS;Bh!s0gKmak0K^RyZU}7z2dh}OT z?6|oO(9krBx7|nC=Sagx3J0LXr%#tBvQvt^1^c`?+V;juqxsbC5=kovtOcm+aK|IT zf;!i@ccU`M@)V~}IZrA&?=b|)U6~B=XEI*D^JJY+rEr}S@yk@`CfX8O?^uW!$Tfb zOT?C6>%`ii>U@ydG@zj58)Qb|^eJdY8s1|APG2b@64=d{!?+biG??lDVQf{IJhj&i67HFnzC~5q_9wTgFcKC)I@#z^)@^_3LnJ z9gZo)m|QMJGxgYxLhH-hhy_D}IBiod9p2$o3f2Hl$>@9d1bw1SK=9s`-3MahZN(3beOSxiKI zTv$hXTDYo3zCrFrD&z;kFY>`e|Nir*T~C|YPjeU11Tl0IgmJ2_AnH%yH9k#SD%l%b z>BL1+1Il@aGI+$))YQ^G)RbBCK58GvYH(RU?{=_57Zp=v=}`Z!tOk^IK(EMr-h}Yd zh^p?&tL^put>b>`^heGY>hm9EDsFTCbtjn4g*d5gy=grvY--n`JE@Fqu!$s@3uG8ki_fh;w+~T6;CIN3T2P`5 zgsILJHnCt7@O)`(^D2BldzIRFW|LWda%743 zvB&oRmbs+-5K9PA-BK0Ur#PVn&jbklzw%fBj8gC^1l=*i$8I ze*0_i5j8#pWs!s&(V&W(Hfx@vL(LZoFprfyGrZVQb%8Z@U|*nhvY}3kRM*J7@q;?D z&As4;FRrEjr(d;=AtC zYLlfSGk8t}cMb;#`nFVcge1XY(m=*o8exdc;Ewy)`u%Q|Qpe$fXQe-t2K04p>PPR~ zznqFnqk|K(Q+NA&T~*kUl#C-xjk_+zu$pYF%S@SJ3tqzaTe#}^?UaJ9&q`oIbTcOEhzD)&Qk9CE%5G^3)evZ9 ze-^@8V{E0oyljqB7A}jD0U{s~sDTb8R9$o3JXLxzm$z*a3%~FJ^J}>$F9aRtK(<2bCZ3g#Yo;>C8AK7B2UUB2)Pn+#k7ryCO8+25m2JE zw6uc$dP-!f`hD3!jPxdUuQVs-47f-UMfwUZtu<2JfVasxaNykqG~BA6cDCZ*fKmK| zbFk}}l^f_7)K(?Dcpm#Mz6EV;ajWZKF$9(wKb6~pUckii6m3p%r<8Rz1g=&%pTpb% zyVU0(9kGi;MRYh34_^xSr?S|leqemb8zeI^^BL;uu8hRMff7rDx2idQ>!XbZCNr~j zbD;tvuC#%V`YW%Us(u&~y|DwT{l#lD&Icdq1$jW30`D|lBcO7fikf2%|F&ZoHXh+M zn3L3|4-UT>vLkJVIs|xzy>1Reoc%7|X^O$=Sk4vw5m1gs(Qkl_$~#-JIFX(Cto8<* zrm6RvnZ6ELeC&M>Y`C4{!Xi%&&@+aFtDTz-&<=EXwg2n~fIK}UBo|oxJBlWlb9Q}i zEj^|E)3h@a{z4p>d}LF5GscuI{BbOCsq=uS59?lbF? zVDi8(Ci2hX4bLid3RQOv=IWWl$S&*wx)US|%4YDb+UKtD{x{CvJE+O9YxfP(1QDcn zqJq+^Af2d4?*h_^fOM$}gq9#kZvp}W3P=}(fHdj73rO!>q(edpfrPjp&-vax`|LU2 z%-QD;CXmTYLh|Ik?{%%~`YoYb(MI?_?l0hQc$eAFzo1v1OY(uZnGHug6Q?_54Q|fr z$ADw7SLd)JXNAOrg6@82s^!fm9ylZMGA1AX_50E=|HFl8uZ~oLx8AE4wjuN`)z^X_ zBuq9v?%&Z~d^k=ao_z|s8fCe#3c*PJZf=M?)ZQ6^*$U7t+Q~{bVOeu;QUpCN8NME0 z@p5kG#}pF{|Bx*2RlfF0cJp|e?CPriwU434x=t5rdgb>|#uN02k(!{fU#D5|fkt)J zwNV2?31m~ON6t^uBKZ}EgFtT9d&iy+V?#+a)l zENyODL)Zw>t+d;`U2xF2kY>1edmA5M$h7}&t@(`M6|lW7ILqf3(+ZCZogY91InVa> ztAmNfa&OKH++g2x)0~#i(u%4mFJMo0vXm1$Bk^2%< z!((Lup(MFfdz%jO5+tCi@)z$rLqdIan*G+AAlMI94|nY1^$ur&sFhu!)ChK?BLJGH zAn*TWk7>xq-iKB7Icu-#gMY3LlJXOq;{p!0&y~z1j$C*z=yUiYq^h+F2f_CKAq*~S z)Cd*}lZ+t;i4LHAs)I(<-vTbra!Fr`N3bXaA9huca`@ki8sH`Rcyh*gcRxj5O~|Xh zNRHq`6>@h?n&hDALZ4)C_&DPB))yH?nLLfko_8*9WiBpwPA)RTLwi^JyjXhm%##vS zA0BX=%CO{;mp!zkD_wvek}0;0wou&vLo}0;EC%YU@Kkhe()ND?_V)yBr7WNr@5BXt zV@!Mm>Q(h$4-v8`>)(xdGL%di(SJK3zK);qAt;FdxA3hW1wqL|#K(Urn`s2~{^Bt3d zt2zRe1?P9z_~4y>tM=8TNlJph4kqvY$Nc7xPun`b(_(!}C>PVH9C*$ae|kT71RlEy zc&AXlBhy-6n=RavdOhLRdaN5K$U!icZwnCS^yZ2C&{nV)HEDIfzFl)LNblH#a`dM> zoH{WrdvPYkJQrnmLd{1scTK^LxuBB|!$u@ijG|Yl4$-q9BebB~jS?kk0pdf5rk(C5MSPYH#h&S2TO~ z^G1iyZaha1-d&Knp3`IQUcKK}qQpL*F`N{h+@rtE>wVm;PY$Td5aKtZp#+kNQ=ID? zi&6fdoomOsvPkRyD-561y3=P4>O>bu-9e3pp9yO^%)fs+oYUF%d1bzmzC zj$Xn8!O~)(McWXie{E79ynKo^wyoHOh6|hL8DMgBui2IzmZZvP1cFfFCxAy7;3bU4 z9HuMQyTcCM882Tn^W~D}#Q?n-K~tVgXC@ZAXGc77jK*p_$-o9hXF`tl*eHGrC*9^X zLAOQip9&i02*~C<&{aeAu?)#xQevwyKw~)ct1}lx@=52)_3*YRpUIzw*a(}}!MO&K zr)|e`k;uirpn$dq3VP+(vh|th`)Kmm+N=XtH83F(&;)hSki_Wr6 ziDS@cFv=|c;n4GAg&V6BwOR~8Qit_EnAdj!uOk~7_ii5w*#HOOeNih1w5SCgv|q%b z@y6$COLIqL7J_j%QT_6_0?%2v%i%|0xQYJIc*jV31rsi=#JS{pw_tyN-DL#)Kc$wC?a09o8NIiEVxmP(S~$ z_<9;Y`Lf|yQlEq2do^3ZvhwTyQ7{#_F|%lsZjbAP(Z++L&Lz3szo4FH8jOv#arZ4^ zovf;O#&Spg7{qyceZnBal^B3SBro0u{kqEwR=fp5XsT`XO+UVoLxZ_8A6Fjug$}#n7)nslmRif0$Cp5~?_dtSOUo_emQkxD~kRTCl zG}elqJFhuqYj5Z+tKI7!=TCQ~{>qknukodHA-uu4&Q5Ql+)dfz*3&9?MNG(pOMTMJ z{{!8Y&o$HALpKyE+4Fk?S6caMey@buc; zTlxk!Dr@4M4^fI?sO5O2bo4|(6T~mBr2+y(Lk0rVHk4U`*C*E%6aCs*?mg=VCgs@} zywcV8HbBz21s_%7X=wTW_DGWAV+}Jcl3haqtPE|H8i5_P%ZYY-072WvgAaanCX~i$ zDg#`r7)c_0w<`H?+g%kI0W}35ymKe_52a0G-VoO#$bvk9qN5^6Vp3E=Wp&6|O7Z4W z!ClFFd8{MUN^ONfqOVDNvwVbG_HW+otiRK0Neg@cs&RJmjcXy?^QpHDSR8)er`TIA zfA}(J_2!@$L&9<@Wswop15)`GHPCtP_2xDaRi0Ivj~pAhJ3Wx|SXM4m5KXK6Gscq# zH3!ry4}7><{(?ps2837K*VPSspCo+5%=vjXQj`L4wmK3CA~kK=`*GUpZ&Hug(#3ygf2VR;A}bekt0lt6F^>=U52R z99NoF<=xydVA>mz-W!2 zEbHp4Dzo+vH{N7XgOGDMb847>N4EAQrujdI^Md72C!zg@c_3akk_y|xx~#&wS~=U=vQ^^m zgl7qP&4)mQA*lWsmc>>$Dmzq8GmI$H-N4 zy;Zy4#twN!gXJTSZFBzoWUI@X1*aF$3XTvE+6b;^3n7pE@d9QzGsHoCA@wsT2#ebo zUC3GY^}O^5|7Lj=rbQhT%NO03C8C7?CYRIx!bA;)#3(jR*G2~JwS9*w>&hV!+zCvQ}1Vwdgs~?V4zEJy*nRz2oXStEld5r5_Js&Rf`#6aL`KB$k*a(`UH! z@ytPSpgiCr2K#jyt~PB`>6u$QmBS66U%vKzqIgWfz|sA^&YJ^Azs(YWeAK|P(Mn8( zqr5QGuhMz4l;=)ktQ0xj_82gJLdZrpAn(q-=GFcz&n~L?QNFx6d(ZUse%~=NW*^QB z%f5V(Gw0z>(c!;jthQ&JM(1uclk|JzYsoa+u?^87f^cpiFyM_mA_A`F^r*1eZfV(e zL3NoCVg-(Q{bUnIl{5Q32n7VA67v!sGVvzombvum%-8RL=f0Nk7Wlx=GLx>QsM&}QS{MN60Pr8&iTYt~*&Ny_^2wj># zk)};NN^o69yZ}T_Ud$I%n{^X53GL1zCCaJspe6oOa%^git@kQt zg+BSv%6FQ5`Ze&J71Y!8P6F1&r-(NUs0w=ucFECXL27U+Y;JYBbh(H8{k5;c8$Y!gs-r9wTmI%k-;~G+8!n zMyD%X_W7+iFD$B?&Q5N%{r>Of$ zNON^A`HzgzidCFo5h7>I5^{ zpfYq(V!X8-Fd(!k9<{iNK12Q}rKg}a)0F_+*B2ewVVpE}3rM_)ypmcHSZ7IU)rxpx z5Fr;9XYx__w@6vD#C!NIdK76+$Y{L-xKob0m&mNZ-Hn!V@t?}V{@fO7UEK(Sk_H6< zPp1rY+n7?8@ef-GLzAWvO!eR4`=2h~_H8}_Cp#={b|gp~BrE~B)N2`v**~upBpwmR z1?vP#_tMO9uOr$Gg{*Iny%a|(y&@T4-74YUu@(^7s((@abZCU#8<0OQx37r`XPBg9 zzYbe`QzXGf{dm>uN&t44=S+bM0ZKXuTasOjFJLeN@yfiq;=7i81Fmi4^|iRVN+!OoIK_FH*7&Zeq!}j2AQ<#u4xM6={leRJgAX z^e;{lvGwYT>&-P~I{&bhqfBNI5xs6|=S-u`mwP>mm@M-*@$qQ|a56(&!`p0>m8?$G zWJOD#%HItBj1nbK7_TGfG(7y*?U{Vz9t+?(g87J7k8WVWX6W$WzVlWe>P5DQZBw9u zgrOCb_GibQWb@r+-ne;lRIe-<#St_QN`kP1fbeX!(dClche!F~J$rps*Ofcrcj9;e zdJ~FKl@#QE-U)fz%8pge>#{E}?cs>Ym26Vg2@ocZ`DYD%FQ&*xj8GRHQ}X#*B5^El#h%RPM~H>+I5&?%`F$z!sttrbq|2fwDABow9YUeT-f32`g${! zJO+eCm{0LcOKHV^)%}w+z;hfhjTleI28`vhM4i#8m^u2f2T3BN6 z!`Rok8tZz@re=3NPI!zVpW%&IFs2a;!ExY4>&8|LmF}Y-M?G-5s^C5pY9sIZ*ziWSwb52wEbL2a?M36kz zu(+$(ban+6Dig7zO?ihI=q&j!HT3;@vtHw)w}9+3uMSS{Bydc;Df3~sOOWB(RpE@Z zxqO};NdUprlxL6UmA=X)w!v`egbNnNm04%s0L#0A^6GBfoJ=g8G~+++u|4xkZdjm9 zeNoQr@dPqAv9Xm{TW)9s2uR_?z@>X#c|iw5OOoxrb{ry}6qg-f+wxs6ZvI&^(;)f^ zm47+&-oP@Y>)EGgBtTocOC|8zN<6Aj4jP;ilNmdZu;}aARMfi zpFIy|J9HDV&f`Y}(qgufE;9Fvr8rp!Wa>*He!_=ennPt*Q+Xp)7L!#wQX@;aLj}z7 zELa3OeFM244NG>C>-pJuO?uc)RyyEKnBuR$pfaTm=W`_R%Un8vGJ2IJF2&plE)lOl zx*n|_f3v%(2A2+5{J3Gmi-m#>tu%_O!X{oH#9Tnur4MM)@<3Cqo;j4jwnJdIKt|5j z<|+2<&NV5SSvF^TUKrgmCJ6w0K~2=|_LmMS3sOY@bz8Z}*r$z5TT1i)AJoUBuTHr% zVUX3L6_WIxWeNn)u)bK;%Lp0O#OpWL1?=S&1Jjx6;Q&F?re))g3D& z1mad$Z_7@ck6nu&?Se*UrWU-HG+?#xVA+r)+8D|y7wET3vgDBX6!%P2(+iaFS^gLK zK9ISL+~|Zgwbv_Aw>F6SJ2b65e}!-92|giC{rrAwQwo0q?^$85P`ZJ)89z!hI>sc$+dlBnQR0#zyZ>a>KaWMFh zFQR6vBWnQ=7!(W&1+AS;h~RF!vT;CjvP}^2@@S!{5BfbZDy#GI=yMs&cG;mWN2}4U zaV*THj{X-lz&l-5$pGimjAUQ8N$N+kV|8$vK}y)U0t;)WfU9pN4@w|*X~t%cm6k4$ zW@cQB`onG9VKb6M&gq^d-P&Q4UL!iRi`?i>(w0UIxs+^0iFV}{S;4Bk%NQmW@nM$t z;Px0)PCT@7r86fUvnC}>rzo(&lW{sbeP6+@NF1tcXfD z11us3fJerf62x_1Jdy#lwb7+EJbl(i&A1HW`jSe`YBe>4@&L~cd}ph%fntn+zJa=puEGVy7^|KN-VO~d)|>M85wF$#M_rV9Zz7xGz({>WJz z?AbCyj1&bMl|Vvg2oEqEpnqM82St8AM8Or4hwX4frLQFv2MdG( z7@ZjBBmBfnJNwPIu1~;{gTM+v4?sS`HqGMpUac$%Dc1{U&;Ek;evwGI zPv~TBhm!UG1vO8#0Lg31-I+1-dGmo1>xK)P@Ye)*am=M?JGlCl*o8?pZ~90|gq0Fm zCygWEBzbJW&v*DNHjs;d6+-OVcslHsyP(IfpygR?D}rdg0%4H;D98QLB4?b4g0i;{ z|78|TL;dl&qQxoFu5zOMO_Tb;tAq90x;Pu#@FyjBQn};fn@ZCbSsBU8%TkNS{T;?? zU+*{AKYKR2ET-knDXR8;%q?w2uO?!Il!dl~#o*HDoLPTf^<}o{YMhTV3m<+M<{01R z)R(-v(|W5;`gOIxt3Qu@z4+?&jorQpNNNDM3xehK>w_eE^_0W%d{AjwGSn%s@P$zy zMal)F0AzVVSMpP>tq>|sJ80-&D z;K_;9do0WAaSOgmNqI4XpJx996jf76V&zokred^tul56Z7aNUJ+{wOc<{^1I^Ld_k zF9&?x@RUG*D1!l1*HQ_uSsmVnQNlo5{Z?F?1E=pwI}rBWXr9J1vPX4lYc1`>1SkmU z9dRF3D=O`93S!7RmLpvmh#dIv!`US-;ELhus#@j zFpNfD@H_n<*+MQu%yG;pz;A(cUkaxo3zn5joYO16VP^EjoHpYlj6a-{JP&ty^q)k5 zfrj;CbQY!#YlxzIsK)}U8=24trH&uy&5N=x3~HwW33Q-{sx*LRK)lBG`${+}e zDxHPr-g%VBL*nhAkH-jUBnVA|XB5P|Lo+12lHwG68r^t5ry$>sKq_*gPP1Sxh$;oD z{=@@bSH2V=PEpV&|8o2whG1>w$HfSI4{L(FQKwN-Sysf|;>0I6%E;<%ALRLpY{KKt4j1CtY7K^z&nsW!(Q|F@)z_jN3XWs zAcK`T(fsN$QVxqo9j>f90u(s)%zH_{{jkDYF4qC|$x?^Ln@^QLm2G ztJKxiY2D~jGivp z@Rzm|K1D0O+|X2q(c#lk^Q+j#+LiQY%0zoT{%w72@67%B65n0UygR#|8sGhNp}*@; zlY9_S48Sc$+TI2EmBa26=Kq48P96#uy*t!TFcOtIXWric-Z)dVN2tfM(Y}SV14bjh zRsRxl4lcY&HxP3HI8qdNpHi%c(cAKhS6i^b#*N!_4~FdoRs1Q-RKHZ?6<+@Z?E?N? zH2{YB1%=#TjIrEl#LERCk3WiL*uC0f*|T0MUhW8X;r-s2aJ-Z_#N*D25T9=QC@3q% z&SYROnTEaij;uf1v%Wy3U{Kh4l(d$cyvnpT#wJ>9Y2Ao*FAV4`LFQR4sDAtfDY`iQ zUV)~skN^!=t(FaJQgNC28e;t0Eq85zq$@Y`i@*w8Yz25K^z%Z}`0TF;h7&PkW~Nrt z-y6v~px{82XJfUoRtT-SNufe|^TpfAvdmu$$1DDU%%*MS^soIgr7mWqO70~yn^O8l zYMfQ z;~A>g$rIRMZWWo+bH~?;0blDrq+4&^u)U31_+GmHwA=Di*5i%$^w8_@Ur>~B)N}T7 z`A&-8Z36cJY+2%ZPW19!w9f0X z``I!jbc-<_71{2Gpv8L??^LNyWZ3I3Ix5VtRAum(j(P8U3Z6Y6P(ruvI;s$>mrQX_o{4c-vY5i}l((g!cF(Znwc zBd%iw99qYO?}^poW$*5s#66X-K*Y9rn1ir@{r@~xlyD!rr$2dE=onpS%kgO|d+HZn z^k3F@V(16|RYu-c)>q~~AN*$JuL4Z+@IqHPOkiGPw07X{we<--l~W0>t*e}omUWqN zmML0eZi8@R7El1?)@SRahjF51V$IyER_yJVYYQ4RrW)YpjD|Gj-Qks;@%rr&50SOX z+RQ)eDiPQ9o0G-cjfr_OHk^mgSVs4Kraxt@{_(z(D<76Ynyc&nR6PP(XGn>U_U` z?L4B(bowPZy~Kj5o@a9vz8`tJYT~FlcwNv6towvux60v}-&9}8h@lC`%e$^AUK`S^ zwtVcy)!tXeRhZZyE9Jv;@#_38zxDPMuzP=)<*T6YgRllEuKEW07fH_pqI6Gf{)dOm0caAmT(*?DrPggL=U`@QNdL&O`{)vje5P}HdvIA_R zHOw8Yd=&8u+O&dpgoc!y@ycczbd&iwUVE7hJ)A$cEVSa_@9?fel5UIL{t21v(c73} zO?AyY$EGnA#^_uV8nne)d40DqY0{IP(uGhA0GvFeznZw@a{ZB)>Ff7s>Qo9qE<@{x zu#QN;654l}ymQI$UEWzb^W7J^&Q&_=rHmg&aLwPV(zvP17RO+>67#;-u3n78&0+kK zBAhG%p)yifBAh1n=BvXswA435Q^VaGJ;Wi{3n-cxtS^w$31P?EVl4_6u<2`CQD+uf zoIkmiJ6DJ3QWd_X9i&3e2Ucf-US>A*uTNB6Q5Znea{yy;rOCsI$Av-GsO3j@rDr51 z1$Kk&>+LS~At<&hMyw;+wG&9i$a6KVuG}(%g=xd7>XKjft}OXLB&4fBE(3DAJ;15A z1{z6Vn^btb&Z)VgF;;_(n~_czt>rerkG>%1`9wMEKv@_~2^!$dMfL!VcgF+&VBFIn z&8&`W_GX(^PO{A}(YCKd^1TZpe+2pHd*2Fpc_v9TNtnt(8PPD~4JSpKrBPLfLA-0P zYSfcSWYR9DtH%as;4t?cj60W z{+q}n*2cCu!uU+SjPuDB%g z^KlC9)a(6#cZ3nYzMCm27YfuRCZZ0+$m!OYt4IJ~#`zipMJp(=pcN)|A=IMxuLU~X zThVclIaviva`3eji(=Q`@^=6K;Z*y3il|rM}%oRSakj z8A_5tiuKrxRVyu(?0bHh_phsuP>{7{13VkGHL0ymL^NAbhS!a|Lc=v1#N{A=ca47X zk|H(kg`deH#*+~YrHISPW3q9t(Q*)c5@=ksIlo17uVGQv5tY0Y0-c)JElTdp#XasoH@EVn zT9BWdl_TsFxNs^L#m%NMf2I#wPXyslDi-=*YFBm?CNUq;raS^?B2dp0=d7A_@+^XM zP*-UlLVf99g|wyuW&c%VCcg_m&&)09mr${1%e|O6#jOid5A|AePM#|25=nhNrSTAu z=@)1RGG%SA06__NNg50q=s>ZKwl6;CShUOG2>|N7Ae#E4~M$!&s`twO*?Ebp$rh z=kB@;xKg3BN0wIw@Y`mfBbzmxiP>Sr%+ae6PeqCzm`B8^#St?sv`=l~cJiOVXvT-U zycOcm*1w>t$aYCA4Jx5?8iv_DM+?o|E+N(6pazK12iX*;)Wo+z!bRJ1E|t}lZ(k(( z58cXVMoMS| zV16&R0W`~koSL4#9=)g;bJFtm;0L-%d5LzO{=H7bqd^`xc#r{FweSskum0Af{aGV# zu?OvY)*fHs;}BXY72D-IEzdogI~@kDT!&MQ8Sw4si*f%Z7%^uF@s~-Z`>=Y$CCq2{ zi;i8Y;lwRd>44O;wflh?fVb!Oq>{foHSDyg>4>X%?-A9!I#0IKJoDn%d=_wViCTXs z^%{`;h$pokdlMTUXc#)OM3t=G@yH;%h6rdCu5>i5#DZ^~;-1z!yZwy)@%CSfU$hev zi9y`N(-&=Hl7w8TG})bcbpKj|jgULP@nLV9YTw460zWzwUEP9(i`yV-K^@{8-$b1@^J0Y3HVjh?^lup5+ zo_{Y|3On?VOvJ@=RoU#&dIM!{Ifo@%_E(is! zoXfpEBHM0;FD1U6*W+Kl{cOXe12?*K=a9MW`6-8AXIt1qCE~ZD9@nh*Udg7jszc`V z(0k@>R0QT7piDkpsyb+x{9}NL?d7P)32wKIG?{OFHOzYYrk80d?KECtcfPRjy<5b( zaiU+5o;fB%i5VN9B@U{7`9ge4D85apU<+Ib3orkf=BK~-WWheV>&rR$-Ij%}Y^CwD z1%{+U+nNUA^Y_|o>Wb*>yyiHQn&+Ogm`bHy%dwJF{3sXNBVwiV@@t($^qNg0Kl!r< zq1X2xBZ0-kIgv@DXxrDEv+08?MnYE&Hp2q#&KYlwLN6DRm93Xuloy=1orioY-sIsU zN&L!qa(+O8Ix;}(lqZxIcFq{i<*NyQ_uhZEB_RJkFmUF}6HIKzA6vR-$q{`q+j6j~ z>St<4FWEo-1>G?>9u{c}w8O<_1%Kxqe6GUR#g&&s_O;BF{<hq9n3q!ap?_wG*Cd>m9T05;^voSQ3^_f?@#X|iw z&`jH5UFYpw;rrM1RAkH~C&Tb>SemuAa5(ceddHd$MaWrO0t^BVnfADXXA?|u$vSX7 zcn_kE2U0Edr@UY<)2jFDnwz!L;XT)fT*2Q;A08Pi$Owj*+(wDH?t1{`Z-;*Ct(^d& zdNu{~!-kTBpU>}hFN7|~1Bm6r2Nu0~bw#=&m*A&Cj@asaJRSD+H*A|ll1goy+F+O` z&1>0p8N;cJs#8|QG88*-x~~OCaVbvXwMvJ!T;*E2)BWgZjvl-u+9v~HuCn|S}x-K?l}VzS~1XXq$-;v3vB+YHdlx%I}`di+!p{t|^6vw8~GAmXH$ zh0q>B8^GUJdT}1}%&UG8LCzE^&5dCLlXrCAFZCS{w&`?vVr;Cc0QbYdy}|u}9tHy= z_5Z1${TE!|-`^jRBS5xkW`7hMUBiraPj_M4c`WhH8eV%TZ21=yAH?grAxZ;uGLpVk zF5a;e+eiYEMxry%znd;;kPI4c$lML@qCGu=suC8!AP*&KyjGjRs{u!$@YIkEl9}QD zr6zrnxR`=e9tmLP1fDJsQ~5v#(nUjOy~cBK+M9}{xf%g%%flvqtOExY*~I9CFR1gL zOy8y=?O#iCKfeo#asWceB-sX-1id)*^zL@^MZKtAD@nBzoQI;lm$QS>OjR zUKRzTp_&7fL+6oFCWnh3h=I5_;D6}YHRn8fceYS2`Yb{#aLAuZkDcm8*oewwFPOCU z`?8?lB#$)OPxpr5sfZ5KwwFKfO2cTouDT)4g5y#bvF^6r#W;_?FUa>0+tCo|l^W?@ zEn)t<4wY};&7|0E8jaq*dIXHX0Vd9^U{st%=+~i9`^*CXk?l*LT<{;lxnkLwt(1we z%t4P0Os`W85f&s{>|}09;bw17*PYZWCc}wf+opZOM_db%`?ysroA68LHJ1dsHu7zd zXvyCEr@R{$>aodvthYV+i2ne#4aWms50vAY{Yb3c>uSr%6x;9@l>ECoaHc)B)ZV62 zOQ>a&2G59P_3O!*TEZ-tOA45p#^|H9&JPad&Zu|3=Ly`Vq>X2!YE)sY;O-`YV$25F<( z>&>sJAzMI7U45+PqokG64f&U*YvI>QOZqgk^e2UD%GhIv<~C|;>f;MD{XYt*#z_3^ zMuCWB%fN3;HW7G+JxqTC`eC&f%CJ4%?OuVmkonb8{>#PTq*y8L(yzULL59bG4{>v) z?l0&d=^J5+AfpF@-Tb3RG;iq^Zz$lSVYD%tGJfxOY0i0~Opjg!;^r$j=k_`aX0+rd zQA1Oo2%=GuA>g?m;L8dJWahEvg>W;UvO=sHePHXJpfsD=8+uF;<{PO?biNK2v94ux|G0A( zAm*lNeH;vrH%)5a#r7#G3N}}@)lCVlHdkl33p+mMG2~tHQM_L27au9HhG zfcxMutEiRRc8^Ra2n1Ib4elZX{%;An{5!RbKpS`oqYD$%E-H>T#-}_mfON zM^AI*8-!-(J|j+hHCk`Ua5i9i(?@r4p2Mr<-$j<2`;XE1@29*ErPRWenfZ47@MgXU zYPUf?*i-h3pFliD->oD(@vV?OSM-mjqOqzv-Yc=UHOrVXmncNe_C1%z>dH+>tP;&` zrcS3sbhe@UPdPErS1{@hes3F!jx<)^IWlfJo%;=jY~L{&hjs^j7nGK~z}JGHU}Wp~a_YDPN68#3*dv^)j-# zHsN^L)ZS{WztBwY3$iZ}%MabpUZ$PlK`SM?_fD^-2bZLb3C0xZ4idLG(mS~pqK zWTiS{oISt8Wcu6s@iPg*3(x1S{v=gZCeH^e_n;NP917&Zby%4)ur9n_FFV7%goxxw zjt;Qkntze+Xg?4dWYtwsC{c82sj*#_G8}am=mB{{nUpwpRv243T?zD4cg>#TkZhG^!U6>%OBOJZ> z1x@-}ML17*FjTy;jX20|eeW-b1D1SZI_cmV8yn3!B+mPF?-9#hnvz}p@YP}QUl3B~ zefa9@Bi$rjxMf(5w$aU;7fwWdn*&Fg@ri#y)}P?75(ua9zrf}oZMqhjFL2AtvAtow z9uU|%*j25o72Jq^Sngrt%&s9h!a(|`B(LU}$E#w?E`;3t3NUkpdJ_7E*L5^$81~Y> z)%|*pj%F?g1p}BLlJlz+Hv$v($pUn$JRN7gN~XUz_@{N%8pKE%|H+rBs)LRsK@7-j zJmNo>rk!6HkQ}K}W&<>~kWw-Y-U$Yx6Ue~@?wRQR6w2Lqe9K@%7!jQCKI0F4;<6>} zb_kjPt%^m1cm8!&%jBh zlak_)7|%H+qg=W{$lMDFdB;8IqiLgM#_{`TD!IL#>4UN^)_9S@pjFz$?$xA}ezMxQ z8n2f!dv9C-**D3?nh2)_yML%v2BkNDI^M-$d!sz;z5<=`XuS-cH1O)7`n1tYAL=+@ zy2)d%cgDSz6~6}+`mq(G-6%=HtupS7o~oL^KlyitI-WgdB9k+^W;6>girVS{)A6c> zlGRs^=uEZqQzl~6oi^*}{z}^Fzo% zhNbr&o&r{`v~R#>_w#?Qcu3l-ulJ2nm}m;z!JB>ZN^S6#^%7K=Fh<}@G)LFSz0&(A z+233YbY~S=HrwvJHJ>m^q7MKY2XSJP5P3n2sI*kaYh6MQDcRQbJ|1hZtGUwzphV}1 zJj(IU=9h~D?@hI=zhxi3WBZMqk7!#S-6s_d?j5DWCVdN%TD79KgQTUQ)I4rjSt)^t zUHEQ;+h@iQ#E8{21RX(mMY#fRICT%@o6f&HJzkk(_#wb;WHu7lopd$t*;O(yxR9#w zwBUoqEOtwIqFDtk*v-_+i7HA0aNX%{-`TbJ;!-rQ)M1m^KCtw0(_d7n$H~rbgbsQab6&q>yIR7sa`O#Vf8mv9U*Kr0Y$Ux8qde3W9(0mMFLzc5q?2%T?~Z z^gelbINHe~!u=!uWqcU-o4W-S6TrNPRQO%O0O77v?E*JqZKpuc5oyjX^|^nao#5XJ z32&o{IT@BW3)8nG{%*aEC@%LN%!(dk{mQ%zd~lWJiu7G_#nC)(CSDaZ@}FmTGQtbc zW*>#yS$RVQcfsV_;;`3UbI1u?)rK+`;%*s6b7PgmDF|ESlsZ^L~G zhXg?FL(kh7@yvnca~4QUY9o`pNeVQQ>;5m1X7O?>=W_!|iOx;mN`S9pyy(B`!-!l> zWOcD7Lfc2w7g=hMP8AFZe1G^L%(Uth*b{2G`4oTxYYe;}hSS`E)-+2;bBE5lRac-l*%a*v#*Z%aKe(oh1 zH!h>^`QUB#*;vkCn=~-O-dwtS237IdKW{}x`W=x+?SjP}G9|exP8CoPGwvl}TrTq9 zCR*<1f+d- z=JPi^UId$$UET zS4en3WIH&tv8@-$Zn_f?T&=~fPW+G>GztI=N&wyH`*J?IzKaeGD?a%+Lm+;{<%W~Osyiz&XIZ2vSh@(;NvNqCt->97QH=Y>S}NmT zfMB?=($bSISIX|(Kq}+;lY)!SBmHH9qznen#Qt+|{NHaYy4KplR?#YF-SzKk3o90T zi%DmMceaMjSJ=rPsL~%;00&A4Lmb$GT!-V&;B@w zc0ohoBv|?4tJn@W!#G}^T5Qitkt(NobY~toJC&>dt1BM_sjRgK_RI?}CzbC^ z&sU`TM#%0T-KA5xo86;F)+Y8Z$#gG{0FF8D1(R{7?T#o6zg{!(o=}xbaoI{ECivGY zy*outu{{A|$ip*{M_h+HHABPG^O+6t&sJY8uC9I=T?#^vP5?VNtFVKULwsH;H7lq3 z?aN!Pr%eRP$2iLQphv%?z%kY&6KN|w^Fa<9&ns)SZ|ah=IzIv*&pjGZTPB{Q#5%4b zqu-21&6|w}J$Cj8E_2>iXxMrLxLm2S3y{iK6Df5IBM%Cw>T3jbv$SY*ke*Yh#B|!> z{mFRoK1Ai{S_TD-`1<_WsJdO>7m;iH3fFD7jBL zX3sP!E3cZEfqO3b;*dE*5YY5LS$G8^xG;ypLpHA#3sPn_hJZXtzvlIF(qjj*Z?)%aIj}Y`Kx{#4q3KPEMMIo?wW#>-H%OrR z4Go6(Fy-99LtNK$ZY_{a-~S)&eRot-@499X6$LelG-;6z0-_)yEm4ssB2uJ7R63C& zH9$xN1f&ZH2neAgQX(bv-aFEJ2LWjbQbI|b?eCnqbLOmj=H8ig{+vHpBn0&`!0ooLi?^ef>cRixs z%Pfdn&0}F_m;scYMU(QQWZr%Vv|-CYRp+Q8GXP@O?ds{C}I-h4`Y;*Bx382975y-a`>~;XZe>R zk;pOc)g{xfKan>O>CV(Rpj!O}e2hN(`2ghCK$D7Va+su>Pe6BJO8LeI=I_>W4rQpD z_DZs7p6-5B2)1yX0|P6E3EBYDVB=EdItpd_so*fNR90K&U=rMai>yy^N&Z^zMB6YV z{yF!8-=>;@>D8nHlvsM_Z;=9k(ctyJ9Fo zOkkMQ49s`s7p@317YU>3dk3Xm=ajB`m$@B`yK59@3@LgLqa`2T9Y|n2Twe6Lr~#yG zXn;>(->H%f><6C)I^5uOBX0zb>#$=VNgg*5jvHhgnlRL>P^JN%oswzJPGLdj3Y}*a z(3NY@rBT;=JIy~5t7KRO)9@012S)#M^V7$Mg4B%|+rIHxbwJ9;mV38M0mJ_KRW!9U zb@0grQX2uQOVV6epO$!5ij0-k7kUZoj}OXT$XS#m(rg|dUpE30l=53^(#LW8pU)XI zh8rvA@!IIO60PQC$NCjNg9mknpjiSeR1!jkj2bi; zw6eY_x^&?h8x3ILz16oNdu+0XEfMgw%d_8lhK%aJ+r=g&4x)h>p3qtd$g#UHt^y&) zcM(`>l>1BE>Zk_Y!$TLE0FE7i6Kgsp^@3o7XoXo~CL|t_)2eB#zK$8E*{ZRIxe)~(-Ll?6G&zcX(}9P?QNyfFt~d)Ao& zmTKj5{KCGs;2dFSFiMr=_4P!G_#<^bUx_Aof5MFAAy{ti+1WE;VXe6yjF}mBcLwf$ z&+YRhn*e_aNomB4jqK0dAzFm<-}6FdnRi8R)!?ki+AfO^+$uaV8hul5 z;CZD?w`@i}`~`*3zf&1{0g`=oI+l=GEzBzTNYnqB3~lV6SQ%N;I59+-BsDiVZKhqW z63@fWYu3g7#sm^xZI?x&X>w0!&2ju@G}#Ilx!+6k_(Ymq0ej7%v1k=?QzWl|Rg{}q zFfwNjz`i_IHg6v*{+#82^7ME}cHuuIUMW}%t0*|$H+Xw9u%K(YKoi3YkY`3a61-j= z(R=Xau1Ad9`5_&QI9evKJ!dTR=I&$@Imdx|dVb))s;B>Pyq`7CzwzzHEhF{>SD*(0 z6C>Xe_(m`*CGaCgcg7_w2kKF>Vsg+`Y96Txe(G-^e@KJ?A&VxWU(tBLC9%edA}lLt z=oNJ76wblPi#A0jB7j0uL>%%Q&PsXLun4M&E7Z&K&fUSo0^W$W$C6ZU)BuYop(mK zQd{r=lzg8f-AuGTuo*6Zu~jXpdF9|+%U21rEEhK>p;Z&f#_9Ygr9TncSGkq<439-PqgEeSMwkCL)aaQ-NL_NOIS@Hgmfgs4WjXbhaCQ6kF3V3vwDbuLVJ3B9o6#06H)^@s`x5quraqh!hTTuF@+#EGAiu?ld-O{_2Ohs@0ZXSd)W6C%|r&Zy35c%!7UA4gnw6)z!zY)(*uVK6gd% zCwWL#uWIQ}B2LL%fe+bNWErl@*tPgCsmbFT8C+gQ`oY6y37Q&XzT5K!g#sZbQnr@; zH}#7>%w)k@7eT1gp|=MC;8#-|`{dG)kT8EM+fzqpt#+z=Gy<6*uk4NVoOQxB-@s=1ti@H4`r(Fsc-nc7}-_jU~J)=^cv8>;_c; z%#Z`L59wRG&k3!pO`pRstwH(w3po3<3oF96W{|zKPeNHBZGZ|SRm=y=uu&_C;vtlU zy{3JS58+K#8CvO#V~K^4=mH@A80b+CZldH+=R#3f*B;{$&_JGcA$@a&1){|Xj7v=v zUUx*!#Fj(V>G~j=Zv0XCg+8Bzk<6rID4r#BQKl0}A|+w*FPxO`$^ z%N+;Jn`vn1%3a#~CADb>Tssd5ZH?Gcq}6=c_=(9vOSjg~YqQy7Wg6A6(R+X|zu=aa z-iizK3bAjKaiOEyf=H)dQ-R2u8kGt>DB^db=UrpHGzPlVSYoHF`%rxY4wy;s0E>PZ zP0Jce0Viv)y5H92JHoAAn`HO-efpl;*0kyp0fa;J?1--zRJz#%>djZQ= zPFcOnd2L{RaCg!ikAMGClSccehVvHUVR^O`ze}qePK^I4K6 z>;h(9lz0&vv*>WOIjTKMtk2BqAl+{~G0*`8p2tN3E7E6pk@t2R?tKwTk62S>j5K;& z^~)~393H1ttNr1YXWO*~`5-DURd9Rl*)b;Bu7$MR5Ieu& zExh=J(A6&Ih&XZ~1rZJuNU8IQJ=E}`@SV2z0n@1glkr}k zn8@OAiQsXAm|B2d%BNRdp{8PJV6MNQ8EbhDq+M{6r_6sj>H)j8l*H}&Q+2X+4%?Go zM;lo!`uwX!QAH-@LL!A_JQHtk*8yd(Obdw~S#O{)RVzEk#9G6#Pt5g}N^!Zy>y__D zC)y2x`o_p5!lJd2ks7%)H7l!-XUB6 z;ZBF}nL{OOYUTNeCU1=ey&zGS9w^O+_IXv!-^LLbrw_yuK-usH~6_YQmQ=8XdMMgB%UK!fXXVdbyQ?ZEu)I6#?e{lMPFBJZf9!RvLJ&b6E6M4uP zsgX+kd?#YSXY_^UFkvQfsxw>RJDT-`YOq>t8NP06n*CKwm{a1kz!;?3yT^0(L9k$+ z@n^hckUsG4BY`p^u~_$>72ZVVo&HI0A5-o-4BqBtf)_ z@z^91cb=7eXjJrN3(<$Mf&r)6f-RG3*^gT38ANeqSC(df5rmrLNw@oo?8US#cX9eA z5@_~4i3gkgMBeTm{NXPY-XfN|i)X0}G0UPX_P^Xu10fd)^hraIa+G#gfy_41!84b^|~oZ5i;MP!d-9W(R_60->NcaW1`5WZ7h8hT;@AE&B6y>Xc*;w*%7(B1Ca;YM?juKdV)$8aMAO2Du8#^k z=sGewY5hoxG)aIp!8uW2uX!B(c$=JrlCE%A1(Idg0%oi?7q@A2ECTrD7!j9_Ofwmx zZ6vBBlT&h!E25>>ce&rnIazJ6W6^Qt+WuFzWNB)l8|$H04BY_u@jXXyxH6k_ zpFK8HH}-O#%Dm@EJ+Mgyr?l6VjZ_ho7C5AIlGS>yv$JHp4)xMo4TuKrWEql?Yij-m zj@KE(aQ&@w@QL6J`Li~s2hw5;*c{#fA=wYe*|52FB+zB-jK?oK&VUoBYF(CGv@t~i!wp`9Y4|-U37?pd3}zX%{TdA9(ay-KXbFDcwY|llca=CeKU~*> z^Q*l@@b27FhpQ({Uy>+Q&uE?X^N0DnyL_ZN`9|9F161~((Ua!A%8Ew8C zJ#Y)2btY%a2bO8;!pwhz#&E6JQh}h+7K&KHiVA_X<9PZ(-0Qd$&X;)YelO-#`5qv} zV#5LGzFLM*2JV1xI?S=>%*W81qSX=-k#Af)11sg9Yn=T0>9v@N&a|nt%ZkXJ92=Wx zvaquE6|~#@&if{Xg%+?WzSLO7b>?b--E}Y&bwBg-tfo-$B)9xu?AA8YIk5}c3qp%p=4M#*S(Wx0mmxs3r>a7_YQbn5kKu2X9CauP&Ho zo+^!aV`IByse0KbA|Hf=1fnD{3tf$1OCOFl&j{e9=SQTO zYWnugfD?5C`d;?C2S?2|-d_qXjXb+Axvw68b;z2?tSM2upmOVLI_Md-OU=zm<-&l0 z5xxMHk2k)Cnlah~xKKuBMhdtI@F`|~cl80eSWcDwZJR?bo!WH0TO{Ze>@w!Y4lZ!! zdKKw5@zVOE;SLk1$7${81Lc!npS)iD+VIX@t0ReUCX3Wr&z_(8(qv5XRD&>X|O2S08 zw+8o{PxnJ#1#v(8lh0W=si|0C_| zA0N|<`oM3apf4A26K5AlC}NxQ8z(S4=D~BytD57s${mtb!$fUb+r^7j08tth3Iu}T z;wfO{x+)2IRGW(~nud3=HrP3wTdtUDE}Xk`#z7sb+y8Oq%h61^7zfG{AanW*&)c-7 ze+iFu%b*7ztN$ftT?y!G1y5k~z1ix~klON65hlgKXBoGt0q#u!8y8Y z#v`3{#qcK@PR>kNTZs_^joBBlcjia5WJU5vARxZ>G&wPL*04^8=n(#J86Q#}7-KuO zWYp?hOkRwIre}If%;V_RI4}0wxrEU@De2#>dOm(J>7y`%W`u=i@KC|K{KBxwDFAlP zx}NrTGUfl{cPbfzjStK171%&V+FQBbptf166B?p){E6Cpm8j|@s5&U%o-}*#FB0qp z@G+JioHZx#z5nnzxf~#MX)&ICE7$Y@$dbN>G>F;VK5s){Tsy8_sG-&v6uC3@NcZly z$+TVl4`MsH$BI-BcUe+S{lQWHI@74gX-&1;wCOo=RQDl~aDoA|0aS)`g=Pj0{!Eb) z)7jmd%$~foz(;xb4d{)tYx(`!W=kc*!z{#2lrzWl*OrkW$gb#n{vs| z$9TAsZBR~ZF|JWaz(YeQZ<;I3SwX(~x@ zUvxop>ARSCJ`dCd@k)s{PI*C;DHy|j!(PodZWh_zzSR=@*~PONt5W*&0#V%Pui#yr zGmf8}xry2iEL01BSrZ!3eCumAkxOmf%8XEZaI7%D9k{afx?f{hX?D#-(PO(_HlhS2 zIsjXXkA-#gkw#N>y4)ncX(}@E{Mm_+lig1yK8p3!O-y~bQHL;-o7d1WWkB<;5DE_F zY2Nj{L!Hl_=)jir*4bd?lj`q59{8BvCi=Mtp`{FLtbRN~AYMSToiVG=cL%-zgf)<( zu#F8v=weB~;-1%GU$(Xbt>N(iNDms(nEf8n-+JEdopeVu# zd1B+U2rpR)kxOI)Uz#iw?`{0yYWlRlQEb5pLwl_8S4Gw!X>maA6XDI1$30FvTfJ;$ zgo*6&&CMm(i_4vJThZ4VsI7ZFN4sag)zwdKPgLzkL-|MTLP*;8<0Uy19QmbOVXONo z#~;o^rnT4MrJZ+!x8He~nMFO_`k~=}!k0!8P%ImegJl~0p*Hvj544^MzQfs>f_B9z zGMfAUi&FKEW$XX$aXe0p3N{G$z%A1yK zO?8*wYhsVL=e1$`#UIoDLwN$tP0m4`Pc6=1j$Wt75yl=qULo(-pX7G$ZBO2%*sZ6L zoao;4o7aA7M_1`VdTSlhfM2u9M!-wrnmKF?V5PMU*C=c(S`xrm^`dOun5km%qM1&x z0p7&+XzPyFmi<3UP zB%(L2y((2Tt5$mTtXnJW`AP0t0-S>UR9pJk}F_K%O>3_HaiN`q?JPzNHr?u?| zuJXb=&^P4t>Wf*{R}Byeo@_=T3Dmgoks%dF#5%XnH{6rn4$ppB;qTbVV6*Oyh^7Vu^hYH`FiqIjLTo%9DSvZA{a)4lg+o9X_{ywMAfQ z4r90@qycbBfH=rRob3Ey^HWsfI0N|pL+rXK&h+;2&5o)@nXmgsQxt3b z#$J*}P#nY`xYP_jGH5=oQM?5;3k%3UezNjK{Sja<8}u7Ag3DU}Ar{~<46~N)=<6w6 z*wMmlrvQkptl`jV=sZv}JTzLjL%?$(Is?0<+Idr{W?V}~ZR8CMa{tJ(ShZuEX*#C2 zt`$O$g3Ywr=v^{eL7V1`9S=xnKVy{+#$kcc;VQYL{@{o3vOfv$vq1 zs=K&&G!L>#lhnUR2fKi@mr8C{FXA)yk=PY4OY`~c<*ZvPh|;MgMSK(|hdtzWxlLAo(Y8jbp z@KZPxTil(eJmd-qL)#C<9?*|>7aMgF9#;o(zsiHY)2=XXTf>W@MCSnMpGF%e{p~jX zl*-`*;*u?oM_Y;C4@eLozkQ3bb)ZA$r{Ra)k)GslumK~N3o`@24DLhq;IV4*prW`? zl=bkUh0Dtr3+dCI!QSG&|Bpk<-&dc%WgN!yhFo~P)o?}0;L{vPCc)bY#*|u9L9$w~ zF4-XC^Z3q}r2M+fJE-ru#>3{hk0@4%OUA>;l>uQ-wFgQnx4{EKD0(_lL;*sP4LTEIvvqDv zDtRrNA+fEj%6=QB>(6V#hZ)S3o|w{Yc$zVXzProlIJXgqC%n^9gRyEncz_gMKv%a`RP^3IoUY~`h8G`!J+ z&KQYJxV^zdiS~F8yR0!le+xFJ%)c#S$jMUX;>dMtoiM<`i&M zts*eIPLGXym%SC9;)m^c@ik?^TN4lP^YPTVU`NCXBn(!3(vG?7rVFY7Tk$#I4~}z`7p{sGb8)N;zu8X81bRZXq1H)tp(3xQELq5?q|e-Y z`)UtjR68l!B~HoG>wTtr{Ouer+Wnd&J3t~Vrc2#eX?js77 zdy6INE)X8&yelve{@lv^-jI_g_CYv^rA5@a_B?SUZn1`2b{hHu4yi4BCDl0H1YV!1 zyfHtiliaI|inJVjlcQObEomf*1>78GIhb7Y!I*V#E$`0s$gG5A>KVj&h znunv&>ln^z?M0)yjXqkNBEyop8}BdoIL$t}Aht{ziOd$d{3CLca+~CzN@5-A`fB=B zEi|(Pa|gEz|B1v=jmVh*N}h;>x&AxCLR^EtdP?ke{3$1WgPhY){1}OOUuy)Vimc2I z!(|k!o^!*i%5}xd#~;!iVgJl54(F^Xv;%`r`(JZ?!<*)^9DY73b~W2VWZfBhYBTI$ z7DSQ)ti(uq7Au8DWY6c#qutflw<5#4wudM&YeSm4&i~j_aV8N$7HffU<{dVhjA$*w z;cHb39`k0f?)&7x$y738lMFU2CNf3Z0;3#sW`&} zzfDTNgOK=?jV75iwqwXOg()6#+r@q63)}pG%v1{2ku{Xkx~7pzlZpQPsvOISVk6+1 zpIZ&P3>i(mbjYuBzjyTQcvc}nm#eMt@IcXl641 z;DDqTiVSH0YRx-K>%An-Ur#6AR9e4R*mbDHP^!Tjyl>b-jPuWzvzzx;epCZlJ&0uKGW%%%wP}hS()ej)B>)XD?W`ad z@=;M20v6!19PV4&q*pa}YmEtI?@$BM4Z;jbJ?xF(UaQ5fZUJuUxF!d=M zSC;qgu{l>E&L_;%U-yc9W^oXum{vD$e;eWcp+?vvm5+M_KIPN)|DOR zkUC2&Gl}N8okl}kwC&waC*NywRT-S`T;A-SQDB760h55k)k^3bxwL-)(`^Qe z@h*N6%d%opq!cBV{)J~RV<2|5U&or%jUHyrR!A9Np;X=bl0nX@R(18q_lv|t9E+<0 zsfu?AGm$*@b~joQhbFmToq0{!6O(2gcwZ5hoyF&rhjF}K%$pe~Ge`tflvJ`gcxS!& z;`a%gWo7biV}T*Mua2SGSs=c^B=KHi&QL)Ywt)e=2Dk8XOG{9F9puW}heZh-bCNLX z*2|-K;iOlZ9RXLC6aa?atD5X4G`L?5Vk=r=a&z)&V7c!tI&Y(#6ZO~qI+l~bSf_RQ ze_^@S*0r3&C{@Sn10|FBO2&(Jc_P0-$uy8Kkk!D|P`xse!oC{Ow7lbHv1DD~X83tM zZkzW&67W-+1;i=@MCn6cCR}ZM!x%ZV$FO7yUr^HinH!c%x^O&L_*mp{Wo~<8HWxXw zFsn7X5cl}IIGdSo@l=E&O^h~qk)$_n=fA!`ZGS~-5G^)SRP(DY zFh2MIrSm_9oBpxL{I?z}OIEwzzN4*F)ZBC-JjHMC>>sG)pcxA~IltiB=bU0R`DEI% zu>0a~Q1~2>mlMA}TbLXpE4w;Do{l>(s|vEzB!y+whNt-$K$sEmBIU5%eKQOGq_5Z4 zmV3?2NcX7s3Yedhm_%tGwmjAqEwxB(Nlx3escOfUB}@?iEY zO4Wj$x^caJR~k1c+jDpDnMs-Mw<%mke<0*bfu1hFP7AXMF#!w>E#(dGomT#3tohOj zo4l(xc@(!%3_xK(g2Q0JYmI#TWJG_F@TVFw;L&jt-I4z@cE;0Y3$qwi@vzPP8CCyX0gY)?Zil^xG8);Sz78XdCs zm6J3UjWB!3S4U_DdL|EP&n7C@s|&RDT@VUKm>53NScp1Z&hi`Nb=JRvG`bLTr2&!4 zUB;GHrcXoOy^pKPB8UZv0>cy1{dp(`Z*d(sJ=;W*gI>h84fFaQ{sZQ*ac*IDNhA@S zcq~c+cKPCjq~7nTypGO6ZKN4Jog?Rxz*|sl~lVo&0s)O}FC(^C!i!oJo_`I?r z1kgfeC9!FmR(g*e)dQCQ>UMYcJjadwfP!vxBJ^QtX4&>e+rIbg$&oQ;HtRQt7FO)w zWndir*Kxt?I7K59$TRx{Zy@TU6ii{{CSi0jRhe+9f$_fG6j}z|ZYUj*qwOf4S|^%3 zDGlDuC7g&7liH5zXc08>5*F7fuii>~3YQ1YVIs1RYZ-==k5`!w6TX*`lhEA8E`%lE z-)ABagh_lRE77xxGc5*Ga@VI{bXb`ofCD&y+P`{zqzAX*zOZ&*0+3nP^)F$_u|7M+ zje@w04cM#okrxgqZCbH64wl^*T}Wpo9c--cK?KB>csBFR`f?jP=6nB(Wvh;l@tpYm z-o#@;(%H7N2*mm?AiSCRHO*2LB4>bdmIPLP@f``*u!OY8S(*WTN3>#KXI~#+A^M^) zSEcRkWqL&R01(*HWIXY@S~90Qzdx*)n_}k!Pbrn*Ov?Piq{~$L)@6rFhw`vsAt|sO zrJj!NM9U8|UTkJEG?dbIcDfcg_jWX86<+v`t`Lu-R~0)*D|~zX|}R7vRulbBKAoYs4Wls#c~ivYCqa@l?L>k^v2Ro*?v*PxN0f z-TryC{Wm{9rDX@-is(#I1WCR64RS&2{LiQS{cCD}w@mq1@^7km|E|jU|6k|dvjY8_ KKS#*#@&5sVE`mt_ literal 0 HcmV?d00001 diff --git a/static/profile/js/profileeditor.js b/static/profile/js/profileeditor.js index c1443e5cade..77a1b205d7a 100644 --- a/static/profile/js/profileeditor.js +++ b/static/profile/js/profileeditor.js @@ -85,7 +85,7 @@ // Fetch data from mongo peStatus.hide().text(translate('Loading profile records ...')).fadeIn('slow'); - $.ajax('/api/v1/profile.json', { + $.ajax('/api/v1/profile.json?count=20', { headers: client.headers() , success: function (records) { if (!records.length) { @@ -681,12 +681,13 @@ // This is a crude way of preventing the user from changing the inputs whilst waiting. // If the user was able to make changes, they'd be lost when the done callback redraws anyway. $('#pe_form').hide(); - + var headers = client.headers(); + headers['Content-Type'] = 'application/json'; $.ajax({ method: 'PUT' , url: '/api/v1/profile/' - , data: adjustedRecord - , headers: client.headers() + , data: JSON.stringify(adjustedRecord) + , headers: headers }).done(function postSuccess (data, status) { console.info('profile saved', data); $('#pe_form').show(); // allow edits again diff --git a/static/report/js/predictions.js b/static/report/js/predictions.js deleted file mode 100644 index 336483a0dce..00000000000 --- a/static/report/js/predictions.js +++ /dev/null @@ -1,40 +0,0 @@ - -var predictedOffset = 0; - -function predictForward() { - predictedOffset += 5; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -function predictMoreForward() { - predictedOffset += 30; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -function predictBackward() { - predictedOffset -= 5; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -function predictMoreBackward() { - predictedOffset -= 30; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -function predictResetToZero() { - predictedOffset = 0; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -$(document).on('change', '#rp_optionspredicted', function() { - if (this.checked) - $("#rp_predictedSettings").show(); - else - $("#rp_predictedSettings").hide(); - predictResetToZero(); -}); diff --git a/static/report/js/report.js b/static/report/js/report.js index f7b02ec44af..9e171bb8730 100644 --- a/static/report/js/report.js +++ b/static/report/js/report.js @@ -459,11 +459,14 @@ if (loadeddays === dayscount) { sorteddaystoshow.sort(); var from = sorteddaystoshow[0]; + var dFrom = sorteddaystoshow[0]; + var dTo = sorteddaystoshow[(sorteddaystoshow.length - 1)]; + if (options.order === report_plugins.consts.ORDER_NEWESTONTOP) { sorteddaystoshow.reverse(); } - loadProfileSwitch(from, function loadProfileSwitchCallback() { - loadProfiles(function loadProfilesCallback() { + loadProfileSwitch(dFrom, function loadProfileSwitchCallback() { + loadProfilesRange(dFrom, dTo, sorteddaystoshow.length, function loadProfilesCallback() { $('#info > b').html('' + translate('Rendering') + ' ...'); window.setTimeout(function () { showreports(options); @@ -651,7 +654,7 @@ if (!datastorage.profileSwitchTreatments) datastorage.profileSwitchTreatments = []; $('#info-' + day).html(''+translate('Loading treatments data of')+' '+day+' ...'); - var tquery = '?find[created_at][$gte]='+new Date(from).toISOString()+'&find[created_at][$lt]='+new Date(to).toISOString(); + var tquery = '?find[created_at][$gte]='+new Date(from).toISOString()+'&find[created_at][$lt]='+new Date(to).toISOString()+'&count=1000'; return $.ajax('/api/v1/treatments.json'+tquery, { headers: client.headers() , cache: false @@ -709,7 +712,7 @@ }); } - function loadProfileSwitch(from, callback) { + function loadProfileSwitch (from, callback) { $('#info > b').html(''+translate('Loading profile switch data') + ' ...'); var tquery = '?find[eventType]=Profile Switch' + '&find[created_at][$lte]=' + new Date(from).toISOString() + '&count=1'; $.ajax('/api/v1/treatments.json'+tquery, { @@ -743,6 +746,69 @@ }).done(callback); } + function loadProfilesRange (dateFrom, dateTo, dayCount, callback) { + $('#info > b').html('' + translate('Loading profile range') + ' ...'); + + $.when( + loadProfilesRangeCore(dateFrom, dateTo, dayCount), + loadProfilesRangePrevious(dateFrom), + loadProfilesRangeNext(dateTo) + ) + .done(callback) + .fail(function () { + datastorage.profiles = []; + }); + } + + function loadProfilesRangeCore (dateFrom, dateTo, dayCount) { + $('#info > b').html('' + translate('Loading core profiles') + ' ...'); + + //The results must be returned in descending order to work with key logic in routines such as getCurrentProfile + var tquery = '?find[startDate][$gte]=' + new Date(dateFrom).toISOString() + '&find[startDate][$lte]=' + new Date(dateTo).toISOString() + '&sort[startDate]=-1&count=1000'; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers(), + async: false, + success: function (records) { + datastorage.profiles = records; + } + }); + } + + function loadProfilesRangePrevious (dateFrom) { + $('#info > b').html('' + translate('Loading previous profile') + ' ...'); + + //Find first one before the start date and add to datastorage.profiles + var tquery = '?find[startDate][$lt]=' + new Date(dateFrom).toISOString() + '&sort[startDate]=-1&count=1'; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers(), + async: false, + success: function (records) { + records.forEach(function (r) { + datastorage.profiles.push(r); + }); + } + }); + } + + function loadProfilesRangeNext (dateTo) { + $('#info > b').html('' + translate('Loading next profile') + ' ...'); + + //Find first one after the end date and add to datastorage.profiles + var tquery = '?find[startDate][$gt]=' + new Date(dateTo).toISOString() + '&sort[startDate]=1&count=1'; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers(), + async: false, + success: function (records) { + records.forEach(function (r) { + //must be inserted as top to maintain profiles being sorted by date in descending order + datastorage.profiles.unshift(r); + }); + } + }); + } function processData(data, day, options, callback) { if (daystoshow[day].treatmentsonly) { diff --git a/static/robots.txt b/static/robots.txt index 1f53798bb4f..68ad19bb9f2 100644 --- a/static/robots.txt +++ b/static/robots.txt @@ -1,2 +1,2 @@ -User-agent: * -Disallow: / +User-agent: Browsershots +Disallow: diff --git a/swagger.json b/swagger.json index 1a9c24c985b..83f28a0469f 100755 --- a/swagger.json +++ b/swagger.json @@ -8,7 +8,7 @@ "info": { "title": "Nightscout API", "description": "Own your DData with the Nightscout API", - "version": "0.12.5", + "version": "14.0.6", "license": { "name": "AGPL 3", "url": "https://www.gnu.org/licenses/agpl.txt" diff --git a/swagger.yaml b/swagger.yaml index 3469ed0c7a8..7429d96a309 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -4,7 +4,7 @@ servers: info: title: Nightscout API description: Own your DData with the Nightscout API - version: 0.12.5 + version: 14.0.6 license: name: AGPL 3 url: 'https://www.gnu.org/licenses/agpl.txt' diff --git a/test b/test new file mode 100644 index 00000000000..e69de29bb2d diff --git a/testing/populate.js b/testing/populate.js index 34c307b7759..c4d9e1422a1 100644 --- a/testing/populate.js +++ b/testing/populate.js @@ -3,19 +3,22 @@ var mongodb = require('mongodb'); var env = require('./../env')(); -var util = require('./helpers/util'); +var util = require('./util'); main(); function main() { var MongoClient = mongodb.MongoClient; - MongoClient.connect(env.storageURI, function connected(err, db) { + MongoClient.connect(env.storageURI, { "useUnifiedTopology" : true, "useNewUrlParser" : true }, function connected(err, client) { console.log('Connecting to mongo...'); if (err) { console.log('Error occurred: ', err); throw err; } + + var db = client.db(); + populate_collection(db); }); } diff --git a/tests/admintools.test.js b/tests/admintools.test.js index bbede54156a..2fe6169f59a 100644 --- a/tests/admintools.test.js +++ b/tests/admintools.test.js @@ -66,7 +66,7 @@ var someData = { describe('admintools', function ( ) { var self = this; - this.timeout(30000); // TODO: see why this test takes longer on Travis to complete + this.timeout(45000); // TODO: see why this test takes longer on CI to complete before(function (done) { benv.setup(function() { @@ -138,7 +138,7 @@ describe('admintools', function ( ) { if (url.indexOf('status.json') > -1) { fn(serverSettings); } else { - fn({message: 'OK'}); + fn({message: {message: 'OK'}}); } return self.$.ajax(); }, @@ -153,7 +153,9 @@ describe('admintools', function ( ) { var d3 = require('d3'); //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; + //d3.timer = function mockTimer() { }; + let timer = d3.timer(function mockTimer() { }); + timer.stop(); var cookieStorageType = self.localStorage._type diff --git a/tests/api.alexa.test.js b/tests/api.alexa.test.js new file mode 100644 index 00000000000..440050eb2cc --- /dev/null +++ b/tests/api.alexa.test.js @@ -0,0 +1,97 @@ +'use strict'; + +var request = require('supertest'); +var language = require('../lib/language')(); +const bodyParser = require('body-parser'); + +require('should'); + +describe('Alexa REST api', function ( ) { + this.timeout(10000); + const apiRoot = require('../lib/api/root'); + const api = require('../lib/api/'); + before(function (done) { + var env = require('../env')( ); + env.settings.enable = ['alexa']; + env.settings.authDefaultRoles = 'readable'; + env.api_secret = 'this is my long pass phrase'; + this.wares = require('../lib/middleware/')(env); + this.app = require('express')( ); + this.app.enable('api'); + var self = this; + require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { + self.app.use('/api', bodyParser({ + limit: 1048576 * 50 + }), apiRoot(env, ctx)); + + self.app.use('/api/v1', bodyParser({ + limit: 1048576 * 50 + }), api(env, ctx)); + done( ); + }); + }); + + it('Launch Request', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "LaunchRequest", + "locale": "en-US" + } + }) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + + const launchText = 'What would you like to check on Nightscout?'; + + res.body.response.outputSpeech.text.should.equal(launchText); + res.body.response.reprompt.outputSpeech.text.should.equal(launchText); + res.body.response.shouldEndSession.should.equal(false); + done( ); + }); + }); + + it('Launch Request With Intent', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "LaunchRequest", + "locale": "en-US", + "intent": { + "name": "UNKNOWN" + } + } + }) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + + const unknownIntentText = 'I\'m sorry, I don\'t know what you\'re asking for.'; + + res.body.response.outputSpeech.text.should.equal(unknownIntentText); + res.body.response.shouldEndSession.should.equal(true); + done( ); + }); + }); + + it('Session Ended', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "SessionEndedRequest", + "locale": "en-US" + } + }) + .expect(200) + .end(function (err) { + if (err) return done(err); + + done( ); + }); + }); +}); + diff --git a/tests/api.devicestatus.test.js b/tests/api.devicestatus.test.js index a618db49056..34a0908610e 100644 --- a/tests/api.devicestatus.test.js +++ b/tests/api.devicestatus.test.js @@ -56,6 +56,7 @@ describe('Devicestatus API', function ( ) { request(self.app) .get('/api/devicestatus/') .query('find[created_at][$gte]=2018-12-16') + .query('find[created_at][$lte]=2018-12-17') .set('api-secret', self.env.api_secret || '') .expect(200) .expect(function (response) { diff --git a/tests/api.root.test.js b/tests/api.root.test.js new file mode 100644 index 00000000000..d2c3609a117 --- /dev/null +++ b/tests/api.root.test.js @@ -0,0 +1,42 @@ +'use strict'; + +const request = require('supertest'); +require('should'); + +describe('Root REST API', function() { + const self = this + , instance = require('./fixtures/api/instance') + , semver = require('semver') + ; + + this.timeout(15000); + + before(async () => { + self.instance = await instance.create({}); + self.app = self.instance.app; + self.env = self.instance.env; + }); + + + after(function after () { + self.instance.server.close(); + }); + + + it('GET /api/versions', async () => { + let res = await request(self.app) + .get('/api/versions') + .expect(200); + + res.body.length.should.be.aboveOrEqual(3); + res.body.forEach(obj => { + const fields = Object.getOwnPropertyNames(obj); + fields.sort().should.be.eql(['url', 'version']); + + semver.valid(obj.version).should.be.ok(); + obj.url.should.startWith('/api'); + }); + }); + +}); + diff --git a/tests/api.treatments.test.js b/tests/api.treatments.test.js index 4ba3739f4c0..2b1d3ee877d 100644 --- a/tests/api.treatments.test.js +++ b/tests/api.treatments.test.js @@ -34,10 +34,11 @@ describe('Treatment API', function ( ) { it('post single treatments', function (done) { self.ctx.treatments().remove({ }, function ( ) { + var now = (new Date()).toISOString(); request(self.app) .post('/api/treatments/') .set('api-secret', self.env.api_secret || '') - .send({eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) + .send({eventType: 'Meal Bolus', created_at: now, carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) .expect(200) .end(function (err) { if (err) { @@ -61,6 +62,25 @@ describe('Treatment API', function ( ) { }); }); + /* + it('saving entry without created_at should fail', function (done) { + + self.ctx.treatments().remove({ }, function ( ) { + request(self.app) + .post('/api/treatments/') + .set('api-secret', self.env.api_secret || '') + .send({eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) + .expect(422) + .end(function (err) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + }); +*/ it('post single treatments in zoned time format', function (done) { @@ -101,12 +121,13 @@ describe('Treatment API', function ( ) { it('post a treatment array', function (done) { self.ctx.treatments().remove({ }, function ( ) { + var now = (new Date()).toISOString(); request(self.app) .post('/api/treatments/') .set('api-secret', self.env.api_secret || '') .send([ - {eventType: 'BG Check', glucose: 100, preBolus: '0', glucoseType: 'Finger', units: 'mg/dl', notes: ''} - , {eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'} + {eventType: 'BG Check', created_at: now, glucose: 100, preBolus: '0', glucoseType: 'Finger', units: 'mg/dl', notes: ''} + , {eventType: 'Meal Bolus', created_at: now, carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'} ]) .expect(200) .end(function (err) { @@ -168,10 +189,11 @@ describe('Treatment API', function ( ) { it('post a treatment, query, delete, verify gone', function (done) { // insert a treatment - needs to be unique from example data console.log('Inserting treatment entry'); + var now = (new Date()).toISOString(); request(self.app) .post('/api/treatments/') .set('api-secret', self.env.api_secret || '') - .send({eventType: 'Meal Bolus', carbs: '99', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) + .send({eventType: 'Meal Bolus', created_at: now, carbs: '99', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) .expect(200) .end(function (err) { if (err) { diff --git a/tests/api.verifyauth.test.js b/tests/api.verifyauth.test.js index a9fd681da7b..48a05f2d9c4 100644 --- a/tests/api.verifyauth.test.js +++ b/tests/api.verifyauth.test.js @@ -26,7 +26,7 @@ describe('Verifyauth REST api', function ( ) { .get('/api/verifyauth') .expect(200) .end(function(err, res) { - res.body.message.should.equal('UNAUTHORIZED'); + res.body.message.message.should.equal('UNAUTHORIZED'); done(); }); }); @@ -37,7 +37,7 @@ describe('Verifyauth REST api', function ( ) { .set('api-secret', self.env.api_secret || '') .expect(200) .end(function(err, res) { - res.body.message.should.equal('OK'); + res.body.message.message.should.equal('OK'); done(); }); }); diff --git a/tests/api3.basic.test.js b/tests/api3.basic.test.js new file mode 100644 index 00000000000..fc7a885269f --- /dev/null +++ b/tests/api3.basic.test.js @@ -0,0 +1,49 @@ +'use strict'; + +const request = require('supertest'); +require('should'); + +describe('Basic REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + ; + + this.timeout(15000); + + before(async () => { + self.instance = await instance.create({}); + self.app = self.instance.app; + self.env = self.instance.env; + }); + + + after(function after () { + self.instance.ctx.bus.teardown(); + }); + + + it('GET /swagger', async () => { + let res = await request(self.app) + .get('/api/v3/swagger.yaml') + .expect(200); + + res.header['content-length'].should.be.above(0); + }); + + + it('GET /version', async () => { + let res = await request(self.app) + .get('/api/v3/version') + .expect(200); + + const apiConst = require('../lib/api3/const.json') + , software = require('../package.json'); + + res.body.version.should.equal(software.version); + res.body.apiVersion.should.equal(apiConst.API3_VERSION); + res.body.srvDate.should.be.within(testConst.YEAR_2019, testConst.YEAR_2050); + }); + +}); + diff --git a/tests/api3.create.test.js b/tests/api3.create.test.js new file mode 100644 index 00000000000..5ea3ab1a869 --- /dev/null +++ b/tests/api3.create.test.js @@ -0,0 +1,487 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 CREATE', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + , utils = require('./fixtures/api3/utils') + ; + + self.validDoc = { + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 CREATE', + eventType: 'Correction Bolus', + insulin: 0.3 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(20000); + + + /** + * Cleanup after successful creation + */ + self.delete = async function deletePermanent (identifier) { + await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`) + .expect(204); + }; + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200); + + return res.body; + }; + + + /** + * Get document detail for futher processing + */ + self.search = async function search (date) { + let res = await self.instance.get(`${self.url}?date$eq=${date}&token=${self.token.read}`) + .expect(200); + + return res.body; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}?token=${self.token.create}`; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.post(`${self.url}`) + .send(self.validDoc) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.post(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should require create permission', async () => { + let res = await self.instance.post(`${self.url}?token=${self.token.read}`) + .send(self.validDoc) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:create'); + }); + + + it('should reject empty body', async () => { + await self.instance.post(self.urlToken) + .send({ }) + .expect(400); + }); + + + it('should accept valid document', async () => { + let res = await self.instance.post(self.urlToken) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + res.headers.location.should.equal(`${self.url}/${self.validDoc.identifier}`); + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(self.validDoc); + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + (body.srvCreated - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiCreate.name); + + await self.delete(self.validDoc.identifier); + }); + + + it('should reject missing date', async () => { + let doc = Object.assign({}, self.validDoc); + delete doc.date; + + let res = await self.instance.post(self.urlToken) + .send(doc) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date null', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date ABC', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: 'ABC' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date -1', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: -1 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + + it('should reject invalid date 1 (too old)', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: 1 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date - illegal format', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: '2019-20-60T50:90:90' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid utcOffset -5000', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: -5000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should reject invalid utcOffset ABC', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: 'ABC' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should accept valid utcOffset', async () => { + await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: 120 })) + .expect(201); + + let body = await self.get(self.validDoc.identifier); + body.utcOffset.should.equal(120); + await self.delete(self.validDoc.identifier); + }); + + + it('should reject invalid utcOffset null', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should reject missing app', async () => { + let doc = Object.assign({}, self.validDoc); + delete doc.app; + + let res = await self.instance.post(self.urlToken) + .send(doc) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should reject invalid app null', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { app: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should reject empty app', async () => { + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { app: '' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should normalize date and store utcOffset', async () => { + await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: '2019-06-10T08:07:08,576+02:00' })) + .expect(201); + + let body = await self.get(self.validDoc.identifier); + body.date.should.equal(1560146828576); + body.utcOffset.should.equal(120); + await self.delete(self.validDoc.identifier); + }); + + + it('should require update permission for deduplication', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc); + + await self.instance.post(self.urlToken) + .send(doc) + .expect(201); + + let createdBody = await self.get(doc.identifier); + createdBody.should.containEql(doc); + + const doc2 = Object.assign({}, doc); + let res = await self.instance.post(self.urlToken) + .send(doc2) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:update'); + await self.delete(doc.identifier); + }); + + + it('should deduplicate document by identifier', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc); + + await self.instance.post(self.urlToken) + .send(doc) + .expect(201); + + let createdBody = await self.get(doc.identifier); + createdBody.should.containEql(doc); + + const doc2 = Object.assign({}, doc, { + insulin: 0.5 + }); + + await self.instance.post(`${self.url}?token=${self.token.all}`) + .send(doc2) + .expect(204); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + + await self.delete(doc2.identifier); + }); + + + it('should deduplicate document by created_at+eventType', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + self.instance.ctx.treatments.create([doc], async (err) => { // let's insert the document in APIv1's way + should.not.exist(err); + + const doc2 = Object.assign({}, doc, { + insulin: 0.4, + identifier: utils.randomString('32', 'aA#') + }); + delete doc2._id; // APIv1 updates input document, we must get rid of _id for the next round + + await self.instance.post(`${self.url}?token=${self.token.all}`) + .send(doc2) + .expect(204); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + + await self.delete(doc2.identifier); + }); + }); + + + it('should not deduplicate treatment only by created_at', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + self.instance.ctx.treatments.create([doc], async (err) => { // let's insert the document in APIv1's way + should.not.exist(err); + + let oldBody = await self.get(doc._id); + delete doc._id; // APIv1 updates input document, we must get rid of _id for the next round + oldBody.should.containEql(doc); + + const doc2 = Object.assign({}, doc, { + eventType: 'Meal Bolus', + insulin: 0.4, + identifier: utils.randomString('32', 'aA#') + }); + + await self.instance.post(`${self.url}?token=${self.token.all}`) + .send(doc2) + .expect(201); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + updatedBody.identifier.should.not.equal(oldBody.identifier); + + await self.delete(doc2.identifier); + await self.delete(oldBody.identifier); + }); + }); + + + it('should overwrite deleted document', async () => { + const date1 = new Date() + , identifier = utils.randomString('32', 'aA#'); + + await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { identifier, date: date1.toISOString() })) + .expect(201); + + await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`) + .expect(204); + + const date2 = new Date(); + let res = await self.instance.post(self.urlToken) + .send(Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() })) + .expect(403); + + res.body.status.should.be.equal(403); + res.body.message.should.be.equal('Missing permission api:treatments:update'); + + res = await self.instance.post(`${self.url}?token=${self.token.all}`) + .send(Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() })) + .expect(204); + + res.body.should.be.empty(); + + let body = await self.get(identifier); + body.date.should.equal(date2.getTime()); + body.identifier.should.equal(identifier); + await self.delete(identifier); + }); + + + it('should calculate the identifier', async () => { + self.validDoc.date = (new Date()).getTime(); + delete self.validDoc.identifier; + const validIdentifier = opTools.calculateIdentifier(self.validDoc); + + let res = await self.instance.post(self.urlToken) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + + let body = await self.get(validIdentifier); + body.should.containEql(self.validDoc); + await self.delete(validIdentifier); + }); + + + it('should deduplicate by identifier calculation', async () => { + self.validDoc.date = (new Date()).getTime(); + delete self.validDoc.identifier; + const validIdentifier = opTools.calculateIdentifier(self.validDoc); + + let res = await self.instance.post(self.urlToken) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + + let body = await self.get(validIdentifier); + body.should.containEql(self.validDoc); + + delete self.validDoc.identifier; + res = await self.instance.post(`${self.url}?token=${self.token.update}`) + .send(self.validDoc) + .expect(204); + + res.body.should.be.empty(); + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + + body = await self.search(self.validDoc.date); + body.length.should.equal(1); + + await self.delete(validIdentifier); + }); + +}); + diff --git a/tests/api3.delete.test.js b/tests/api3.delete.test.js new file mode 100644 index 00000000000..7cee15410a0 --- /dev/null +++ b/tests/api3.delete.test.js @@ -0,0 +1,53 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 UPDATE', function() { + const self = this + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + ; + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}?token=${self.token.delete}`; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.delete(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.delete(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + +}); + diff --git a/tests/api3.generic.workflow.test.js b/tests/api3.generic.workflow.test.js new file mode 100644 index 00000000000..7cfbc53f618 --- /dev/null +++ b/tests/api3.generic.workflow.test.js @@ -0,0 +1,293 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('Generic REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.urlLastModified = '/api/v3/lastModified'; + self.historyTimestamp = 0; + + self.docOriginal = { + eventType: 'Correction Bolus', + insulin: 1, + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' Generic REST API3' + }; + self.identifier = opTools.calculateIdentifier(self.docOriginal); + self.docOriginal.identifier = self.identifier; + + this.timeout(30000); + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.urlCol = '/api/v3/treatments'; + self.urlResource = self.urlCol + '/' + self.identifier; + self.urlHistory = self.urlCol + '/history'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}?token=${self.token.create}`; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + self.checkHistoryExistence = async function checkHistoryExistence (assertions) { + + let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}?token=${self.token.read}`) + .expect(200); + + res.body.length.should.be.above(0); + res.body.should.matchAny(value => { + value.identifier.should.be.eql(self.identifier); + value.srvModified.should.be.above(self.historyTimestamp); + + if (typeof(assertions) === 'function') { + assertions(value); + } + + self.historyTimestamp = value.srvModified; + }); + }; + + + it('LAST MODIFIED to get actual server timestamp', async () => { + let res = await self.instance.get(`${self.urlLastModified}?token=${self.token.read}`) + .expect(200); + + self.historyTimestamp = res.body.collections.treatments; + if (!self.historyTimestamp) { + self.historyTimestamp = res.body.srvDate - (10 * 60 * 1000); + } + self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019); + }); + + + it('STATUS to get actual server timestamp', async () => { + let res = await self.instance.get(`/api/v3/status?token=${self.token.read}`) + .expect(200); + + self.historyTimestamp = res.body.srvDate; + self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019); + }); + + + it('READ of not existing document is not found', async () => { + await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(404); + }); + + + it('SEARCH of not existing document (not found)', async () => { + let res = await self.instance.get(`${self.urlCol}?token=${self.token.read}`) + .query({ 'identifier_eq': self.identifier }) + .expect(200); + + res.body.should.have.length(0); + }); + + + it('DELETE of not existing document is not found', async () => { + await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .expect(404); + }); + + + it('CREATE new document', async () => { + await self.instance.post(`${self.urlCol}?token=${self.token.create}`) + .send(self.docOriginal) + .expect(201); + }); + + + it('READ existing document', async () => { + let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + + res.body.should.containEql(self.docOriginal); + self.docActual = res.body; + + if (self.historyTimestamp >= self.docActual.srvModified) { + self.historyTimestamp = self.docActual.srvModified - 1; + } + }); + + + it('SEARCH existing document (found)', async () => { + let res = await self.instance.get(`${self.urlCol}?token=${self.token.read}`) + .query({ 'identifier$eq': self.identifier }) + .expect(200); + + res.body.length.should.be.above(0); + res.body.should.matchAny(value => { + value.identifier.should.be.eql(self.identifier); + }); + }); + + + it('new document in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('UPDATE document', async () => { + self.docActual.insulin = 0.5; + + await self.instance.put(`${self.urlResource}?token=${self.token.update}`) + .send(self.docActual) + .expect(204); + + self.docActual.subject = self.subject.apiUpdate.name; + }); + + + it('document changed in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('document changed in READ', async () => { + let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + + delete self.docActual.srvModified; + res.body.should.containEql(self.docActual); + self.docActual = res.body; + }); + + + it('PATCH document', async () => { + self.docActual.carbs = 5; + self.docActual.insulin = 0.4; + + await self.instance.patch(`${self.urlResource}?token=${self.token.update}`) + .send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin }) + .expect(204); + }); + + + it('document changed in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('document changed in READ', async () => { + let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + + delete self.docActual.srvModified; + res.body.should.containEql(self.docActual); + self.docActual = res.body; + }); + + + it('soft DELETE', async () => { + await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .expect(204); + }); + + + it('READ of deleted is gone', async () => { + await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(410); + }); + + + + it('SEARCH of deleted document missing it', async () => { + let res = await self.instance.get(`${self.urlCol}?token=${self.token.read}`) + .query({ 'identifier_eq': self.identifier }) + .expect(200); + + res.body.should.have.length(0); + }); + + + it('document deleted in HISTORY', async () => { + await self.checkHistoryExistence(value => { + value.isValid.should.be.eql(false); + }); + }); + + + it('permanent DELETE', async () => { + await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .query({ 'permanent': 'true' }) + .expect(204); + }); + + + it('READ of permanently deleted is not found', async () => { + await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(404); + }); + + + it('document permanently deleted not in HISTORY', async () => { + let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}?token=${self.token.read}`); + + if (res.status === 200) { + res.body.should.matchEach(value => { + value.identifier.should.not.be.eql(self.identifier); + }); + } else { + res.status.should.equal(204); + } + }); + + + it('should not modify read-only document', async () => { + await self.instance.post(`${self.urlCol}?token=${self.token.create}`) + .send(Object.assign({}, self.docOriginal, { isReadOnly: true })) + .expect(201); + + let res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + + self.docActual = res.body; + delete self.docActual.srvModified; + const readOnlyMessage = 'Trying to modify read-only document'; + + res = await self.instance.post(`${self.urlCol}?token=${self.token.update}`) + .send(Object.assign({}, self.docActual, { insulin: 0.41 })) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.put(`${self.urlResource}?token=${self.token.update}`) + .send(Object.assign({}, self.docActual, { insulin: 0.42 })) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.patch(`${self.urlResource}?token=${self.token.update}`) + .send({ insulin: 0.43 }) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .query({ 'permanent': 'true' }) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.get(`${self.urlResource}?token=${self.token.read}`) + .expect(200); + res.body.should.containEql(self.docOriginal); + }); + +}); + diff --git a/tests/api3.patch.test.js b/tests/api3.patch.test.js new file mode 100644 index 00000000000..36dccc94bfa --- /dev/null +++ b/tests/api3.patch.test.js @@ -0,0 +1,219 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 PATCH', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.validDoc = { + date: (new Date()).getTime(), + utcOffset: -180, + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 PATCH', + eventType: 'Correction Bolus', + insulin: 0.3 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200); + + return res.body; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}/${self.validDoc.identifier}?token=${self.token.update}`; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.patch(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.patch(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should not found not existing document', async () => { + let res = await self.instance.patch(self.urlToken) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + + // now let's insert the document for further patching + res = await self.instance.post(`${self.url}?token=${self.token.create}`) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + }); + + + it('should reject identifier alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { identifier: 'MODIFIED'})) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field identifier cannot be modified by the client'); + }); + + + it('should reject date alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: self.validDoc.date + 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field date cannot be modified by the client'); + }); + + + it('should reject utcOffset alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: self.utcOffset - 120 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field utcOffset cannot be modified by the client'); + }); + + + it('should reject eventType alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { eventType: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field eventType cannot be modified by the client'); + }); + + + it('should reject device alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { device: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field device cannot be modified by the client'); + }); + + + it('should reject app alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { app: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field app cannot be modified by the client'); + }); + + + it('should reject srvCreated alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { srvCreated: self.validDoc.date - 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvCreated cannot be modified by the client'); + }); + + + it('should reject subject alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { subject: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field subject cannot be modified by the client'); + }); + + + it('should reject srvModified alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { srvModified: self.validDoc.date - 100000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvModified cannot be modified by the client'); + }); + + + it('should reject modifiedBy alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { modifiedBy: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field modifiedBy cannot be modified by the client'); + }); + + + it('should reject isValid alteration', async () => { + let res = await self.instance.patch(self.urlToken) + .send(Object.assign({}, self.validDoc, { isValid: false })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field isValid cannot be modified by the client'); + }); + + + it('should patch document', async () => { + self.validDoc.carbs = 10; + + let res = await self.instance.patch(self.urlToken) + .send(self.validDoc) + .expect(204); + + res.body.should.be.empty(); + + let body = await self.get(self.validDoc.identifier); + body.carbs.should.equal(10); + body.insulin.should.equal(0.3); + body.subject.should.equal(self.subject.apiCreate.name); + body.modifiedBy.should.equal(self.subject.apiUpdate.name); + }); + +}); + diff --git a/tests/api3.read.test.js b/tests/api3.read.test.js new file mode 100644 index 00000000000..d9f73ebf13a --- /dev/null +++ b/tests/api3.read.test.js @@ -0,0 +1,180 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 READ', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.validDoc = { + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 READ', + uploaderBattery: 58 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/devicestatus'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.get(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.get(`/api/v3/NOT_EXIST/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should not found not existing document', async () => { + await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .expect(404); + }); + + + it('should read just created document', async () => { + let res = await self.instance.post(`${self.url}?token=${self.token.create}`) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .expect(200); + + res.body.should.containEql(self.validDoc); + res.body.should.have.property('srvCreated').which.is.a.Number(); + res.body.should.have.property('srvModified').which.is.a.Number(); + res.body.should.have.property('subject'); + self.validDoc.subject = res.body.subject; // let's store subject for later tests + }); + + + it('should contain only selected fields', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=date,device,subject&token=${self.token.read}`) + .expect(200); + + const correct = { + date: self.validDoc.date, + device: self.validDoc.device, + subject: self.validDoc.subject + }; + res.body.should.eql(correct); + }); + + + it('should contain all fields', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=_all&token=${self.token.read}`) + .expect(200); + + for (let fieldName of ['app', 'date', 'device', 'identifier', 'srvModified', 'uploaderBattery', 'subject']) { + res.body.should.have.property(fieldName); + } + }); + + + it('should not send unmodified document since', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .set('If-Modified-Since', new Date(new Date().getTime() + 1000).toUTCString()) + .expect(304); + + res.body.should.be.empty(); + }); + + + it('should send modified document since', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .set('If-Modified-Since', new Date(new Date(self.validDoc.date).getTime() - 1000).toUTCString()) + .expect(200); + + res.body.should.containEql(self.validDoc); + }); + + + it('should recognize softly deleted document', async () => { + let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?token=${self.token.delete}`) + .expect(204); + + res.body.should.be.empty(); + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .expect(410); + + res.body.should.be.empty(); + }); + + + it('should not found permanently deleted document', async () => { + let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?permanent=true&token=${self.token.delete}`) + .expect(204); + + res.body.should.be.empty(); + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?token=${self.token.read}`) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should found document created by APIv1', async () => { + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + self.instance.ctx.devicestatus.create([doc], async (err) => { // let's insert the document in APIv1's way + should.not.exist(err); + const identifier = doc._id.toString(); + delete doc._id; + + let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200); + + res.body.should.containEql(doc); + + res = await self.instance.delete(`${self.url}/${identifier}?permanent=true&token=${self.token.delete}`) + .expect(204); + + res.body.should.be.empty(); + }); + }); + + +}); + diff --git a/tests/api3.renderer.test.js b/tests/api3.renderer.test.js new file mode 100644 index 00000000000..70401897025 --- /dev/null +++ b/tests/api3.renderer.test.js @@ -0,0 +1,268 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 output renderers', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + , _ = require('lodash') + , xml2js = require('xml2js') + , csvParse = require('csv-parse/lib/sync') + ; + + self.historyFrom = (new Date()).getTime() - 1000; // starting timestamp for HISTORY operations + + self.doc1 = testConst.SAMPLE_ENTRIES[0]; + self.doc1.date = (new Date()).getTime() - (5 * 60 * 1000); + self.doc1.identifier = opTools.calculateIdentifier(self.doc1); + + self.doc2 = testConst.SAMPLE_ENTRIES[1]; + self.doc2.date = (new Date()).getTime(); + self.doc2.identifier = opTools.calculateIdentifier(self.doc2); + + self.xmlParser = new xml2js.Parser({ + explicitArray: false + }); + + self.csvParserOptions = { + columns: true, + skip_empty_lines: true + }; + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/entries'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + }); + + + after(() => { + self.instance.server.close(); + }); + + + /** + * Checks if all properties from obj1 are string identical in obj2 + * (comparison of properties is made using toString()) + * @param {Object} obj1 + * @param {Object} obj2 + */ + self.checkProps = function checkProps (obj1, obj2) { + for (let propName in obj1) { + obj1[propName].toString().should.eql(obj2[propName].toString()); + } + }; + + + /** + * Checks if all objects from arrModel exist in arr + * (with string identical properties) + * @param arrModel + * @param arr + */ + self.checkItems = function checkItems (arrModel, arr) { + for (let itemModel of arrModel) { + const item = _.find(arr, (doc) => doc.identifier === itemModel.identifier); + item.should.not.be.empty(); + self.checkProps(itemModel, item); + } + }; + + + /** + * Checks if given text is valid XML. + * Next checks if all objects from arrModel exist in parsed array + * (with string identical properties) + * @param arrModel + * @param xmlText + * @returns {Promise} + */ + self.checkXmlItems = async function checkXmlItems (arrModel, xmlText) { + xmlText.should.startWith(''); + + const xml = await self.xmlParser.parseStringPromise(xmlText); + xml.items.should.not.be.empty(); + let items = xml.items.item; + items.should.be.Array(); + items.length.should.be.aboveOrEqual(arrModel.length); + + self.checkItems(arrModel, items); + }; + + + /** + * Checks if given text is valid CSV. + * Next checks if all objects from arrModel exist in parsed array + * (with string identical properties) + * @param arrModel + * @param csvText + * @returns {Promise} + */ + self.checkCsvItems = async function checkXmlItems (arrModel, csvText) { + csvText.should.not.be.empty(); + + const items = csvParse(csvText, self.csvParserOptions); + items.should.be.Array(); + items.length.should.be.aboveOrEqual(arrModel.length); + + self.checkItems(arrModel, items); + }; + + + it('should create 2 mock documents', async () => { + + async function createDoc (doc) { + + let res = await self.instance.post(`${self.url}?token=${self.token.create}`) + .send(doc) + .expect(201); + + res.body.should.be.empty(); + + res = await self.instance.get(`${self.url}/${doc.identifier}?token=${self.token.read}`) + .expect(200); + return res.body; + } + + self.doc1json = await createDoc(self.doc1); + self.doc2json = await createDoc(self.doc2); + }); + + + it('READ/SEARCH/HISTORY should not accept unsupported content type', async () => { + + async function check406 (request) { + const res = await request + .expect(406); + res.body.message.should.eql('Unsupported output format requested'); + } + + await check406(self.instance.get(`${self.url}/${self.doc1.identifier}.ttf?fields=_all&token=${self.token.read}`)); + await check406(self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all&token=${self.token.read}`) + .set('Accept', 'font/ttf')); + + await check406(self.instance.get(`${self.url}.ttf?fields=_all&token=${self.token.read}`)); + await check406(self.instance.get(`${self.url}?fields=_all&token=${self.token.read}`) + .set('Accept', 'font/ttf')); + + await check406(self.instance.get(`${self.url}/history/${self.doc1.date}.ttf?token=${self.token.read}`)); + await check406(self.instance.get(`${self.url}/history/${self.doc1.date}?token=${self.token.read}`) + .set('Accept', 'font/ttf')); + }); + + + it('READ should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}/${self.doc1.identifier}.xml?fields=_all&token=${self.token.read}`) + .expect(200); + + res.text.should.startWith(''); + + const xml = await self.xmlParser.parseStringPromise(res.text); + xml.item.should.not.be.empty(); + self.checkProps(self.doc1, xml.item); + + let res2 = await self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all&token=${self.token.read}`) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.eql(res2.text); + }); + + + it('READ should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}/${self.doc1.identifier}.csv?fields=_all&token=${self.token.read}`) + .expect(200); + + await self.checkCsvItems([self.doc1], res.text); + + let res2 = await self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all&token=${self.token.read}`) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.eql(res2.text); + }); + + + it('SEARCH should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}.xml?token=${self.token.read}&date$gte=${self.doc1.date}`) + .expect(200); + + await self.checkXmlItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}?token=${self.token.read}&date$gte=${self.doc1.date}`) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('SEARCH should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}.csv?token=${self.token.read}&date$gte=${self.doc1.date}`) + .expect(200); + + await self.checkCsvItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}?token=${self.token.read}&date$gte=${self.doc1.date}`) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('HISTORY should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}/history/${self.historyFrom}.xml?token=${self.token.read}`) + .expect(200); + + await self.checkXmlItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}/history/${self.historyFrom}?token=${self.token.read}`) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('HISTORY should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}/history/${self.historyFrom}.csv?token=${self.token.read}`) + .expect(200); + + await self.checkCsvItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}/history/${self.historyFrom}?token=${self.token.read}`) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('should remove mock documents', async () => { + + async function deleteDoc (identifier) { + await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`) + .query({ 'permanent': 'true' }) + .expect(204); + } + + await deleteDoc(self.doc1.identifier); + await deleteDoc(self.doc2.identifier); + }); +}); + diff --git a/tests/api3.search.test.js b/tests/api3.search.test.js new file mode 100644 index 00000000000..af109a18451 --- /dev/null +++ b/tests/api3.search.test.js @@ -0,0 +1,261 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 SEARCH', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.docs = testConst.SAMPLE_ENTRIES; + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = function get (identifier, done) { + self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200) + .end((err, res) => { + should.not.exist(err); + done(res.body); + }); + }; + + + /** + * Create given document in a promise + */ + self.create = (doc) => new Promise((resolve) => { + doc.identifier = opTools.calculateIdentifier(doc); + self.instance.post(`${self.url}?token=${self.token.all}`) + .send(doc) + .end((err) => { + should.not.exist(err); + self.get(doc.identifier, resolve); + }); + }); + + + before(async () => { + self.testStarted = new Date(); + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/entries'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}?token=${self.token.read}`; + self.urlTest = `${self.urlToken}&srvModified$gte=${self.testStarted.getTime()}`; + + const promises = testConst.SAMPLE_ENTRIES.map(doc => self.create(doc)); + self.docs = await Promise.all(promises); + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.get(self.url) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.get(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should found at least 10 documents', async () => { + let res = await self.instance.get(self.urlToken) + .expect(200); + + res.body.length.should.be.aboveOrEqual(self.docs.length); + }); + + + it('should found at least 10 documents from test start', async () => { + let res = await self.instance.get(self.urlTest) + .expect(200); + + res.body.length.should.be.aboveOrEqual(self.docs.length); + }); + + + it('should reject invalid limit - not a number', async () => { + let res = await self.instance.get(`${self.urlToken}&limit=INVALID`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter limit out of tolerance'); + }); + + + it('should reject invalid limit - negative number', async () => { + let res = await self.instance.get(`${self.urlToken}&limit=-1`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter limit out of tolerance'); + }); + + + it('should reject invalid limit - zero', async () => { + let res = await self.instance.get(`${self.urlToken}&limit=0`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter limit out of tolerance'); + }); + + + it('should accept valid limit', async () => { + let res = await self.instance.get(`${self.urlToken}&limit=3`) + .expect(200); + + res.body.length.should.be.equal(3); + }); + + + it('should reject invalid skip - not a number', async () => { + let res = await self.instance.get(`${self.urlToken}&skip=INVALID`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter skip out of tolerance'); + }); + + + it('should reject invalid skip - negative number', async () => { + let res = await self.instance.get(`${self.urlToken}&skip=-5`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter skip out of tolerance'); + }); + + + it('should reject both sort and sort$desc', async () => { + let res = await self.instance.get(`${self.urlToken}&sort=date&sort$desc=created_at`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameters sort and sort_desc cannot be combined'); + }); + + + it('should sort well by date field', async () => { + let res = await self.instance.get(`${self.urlTest}&sort=date`) + .expect(200); + + const ascending = res.body; + const length = ascending.length; + length.should.be.aboveOrEqual(self.docs.length); + + res = await self.instance.get(`${self.urlTest}&sort$desc=date`) + .expect(200); + + const descending = res.body; + descending.length.should.equal(length); + + for (let i in ascending) { + ascending[i].should.eql(descending[length - i - 1]); + + if (i > 0) { + ascending[i - 1].date.should.be.lessThanOrEqual(ascending[i].date); + } + } + }); + + + it('should skip documents', async () => { + let res = await self.instance.get(`${self.urlToken}&sort=date&limit=8`) + .expect(200); + + const fullDocs = res.body; + fullDocs.length.should.be.equal(8); + + res = await self.instance.get(`${self.urlToken}&sort=date&skip=3&limit=5`) + .expect(200); + + const skipDocs = res.body; + skipDocs.length.should.be.equal(5); + + for (let i = 0; i < 3; i++) { + skipDocs[i].should.be.eql(fullDocs[i + 3]); + } + }); + + + it('should project selected fields', async () => { + let res = await self.instance.get(`${self.urlToken}&fields=date,app,subject`) + .expect(200); + + res.body.forEach(doc => { + const docFields = Object.getOwnPropertyNames(doc); + docFields.sort().should.be.eql(['app', 'date', 'subject']); + }); + }); + + + it('should project all fields', async () => { + let res = await self.instance.get(`${self.urlToken}&fields=_all`) + .expect(200); + + res.body.forEach(doc => { + Object.getOwnPropertyNames(doc).length.should.be.aboveOrEqual(10); + Object.prototype.hasOwnProperty.call(doc, '_id').should.not.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'identifier').should.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'srvModified').should.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'srvCreated').should.be.true(); + }); + }); + + + it('should not exceed the limit of docs count', async () => { + const apiApp = self.instance.ctx.apiApp + , limitBackup = apiApp.get('API3_MAX_LIMIT'); + apiApp.set('API3_MAX_LIMIT', 5); + let res = await self.instance.get(`${self.urlToken}&limit=10`) + .expect(400); + + res.body.status.should.be.equal(400); + res.body.message.should.be.equal('Parameter limit out of tolerance'); + apiApp.set('API3_MAX_LIMIT', limitBackup); + }); + + + it('should respect the ceiling (hard) limit of docs', async () => { + const apiApp = self.instance.ctx.apiApp + , limitBackup = apiApp.get('API3_MAX_LIMIT'); + apiApp.set('API3_MAX_LIMIT', 5); + let res = await self.instance.get(`${self.urlToken}`) + .expect(200); + + res.body.length.should.be.equal(5); + apiApp.set('API3_MAX_LIMIT', limitBackup); + }); + +}); + diff --git a/tests/api3.security.test.js b/tests/api3.security.test.js new file mode 100644 index 00000000000..0e88e9fae19 --- /dev/null +++ b/tests/api3.security.test.js @@ -0,0 +1,189 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +const request = require('supertest') + , apiConst = require('../lib/api3/const.json') + , semver = require('semver') + , moment = require('moment') + ; +require('should'); + +describe('Security of REST API3', function() { + const self = this + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + ; + + this.timeout(30000); + + + before(async () => { + self.http = await instance.create({ useHttps: false }); + self.https = await instance.create({ }); + + let authResult = await authSubject(self.https.ctx.authorization.storage); + self.subject = authResult.subject; + self.token = authResult.token; + }); + + + after(() => { + self.http.ctx.bus.teardown(); + self.https.ctx.bus.teardown(); + }); + + + it('should require HTTPS', async () => { + if (semver.gte(process.version, '10.0.0')) { + let res = await request(self.http.baseUrl) // hangs on 8.x.x (no reason why) + .get('/api/v3/test') + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal(apiConst.MSG.HTTP_403_NOT_USING_HTTPS); + } + }); + + + it('should require Date header', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_MISSING_DATE); + }); + + + it('should validate Date header syntax', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Date', 'invalid date header') + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_BAD_DATE); + }); + + + it('should reject Date header out of tolerance', async () => { + const oldDate = new Date((new Date() * 1) - 2 * 3600 * 1000) + , futureDate = new Date((new Date() * 1) + 2 * 3600 * 1000); + + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Date', oldDate.toUTCString()) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); + + res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Date',futureDate.toUTCString()) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE); + }); + + + it('should reject invalid now ABC', async () => { + let res = await request(self.https.baseUrl) + .get(`/api/v3/test?now=ABC`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Bad Date header'); + }); + + + it('should reject invalid now -1', async () => { + let res = await request(self.https.baseUrl) + .get(`/api/v3/test?now=-1`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Bad Date header'); + }); + + + it('should reject invalid now - illegal format', async () => { + let res = await request(self.https.baseUrl) + .get(`/api/v3/test?now=2019-20-60T50:90:90`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Bad Date header'); + }); + + + it('should require token', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Date', new Date().toUTCString()) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN); + }); + + + it('should require valid token', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test?token=invalid_token') + .set('Date', new Date().toUTCString()) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN); + }); + + + it('should deny subject denied', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test?token=' + self.subject.denied.accessToken) + .set('Date', new Date().toUTCString()) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal(apiConst.MSG.HTTP_403_MISSING_PERMISSION.replace('{0}', 'api:entries:read')); + }); + + + it('should allow subject with read permission', async () => { + await request(self.https.baseUrl) + .get('/api/v3/test?token=' + self.token.read) + .set('Date', new Date().toUTCString()) + .expect(200); + }); + + + it('should accept valid now - epoch in ms', async () => { + await request(self.https.baseUrl) + .get(`/api/v3/test?token=${self.token.read}&now=${moment().valueOf()}`) + .expect(200); + }); + + + it('should accept valid now - epoch in seconds', async () => { + await request(self.https.baseUrl) + .get(`/api/v3/test?token=${self.token.read}&now=${moment().unix()}`) + .expect(200); + }); + + + it('should accept valid now - ISO 8601', async () => { + await request(self.https.baseUrl) + .get(`/api/v3/test?token=${self.token.read}&now=${moment().toISOString()}`) + .expect(200); + }); + + + it('should accept valid now - RFC 2822', async () => { + await request(self.https.baseUrl) + .get(`/api/v3/test?token=${self.token.read}&now=${moment().utc().format('ddd, DD MMM YYYY HH:mm:ss [GMT]')}`) + .expect(200); + }); + +}); \ No newline at end of file diff --git a/tests/api3.socket.test.js b/tests/api3.socket.test.js new file mode 100644 index 00000000000..9560c200c65 --- /dev/null +++ b/tests/api3.socket.test.js @@ -0,0 +1,178 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('Socket.IO in REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , apiConst = require('../lib/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , utils = require('./fixtures/api3/utils') + ; + + self.identifier = utils.randomString('32', 'aA#'); // let's have a brand new identifier for your testing document + + self.docOriginal = { + identifier: self.identifier, + eventType: 'Correction Bolus', + insulin: 1, + date: (new Date()).getTime(), + app: testConst.TEST_APP + }; + + this.timeout(30000); + + before(async () => { + self.instance = await instance.create({ + storageSocket: true + }); + + self.app = self.instance.app; + self.env = self.instance.env; + self.colName = 'treatments'; + self.urlCol = `/api/v3/${self.colName}`; + self.urlResource = self.urlCol + '/' + self.identifier; + self.urlHistory = self.urlCol + '/history'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.socket = self.instance.clientSocket; + }); + + + after(() => { + if(self.instance && self.instance.clientSocket && self.instance.clientSocket.connected) { + self.instance.clientSocket.disconnect(); + } + self.instance.ctx.bus.teardown(); + }); + + + it('should not subscribe without accessToken', done => { + self.socket.emit('subscribe', { }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN); + done(); + }); + }); + + + it('should not subscribe by invalid accessToken', done => { + self.socket.emit('subscribe', { accessToken: 'INVALID' }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN); + done(); + }); + }); + + + it('should not subscribe by subject with no rights', done => { + self.socket.emit('subscribe', { accessToken: self.token.denied }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_UNAUTHORIZED_TO_ANY); + done(); + }); + }); + + + it('should subscribe by valid accessToken', done => { + const cols = ['entries', 'treatments']; + + self.socket.emit('subscribe', { + accessToken: self.token.all, + collections: cols + }, function (data) { + data.success.should.equal(true); + should(data.collections.sort()).be.eql(cols); + done(); + }); + }); + + + it('should emit create event on CREATE', done => { + + self.socket.once('create', (event) => { + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docOriginal); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.post(`${self.urlCol}?token=${self.token.create}`) + .send(self.docOriginal) + .expect(201) + .end((err) => { + should.not.exist(err); + }); + }); + + + it('should emit update event on UPDATE', done => { + + self.docActual.insulin = 0.5; + + self.socket.once('update', (event) => { + delete self.docActual.srvModified; + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docActual); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.put(`${self.urlResource}?token=${self.token.update}`) + .send(self.docActual) + .expect(204) + .end((err) => { + should.not.exist(err); + self.docActual.subject = self.subject.apiUpdate.name; + }); + }); + + + it('should emit update event on PATCH', done => { + + self.docActual.carbs = 5; + self.docActual.insulin = 0.4; + + self.socket.once('update', (event) => { + delete self.docActual.srvModified; + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docActual); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.patch(`${self.urlResource}?token=${self.token.update}`) + .send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin }) + .expect(204) + .end((err) => { + should.not.exist(err); + }); + }); + + + it('should emit delete event on DELETE', done => { + + self.socket.once('delete', (event) => { + event.colName.should.equal(self.colName); + event.identifier.should.equal(self.identifier); + done(); + }); + + self.instance.delete(`${self.urlResource}?token=${self.token.delete}`) + .expect(204) + .end((err) => { + should.not.exist(err); + }); + }); + +}); + diff --git a/tests/api3.update.test.js b/tests/api3.update.test.js new file mode 100644 index 00000000000..481827b05d6 --- /dev/null +++ b/tests/api3.update.test.js @@ -0,0 +1,290 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 UPDATE', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , utils = require('./fixtures/api3/utils') + ; + + self.validDoc = { + identifier: utils.randomString('32', 'aA#'), + date: (new Date()).getTime(), + utcOffset: -180, + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 UPDATE', + eventType: 'Correction Bolus', + insulin: 0.3 + }; + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}?token=${self.token.read}`) + .expect(200); + + return res.body; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + self.urlToken = `${self.url}/${self.validDoc.identifier}?token=${self.token.update}` + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.put(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.put(`/api/v3/NOT_EXIST?token=${self.url}`) + .send(self.validDoc) + .expect(404); + + res.body.should.be.empty(); + }); + + + it('should require update permission for upsert', async () => { + let res = await self.instance.put(`${self.url}/${self.validDoc.identifier}?token=${self.token.update}`) + .send(self.validDoc) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:create'); + }); + + + it('should upsert not existing document', async () => { + let res = await self.instance.put(`${self.url}/${self.validDoc.identifier}?token=${self.token.all}`) + .send(self.validDoc) + .expect(201); + + res.body.should.be.empty(); + + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(self.validDoc); + should.not.exist(body.modifiedBy); + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + (body.srvCreated - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiAll.name); + }); + + + it('should update the document', async () => { + self.validDoc.carbs = 10; + delete self.validDoc.insulin; + + let res = await self.instance.put(self.urlToken) + .send(self.validDoc) + .expect(204); + + res.body.should.be.empty(); + + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(self.validDoc); + should.not.exist(body.insulin); + should.not.exist(body.modifiedBy); + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiUpdate.name); + }); + + + it('should update unmodified document since', async () => { + const doc = Object.assign({}, self.validDoc, { + carbs: 11 + }); + let res = await self.instance.put(self.urlToken) + .set('If-Unmodified-Since', new Date(new Date().getTime() + 1000).toUTCString()) + .send(doc) + .expect(204); + + res.body.should.be.empty(); + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(doc); + }); + + + it('should not update document modified since', async () => { + const doc = Object.assign({}, self.validDoc, { + carbs: 12 + }); + let body = await self.get(doc.identifier); + self.validDoc = body; + + let res = await self.instance.put(self.urlToken) + .set('If-Unmodified-Since', new Date(new Date(body.srvModified).getTime() - 1000).toUTCString()) + .send(doc) + .expect(412); + + res.body.should.be.empty(); + + body = await self.get(doc.identifier); + body.should.eql(self.validDoc); + }); + + + it('should reject date alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { date: self.validDoc.date + 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field date cannot be modified by the client'); + }); + + + it('should reject utcOffset alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { utcOffset: self.utcOffset - 120 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field utcOffset cannot be modified by the client'); + }); + + + it('should reject eventType alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { eventType: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field eventType cannot be modified by the client'); + }); + + + it('should reject device alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { device: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field device cannot be modified by the client'); + }); + + + it('should reject app alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { app: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field app cannot be modified by the client'); + }); + + + it('should reject srvCreated alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { srvCreated: self.validDoc.date - 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvCreated cannot be modified by the client'); + }); + + + it('should reject subject alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { subject: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field subject cannot be modified by the client'); + }); + + + it('should reject srvModified alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { srvModified: self.validDoc.date - 100000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvModified cannot be modified by the client'); + }); + + + it('should reject modifiedBy alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { modifiedBy: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field modifiedBy cannot be modified by the client'); + }); + + + it('should reject isValid alteration', async () => { + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { isValid: false })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field isValid cannot be modified by the client'); + }); + + + it('should ignore identifier alteration in body', async () => { + self.validDoc = await self.get(self.validDoc.identifier); + + let res = await self.instance.put(self.urlToken) + .send(Object.assign({}, self.validDoc, { identifier: 'MODIFIED' })) + .expect(204); + + res.body.should.be.empty(); + }); + + + it('should not update deleted document', async () => { + let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?token=${self.token.delete}`) + .expect(204); + + res.body.should.be.empty(); + + res = await self.instance.put(self.urlToken) + .send(self.validDoc) + .expect(410); + + res.body.should.be.empty(); + }); + +}); + diff --git a/tests/ar2.test.js b/tests/ar2.test.js index 9dbf6de14cd..01f4f3d41a1 100644 --- a/tests/ar2.test.js +++ b/tests/ar2.test.js @@ -147,18 +147,18 @@ describe('ar2', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var now = Date.now(); var before = now - FIVE_MINS; ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; var sbx = prepareSandbox(); - ar2.alexa.intentHandlers.length.should.equal(1); + ar2.virtAsst.intentHandlers.length.should.equal(1); - ar2.alexa.intentHandlers[0].intentHandler(function next(title, response) { + ar2.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('AR2 Forecast'); - response.should.equal('You are expected to be between 109 and 120 over the in 30 minutes'); + response.should.equal('According to the AR2 forecast you are expected to be between 109 and 120 over the next in 30 minutes'); done(); }, [], sbx); }); diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js index 0bcfd3bc268..fa97f84274e 100644 --- a/tests/basalprofileplugin.test.js +++ b/tests/basalprofileplugin.test.js @@ -77,7 +77,7 @@ describe('basalprofile', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var data = {}; var ctx = { @@ -92,14 +92,14 @@ describe('basalprofile', function ( ) { var sbx = sandbox.clientInit(ctx, time, data); sbx.data.profile = profile; - basal.alexa.intentHandlers.length.should.equal(1); - basal.alexa.rollupHandlers.length.should.equal(1); + basal.virtAsst.intentHandlers.length.should.equal(1); + basal.virtAsst.rollupHandlers.length.should.equal(1); - basal.alexa.intentHandlers[0].intentHandler(function next(title, response) { + basal.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Basal'); response.should.equal('Your current basal is 0.175 units per hour'); - basal.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + basal.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { should.not.exist(err); response.results.should.equal('Your current basal is 0.175 units per hour'); response.priority.should.equal(1); diff --git a/tests/bgnow.test.js b/tests/bgnow.test.js index 819f3dafbfc..c87e513c48d 100644 --- a/tests/bgnow.test.js +++ b/tests/bgnow.test.js @@ -9,6 +9,7 @@ var SIX_MINS = 360000; describe('BG Now', function ( ) { var ctx = { language: require('../lib/language')() + , settings: require('../lib/settings')() }; ctx.levels = require('../lib/levels'); diff --git a/tests/bridge.test.js b/tests/bridge.test.js index 99c1587fab4..66b69f64c3a 100644 --- a/tests/bridge.test.js +++ b/tests/bridge.test.js @@ -10,6 +10,7 @@ describe('bridge', function ( ) { bridge: { userName: 'nightscout' , password: 'wearenotwaiting' + , interval: 60000 } } }; @@ -27,6 +28,7 @@ describe('bridge', function ( ) { opts.login.accountName.should.equal('nightscout'); opts.login.password.should.equal('wearenotwaiting'); + opts.interval.should.equal(60000); }); it('store entries from share', function (done) { @@ -39,4 +41,43 @@ describe('bridge', function ( ) { bridge.bridged(mockEntries)(null); }); + it('set too low bridge interval option from env', function () { + var tooLowInterval = { + extendedSettings: { + bridge: { interval: 900 } + } + }; + + var opts = bridge.options(tooLowInterval); + should.exist(opts); + + opts.interval.should.equal(150000); + }); + + it('set too high bridge interval option from env', function () { + var tooHighInterval = { + extendedSettings: { + bridge: { interval: 500000 } + } + }; + + var opts = bridge.options(tooHighInterval); + should.exist(opts); + + opts.interval.should.equal(150000); + }); + + it('set no bridge interval option from env', function () { + var noInterval = { + extendedSettings: { + bridge: { } + } + }; + + var opts = bridge.options(noInterval); + should.exist(opts); + + opts.interval.should.equal(150000); + }); + }); diff --git a/tests/careportal.test.js b/tests/careportal.test.js index 782bc4fa566..36f48d3a5a4 100644 --- a/tests/careportal.test.js +++ b/tests/careportal.test.js @@ -49,7 +49,7 @@ describe('client', function ( ) { client.init(); - client.dataUpdate(nowData); + client.dataUpdate(nowData, true); client.careportal.prepareEvents(); diff --git a/tests/client.renderer.test.js b/tests/client.renderer.test.js index 569691cd717..5dc707ab2b1 100644 --- a/tests/client.renderer.test.js +++ b/tests/client.renderer.test.js @@ -54,13 +54,20 @@ describe('renderer', () => { } } , futureOpacity: (millsDifference) => { return 1; } + , createAdjustedRange: () => { return [ + { getTime: () => { return extent.times[0]}}, + { getTime: () => { return extent.times[1]}} + ] } } , latestSGV: { mills: 120 } }; describe(`data.mills ${extent.mills} and chart().brush.extent() times ${extent.times}`, () => { it(extent.expectation, () => { - renderer(mockClient, {}).highlightBrushPoints(mockData).should.equal(extent.expectedOpacity); + var selectedRange = mockClient.chart.createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); + renderer(mockClient, {}).highlightBrushPoints(mockData, from, to).should.equal(extent.expectedOpacity); }); }); }); diff --git a/tests/cob.test.js b/tests/cob.test.js index dbbecda0b67..54fbcb6c50d 100644 --- a/tests/cob.test.js +++ b/tests/cob.test.js @@ -97,7 +97,7 @@ describe('COB', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var data = { treatments: [{ carbs: '8' @@ -110,9 +110,9 @@ describe('COB', function ( ) { var sbx = sandbox.clientInit(ctx, Date.now(), data); cob.setProperties(sbx); - cob.alexa.intentHandlers.length.should.equal(1); + cob.virtAsst.intentHandlers.length.should.equal(1); - cob.alexa.intentHandlers[0].intentHandler(function next(title, response) { + cob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current COB'); response.should.equal('You have 8 carbohydrates on board'); done(); diff --git a/tests/dbsize.test.js b/tests/dbsize.test.js new file mode 100644 index 00000000000..0a66bb3ad6d --- /dev/null +++ b/tests/dbsize.test.js @@ -0,0 +1,320 @@ +'use strict'; + +require('should'); + +describe('Database Size', function() { + + var dataInRange = { dbstats: { dataSize: 1024 * 1024 * 137, indexSize: 1024 * 1024 * 48, fileSize: 1024 * 1024 * 256 } }; + var dataWarn = { dbstats: { dataSize: 1024 * 1024 * 250, indexSize: 1024 * 1024 * 100, fileSize: 1024 * 1024 * 360 } }; + var dataUrgent = { dbstats: { dataSize: 1024 * 1024 * 300, indexSize: 1024 * 1024 * 150, fileSize: 1024 * 1024 * 496 } }; + + var env = require('../env')(); + + it('display database size in range', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: require('../lib/language')() + }; + ctx.language.set('en'); + ctx.levels = require('../lib/levels'); + + var sbx = sandbox.clientInit(ctx, Date.now(), dataInRange); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('dbsize'); + var result = setter(); + result.display.should.equal('37%'); + result.status.should.equal('current'); + done(); + }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('display database size warning', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: require('../lib/language')() + }; + ctx.language.set('en'); + ctx.levels = require('../lib/levels'); + + var sbx = sandbox.clientInit(ctx, Date.now(), dataWarn); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('dbsize'); + var result = setter(); + result.display.should.equal('70%'); + result.status.should.equal('warn'); + done(); + }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + + dbsize.setProperties(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('display database size urgent', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: require('../lib/language')() + }; + ctx.language.set('en'); + ctx.levels = require('../lib/levels'); + + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('dbsize'); + var result = setter(); + result.display.should.equal('90%'); + result.status.should.equal('urgent'); + done(); + }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('display database size warning notiffication', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: require('../lib/language')() + , notifications: require('../lib/notifications')(env, ctx) + }; + ctx.notifications.initRequests(); + ctx.language.set('en'); + ctx.levels = require('../lib/levels'); + + var sbx = sandbox.clientInit(ctx, Date.now(), dataWarn); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + + dbsize.setProperties(sbx); + dbsize.checkNotifications(sbx); + + var notif = ctx.notifications.findHighestAlarm('Database Size'); + notif.level.should.equal(ctx.levels.WARN); + notif.title.should.equal('Warning Database Size near its limits!'); + notif.message.should.equal('Database size is 350 MiB out of 496 MiB. Please backup and clean up database!'); + done(); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('display database size urgent notiffication', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: require('../lib/language')() + , notifications: require('../lib/notifications')(env, ctx) + }; + ctx.notifications.initRequests(); + ctx.language.set('en'); + ctx.levels = require('../lib/levels'); + + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + + dbsize.setProperties(sbx); + dbsize.checkNotifications(sbx); + + var notif = ctx.notifications.findHighestAlarm('Database Size'); + notif.level.should.equal(ctx.levels.URGENT); + notif.title.should.equal('Urgent Database Size near its limits!'); + notif.message.should.equal('Database size is 450 MiB out of 496 MiB. Please backup and clean up database!'); + done(); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('set a pill to the database size in percent', function(done) { + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('90%'); + options.labelClass.should.equal('plugicon-database'); + options.pillClass.should.equal('urgent'); + done(); + } + } + , language: require('../lib/language')() + }; + ctx.language.set('en'); + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + dbsize.updateVisualisation(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('set a pill to the database size in MiB', function(done) { + var ctx = { + settings: { + extendedSettings: { + empty: false + , dbsize: { + inMib: true + } + } + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('450MiB'); + options.labelClass.should.equal('plugicon-database'); + options.pillClass.should.equal('urgent'); + done(); + } + } + , language: require('../lib/language')() + }; + ctx.language.set('en'); + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx.withExtendedSettings(dbsize)); + dbsize.updateVisualisation(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('configure warn level percentage', function(done) { + + var ctx = { + settings: { + extendedSettings: { + empty: false + , dbsize: { + warnPercentage: 30 + } + } + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('37%'); + options.pillClass.should.equal('warn'); + done(); + } + } + , language: require('../lib/language')() + }; + ctx.language.set('en'); + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataInRange); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx.withExtendedSettings(dbsize)); + dbsize.updateVisualisation(sbx); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('configure urgent level percentage', function(done) { + + var ctx = { + settings: { + extendedSettings: { + empty: false + , dbsize: { + warnPercentage: 30 + , urgentPercentage: 36 + } + } + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('37%'); + options.pillClass.should.equal('urgent'); + done(); + } + } + , language: require('../lib/language')() + }; + ctx.language.set('en'); + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataInRange); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx.withExtendedSettings(dbsize)); + dbsize.updateVisualisation(sbx); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('hide the pill if there is no info regarding database size', function(done) { + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.hide.should.equal(true); + done(); + } + } + , language: require('../lib/language')() + }; + ctx.language.set('en'); + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), {}); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + dbsize.updateVisualisation(sbx); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('should handle virtAsst requests', function(done) { + + var ctx = { + settings: {} + , language: require('../lib/language')() + }; + ctx.language.set('en'); + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + + dbsize.virtAsst.intentHandlers.length.should.equal(2); + + dbsize.virtAsst.intentHandlers[0].intentHandler(function next (title, response) { + title.should.equal('Database file size'); + response.should.equal('450 MiB that is 90% of available database space'); + + dbsize.virtAsst.intentHandlers[1].intentHandler(function next (title, response) { + title.should.equal('Database file size'); + response.should.equal('450 MiB that is 90% of available database space'); + + done(); + }, [], sbx); + + }, [], sbx); + + }); + +}); diff --git a/tests/ddata.test.js b/tests/ddata.test.js index f3757348c53..ceb163b7c4f 100644 --- a/tests/ddata.test.js +++ b/tests/ddata.test.js @@ -41,19 +41,6 @@ describe('ddata', function ( ) { done( ); }); - it('has #split( )', function (done) { - var date = new Date( ); - var time = date.getTime( ); - var cutoff = 1000 * 60 * 5; - var max = 1000 * 60 * 60 * 24 * 2; - var pieces = ctx.ddata.splitRecent(time, cutoff, max); - should.exist(pieces); - should.exist(pieces.first); - should.exist(pieces.rest); - - done( ); - }); - // TODO: ensure partition function gets called via: // Properties // * ddata.devicestatus diff --git a/tests/env.test.js b/tests/env.test.js index 90313dfd39f..7990b470692 100644 --- a/tests/env.test.js +++ b/tests/env.test.js @@ -2,7 +2,7 @@ require('should'); -describe('env', function ( ) { +describe('env', function () { it( 'show the right plugins', function () { process.env.SHOW_PLUGINS = 'iob'; process.env.ENABLE = 'iob cob'; @@ -68,4 +68,88 @@ describe('env', function ( ) { env.insecureUseHttp.should.be.false(); // not defined should be false env.secureHstsHeader.should.be.true(); }); + + describe( 'DISPLAY_UNITS', function () { + const MMOL = 'mmol'; + const MGDL = 'mg/dl'; + describe ( 'mmol', function () { + it( 'mmol => mmol', function () { + process.env.DISPLAY_UNITS = MMOL; + var env = require( '../env' )(); + env.settings.units.should.equal( MMOL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'mmol/l => mmol', function () { + process.env.DISPLAY_UNITS = 'mmol/l'; + var env = require( '../env' )(); + env.settings.units.should.equal( MMOL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'mmol/L => mmol', function () { + process.env.DISPLAY_UNITS = 'mmol/L'; + var env = require( '../env' )(); + env.settings.units.should.equal( MMOL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'MMOL => mmol', function () { + process.env.DISPLAY_UNITS = 'MMOL'; + var env = require( '../env' )(); + env.settings.units.should.equal( MMOL ); + delete process.env.DISPLAY_UNITS; + } ); + } ); + + describe ( 'mg/dl', function () { + it( 'mg/dl => mg/dl', function () { + process.env.DISPLAY_UNITS = MGDL; + var env = require( '../env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'mg/dL => mg/dl', function () { + process.env.DISPLAY_UNITS = 'mg/dL'; + var env = require( '../env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'MG/DL => mg/dl', function () { + process.env.DISPLAY_UNITS = 'MG/DL'; + var env = require( '../env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'mgdl => mg/dl', function () { + process.env.DISPLAY_UNITS = 'mgdl'; + var env = require( '../env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + } ); + + describe ( 'default: mg/dl', function () { + it( ' => mg/dl', function () { + var random; + while (!random || random.toLowerCase() === MGDL) + random = [...Array(~~(Math.random()*20)+1)].map(i=>(~~(Math.random()*36)).toString(36)).join(''); + + process.env.DISPLAY_UNITS = random; + var env = require( '../env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( ' => mg/dl', function () { + delete process.env.DISPLAY_UNITS; + var env = require( '../env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + } ); + } ); }) diff --git a/tests/expressextensions.test.js b/tests/expressextensions.test.js new file mode 100644 index 00000000000..b65ebd8952b --- /dev/null +++ b/tests/expressextensions.test.js @@ -0,0 +1,33 @@ +'use strict'; + +require('should'); + +var extensionsMiddleware = require('../lib/middleware/express-extension-to-accept.js'); + +var acceptJsonRequests = extensionsMiddleware(['json']); + +describe('Express extension middleware', function ( ) { + + it('Valid json request should be given accept header for application/json', function () { + var entriesRequest = { + path: '/api/v1/entries.json', + url: '/api/v1/entries.json', + headers: {} + }; + + acceptJsonRequests(entriesRequest, {}, () => {}); + entriesRequest.headers.accept.should.equal('application/json'); + }); + + it('Invalid json request should NOT be given accept header', function () { + var invalidEntriesRequest = { + path: '/api/v1/entriesXjson', + url: '/api/v1/entriesXjson', + headers: {} + }; + + acceptJsonRequests(invalidEntriesRequest, {}, () => {}); + should(invalidEntriesRequest.headers.accept).not.be.ok; + }); + +}); diff --git a/tests/fail.test.js b/tests/fail.test.js new file mode 100644 index 00000000000..eefda445b3d --- /dev/null +++ b/tests/fail.test.js @@ -0,0 +1,14 @@ +'use strict'; + +require('should'); + +// This test is included just so we have an easy to template to intentionally cause +// builds to fail + +describe('fail', function ( ) { + + it('should not fail', function () { + true.should.equal(true); + }); + +}); diff --git a/tests/fixtures/api/instance.js b/tests/fixtures/api/instance.js new file mode 100644 index 00000000000..ed5b28474c9 --- /dev/null +++ b/tests/fixtures/api/instance.js @@ -0,0 +1,98 @@ +'use strict'; + +const fs = require('fs') + , path = require('path') + , language = require('../../../lib/language')() + , apiRoot = require('../../../lib/api/root') + , http = require('http') + , https = require('https') + ; + +function configure () { + const self = { }; + + self.prepareEnv = function prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }) { + + if (useHttps) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + else { + process.env.INSECURE_USE_HTTP = true; + } + process.env.API_SECRET = apiSecret; + + process.env.HOSTNAME = 'localhost'; + const env = require('../../../env')(); + + if (useHttps) { + env.ssl = { + key: fs.readFileSync(path.join(__dirname, '../api3/localhost.key')), + cert: fs.readFileSync(path.join(__dirname, '../api3/localhost.crt')) + }; + } + + env.settings.authDefaultRoles = authDefaultRoles; + env.settings.enable = enable; + + return env; + }; + + + /* + * Create new web server instance for testing purposes + */ + self.create = function createHttpServer ({ + apiSecret = 'this is my long pass phrase', + useHttps = true, + authDefaultRoles = '', + enable = ['careportal', 'api'] + }) { + + return new Promise(function (resolve, reject) { + + try { + let instance = { }, + hasBooted = false + ; + + instance.env = self.prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }); + + self.wares = require('../../../lib/middleware/')(instance.env); + instance.app = require('express')(); + instance.app.enable('api'); + + require('../../../lib/server/bootevent')(instance.env, language).boot(function booted (ctx) { + instance.ctx = ctx; + instance.ctx.ddata = require('../../../lib/data/ddata')(); + instance.ctx.apiRootApp = apiRoot(instance.env, ctx); + + instance.app.use('/api', instance.ctx.apiRootApp); + + const transport = useHttps ? https : http; + + instance.server = transport.createServer(instance.env.ssl || { }, instance.app).listen(0); + instance.env.PORT = instance.server.address().port; + + instance.baseUrl = `${useHttps ? 'https' : 'http'}://${instance.env.HOSTNAME}:${instance.env.PORT}`; + + console.log(`Started ${useHttps ? 'SSL' : 'HTTP'} instance on ${instance.baseUrl}`); + hasBooted = true; + resolve(instance); + }); + + setTimeout(function watchDog() { + if (!hasBooted) + reject('timeout'); + }, 30000); + + } catch (err) { + reject(err); + } + }); + }; + + + return self; +} + +module.exports = configure(); \ No newline at end of file diff --git a/tests/fixtures/api3/authSubject.js b/tests/fixtures/api3/authSubject.js new file mode 100644 index 00000000000..6036103b0e5 --- /dev/null +++ b/tests/fixtures/api3/authSubject.js @@ -0,0 +1,94 @@ +'use strict'; + +const _ = require('lodash'); + +function createRole (authStorage, name, permissions) { + + return new Promise((resolve, reject) => { + + let role = _.find(authStorage.roles, { name }); + + if (role) { + resolve(role); + } + else { + authStorage.createRole({ + "name": name, + "permissions": permissions, + "notes": "" + }, function afterCreate (err) { + + if (err) + reject(err); + + role = _.find(authStorage.roles, { name }); + resolve(role); + }); + } + }); +} + + +function createTestSubject (authStorage, subjectName, roles) { + + return new Promise((resolve, reject) => { + + const subjectDbName = 'test-' + subjectName; + let subject = _.find(authStorage.subjects, { name: subjectDbName }); + + if (subject) { + resolve(subject); + } + else { + authStorage.createSubject({ + "name": subjectDbName, + "roles": roles, + "notes": "" + }, function afterCreate (err) { + + if (err) + reject(err); + + subject = _.find(authStorage.subjects, { name: subjectDbName }); + resolve(subject); + }); + } + }); +} + + +async function authSubject (authStorage) { + + await createRole(authStorage, 'apiAll', 'api:*:*'); + await createRole(authStorage, 'apiAdmin', 'api:*:admin'); + await createRole(authStorage, 'apiCreate', 'api:*:create'); + await createRole(authStorage, 'apiRead', 'api:*:read'); + await createRole(authStorage, 'apiUpdate', 'api:*:update'); + await createRole(authStorage, 'apiDelete', 'api:*:delete'); + + const subject = { + apiAll: await createTestSubject(authStorage, 'apiAll', ['apiAll']), + apiAdmin: await createTestSubject(authStorage, 'apiAdmin', ['apiAdmin']), + apiCreate: await createTestSubject(authStorage, 'apiCreate', ['apiCreate']), + apiRead: await createTestSubject(authStorage, 'apiRead', ['apiRead']), + apiUpdate: await createTestSubject(authStorage, 'apiUpdate', ['apiUpdate']), + apiDelete: await createTestSubject(authStorage, 'apiDelete', ['apiDelete']), + admin: await createTestSubject(authStorage, 'admin', ['admin']), + readable: await createTestSubject(authStorage, 'readable', ['readable']), + denied: await createTestSubject(authStorage, 'denied', ['denied']) + }; + + const token = { + all: subject.apiAll.accessToken, + admin: subject.apiAdmin.accessToken, + create: subject.apiCreate.accessToken, + read: subject.apiRead.accessToken, + update: subject.apiUpdate.accessToken, + delete: subject.apiDelete.accessToken, + denied: subject.denied.accessToken + }; + + return {subject, token}; +} + +module.exports = authSubject; \ No newline at end of file diff --git a/tests/fixtures/api3/const.json b/tests/fixtures/api3/const.json new file mode 100644 index 00000000000..a0acf37cfee --- /dev/null +++ b/tests/fixtures/api3/const.json @@ -0,0 +1,138 @@ +{ + "YEAR_2019": 1546304400000, + "YEAR_2050": 2524611600000, + "TEST_APP": "cgm-remote-monitor.test", + "TEST_DEVICE": "Samsung XCover 4-123456735643809", + + "SAMPLE_ENTRIES": [ + { + "date": 1491717830000.0, + "device": "dexcom", + "direction": "FortyFiveUp", + "filtered": 167584, + "noise": 2, + "rssi": 183, + "sgv": 149, + "type": "sgv", + "unfiltered": 171584, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718130000.0, + "device": "dexcom", + "direction": "FortyFiveUp", + "filtered": 170656, + "noise": 2, + "rssi": 181, + "sgv": 152, + "type": "sgv", + "unfiltered": 175776, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718430000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 173536, + "noise": 2, + "rssi": 185, + "sgv": 155, + "type": "sgv", + "unfiltered": 180864, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718730000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 177120, + "noise": 2, + "rssi": 186, + "sgv": 159, + "type": "sgv", + "unfiltered": 182080, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719030000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 181088, + "noise": 2, + "rssi": 165, + "sgv": 163, + "type": "sgv", + "unfiltered": 186912, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719330000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 184736, + "noise": 1, + "rssi": 162, + "sgv": 170, + "type": "sgv", + "unfiltered": 188512, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719630000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 187776, + "noise": 1, + "rssi": 175, + "sgv": 175, + "type": "sgv", + "unfiltered": 192608, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719930000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 190816, + "noise": 1, + "rssi": 181, + "sgv": 179, + "type": "sgv", + "unfiltered": 196640, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491720230000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 194016, + "noise": 1, + "rssi": 203, + "sgv": 181, + "type": "sgv", + "unfiltered": 199008, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491720530000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 197536, + "noise": 1, + "rssi": 184, + "sgv": 186, + "type": "sgv", + "unfiltered": 203296, + "app": "cgm-remote-monitor.test" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/api3/instance.js b/tests/fixtures/api3/instance.js new file mode 100644 index 00000000000..a7693ab3c40 --- /dev/null +++ b/tests/fixtures/api3/instance.js @@ -0,0 +1,163 @@ +'use strict'; + +var fs = require('fs') + , language = require('../../../lib/language')() + , api = require('../../../lib/api3/') + , http = require('http') + , https = require('https') + , request = require('supertest') + , websocket = require('../../../lib/server/websocket') + , io = require('socket.io-client') + ; + +function configure () { + const self = { }; + + self.prepareEnv = function prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }) { + + if (useHttps) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + else { + process.env.INSECURE_USE_HTTP = true; + } + process.env.API_SECRET = apiSecret; + + process.env.HOSTNAME = 'localhost'; + const env = require('../../../env')(); + + if (useHttps) { + env.ssl = { + key: fs.readFileSync(__dirname + '/localhost.key'), + cert: fs.readFileSync(__dirname + '/localhost.crt') + }; + } + + env.settings.authDefaultRoles = authDefaultRoles; + env.settings.enable = enable; + + return env; + }; + + + self.addSecuredOperations = function addSecuredOperations (instance) { + + instance.get = (url) => request(instance.baseUrl).get(url).set('Date', new Date().toUTCString()); + + instance.post = (url) => request(instance.baseUrl).post(url).set('Date', new Date().toUTCString()); + + instance.put = (url) => request(instance.baseUrl).put(url).set('Date', new Date().toUTCString()); + + instance.patch = (url) => request(instance.baseUrl).patch(url).set('Date', new Date().toUTCString()); + + instance.delete = (url) => request(instance.baseUrl).delete(url).set('Date', new Date().toUTCString()); + }; + + + self.bindSocket = function bindSocket (storageSocket, instance) { + + return new Promise(function (resolve, reject) { + if (!storageSocket) { + resolve(); + } + else { + let socket = io(`${instance.baseUrl}/storage`, { + origins:"*", + transports: ['websocket', 'flashsocket', 'polling'], + rejectUnauthorized: false + }); + + socket.on('connect', function () { + resolve(socket); + }); + socket.on('connect_error', function (error) { + console.error(error); + reject(error); + }); + } + }); + }; + + + self.unbindSocket = function unbindSocket (instance) { + if (instance.clientSocket.connected) { + instance.clientSocket.disconnect(); + } + }; + + /* + * Create new web server instance for testing purposes + */ + self.create = function createHttpServer ({ + apiSecret = 'this is my long pass phrase', + disableSecurity = false, + useHttps = true, + authDefaultRoles = '', + enable = ['careportal', 'api'], + storageSocket = null + }) { + + return new Promise(function (resolve, reject) { + + try { + let instance = { }, + hasBooted = false + ; + + instance.env = self.prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }); + + self.wares = require('../../../lib/middleware/')(instance.env); + instance.app = require('express')(); + instance.app.enable('api'); + + require('../../../lib/server/bootevent')(instance.env, language).boot(function booted (ctx) { + instance.ctx = ctx; + instance.ctx.ddata = require('../../../lib/data/ddata')(); + instance.ctx.apiApp = api(instance.env, ctx); + + if (disableSecurity) { + instance.ctx.apiApp.set('API3_SECURITY_ENABLE', false); + } + + instance.app.use('/api/v3', instance.ctx.apiApp); + + const transport = useHttps ? https : http; + + instance.server = transport.createServer(instance.env.ssl || { }, instance.app).listen(0); + instance.env.PORT = instance.server.address().port; + + instance.baseUrl = `${useHttps ? 'https' : 'http'}://${instance.env.HOSTNAME}:${instance.env.PORT}`; + + self.addSecuredOperations(instance); + + websocket(instance.env, instance.ctx, instance.server); + + self.bindSocket(storageSocket, instance) + .then((socket) => { + instance.clientSocket = socket; + + console.log(`Started ${useHttps ? 'SSL' : 'HTTP'} instance on ${instance.baseUrl}`); + hasBooted = true; + resolve(instance); + }) + .catch((reason) => { + console.error(reason); + reject(reason); + }); + }); + + setTimeout(function watchDog() { + if (!hasBooted) + reject('timeout'); + }, 30000); + + } catch (err) { + reject(err); + } + }); + }; + + return self; +} + +module.exports = configure(); \ No newline at end of file diff --git a/tests/fixtures/api3/localhost.crt b/tests/fixtures/api3/localhost.crt new file mode 100644 index 00000000000..21a2a39b0a4 --- /dev/null +++ b/tests/fixtures/api3/localhost.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJAIx0y57dTqDpMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xOTAyMDQxOTM1MDhaFw0yOTAyMDExOTM1MDhaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKCeBaqAJU+nrzNUZMsD1jYQpmcw8+6tG69KQY2XmqMsaPupo2ArwUlYD3pm +F1HTf9Lkq8u07rlUyMaSSRYrY56vPrMWGSK5Elm4kF8DNS4b/55KwZC+YQM0ZuJK +wSM6WX4G7JwV936HKJAT+Ec+8Ofq3GQzA9+Z4x2zMwNGC8AghtPjsCk68ORCmr+5 +fdCdC1Rz9hE92Nmofi8e1hUTeZmFROx8hcYRhxYXLIWVxALc/t8yY3MZfsRuZXcP +/3PageAn0ecxhqlWBY23GDQx7OSEZxSEPgqxnAHQfQXIrPRjMkFNHeMM7HTvITAG +VCc99zEG3Jy5hatm+RAajdWBH4sCAwEAAaNQME4wHQYDVR0OBBYEFJJVZn5Y91O7 +JUKeHW4La8eseKKwMB8GA1UdIwQYMBaAFJJVZn5Y91O7JUKeHW4La8eseKKwMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAFOU19t9h6C1Hakkik/93kun +pwG7v8VvDPjKECR5KlNPKNZUOQaiMAVHgNwPWV8q+qvfydzIpDrTd/O5eOaOduLx +gDVDj078Q05j17RUC+ct5yQ6lPgEHlnkI0Zr/hgFyNC+mtK7oIm6BT8wSSRbv7AG +3wQzCA5UvW/BQ8rtNZSC42Jyr0BR0ZS9Fo3Gc4v/nZJlgkiBvU2gKVQ7VRKxybCn +0hDghVwTfBPq7PKmupLX82ktwhYpDJZXCsOVfq9mF6nbQ6b0MieXFD+7cBlEXb1e +3VgtVzKYyqh/Oex4HfMThzAJZSWa0E4FShr5XdTdIc3nB4Vgbsis5l9Yrcp3Xo4= +-----END CERTIFICATE----- diff --git a/tests/fixtures/api3/localhost.key b/tests/fixtures/api3/localhost.key new file mode 100644 index 00000000000..2486c15fefe --- /dev/null +++ b/tests/fixtures/api3/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAoJ4FqoAlT6evM1RkywPWNhCmZzDz7q0br0pBjZeaoyxo+6mj +YCvBSVgPemYXUdN/0uSry7TuuVTIxpJJFitjnq8+sxYZIrkSWbiQXwM1Lhv/nkrB +kL5hAzRm4krBIzpZfgbsnBX3focokBP4Rz7w5+rcZDMD35njHbMzA0YLwCCG0+Ow +KTrw5EKav7l90J0LVHP2ET3Y2ah+Lx7WFRN5mYVE7HyFxhGHFhcshZXEAtz+3zJj +cxl+xG5ldw//c9qB4CfR5zGGqVYFjbcYNDHs5IRnFIQ+CrGcAdB9Bcis9GMyQU0d +4wzsdO8hMAZUJz33MQbcnLmFq2b5EBqN1YEfiwIDAQABAoIBADoh95sGVnrGDjtd +yD1SXi2jSRcAOMmiDesbzS4aOPXmFPlBJMiiDYsmPDPoz3fmPNVvvl40VlLtxN1a +BOnpOl0swFzBGsfehC3FBzvcRVsy9wmrtPNWdHZceQBeXhkJ/WoHx4uWx8Ub1iqP +j8T5mufVsX7yl+xOHk2ZllUQ/R/EEz9x00pkiH8Vsn8DhFI5KNqgi4n4c36T3vrn +MjTp+1o7bJ/cEnvXLi+IG2CO5y5hVbu3iKb+71YOGc6f/AJVzZ3MegC3KMFho9lh +DbDzumMuW8fZNyBfslXXoOr6oDqNq92n/jC/2hR8Xlth/aafisJiIVGydeVdDXhM +gDjdroECgYEAy3hXuo/Q1acncInGhIJvHjS/sVShP2epHz9zp8XuWl4NCuGP5V2c +jLT0hDW+ZKTUFweK9sQJNta81gs4pYc+2HGI8RP65XW4vgesNoKbBcE9xhEq0HMX +KN3/MJiwkNkM95T3nWqulhzNszhgNbZDMAU3Ule+o4n8udwOlFCTeXMCgYEAyhV4 +PoL3wp05BY0ssyKEqld3EqHNlPdQeJe1Dg9LSBy+3Z9sNngRD1/FuTo7RX6UY0FH +MaSI1JwhHSQ+2GNkqdMvVAilTXIDRw8vU9B77bYiHjny8+vMU06I9V3cJ57bNfmR +NUJtPmGO9xQ5UYxhP9rFOcI4MIecSzu1tvqiG4kCgYB01NoS7sdsFrnnvcS2i6rA +PmufqEeaf6w1nBqNyHJPg1eb2t7kRfdBOBp6291CLv71Zkhd3zynN3BguzrAmUL1 +x2Npgh57qTf2LbOt7RqUmFwfIfZikONIfQgt4E7qLSdr9iakRgCPg2R9ty5PSSOV +LDmS131IrE/obLoWYZn8jwKBgQDIaAxMahONA+CFueCHcgcA6yah6qZ3QeCjB0g9 +vjsZM7CxFqX5So8YoRDzxWT8YTCFUjppZ9NujbtlLAnLDJ7KsC2yd7R/Hj9T3CJC +S3JrZoFlWnCvJ7wFLdAzDTcEb8zTNUGlANBX2eYu7/Z8Aex7p9iJlCunLQV5sqhd +4yaaiQKBgQCERrz1XcJpM8S93nXdAv3Nn1bwA1V/ylx42DRxNEBl2JZQ1sQeqN36 +JvXPXhVZ3vTQDhVUqcVgqJIAb2xMviIVBnssOq3+pi/hOs13rakJf4AOulZ/3Si7 +HSLdymfQAMEKczU2261kw4pjPwiurkjAFWbQG2C8RGE/rR2y38PkDg== +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/api3/utils.js b/tests/fixtures/api3/utils.js new file mode 100644 index 00000000000..942f948c10e --- /dev/null +++ b/tests/fixtures/api3/utils.js @@ -0,0 +1,21 @@ +'use strict'; + +function randomString (length, chars) { + let mask = ''; + if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; + if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + if (chars.indexOf('#') > -1) mask += '0123456789'; + if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; + + let result = ''; + + for (let i = length; i > 0; --i) + result += mask[Math.floor(Math.random() * mask.length)]; + + return result; +} + + +module.exports = { + randomString +}; \ No newline at end of file diff --git a/tests/iob.test.js b/tests/iob.test.js index 30872e4fb4d..3b92fb8d05a 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -7,10 +7,11 @@ describe('IOB', function() { var ctx = {}; ctx.language = require('../lib/language')(); ctx.language.set('en'); + ctx.settings = require('../lib/settings')(); var iob = require('../lib/plugins/iob')(ctx); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var sbx = { properties: { @@ -20,14 +21,14 @@ describe('IOB', function() { } }; - iob.alexa.intentHandlers.length.should.equal(1); - iob.alexa.rollupHandlers.length.should.equal(1); + iob.virtAsst.intentHandlers.length.should.equal(1); + iob.virtAsst.rollupHandlers.length.should.equal(1); - iob.alexa.intentHandlers[0].intentHandler(function next(title, response) { + iob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current IOB'); response.should.equal('You have 1.50 units of insulin on board'); - iob.alexa.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + iob.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { should.not.exist(err); response.results.should.equal('and you have 1.50 units of insulin on board.'); response.priority.should.equal(2); diff --git a/tests/loop.test.js b/tests/loop.test.js index 9c65ff9bdd1..bfe11d5075c 100644 --- a/tests/loop.test.js +++ b/tests/loop.test.js @@ -6,6 +6,7 @@ var moment = require('moment'); var ctx = { language: require('../lib/language')() + , settings: require('../lib/settings')() }; ctx.language.set('en'); var env = require('../env')(); @@ -243,7 +244,7 @@ describe('loop', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -255,14 +256,14 @@ describe('loop', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); loop.setProperties(sbx); - loop.alexa.intentHandlers.length.should.equal(2); + loop.virtAsst.intentHandlers.length.should.equal(2); - loop.alexa.intentHandlers[0].intentHandler(function next(title, response) { + loop.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Loop Forecast'); response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); - loop.alexa.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Last loop'); + loop.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last Loop'); response.should.equal('The last successful loop was a few seconds ago'); done(); }, [], sbx); diff --git a/tests/mongo-storage.test.js b/tests/mongo-storage.test.js index df85766a29d..ee706906ff8 100644 --- a/tests/mongo-storage.test.js +++ b/tests/mongo-storage.test.js @@ -36,7 +36,7 @@ describe('mongo storage', function () { (function () { return require('../lib/storage/mongo-storage')(env, false, true); - }).should.throw('MongoDB connection string is missing. Please set MONGO_CONNECTION environment variable'); + }).should.throw('MongoDB connection string is missing. Please set MONGODB_URI environment variable'); done(); }); @@ -44,11 +44,17 @@ describe('mongo storage', function () { it('An invalid connection-string should throw an error.', function (done) { env.storageURI = 'This is not a MongoDB connection-string'; - (function () { - return require('../lib/storage/mongo-storage')(env, false, true); - }).should.throw(Error); - - done(); + (async function () { + try { + let foo = await require('../lib/storage/mongo-storage')(env, false, true); + false.should.be.true(); + } + catch (err) { + console.log('We have failed, this is good!'); + done(); + } + })(); + }); }); diff --git a/tests/openaps.test.js b/tests/openaps.test.js index ed3dd6d3b9f..b2e767c8fe2 100644 --- a/tests/openaps.test.js +++ b/tests/openaps.test.js @@ -6,6 +6,7 @@ var moment = require('moment'); var ctx = { language: require('../lib/language')() + , settings: require('../lib/settings')() }; ctx.language.set('en'); var env = require('../env')(); @@ -370,7 +371,7 @@ describe('openaps', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -382,14 +383,14 @@ describe('openaps', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); openaps.setProperties(sbx); - openaps.alexa.intentHandlers.length.should.equal(2); + openaps.virtAsst.intentHandlers.length.should.equal(2); - openaps.alexa.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Loop Forecast'); + openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('OpenAPS Forecast'); response.should.equal('The OpenAPS Eventual BG is 125'); - openaps.alexa.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Last loop'); + openaps.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last Loop'); response.should.equal('The last successful loop was 2 minutes ago'); done(); }, [], sbx); diff --git a/tests/profile.test.js b/tests/profile.test.js index 8171f459e3d..5928ccd2618 100644 --- a/tests/profile.test.js +++ b/tests/profile.test.js @@ -5,6 +5,10 @@ describe('Profile', function ( ) { var profile_empty = require('../lib/profilefunctions')(); + beforeEach(function() { + profile_empty.clear(); + }); + it('should say it does not have data before it has data', function() { var hasData = profile_empty.hasData(); hasData.should.equal(false); @@ -30,8 +34,6 @@ describe('Profile', function ( ) { }; var profile = require('../lib/profilefunctions')([profileData]); -// console.log(profile); - var now = Date.now(); it('should know what the DIA is with old style profiles', function() { @@ -186,5 +188,199 @@ describe('Profile', function ( ) { dia.should.equal(9); }); + var multiProfileData = + [ + { + "startDate": "2015-06-25T00:00:00.000Z", + "defaultProfile": "20150625-1", + "store": { + "20150625-1": { + "dia": "4", + "timezone": moment.tz().zoneName(), //Assume these are in the localtime zone so tests pass when not on UTC time + "startDate": "1970-01-01T00:00:00.000Z", + 'sens': [ + { + 'time': '00:00', + 'value': 12 + }, + { + 'time': '02:00', + 'value': 13 + }, + { + 'time': '07:00', + 'value': 14 + } + ], + 'carbratio': [ + { + 'time': '00:00', + 'value': 16 + }, + { + 'time': '06:00', + 'value': 15 + }, + { + 'time': '14:00', + 'value': 17 + } + ], + 'carbs_hr': 30, + 'target_low': 4.5, + 'target_high': 8, + "units": "mmol", + "basal": [ + { + "time": "00:00", + "value": "0.5", + "timeAsSeconds": "0" + }, + { + "time": "09:00", + "value": "0.25", + "timeAsSeconds": "32400" + }, + { + "time": "12:30", + "value": "0.9", + "timeAsSeconds": "45000" + }, + { + "time": "17:00", + "value": "0.3", + "timeAsSeconds": "61200" + }, + { + "time": "20:00", + "value": "1", + "timeAsSeconds": "72000" + } + ] + } + }, + "units": "mmol", + "mills": "1435190400000" + }, + { + "startDate": "2015-06-21T00:00:00.000Z", + "defaultProfile": "20190621-1", + "store": { + "20190621-1": { + "dia": "4", + "timezone": moment.tz().zoneName(), //Assume these are in the localtime zone so tests pass when not on UTC time + "startDate": "1970-01-01T00:00:00.000Z", + 'sens': [ + { + 'time': '00:00', + 'value': 11 + }, + { + 'time': '02:00', + 'value': 10 + }, + { + 'time': '07:00', + 'value': 9 + } + ], + 'carbratio': [ + { + 'time': '00:00', + 'value': 12 + }, + { + 'time': '06:00', + 'value': 13 + }, + { + 'time': '14:00', + 'value': 14 + } + ], + 'carbs_hr': 35, + 'target_low': 4.2, + 'target_high': 9, + "units": "mmol", + "basal": [ + { + "time": "00:00", + "value": "0.3", + "timeAsSeconds": "0" + }, + { + "time": "09:00", + "value": "0.4", + "timeAsSeconds": "32400" + }, + { + "time": "12:30", + "value": "0.5", + "timeAsSeconds": "45000" + }, + { + "time": "17:00", + "value": "0.6", + "timeAsSeconds": "61200" + }, + { + "time": "23:00", + "value": "0.7", + "timeAsSeconds": "82800" + } + ] + } + }, + "units": "mmol", + "mills": "1434844800000" + } + ]; + + var multiProfile = require('../lib/profilefunctions')(multiProfileData); + + var noon = new Date('2015-06-22 12:00:00').getTime(); + var threepm = new Date('2015-06-26 15:00:00').getTime(); + + it('should return profile units when configured', function () { + var value = multiProfile.getUnits(); + value.should.equal('mmol'); + }); + + + it('should know what the basal rate is at 12:00 with multiple profiles', function () { + var value = multiProfile.getBasal(noon); + value.should.equal(0.4); + }); + + it('should know what the basal rate is at 15:00 with multiple profiles', function () { + var value = multiProfile.getBasal(threepm); + value.should.equal(0.9); + }); + + it('should know what the carbratio is at 12:00 with multiple profiles', function () { + var carbRatio = multiProfile.getCarbRatio(noon); + carbRatio.should.equal(13); + }); + + it('should know what the carbratio is at 15:00 with multiple profiles', function () { + var carbRatio = multiProfile.getCarbRatio(threepm); + carbRatio.should.equal(17); + }); + + it('should know what the sensitivity is at 12:00 with multiple profiles', function () { + var dia = multiProfile.getSensitivity(noon); + dia.should.equal(9); + }); + + it('should know what the sensitivity is at 15:00 with multiple profiles', function () { + var dia = multiProfile.getSensitivity(threepm); + dia.should.equal(14); + }); + + + it('should select the correct profile for 15:00 with multiple profiles', function () { + var curProfile = multiProfile.getCurrentProfile(threepm); + curProfile.carbs_hr.should.equal(30); + }); }); \ No newline at end of file diff --git a/tests/profileeditor.test.js b/tests/profileeditor.test.js index 9c915027c84..42656b511af 100644 --- a/tests/profileeditor.test.js +++ b/tests/profileeditor.test.js @@ -4,7 +4,6 @@ require('should'); var _ = require('lodash'); var benv = require('benv'); var read = require('fs').readFileSync; -var serverSettings = require('./fixtures/default-server-settings'); var nowData = require('../lib/data/ddata')(); nowData.sgvs.push({ mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' }); @@ -66,7 +65,7 @@ var exampleProfile = { var someData = { - '/api/v1/profile.json': [exampleProfile] + '/api/v1/profile.json?count=20': [exampleProfile] }; @@ -121,7 +120,8 @@ describe('Profile editor', function ( ) { client.init(); client.dataUpdate(nowData); - //var result = $('body').html(); + // var result = $('body').html(); + // console.log(result); //var filesys = require('fs'); //var logfile = filesys.createWriteStream('out.html', { flags: 'a'} ) //logfile.write($('body').html()); diff --git a/tests/pump.test.js b/tests/pump.test.js index c6def822058..d051cbe7163 100644 --- a/tests/pump.test.js +++ b/tests/pump.test.js @@ -6,6 +6,7 @@ var moment = require('moment'); var ctx = { language: require('../lib/language')() + , settings: require('../lib/settings')() }; ctx.language.set('en'); var env = require('../env')(); @@ -254,7 +255,7 @@ describe('pump', function ( ) { done(); }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var ctx = { settings: { units: 'mg/dl' @@ -266,16 +267,28 @@ describe('pump', function ( ) { var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); pump.setProperties(sbx); - pump.alexa.intentHandlers.length.should.equal(2); + pump.virtAsst.intentHandlers.length.should.equal(4); - pump.alexa.intentHandlers[0].intentHandler(function next(title, response) { - title.should.equal('Remaining insulin'); + pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Insulin Remaining'); response.should.equal('You have 86.4 units remaining'); - pump.alexa.intentHandlers[1].intentHandler(function next(title, response) { - title.should.equal('Pump battery'); + pump.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Pump Battery'); response.should.equal('Your pump battery is at 1.52 volts'); - done(); + + pump.virtAsst.intentHandlers[2].intentHandler(function next(title, response) { + title.should.equal('Insulin Remaining'); + response.should.equal('You have 86.4 units remaining'); + + pump.virtAsst.intentHandlers[3].intentHandler(function next(title, response) { + title.should.equal('Pump Battery'); + response.should.equal('Your pump battery is at 1.52 volts'); + done(); + }, [], sbx); + + }, [], sbx); + }, [], sbx); }, [], sbx); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js index ab91d2bf722..48c21186cc5 100644 --- a/tests/rawbg.test.js +++ b/tests/rawbg.test.js @@ -35,16 +35,16 @@ describe('Raw BG', function ( ) { }); - it('should handle alexa requests', function (done) { + it('should handle virtAsst requests', function (done) { var sandbox = require('../lib/sandbox')(); var sbx = sandbox.clientInit(ctx, Date.now(), data); rawbg.setProperties(sbx); - rawbg.alexa.intentHandlers.length.should.equal(1); + rawbg.virtAsst.intentHandlers.length.should.equal(1); - rawbg.alexa.intentHandlers[0].intentHandler(function next(title, response) { + rawbg.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { title.should.equal('Current Raw BG'); response.should.equal('Your raw bg is 113'); diff --git a/tests/reports.test.js b/tests/reports.test.js index 3c79e3b096a..c77f87f2e8f 100644 --- a/tests/reports.test.js +++ b/tests/reports.test.js @@ -15,19 +15,19 @@ var nowData = { var someData = { '/api/v1/entries.json?find[date][$gte]=1438992000000&find[date][$lt]=1439078400000&count=10000': [{'_id':'55c697f9459cf1fa5ed71cd8','unfiltered':213888,'filtered':218560,'direction':'Flat','device':'dexcom','rssi':172,'sgv':208,'dateString':'Sat Aug 08 16:58:44 PDT 2015','type':'sgv','date':1439078324000,'noise':1},{'_id':'55c696cc459cf1fa5ed71cd7','unfiltered':217952,'filtered':220864,'direction':'Flat','device':'dexcom','rssi':430,'sgv':212,'dateString':'Sat Aug 08 16:53:45 PDT 2015','type':'sgv','date':1439078025000,'noise':1},{'_id':'55c5d0c6459cf1fa5ed71a04','device':'dexcom','scale':1.1,'dateString':'Sat Aug 08 02:48:05 PDT 2015','date':1439027285000,'type':'cal','intercept':31102.323470336833,'slope':776.9097574914869},{'_id':'55c5d0c5459cf1fa5ed71a03','device':'dexcom','dateString':'Sat Aug 08 02:48:03 PDT 2015','mbg':120,'date':1439027283000,'type':'mbg'}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-08-09T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Correction Bolus','glucose':201,'glucoseType':'Finger','insulin':0.65,'units':'mg/dl','created_at':'2015-08-08T23:22:00.000Z','_id':'55c695628a00a3c97a6611ed'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':163,'glucoseType':'Sensor','insulin':0.7,'units':'mg/dl','created_at':'2015-08-08T22:53:11.021Z','_id':'55c68857cd6dd2036036705f'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-08-09T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Correction Bolus','glucose':201,'glucoseType':'Finger','insulin':0.65,'units':'mg/dl','created_at':'2015-08-08T23:22:00.000Z','_id':'55c695628a00a3c97a6611ed'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':163,'glucoseType':'Sensor','insulin':0.7,'units':'mg/dl','created_at':'2015-08-08T22:53:11.021Z','_id':'55c68857cd6dd2036036705f'}], '/api/v1/entries.json?find[date][$gte]=1439078400000&find[date][$lt]=1439164800000&count=10000': [{'_id':'55c7e85f459cf1fa5ed71dc8','unfiltered':183520,'filtered':193120,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':161,'sgv':149,'dateString':'Sun Aug 09 16:53:40 PDT 2015','type':'sgv','date':1439164420000,'noise':1},{'_id':'55c7e270459cf1fa5ed71dc7','unfiltered':199328,'filtered':192608,'direction':'Flat','device':'dexcom','rssi':161,'sgv':166,'dateString':'Sun Aug 09 16:28:40 PDT 2015','type':'sgv','date':1439162920000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-09T00:00:00.000Z&find[created_at][$lt]=2015-08-10T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Snack Bolus','carbs':18,'insulin':1.1,'created_at':'2015-08-09T22:41:56.253Z','_id':'55c7d734270fbd97191013c2'},{'enteredBy':'Dad','eventType':'Carb Correction','carbs':5,'created_at':'2015-08-09T21:39:13.995Z','_id':'55c7c881270fbd97191013b4'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-09T00:00:00.000Z&find[created_at][$lt]=2015-08-10T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Snack Bolus','carbs':18,'insulin':1.1,'created_at':'2015-08-09T22:41:56.253Z','_id':'55c7d734270fbd97191013c2'},{'enteredBy':'Dad','eventType':'Carb Correction','carbs':5,'created_at':'2015-08-09T21:39:13.995Z','_id':'55c7c881270fbd97191013b4'}], '/api/v1/entries.json?find[date][$gte]=1439164800000&find[date][$lt]=1439251200000&count=10000': [{'_id':'55c93af4459cf1fa5ed71ecc','unfiltered':193248,'filtered':188384,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':194,'sgv':193,'dateString':'Mon Aug 10 16:58:36 PDT 2015','type':'sgv','date':1439251116000,'noise':1},{'_id':'55c939d8459cf1fa5ed71ecb','unfiltered':189888,'filtered':184960,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':931,'sgv':188,'dateString':'Mon Aug 10 16:53:38 PDT 2015','type':'sgv','date':1439250818000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-10T00:00:00.000Z&find[created_at][$lt]=2015-08-11T00:00:00.000Z': [{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':180,'glucoseType':'Sensor','carbs':18,'insulin':1.9,'units':'mg/dl','created_at':'2015-08-10T23:53:31.970Z','_id':'55c9397b865550df020e3560'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':140,'glucoseType':'Finger','carbs':50,'insulin':3.4,'units':'mg/dl','created_at':'2015-08-10T20:41:23.516Z','_id':'55c90c73865550df020e3539'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-10T00:00:00.000Z&find[created_at][$lt]=2015-08-11T00:00:00.000Z&count=1000': [{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':180,'glucoseType':'Sensor','carbs':18,'insulin':1.9,'units':'mg/dl','created_at':'2015-08-10T23:53:31.970Z','_id':'55c9397b865550df020e3560'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':140,'glucoseType':'Finger','carbs':50,'insulin':3.4,'units':'mg/dl','created_at':'2015-08-10T20:41:23.516Z','_id':'55c90c73865550df020e3539'}], '/api/v1/entries.json?find[date][$gte]=1439251200000&find[date][$lt]=1439337600000&count=10000': [{'_id':'55ca8c6e459cf1fa5ed71fe2','unfiltered':174080,'filtered':184576,'direction':'FortyFiveDown','device':'dexcom','rssi':169,'sgv':156,'dateString':'Tue Aug 11 16:58:32 PDT 2015','type':'sgv','date':1439337512000,'noise':1},{'_id':'55ca8b42459cf1fa5ed71fe1','unfiltered':180192,'filtered':192768,'direction':'FortyFiveDown','device':'dexcom','rssi':182,'sgv':163,'dateString':'Tue Aug 11 16:53:32 PDT 2015','type':'sgv','date':1439337212000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-11T00:00:00.000Z&find[created_at][$lt]=2015-08-12T00:00:00.000Z': [{'created_at':'2015-08-11T23:37:00.000Z','eventType':'Snack Bolus','carbs':18,'_id':'55ca8644ca3c57683d19c211'},{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':203,'glucoseType':'Sensor','insulin':1,'preBolus':15,'units':'mg/dl','created_at':'2015-08-11T23:22:00.000Z','_id':'55ca8644ca3c57683d19c210'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-11T00:00:00.000Z&find[created_at][$lt]=2015-08-12T00:00:00.000Z&count=1000': [{'created_at':'2015-08-11T23:37:00.000Z','eventType':'Snack Bolus','carbs':18,'_id':'55ca8644ca3c57683d19c211'},{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':203,'glucoseType':'Sensor','insulin':1,'preBolus':15,'units':'mg/dl','created_at':'2015-08-11T23:22:00.000Z','_id':'55ca8644ca3c57683d19c210'}], '/api/v1/entries.json?find[date][$gte]=1439337600000&find[date][$lt]=1439424000000&count=10000': [{'_id':'55cbddee38a8d88ad1b48647','unfiltered':165760,'filtered':167488,'direction':'Flat','device':'dexcom','rssi':165,'sgv':157,'dateString':'Wed Aug 12 16:58:28 PDT 2015','type':'sgv','date':1439423908000,'noise':1},{'_id':'55cbdccc38a8d88ad1b48644','unfiltered':167456,'filtered':169312,'direction':'Flat','device':'dexcom','rssi':168,'sgv':159,'dateString':'Wed Aug 12 16:53:28 PDT 2015','type':'sgv','date':1439423608000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-12T00:00:00.000Z&find[created_at][$lt]=2015-08-13T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Correction Bolus','insulin':0.8,'created_at':'2015-08-12T23:21:08.907Z','_id':'55cbd4e47e726599048a3f91'},{'enteredBy':'Dad','eventType':'Note','notes':'Milk now','created_at':'2015-08-12T21:23:00.000Z','_id':'55cbba4e7e726599048a3f79'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-12T00:00:00.000Z&find[created_at][$lt]=2015-08-13T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Correction Bolus','insulin':0.8,'created_at':'2015-08-12T23:21:08.907Z','_id':'55cbd4e47e726599048a3f91'},{'enteredBy':'Dad','eventType':'Note','notes':'Milk now','created_at':'2015-08-12T21:23:00.000Z','_id':'55cbba4e7e726599048a3f79'}], '/api/v1/entries.json?find[date][$gte]=1439424000000&find[date][$lt]=1439510400000&count=10000': [{'_id':'55cd2f6738a8d88ad1b48ca1','unfiltered':209792,'filtered':229344,'direction':'SingleDown','device':'dexcom','rssi':436,'sgv':205,'dateString':'Thu Aug 13 16:58:24 PDT 2015','type':'sgv','date':1439510304000,'noise':1},{'_id':'55cd2e3b38a8d88ad1b48c95','unfiltered':220928,'filtered':237472,'direction':'FortyFiveDown','device':'dexcom','rssi':418,'sgv':219,'dateString':'Thu Aug 13 16:53:24 PDT 2015','type':'sgv','date':1439510004000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-13T00:00:00.000Z&find[created_at][$lt]=2015-08-14T00:00:00.000Z': [{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':250,'glucoseType':'Sensor','insulin':0.75,'units':'mg/dl','created_at':'2015-08-13T23:45:56.927Z','_id':'55cd2c3497fa97ac5d8bc53b'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':198,'glucoseType':'Sensor','insulin':1.1,'units':'mg/dl','created_at':'2015-08-13T23:11:00.293Z','_id':'55cd240497fa97ac5d8bc535'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-13T00:00:00.000Z&find[created_at][$lt]=2015-08-14T00:00:00.000Z&count=1000': [{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':250,'glucoseType':'Sensor','insulin':0.75,'units':'mg/dl','created_at':'2015-08-13T23:45:56.927Z','_id':'55cd2c3497fa97ac5d8bc53b'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':198,'glucoseType':'Sensor','insulin':1.1,'units':'mg/dl','created_at':'2015-08-13T23:11:00.293Z','_id':'55cd240497fa97ac5d8bc535'}], '/api/v1/entries.json?find[date][$gte]=1439510400000&find[date][$lt]=1439596800000&count=10000': [{'_id':'55ce80e338a8d88ad1b49397','unfiltered':179936,'filtered':202080,'direction':'SingleDown','device':'dexcom','rssi':179,'sgv':182,'dateString':'Fri Aug 14 16:58:20 PDT 2015','type':'sgv','date':1439596700000,'noise':1},{'_id':'55ce7fb738a8d88ad1b4938d','unfiltered':192288,'filtered':213792,'direction':'SingleDown','device':'dexcom','rssi':180,'sgv':197,'dateString':'Fri Aug 14 16:53:20 PDT 2015','type':'sgv','date':1439596400000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-14T00:00:00.000Z&find[created_at][$lt]=2015-08-15T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Site Change','glucose':268,'glucoseType':'Finger','insulin':1.75,'units':'mg/dl','created_at':'2015-08-14T00:00:00.000Z','_id':'55ce78fe925aa80e7071e5d6'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':89,'glucoseType':'Finger','carbs':54,'insulin':3.15,'units':'mg/dl','created_at':'2015-08-14T21:00:00.000Z','_id':'55ce59bb925aa80e7071e5ba'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-14T00:00:00.000Z&find[created_at][$lt]=2015-08-15T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Site Change','glucose':268,'glucoseType':'Finger','insulin':1.75,'units':'mg/dl','created_at':'2015-08-14T00:00:00.000Z','_id':'55ce78fe925aa80e7071e5d6'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':89,'glucoseType':'Finger','carbs':54,'insulin':3.15,'units':'mg/dl','created_at':'2015-08-14T21:00:00.000Z','_id':'55ce59bb925aa80e7071e5ba'}], '/api/v1/entries.json?find[date][$gte]=1439596800000&find[date][$lt]=1439683200000&count=10000': [{'_id':'55cfd25f38a8d88ad1b49931','unfiltered':283136,'filtered':304768,'direction':'SingleDown','device':'dexcom','rssi':185,'sgv':306,'dateString':'Sat Aug 15 16:58:16 PDT 2015','type':'sgv','date':1439683096000,'noise':1},{'_id':'55cfd13338a8d88ad1b4992e','unfiltered':302528,'filtered':312576,'direction':'FortyFiveDown','device':'dexcom','rssi':179,'sgv':329,'dateString':'Sat Aug 15 16:53:16 PDT 2015','type':'sgv','date':1439682796000,'noise':1}], '/api/v1/food/regular.json': [{'_id':'552ece84a6947ea011db35bb','type':'food','category':'Zakladni','subcategory':'Sladkosti','name':'Bebe male','portion':18,'carbs':12,'gi':1,'unit':'pcs','created_at':'2015-04-15T20:48:04.966Z'}], '/api/v1/treatments.json?find[eventType]=/BG Check/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.000Z': [ @@ -261,12 +261,14 @@ describe('reports', function ( ) { var result = $('body').html(); //var filesys = require('fs'); //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) - //logfile.write($('body').html()); - + //logfile.write(result); + //console.log('RESULT', result); + result.indexOf('Milk now').should.be.greaterThan(-1); // daytoday - result.indexOf('50 g (1.67U)').should.be.greaterThan(-1); // daytoday + result.indexOf('50 g').should.be.greaterThan(-1); // daytoday + result.indexOf('TDD average: 2.9U').should.be.greaterThan(-1); // daytoday result.indexOf('
0%100%0%264.7%6In Range: 47.6%1016 (100%)
+ + + + +
+ + + + + +

Oops - Nightscout is having trouble

+ +

Don't panic, we can work this out! This happens to the best of us.

+

Check the errors below and then refer to the + troubleshooting documentation.

+ +

Errors occurred during startup:

+
<%- errors %>
+
+ + + + diff --git a/views/foodindex.html b/views/foodindex.html index d42eb16a50b..7168897467f 100644 --- a/views/foodindex.html +++ b/views/foodindex.html @@ -24,30 +24,17 @@ - + - + <% include preloadCSS %> - + + <%- include('partials/toolbar') %> -
X
- - -
-
- Status: Not loaded -
-

Nightscout

-
-

Food Editor

-
-
- -
Your database @@ -121,12 +108,12 @@

Food Editor

- Authentication status: -
- - - - + + <%- include('partials/authentication-status') %> + + + + diff --git a/views/frame.html b/views/frame.html new file mode 100644 index 00000000000..aff6ea0d6ce --- /dev/null +++ b/views/frame.html @@ -0,0 +1,54 @@ +<% + +let urlArray = []; +let nameArray = []; + +for (let i = 0; i <= 8; i++) { + let u = settings['frameUrl' + i]; + let n = settings['frameName' + i] || " "; + if (u) { + urlArray.push(u); + nameArray.push(n); + } +} + +const sitesPerRow = urlArray.length > 3 ? Math.round(urlArray.length / 2) : urlArray.length; +const rows = urlArray.length > 3 ? 2 : 1; + +%> + + Nightscout multiframe view + + + + + + <% let s = 0; + for (let r = 1; r <= rows; r++) { + %> + <% for (let sp = 0; sp < sitesPerRow; sp++) { + let pointer = sp + s; + %> + <% } %> + + + <% for (let sp = 0; sp < sitesPerRow; sp++) { + let pointer = sp + s; + %> + <% } %> + + <% s += sitesPerRow; } %> +
<%= nameArray[pointer] %>
+ + + \ No newline at end of file diff --git a/views/index.html b/views/index.html index 3a156008903..dbff801392f 100644 --- a/views/index.html +++ b/views/index.html @@ -1,699 +1,751 @@ - - - - - - - - Nightscout - - - - - - - - - - - - - - - - - - - - - - - - - -<% include preloadCSS %> - - -
-
-

-

Loading the client

-
-
-
-
-
-
-
-
-
- -
Nightscout
-
-
- -
-
-
- - -
-
-
- -
-
-
---
-
-
-
-
-
    -
  • 2HR
  • -
  • 3HR
  • -
  • 6HR
  • -
  • 12HR
  • -
  • 24HR
  • -
  • ...
  • -
- -
-
-
-
+ + + <% include preloadCSS %> + + + +
+
+

+

Loading the client

+
+
+
+
+
+
+
+
+ <%- include('partials/toolbar') %> + +
+ +
+
+
+ + +
+
+
-
-
- -
- Settings -
-
Units
-
-
-
-
-
Date format
-
-
-
-
-
Language
-
- -
-
-
-
Scale
-
- -
-
-
-
Render Basal
-
- -
-
-
-
Enable Alarms
-
-
-
-
-
- - - mins -
-
- - - mins -
-
-
-
-
Night Mode
-
-
-
-
Edit Mode
-
-
-
-
Show Raw BG Data
-
-
-
-
-
-
Custom Title
-
-
-
-
Theme
-
-
-
-
-
-
Show Plugins
-
-
- - - -
- - -
- About -
-
version
-
head
-

-

License: AGPL
-
Copyright © 2017 Nightscout contributors
-

- -
-
+
+
+
---
+
+
+
+
+ +
    +
  • Hours:
  • +
  • 2
  • +
  • 3
  • +
  • 4
  • +
  • 6
  • +
  • 12
  • +
  • 24
  • +
  • ...
  • +
+
+ +
+
+
+
+
-
-
-
- Log a Treatment - - - -
- Targets - - -
- -
- Glucose Reading - - - - -
-
- - - -
- - - - - - - - - - -
- - - -
- Event Time - - - - -
- - -
-
+
+
+ +
+ Settings +
+
Units
+
+
+
+
+
Date format
+
+
+
+
+
Language
+
+ +
+
+
+
Scale
+
+ +
+
+
+
Render Basal
+
+ +
+
+
+
Render Bolus Amount
+
+ +
+
+ +
+
Enable Alarms
+
+
+
+
+
+ + + mins +
+
+ + + mins +
+
+
+
+
Night Mode
+
+
+
+
Edit Mode
+
+
+
+
Show Raw BG Data
+
+
+
+
+
+
Custom Title
+
+
+
+
Theme
+
+
+
+
+
+
Show Plugins
+
+
+ + + + <%- include('partials/authentication-status') %> + +
+ About +
+
version
+
head
+

+

License: AGPL
+
Copyright © 2017 Nightscout contributors
+

+ +
+
+
+ +
+
+
+ Log a Treatment + + -
- -
- - Bolus Wizard - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - BG: - - - - -
- - - - - - - - - - 0.00 -
- Quickpick: -
- -
- - or - - Add from database - - -
-
-
- - - Carbs: - - g - - - -
- - - COB: - - g - - - -
- - - IOB: - - 0.00 -
- - Other correction: - - -
- - Rounding: - - 0.00 -
- - Calculation is in target range. -
- - - Insulin needed: - - 0.00 -
- - Carbs needed: - - - -
- - Basal rate: - - -
-
- +
+ Targets + + +
+ +
+ Glucose Reading + + + + +
+ + + + + +
+ + + +
+ + + + + + + + + + + +
+ + + +
+ Event Time + + + + +
+ + +
+ +
+ +
+
+
+ + Bolus Wizard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + BG: + + + + +
+ + + + + + + + + + 0.00 +
+ Quickpick:
- +
+ + or + + Add from database + + +
+
+
+ + + Carbs: + + g + + + +
+ + + COB: + + g + + + +
+ + + IOB: + + 0.00 +
+ + Other correction: + + +
+ + Rounding: + + 0.00 +
+ + Calculation is in target range. +
+ + + Insulin needed: + + 0.00 +
+ + Carbs needed: + + + +
+ + Basal rate: + + +
+
+ +
+ +
- - - -
+ + + +
- - - -
- Event Time: - - - - - - -
- -
- -
- - + + + +
+ Event Time: + + + + + +
+ +
+ + + +
-
- - -
+
+ + +
+ + + + + + + + - - - - diff --git a/views/nightscout.appcache b/views/nightscout.appcache deleted file mode 100644 index 3823f894e5f..00000000000 --- a/views/nightscout.appcache +++ /dev/null @@ -1,37 +0,0 @@ -CACHE MANIFEST - -/images/launch.png -/images/apple-touch-icon-57x57.png -/images/apple-touch-icon-60x60.png -/images/apple-touch-icon-72x72.png -/images/apple-touch-icon-76x76.png -/images/apple-touch-icon-114x114.png -/images/apple-touch-icon-120x120.png -/images/apple-touch-icon-144x144.png -/images/apple-touch-icon-152x152.png -/images/apple-touch-icon-180x180.png -/images/favicon-32x32.png -/images/android-chrome-192x192.png -/images/favicon-96x96.png -/images/favicon-16x16.png -/manifest.json -/images/favicon.ico -/images/mstile-144x144.png -/css/ui-darkness/jquery-ui.min.css?v=<%= locals.cachebuster %> -/css/jquery.tooltips.css?v=<%= locals.cachebuster %> -/audio/alarm.mp3 -/audio/alarm2.mp3 -/css/ui-darkness/images/ui-icons_ffffff_256x240.png -/css/ui-darkness/images/ui-icons_cccccc_256x240.png -/css/ui-darkness/images/ui-bg_inset-soft_25_000000_1x100.png -/css/ui-darkness/images/ui-bg_gloss-wave_25_333333_500x100.png -/css/main.css?v=<%= locals.cachebuster %> -/bundle/js/bundle.app.js?v=<%= locals.cachebuster %> -/bundle/js/bundle.clock.js?v=<%= locals.cachebuster %> -/bundle/js/bundle.report.js?v=<%= locals.cachebuster %> -/socket.io/socket.io.js?v=<%= locals.cachebuster %> -/js/client.js?v=<%= locals.cachebuster %> -/images/logo2.png - -NETWORK: -* diff --git a/views/partials/authentication-status.ejs b/views/partials/authentication-status.ejs new file mode 100644 index 00000000000..db969c906b1 --- /dev/null +++ b/views/partials/authentication-status.ejs @@ -0,0 +1,3 @@ +
+ Authentication status: +
\ No newline at end of file diff --git a/views/partials/toolbar.ejs b/views/partials/toolbar.ejs new file mode 100644 index 00000000000..b98f2756c3d --- /dev/null +++ b/views/partials/toolbar.ejs @@ -0,0 +1,34 @@ +
+

Nightscout

+ + <%if (type !== 'index') { %> + X + <% } %> + + <%if (type === 'food') { %> +
+ Status: Not loaded +
+ <% } %> + <%if (type === 'profile') { %> +
+ Status: Not loaded +
+ <% } %> + <%if (type === 'index') { %> + + <% } %> +
+ +<%if (title) { %> +
+

<%= title %>

+
+<% } %> \ No newline at end of file diff --git a/views/profileindex.html b/views/profileindex.html index d64f1300917..d915d0c8cc8 100644 --- a/views/profileindex.html +++ b/views/profileindex.html @@ -25,26 +25,15 @@ - + - + <% include preloadCSS %> - -
X
- -
-
- Status: Not loaded -
-

Nightscout

-
- -
-

Profile Editor

-
+ + <%- include('partials/toolbar') %>
@@ -168,18 +157,17 @@

Profile Editor

- Authentication status: - -

Status: Not loaded

+ <%- include('partials/authentication-status') %> + - - - + + + diff --git a/views/reportindex.html b/views/reportindex.html index 5fa745ce34c..8d02bf08f58 100644 --- a/views/reportindex.html +++ b/views/reportindex.html @@ -1,39 +1,40 @@ - - Nightscout reporting + + Nightscout Reporting + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + <% include preloadCSS %> + + + + <%- include('partials/toolbar') %> - - - <% include preloadCSS %> - - - -
X
- -

Nightscout reporting

-
    -
+
    +
+
@@ -48,7 +49,7 @@ Last monthLast 3 months - + - - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
Notes contain:
- Event Type: -
- Mo - Tu - We - Th - Fr - Sa - Su -
- Target bg range bottom: - - top: - -
- Order: - - - -   - - -
+ Event Type: +
+ + + + + + + +
+ Target BG range bottom: + + top: + +
+ Order: + + + +   + + +

+
-
- Authentication status: + <%- include('partials/authentication-status') %> - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/views/service-worker.js b/views/service-worker.js new file mode 100644 index 00000000000..d52441d303a --- /dev/null +++ b/views/service-worker.js @@ -0,0 +1,175 @@ +'use strict'; + +var CACHE = '<%= locals.cachebuster %>'; + +const CACHE_LIST = [ + '/images/launch.png', + '/images/apple-touch-icon-57x57.png', + '/images/apple-touch-icon-60x60.png', + '/images/apple-touch-icon-72x72.png', + '/images/apple-touch-icon-76x76.png', + '/images/apple-touch-icon-114x114.png', + '/images/apple-touch-icon-120x120.png', + '/images/apple-touch-icon-144x144.png', + '/images/apple-touch-icon-152x152.png', + '/images/apple-touch-icon-180x180.png', + '/images/favicon-32x32.png', + '/images/android-chrome-192x192.png', + '/images/favicon-96x96.png', + '/images/favicon-16x16.png', + '/manifest.json', + '/images/favicon.ico', + '/images/mstile-144x144.png', + '/css/ui-darkness/jquery-ui.min.css', + '/css/jquery.tooltips.css', + '/css/ui-darkness/images/ui-icons_ffffff_256x240.png', + '/css/ui-darkness/images/ui-icons_cccccc_256x240.png', + '/css/ui-darkness/images/ui-bg_inset-soft_25_000000_1x100.png', + '/css/ui-darkness/images/ui-bg_gloss-wave_25_333333_500x100.png', + '/css/main.css', + '/bundle/js/bundle.app.js', + '/bundle/js/bundle.clock.js', + '/bundle/js/bundle.report.js', + '/socket.io/socket.io.js', + '/js/client.js', + '/images/logo2.png' +]; + +function returnRangeRequest(request) { + return caches + .open(CACHE) + .then((cache) => { + return cache.match(request.url); + }) + .then((res) => { + if (!res) { + return fetch(request) + .then(res => { + const clonedRes = res.clone(); + return caches + .open(CACHE) + .then(cache => cache.put(request, clonedRes)) + .then(() => res); + }) + .then(res => { + return res.arrayBuffer(); + }); + } + return res.arrayBuffer(); + }) + .then((arrayBuffer) => { + const bytes = /^bytes=(\d+)-(\d+)?$/g.exec( + request.headers.get('range') + ); + if (bytes) { + const start = Number(bytes[1]); + const end = Number(bytes[2]) || arrayBuffer.byteLength - 1; + return new Response(arrayBuffer.slice(start, end + 1), { + status: 206, + statusText: 'Partial Content', + headers: [ + ['Content-Range', `bytes ${start}-${end}/${arrayBuffer.byteLength}`] + ] + }); + } else { + return new Response(null, { + status: 416, + statusText: 'Range Not Satisfiable', + headers: [['Content-Range', `*/${arrayBuffer.byteLength}`]] + }); + } + }); +} + +// Open a cache and `put()` the assets to the cache. +// Return a promise resolving when all the assets are added. +function precache() { + return caches.open(CACHE) + .then((cache) => { + // if any cache requests fail, don't interrupt other requests in progress + return Promise.allSettled( + CACHE_LIST.map((url) => { + // `no-store` in case of partial content responses and + // because we're making our own cache + let request = new Request(url, { cache: 'no-store' }); + return fetch(request).then((response) => { + // console.log('Caching response', url, response); + cache.put(url, response); + }).catch((err) => { + console.log('Could not precache asset', url, err); + }); + }) + ); + }); +} + +// Try to read the requested resource from cache. +// If the requested resource does not exist in the cache, fetch it from +// network and cache the response. +function fromCache(request) { + return caches.open(CACHE).then((cache) => { + return cache.match(request).then((matching) => { + console.log(matching); + if(matching){ + return matching; + } + + return fetch(request).then((response) => { + // console.log('Response from network is:', response); + cache.put(request, response.clone()); + return response; + }).catch((error) => { + // This catch() will handle exceptions thrown from the fetch() operation. + // Note that a HTTP error response (e.g. 404) will NOT trigger an exception. + // It will return a normal response object that has the appropriate error code set. + console.error('Fetching failed:', error); + + throw error; + }); + }); + }); +} + +// On install, cache some resources. +self.addEventListener('install', (evt) => { + // console.log('The service worker is being installed.'); + self.skipWaiting(); + evt.waitUntil(precache()); +}); + +function inCache(request) { + let found = false; + CACHE_LIST.forEach((e) => { + if (request.url.endsWith(e)) { + found = true; + } + }); + return found; +} + +self.addEventListener('fetch', (evt) => { + if (!evt.request.url.startsWith(self.location.origin) || CACHE === 'developmentMode' || !inCache(evt.request) || evt.request.method !== 'GET') { + //console.log('Skipping cache for ', evt.request.url); + return void evt.respondWith(fetch(evt.request)); + } + if (evt.request.headers.get('range')) { + evt.respondWith(returnRangeRequest(evt.request)); + } else { + evt.respondWith(fromCache(evt.request)); + } +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE) { + // console.log('Deleting out of date cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + })); + +}); diff --git a/views/translationsindex.html b/views/translationsindex.html index 4145f3d1db0..6543bb06259 100644 --- a/views/translationsindex.html +++ b/views/translationsindex.html @@ -23,19 +23,18 @@ + <% include preloadCSS %> - -
X
+ + <%- include('partials/toolbar') %> -

Nightscout translations

-
- Authentication status: + <%- include('partials/authentication-status') %>