Merge pull request #4272 from nightscout/release/0.11

Draft release notes for upcoming 0.11 release (currently release candidate phase).

# Changes

Over 360 commits, 89 files changed, +8,428 / −6,569 lines of changes (full list of changes here: 
https://github.com/nightscout/cgm-remote-monitor/pull/4022/commits )

## New features
- Fully secure by default out of the box.  Unsecure access via http is not allowed anymore by default. This might force you to re-authenticate with your `API_SECRET` or token if you were using unsecure access. (@PieterGit )
- No outdated packages with vulnerabilities are being used anymore (@PieterGit ) 
- Add Week to Week report (@jpcunningh, https://github.com/nightscout/cgm-remote-monitor/pull/4123 ) 
- Add Loopalyzer report to analyse looping. Visualize your loop (@lixgbg, https://github.com/nightscout/cgm-remote-monitor/pull/3629 https://github.com/nightscout/cgm-remote-monitor/pull/4235 )
- Add predictions support to Day to Day report (@lixgbg, https://github.com/nightscout/cgm-remote-monitor/pull/3179 )
- Add cgm sensor stop to Careportal (@jpcunningh, https://github.com/nightscout/cgm-remote-monitor/pull/4060)

## Removed features
- remove `mqtt` module, because it had a security issue and was not used
- remove `sgvdata`  module, because it had a security issue, added a lot of complexity and wasn't needed (@PieterGit ). Replacement implementation for CSV and TSV export (@sulkaharo ).

## Improvements
- Fix MongoDB database insert handling. Log error on inserts and don't crash in case the MongoDB disk is full or MongoDB quota is reached (@sulkaharo and @jpcunningh)
- Upgrade packages to recent version, fixing all known security issues with dependencies (@PieterGit)
- Redirect redirect HTTP to HTTPS and implement HSTS (@jweismann, @PieterGit, https://github.com/nightscout/cgm-remote-monitor/pull/4044 and https://github.com/nightscout/cgm-remote-monitor/issues/4010	and https://github.com/nightscout/cgm-remote-monitor/pull/4253 )
- Technical improvement: Migrate from `uglify-js` to `terser-webpack-plugin` (@PieterGit)
- Streamlined Heroku deployment template with more descriptive text and more appropriate defaults for new users (@unsoluble, https://github.com/nightscout/cgm-remote-monitor/pull/4116 )

## Bug fixes
- Fix CGM voltageb battery warning level to match xDrip+ (@jpcunningh, https://github.com/nightscout/cgm-remote-monitor/pull/3954 )
- Fix daylight saving and reloading bug in profile editor, (@DigitalDan1, @Kywalh https://github.com/nightscout/cgm-remote-monitor/pull/4029 and https://github.com/nightscout/cgm-remote-monitor/pull/4074 )
- Reduce the amount of Profile Switch treatments being loaded to fix UI slowdown and Nightscout home screen losing AAPS data from >3 hours ago, (@sulkaharo, @vickster1, https://github.com/nightscout/cgm-remote-monitor/issues/4055 )
- Upgrade to [share2nightscout 0.2.0](https://github.com/nightscout/share2nightscout-bridge/releases/tag/0.2.0). Prevent Nightscout server crashes in case Dexcom server does not respond (@PieterGit, @veryfancy)
- Fix UI so pills are updated immediately after new data is loaded (@sulkaharo)
- Fixes to If-Modified-Since HTTP header handling for BG data (@sulkaharo)

## Documentation and language updates
- Language updates for Danish, Dutch, German, Hebrew, Norwegian, Russian 
- New languages: Japanese, Turkish
- Update Alexa documentation. Note that some Alexa improvements are postponed to Nightscout 0.12 because the Alexa plugin needs refactoring, see https://github.com/nightscout/cgm-remote-monitor/issues/4168#issuecomment-451701994
- Update IFTTT maker-setup.md docs (@Dave9111, @unsoluble, https://github.com/nightscout/cgm-remote-monitor/pull/4206 )
- Updated various docs, including [CONTRIBUTING](https://github.com/nightscout/cgm-remote-monitor/blob/dev/CONTRIBUTING.md) documentation

# Upgrade notes
- We only allow Nightscout to start with a secure Node JS. 
  - Latest Node 8 LTS (8.15.0 or later) and Latest Node 10 LTS (10.15.1 or later) are recommended and supported. 
  - Latest Node version on Azure (currently 10.14.0) is tolerated, but not recommended
  - Other versions will not start 
- The [rawbg](https://github.com/nightscout/cgm-remote-monitor#rawbg-raw-bg) settings are converted to a single setting tri-state variable.
- We improved security and added several new environment variables such as [INSECURE_USE_HTTP and SECURE_HSTS_HEADER](https://github.com/nightscout/cgm-remote-monitor/#predefined-values-for-your-server-settings-optional)
  - Your site redirects to https by default. If you don't want that or use a Nginx or Apache proxy, set `INSECURE_USE_HTTP` to `true`.
  - We enabled [HTTP Strict Transport Security (HSTS)](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) headers by default, settings `SECURE_HSTS_HEADER` and `SECURE_HSTS_HEADER_*`

## Upgrade notes for Azure users

We recommend Azure users consider migrating their hosting to Heroku, as we've observed Heroku users have significantly less issues with having their sites work reliably. If you want to continue using Azure, change the following configuration variables in Azure before updating to the latest Nightscout version:
```
WEBSITE_NODE_DEFAULT_VERSION=10.14.1
SCM_COMMAND_IDLE_TIMEOUT=300
```

# Install instructions

Install instructions can be found: https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md#install

# Contributors to this release

The release coordination for this release was done by @PieterGit 
We would like to thank the following people for their contribution (in alphabetical order):
@anderser, @apanasef, @balshor, @bewest, @blocklist_twitter, @CaroGo, @cascer1, @cluckj, @danamlewis, @Dave9111, @diabetlum, @herzogmedia, @janrpn, @jasoncalabrese, @jpcunningh, @jweismann, @kenstack, @Kywalh, @lixgbg, @LuminaryXion, @MilosKozak, @mitrei, @PaperT1D, @PieterGit, @unsoluble, @rarneson, @renegadeandy, @scottleibrand, @sulkaharo, @T-o-b-i-a-s, @tynbendad, @unsoluble, @veryfancy, @viq, @wootmasterslick

(if I forgot somebody, please respond)

# TODO

TODO: Translations, Languages with less than 80% will be removed in a future Nightscout version. Currently the following languages are at risk: 
中文(繁體) (zh_tw), Hrvatski (hr), Ελληνικά (el), 한국어 (ko)
See https://gitter.im/nightscout/public?at=5bef2f34de42d46bba766f66

TODO: Fix Codacy errors, https://app.codacy.com/app/Nightscout/cgm-remote-monitor/issues?bid=2452379&filters=W3siaWQiOiJDYXRlZ29yeSIsInZhbHVlcyI6WyJFcnJvciBQcm9uZSJdfV0=

TODO: test dev after all new features are merged for at least two weeks
This commit is contained in:
Sulka Haro
2019-02-07 23:12:08 +02:00
committed by GitHub
89 changed files with 8360 additions and 6521 deletions
+1 -1
View File
@@ -1 +1 @@
8.11.x
8.x
+3 -2
View File
@@ -1,9 +1,10 @@
language: node_js
os: osx
node_js:
- "10"
- "8"
os:
- osx
before_install:
- if [[ `npm --version` != "5.8.0" ]]; then npm install -g npm@latest; npm --version; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
# https://github.com/Homebrew/homebrew-core/issues/26358
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew unlink python; fi
+175 -9
View File
@@ -11,6 +11,11 @@
- [Comments and issues](#comments-and-issues)
- [Co-ordination](#co-ordination)
- [Other Dev Tips](#other-dev-tips)
- [List of Contributors](#list-of-contributors)
- [Core developers, contributing developers, coordinators and documentation writers](#core-developers-contributing-developers-coordinators-and-documentation-writers)
- [Plugin contributors](#plugin-contributors)
- [Translators](#translators)
- [List of all contributors](#list-of-all-contributors)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -50,11 +55,11 @@ You can get the dev branch checked out using `git checkout dev`.
## Style Guide
Some simple rules, that will make it easier to maintain our codebase:
Some simple rules that will make it easier to maintain our codebase:
* All indenting should use 2 space where possible (js, css, html, etc)
* A space before function parameters, such as: `function boom (name, callback) { }`, this makes searching for calls easier
* Name your callback functions, such as `boom('the name', function afterBoom ( result ) { }`
* All indenting should use 2 space where possible (js, css, html, etc).
* Include a space before function parameters, such as: `function boom (name, callback) { }`, this makes searching for function calls easier.
* Name your callback functions, such as `boom('the name', function afterBoom ( result ) { }`.
* Don't include author names in the header of your files, if you need to give credit to someone else do it in the commit comment.
* Use single quotes.
* Use the comma first style, for example:
@@ -71,7 +76,15 @@ If in doubt, format your code with `js-beautify --indent-size 2 --comma-first -
## Create a prototype
<<<<<<< HEAD
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.
=======
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.
>>>>>>> master
## Submit a pull request
@@ -82,6 +95,24 @@ 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.
<<<<<<< HEAD
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.
When adding new features that add confugration options, please ensure the `README` document is amended with information on the new configuration.
## Bug fixing
If you've fixed a bug, please consider adding a unit test to the `/tests` folder that reproduces the original bug without the change.
Try to identify the root cause of the issue and fix the issue. Pull requests that simply add null checks to hide issues are unlikely to be accepted.
This can be done by committing your code `git commit -avm 'my
improvements are here'`, and pushing it to the branch you created on your own
fork. This will probably look something like
`git push -u origin wip/add-my-widget`.
=======
Please include a description of what the features do and rationalize why the changes are needed.
@@ -94,6 +125,7 @@ When adding new features that add confugration options, please ensure the `READM
If you've fixed a bug, please consider adding a unit test to the `/tests` folder that reproduces the original bug without the change.
Try to identify the root cause of the issue and fix the issue. Pull requests that simply add null checks to hide issues are unlikely to be accepted.
>>>>>>> master
Please include instructions how to test the changes.
@@ -105,18 +137,152 @@ We encourage liberal use of the comments, including images where appropriate.
Most cgm-remote-monitor hackers use github's ticketing system, along with Facebook cgm-in-the-cloud, and gitter.
<<<<<<< HEAD
We use [git-flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow), with `master` as our production, stable branch, and
`dev` is used to queue changes for upcoming releases. Everything else is
done on branches, hopefully with names that indicate what to expect.
Once `dev` has been reviewed and people feel it's time to release, we
follow the git-flow release process, which creates a new tag and bumps
the version correctly. See [sem-ver](https://semver.org/) for versioning strategy.
Every commit is tested by Travis CI. We encourage adding tests to
validate your design. We encourage discussing your use cases to help
everyone get a better understanding of your design.
=======
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.
Once `dev` has been reviewed and people feel it's time to release, we follow the git-flow release process, which creates a new tag and bumps the version correctly. See sem-ver for versioning strategy.
Every commit is tested by travis. We encourage adding tests to validate your design. We encourage discussing your use cases to help everyone get a better understanding of your design.
>>>>>>> master
## Other Dev Tips
* Join the [Gitter chat][gitter-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, and for the old code too. We are aiming for a full test coverage.
* If your going to be working in old code that needs lots of reformatting consider doing the clean as a separate PR.
* If you can find others to help test your PR is will help get them merged in sooner.
* 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.
* If you're going to be working in old code that needs lots of reformatting, consider doing it as a separate PR.
* If you can find others to help test your PR, it will help get them merged in sooner.
## List of Contributors
We welcome new contributors. We do not only need core contributors. Regular or one time contributors are welcomed as well.
Also if you can't code, it's possible to contribute by improving the documentation or by translating Nightscout in your own language
### Core developers, contributing developers, coordinators and documentation writers
[@andrew-warrington]: https://github.com/andrew-warrington
[@apanasef]: https://github.com/apanasef
[@bewest]: https://github.com/bewest
[@danamlewis]: https://github.com/danamlewis
[@herzogmedia]: https://github.com/herzogmedia
[@jamieowendexcom ]: https://github.com/jamieowendexcom
[@jasoncalabrese]: https://github.com/jasoncalabrese
[@jizhongwen]: https://github.com/jizhongwen
[@jpcunningh]: https://github.com/jpcunningh
[@jweismann]: https://github.com/jweismann
[@komarserjio]: https://github.com/komarserjio
[@LuminaryXion]: https://github.com/LuminaryXion
[@mcdafydd]: https://github.com/mcdafydd
[@mdomox]: https://github.com/mdomox
[@MilosKozak]: https://github.com/MilosKozak
[@oteroos]: https://github.com/oteroos
[@PieterGit]: https://github.com/PieterGit
[@rarneson]: https://github.com/rarneson
[@rickfriele]: https://github.com/rickfriele
[@scottleibrand]: https://github.com/scottleibrand
[@sulkaharo]: https://github.com/sulkaharo
[@tynbendad]: https://github.com/tynbendad
[@unsoluble]: https://github.com/unsoluble
[@viderehh]: https://github.com/viderehh
| Contribution area | List of contributors |
| ------------------------------------- | ---------------------------------- |
| Core developers: | [@jasoncalabrese] [@MilosKozak] [@PieterGit] [@sulkaharo] |
| Former Core developers: (not active): | [@bewest] |
| Contributing developers: | [@jpcunningh] [@scottleibrand] [@komarserjio] [@jweismann] |
| Release coordination 0.10.x: | [@PieterGit] [@sulkaharo] |
| 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] |
### Plugin contributors
| Contribution area | List of developers | List of testers
| ------------------------------------- | -------------------- | -------------------- |
| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| Please volunteer | 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 |
| [`bridge` (Share2Nightscout bridge)](README.md#bridge-share2nightscout-bridge)| Please volunteer | Please volunteer |
| [`bwp` (Bolus Wizard Preview)](README.md#bwp-bolus-wizard-preview)| Please volunteer | Please volunteer |
| [`cage` (Cannula Age)](README.md#cage-cannula-age)| [@jpcunningh] | Please volunteer |
| [`careportal` (Careportal)](README.md#careportal-careportal)| Please volunteer | Please volunteer |
| [`cob` (Carbs-on-Board)](README.md#cob-carbs-on-board)| Please volunteer | Please volunteer |
| [`cors` (CORS)](README.md#cors-cors)| Please volunteer | Please volunteer |
| [`delta` (BG Delta)](README.md#delta-bg-delta)| Please volunteer | Please volunteer |
| [`devicestatus` (Device Status)](README.md#devicestatus-device-status)| Please volunteer | Please volunteer |
| [`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] |
| [`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 |
| [`mmconnect` (MiniMed Connect bridge)](README.md#mmconnect-minimed-connect-bridge)| Please volunteer | Please volunteer |
| [`openaps` (OpenAPS)](README.md#openaps-openaps)| Please volunteer | Please volunteer |
| [`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 |
| [`simplealarms` (Simple BG Alarms)](README.md#simplealarms-simple-bg-alarms)| Please volunteer | 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 |
| [`xdrip-js` (xDrip-js)](README.md#xdrip-js-xdrip-js)| [@jpcunningh] | Please volunteer |
### Translators
See `/translations` of your Nightscout, to view the current translation coverage and the missing items.
Languages with less than 90% coverage will be removed in a future Nightscout versions.
| Language | List of translators | Status
| ------------- | -------------------- |-------------------- |
| Български (`bg`) |Please volunteer| OK |
| Čeština (`cs`) |Please volunteer|OK |
| Deutsch (`de`) |[@viderehh] [@herzogmedia] |OK |
| Dansk (`dk`) |Please volunteer|OK |
| Ελληνικά `(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`)|Please volunteer|Needs attention: 47.8%|
| 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|
| Português (Brasil) (`pt`)|Please volunteer|OK|
| Română (`ro`)|Please volunteer|OK|
| Русский (`ru`)|[@apanasef]|OK|
| Slovenčina (`sk`)|Please volunteer|OK|
| Svenska (`sv`)|Please volunteer|OK|
| 中文(简体) (`zh_cn`) | [@jizhongwen]|OK|
| 中文(繁體) (`zh_tw`) | [@jizhongwen]|Needs attention: 25.0%
| 日本語 (`ja_jp`) | [@LuminaryXion]|
### List of all contributors
| Contribution area | List of contributors |
| ------------------------------------- | -------------------- |
| All active developers: | [@jasoncalabrese] [@jpcunningh] [@jweismann] [@komarserjio] [@mdomox] [@MilosKozak] [@PieterGit] [@rickfriele] [@sulkaharo]
| All active testers/documentors: | [@danamlewis] [@jamieowendexcom] [@mcdafydd] [@oteroos] [@rarneson] [@tynbendad] [@unsoluble]
| All active translators: | [@apanasef] [@jizhongwen] [@viderehh] [@herzogmedia] [@LuminaryXion]
+2 -1
View File
@@ -22,7 +22,8 @@ MOCHA=./node_modules/mocha/bin/_mocha
# Pinned from dependency list.
ISTANBUL=./node_modules/.bin/istanbul
ANALYZED=./coverage/lcov.info
export CODACY_REPO_TOKEN=e29ae5cf671f4f918912d9864316207c
# Following token deprecated
# export CODACY_REPO_TOKEN=e29ae5cf671f4f918912d9864316207c
DOCKER_IMAGE=nightscout/cgm-remote-monitor-travis
+64 -33
View File
@@ -9,7 +9,7 @@ Nightscout Web Monitor (a.k.a. cgm-remote-monitor)
[![Codacy Badge][codacy-img]][codacy-url]
[![Gitter chat][gitter-img]][gitter-url]
[![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/) [![Deploy to Heroku][heroku-img]][heroku-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]
This acts as a web-based CGM (Continuous Glucose Monitor) to allow
multiple caregivers to remotely view a patient's glucose data in
@@ -39,6 +39,8 @@ Community maintained fork of the
[gitter-url]: https://gitter.im/nightscout/public
[heroku-img]: https://www.herokucdn.com/deploy/button.png
[heroku-url]: https://heroku.com/deploy
[update-img]: update.png
[update-fork]: http://nightscout.github.io/pages/update-fork/
[original]: https://github.com/rnpenguin/cgm-remote-monitor
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
@@ -70,7 +72,7 @@ Community maintained fork of the
- [`ar2` (AR2 Forecasting)](#ar2-ar2-forecasting)
- [`simplealarms` (Simple BG Alarms)](#simplealarms-simple-bg-alarms)
- [`profile` (Treatment Profile)](#profile-treatment-profile)
- [Advanced Plugins](#advanced-plugins)
- [Advanced Plugins:](#advanced-plugins)
- [`careportal` (Careportal)](#careportal-careportal)
- [`boluscalc` (Bolus Wizard)](#boluscalc-bolus-wizard)
- [`food` (Custom Foods)](#food-custom-foods)
@@ -81,6 +83,7 @@ Community maintained fork of the
- [`cage` (Cannula Age)](#cage-cannula-age)
- [`sage` (Sensor Age)](#sage-sensor-age)
- [`iage` (Insulin Age)](#iage-insulin-age)
- [`bage` (Battery Age)](#bage-battery-age)
- [`treatmentnotify` (Treatment Notifications)](#treatmentnotify-treatment-notifications)
- [`basal` (Basal Profile)](#basal-basal-profile)
- [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge)
@@ -90,6 +93,7 @@ Community maintained fork of the
- [`loop` (Loop)](#loop-loop)
- [`xdrip-js` (xDrip-js)](#xdrip-js-xdrip-js)
- [`alexa` (Amazon Alexa)](#alexa-amazon-alexa)
- [`speech` (Speech)](#speech-speech)
- [`cors` (CORS)](#cors-cors)
- [Extended Settings](#extended-settings)
- [Pushover](#pushover)
@@ -106,17 +110,28 @@ Community maintained fork of the
Supported configurations:
If you plan to use Nightscout, we recommend using [Heroku](http://openaps.readthedocs.io/en/latest/docs/While%20You%20Wait%20For%20Gear/nightscout-setup.html#nightscout-setup-with-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](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.
- [Nightscout Setup with Heroku] (http://openaps.readthedocs.io/en/latest/docs/While%20You%20Wait%20For%20Gear/nightscout-setup.html#nightscout-setup-with-heroku) (recommended)
- [Nightscout Setup with Microsoft Azure] (http://www.nightscout.info/wiki/faqs-2/azure-2) (not recommended, please
- [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)
Software requirements:
Minimum browser requirements for viewing the site:
- [Node.js](http://nodejs.org/) Latest Node 8 LTS (Node 8.11.3 or later). Use [Install instructions for Node](https://nodejs.org/en/download/package-manager/) or use `setup.sh`)
- Android 4
- Chrome 68
- Edge 15
- Firefox 61
- Internet Explorer: not supported, ie8 is known not to work
- iOS 9
- Safari 11
- Opera: 54
Windows installation software requirements:
- [Node.js](http://nodejs.org/) Latest Node 8 LTS (Node 8.15.0 or later) or Node 10 LTS (Node 10.15.1 or later; Node 10.14.1 works for Azure). 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:
@@ -125,16 +140,15 @@ As a non-root user clone this repo then install dependencies into the root of th
$ npm install
```
Installation notes for Microsoft Azure, Windows and Node 10:
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. Please use:
```
WEBSITE_NODE_DEFAULT_VERSION=8.11.1
WEBSITE_NODE_DEFAULT_VERSION=10.14.1
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.
- There may be some issues with Node 10.6.0 or later with Nightscout. Node 10 support will be in the 0.11 release. Please don't use Nightscout with (Node 9 or) Node 10 at this moment.
- 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.
# Usage
@@ -144,7 +158,6 @@ MongoDB server such as [mongolab][mongodb].
[mongodb]: https://mongolab.com
[autoconfigure]: https://nightscout.github.io/pages/configure/
[mongostring]: https://nightscout.github.io/pages/mongostring/
[update-fork]: http://nightscout.github.io/pages/update-fork/
## Updating my version?
The easiest way to update your version of cgm-remote-monitor to our latest
@@ -238,9 +251,9 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
* `MONGO_ACTIVITY_COLLECTION`(`activity`) - The collection used to store activity data
* `PORT` (`1337`) - The port that the node.js application will listen on.
* `HOSTNAME` - The hostname that the node.js application will listen on, null by default for any hostname for IPv6 you may need to use `::`.
* `SSL_KEY` - Path to your ssl key file, so that ssl(https) can be enabled directly in node.js
* `SSL_CERT` - Path to your ssl cert file, so that ssl(https) can be enabled directly in node.js
* `SSL_CA` - Path to your ssl ca file, so that ssl(https) can be enabled directly in node.js
* `SSL_KEY` - Path to your ssl key file, so that ssl(https) can be enabled directly in node.js. If using Let's Encrypt, make this variable the path to your privkey.pem file (private key).
* `SSL_CERT` - Path to your ssl cert file, so that ssl(https) can be enabled directly in node.js. If using Let's Encrypt, make this variable the path to fullchain.pem file (cert + ca).
* `SSL_CA` - Path to your ssl ca file, so that ssl(https) can be enabled directly in node.js. If using Let's Encrypt, make this variable the path to chain.pem file (chain).
* `HEARTBEAT` (`60`) - Number of seconds to wait in between database checks
* `DEBUG_MINIFY` (`true`) - Debug option, setting to `false` will disable bundle minification to help tracking down error and speed up development
@@ -265,7 +278,14 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
* 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
### Views
### Predefined values for your server settings (optional)
* `INSECURE_USE_HTTP` (`false`) - Redirect http url's to https. Possible values `false`, or `true`.
* `SECURE_HSTS_HEADER` (`true`) - Add HTTP Strict Transport Security (HSTS) header. Possible values `false`, or `true`.
* `SECURE_HSTS_HEADER_INCLUDESUBDOMAINS` (`false`) - includeSubdomains options for HSTS. Possible values `false`, or `true`.
* `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`. Currently Nightscout is not yet compatible with CSP.
### Views
There are a few alternate web views available that display a simplified BG stream. Append any of these to your Nightscout URL:
* `/clock.html` - Shows current BG. Grey text on a black background.
@@ -339,7 +359,11 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
An option plugin to enable adding foods from database in Bolus Wizard and enable .
##### `rawbg` (Raw BG)
Calculates BG using sensor and calibration records from and displays an alternate BG values and noise levels.
Calculates BG using sensor and calibration records from and displays an alternate BG values and noise levels. Defaults that can be adjusted with [extended setting](#extended-settings)
* `DISPLAY` (`unsmoothed`) - Allows the user to control which algorithm is used to calculate the displayed raw BG values using the most recent calibration record.
* `unfiltered` - Raw BG is calculated by applying the calibration to the glucose record's unfiltered value.
* `filtered` - Raw BG is calculated by applying the calibration to the glucose record's filtered value. The glucose record's filtered values are generally produced by the CGM by a running average of the unfiltered values to produce a smoothed value when the sensor noise is high.
* `unsmoothed` - Raw BG is calculated by first finding the ratio of the calculated filtered value (the same value calculated by the `filtered` setting) to the reported glucose value. The displayed raw BG value is calculated by dividing the calculated unfiltered value (the same value calculated by the `unfiltered` setting) by the ratio. The effect is to exagerate changes in trend direction so the trend changes are more noticeable to the user. This is the legacy raw BG calculation algorithm.
##### `iob` (Insulin-on-Board)
Adds the IOB pill visualization in the client and calculates values that used by other plugins. Uses treatments with insulin doses and the `dia` and `sens` fields from the [treatment profile](#treatment-profile).
@@ -376,6 +400,14 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
* `IAGE_WARN` (`48`) - If time since last `Insulin Change` matches `IAGE_WARN`, user will be alarmed to to change the insulin reservoir
* `IAGE_URGENT` (`72`) - If time since last `Insulin Change` matches `IAGE_URGENT`, user will be issued a persistent warning of overdue change.
##### `bage` (Battery Age)
Calculates the number of days and hours since the last `Pump Battery Change` treatment that was recorded.
* `BAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming pump battery change.
* `BAGE_DISPLAY` (`days`) - Set to `hours` to display time since last `Pump Battery Change` in hours only.
* `BAGE_INFO` (`312`) - If time since last `Pump Battery Change` matches `BAGE_INFO` hours, user will be warned of upcoming pump battery change (default of 312 hours is 13 days).
* `BAGE_WARN` (`336`) - If time since last `Pump Battery Change` matches `BAGE_WARN` hours, user will be alarmed to to change the pump battery (default of 336 hours is 14 days).
* `BAGE_URGENT` (`360`) - If time since last `Pump Battery Change` matches `BAGE_URGENT` hours, user will be issued a persistent warning of overdue change (default of 360 hours is 15 days).
##### `treatmentnotify` (Treatment Notifications)
Generates notifications when a treatment has been entered and snoozes alarms minutes after a treatment. Default snooze is 10 minutes, and can be set using the `TREATMENTNOTIFY_SNOOZE_MINS` [extended setting](#extended-settings).
@@ -459,7 +491,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
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.
#### Pushover
@@ -480,29 +512,28 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
* `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
In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Maker](https://ifttt.com/maker).
In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Webhooks](https://ifttt.com/maker_webhooks).
With Maker you are able to integrate with all the other [IFTTT Channels](https://ifttt.com/channels). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more.
With Maker you are able to integrate with all the other [IFTTT Services](https://ifttt.com/services). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more.
1. Setup IFTTT account: [login](https://ifttt.com/login) or [create an account](https://ifttt.com/join)
2. Find your secret key on the [maker page](https://ifttt.com/maker)
3. Configure Nightscout by setting these environment variables:
* `ENABLE` - `maker` should be added to the list of plugin, for example: `ENABLE="maker"`.
* `MAKER_KEY` - Set this to your secret key that you located in step 2, for example: `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` This also support a space delimited list of keys.
* `MAKER_ANNOUNCEMENT_KEY` - An optional Maker key, will be used for system wide user generated announcements. If not defined this will fallback to `MAKER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys.
4. [Create a recipe](https://ifttt.com/myrecipes/personal/new) or see [more detailed instructions](lib/plugins/maker-setup.md#create-a-recipe)
2. Follow the [Detailed IFTTT setup Instructions](docs/plugins/maker-setup.md)
3. Configure Nightscout by setting these webpage environment variables:
* `ENABLE` - `maker` should be added to the list of plugins, for example: `ENABLE="maker"`.
* `MAKER_KEY` - Set this to your secret key (see [[Detailed Instructions](docs/plugins/maker-setup.md) ) `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` This also supports a space delimited list of keys.
* `MAKER_ANNOUNCEMENT_KEY` - An optional Maker key, will be used for system wide user generated announcements. If not defined this will fallback to `MAKER_KEY`. A possible use for this is sending important messages and alarms to another device that you don't want to send all notification too. This also support a space delimited list of keys.
Plugins can create custom events, but all events sent to maker will be prefixed with `ns-`. The core events are:
Plugins can create custom events, but all events sent to IFTTT webhooks will be prefixed with `ns-`. The core events are:
* `ns-event` - This event is sent to the maker service for all alarms and notifications. This is good catch all event for general logging.
* `ns-allclear` - This event is sent to the maker service when an alarm has been ack'd or when the server starts up without triggering any alarms. For example, you could use this event to turn a light to green.
* `ns-info` - Plugins that generate notifications at the info level will cause this event to also be triggered. It will be sent in addition to `ns-event`.
@@ -577,16 +608,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 <http://www.gnu.org/licenses/>.
+37 -5
View File
@@ -14,6 +14,39 @@ function create(env, ctx) {
var appInfo = env.name + ' ' + env.version;
app.set('title', appInfo);
app.enable('trust proxy'); // Allows req.secure test on heroku https connections.
var insecureUseHttp = env.insecureUseHttp;
var secureHstsHeader = env.secureHstsHeader;
console.info('Security settings: INSECURE_USE_HTTP=',insecureUseHttp,', SECURE_HSTS_HEADER=',secureHstsHeader);
if (!insecureUseHttp) {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https')
res.redirect(`https://${req.header('host')}${req.url}`);
else
next()
})
if (secureHstsHeader) { // Add HSTS (HTTP Strict Transport Security) header
const helmet = require('helmet');
var includeSubDomainsValue = env.secureHstsHeaderIncludeSubdomains;
var preloadValue = env.secureHstsHeaderPreload;
app.use(helmet({
hsts: {
maxAge: 31536000,
includeSubDomains: includeSubDomainsValue,
preload: preloadValue
}
}))
if (env.secureCsp) {
app.use(helmet.contentSecurityPolicy({ //TODO make NS work without 'unsafe-inline'
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", 'https://fonts.googleapis.com/',"'unsafe-inline'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
fontSrc: [ "'self'", 'https://fonts.gstatic.com/']
}
}));
}
}
}
app.set('view engine', 'ejs');
// this allows you to render .html files as templates in addition to .ejs
@@ -84,7 +117,7 @@ function create(env, ctx) {
});
});
app.get("/nightscout.appcache", (req, res) => {
app.get("/appcache/*", (req, res) => {
res.render("nightscout.appcache", {
locals: app.locals
});
@@ -141,7 +174,8 @@ function create(env, ctx) {
// serve the static content
app.use(staticFiles);
var swaggerFiles = express.static(env.swagger_files, {
const swaggerUiAssetPath = require("swagger-ui-dist").getAbsoluteFSPath();
var swaggerFiles = express.static(swaggerUiAssetPath, {
maxAge: maxAge
});
@@ -160,7 +194,6 @@ function create(env, ctx) {
console.log('Production environment detected, enabling Minify');
var minify = require('express-minify');
var myUglifyJS = require('uglify-js');
var myCssmin = require('cssmin');
app.use(minify({
@@ -171,7 +204,6 @@ function create(env, ctx) {
stylus_match: /stylus/,
coffee_match: /coffeescript/,
json_match: /json/,
uglifyJS: myUglifyJS,
cssmin: myCssmin,
cache: __dirname + '/tmp',
onerror: undefined,
@@ -208,4 +240,4 @@ function create(env, ctx) {
//}
return app;
}
module.exports = create;
module.exports = create;
+123 -128
View File
@@ -2,155 +2,150 @@
"name": "CGM Remote Monitor",
"repository": "https://github.com/nightscout/cgm-remote-monitor",
"env": {
"MONGO_COLLECTION": {
"description": "REQUIRED: The mongo collection used for CGM data. Default value is 'entries'. Most users should use the default.",
"value": "entries",
"required": true
},
"API_SECRET": {
"description": "REQUIRED: A secret passphrase that must be at least 12 characters long, required to enable POST and PUT; also required for the Care Portal.",
"value": "",
"required": true
},
"DISPLAY_UNITS": {
"description": "Choices: mg/dl and mmol. Setting to mmol puts the entire server into mmol mode by default, no further settings needed.",
"value": "",
"required": false
},
"ENABLE": {
"description": "Used to enable optional features, expects a space delimited list, such as: careportal rawbg iob, see https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md for more info.",
"value": "",
"required": false
},
"DISABLE": {
"description": "Used to disable default features, expects a space delimited list, such as: direction upbat, see https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md for more info.",
"value": "",
"required": false
},
"ALARM_TYPES": {
"description": "Alarm behavior currently 2 alarm types are supported simple and predict, 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",
"value": "",
"required": false
},
"BG_HIGH": {
"description": "Urgent high BG alarm. Default null value implies 260. Must be set in mg/dL (multiply with 18 if you have a value in mmol/L). Only used with simple alarms.",
"value": "",
"required": false
},
"BG_TARGET_TOP": {
"description": "Non-urgent high BG alarm, the top of your target range. Default null value implies 180. Must be set in mg/dL (multiply with 18 if you have a value in mmol/L). Only used with simple alarms.",
"value": "",
"required": false
},
"BG_TARGET_BOTTOM": {
"description": "Non urgent low BG alarm, the bottom of your target range. Default null value implies 80. Must be set in mg/dL (multiply with 18 if you have a value in mmol/L). Only used with simple alarms.",
"value": "",
"required": false
},
"BG_LOW": {
"description": "Urgent Low BG alarm. Default null value implies 55. Must be set in mg/dL (multiply with 18 if you have a value in mmol/L). Only used with simple alarms.",
"value": "",
"required": false
},
"PUSHOVER_API_TOKEN": {
"description": "Pushover API token, required for Pushover notifications. Leave blank if not using Pushover.",
"value": "",
"required": false
},
"PUSHOVER_USER_KEY": {
"description": "Pushover user key, required for Pushover notifications. Leave blank if not using Pushover.",
"value": "",
"required": false
},
"PUSHOVER_ANNOUNCEMENT_KEY": {
"description": "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`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. Leave blank if not using Pushover",
"value": "",
"required": false
},
"CUSTOM_TITLE": {
"description": "Customize the name of the website, usually the name of T1.",
"value": "",
"required": false
},
"BRIDGE_USER_NAME": {
"description": "Share bridge - Your user name for the Share service. ENSURE bridge is in ENABLE if you want to use the share bridge.",
"value": "",
"required": false
},
"BRIDGE_PASSWORD": {
"description": "Share bridge - Your password for the Share service. ENSURE bridge is in ENABLE if you want to use the share bridge.",
"value": "",
"required": false
},
"TIME_FORMAT": {
"description": "Browser default time mode. Valid settings are 12 or 24",
"value": "12",
"required": false
},
"NIGHT_MODE": {
"description": "Browser defaults to night mode. Valid settings are on or off",
"value": "off",
"required": false
},
"SHOW_RAWBG": {
"description": "Browser default raw display mode. Valid settings are always, never, or noise",
"value": "never",
"required": false
},
"THEME": {
"description": "Browser default theme setting. Valid settings are default, colors, or colorblindfriendly",
"value": "default",
"required": false
},
"ALARM_URGENT_HIGH": {
"description": "Browser default urgent high alarm enabled. Valid settings are on or off",
"value": "on",
"required": false
},
"ALARM_HIGH": {
"description": "Browser default high alarm enabled. Valid settings are on or off",
"description": "Default setting for new browser views, for the High alarm (triggered when BG crosses BG_TARGET_TOP). ('on' or 'off')",
"value": "on",
"required": false
},
"ALARM_LOW": {
"description": "Browser default low alarm enabled. Valid settings are on or off",
"description": "Default setting for new browser views, for the Low alarm (triggered when BG crosses BG_TARGET_BOTTOM). ('on' or 'off')",
"value": "on",
"required": false
},
"ALARM_URGENT_LOW": {
"description": "Browser default urgent low alarm enabled. Valid settings are on or off",
"value": "on",
"required": false
},
"ALARM_TIMEAGO_WARN": {
"description": "Browser default warn after time of last data exceeds ALARM_TIMEAGO_WARN_MINS alarm enabled. Valid settings are on or off",
"value": "on",
"required": false
},
"ALARM_TIMEAGO_WARN_MINS": {
"description": "Browser default minutes since the last reading to trigger a warning.",
"value": "15",
"required": false
},
"ALARM_TIMEAGO_URGENT": {
"description": "Browser default urgent warning after time of last data exceeds ALARM_TIMEAGO_URGENT_MINS alarm enabled. Valid settings are on or off",
"description": "Default setting for new browser views, for an urgent alarm when CGM data hasn't been received in the number of minutes set in ALARM_TIMEAGO_URGENT_MINS. ('on' or 'off')",
"value": "on",
"required": false
},
"ALARM_TIMEAGO_URGENT_MINS": {
"description": "Browser default minutes since last reading to trigger an urgent alarm.",
"description": "Default setting for new browser views, for the number of minutes since the last CGM reading to trigger an ALARM_TIMEAGO_URGENT alarm.",
"value": "30",
"required": false
},
"MAKER_KEY": {
"description": "Maker Key - Set this to your secret key Note for additional info see https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#ifttt-maker , maker should be added to enable if you want to use maker, Leave blank if not using maker",
"ALARM_TIMEAGO_WARN": {
"description": "Default setting for new browser views, for a warning alarm when CGM data hasn't been received in the number of minutes set in ALARM_TIMEAGO_WARN_MINS. ('on' or 'off')",
"value": "on",
"required": false
},
"ALARM_TIMEAGO_WARN_MINS": {
"description": "Default setting for new browser views, for the number of minutes since the last CGM reading to trigger an ALARM_TIMEAGO_WARN alarm.",
"value": "15",
"required": false
},
"ALARM_TYPES": {
"description": "'simple' and/or 'predict'. Simple alarms trigger when BG crosses the various thresholds set below. Predict alarms use a formula that forecasts where the BG is going based on its trend. You will *not* get warnings when crossing the BG thresholds set below when using the predict type.",
"value": "simple",
"required": false
},
"ALARM_URGENT_HIGH": {
"description": "Default setting for new browser views, for the Urgent High alarm (triggered when BG crosses BG_HIGH). ('on' or 'off')",
"value": "on",
"required": false
},
"ALARM_URGENT_LOW": {
"description": "Default setting for new browser views, for the Urgent Low alarm (triggered when BG crosses BG_LOW). ('on' or 'off')",
"value": "on",
"required": false
},
"API_SECRET": {
"description": "A passphrase that must be at least 12 characters long. Avoid 'special' characters, which can cause problems in some cases.",
"value": "",
"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).",
"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).",
"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).",
"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).",
"value": "180",
"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": "",
"required": false
},
"MAKER_ANNOUNCEMENT_KEY": {
"description": "Maker Announcement Key - Set this to your secret key for announcements Note for additional info see https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#ifttt-maker , maker should be added to enable if you want to use maker Leave blank if not using maker",
"BRIDGE_SERVER": {
"description": "If you are bridging from the Dexcom Share service, and are anywhere *outside* the US, change this to EU. ('US' or 'EU')",
"value": "US",
"required": false
},
"BRIDGE_USER_NAME": {
"description": "Your Dexcom account username, to receive CGM data from the Dexcom Share service. Also make sure to include 'bridge' in your ENABLE line.",
"value": "",
"required": false
},
"CUSTOM_TITLE": {
"description": "The display name for the Nightscout site. Appears in the upper left of the main view. Often set to the name of the CGM wearer.",
"value": "",
"required": false
},
"DISPLAY_UNITS": {
"description": "Preferred BG units for the site:'mg/dl' or 'mmol'. (Note that it is *not* 'mmol/L')",
"value": "mg/dl",
"required": false
},
"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",
"required": false
},
"MMCONNECT_USER_NAME": {
"description": "Your CareLink account username, to receive CGM data from the CareLink service. Also make sure to include 'mmconnect' in your ENABLE line.",
"value": "",
"required": false
},
"MMCONNECT_PASSWORD": {
"description": "Your CareLink account password, to receive CGM data from the CareLink service. Also make sure to include 'mmconnect' in your ENABLE line.",
"value": "",
"required": false
},
"MMCONNECT_SERVER": {
"description": "If you are bridging from the CareLink service, and are anywhere *outside* the US, change this to EU. ('US' or 'EU')",
"value": "US",
"required": false
},
"MONGO_COLLECTION": {
"description": "The Mongo collection where CGM data is stored.",
"value": "entries",
"required": true
},
"NIGHT_MODE": {
"description": "Default setting for new browser views, for whether Night Mode should be enabled. ('on' or 'off')",
"value": "off",
"required": false
},
"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",
"required": false
},
"SHOW_RAWBG": {
"description": "Default setting for new browser views, for the display of raw CGM data (if available). ('always', 'never', or 'noise')",
"value": "never",
"required": false
},
"THEME": {
"description": "Default setting for new browser views, for the color theme of the CGM graph. ('default', 'colors', or 'colorblindfriendly')",
"value": "colors",
"required": false
},
"TIME_FORMAT": {
"description": "Default setting for new browser views, for the time mode. ('12' or '24')",
"value": "12",
"required": false
}
},
"addons": [
-17
View File
@@ -1,17 +0,0 @@
'use strict';
var browserify_express = require('browserify-express');
function bundle(env) {
return browserify_express({
entry: __dirname + '/bundle.source.js',
watch: __dirname + '/../lib/',
mount: '/public/js/bundle.js',
verbose: true,
minify: env.debug.minify,
bundle_opts: { debug: true }, // enable inline sourcemap on js files
write_file: __dirname + '/bundle.out.js'
});
}
module.exports = bundle;
@@ -1,3 +1,24 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Nightscout Alexa Plugin](#nightscout-alexa-plugin)
- [Overview](#overview)
- [Activate the Nightscout Alexa Plugin](#activate-the-nightscout-alexa-plugin)
- [Create Your Alexa Skill](#create-your-alexa-skill)
- [Get an Amazon Developer account](#get-an-amazon-developer-account)
- [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)
- [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)
- [Adding Alexa support to a plugin](#adding-alexa-support-to-a-plugin)
- [Intent Handlers](#intent-handlers)
- [Rollup handlers](#rollup-handlers)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Nightscout Alexa Plugin
======================================
@@ -14,6 +35,7 @@ To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-p
1. Your Nightscout site needs to be new enough that it supports the `alexa` plugin. It needs to be [version 0.9.1 (Grilled Cheese)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/0.9.1) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version.
1. Add `alexa` 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.)
1. The Alexa plugin pulls its units preferences from your site's defaults. If you don't have a `DISPLAY_UNITS` entry, it will default to `mg/dl`. If you want it to use mmol/L, make sure you have a `DISPLAY_UNITS` line, and set it to `mmol` (*not* `mmol/l`).
## Create Your Alexa Skill
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+107
View File
@@ -0,0 +1,107 @@
**Table of Contents**
- [Nightscout/IFTTT Maker](#nightscoutifttt-maker)
- [Overview](#overview)
- [Events](#events)
- [Creating an Applet](#creating-an-applet)
- [1. Choose a Service](#1.-Choose-a-Service)
- [2. Choose a Trigger](#2.-Choose-a-Trigger)
- [3. Complete the Trigger field](#3.-Complete-the-Trigger-field)
- [4. Create an Action](#4.-Create-an-Action)
- [5. Complete Action Fields](#5.-Complete-Action-Fields)
- [6. Review and Finish](#6.-Review-and-Finish)
- [7. Get your Maker Key](#7.-Get-your-Maker-Key)
- [8. Configure your Nightscout site](#8.-Configure-your-Nightscout-site)
- [9. Configure the IFTTT mobile app](#9.-Configure-the-IFTTT-mobile-app)
Nightscout/IFTTT Maker
======================================
## Overview
In addition to Nightscout's web-based alarms, your site can also trigger push notifications (or other actions) through the [IFTTT Maker](https://ifttt.com/maker) service. With Maker you are able to integrate with all the other [IFTTT Services](https://ifttt.com/channels). For example, you can send a tweet when there is an alarm, change the color of a smart light, send an email, send a text, and much more.
#### Note: There have been some recent reports of the IFTTT service delaying Nightscout alarms. Be sure to test your implementation before relying solely on its alerts. [Pushover](https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#pushover) is an alternate push notification service that might be worth considering as well.
## Events
Plugins can create custom events, but all events sent to Maker will be prefixed with `ns-`. The core events are:
* `ns-event` - This event is sent on *all* alarms and notifications. This is good catch-all event for general logging.
* `ns-allclear` - This event is sent when an alarm has been acknowledged or when the server starts up without triggering any alarms. (For example, you could use this event to turn a light to green.)
* `ns-info` - All notifications at the `info` level will cause this event to be triggered.
* `ns-warning` - All notifications at the `warning` level will cause this event to be triggered.
* `ns-urgent` - All notifications at the `urgent` level will cause this event to be triggered.
* `ns-warning-high` - This event is triggered when crossing the `BG_TARGET_TOP` threshold.
* `ns-urgent-high` - This event is triggered when crossing the `BG_HIGH` threshold.
* `ns-warning-low` - This event is triggered when crossing the `BG_TARGET_BOTTOM` threshold.
* `ns-urgent-low` - This event is triggered when crossing the `BG_LOW` threshold.
* `ns-info-treatmentnotify` - This event is triggered when a treatment is entered into the `careportal`.
* `ns-warning-bwp` - This event is triggered when the `bwp` plugin generates a warning alarm.
* `ns-urgent-bwp` - This event is triggered when the `bwp` plugin generates an urgent alarm.
## Creating an Applet
Set up an [IFTTT](https://ifttt.com/) account, and log into it.
### 1. Choose a Service
On the "My Applets" page, click "New Applet", then click the blue `+this`. Search for "webhooks" among the services, and click it.
![service_search](./maker-setup-images/service_search.jpg)
### 2. Choose a Trigger
Click on the "Receive a web request" box.
### 3. Complete the Trigger field
Enter one of the Nightscout events listed above (like `ns-urgent-low`), and click "Create Trigger".
![set_trigger](./maker-setup-images/set_trigger.jpg)
### 4. Create an Action
Click on the blue `+that`.
![blue_that](./maker-setup-images/blue_that.jpg)
Search for the action you'd like this event to trigger. In this example, we'll choose the `Notifications` action to send a push notification.
![action_search](./maker-setup-images/action_search.jpg)
Choose the "Send a notification from the IFTTT app" action type for a basic push alert. You can experiment with the "rich" notifications later.
### 5. Complete Action Fields
Enter the message that will display in this push notification. In this example, it was triggered on an `ns-urgent-low`, so we'll write something like "Urgent Low!". We can also display the current BG by including the `Value2` ingredient (via the "Add ingredient" button).
Click the "Create action" button when you're done.
![notification_message](./maker-setup-images/notification_message.jpg)
### 6. Review and Finish
Click the "Finish" button if your applet looks good.
### 7. Get your Maker Key
Click on "My Applets" in the main menu, then click the "Services" tab, then search for "Webhooks" and select it.
![service_settings_search](./maker-setup-images/service_settings_search.jpg)
Go to the `Settings` for this service, in the upper right.
![webhooks_settings](./maker-setup-images/webhooks_settings.jpg)
The string of characters at the end of the URL here is your `MAKER_KEY`. Copy it from there, so we can paste it into your Config Vars.
![maker_key](./maker-setup-images/maker_key.jpg)
### 8. Configure your Nightscout site
From your [Heroku dashboard](https://dashboard.heroku.com), go to your app's Settings page, then click the "Reveal Config Vars" button. Find the `MAKER_KEY` entry, and edit its value, pasting in your Maker Key. If you don't already have a `MAKER_KEY` line, add it to the bottom of the list.
![config_vars_maker](./maker-setup-images/config_vars_maker.jpg)
Find your `ENABLE` line, and add `maker` to the list of enabled plugins.
![config_vars_enable](./maker-setup-images/config_vars_enable.jpg)
### 9. Configure the IFTTT mobile app
That's all of the services complete. In order to receive push notifications on a mobile device, you'll need to have the IFTTT app installed and logged into the same account you set up the actions in.
To add more alerts for different events, just create a new applet for each trigger.
+10 -17
View File
@@ -25,7 +25,6 @@ function config ( ) {
env.HOSTNAME = readENV('HOSTNAME', null);
env.IMPORT_CONFIG = readENV('IMPORT_CONFIG', null);
env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/');
env.swagger_files = readENV('NIGHTSCOUT_SWAGGER_FILES', __dirname + '/node_modules/swagger-ui-dist/');
env.debug = {
minify: readENVTruthy('DEBUG_MINIFY', true)
};
@@ -56,6 +55,13 @@ function setSSL() {
env.ca = fs.readFileSync(env.SSL_CA);
}
}
env.insecureUseHttp = readENVTruthy("INSECURE_USE_HTTP", false);
env.secureHstsHeader = readENVTruthy("SECURE_HSTS_HEADER", true);
env.secureHstsHeaderIncludeSubdomains = readENVTruthy("SECURE_HSTS_HEADER_INCLUDESUBDOMAINS", false);
env.secureHstsHeaderPreload= readENVTruthy("SECURE_HSTS_HEADER_PRELOAD", false);
env.secureCsp = readENVTruthy("SECURE_CSP", false);
}
// A little ugly, but we don't want to read the secret into a var
@@ -92,20 +98,6 @@ function setVersion() {
function setStorage() {
env.storageURI = readENV('STORAGE_URI') || readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI') || readENV('MONGODB_URI');
env.entries_collection = readENV('ENTRIES_COLLECTION') || readENV('MONGO_COLLECTION', 'entries');
env.MQTT_MONITOR = readENV('MQTT_MONITOR', null);
if (env.MQTT_MONITOR) {
var hostDbCollection = [env.storageURI.split('mongodb://').pop().split('@').pop(), env.entries_collection].join('/');
var mongoHash = crypto.createHash('sha1');
mongoHash.update(hostDbCollection);
//some MQTT servers only allow the client id to be 23 chars
env.mqtt_client_id = mongoHash.digest('base64').substring(0, 23);
console.info('Using Mongo host/db/collection to create the default MQTT client_id', hostDbCollection);
if (env.MQTT_MONITOR.indexOf('?clientId=') === -1) {
console.info('Set MQTT client_id to: ', env.mqtt_client_id);
} else {
console.info('MQTT configured to use a custom client id, it will override the default: ', env.mqtt_client_id);
}
}
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');
@@ -159,7 +151,8 @@ function readENV(varName, defaultValue) {
function readENVTruthy(varName, defaultValue) {
var value = readENV(varName, 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; }
else if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; }
else { value=defaultValue }
return value;
}
@@ -193,6 +186,6 @@ function findExtendedSettings (envs) {
}
});
return extended;
}
}
module.exports = config;
+80
View File
@@ -0,0 +1,80 @@
'use strict';
var moment = require('moment');
var cleanentriesdb = {
name: 'cleanentriesdb'
, label: 'Clean Mongo entries (glucose entries) database'
, pluginType: 'admin'
};
function init() {
return cleanentriesdb;
}
module.exports = init;
cleanentriesdb.actions = [
{
name: 'Delete all documents from entries collection older than 180 days'
, description: 'This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.'
, buttonLabel: 'Delete old documents'
, confirmText: 'Delete old documents from entries collection?'
, preventClose: true
}
];
cleanentriesdb.actions[0].init = function init(client, callback) {
var translate = client.translate;
var $status = $('#admin_' + cleanentriesdb.name + '_0_status');
$status.hide();
var numDays = '<br/>'
+ '<label for="admin_entries_days">'
+ translate('Number of Days to Keep:')
+ ' <input id="admin_entries_days" value="180" size="3" min="1"/>'
+ '</label>';
$('#admin_' + cleanentriesdb.name + '_0_html').html(numDays);
if (callback) { callback(); }
};
cleanentriesdb.actions[0].code = function deleteOldRecords(client, callback) {
var translate = client.translate;
var $status = $('#admin_' + cleanentriesdb.name + '_0_status');
var numDays = Number($('#admin_entries_days').val());
if (isNaN(numDays) || (numDays < 3)) {
alert(translate('%1 is not a valid number - must be more than 2', { params: [$('#admin_entries_days').val()] }));
if (callback) { callback(); }
return;
}
var endDate = moment().subtract(numDays, 'days');
if (!client.hashauth.isAuthenticated()) {
alert(translate('Your device is not authenticated yet'));
if (callback) {
callback();
}
return;
}
$status.hide().text(translate('Deleting records ...')).fadeIn('slow');
$.ajax('/api/v1/entries/?find[date][$lte]=' + endDate.valueOf(), {
method: 'DELETE'
, headers: client.headers()
, success: function (retVal) {
$status.hide().text(translate('%1 records deleted',{ params: [retVal.n] })).fadeIn('slow');
}
, error: function () {
$status.hide().text(translate('Error')).fadeIn('slow');
}
}).done(function success () {
if (callback) { callback(); }
}).fail(function fail() {
if (callback) { callback(); }
});
};
+64
View File
@@ -1,5 +1,7 @@
'use strict';
var moment = require('moment');
var cleanstatusdb = {
name: 'cleanstatusdb'
, label: 'Clean Mongo status database'
@@ -19,6 +21,13 @@ cleanstatusdb.actions = [
, buttonLabel: 'Delete all documents'
, confirmText: 'Delete all documents from devicestatus collection?'
}
, {
name: 'Delete all documents from devicestatus collection older than 30 days'
, description: 'This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.'
, buttonLabel: 'Delete old documents'
, confirmText: 'Delete old documents from devicestatus collection?'
, preventClose: true
}
];
cleanstatusdb.actions[0].init = function init(client, callback) {
@@ -67,3 +76,58 @@ cleanstatusdb.actions[0].code = function deleteRecords(client, callback) {
}
});
};
cleanstatusdb.actions[1].init = function init(client, callback) {
var translate = client.translate;
var $status = $('#admin_' + cleanstatusdb.name + '_1_status');
$status.hide();
var numDays = '<br/>'
+ '<label for="admin_devicestatus_days">'
+ translate('Number of Days to Keep:')
+ ' <input id="admin_devicestatus_days" value="30" size="3" min="1"/>'
+ '</label>';
$('#admin_' + cleanstatusdb.name + '_1_html').html(numDays);
if (callback) { callback(); }
};
cleanstatusdb.actions[1].code = function deleteOldRecords(client, callback) {
var translate = client.translate;
var $status = $('#admin_' + cleanstatusdb.name + '_1_status');
var numDays = Number($('#admin_devicestatus_days').val());
if (isNaN(numDays) || (numDays < 1)) {
alert(translate('%1 is not a valid number', { params: [$('#admin_devicestatus_days').val()] }));
if (callback) { callback(); }
return;
}
var endDate = moment().subtract(numDays, 'days');
var dateStr = endDate.format('YYYY-MM-DD');
if (!client.hashauth.isAuthenticated()) {
alert(translate('Your device is not authenticated yet'));
if (callback) {
callback();
}
return;
}
$status.hide().text(translate('Deleting records ...')).fadeIn('slow');
$.ajax('/api/v1/devicestatus/?find[created_at][$lte]=' + dateStr, {
method: 'DELETE'
, headers: client.headers()
, success: function (retVal) {
$status.text(translate('%1 records deleted',{ params: [retVal.n] }));
}
, error: function () {
$status.hide().text(translate('Error')).fadeIn('slow');
}
}).done(function success () {
if (callback) { callback(); }
}).fail(function fail() {
if (callback) { callback(); }
});
};
+81
View File
@@ -0,0 +1,81 @@
'use strict';
var moment = require('moment');
var cleantreatmentsdb = {
name: 'cleantreatmentsdb'
, label: 'Clean Mongo treatments database'
, pluginType: 'admin'
};
function init() {
return cleantreatmentsdb;
}
module.exports = init;
cleantreatmentsdb.actions = [
{
name: 'Delete all documents from treatments collection older than 180 days'
, description: 'This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.'
, buttonLabel: 'Delete old documents'
, confirmText: 'Delete old documents from treatments collection?'
, preventClose: true
}
];
cleantreatmentsdb.actions[0].init = function init(client, callback) {
var translate = client.translate;
var $status = $('#admin_' + cleantreatmentsdb.name + '_0_status');
$status.hide();
var numDays = '<br/>'
+ '<label for="admin_treatments_days">'
+ translate('Number of Days to Keep:')
+ ' <input id="admin_treatments_days" value="180" size="3" min="1"/>'
+ '</label>';
$('#admin_' + cleantreatmentsdb.name + '_0_html').html(numDays);
if (callback) { callback(); }
};
cleantreatmentsdb.actions[0].code = function deleteOldRecords(client, callback) {
var translate = client.translate;
var $status = $('#admin_' + cleantreatmentsdb.name + '_0_status');
var numDays = Number($('#admin_treatments_days').val());
if (isNaN(numDays) || (numDays < 3)) {
alert(translate('%1 is not a valid number - must be more than 2', { params: [$('#admin_treatments_days').val()] }));
if (callback) { callback(); }
return;
}
var endDate = moment().subtract(numDays, 'days');
var dateStr = endDate.format('YYYY-MM-DD');
if (!client.hashauth.isAuthenticated()) {
alert(translate('Your device is not authenticated yet'));
if (callback) {
callback();
}
return;
}
$status.hide().text(translate('Deleting records ...')).fadeIn('slow');
$.ajax('/api/v1/treatments/?find[created_at][$lte]=' + dateStr, {
method: 'DELETE'
, headers: client.headers()
, success: function (retVal) {
$status.hide().text(translate('%1 records deleted',{ params: [retVal.n] })).fadeIn('slow');
}
, error: function () {
$status.hide().text(translate('Error')).fadeIn('slow');
}
}).done(function success () {
if (callback) { callback(); }
}).fail(function fail() {
if (callback) { callback(); }
});
};
+3 -1
View File
@@ -8,6 +8,8 @@ function init() {
require('./subjects')()
, require('./roles')()
, require('./cleanstatusdb')()
, require('./cleantreatmentsdb')()
, require('./cleanentriesdb')()
, require('./futureitems')()
];
@@ -84,4 +86,4 @@ function init() {
}
module.exports = init;
module.exports = init;
+45 -10
View File
@@ -43,17 +43,52 @@ function configure (app, wares, ctx) {
api.post('/devicestatus/', ctx.authorization.isPermitted('api:devicestatus:create'), doPost);
// delete record
api.delete('/devicestatus/:_id', ctx.authorization.isPermitted('api:devicestatus:delete'), function(req, res) {
ctx.devicestatus.remove(req.params._id, function (err, removed) {
if (err) {
res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err);
} else {
res.json(removed);
}
});
});
/**
* @function delete_records
* Delete devicestatus. The query logic works the same way as find/list. This
* endpoint uses same search logic to remove records from the database.
*/
function delete_records(req, res, next) {
var query = req.query;
if (!query.count) {
query.count = 10
}
console.log('Delete records with query: ', query);
// remove using the query
ctx.devicestatus.remove(query, function(err, stat) {
if (err) {
console.log('devicestatus delete error: ', err);
return next(err);
}
// yield some information about success of operation
res.json(stat);
console.log('devicestatus records deleted');
return next();
});
}
api.delete('/devicestatus/:id', ctx.authorization.isPermitted('api:devicestatus:delete'), function(req, res, next) {
if (!req.query.find) {
req.query.find = {
_id: req.params.id
};
} else {
req.query.find._id = req.params.id;
}
if (req.query.find._id === '*') {
// match any record id
delete req.query.find._id;
}
next();
}, delete_records);
// delete record that match query
api.delete('/devicestatus/', ctx.authorization.isPermitted('api:devicestatus:delete'), delete_records);
}
if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/) {
+631 -622
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -44,7 +44,6 @@ function configure (app, wares, ctx) {
res.json(created.ops);
console.log('Profile created', created);
}
});
});
+46 -5
View File
@@ -99,11 +99,52 @@ function configure(app, wares, ctx) {
limit: 1048576 * 50
}), ctx.authorization.isPermitted('api:treatments:create'), post_response);
api.delete('/treatments/:_id', ctx.authorization.isPermitted('api:treatments:delete'), function(req, res) {
ctx.treatments.remove(req.params._id, function() {
res.json({});
});
});
/**
* @function delete_records
* Delete treatments. The query logic works the same way as find/list. This
* endpoint uses same search logic to remove records from the database.
*/
function delete_records(req, res, next) {
var query = req.query;
if (!query.count) {
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();
});
}
api.delete('/treatments/:id', ctx.authorization.isPermitted('api:treatments:delete'), function(req, res, next) {
if (!req.query.find) {
req.query.find = {
_id: req.params.id
};
} else {
req.query.find._id = req.params.id;
}
if (req.query.find._id === '*') {
// match any record id
delete req.query.find._id;
}
next();
}, delete_records);
// delete record that match query
api.delete('/treatments/', ctx.authorization.isPermitted('api:treatments:delete'), delete_records);
// update record
api.put('/treatments/', ctx.authorization.isPermitted('api:treatments:update'), function(req, res) {
+5
View File
@@ -28,6 +28,11 @@ function init (env, ctx) {
obj.created_at = (new Date()).toISOString();
}
collection.insert(obj, function (err, doc) {
if (err != null && err.message) {
console.log('Data insertion error', err.message);
fn(err.message, null);
return;
}
storage.reload(function loaded() {
fn(null, doc.ops);
});
+5 -4
View File
@@ -1076,9 +1076,11 @@ client.load = function load(serverSettings, callback) {
function prepareEntries ( ) {
// Post processing after data is in
var temp1 = [ ];
if (client.ddata.cal && client.rawbg.isEnabled(client.sbx)) {
var sbx = client.sbx.withExtendedSettings(client.rawbg);
if (client.ddata.cal && client.rawbg.isEnabled(sbx)) {
temp1 = client.ddata.sgvs.map(function (entry) {
var rawbgValue = client.rawbg.showRawBGs(entry.mgdl, entry.noise, client.ddata.cal, client.sbx) ? client.rawbg.calc(entry, client.ddata.cal, client.sbx) : 0;
var rawbgValue = client.rawbg.showRawBGs(entry.mgdl, entry.noise, client.ddata.cal, sbx) ? client.rawbg.calc(entry, client.ddata.cal, sbx) : 0;
if (rawbgValue > 0) {
return { mills: entry.mills - 2000, mgdl: rawbgValue, color: 'white', type: 'rawbg' };
} else {
@@ -1152,10 +1154,9 @@ client.load = function load(serverSettings, callback) {
} else if (!inRetroMode()) {
chart.update(false);
client.plugins.updateVisualisations(client.nowSBX);
brushed();
}
}
};
module.exports = client;
+3 -2
View File
@@ -137,10 +137,11 @@ function init (client, d3) {
function getRawbgInfo ( ) {
var info = { };
var sbx = client.sbx.withExtendedSettings(client.rawbg);
if (d.type === 'sgv') {
info.noise = client.rawbg.noiseCodeToDisplay(d.mgdl, d.noise);
if (client.rawbg.showRawBGs(d.mgdl, d.noise, client.ddata.cal, client.sbx)) {
info.value = utils.scaleMgdl(client.rawbg.calc(d, client.ddata.cal, client.sbx));
if (client.rawbg.showRawBGs(d.mgdl, d.noise, client.ddata.cal, sbx)) {
info.value = utils.scaleMgdl(client.rawbg.calc(d, client.ddata.cal, sbx));
}
}
return info;
+4 -2
View File
@@ -259,11 +259,13 @@ function loadProfileSwitchTreatments(ddata, ctx, callback) {
var tq = {
find: {
eventType: 'Profile Switch',
created_at: dateRange
created_at: dateRange,
duration: 0
},
sort: {
created_at: -1
}
},
count: 1
};
ctx.treatments.list(tq, function(err, results) {
+1 -1
View File
@@ -79,7 +79,7 @@ hashauth.init = function init(client, $) {
var translate = client.translate;
hashauth.injectHtml();
$( '#requestauthenticationdialog' ).dialog({
width: 500
width: 350
, height: 240
, buttons: [
{
+1602 -164
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -69,6 +69,10 @@ function init() {
, name: 'CGM Sensor Insert'
, bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
}
, { val: 'Sensor Stop'
, name: 'CGM Sensor Stop'
, bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
}
, { val: 'Pump Battery Change'
, name: 'Pump Battery Change'
, bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
-88
View File
@@ -1,88 +0,0 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Nightscout/IFTTT Maker](#nightscoutifttt-maker)
- [Overview](#overview)
- [Events](#events)
- [Configuration](#configuration)
- [Create a recipe](#create-a-recipe)
- [Start [creating a recipe](https://ifttt.com/myrecipes/personal/new)](#start-creating-a-recipehttpsiftttcommyrecipespersonalnew)
- [1. Choose a Trigger Channel](#1-choose-a-trigger-channel)
- [2. Choose a Trigger](#2-choose-a-trigger)
- [3. Complete Trigger Fields](#3-complete-trigger-fields)
- [4. That](#4-that)
- [5. Choose an Action](#5-choose-an-action)
- [6. Complete Action Fields](#6-complete-action-fields)
- [7. Create and Connect](#7-create-and-connect)
- [Result](#result)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Nightscout/IFTTT Maker
======================================
## Overview
In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Maker](https://ifttt.com/maker).
With Maker you are able to integrate with all the other [IFTTT Channels](https://ifttt.com/channels). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more.
## Events
Plugins can create custom events, but all events sent to maker will be prefixed with `ns-`. The core events are:
* `ns-event` - This event is sent to the maker service for all alarms and notifications. This is good catch all event for general logging.
* `ns-allclear` - This event is sent to the maker service when an alarm has been ack'd or when the server starts up without triggering any alarms. For example, you could use this event to turn a light to green.
* `ns-info` - Plugins that generate notifications at the info level will cause this event to also be triggered. It will be sent in addition to `ns-event`.
* `ns-warning` - Alarms at the warning level with cause this event to also be triggered. It will be sent in addition to `ns-event`.
* `ns-urgent` - Alarms at the urgent level with cause this event to also be triggered. It will be sent in addition to `ns-event`.
* `ns-warning-high` - Alarms at the warning level with cause this event to also be triggered. It will be sent in addition to `ns-event` and `ns-warning`.
* `ns-urgent-high` - Alarms at the urgent level with cause this event to also be triggered. It will be sent in addition to `ns-event` and `ns-urgent`.
* `ns-warning-low` - Alarms at the warning level with cause this event to also be triggered. It will be sent in addition to `ns-event` and `ns-warning`.
* `ns-urgent-low` - Alarms at the urgent level with cause this event to also be triggered. It will be sent in addition to `ns-event` and `ns-urgent`.
* `ns-info-treatmentnotify` - When a treatment is entered into the care portal this event is triggered. It will be sent in addition to `ns-event` and `ns-info`.
* `ns-warning-bwp` - When the BWP plugin generates a warning alarm. It will be sent in addition to `ns-event` and `ns-warning`.
* `ns-urgent-bwp` - When the BWP plugin generates an urgent alarm. It will be sent in addition to `ns-event` and `ns-urget`.
## Configuration
1. Setup IFTTT account: [login](https://ifttt.com/login) or [create an account](https://ifttt.com/join)
2. Find your secret key on the [maker page](https://ifttt.com/maker)
3. Configure Nightscout by setting these environment variables:
* `ENABLE` - `maker` should be added to the list of plugin, for example: `ENABLE="maker"`.
* `MAKER_KEY` - Set this to your secret key that you located in step 2, for example: `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"`
## Create a recipe
### Start [creating a recipe](https://ifttt.com/myrecipes/personal/new)
![screen shot 2015-06-29 at 10 58 48 pm](https://cloud.githubusercontent.com/assets/751143/8425240/bab51986-1eb8-11e5-88fb-5aed311896be.png)
### 1. Choose a Trigger Channel
![screen shot 2015-06-29 at 10 59 01 pm](https://cloud.githubusercontent.com/assets/751143/8425243/c007ace6-1eb8-11e5-96d1-b13f9c3d071f.png)
### 2. Choose a Trigger
![screen shot 2015-06-29 at 10 59 18 pm](https://cloud.githubusercontent.com/assets/751143/8425246/c77c5a4e-1eb8-11e5-9084-32ae40518ee0.png)
### 3. Complete Trigger Fields
![screen shot 2015-06-29 at 10 59 33 pm](https://cloud.githubusercontent.com/assets/751143/8425249/ced7b450-1eb8-11e5-95a3-730f6b9b2925.png)
### 4. That
![screen shot 2015-06-29 at 10 59 46 pm](https://cloud.githubusercontent.com/assets/751143/8425251/d46e1dc8-1eb8-11e5-91be-8dc731e308b2.png)
### 5. Choose an Action
![screen shot 2015-06-29 at 11 00 12 pm](https://cloud.githubusercontent.com/assets/751143/8425254/de634844-1eb8-11e5-8f09-cd43c41ccf3f.png)
### 6. Complete Action Fields
**Example:** `Nightscout: {{Value1}} {{Value2}} {{Value3}}`
![screen shot 2015-06-29 at 11 02 14 pm](https://cloud.githubusercontent.com/assets/751143/8425267/f2da6dd4-1eb8-11e5-8e4d-cad2590d111f.png)
![screen shot 2015-06-29 at 11 02 21 pm](https://cloud.githubusercontent.com/assets/751143/8425272/f83ceb62-1eb8-11e5-8ea2-afd4dcbd391f.png)
### 7. Create and Connect
![screen shot 2015-06-29 at 11 02 43 pm](https://cloud.githubusercontent.com/assets/751143/8425277/fe52f618-1eb8-11e5-8d7f-e0b34eebe29a.png)
### Result
![cinpiqkumaa33u7](https://cloud.githubusercontent.com/assets/751143/8425925/e7d08d2c-1ebf-11e5-853c-cdc5381c4186.png)
+12 -2
View File
@@ -13,6 +13,12 @@ function init (ctx) {
, pillFlip: true
};
rawbg.getPrefs = function getPrefs (sbx) {
return {
display: (sbx && sbx.extendedSettings.display) ? sbx.extendedSettings.display : 'unsmoothed'
};
};
rawbg.setProperties = function setProperties (sbx) {
sbx.offerProperty('rawbg', function setRawBG ( ) {
var result = { };
@@ -49,14 +55,18 @@ function init (ctx) {
sbx.pluginBase.updatePillText(rawbg, options);
};
rawbg.calc = function calc(sgv, cal) {
rawbg.calc = function calc(sgv, cal, sbx) {
var raw = 0;
var cleaned = cleanValues(sgv, cal);
var prefs = rawbg.getPrefs(sbx);
if (cleaned.slope === 0 || cleaned.unfiltered === 0 || cleaned.scale === 0) {
raw = 0;
} else if (cleaned.filtered === 0 || sgv.mgdl < 40) {
} else if (cleaned.filtered === 0 || sgv.mgdl < 40 || prefs.display === 'unfiltered') {
raw = cleaned.scale * (cleaned.unfiltered - cleaned.intercept) / cleaned.slope;
} else if (prefs.display === 'filtered') {
raw = cleaned.scale * (cleaned.filtered - cleaned.intercept) / cleaned.slope;
} else {
var ratio = cleaned.scale * (cleaned.filtered - cleaned.intercept) / cleaned.slope / sgv.mgdl;
raw = cleaned.scale * (cleaned.unfiltered - cleaned.intercept) / cleaned.slope / ratio;
+1 -1
View File
@@ -172,7 +172,7 @@ function init(ctx) {
result.level = levels.WARN;
}
if (sensorInfo.xdripjs.voltageb && (sensorInfo.xdripjs.voltageb < prefs.warnBatV)) {
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';
+127
View File
@@ -31,6 +31,7 @@ daytoday.html = function html(client) {
+ '<input type="checkbox" id="rp_optionsraw"><span style="color:gray;opacity:1">'+translate('Raw')+'</span>'
+ '<input type="checkbox" id="rp_optionsiob"><span style="color:blue;opacity:0.5">'+translate('IOB')+'</span>'
+ '<input type="checkbox" id="rp_optionscob"><span style="color:red;opacity:0.5">'+translate('COB')+'</span>'
+ '<input type="checkbox" id="rp_optionspredicted"><span style="color:sienna;opacity:0.5">'+translate('Predictions')+'</span>'
+ '<input type="checkbox" id="rp_optionsopenaps"><span style="color:sienna;opacity:0.5">'+translate('OpenAPS')+'</span>'
+ '<input type="checkbox" id="rp_optionsdistribution"><span style="color:blue;opacity:0.5">'+translate('Insulin distribution')+'</span>'
+ '&nbsp;'+translate('Size')
@@ -39,6 +40,7 @@ daytoday.html = function html(client) {
+ ' <option x="1000" y="300" selected>1000x300px</option>'
+ ' <option x="1200" y="400">1200x400px</option>'
+ ' <option x="1550" y="600">1550x600px</option>'
+ ' <option x="2400" y="800">2400x800px</option>'
+ '</select>'
+ '<br>'
+ translate('Scale') + ': '
@@ -46,6 +48,19 @@ daytoday.html = function html(client) {
+ translate('Linear')
+ '<input type="radio" name="rp_scale" id="rp_log">'
+ translate('Logarithmic')
+ '<div id="rp_predictedSettings" style="display:none">'
+ translate('Truncate predictions: ')
+ '<input type="checkbox" id="rp_optionsPredictedTruncate" checked>'
+ '<br>'
+ translate('Predictions offset') + ': '
+ '<b><label id="rp_predictedOffset"></label> minutes</b>'
+ '&nbsp;&nbsp;&nbsp;&nbsp;'
+ '<input type="button" onclick="predictMoreBackward();" value="' + translate('-30 min')+'">'
+ '<input type="button" onclick="predictBackward();" value="' + translate('-5 min')+'">'
+ '<input type="button" onclick="predictResetToZero();" value="' + translate('Zero')+'">'
+ '<input type="button" onclick="predictForward();" value="' + translate('+5 min')+'">'
+ '<input type="button" onclick="predictMoreForward();" value="' + translate('+30 min')+'">'
+ '</div>'
+ '<br>'
+ '<div id="daytodaycharts">'
+ '</div>'
@@ -292,6 +307,115 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options)
return sel;
}
// PREDICTIONS START
//
function preparePredictedData() {
var treatmentsTimestamps = []; // Only timestamps for (carbs and bolus insulin) treatments will be captured in this array
treatmentsTimestamps.push(dataRange[0]); // Create a fake timestamp at midnight so we can show predictions during night
for (var i in data.treatments) {
var treatment = data.treatments[i];
if (undefined != treatment.carbs && null != treatment.carbs && treatment.carbs > 0) {
if (treatment.timestamp)
treatmentsTimestamps.push(treatment.timestamp);
else if (treatment.created_at)
treatmentsTimestamps.push(treatment.created_at);
}
if (undefined != treatment.insulin && null != treatment.insulin && treatment.insulin > 0) {
if (treatment.timestamp)
treatmentsTimestamps.push(treatment.timestamp);
else if (treatment.created_at)
treatmentsTimestamps.push(treatment.created_at);
}
}
var predictions = [];
if (data && data.devicestatus) {
for (var i = data.devicestatus.length - 1; i >= 0; i--) {
if (data.devicestatus[i].loop && data.devicestatus[i].loop.predicted) {
predictions.push(data.devicestatus[i].loop.predicted);
} else if (data.devicestatus[i].openaps && data.devicestatus[i].openaps.suggested && data.devicestatus[i].openaps.suggested.predBGs) {
var entry = {};
entry.startDate = data.devicestatus[i].openaps.suggested.timestamp;
// For OpenAPS/AndroidAPS we fall back from COB if present, to UAM, then IOB
if (data.devicestatus[i].openaps.suggested.predBGs.COB) {
entry.values = data.devicestatus[i].openaps.suggested.predBGs.COB;
} else if (data.devicestatus[i].openaps.suggested.predBGs.UAM) {
entry.values = data.devicestatus[i].openaps.suggested.predBGs.UAM;
} else entry.values = data.devicestatus[i].openaps.suggested.predBGs.IOB;
predictions.push(entry);
}
}
}
var p = [];
if (predictions.length > 0 && treatmentsTimestamps.length > 0) {
// Iterate over all treatments, find the predictions for each and add them to the predicted array p
for (var treatmentsIndex = 0; treatmentsIndex < treatmentsTimestamps.length; treatmentsIndex++) {
var timestamp = treatmentsTimestamps[treatmentsIndex];
var predictedIndex = findPredicted(predictions, timestamp, predictedOffset); // Find predictions offset before or after timestamp
if (predictedIndex != null) {
var entry = predictions[predictedIndex]; // Start entry
var d = moment(entry.startDate);
var end = moment().endOf('day');
if (options.predictedTruncate) {
if (predictedOffset >= 0) {
// If we are looking forward we want to stop at the next treatment
if (treatmentsIndex < treatmentsTimestamps.length - 1) {
end = moment(treatmentsTimestamps[treatmentsIndex + 1]);
}
} else {
// If we are looking back, then we want to stop at "this" treatment
end = moment(treatmentsTimestamps[treatmentsIndex]);
}
}
for (var entryIndex in entry.values) {
if (!d.isAfter(end)) {
var value = {};
value.sgv = client.utils.scaleMgdl(entry.values[entryIndex]);
value.date = d.toDate();
value.color = 'purple';
p.push(value);
d.add(5, 'minutes');
}
}
}
}
}
return p;
}
/* Find the earliest new predicted instance that has a timestamp equal to or larger than timestamp */
/* (so if we have bolused or eaten we want to find the prediction that Loop has estimated just after that) */
/* Returns the index into the predictions array that is the predicted we are looking for */
function findPredicted(predictions, timestamp, offset) {
var ts = moment(timestamp).add(offset, 'minutes');
var predicted = null;
if (offset && offset < 0) { // If offset is negative, start searching from first prediction going forward
for (var i = 0; i < predictions.length; i++) {
if (predictions[i] && predictions[i].startDate && moment(predictions[i].startDate) <= ts) {
predicted = i;
}
}
} else { // If offset is positive or zero, start searching from last prediction going backward
for (var i = predictions.length - 1; i > 0; i--) {
if (predictions[i] && predictions[i].startDate && moment(predictions[i].startDate) >= ts) {
predicted = i;
}
}
}
return predicted;
}
//
// PREDICTIONS ENDS
// bind up the context chart data to an array of circles
var contextData = (options.predicted ? data.sgv.concat(preparePredictedData()) : data.sgv);
var contextCircles = context.selectAll('circle').data(contextData);
// if new circle then just display
prepareContextCircles(contextCircles.enter().append('circle'));
@@ -899,6 +1023,9 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options)
});
function appendProfileSwitch(context, treatment) {
if (!treatment.cutting && !treatment.profile) { return; }
var sign = treatment.first ? '▲▲▲' : '▬▬▬';
var text;
if (treatment.cutting) {
+4 -2
View File
@@ -32,11 +32,13 @@ glucosedistribution.html = function html(client) {
'<br/><div id="explanation">' +
'* ' + translate('This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:') +
'Nathan, David M., et al. "Translating the A1C assay into estimated average glucose values." <i>Diabetes care</i> 31.8 (2008): 1473-1478.' + '<br/><br/>' +
translate('Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.') + '<br/><br/>' +
translate('Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.') + '<br/><br/>' +
translate('Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.')+ '<br/><br/>' +
translate('Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.')+ '<br/><br/>' +
translate('Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.')+ '<br/><br/>' +
translate('GVI and PGS are measures developed by Dexcom, detailed <a href="http://www.healthline.com/diabetesmine/a-new-view-of-glycemic-variability-how-long-is-your-line#2">here</a>.')+
translate('GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed <a href="')+
'https://web.archive.org/web/20160523152519/http://www.healthline.com/diabetesmine/a-new-view-of-glycemic-variability-how-long-is-your-line'+
translate('">can be found here</a>.')+
'</div><br/><br/>' +
translate('Filter by hours') + ':' +
'<br/>' +
+2
View File
@@ -11,6 +11,7 @@ function init() {
}
, allPlugins = [
require('./daytoday')()
, require('./weektoweek')()
, require('./dailystats')()
, require('./glucosedistribution')()
, require('./hourlystats')()
@@ -19,6 +20,7 @@ function init() {
, require('./calibrations')()
, require('./treatments')()
, require('./profiles')()
, require('./loopalyzer')()
];
consts.scaleYFromSettings = function scaleYFromSettings (client) {
File diff suppressed because it is too large Load Diff
+333
View File
@@ -0,0 +1,333 @@
'use strict';
var _ = require('lodash');
var moment = window.moment;
var d3 = (global && global.d3) || require('d3');
var dayColors = [
'rgb(73, 22, 153)'
, 'rgb(34, 201, 228)'
, 'rgb(0, 153, 123)'
, 'rgb(135, 135, 228)'
, 'rgb(135, 49, 204)'
, 'rgb(36, 36, 228)'
, 'rgb(0, 234, 188)'
];
var weektoweek = {
name: 'weektoweek'
, label: 'Week to week'
, pluginType: 'report'
};
function init() {
return weektoweek;
}
module.exports = init;
weektoweek.html = function html(client) {
var translate = client.translate;
var ret =
'<h2>' + translate('Week to week') + '</h2>'
+ '<b>' + translate('To see this report, press SHOW while in this view') + '</b><br>'
+ '&nbsp;'+translate('Size')
+ ' <select id="wrp_size">'
+ ' <option x="800" y="250">800x250px</option>'
+ ' <option x="1000" y="300" selected>1000x300px</option>'
+ ' <option x="1200" y="400">1200x400px</option>'
+ ' <option x="1550" y="600">1550x600px</option>'
+ '</select>'
+ '<br>'
+ translate('Scale') + ': '
+ '<input type="radio" name="wrp_scale" id="wrp_linear" checked>'
+ translate('Linear')
+ '<input type="radio" name="wrp_scale" id="wrp_log">'
+ translate('Logarithmic')
+ '<br>'
+ '<div id="weektoweekcharts">'
+ '</div>'
;
return ret;
};
weektoweek.prepareHtml = function weektoweekPrepareHtml(weekstoshow) {
$('#weektoweekcharts').html('');
var colorIdx = 0;
var legend = '<table>';
legend += '<tr><td><svg width="16" height="16"><g><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>Sunday</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>Monday</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>Tuesday</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>Wednesday</td></tr>';
legend += '<tr><td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>Thursday</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>Friday</td>';
legend += '<td><svg width="16" height="16"><circle fill="' + dayColors[colorIdx++] + '" r="40"></circle></g></svg></td><td>Saturday</td></tr>';
legend += '</table>';
$('#weektoweekcharts').append($(legend));
weekstoshow.forEach(function eachWeek(d) {
$('#weektoweekcharts').append($('<table><tr><td><div id="weektoweekchart-' + d[0] + '-' + d[d.length-1] + '"></div></td><td><div id="weektoweekstatchart-' + d[0] + '-' + d[d.length-1] + '"></td></tr></table>'));
});
};
weektoweek.report = function report_weektoweek(datastorage, sorteddaystoshow, options) {
var Nightscout = window.Nightscout;
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 = [ ];
var startDay = moment(sorteddaystoshow[0] + ' 00:00:00');
sorteddaystoshow.forEach( function eachDay(day) {
var weekNum = Math.abs(moment(day + ' 00:00:00').diff(startDay, 'weeks'));
if (typeof weekstoshow[weekNum] === 'undefined') {
weekstoshow[weekNum] = [ ];
}
weekstoshow[weekNum].push(day);
});
weekstoshow = weekstoshow.map(function orderWeek(week) {
return _.sortBy(week);
});
weektoweek.prepareHtml(weekstoshow);
weekstoshow.forEach( function eachWeek(week) {
var sgvData = [ ];
var weekStart = moment(week[0] + ' 00:00:00');
week.forEach( function eachDay(day) {
var dayNum = Math.abs(moment(day + ' 00:00:00').diff(weekStart, 'days'));
datastorage[day].sgv.forEach ( function eachSgv(sgv) {
var sgvWeekday = moment(sgv.date).day();
var sgvColor = dayColors[sgvWeekday];
if (sgv.color === 'gray') {
sgvColor = sgv.color;
}
sgvData.push( {
'color': sgvColor
, 'date': moment(sgv.date).subtract(dayNum, 'days').toDate()
, 'filtered': sgv.filtered
, 'mills': sgv.mills - dayNum * 24*60*60000
, 'noise': sgv.noise
, 'sgv': sgv.sgv
, 'type': sgv.type
, 'unfiltered': sgv.unfiltered
, 'y': sgv.y
});
});
});
drawChart(week, sgvData, options);
});
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);
} else {
return t12[i];
}
}
function drawChart(week, sgvData, options) {
var tickValues
, charts
, context
, xScale2, yScale2
, xAxis2, yAxis2
, dateFn = function (d) { return new Date(d.date); };
tickValues = client.ticks(client, {
scaleY: options.weekscale === report_plugins.consts.SCALE_LOG ? 'log' : 'linear'
, targetTop: options.targetHigh
, targetBottom: options.targetLow
});
// add defs for combo boluses
var dashWidth = 5;
d3.select('body').append('svg')
.append('defs')
.append('pattern')
.attr('id', 'hash')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 6)
.attr('height', 6)
.attr('x', 0)
.attr('y', 0)
.append('g')
.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);
// create svg and g to contain the chart contents
charts = d3.select('#weektoweekchart-' + week[0] + '-' + week[week.length-1]).html(
'<b>'+
report_plugins.utils.localeDate(week[0])+
'-' +
report_plugins.utils.localeDate(week[week.length-1])+
'</b><br>'
).append('svg');
charts.append('rect')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', 'WhiteSmoke');
context = charts.append('g');
// define the parts of the axis that aren't dependent on width or height
xScale2 = d3.time.scale()
.domain(d3.extent(sgvData, dateFn));
if (options.weekscale === report_plugins.consts.SCALE_LOG) {
yScale2 = d3.scale.log()
.domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]);
} else {
yScale2 = d3.scale.linear()
.domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]);
}
xAxis2 = d3.svg.axis()
.scale(xScale2)
.tickFormat(timeTicks)
.ticks(24)
.orient('bottom');
yAxis2 = d3.svg.axis()
.scale(yScale2)
.tickFormat(d3.format('d'))
.tickValues(tickValues)
.orient('left');
// get current data range
var dataRange = d3.extent(sgvData, dateFn);
// get the entire container height and width subtracting the padding
var chartWidth = options.weekwidth - padding.left - padding.right;
var chartHeight = options.weekheight - padding.top - padding.bottom;
//set the width and height of the SVG element
charts.attr('width', options.weekwidth)
.attr('height', options.weekheight);
// ranges are based on the width and height available so reset
xScale2.range([0, chartWidth]);
yScale2.range([chartHeight,0]);
// add target BG rect
context.append('rect')
.attr('x', xScale2(dataRange[0])+padding.left)
.attr('y', yScale2(options.targetHigh)+padding.top)
.attr('width', xScale2(dataRange[1]- xScale2(dataRange[0])))
.attr('height', yScale2(options.targetLow)-yScale2(options.targetHigh))
.style('fill', '#D6FFD6')
.attr('stroke', 'grey');
// create the x axis container
context.append('g')
.attr('class', 'x axis');
// create the y axis container
context.append('g')
.attr('class', 'y axis');
context.select('.y')
.attr('transform', 'translate(' + (padding.left) + ',' + padding.top + ')')
.style('stroke', 'black')
.style('shape-rendering', 'crispEdges')
.style('fill', 'none')
.call(yAxis2);
// if first run then just display axis with no transition
context.select('.x')
.attr('transform', 'translate(' + padding.left + ',' + (chartHeight + padding.top) + ')')
.style('stroke', 'black')
.style('shape-rendering', 'crispEdges')
.style('fill', 'none')
.call(xAxis2);
_.each(tickValues, function (n, li) {
context.append('line')
.attr('class', 'high-line')
.attr('x1', xScale2(dataRange[0])+padding.left)
.attr('y1', yScale2(tickValues[li])+padding.top)
.attr('x2', xScale2(dataRange[1])+padding.left)
.attr('y2', yScale2(tickValues[li])+padding.top)
.style('stroke-dasharray', ('1, 5'))
.attr('stroke', 'grey');
});
// bind up the context chart data to an array of circles
var contextCircles = context.selectAll('circle')
.data(sgvData);
function prepareContextCircles(sel) {
var badData = [];
sel.attr('cx', function (d) {
return xScale2(d.date) + padding.left;
})
.attr('cy', function (d) {
if (isNaN(d.sgv)) {
badData.push(d);
return yScale2(client.utils.scaleMgdl(450) + padding.top);
} else {
return yScale2(d.sgv) + padding.top;
}
})
.attr('fill', function (d) {
if (d.color === 'gray') {
return 'transparent';
}
return d.color;
})
.style('opacity', function () { return 0.5 })
.attr('stroke-width', function (d) {if (d.type === 'mbg') { return 2; } else { return 0; }})
.attr('stroke', function () { return 'black'; })
.attr('r', function(d) {
if (d.type === 'mbg') {
return 4;
} else {
return 2 + (options.weekwidth - 800) / 400;
}
})
.on('mouseout', hideTooltip);
if (badData.length > 0) {
console.warn('Bad Data: isNaN(sgv)', badData);
}
return sel;
}
// if new circle then just display
prepareContextCircles(contextCircles.enter().append('circle'));
contextCircles.exit()
.remove();
}
function hideTooltip ( ) {
client.tooltip.transition()
.duration(TOOLTIP_TRANS_MS)
.style('opacity', 0);
}
};
+7 -1
View File
@@ -9,6 +9,11 @@ function storage (env, ctx) {
function create (obj, fn) {
obj.created_at = (new Date( )).toISOString( );
api().insert(obj, function (err, doc) {
if (err != null && err.message) {
console.log('Activity data insertion error', err.message);
fn(err.message, null);
return;
}
fn(null, doc.ops);
});
}
@@ -56,7 +61,8 @@ function storage (env, ctx) {
}
function remove (_id, fn) {
return api( ).remove({ '_id': new ObjectID(_id) }, fn);
var objId = new ObjectID(_id);
return api( ).remove({ '_id': objId }, fn);
}
function api ( ) {
+40 -2
View File
@@ -2,10 +2,46 @@
var _ = require('lodash');
var UPDATE_THROTTLE = 1000;
var UPDATE_THROTTLE = 5000;
function boot (env, language) {
//////////////////////////////////////////////////
// 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
// Other versions will not start
// More explicit:
// < 8 does not work, not supported
// >= 8.15.0 works, supported and recommended
// == 9.x does not work, not supported
// == 10.14.1 works, not fully supported and not recommended (Azure version)
// == 10.14.2 does not work, not supported and not recommended,
// >= 10.15.1 works, supported and recommended
// >= 11.6.0 does not work, not recommended, will not be supported. We only support Node LTS releases
///////////////////////////////////////////////////
function checkNode (ctx, next) {
var semver = require('semver');
var nodeVersion = process.version;
if ( semver.satisfies(nodeVersion, '^8.15.0') || semver.satisfies(nodeVersion, '^10.15.1')) {
//Latest Node 8 LTS and Latest Node 10 LTS are recommended and supported.
console.debug('Node version ' + nodeVersion + ' is supported');
next();
}
else if ( semver.eq(nodeVersion, '10.14.1')) {
//Latest Node version on Azure is tolerated, but not recommended
console.log('WARNING: Node version v10.14.1 and Microsoft Azure are not recommended.');
console.log('WARNING: Please migrate to another hosting provider. Your Node version is outdated and insecure');
next();
} else {
// Other versions will not start
console.log( 'ERROR: Node version ' + nodeVersion + ' is not supported. Please upgrade your Node');
process.exit(1);
}
}
function checkEnv (ctx, next) {
ctx.language = language;
if (env.err) {
@@ -105,7 +141,7 @@ function boot (env, language) {
if (hasBootErrors(ctx)) {
return next();
}
ctx.levels = require('../levels');
ctx.levels.translate = ctx.language.translate;
@@ -179,6 +215,7 @@ function boot (env, language) {
});
ctx.bus.on('data-loaded', function updatePlugins ( ) {
console.info('reloading sandbox data');
var sbx = require('../sandbox')().serverInit(env, ctx);
ctx.plugins.setProperties(sbx);
ctx.notifications.initRequests();
@@ -227,6 +264,7 @@ function boot (env, language) {
}
return require('bootevent')( )
.acquire(checkNode)
.acquire(checkEnv)
.acquire(augmentSettings)
.acquire(setupStorage)
+7 -8
View File
@@ -10,6 +10,11 @@ function storage (collection, ctx) {
obj.created_at = (new Date()).toISOString();
}
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');
});
@@ -59,14 +64,8 @@ function storage (collection, ctx) {
).toArray(toArray);
}
function remove (_id, fn) {
var filter;
if (_id === '*') {
filter = {};
} else {
filter = { '_id': new ObjectID(_id) };
}
return api( ).remove(filter, fn);
function remove (opts, fn) {
return api( ).remove(query_for(opts), fn);
}
function api() {
+5 -5
View File
@@ -1,7 +1,6 @@
'use strict';
var es = require('event-stream');
var sgvdata = require('sgvdata');
var find_options = require('./query');
var ObjectID = require('mongodb').ObjectID;
@@ -46,7 +45,11 @@ function storage(env, ctx) {
}
function remove (opts, fn) {
api( ).remove(query_for(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);
});
}
// return writable stream to lint each sgv record passing through it
@@ -75,8 +78,6 @@ function storage(env, ctx) {
//function update (fn) {
//}
//
//function remove (fn) {
//}
// store new documents using the storage mechanism
function create (docs, fn) {
@@ -121,7 +122,6 @@ function storage(env, ctx) {
// Expose all the useful functions
api.list = list;
api.echo = sgvdata.sync.json.echo;
api.map = map;
api.create = create;
api.remove = remove;
+7 -1
View File
@@ -6,6 +6,11 @@ function storage (env, ctx) {
function create (obj, fn) {
obj.created_at = (new Date( )).toISOString( );
api().insert(obj, function (err, doc) {
if (err != null && err.message) {
console.log('Data insertion error', err.message);
fn(err.message, null);
return;
}
fn(null, doc.ops);
});
}
@@ -36,7 +41,8 @@ function storage (env, ctx) {
}
function remove (_id, fn) {
return api( ).remove({ '_id': new ObjectID(_id) }, fn);
var objId = new ObjectID(_id);
return api( ).remove({ '_id': objId }, fn);
}
-317
View File
@@ -1,317 +0,0 @@
'use strict';
var es = require('event-stream');
var Long = require('long');
var decoders = require('sgvdata/lib/protobuf');
var direction = require('sgvdata/lib/utils').direction;
var moment = require('moment');
var url = require('url');
function init (env, ctx) {
function mqtt ( ) {
return mqtt;
}
var info = url.parse(env.MQTT_MONITOR);
var username = info.auth.split(':').slice(0, -1).join('');
var shared_topic = '/downloads/' + username + '/#';
var alias_topic = '/downloads/' + username + '/protobuf';
var notification_topic = '/notifications/' + username + '/json';
env.mqtt_shared_topic = shared_topic;
mqtt.client = connect(env);
var downloads = mqtt.downloads = downloader();
if (mqtt.client) {
listenForMessages(ctx);
}
mqtt.every = every;
mqtt.entries = process();
//expose for tests that don't need to connect
mqtt.sgvSensorMerge = sgvSensorMerge;
function listenForMessages ( ) {
mqtt.client.on('message', function (topic, msg) {
console.log('topic', topic);
// XXX: ugly hack
if (topic === alias_topic) {
topic = '/downloads/protobuf';
}
console.log(topic, 'on message', 'msg', msg.length);
switch (topic) {
case '/uploader':
console.log({type: topic, msg: msg.toString()});
break;
case '/downloads/protobuf':
downloadProtobuf(msg, topic, downloads, ctx);
break;
default:
console.log(topic, 'on message', 'msg', msg);
// ctx.entries.write(msg);
break;
}
});
}
mqtt.emitNotification = function emitNotification(notify) {
console.info('Publishing notification to mqtt: ', notify);
[notification_topic, '/notifications/json'].forEach(function iter_notify (topic) {
mqtt.client.publish(topic, JSON.stringify(notify), function mqttCallback (err) {
if (err) {
console.error('Unable to publish notification to MQTT', err);
}
});
});
};
return mqtt();
}
function connect (env) {
var uri = env.MQTT_MONITOR;
var shared_topic = env.mqtt_shared_topic;
if (!uri) {
return null;
}
var opts = {
encoding: 'binary',
clean: false,
clientId: env.mqtt_client_id
};
var client = require('mqtt').connect(uri, opts);
function granted () { console.log('granted', arguments); }
client.subscribe('sgvs');
client.subscribe('published');
client.subscribe('/downloads/protobuf', {qos: 2}, granted);
client.subscribe(shared_topic, {qos: 2}, granted);
client.subscribe('/uploader', granted);
client.subscribe('/entries/sgv', granted);
return client;
}
function process ( ) {
var stream = es.through(
function _write(data) {
this.push(data);
}
);
return stream;
}
function every (storage) {
function iter(item, next) {
storage.create(item, next);
}
return es.map(iter);
}
function downloader ( ) {
var opts = {
model: decoders.models.G4Download
, json: function (o) {
return o;
}
, payload: function (o) {
return o;
}
};
return decoders(opts);
}
function downloadProtobuf (msg, topic, downloads, ctx) {
var b = new Buffer(msg, 'binary');
console.log('BINARY', b.length, b.toString('hex'));
var packet;
try {
packet = downloads.parse(b);
if (!packet.type) {
packet.type = topic;
}
console.log('DOWNLOAD msg', msg.length, packet);
console.log('download SGV', packet.sgv[0]);
console.log('download_timestamp', packet.download_timestamp, new Date(Date.parse(packet.download_timestamp)));
console.log('WRITE TO MONGO');
var download_timestamp = moment(packet.download_timestamp);
if (packet.download_status === 0) {
es.readArray(sgvSensorMerge(packet)).pipe(ctx.entries.persist(function empty(err, result) {
console.log('DONE WRITING MERGED SGV TO MONGO', err, result);
}));
iter_mqtt_record_stream(packet, 'cal', toCal)
.pipe(ctx.entries.persist(function empty(err, result) {
console.log('DONE WRITING Cal TO MONGO', err, result.length);
}));
iter_mqtt_record_stream(packet, 'meter', toMeter)
.pipe(ctx.entries.persist(function empty(err, result) {
console.log('DONE WRITING Meter TO MONGO', err, result.length);
}));
}
packet.type = 'download';
ctx.devicestatus.create({
uploaderBattery: packet.uploader_battery,
created_at: download_timestamp.toISOString()
}, function empty(err, result) {
console.log('DONE WRITING TO MONGO devicestatus ', result, err);
});
ctx.entries.create([ packet ], function empty(err) {
if (err) {
console.log('Error writting to mongo: ', err);
} else {
console.log('Download written to mongo: ', packet);
}
});
} catch (e) {
console.log('DID NOT PARSE', e);
}
}
function toSGV (proto, vars) {
vars.sgv = proto.sgv_mgdl;
vars.direction = direction(proto.trend);
vars.noise = proto.noise;
vars.type = 'sgv';
return vars;
}
function toCal (proto, vars) {
vars.slope = proto.slope;
vars.intercept = proto.intercept;
vars.scale = proto.scale;
vars.type = 'cal';
return vars;
}
function toSensor (proto, vars) {
vars.filtered = new Long(proto.filtered).toInt();
vars.unfiltered = new Long(proto.unfiltered).toInt();
vars.rssi = proto.rssi;
vars.type = 'sensor';
return vars;
}
function toMeter (proto, result) {
result.type = 'mbg';
result.mbg = proto.mbg || proto.meter_bg_mgdl;
return result;
}
function toTimestamp (proto, receiver_time, download_time) {
var record_offset = receiver_time - proto.sys_timestamp_sec;
var record_time = download_time.clone( ).subtract(record_offset, 'second');
var obj = {
device: 'dexcom'
, date: record_time.unix() * 1000
, dateString: record_time.format( )
};
return obj;
}
function timestampFactory (packet) {
var receiver_time = packet.receiver_system_time_sec;
var download_time = moment(packet.download_timestamp);
function timestamp (item) {
return toTimestamp(item, receiver_time, download_time.clone( ));
}
return timestamp;
}
function timeSort (a, b) {
return a.date - b.date;
}
function sgvSensorMerge (packet) {
var timestamp = timestampFactory(packet);
var sgvs = (packet['sgv'] || []).map(function(sgv) {
var timestamped = timestamp(sgv);
return toSGV(sgv, timestamped);
}).sort(timeSort);
var sensors = (packet['sensor'] || []).map(function(sensor) {
var timestamped = timestamp(sensor);
return toSensor(sensor, timestamped);
}).sort(timeSort);
//based on com.nightscout.core.dexcom.Utils#mergeGlucoseDataRecords
var merged = []
, sgvsLength = sgvs.length
, sensorsLength = sensors.length;
if (sgvsLength >= 0 && sensorsLength === 0) {
merged = sgvs;
} else {
var smallerLength = Math.min(sgvsLength, sensorsLength);
for (var i = 1; i <= smallerLength; i++) {
var sgv = sgvs[sgvsLength - i];
var sensor = sensors[sensorsLength - i];
if (sgv && sensor && Math.abs(sgv.date - sensor.date) < 10000) {
//timestamps are close so merge
sgv.filtered = sensor.filtered;
sgv.unfiltered = sensor.unfiltered;
sgv.rssi = sensor.rssi;
merged.push(sgv);
} else {
console.info('mismatch or missing, sgv: ', sgv, ' sensor: ', sensor);
//timestamps aren't close enough so add both
if (sgv) { merged.push(sgv); }
//but the sensor will become and sgv now
if (sensor) {
sensor.type = 'sgv';
merged.push(sensor);
}
}
}
//any extra sgvs?
if (sgvsLength > smallerLength) {
for (var j = 0; j < sgvsLength - smallerLength; j++) {
var extraSGV = sgvs[j];
merged.push(extraSGV);
}
}
//any extra sensors?
if (sensorsLength > smallerLength) {
for (var k = 0; k < sensorsLength - smallerLength; k++) {
var extraSensor = sensors[k];
//from now on we consider it a sgv
extraSensor.type = 'sgv';
merged.push(extraSensor);
}
}
}
return merged;
}
function iter_mqtt_record_stream (packet, prop, sync) {
var list = packet[prop];
console.log('incoming', prop, (list || [ ]).length);
var stream = es.readArray(list || [ ]);
var receiver_time = packet.receiver_system_time_sec;
var download_time = moment(packet.download_timestamp);
function map(item, next) {
var timestamped = toTimestamp(item, receiver_time, download_time.clone( ));
var r = sync(item, timestamped);
if (!('type' in r)) {
r.type = prop;
}
console.log('ITEM', item, 'TO', prop, r);
next(null, r);
}
return stream.pipe(es.map(map));
}
init.downloadProtobuf = downloadProtobuf;
module.exports = init;
+2 -1
View File
@@ -32,7 +32,8 @@ function storage (collection, ctx) {
}
function remove (_id, fn) {
api( ).remove({ '_id': new ObjectID(_id) }, fn);
var objId = new ObjectID(_id);
api( ).remove({ '_id': objId }, fn);
ctx.bus.emit('data-received');
}
+6 -5
View File
@@ -96,11 +96,12 @@ function storage (env, ctx) {
return find_options(opts, storage.queryOpts);
}
function remove (_id, fn) {
api( ).remove({ '_id': new ObjectID(_id) }, fn);
ctx.bus.emit('data-received');
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
ctx.bus.emit('data-received');
fn(err, stat);
});
}
function save (obj, fn) {
+18 -3
View File
@@ -226,8 +226,9 @@ function init (env, ctx, server) {
return;
}
var objId = new ObjectID(data._id);
ctx.store.collection(collection).update(
{ '_id': new ObjectID(data._id) },
{ '_id': objId },
{ $unset: data.data }
);
@@ -325,8 +326,9 @@ function init (env, ctx, server) {
if (err || array.length > 0) {
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': new ObjectID(array[0]._id) },
{ '_id': objId },
{ $set: {created_at: data.data.created_at} }
);
if (callback) {
@@ -338,6 +340,10 @@ function init (env, ctx, server) {
// if not found create new record
console.log(LOG_DEDUP + 'Adding new record');
ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) {
if (err != null && err.message) {
console.log('treatments data insertion error: ', err.message);
return;
}
if (callback) {
callback(doc.ops);
}
@@ -367,6 +373,10 @@ function init (env, ctx, server) {
}
});
ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) {
if (err != null && err.message) {
console.log('devicestatus insertion error: ', err.message);
return;
}
if (callback) {
callback(doc.ops);
}
@@ -374,6 +384,10 @@ function init (env, ctx, server) {
});
} else {
ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) {
if (err != null && err.message) {
console.log(data.collection + ' insertion error: ', err.message);
return;
}
if (callback) {
callback(doc.ops);
}
@@ -398,8 +412,9 @@ function init (env, ctx, server) {
return;
}
var objId = new ObjectID(data._id);
ctx.store.collection(collection).remove(
{ '_id': new ObjectID(data._id) }
{ '_id': objId }
);
if (callback) {
+10 -1
View File
@@ -41,7 +41,12 @@ function init ( ) {
, bgTargetTop: 180
, bgTargetBottom: 80
, bgLow: 55
}
},
insecureUseHttp: false,
secureHstsHeader: true,
secureHstsHeaderIncludeSubdomains: false,
secureHstsHeaderPreload: false,
secureCsp: false
};
var valueMappers = {
@@ -59,6 +64,10 @@ function init ( ) {
, alarmTimeagoUrgent: mapTruthy
, alarmWarnMins: mapNumberArray
, timeFormat: mapNumber
, insecureUseHttp: mapTruthy
, secureHstsHeader: mapTruthy
, secureCsp: mapTruthy
};
function mapNumberArray (value) {
+2 -1
View File
@@ -25,7 +25,8 @@ function init (env, cb, forceNewConnection) {
console.log('Setting up new connection to MongoDB');
var timeout = 30 * 1000;
var options = { reconnectInterval: 10000, reconnectTries: 500, connectTimeoutMS: timeout, socketTimeoutMS: timeout };
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) {
+1853 -4622
View File
File diff suppressed because it is too large Load Diff
+33 -34
View File
@@ -1,6 +1,6 @@
{
"name": "Nightscout",
"version": "0.10.3-master-20180805",
"name": "nightscout",
"version": "0.11.0-rc1-20190205",
"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",
@@ -48,67 +48,66 @@
}
},
"engines": {
"node": "8.11.x",
"npm": "6.x || 5.8.x || 5.7.x || 5.6.x"
"node": "^10.14.1 || ^8.15.0",
"npm": "6.x"
},
"dependencies": {
"ajv": "^6.5.1",
"async": "^0.9.2",
"body-parser": "^1.18.3",
"bootevent": "0.0.1",
"compression": "^1.7.3",
"css-loader": "^0.28.11",
"css-loader": "^1.0.1",
"cssmin": "^0.4.3",
"d3": "^3.5.17",
"ejs": "^2.6.1",
"errorhandler": "^1.5.0",
"event-stream": "^3.3.4",
"event-stream": "^4.0.1",
"expand-braces": "^0.1.2",
"expose-loader": "^0.7.5",
"express": "^4.16.3",
"express": "^4.16.4",
"express-minify": "^1.0.0",
"file-loader": "^1.1.11",
"flot": "^0.8.0-alpha",
"helmet": "^3.15.0",
"jquery": "^3.3.1",
"jquery-ui-bundle": "^1.12.1-migrate",
"jquery.tooltips": "^1.0.0",
"js-storage": "^1.0.4",
"jsonwebtoken": "^8.3.0",
"lodash": "^4.17.10",
"long": "^3.2.0",
"mime": "^2.3.1",
"jsdom": "~11.11.0",
"jsonwebtoken": "^8.4.0",
"lodash": "^4.17.11",
"mime": "^2.4.0",
"minimed-connect-to-nightscout": "^1.1.1",
"moment": "^2.22.2",
"moment-timezone": "^0.5.21",
"mongodb": "~3.0.11",
"moment": "^2.24.0",
"moment-timezone": "^0.5.23",
"mongodb": "^3.1.13",
"mongomock": "^0.1.2",
"mqtt": "^2.18.3",
"node-cache": "^4.2.0",
"parse-duration": "^0.1.1",
"prettyjson": "^1.2.1",
"pushover-notifications": "^0.2.4",
"pushover-notifications": "^1.2.0",
"random-token": "0.0.8",
"request": "^2.87.0",
"sgvdata": "git://github.com/ktind/sgvdata.git#wip/protobuf",
"share2nightscout-bridge": "git://github.com/nightscout/share2nightscout-bridge.git#wip/generalize",
"shiro-trie": "^0.3.13",
"request": "^2.88.0",
"semver": "^5.6.0",
"share2nightscout-bridge": "^0.2.0",
"shiro-trie": "^0.4.8",
"simple-statistics": "^0.7.0",
"socket.io": "^2.1.1",
"style-loader": "^0.20.2",
"traverse": "^0.6.6",
"uglify-js": "^3.4.6",
"uuid": "^3.2.1"
"socket.io": "~2.1.1",
"swagger-ui-dist": "^3.20.6",
"terser": "^3.16.1",
"traverse": "^0.6.6"
},
"devDependencies": {
"benv": "^3.3.0",
"clear-require": "^2.0.0",
"expose-loader": "^0.7.5",
"file-loader": "^3.0.1",
"istanbul": "^0.4.5",
"mocha": "^5.2.0",
"moment-locales-webpack-plugin": "^1.0.7",
"should": "^13.2.2",
"supertest": "^3.1.0",
"webpack": "^4.16.3",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-command": "^0.4.1"
"should": "^13.2.3",
"style-loader": "^0.23.1",
"supertest": "^3.4.2",
"terser-webpack-plugin": "^1.2.2",
"webpack": "^4.29.1",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.2.1"
}
}
-9
View File
@@ -54,12 +54,6 @@ require('./lib/server/bootevent')(env, language).boot(function booted (ctx) {
return;
}
if (env.MQTT_MONITOR) {
ctx.mqtt = require('./lib/server/mqtt')(env, ctx);
var es = require('event-stream');
es.pipeline(ctx.mqtt.entries, ctx.entries.map( ), ctx.mqtt.every(ctx.entries));
}
///////////////////////////////////////////////////
// setup socket io for data and message transmission
///////////////////////////////////////////////////
@@ -71,9 +65,6 @@ require('./lib/server/bootevent')(env, language).boot(function booted (ctx) {
ctx.bus.on('notification', function(notify) {
websocket.emitNotification(notify);
if (ctx.mqtt) {
ctx.mqtt.emitNotification(notify);
}
});
//after startup if there are no alarms send all clear
+1 -3
View File
@@ -2,8 +2,6 @@
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get update
sudo apt-get install -y nodejs
sudo apt-get install -y python-software-properties python git
sudo apt-get install -y build-essential
sudo apt-get install -y nodejs build-essential git
npm install
+8
View File
@@ -1,3 +1,11 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [colorbrewer](#colorbrewer)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
colorbrewer
===========
+12
View File
@@ -86,6 +86,18 @@ body {
z-index: 2;
}
button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.ui-button {
background-color: grey;
border: 1px solid white;
padding: 2px;
}
a, a:visited, a:link {
color: #2196f3;
text-decoration: none;
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -1,11 +1,11 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?25902905');
src: url('../font/fontello.eot?25902905#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?25902905') format('woff2'),
url('../font/fontello.woff?25902905') format('woff'),
url('../font/fontello.ttf?25902905') format('truetype'),
url('../font/fontello.svg?25902905#fontello') format('svg');
src: url('../font/fontello.eot?50735338');
src: url('../font/fontello.eot?50735338#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?50735338') format('woff2'),
url('../font/fontello.woff?50735338') format('woff'),
url('../font/fontello.ttf?50735338') format('truetype'),
url('../font/fontello.svg?50735338#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?25902905#fontello') format('svg');
src: url('../font/fontello.svg?50735338#fontello') format('svg');
}
}
*/
+30 -33
View File
@@ -229,11 +229,11 @@ body {
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?94753240');
src: url('./font/fontello.eot?94753240#iefix') format('embedded-opentype'),
url('./font/fontello.woff?94753240') format('woff'),
url('./font/fontello.ttf?94753240') format('truetype'),
url('./font/fontello.svg?94753240#fontello') format('svg');
src: url('./font/fontello.eot?92660326');
src: url('./font/fontello.eot?92660326#iefix') format('embedded-opentype'),
url('./font/fontello.woff?92660326') format('woff'),
url('./font/fontello.ttf?92660326') format('truetype'),
url('./font/fontello.svg?92660326#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -275,7 +275,7 @@ body {
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
</style>
<link rel="stylesheet" href="css/animation.css"><!--[if IE 7]><link rel="stylesheet" href="css/fontello-ie7.css"><![endif]-->
<link rel="stylesheet" href="css/animation.css"><!--[if IE 7]><link rel="stylesheet" href="css/" + font.fontname + "-ie7.css"><![endif]-->
<script>
function toggleCodes(on) {
var obj = document.getElementById('icons');
@@ -291,49 +291,46 @@ body {
</head>
<body>
<div class="container header">
<h1>
fontello
<small>font demo</small>
</h1>
<h1>fontello <small>font demo</small></h1>
<label class="switch">
<input type="checkbox" onclick="toggleCodes(this.checked)">show codes
</label>
</div>
<div id="icons" class="container">
<div class="container" id="icons">
<div class="row">
<div title="Code: 0xe800" class="the-icons span3"><i class="demo-icon icon-help-circled">&#xe800;</i> <span class="i-name">icon-help-circled</span><span class="i-code">0xe800</span></div>
<div title="Code: 0xe801" class="the-icons span3"><i class="demo-icon icon-angle-double-up">&#xe801;</i> <span class="i-name">icon-angle-double-up</span><span class="i-code">0xe801</span></div>
<div title="Code: 0xe802" class="the-icons span3"><i class="demo-icon icon-angle-double-down">&#xe802;</i> <span class="i-name">icon-angle-double-down</span><span class="i-code">0xe802</span></div>
<div title="Code: 0xe803" class="the-icons span3"><i class="demo-icon icon-menu">&#xe803;</i> <span class="i-name">icon-menu</span><span class="i-code">0xe803</span></div>
<div class="the-icons span3" title="Code: 0xe800"><i class="demo-icon icon-help-circled">&#xe800;</i> <span class="i-name">icon-help-circled</span><span class="i-code">0xe800</span></div>
<div class="the-icons span3" title="Code: 0xe801"><i class="demo-icon icon-angle-double-up">&#xe801;</i> <span class="i-name">icon-angle-double-up</span><span class="i-code">0xe801</span></div>
<div class="the-icons span3" title="Code: 0xe802"><i class="demo-icon icon-angle-double-down">&#xe802;</i> <span class="i-name">icon-angle-double-down</span><span class="i-code">0xe802</span></div>
<div class="the-icons span3" title="Code: 0xe803"><i class="demo-icon icon-menu">&#xe803;</i> <span class="i-name">icon-menu</span><span class="i-code">0xe803</span></div>
</div>
<div class="row">
<div title="Code: 0xe804" class="the-icons span3"><i class="demo-icon icon-battery-25">&#xe804;</i> <span class="i-name">icon-battery-25</span><span class="i-code">0xe804</span></div>
<div title="Code: 0xe805" class="the-icons span3"><i class="demo-icon icon-battery-50">&#xe805;</i> <span class="i-name">icon-battery-50</span><span class="i-code">0xe805</span></div>
<div title="Code: 0xe806" class="the-icons span3"><i class="demo-icon icon-cog">&#xe806;</i> <span class="i-name">icon-cog</span><span class="i-code">0xe806</span></div>
<div title="Code: 0xe807" class="the-icons span3"><i class="demo-icon icon-battery-75">&#xe807;</i> <span class="i-name">icon-battery-75</span><span class="i-code">0xe807</span></div>
<div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-battery-25">&#xe804;</i> <span class="i-name">icon-battery-25</span><span class="i-code">0xe804</span></div>
<div class="the-icons span3" title="Code: 0xe805"><i class="demo-icon icon-battery-50">&#xe805;</i> <span class="i-name">icon-battery-50</span><span class="i-code">0xe805</span></div>
<div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-cog">&#xe806;</i> <span class="i-name">icon-cog</span><span class="i-code">0xe806</span></div>
<div class="the-icons span3" title="Code: 0xe807"><i class="demo-icon icon-battery-75">&#xe807;</i> <span class="i-name">icon-battery-75</span><span class="i-code">0xe807</span></div>
</div>
<div class="row">
<div title="Code: 0xe808" class="the-icons span3"><i class="demo-icon icon-battery-100">&#xe808;</i> <span class="i-name">icon-battery-100</span><span class="i-code">0xe808</span></div>
<div title="Code: 0xe809" class="the-icons span3"><i class="demo-icon icon-cancel-circled">&#xe809;</i> <span class="i-name">icon-cancel-circled</span><span class="i-code">0xe809</span></div>
<div title="Code: 0xe80a" class="the-icons span3"><i class="demo-icon icon-volume">&#xe80a;</i> <span class="i-name">icon-volume</span><span class="i-code">0xe80a</span></div>
<div title="Code: 0xe80b" class="the-icons span3"><i class="demo-icon icon-plus">&#xe80b;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe80b</span></div>
<div class="the-icons span3" title="Code: 0xe808"><i class="demo-icon icon-battery-100">&#xe808;</i> <span class="i-name">icon-battery-100</span><span class="i-code">0xe808</span></div>
<div class="the-icons span3" title="Code: 0xe809"><i class="demo-icon icon-cancel-circled">&#xe809;</i> <span class="i-name">icon-cancel-circled</span><span class="i-code">0xe809</span></div>
<div class="the-icons span3" title="Code: 0xe80a"><i class="demo-icon icon-volume">&#xe80a;</i> <span class="i-name">icon-volume</span><span class="i-code">0xe80a</span></div>
<div class="the-icons span3" title="Code: 0xe80b"><i class="demo-icon icon-plus">&#xe80b;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe80b</span></div>
</div>
<div class="row">
<div title="Code: 0xe80c" class="the-icons span3"><i class="demo-icon icon-hourglass">&#xe80c;</i> <span class="i-name">icon-hourglass</span><span class="i-code">0xe80c</span></div>
<div title="Code: 0xe80d" class="the-icons span3"><i class="demo-icon icon-calc">&#xe80d;</i> <span class="i-name">icon-calc</span><span class="i-code">0xe80d</span></div>
<div title="Code: 0xe80e" class="the-icons span3"><i class="demo-icon icon-tint">&#xe80e;</i> <span class="i-name">icon-tint</span><span class="i-code">0xe80e</span></div>
<div title="Code: 0xe80f" class="the-icons span3"><i class="demo-icon icon-chart-line">&#xe80f;</i> <span class="i-name">icon-chart-line</span><span class="i-code">0xe80f</span></div>
<div class="the-icons span3" title="Code: 0xe80c"><i class="demo-icon icon-hourglass">&#xe80c;</i> <span class="i-name">icon-hourglass</span><span class="i-code">0xe80c</span></div>
<div class="the-icons span3" title="Code: 0xe80d"><i class="demo-icon icon-calc">&#xe80d;</i> <span class="i-name">icon-calc</span><span class="i-code">0xe80d</span></div>
<div class="the-icons span3" title="Code: 0xe80e"><i class="demo-icon icon-tint">&#xe80e;</i> <span class="i-name">icon-tint</span><span class="i-code">0xe80e</span></div>
<div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-chart-line">&#xe80f;</i> <span class="i-name">icon-chart-line</span><span class="i-code">0xe80f</span></div>
</div>
<div class="row">
<div title="Code: 0xe810" class="the-icons span3"><i class="demo-icon icon-sort-numeric">&#xe810;</i> <span class="i-name">icon-sort-numeric</span><span class="i-code">0xe810</span></div>
<div title="Code: 0xe811" class="the-icons span3"><i class="demo-icon icon-edit">&#xe811;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe811</span></div>
<div title="Code: 0xe812" class="the-icons span3"><i class="demo-icon icon-lock">&#xe812;</i> <span class="i-name">icon-lock</span><span class="i-code">0xe812</span></div>
<div title="Code: 0xe813" class="the-icons span3"><i class="demo-icon icon-lock-open">&#xe813;</i> <span class="i-name">icon-lock-open</span><span class="i-code">0xe813</span></div>
<div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-sort-numeric">&#xe810;</i> <span class="i-name">icon-sort-numeric</span><span class="i-code">0xe810</span></div>
<div class="the-icons span3" title="Code: 0xe811"><i class="demo-icon icon-edit">&#xe811;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe811</span></div>
<div class="the-icons span3" title="Code: 0xe812"><i class="demo-icon icon-lock">&#xe812;</i> <span class="i-name">icon-lock</span><span class="i-code">0xe812</span></div>
<div class="the-icons span3" title="Code: 0xe813"><i class="demo-icon icon-lock-open">&#xe813;</i> <span class="i-name">icon-lock-open</span><span class="i-code">0xe813</span></div>
</div>
<div class="row">
<div title="Code: 0xe814" class="the-icons span3"><i class="demo-icon icon-trash-empty">&#xe814;</i> <span class="i-name">icon-trash-empty</span><span class="i-code">0xe814</span></div>
<div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-trash-empty">&#xe814;</i> <span class="i-name">icon-trash-empty</span><span class="i-code">0xe814</span></div>
</div>
</div>
<div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
</body>
</html>
</html>
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2016 by original authors @ fontello.com</metadata>
<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+14 -2
View File
@@ -18,6 +18,10 @@
client.init(function loaded () {
if (c_profile !== null) {
return; // already loaded so don't load again
}
var translate = client.translate;
var defaultprofile = {
@@ -630,11 +634,11 @@
}
function toTimeString(minfrommidnight) {
return moment().startOf('day').add(minfrommidnight,'minutes').format('HH:mm');
return moment.utc().startOf('day').add(minfrommidnight,'minutes').format('HH:mm'); // using utc to avoid daylight saving offset
}
function toDisplayTime (minfrommidnight) {
var time = moment().startOf('day').add(minfrommidnight,'minutes');
var time = moment.utc().startOf('day').add(minfrommidnight,'minutes'); // using utc to avoid daylight saving offset
return client.settings.timeFormat === 24 ? time.format('HH:mm') : time.format('h:mm A');
}
@@ -678,7 +682,13 @@
delete adjustedRecord.convertedOnTheFly;
console.info('saving profile');
peStatus.hide().text(translate('Saving profile')).fadeIn('slow');
// Hide the form until the ajax PUT is done.
// 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();
$.ajax({
method: 'PUT'
, url: '/api/v1/profile/'
@@ -686,12 +696,14 @@
, headers: client.headers()
}).done(function postSuccess (data, status) {
console.info('profile saved', data);
$('#pe_form').show(); // allow edits again
peStatus.hide().text(status).fadeIn('slow');
record._id = data._id;
initRecord();
dirty = false;
}).fail(function(xhr, status, errorThrown) {
console.error('Profile not saved', status, errorThrown);
$('#pe_form').show(); // allow edits again
peStatus.hide().text(status).fadeIn('slow');
});
return false;
+52
View File
@@ -0,0 +1,52 @@
// Moves backward one day
function loopalyzerBackward() {
var moment = window.moment;
var from = moment($("#rp_from").val()).subtract(1,'day');
var to = moment($("#rp_to").val()).subtract(1,'day');
$("#rp_from").val(from.format('YYYY-MM-DD'));
$("#rp_to").val(to.format('YYYY-MM-DD'));
$("#rp_show").click();
}
// Moves backward same amount as shown (e.g. whole week)
function loopalyzerMoreBackward() {
var moment = window.moment;
var from = moment($("#rp_from").val())
var to = moment($("#rp_to").val())
var diff = to.diff(from, 'days') + 1;
from.subtract(diff, 'days');
to.subtract(diff, 'days');
$("#rp_from").val(from.format('YYYY-MM-DD'));
$("#rp_to").val(to.format('YYYY-MM-DD'));
$("#rp_show").click();
}
// Moves forward one day
function loopalyzerForward() {
var moment = window.moment;
var from = moment($("#rp_from").val()).add(1,'day');
var to = moment($("#rp_to").val()).add(1,'day');
if (to <= moment()) {
$("#rp_from").val(from.format('YYYY-MM-DD'));
$("#rp_to").val(to.format('YYYY-MM-DD'));
$("#rp_show").click();
}
}
// Moves forward same amount as shown (e.g. whole week)
function loopalyzerMoreForward() {
var moment = window.moment;
var from = moment($("#rp_from").val())
var to = moment($("#rp_to").val())
var diff = to.diff(from, 'days') + 1;
from.add(diff, 'days');
to.add(diff, 'days');
if (to > moment()) {
to = moment();
from = moment();
from.subtract(diff-1, 'days');
}
$("#rp_from").val(from.format('YYYY-MM-DD'));
$("#rp_to").val(to.format('YYYY-MM-DD'));
$("#rp_show").click();
}
+40
View File
@@ -0,0 +1,40 @@
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();
});
+21 -2
View File
@@ -170,8 +170,10 @@
if (client.settings.scaleY === 'linear') {
$('#rp_linear').prop('checked', true);
$('#wrp_linear').prop('checked', true);
} else {
$('#rp_log').prop('checked', true);
$('#wrp_log').prop('checked', true);
}
$('.menutab').click(switchreport_handler);
@@ -195,6 +197,8 @@
var options = {
width: 1000
, height: 300
, weekwidth: 1000
, weekheight: 300
, targetLow: 3.5
, targetHigh: 10
, raw: true
@@ -206,6 +210,7 @@
, cob : true
, basal : true
, scale: report_plugins.consts.scaleYFromSettings(client)
, weekscale: report_plugins.consts.scaleYFromSettings(client)
, units: client.settings.units
};
@@ -219,6 +224,8 @@
options.iob = $('#rp_optionsiob').is(':checked');
options.cob = $('#rp_optionscob').is(':checked');
options.openAps = $('#rp_optionsopenaps').is(':checked');
options.predicted = $('#rp_optionspredicted').is(':checked');
options.predictedTruncate = $('#rp_optionsPredictedTruncate').is(':checked');
options.basal = $('#rp_optionsbasal').is(':checked');
options.notes = $('#rp_optionsnotes').is(':checked');
options.food = $('#rp_optionsfood').is(':checked');
@@ -226,9 +233,18 @@
options.insulindistribution = $('#rp_optionsdistribution').is(':checked');
options.carbs = $('#rp_optionscarbs').is(':checked');
options.scale = ( $('#rp_linear').is(':checked') ? report_plugins.consts.SCALE_LINEAR : report_plugins.consts.SCALE_LOG );
options.weekscale = ( $('#wrp_linear').is(':checked') ? report_plugins.consts.SCALE_LINEAR : report_plugins.consts.SCALE_LOG );
options.order = ( $('#rp_oldestontop').is(':checked') ? report_plugins.consts.ORDER_OLDESTONTOP : report_plugins.consts.ORDER_NEWESTONTOP );
options.width = parseInt($('#rp_size :selected').attr('x'));
options.weekwidth = parseInt($('#wrp_size :selected').attr('x'));
options.height = parseInt($('#rp_size :selected').attr('y'));
options.weekheight = parseInt($('#wrp_size :selected').attr('y'));
options.loopalyzer = $("#loopalyzer").hasClass( "selected" ); // We only want to run through Loopalyzer if that tab is selected
if (options.loopalyzer) {
options.iob = true;
options.cob = true;
options.openAps = true;
}
var matchesneeded = 0;
@@ -510,6 +526,8 @@
if (plugin.name == 'daytoday' && ! $('#daytoday').hasClass('selected')) skipRender = true;
if (plugin.name == 'treatments' && ! $('#treatments').hasClass('selected')) skipRender = true;
if (plugin.name == 'weektoweek' && ! $('#weektoweek').hasClass('selected')) skipRender = true;
if (plugin.name == 'loopalyzer' && ! $('#loopalyzer').hasClass('selected')) skipRender = true;
if (skipRender) {
console.log('Skipping ',plugin.name);
@@ -545,7 +563,7 @@
function loadData(day, options, callback) {
// check for loaded data
if ((options.openAps || options.iob || options.cob) && datastorage[day] && !datastorage[day].devicestatus.length) {
if ((options.openAps || options.predicted || options.iob || options.cob) && datastorage[day] && !datastorage[day].devicestatus.length) {
// OpenAPS requested but data not loaded. Load anyway ...
} else if (datastorage[day] && day !== moment().format('YYYY-MM-DD')) {
callback(day);
@@ -635,6 +653,7 @@
var tquery = '?find[created_at][$gte]='+new Date(from).toISOString()+'&find[created_at][$lt]='+new Date(to).toISOString();
return $.ajax('/api/v1/treatments.json'+tquery, {
headers: client.headers()
, cache: false
, success: function (xhr) {
treatmentData = xhr.map(function (treatment) {
var timestamp = new Date(treatment.timestamp || treatment.created_at);
@@ -665,7 +684,7 @@
data.devicestatus = [];
return $.Deferred().resolve();
}
if(options.iob || options.cob || options.openAps) {
if (options.iob || options.cob || options.openAps || options.predicted) {
$('#info-' + day).html('<b>'+translate('Loading device status data of')+' '+day+' ...</b>');
var tquery = '?find[created_at][$gte]=' + new Date(from).toISOString() + '&find[created_at][$lt]=' + new Date(to).toISOString() + '&count=10000';
return $.ajax('/api/v1/devicestatus.json'+tquery, {
Regular → Executable
+483 -1
View File
@@ -8,7 +8,7 @@
"info": {
"title": "Nightscout API",
"description": "Own your DData with the Nightscout API",
"version": "0.10.3-master-20180805",
"version": "0.11.0-rc1-20190205",
"license": {
"name": "AGPL 3",
"url": "https://www.gnu.org/licenses/agpl.txt"
@@ -577,6 +577,81 @@
"description": "Treatments to be uploaded.",
"required": true
}
},
"delete": {
"tags": [
"Treatments"
],
"summary": "Delete treatments matching query.",
"description": "Remove treatments, same search syntax as GET.",
"operationId": "remove",
"parameters": [
{
"name": "find",
"in": "query",
"description": "The query used to find treatments to delete, support nested query syntax, for example `find[insulin][$gte]=3`. All find parameters are interpreted as strings.",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "count",
"in": "query",
"description": "Number of entries to return.",
"required": false,
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Empty list is success."
}
}
}
},
"/treatments/{spec}": {
"delete": {
"summary": "Delete treatments record with id provided in spec",
"description": "The Treatments endpoint returns information about the\nNightscout devicestatus records.\n",
"parameters": [
{
"name": "spec",
"in": "path",
"description": "treatment id, such as `55cf81bc436037528ec75fa5`\n",
"required": true,
"schema": {
"type": "string"
}
}
],
"tags": [
"Treatments"
],
"responses": {
"200": {
"description": "A status record of the delete.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteStatus"
}
}
}
},
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/profile": {
@@ -640,6 +715,166 @@
}
}
}
},
"/devicestatus/": {
"get": {
"summary": "All Devicestatuses matching query",
"description": "The Devicestatus endpoint returns information about the Nightscout devicestatus records.",
"parameters": [
{
"name": "find",
"in": "query",
"description": "The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings.",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "count",
"in": "query",
"description": "Number of devicestatus records to return.",
"required": false,
"schema": {
"type": "number"
}
}
],
"tags": [
"Devicestatus"
],
"responses": {
"200": {
"description": "An array of devicestatus entries",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Devicestatuses"
}
}
}
},
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"post": {
"tags": [
"Devicestatus"
],
"summary": "Add new devicestatus records.",
"description": "",
"operationId": "addDevicestatuses",
"responses": {
"200": {
"description": "Rejected list of device statuses. Empty list is success."
},
"405": {
"description": "Invalid input"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Devicestatuses"
}
}
},
"description": "Device statuses to be uploaded.",
"required": true
}
},
"delete": {
"summary": "Delete all Devicestatus records matching query",
"description": "The Devicestatus endpoint returns information about the\nNightscout devicestatus records.\n",
"parameters": [
{
"name": "find",
"in": "query",
"description": "The query used to find entries, support nested query syntax, for\nexample `find[created_at][$gte]=2015-08-27`. All find parameters\nare interpreted as strings.\n",
"required": false,
"schema": {
"type": "string"
}
}
],
"tags": [
"Devicestatus"
],
"responses": {
"200": {
"description": "A status record of the delete.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteStatus"
}
}
}
},
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/devicestatus/{spec}": {
"delete": {
"summary": "Delete devicestatus record with id provided in spec",
"description": "The Devicestatus endpoint returns information about the\nNightscout devicestatus records.\n",
"parameters": [
{
"name": "spec",
"in": "path",
"description": "entry id, such as `55cf81bc436037528ec75fa5`\n",
"required": true,
"schema": {
"type": "string"
}
}
],
"tags": [
"Devicestatus"
],
"responses": {
"200": {
"description": "A status record of the delete.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteStatus"
}
}
}
},
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
},
"components": {
@@ -710,6 +945,211 @@
"$ref": "#/components/schemas/Entry"
}
},
"Devicestatus": {
"required": [
"device",
"created_at"
],
"properties": {
"device": {
"type": "string",
"description": "Device type and hostname for example openaps://hostname"
},
"created_at": {
"type": "string",
"description": "dateString, prefer ISO `8601`"
},
"openaps": {
"type": "string",
"description": "OpenAPS devicestatus record - TODO: Fill Out Details"
},
"loop": {
"type": "string",
"description": "Loop devicestatus record - TODO: Fill Out Details"
},
"pump": {
"$ref": "#/components/schemas/pump"
},
"uploader": {
"$ref": "#/components/schemas/uploader"
},
"xdripjs": {
"$ref": "#/components/schemas/xdripjs"
}
}
},
"Devicestatuses": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Devicestatus"
}
},
"pump": {
"properties": {
"clock": {
"type": "string",
"description": "dateString, prefer ISO `8601`"
},
"battery": {
"$ref": "#/components/schemas/pumpbattery"
},
"reservoir": {
"type": "number",
"description": "Amount of insulin remaining in pump reservoir"
},
"status": {
"$ref": "#/components/schemas/pumpstatus"
}
}
},
"pumpbattery": {
"properties": {
"status": {
"type": "string",
"description": "Pump Battery Status String"
},
"voltage": {
"type": "number",
"description": "Pump Battery Voltage Level"
}
}
},
"pumpstatus": {
"properties": {
"status": {
"type": "string",
"description": "Pump Status String"
},
"bolusing": {
"type": "boolean",
"description": "Is Pump Bolusing"
},
"suspended": {
"type": "boolean",
"description": "Is Pump Suspended"
},
"timestamp": {
"type": "string",
"description": "dateString, prefer ISO `8601`"
}
}
},
"uploader": {
"properties": {
"batteryVoltage": {
"type": "number",
"description": "Uploader Device Battery Voltage"
},
"battery": {
"type": "number",
"description": "Uploader Device Battery Percentage Charge Remaining"
}
}
},
"xdripjs": {
"properties": {
"state": {
"type": "number",
"description": "CGM Sensor Session State Code"
},
"stateString": {
"type": "string",
"description": "CGM Sensor Session State String"
},
"stateStringShort": {
"type": "string",
"description": "CGM Sensor Session State Short String"
},
"txId": {
"type": "string",
"description": "CGM Transmitter ID"
},
"txStatus": {
"type": "number",
"description": "CGM Transmitter Status"
},
"txStatusString": {
"type": "string",
"description": "CGM Transmitter Status String"
},
"txStatusStringShort": {
"type": "string",
"description": "CGM Transmitter Status Short String"
},
"txActivation": {
"type": "number",
"description": "CGM Transmitter Activation Milliseconds After Epoch"
},
"mode": {
"type": "string",
"description": "Mode xdrip-js Application Operationg: expired, not expired, etc."
},
"timestamp": {
"type": "number",
"description": "Last Update Milliseconds After Epoch"
},
"rssi": {
"type": "number",
"description": "Receive Signal Strength of Transmitter"
},
"unfiltered": {
"type": "number",
"description": "Most Recent Raw Unfiltered Glucose"
},
"filtered": {
"type": "number",
"description": "Most Recent Raw Filtered Glucose"
},
"noise": {
"type": "number",
"description": "Calculated Noise Value - 1=Clean, 2=Light, 3=Medium, 4=Heavy"
},
"noiseString": {
"type": "number",
"description": "Noise Value String"
},
"slope": {
"type": "number",
"description": "Calibration Slope Value"
},
"intercept": {
"type": "number",
"description": "Calibration Intercept Value"
},
"calType": {
"type": "string",
"description": "Algorithm Used to Calculate Calibration Values"
},
"lastCalibrationDate": {
"type": "number",
"description": "Most Recent Calibration Milliseconds After Epoch"
},
"sessionStart": {
"type": "number",
"description": "Sensor Session Start Milliseconds After Epoch"
},
"batteryTimestamp": {
"type": "number",
"description": "Most Recent Batter Status Read Milliseconds After Epoch"
},
"voltagea": {
"type": "number",
"description": "Voltage of Battery A"
},
"voltageb": {
"type": "number",
"description": "Voltage of Battery B"
},
"temperature": {
"type": "number",
"description": "Transmitter Temperature"
},
"resistance": {
"type": "number",
"description": "Sensor Resistance"
}
}
},
"Treatment": {
"properties": {
"_id": {
@@ -946,6 +1386,48 @@
"type": "object"
}
}
},
"DeleteStatus": {
"properties": {
"n": {
"type": "integer",
"format": "int32",
"description": "Number of records deleted"
},
"optime": {
"$ref": "#/components/schemas/optime"
},
"electionId": {
"type": "string",
"description": "Election id of operation"
},
"ok": {
"type": "integer",
"format": "int32",
"description": "Status of whether delete was successful"
},
"operationTime": {
"type": "string",
"description": "Time delete operation was executed"
},
"$clusterTime": {
"type": "string",
"description": "Information about execution time in cluster environment"
}
}
},
"optime": {
"properties": {
"ts": {
"type": "string",
"description": "Time the operation started"
},
"t": {
"type": "integer",
"format": "int32",
"description": "Time the operation took to complete"
}
}
}
}
}
Regular → Executable
+349 -3
View File
@@ -4,7 +4,7 @@ servers:
info:
title: Nightscout API
description: Own your DData with the Nightscout API
version: 0.10.3-master-20180805
version: 0.11.0-rc1-20190205
license:
name: AGPL 3
url: 'https://www.gnu.org/licenses/agpl.txt'
@@ -444,6 +444,60 @@ paths:
$ref: '#/components/schemas/Treatments'
description: Treatments to be uploaded.
required: true
delete:
tags:
- Treatments
summary: Delete treatments matching query.
description: 'Remove treatments, same search syntax as GET.'
operationId: remove
parameters:
- name: find
in: query
description: >-
The query used to find treatments to delete,
support nested query syntax, for example `find[insulin][$gte]=3`.
All find parameters are interpreted as strings.
required: false
schema:
type: string
- name: count
in: query
description: Number of entries to return.
required: false
schema:
type: number
responses:
'200':
description: Empty list is success.
'/treatments/{spec}':
delete:
summary: Delete treatments record with id provided in spec
description: |
The Treatments endpoint returns information about the
Nightscout devicestatus records.
parameters:
- name: spec
in: path
description: |
treatment id, such as `55cf81bc436037528ec75fa5`
required: true
schema:
type: string
tags:
- Treatments
responses:
'200':
description: A status record of the delete.
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteStatus'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/profile:
get:
summary: Profile
@@ -484,6 +538,120 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/devicestatus/:
get:
summary: All Devicestatuses matching query
description: >-
The Devicestatus endpoint returns information about the Nightscout
devicestatus records.
parameters:
- name: find
in: query
description: >-
The query used to find entries, support nested query syntax, for
example `find[dateString][$gte]=2015-08-27`. All find parameters
are interpreted as strings.
required: false
schema:
type: string
- name: count
in: query
description: Number of devicestatus records to return.
required: false
schema:
type: number
tags:
- Devicestatus
responses:
'200':
description: An array of devicestatus entries
content:
application/json:
schema:
$ref: '#/components/schemas/Devicestatuses'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
tags:
- Devicestatus
summary: Add new devicestatus records.
description: ''
operationId: addDevicestatuses
responses:
'200':
description: Rejected list of device statuses. Empty list is success.
'405':
description: Invalid input
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Devicestatuses'
description: Device statuses to be uploaded.
required: true
delete:
summary: Delete all Devicestatus records matching query
description: |
The Devicestatus endpoint returns information about the
Nightscout devicestatus records.
parameters:
- name: find
in: query
description: |
The query used to find entries, support nested query syntax, for
example `find[created_at][$gte]=2015-08-27`. All find parameters
are interpreted as strings.
required: false
schema:
type: string
tags:
- Devicestatus
responses:
'200':
description: A status record of the delete.
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteStatus'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'/devicestatus/{spec}':
delete:
summary: Delete devicestatus record with id provided in spec
description: |
The Devicestatus endpoint returns information about the
Nightscout devicestatus records.
parameters:
- name: spec
in: path
description: |
entry id, such as `55cf81bc436037528ec75fa5`
required: true
schema:
type: string
tags:
- Devicestatus
responses:
'200':
description: A status record of the delete.
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteStatus'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
securitySchemes:
api_secret:
@@ -495,7 +663,9 @@ components:
type: apiKey
name: token
in: query
description: Add token as query item in the URL. You can manage access Token in `/admin`. This uses json webtokens.
description: >-
Add token as query item in the URL. You can manage access Token in
`/admin`. This uses json webtokens.
jwtoken:
type: http
scheme: bearer
@@ -543,6 +713,152 @@ components:
type: array
items:
$ref: '#/components/schemas/Entry'
Devicestatus:
required:
- device
- created_at
properties:
device:
type: string
description: 'Device type and hostname for example openaps://hostname'
created_at:
type: string
description: 'dateString, prefer ISO `8601`'
openaps:
type: string
description: 'OpenAPS devicestatus record - TODO: Fill Out Details'
loop:
type: string
description: 'Loop devicestatus record - TODO: Fill Out Details'
pump:
$ref: '#/components/schemas/pump'
uploader:
$ref: '#/components/schemas/uploader'
xdripjs:
$ref: '#/components/schemas/xdripjs'
Devicestatuses:
type: array
items:
$ref: '#/components/schemas/Devicestatus'
pump:
properties:
clock:
type: string
description: 'dateString, prefer ISO `8601`'
battery:
$ref: '#/components/schemas/pumpbattery'
reservoir:
type: number
description: Amount of insulin remaining in pump reservoir
status:
$ref: '#/components/schemas/pumpstatus'
pumpbattery:
properties:
status:
type: string
description: Pump Battery Status String
voltage:
type: number
description: Pump Battery Voltage Level
pumpstatus:
properties:
status:
type: string
description: Pump Status String
bolusing:
type: boolean
description: Is Pump Bolusing
suspended:
type: boolean
description: Is Pump Suspended
timestamp:
type: string
description: 'dateString, prefer ISO `8601`'
uploader:
properties:
batteryVoltage:
type: number
description: Uploader Device Battery Voltage
battery:
type: number
description: Uploader Device Battery Percentage Charge Remaining
xdripjs:
properties:
state:
type: number
description: CGM Sensor Session State Code
stateString:
type: string
description: CGM Sensor Session State String
stateStringShort:
type: string
description: CGM Sensor Session State Short String
txId:
type: string
description: CGM Transmitter ID
txStatus:
type: number
description: CGM Transmitter Status
txStatusString:
type: string
description: CGM Transmitter Status String
txStatusStringShort:
type: string
description: CGM Transmitter Status Short String
txActivation:
type: number
description: CGM Transmitter Activation Milliseconds After Epoch
mode:
type: string
description: 'Mode xdrip-js Application Operationg: expired, not expired, etc.'
timestamp:
type: number
description: Last Update Milliseconds After Epoch
rssi:
type: number
description: Receive Signal Strength of Transmitter
unfiltered:
type: number
description: Most Recent Raw Unfiltered Glucose
filtered:
type: number
description: Most Recent Raw Filtered Glucose
noise:
type: number
description: 'Calculated Noise Value - 1=Clean, 2=Light, 3=Medium, 4=Heavy'
noiseString:
type: number
description: Noise Value String
slope:
type: number
description: Calibration Slope Value
intercept:
type: number
description: Calibration Intercept Value
calType:
type: string
description: Algorithm Used to Calculate Calibration Values
lastCalibrationDate:
type: number
description: Most Recent Calibration Milliseconds After Epoch
sessionStart:
type: number
description: Sensor Session Start Milliseconds After Epoch
batteryTimestamp:
type: number
description: Most Recent Batter Status Read Milliseconds After Epoch
voltagea:
type: number
description: Voltage of Battery A
voltageb:
type: number
description: Voltage of Battery B
temperature:
type: number
description: Transmitter Temperature
resistance:
type: number
description: Sensor Resistance
Treatment:
properties:
_id:
@@ -723,4 +1039,34 @@ components:
message:
type: string
fields:
type: object
type: object
DeleteStatus:
properties:
'n':
type: integer
format: int32
description: Number of records deleted
optime:
$ref: '#/components/schemas/optime'
electionId:
type: string
description: Election id of operation
ok:
type: integer
format: int32
description: Status of whether delete was successful
operationTime:
type: string
description: Time delete operation was executed
$clusterTime:
type: string
description: Information about execution time in cluster environment
optime:
properties:
ts:
type: string
description: Time the operation started
t:
type: integer
format: int32
description: Time the operation took to complete
+36 -4
View File
@@ -29,6 +29,9 @@ var someData = {
'created_at': '2025-09-28T16:41:07.144Z'
}
],
'/api/v1/devicestatus/?find[created_at][$lte]=': {
n: 1
},
'/api/v1/treatments.json?&find[created_at][$gte]=': [
{
'_id': '5609a9203c8104a8195b1c1e',
@@ -38,6 +41,9 @@ var someData = {
'created_at': '2025-09-28T20:54:00.000Z'
}
],
'/api/v1/treatments/?find[created_at][$lte]=': {
n: 1
},
'/api/v1/entries.json?&find[date][$gte]=': [
{
'_id': '560983f326c5a592d9b9ae0c',
@@ -51,8 +57,11 @@ var someData = {
'rssi': 178,
'noise': 1
}
]
};
],
'/api/v1/entries/?find[date][$lte]=': {
n: 1
},
};
describe('admintools', function ( ) {
@@ -98,9 +107,14 @@ describe('admintools', function ( ) {
if (opts && opts.success && opts.success.call) {
if (url.indexOf('/api/v1/treatments.json?&find[created_at][$gte]=')===0) {
url = '/api/v1/treatments.json?&find[created_at][$gte]=';
}
if (url.indexOf('/api/v1/entries.json?&find[date][$gte]=')===0) {
} else if (url.indexOf('/api/v1/entries.json?&find[date][$gte]=')===0) {
url = '/api/v1/entries.json?&find[date][$gte]=';
} else if (url.indexOf('/api/v1/devicestatus/?find[created_at][$lte]=')===0) {
url = '/api/v1/devicestatus/?find[created_at][$lte]=';
} else if (url.indexOf('/api/v1/treatments/?find[created_at][$lte]=')===0) {
url = '/api/v1/treatments/?find[created_at][$lte]=';
} else if (url.indexOf('/api/v1/entries/?find[date][$lte]=')===0) {
url = '/api/v1/entries/?find[date][$lte]=';
}
return {
done: function mockDone (fn) {
@@ -219,6 +233,12 @@ describe('admintools', function ( ) {
$('#admin_cleanstatusdb_0_html + button').click();
$('#admin_cleanstatusdb_0_status').text().should.equal('All records removed ...'); // devicestatus code result
$('#admin_cleanstatusdb_1_html + button').text().should.equal('Delete old documents'); // devicestatus button
$('#admin_cleanstatusdb_1_status').text().should.equal(''); // devicestatus init result
$('#admin_cleanstatusdb_1_html + button').click();
$('#admin_cleanstatusdb_1_status').text().should.equal('1 records deleted'); // devicestatus code result
$('#admin_futureitems_0_html + button').text().should.equal('Remove treatments in the future'); // futureitems button 0
$('#admin_futureitems_0_status').text().should.equal('Database contains 1 future records'); // futureitems init result 0
@@ -231,6 +251,18 @@ describe('admintools', function ( ) {
$('#admin_futureitems_1_html + button').click();
$('#admin_futureitems_1_status').text().should.equal('Record 560983f326c5a592d9b9ae0c removed ...'); // futureitems code result 1
$('#admin_cleantreatmentsdb_0_html + button').text().should.equal('Delete old documents'); // treatments button
$('#admin_cleantreatmentsdb_0_status').text().should.equal(''); // treatments init result
$('#admin_cleantreatmentsdb_0_html + button').click();
$('#admin_cleantreatmentsdb_0_status').text().should.equal('1 records deleted'); // treatments code result
$('#admin_cleanentriesdb_0_html + button').text().should.equal('Delete old documents'); // entries button
$('#admin_cleanentriesdb_0_status').text().should.equal(''); // entries init result
$('#admin_cleanentriesdb_0_html + button').click();
$('#admin_cleanentriesdb_0_status').text().should.equal('1 records deleted'); // entries code result
done();
});
+97
View File
@@ -0,0 +1,97 @@
'use strict';
var _ = require('lodash');
var request = require('supertest');
var should = require('should');
var language = require('../lib/language')();
describe('Devicestatus API', function ( ) {
this.timeout(10000);
var self = this;
var api = require('../lib/api/');
beforeEach(function (done) {
process.env.API_SECRET = 'this is my long pass phrase';
self.env = require('../env')();
self.env.settings.authDefaultRoles = 'readable';
self.env.settings.enable = ['careportal', 'api'];
this.wares = require('../lib/middleware/')(self.env);
self.app = require('express')();
self.app.enable('api');
require('../lib/server/bootevent')(self.env, language).boot(function booted(ctx) {
self.ctx = ctx;
self.ctx.ddata = require('../lib/data/ddata')();
self.app.use('/api', api(self.env, ctx));
done();
});
});
after(function () {
// delete process.env.API_SECRET;
});
it('post a devicestatus, query, delete, verify gone', function (done) {
// insert a devicestatus - needs to be unique from example data
console.log('Inserting devicestatus entry');
request(self.app)
.post('/api/devicestatus/')
.set('api-secret', self.env.api_secret || '')
.send({
device: 'xdripjs://rigName'
, xdripjs: {
state: 6
, stateString: 'OK'
, txStatus: 0
, txStatusString: 'OK'
}
, created_at: '2018-12-16T01:00:52Z'
})
.expect(200)
.end(function (err) {
if (err) {
done(err);
} else {
// make sure devicestatus was inserted successfully
console.log('Ensuring devicestatus entry was inserted successfully');
request(self.app)
.get('/api/devicestatus/')
.query('find[created_at][$gte]=2018-12-16')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.expect(function (response) {
response.body[0].xdripjs.state.should.equal(6);
})
.end(function (err) {
if (err) {
done(err);
} else {
// delete the treatment
console.log('Deleting test treatment entry');
request(self.app)
.delete('/api/devicestatus/')
.query('find[created_at][$gte]=2018-12-16')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.end(function (err) {
if (err) {
done(err);
} else {
// make sure it was deleted
console.log('Testing if devicestatus was deleted');
request(self.app)
.get('/api/devicestatus/')
.query('find[created_at][$lte]=2018-12-16')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.expect(function (response) {
response.body.length.should.equal(0);
})
.end(done);
}
});
}
});
}
});
});
});
+85 -29
View File
@@ -8,19 +8,19 @@ require('should');
describe('Entries REST api', function ( ) {
var entries = require('../lib/api/entries/');
var self = this;
this.timeout(10000);
before(function (done) {
var env = require('../env')( );
env.settings.authDefaultRoles = 'readable';
this.wares = require('../lib/middleware/')(env);
this.archive = null;
this.app = require('express')( );
this.app.enable('api');
var self = this;
bootevent(env, language).boot(function booted (ctx) {
self.env = require('../env')( );
self.env.settings.authDefaultRoles = 'readable';
self.wares = require('../lib/middleware/')(self.env);
self.archive = null;
self.app = require('express')( );
self.app.enable('api');
bootevent(self.env, language).boot(function booted (ctx) {
self.app.use('/', entries(self.app, self.wares, ctx));
self.archive = require('../lib/server/entries')(env, ctx);
self.archive = require('../lib/server/entries')(self.env, ctx);
var creating = load('json');
creating.push({type: 'sgv', sgv: 100, date: Date.now()});
@@ -31,15 +31,15 @@ describe('Entries REST api', function ( ) {
beforeEach(function (done) {
var creating = load('json');
creating.push({type: 'sgv', sgv: 100, date: Date.now()});
this.archive.create(creating, done);
self.archive.create(creating, done);
});
afterEach(function (done) {
this.archive( ).remove({ }, done);
self.archive( ).remove({ }, done);
});
after(function (done) {
this.archive( ).remove({ }, done);
self.archive( ).remove({ }, done);
});
// keep this test pinned at or near the top in order to validate all
@@ -48,7 +48,7 @@ describe('Entries REST api', function ( ) {
// function callback logic in entries.js.
it('gets requested number of entries', function (done) {
var count = 30;
request(this.app)
request(self.app)
.get('/entries.json?find[dateString][$gte]=2014-07-19&count=' + count)
.expect(200)
.end(function (err, res) {
@@ -59,7 +59,7 @@ describe('Entries REST api', function ( ) {
it('gets default number of entries', function (done) {
var defaultCount = 10;
request(this.app)
request(self.app)
.get('/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20')
.expect(200)
.end(function (err, res) {
@@ -70,7 +70,7 @@ describe('Entries REST api', function ( ) {
it('gets entries in right order', function (done) {
var defaultCount = 10;
request(this.app)
request(self.app)
.get('/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20')
.expect(200)
.end(function (err, res) {
@@ -88,7 +88,7 @@ describe('Entries REST api', function ( ) {
it('/echo/ api shows query', function (done) {
request(this.app)
request(self.app)
.get('/echo/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20')
.expect(200)
.end(function (err, res) {
@@ -102,7 +102,7 @@ describe('Entries REST api', function ( ) {
});
it('/slice/ can slice time', function (done) {
var app = this.app;
var app = self.app;
request(app)
.get('/slice/entries/dateString/sgv/2014-07.json?count=20')
.expect(200)
@@ -114,7 +114,7 @@ describe('Entries REST api', function ( ) {
it('/times/echo can describe query', function (done) {
var app = this.app;
var app = self.app;
request(app)
.get('/times/echo/2014-07/.*T{00..05}:.json?count=20&find[sgv][$gte]=160')
.expect(200)
@@ -127,7 +127,7 @@ describe('Entries REST api', function ( ) {
});
it('/slice/ can slice with multiple prefix', function (done) {
var app = this.app;
var app = self.app;
request(app)
.get('/slice/entries/dateString/sgv/2014-07-{17..20}.json?count=20')
.expect(200)
@@ -138,7 +138,7 @@ describe('Entries REST api', function ( ) {
});
it('/slice/ can slice time with prefix and no results', function (done) {
var app = this.app;
var app = self.app;
request(app)
.get('/slice/entries/dateString/sgv/1999-07.json?count=20&find[sgv][$lte]=401')
.expect(200)
@@ -149,7 +149,7 @@ describe('Entries REST api', function ( ) {
});
it('/times/ can get modal times', function (done) {
var app = this.app;
var app = self.app;
request(app)
.get('/times/2014-07-/{0..30}T.json?')
.expect(200)
@@ -160,7 +160,7 @@ describe('Entries REST api', function ( ) {
});
it('/times/ can get modal minutes and times', function (done) {
var app = this.app;
var app = self.app;
request(app)
.get('/times/20{14..15}-07/T{09..10}.json?')
.expect(200)
@@ -170,7 +170,7 @@ describe('Entries REST api', function ( ) {
});
});
it('/times/ can get multiple prefixen and modal minutes and times', function (done) {
var app = this.app;
var app = self.app;
request(app)
.get('/times/20{14..15}/T.*:{00..60}.json?')
.expect(200)
@@ -181,7 +181,7 @@ describe('Entries REST api', function ( ) {
});
it('/entries/current.json', function (done) {
request(this.app)
request(self.app)
.get('/entries/current.json')
.expect(200)
.end(function (err, res) {
@@ -192,8 +192,8 @@ describe('Entries REST api', function ( ) {
});
it('/entries/:id', function (done) {
var app = this.app;
this.archive.list({count: 1}, function(err, records) {
var app = self.app;
self.archive.list({count: 1}, function(err, records) {
var currentId = records.pop()._id.toString();
request(app)
.get('/entries/'+currentId+'.json')
@@ -207,7 +207,7 @@ describe('Entries REST api', function ( ) {
});
it('/entries/:model', function (done) {
var app = this.app;
var app = self.app;
request(app)
.get('/entries/sgv/.json?count=10&find[dateString][$gte]=2014')
.expect(200)
@@ -218,7 +218,7 @@ describe('Entries REST api', function ( ) {
});
it('disallow POST by readable /entries/preview', function (done) {
request(this.app)
request(self.app)
.post('/entries/preview.json')
.send(load('json'))
.expect(401)
@@ -229,7 +229,7 @@ describe('Entries REST api', function ( ) {
});
it('disallow deletes unauthorized', function (done) {
var app = this.app;
var app = self.app;
request(app)
.delete('/entries/sgv?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20')
@@ -249,4 +249,60 @@ describe('Entries REST api', function ( ) {
});
});
it('post an entry, query, delete, verify gone', function (done) {
// insert a glucose entry - needs to be unique from example data
console.log('Inserting glucose entry')
request(self.app)
.post('/entries/')
.set('api-secret', self.env.api_secret || '')
.send({
"type": "sgv", "sgv": "199", "dateString": "2014-07-20T00:44:15.000-07:00"
, "date": 1405791855000, "device": "dexcom", "direction": "NOT COMPUTABLE"
})
.expect(200)
.end(function (err) {
if (err) {
done(err);
} else {
// make sure treatment was inserted successfully
console.log('Ensuring glucose entry was inserted successfully');
request(self.app)
.get('/entries.json?find[dateString][$gte]=2014-07-20&count=100')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.expect(function (response) {
response.body[0].sgv.should.equal('199');
})
.end(function (err) {
if (err) {
done(err);
} else {
// delete the glucose entry
console.log('Deleting test glucose entry');
request(self.app)
.delete('/entries.json?find[dateString][$gte]=2014-07-20&count=100')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.end(function (err) {
if (err) {
done(err);
} else {
// make sure it was deleted
console.log('Testing if glucose entry was deleted');
request(self.app)
.get('/entries.json?find[dateString][$gte]=2014-07-20&count=100')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.expect(function (response) {
response.body.length.should.equal(0);
})
.end(done);
}
});
}
});
}
});
});
});
+55
View File
@@ -125,4 +125,59 @@ 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');
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'})
.expect(200)
.end(function (err) {
if (err) {
done(err);
} else {
// make sure treatment was inserted successfully
console.log('Ensuring treatment entry was inserted successfully');
request(self.app)
.get('/api/treatments/')
.query('find[carbs]=99')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.expect(function (response) {
response.body[0].carbs.should.equal(99);
})
.end(function (err) {
if (err) {
done(err);
} else {
// delete the treatment
console.log('Deleting test treatment entry');
request(self.app)
.delete('/api/treatments/')
.query('find[carbs]=99')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.end(function (err) {
if (err) {
done(err);
} else {
// make sure it was deleted
console.log('Testing if entry was deleted');
request(self.app)
.get('/api/treatments/')
.query('find[carbs]=99')
.set('api-secret', self.env.api_secret || '')
.expect(200)
.expect(function (response) {
response.body.length.should.equal(0);
})
.end(done);
}
});
}
});
}
});
});
});
+39 -23
View File
@@ -3,53 +3,69 @@
require('should');
describe('env', function ( ) {
it('show the right plugins', function () {
it( 'show the right plugins', function () {
process.env.SHOW_PLUGINS = 'iob';
process.env.ENABLE = 'iob cob';
var env = require('../env')();
var env = require( '../env' )();
var showPlugins = env.settings.showPlugins;
showPlugins.should.containEql('iob');
showPlugins.should.containEql('delta');
showPlugins.should.containEql('direction');
showPlugins.should.containEql('upbat');
showPlugins.should.containEql( 'iob' );
showPlugins.should.containEql( 'delta' );
showPlugins.should.containEql( 'direction' );
showPlugins.should.containEql( 'upbat' );
delete process.env.SHOW_PLUGINS;
delete process.env.ENABLE;
});
} );
it('get extended settings', function () {
it( 'get extended settings', function () {
process.env.ENABLE = 'scaryplugin';
process.env.SCARYPLUGIN_DO_THING = 'yes';
var env = require('../env')();
env.settings.isEnabled('scaryplugin').should.equal(true);
var env = require( '../env' )();
env.settings.isEnabled( 'scaryplugin' ).should.equal( true );
//Note the camelCase
env.extendedSettings.scaryplugin.doThing.should.equal('yes');
env.extendedSettings.scaryplugin.doThing.should.equal( 'yes' );
delete process.env.ENABLE;
delete process.env.SCARYPLUGIN_DO_THING;
});
} );
it('add pushover to enable if one of the env vars is set', function () {
it( 'add pushover to enable if one of the env vars is set', function () {
process.env.PUSHOVER_API_TOKEN = 'abc12345';
var env = require('../env')();
env.settings.enable.should.containEql('pushover');
env.extendedSettings.pushover.apiToken.should.equal('abc12345');
var env = require( '../env' )();
env.settings.enable.should.containEql( 'pushover' );
env.extendedSettings.pushover.apiToken.should.equal( 'abc12345' );
delete process.env.PUSHOVER_API_TOKEN;
});
} );
it('add pushover to enable if one of the weird azure env vars is set', function () {
it( 'add pushover to enable if one of the weird azure env vars is set', function () {
process.env.CUSTOMCONNSTR_PUSHOVER_API_TOKEN = 'abc12345';
var env = require('../env')();
env.settings.enable.should.containEql('pushover');
env.extendedSettings.pushover.apiToken.should.equal('abc12345');
var env = require( '../env' )();
env.settings.enable.should.containEql( 'pushover' );
env.extendedSettings.pushover.apiToken.should.equal( 'abc12345' );
delete process.env.PUSHOVER_API_TOKEN;
});
} );
});
it( 'readENVTruthy ', function () {
process.env.INSECURE_USE_HTTP = 'true';
var env = require( '../env' )();
env.insecureUseHttp.should.be.true();
process.env.INSECURE_USE_HTTP = 'false';
env = require( '../env' )();
env.insecureUseHttp.should.be.false();
process.env.INSECURE_USE_HTTP = 'not set ok, so use default value false';
env = require( '../env' )();
env.insecureUseHttp.should.be.false();
delete process.env.INSECURE_USE_HTTP; // unset INSECURE_USE_HTTP
process.env.SECURE_HSTS_HEADER = 'true';
env = require( '../env' )();
env.insecureUseHttp.should.be.false(); // not defined should be false
env.secureHstsHeader.should.be.true();
});
})
-182
View File
@@ -1,182 +0,0 @@
'use strict';
var should = require('should');
var FIVE_MINS = 5 * 60 * 1000;
describe('mqtt', function ( ) {
var self = this;
before(function () {
process.env.MQTT_MONITOR = 'mqtt://user:password@localhost:12345';
process.env.STORAGE_URI='mongodb://localhost:27017/test_db';
process.env.ENTRIES_COLLECTION='test_sgvs';
self.env = require('../env')();
self.es = require('event-stream');
self.results = self.es.through(function (ch) { this.push(ch); });
function outputs (fn) {
return self.es.writeArray(function (err, results) {
fn(err, results);
self.results.write(err || results);
});
}
function written (data, fn) {
self.results.write(data);
setTimeout(fn, 5);
}
self.mqtt = require('../lib/server/mqtt')(self.env, {entries: { persist: outputs, create: written }, devicestatus: { create: written } });
});
after(function () {
delete process.env.MQTT_MONITOR;
});
var now = Date.now()
, prev1 = now - FIVE_MINS
, prev2 = prev1 - FIVE_MINS
;
it('setup env correctly', function (done) {
self.env.mqtt_client_id.should.equal('nGVkio2g7p9+WOoiHB9YgmM');
done();
});
it('handle a download with only sgvs', function (done) {
var packet = {
sgv: [
{sgv_mgdl: 110, trend: 4, date: prev2}
, {sgv_mgdl: 105, trend: 4, date: prev1}
, {sgv_mgdl: 100, trend: 4, date: now}
]
};
var merged = self.mqtt.sgvSensorMerge(packet);
merged.length.should.equal(packet.sgv.length);
done();
});
it('merge sgvs and sensor records that match up', function (done) {
var packet = {
sgv: [
{sgv_mgdl: 110, trend: 4, date: prev2}
, {sgv_mgdl: 105, trend: 4, date: prev1}
, {sgv_mgdl: 100, trend: 4, date: now}
]
, sensor: [
{filtered: 99999, unfiltered: 99999, rssi: 200, date: prev2}
, {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev1}
, {filtered: 99999, unfiltered: 99999, rssi: 200, date: now}
]
};
var merged = self.mqtt.sgvSensorMerge(packet);
merged.length.should.equal(packet.sgv.length);
merged.filter(function (sgv) {
return sgv.filtered && sgv.unfiltered && sgv.rssi;
}).length.should.equal(packet.sgv.length);
done();
});
it('downloadProtobuf should dispatch', function (done) {
var payload = new Buffer('0a1108b70110d6d1fa6318f08df963200428011a1d323031352d30382d32335432323a35333a35352e3634392d30373a303020d7d1fa6328004a1508e0920b10c0850b18b20120d5d1fa6328ef8df963620a534d34313837393135306a053638393250', 'hex');
// var payload = self.mqtt.downloads.format(packet);
console.log('yaploda', '/downloads/protobuf', payload);
var l = [ ];
self.results.on('data', function (chunk) {
l.push(chunk);
console.log('test data', l.length, chunk.length, chunk);
switch (l.length) {
case 0: // devicestatus
break;
case 2: // sgv
break;
case 3: // sgv
chunk.length.should.equal(1);
var first = chunk[0];
should.exist(first.sgv);
should.exist(first.noise);
should.exist(first.date);
should.exist(first.dateString);
first.type.should.equal('sgv');
break;
case 4: // cal
break;
case 1: // meter
break;
default:
break;
}
if (l.length >= 5) {
self.results.end( );
}
});
self.results.on('end', function ( ) {
done( );
});
self.mqtt.client.emit('message', '/downloads/protobuf', payload);
});
it('merge sgvs and sensor records that match up, and get the sgvs that don\'t match', function (done) {
var packet = {
sgv: [
{sgv_mgdl: 110, trend: 4, date: prev2}
, {sgv_mgdl: 105, trend: 4, date: prev1}
, {sgv_mgdl: 100, trend: 4, date: now}
]
, sensor: [
{filtered: 99999, unfiltered: 99999, rssi: 200, date: now}
]
};
var merged = self.mqtt.sgvSensorMerge(packet);
merged.length.should.equal(packet.sgv.length);
var withBoth = merged.filter(function (sgv) {
return sgv.sgv && sgv.filtered && sgv.unfiltered && sgv.rssi;
});
withBoth.length.should.equal(1);
done();
});
it('merge sgvs and sensor records that match up, and get the sensors that don\'t match', function (done) {
var packet = {
sgv: [
{sgv_mgdl: 100, trend: 4, date: now}
]
, sensor: [
{filtered: 99999, unfiltered: 99999, rssi: 200, date: prev2}
, {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev1}
, {filtered: 99999, unfiltered: 99999, rssi: 200, date: now}
]
};
var merged = self.mqtt.sgvSensorMerge(packet);
merged.length.should.equal(packet.sensor.length);
var withBoth = merged.filter(function (sgv) {
return sgv.sgv && sgv.filtered && sgv.unfiltered && sgv.rssi;
});
withBoth.length.should.equal(1);
done();
});
});
+1 -1
View File
@@ -4,7 +4,7 @@ require('should');
var benv = require('benv');
describe('pluginbase', function ( ) {
this.timeout(40000); // TODO: see why this test takes longer on Travis to complete
this.timeout(50000); // TODO: see why this test takes longer on Travis to complete
var headless = require('./fixtures/headless')(benv, this);
+54
View File
@@ -276,4 +276,58 @@ describe('reports', function ( ) {
});
});
it ('should produce week to week report', function (done) {
var client = window.Nightscout.client;
var hashauth = require('../lib/hashauth');
hashauth.init(client,$);
hashauth.verifyAuthentication = function mockVerifyAuthentication(next) {
hashauth.authenticated = true;
next(true);
};
window.confirm = function mockConfirm () {
return true;
};
window.alert = function mockAlert () {
return true;
};
window.setTimeout = function mockSetTimeout (call) {
call();
};
client.init(function afterInit ( ) {
client.dataUpdate(nowData);
console.log('Sending profile to client');
// Load profile, we need to operate in UTC
client.sbx.data.profile.loadData(exampleProfile);
$('#weektoweek').addClass('selected');
$('a.presetdates :first').click();
$('#rp_from').val('2015-08-08');
$('#rp_to').val('2015-09-07');
$('#wrp_log').prop('checked', true);
$('#rp_show').click();
$('#wrp_linear').prop('checked', true);
$('#rp_show').click();
$('.ui-button:contains("Save")').click();
var result = $('body').html();
result.indexOf('<circle cx="978" cy="267.34375" fill="rgb(73, 22, 153)"').should.be.greaterThan(-1); // weektoweek Sunday sample point
result.indexOf('<circle cx="35" cy="267.34375" fill="rgb(34, 201, 228)"').should.be.greaterThan(-1); // weektoweek Monday sample point
result.indexOf('<circle cx="978" cy="267.34375" fill="rgb(0, 153, 123)"').should.be.greaterThan(-1); // weektoweek Tuesday sample point
result.indexOf('<circle cx="978" cy="267.34375" fill="rgb(135, 135, 228)"').should.be.greaterThan(-1); // weektoweek Wednesday sample point
result.indexOf('<circle cx="978" cy="267.34375" fill="rgb(135, 49, 204)"').should.be.greaterThan(-1); // weektoweek Thursday sample point
result.indexOf('<circle cx="978" cy="267.34375" fill="rgb(36, 36, 228)"').should.be.greaterThan(-1); // weektoweek Friday sample point
result.indexOf('<circle cx="978" cy="267.34375" fill="rgb(0, 234, 188)"').should.be.greaterThan(-1); // weektoweek Saturday sample point
done();
});
});
});
+3
View File
@@ -29,6 +29,9 @@ describe('settings', function ( ) {
settings.alarmTimeagoUrgentMins.should.equal(30);
settings.language.should.equal('en');
settings.showPlugins.should.equal('');
settings.insecureUseHttp.should.equal(false);
settings.secureHstsHeader.should.equal(true);
settings.secureCsp.should.equal(false);
});
it('support setting from env vars', function () {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+7 -1
View File
@@ -124,7 +124,13 @@
// Generate and insert the clock.
var timeDivisor = (window.serverSettings.settings.timeFormat) ? window.serverSettings.settings.timeFormat : 12;
var today = new Date(),
h = today.getHours() % timeDivisor || timeDivisor,
h = today.getHours() % timeDivisor;
if (timeDivisor == 12) {
h = (h == 0) ? 12 : h; // In the case of 00:xx, change to 12:xx for 12h time
}
if (timeDivisor == 24) {
h = (h < 10) ? ("0" + h) : h; // Pad the hours with a 0 in 24h time
}
m = today.getMinutes();
if (m < 10) m = "0" + m;
$('#clock').text(h + ":" + m);
+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html manifest="nightscout.appcache">
<html manifest="appcache/nightscout-<%= locals.cachebuster %>.appcache">
<head>
<meta name="viewport" content="width=device-width, maximum-scale=1, initial-scale=1, user-scalable=0" />
<meta name="apple-mobile-web-app-capable" content="yes">
+4 -2
View File
@@ -121,6 +121,8 @@
<script src="/js/bundle.js?v=<%= locals.cachebuster %>"></script>
<script src="/socket.io/socket.io.js?v=<%= locals.cachebuster %>"></script>
<script src="/report/js/report.js?v=<%= locals.cachebuster %>"></script>
<script src="/report/js/flotcandle.js?v=<%= locals.cachebuster %>"></script>
</body>
<script src="/report/js/flotcandle.js?v=<%= locals.cachebuster %>"></script>
<script src="/report/js/predictions.js?v=<%= locals.cachebuster %>"></script>
<script src="/report/js/loopalyzer.js?v=<%= locals.cachebuster %>"></script>
</body>
</html>
+100 -92
View File
@@ -1,105 +1,113 @@
const path = require('path');
const webpack = require('webpack');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const pluginArray = [];
const sourceMapType = 'source-map';
const TerserPlugin = require('terser-webpack-plugin');
/*if (process.env.NODE_ENV !== 'development') {
console.log('Development environment detected, enabling Bundle Analyzer');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var pluginArray = [];
pluginArray.push(new BundleAnalyzerPlugin({
// Can be `server`, `static` or `disabled`.
// In `server` mode analyzer will start HTTP server to show bundle report.
// In `static` mode single HTML file with bundle report will be generated.
// In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting
// `generateStatsFile` to `true`.
analyzerMode: 'static',
// Host that will be used in `server` mode to start HTTP server.
analyzerHost: '127.0.0.1',
// Port that will be used in `server` mode to start HTTP server.
analyzerPort: 8888,
// Path to bundle report file that will be generated in `static` mode.
// Relative to bundles output directory.
reportFilename: 'bundle_report.html',
// Module sizes to show in report by default.
// Should be one of `stat`, `parsed` or `gzip`.
// See "Definitions" section for more information.
defaultSizes: 'parsed',
// Automatically open report in default browser
openAnalyzer: true,
// If `true`, Webpack Stats JSON file will be generated in bundles output directory
generateStatsFile: false,
// Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`.
// Relative to bundles output directory.
statsFilename: 'stats.json',
// Options for `stats.toJson()` method.
// For example you can exclude sources of your modules from stats file with `source: false` option.
// See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
statsOptions: null,
// Log level. Can be 'info', 'warn', 'error' or 'silent'.
logLevel: 'info'
}));
}*/
var sourceMapType = 'source-map';
if (process.env.NODE_ENV !== 'development') {
/*
console.log('Development environment detected, enabling Bundle Analyzer');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
pluginArray.push(new BundleAnalyzerPlugin({
// Can be `server`, `static` or `disabled`.
// In `server` mode analyzer will start HTTP server to show bundle report.
// In `static` mode single HTML file with bundle report will be generated.
// In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`.
analyzerMode: 'static',
// Host that will be used in `server` mode to start HTTP server.
analyzerHost: '127.0.0.1',
// Port that will be used in `server` mode to start HTTP server.
analyzerPort: 8888,
// Path to bundle report file that will be generated in `static` mode.
// Relative to bundles output directory.
reportFilename: 'bundle_report.html',
// Module sizes to show in report by default.
// Should be one of `stat`, `parsed` or `gzip`.
// See "Definitions" section for more information.
defaultSizes: 'parsed',
// Automatically open report in default browser
openAnalyzer: true,
// If `true`, Webpack Stats JSON file will be generated in bundles output directory
generateStatsFile: false,
// Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`.
// Relative to bundles output directory.
statsFilename: 'stats.json',
// Options for `stats.toJson()` method.
// For example you can exclude sources of your modules from stats file with `source: false` option.
// See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
statsOptions: null,
// Log level. Can be 'info', 'warn', 'error' or 'silent'.
logLevel: 'info'
}));
*/
}
var jq = new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery'",
"window.$": "jquery"
});
pluginArray.push(jq);
pluginArray.push(new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
'window.$': 'jquery'
}));
// Strip all locales except the ones defined in lib/language.js
// (“en” is built into Moment and cant be removed, 'dk' is not defined in moment)
var momentLocales = new MomentLocalesPlugin({
localesToKeep: ['bg', 'cs', 'de', 'el', 'es', 'fi', 'fr', 'he', 'hr', 'it', 'ko', 'nb', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sv', 'zh_cn', 'zh_tw'],
}) ;
pluginArray.push(momentLocales);
pluginArray.push(new MomentLocalesPlugin({
localesToKeep: ['bg', 'cs', 'de', 'el', 'es', 'fi', 'fr', 'he', 'hr', 'it', 'ko', 'nb', 'nl', 'pl', 'pt', 'ro', 'ru',
'sk', 'sv', 'zh_cn', 'zh_tw'],
}));
module.exports = {
context: path.resolve(__dirname, '.'),
entry: {
app: './bundle/bundle.source.js'
context: path.resolve(__dirname, '.'),
entry: {
app: './bundle/bundle.source.js'
},
output: {
path: path.resolve(__dirname, './tmp'),
publicPath: '/',
filename: 'js/bundle.js',
sourceMapFilename: 'js/bundle.js.map',
},
devtool: sourceMapType,
optimization: {
minimizer: [
new TerserPlugin( {
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
ie8: false,
safari10: false
// https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
}
} ),
],
},
plugins: pluginArray,
module: {
rules: [{
test: /\.(jpe?g|png|gif)$/i,
loader: 'file-loader',
query: {
name: '[name].[ext]',
outputPath: 'images/'
//the images will be emmited to public/assets/images/ folder
//the images will be put in the DOM <style> tag as eg. background: url(assets/images/image.png);
},
exclude: /node_modules/
},
output: {
path: path.resolve(__dirname, './tmp'),
publicPath: '/',
filename: 'js/bundle.js',
sourceMapFilename: "js/bundle.js.map",
},
devtool: sourceMapType,
plugins: pluginArray,
module: {
rules: [{
test: /\.(jpe?g|png|gif)$/i,
loader: "file-loader",
query: {
name: '[name].[ext]',
outputPath: 'images/'
//the images will be emmited to public/assets/images/ folder
//the images will be put in the DOM <style> tag as eg. background: url(assets/images/image.png);
}
},
{
test: /\.css$/,
loaders: ["style-loader", "css-loader"]
}, {
test: require.resolve('jquery'),
use: [{
loader: 'expose-loader',
options: '$'
}]
}
]
}
{
test: /\.css$/,
loaders: ['style-loader', 'css-loader'],
exclude: /node_modules/
},
{
test: require.resolve('jquery'),
use: [{
loader: 'expose-loader',
options: '$'
}]
}
]
}
};