diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05b9aa4dbaf..ede1c7e88c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,6 @@ - # Contributing to cgm-remote-monitor [![Build Status][build-img]][build-url] @@ -42,11 +41,17 @@ [waffle]: https://waffle.io/nightscout/cgm-remote-monitor [progress-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=in+progress&title=In+Progress -## Design & new features +## Installation for development -If you intend to add a new feature, please allow the community to participate in the design process by creating an issue to discuss your design. For new features, the issue should describe what use cases the new feature intends to solve, or which existing use cases are being improved. +Nightscout is a Node.js application. The basic installation of the software for local purposes is: -Note Nighscout has a plugin architecture for adding new features. We expect most code for new features live inside a Plugin, so the code retains a clear separation of concerns. If the Plugin API doesn't implement all features you need to implement your feature, please discuss with us on adding those features to the API. Note new features should under almost no circumstances require changes to the existing plugins. +1. Clone the software to your local machine using git +2. Install Node from https://nodejs.org/en/download/ +2. Use `npm` to install Nightscout dependencies by invokin `npm install` in the project directory. Note the + dependency installation has to be done usign a non-root user - _do not use root_ for development and hosting + the software! +3. Get a Mongo database by either installing Mongo locally, or get a free cloud account from mLab or Mongodb Atlas. +4. Configure nightscout by copying `my.env.template` to `my.env` and run it - see the next chapter in the instructions ## Develop on `dev` @@ -56,8 +61,27 @@ You can get the dev branch checked out using `git checkout dev`. Once checked out, install the dependencies using `npm install`, then copy the included `my.env.template`file to `my.env` and edit the file to include your settings (like the Mongo URL). Leave the `NODE_ENV=development` line intact. Once set, run the site using `npm run dev`. This will start Nigthscout in the development mode, with different code packaging rules and automatic restarting of the server using nodemon, when you save changed files on disk. The client also hot-reloads new code in, but it's recommended to reload the the website after changes due to the way the plugin sandbox works. +Note the template sets `INSECURE_USE_HTTP` to `true` to enable the site to work over HTTP in local development. + If you want to additionaly test the site in production mode, create a file called `my.prod.env` that's a copy of the dev file but with `NODE_ENV=production` and start the site using `npm run prod`. +## REST API + +Nightscout implements a REST API for data syncronization. The API is documented using Swagger. To access the documentation +for the API, run Nightscout locally and load the documentation from /api-docs (or read the associated swagger.json and swagger.yaml +files locally). + +Note all dates used to access the API and dates stored in the objects are expected to comply with the ISO-8601 format and +be deserializable by the Javascript Date class. Of note here is the dates can contain a plus sign which has a special meaning in +URL encoding, so when issuing requests that place dates to the URL, take special care to ensure the data is properly URL +encoded. + +## Design & new features + +If you intend to add a new feature, please allow the community to participate in the design process by creating an issue to discuss your design. For new features, the issue should describe what use cases the new feature intends to solve, or which existing use cases are being improved. + +Note Nighscout has a plugin architecture for adding new features. We expect most code for new features live inside a Plugin, so the code retains a clear separation of concerns. If the Plugin API doesn't implement all features you need to implement your feature, please discuss with us on adding those features to the API. Note new features should under almost no circumstances require changes to the existing plugins. + ## Style Guide Some simple rules that will make it easier to maintain our codebase: diff --git a/README.md b/README.md index cf26a586d11..fd85b987107 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ If you plan to use Nightscout, we recommend using [Heroku](http://www.nightscout ## Windows installation software requirements: -- [Node.js](http://nodejs.org/) Latest Node 8 LTS (Node 8.15.1 or later) or Node 10 LTS (Node 10.15.2 or later; Node 10.14.1 works for Azure). Node versions that do not have the latest security patches will not work. Use [Install instructions for Node](https://nodejs.org/en/download/package-manager/) or use `setup.sh`) +- [Node.js](http://nodejs.org/) Latest Node 8 LTS (Node 8.15.1 or later) or Node 10 LTS (Node 10.16.0 or later; Node 10.15.2 works for Azure). Node versions that do not have the latest security patches will not work. Use [Install instructions for Node](https://nodejs.org/en/download/package-manager/) or use `setup.sh`) - [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: @@ -148,17 +148,16 @@ $ npm install ## Installation notes for users with nginx or Apache reverse proxy for SSL/TLS offloading: -- Set `INSECURE_USE_HTTP` to `false`, to be able to use non secure HTTP connections to Nightscout server -- Your site redirects insecure connections to `https` by default. If you don't want that and use a Nginx or Apache proxy, set `INSECURE_USE_HTTP` to `true`. This will allow (unsafe) http traffic. +- Your site redirects insecure connections to `https` by default. If you use a reverse proxy like nginx or Apache to handle the connection security for you, make sure it sets the `X-Forwarded-Proto` header. Otherwise nightscout will be unable to know if it was called through a secure connection and will try to redirect you to the https version. If you're unable to set this Header, you can change the `INSECURE_USE_HTTP` setting in nightscout to true in order to allow insecure connections without being redirected. - In case you use a proxy. Do not use an external network interfaces for hosting Nightscout. Make sure the unsecure port is not available from a remote network connection - HTTP Strict Transport Security (HSTS) headers are enabled by default, use settings `SECURE_HSTS_HEADER` and `SECURE_HSTS_HEADER_*` - See [Predefined values for your server settings](#predefined-values-for-your-server-settings-optional) for more details ## Installation notes for Microsoft Azure, Windows: -- 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: +- If deploying the software to Microsoft Azure, you must set ** in the app settings for *WEBSITE_NODE_DEFAULT_VERSION* and *SCM_COMMAND_IDLE_TIMEOUT* **before** you deploy the latest Nightscout or the site deployment will likely fail. Other hosting environments do not require this setting. Additionally, if using the Azure free hosting tier, the installation might fail due to resource constraints imposed by Azure on the free hosting. Please set the following settings to the environment in Azure: ``` -WEBSITE_NODE_DEFAULT_VERSION=10.14.1 +WEBSITE_NODE_DEFAULT_VERSION=10.15.2 SCM_COMMAND_IDLE_TIMEOUT=300 ``` - See [install MongoDB, Node.js, and Nightscouton a single Windows system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). if you want to host your Nightscout outside of the cloud. Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10. @@ -192,7 +191,6 @@ mongo string. You can copy and paste the text in the gray box into your Use the [autoconfigure tool][autoconfigure] to sync an uploader to your config. - ## Nightscout API The Nightscout API enables direct access to your DData without the need for direct Mongo access. @@ -202,6 +200,8 @@ The server status and settings are available from `/api/v1/status.json`. By default the `/entries` and `/treatments` APIs limit results to the the most recent 10 values from the last 2 days. You can get many more results, by using the `count`, `date`, `dateString`, and `created_at` parameters, depending on the type of data you're looking for. +Once you've installed Nightscout, you can access API documentation by loading `/api-docs` URL in your instance. + #### Example Queries (replace `http://localhost:1337` with your base url, YOUR-SITE) @@ -215,7 +215,6 @@ You can get many more results, by using the `count`, `date`, `dateString`, and ` The API is Swagger enabled, so you can generate client code to make working with the API easy. To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.html or review [swagger.yaml](swagger.yaml). - ## Environment `VARIABLE` (default) - description @@ -237,7 +236,6 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `IMPORT_CONFIG` - Used to import settings and extended settings from a url such as a gist. Structure of file should be something like: `{"settings": {"theme": "colors"}, "extendedSettings": {"upbat": {"enableAlerts": true}}}` * `TREATMENTS_AUTH` (`on`) - possible values `on` or `off`. Deprecated, if set to `off` the `careportal` role will be added to `AUTH_DEFAULT_ROLES` - ### Alarms These alarm setting effect all delivery methods (browser, pushover, maker, etc), some settings can be overridden per client (web browser) @@ -274,6 +272,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `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 + * `DE_NORMALIZE_DATES`(`true`) - The Nightscout REST API normalizes all entered dates to UTC zone. Some Nightscout clients have broken date deserialization logic and expect to received back dates in zoned formats. Setting this variable to `true` causes the REST API to serialize dates sent to Nightscout in zoned format back to zoned format when served to clients over REST. ### Predefined values for your browser settings (optional) @@ -304,12 +303,13 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `SECURE_CSP` (`false`) - Add Content Security Policy headers. Possible values `false`, or `true`. * `SECURE_CSP_REPORT_ONLY` (`false`) - If set to `true` allows to experiment with policies by monitoring (but not enforcing) their effects. Possible values `false`, or `true`. - ### Views +### 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. - * `/bgclock.html` - Shows current BG, trend arrow, and time of day. Grey text on a black background. - * `/clock-color.html` - Shows current BG and trend arrow. White text on a background that changes color to indicate current BG threshold (green = in range; blue = below range; yellow = above range; red = urgent below/above). + There are a few alternate web views available from the main menu that display a simplified BG stream. (If you launch one of these in a fullscreen view in iOS, you can use a left-to-right swipe gesture to exit the view.) + * `Clock` - Shows current BG, trend arrow, and time of day. Grey text on a black background. + * `Color` - Shows current BG and trend arrow. White text on a background that changes color to indicate current BG threshold (green = in range; blue = below range; yellow = above range; red = urgent below/above). + * `Simple` - Shows current BG. Grey text on a black background. + * Optional configuration: set `SHOW_CLOCK_CLOSEBUTTON` to `false` to never show the small X button in clock views. For bookmarking a clock view without the close box but have it appear when navigating to a clock from the Nightscout menu, don't change the settng, but remove the `showClockClosebutton=true` parameter from the clock view URL. ### Plugins @@ -479,6 +479,13 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `OPENAPS_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceed before an urgent alarm is triggered * `OPENAPS_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display by default. Any of the following fields: `status-symbol`, `status-label`, `iob`, `meal-assist`, `freq`, and `rssi` * `OPENAPS_RETRO_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display in retro mode. Any of the above fields. + * `OPENAPS_PRED_IOB_COLOR` (`#1e88e5`) - The color to use for IOB prediction lines. Colors can be in either `#RRGGBB` or `#RRGGBBAA` format. + * `OPENAPS_PRED_COB_COLOR` (`#FB8C00FF`) - The color to use for COB prediction lines. Same format as above. + * `OPENAPS_PRED_ACOB_COLOR` (`#FB8C0080`) - The color to use for ACOB prediction lines. Same format as above. + * `OPENAPS_PRED_ZT_COLOR` (`#00d2d2`) - The color to use for ZT prediction lines. Same format as above. + * `OPENAPS_PRED_UAM_COLOR` (`#c9bd60`) - The color to use for UAM prediction lines. Same format as above. + * `OPENAPS_COLOR_PREDICTION_LINES` (`true`) - Enables / disables the colored lines vs the classic purple color. + Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker). diff --git a/app.js b/app.js index fe9f0c6fe00..05f1d8ff694 100644 --- a/app.js +++ b/app.js @@ -8,288 +8,286 @@ const bodyParser = require('body-parser'); const path = require('path'); const fs = require('fs'); -function create(env, ctx) { - var app = express(); - 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; - if (!insecureUseHttp) { - console.info('Redirecting http traffic to https because INSECURE_USE_HTTP=', insecureUseHttp); - app.use((req, res, next) => { - if (req.header('x-forwarded-proto') == 'https' || req.secure) { - next(); +function create (env, ctx) { + var app = express(); + 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; + if (!insecureUseHttp) { + console.info('Redirecting http traffic to https because INSECURE_USE_HTTP=', insecureUseHttp); + app.use((req, res, next) => { + if (req.header('x-forwarded-proto') == 'https' || req.secure) { + next(); + } else { + res.redirect(307, `https://${req.header('host')}${req.url}`); + } + }) + if (secureHstsHeader) { // Add HSTS (HTTP Strict Transport Security) header + console.info('Enabled SECURE_HSTS_HEADER (HTTP Strict Transport Security)'); + const helmet = require('helmet'); + var includeSubDomainsValue = env.secureHstsHeaderIncludeSubdomains; + var preloadValue = env.secureHstsHeaderPreload; + app.use(helmet({ + hsts: { + maxAge: 31536000 + , includeSubDomains: includeSubDomainsValue + , preload: preloadValue + } + , frameguard: false + })); + if (env.secureCsp) { + var secureCspReportOnly = env.secureCspReportOnly; + if (secureCspReportOnly) { + console.info('Enabled SECURE_CSP (Content Security Policy header). Not enforcing. Report only.'); } else { - res.redirect(`https://${req.header('host')}${req.url}`); + console.info('Enabled SECURE_CSP (Content Security Policy header). Enforcing.'); } - }) - if (secureHstsHeader) { // Add HSTS (HTTP Strict Transport Security) header - console.info('Enabled SECURE_HSTS_HEADER (HTTP Strict Transport Security)'); - const helmet = require('helmet'); - var includeSubDomainsValue = env.secureHstsHeaderIncludeSubdomains; - var preloadValue = env.secureHstsHeaderPreload; - app.use(helmet({ - hsts: { - maxAge: 31536000, - includeSubDomains: includeSubDomainsValue, - preload: preloadValue - }, - frameguard: false - })); - if (env.secureCsp) { - var secureCspReportOnly= env.secureCspReportOnly; - if (secureCspReportOnly) { - console.info( 'Enabled SECURE_CSP (Content Security Policy header). Not enforcing. Report only.' ); - } else { - console.info( 'Enabled SECURE_CSP (Content Security Policy header). Enforcing.' ); - } - 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/', 'data:'], - imgSrc: [ "'self'", 'data:'], - objectSrc: ["'none'"], // Restricts , , and elements - reportUri: '/report-violation', - frameAncestors: ["'none'"], // Clickjacking protection, using frame-ancestors - baseUri: ["'none'"], // Restricts use of the tag - formAction: ["'self'"], // Restricts where
contents may be submitted - }, - reportOnly: secureCspReportOnly - })); - app.use(helmet.referrerPolicy({ policy: 'no-referrer' })); - app.use(helmet.featurePolicy({ features: { payment: ["'none'"], } })); - app.use(bodyParser.json({type: ['json', 'application/csp-report'] })) - app.post('/report-violation', (req, res) => { - if (req.body) { - console.log('CSP Violation: ', req.body) } - else { - console.log('CSP Violation: No data received!') - } - res.status(204).end() - }) + 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/', 'data:'] + , imgSrc: ["'self'", 'data:'] + , objectSrc: ["'none'"], // Restricts , , and elements + reportUri: '/report-violation' + , frameAncestors: ["'none'"], // Clickjacking protection, using frame-ancestors + baseUri: ["'none'"], // Restricts use of the tag + formAction: ["'self'"], // Restricts where contents may be submitted } - } - } - else { - console.info('Security settings: INSECURE_USE_HTTP=',insecureUseHttp,', SECURE_HSTS_HEADER=',secureHstsHeader); - } - - app.set('view engine', 'ejs'); - // this allows you to render .html files as templates in addition to .ejs - app.engine('html', require('ejs').renderFile); - app.engine('appcache', require('ejs').renderFile); - app.set("views", path.join(__dirname, "views/")); - - let cacheBuster = 'developmentMode'; - if (process.env.NODE_ENV !== 'development') { - cacheBuster = fs.readFileSync(process.cwd() + '/tmp/cacheBusterToken').toString().trim(); - } - app.locals.cachebuster = cacheBuster; - - if (ctx.bootErrors && ctx.bootErrors.length > 0) { - app.get('*', require('./lib/server/booterror')(ctx)); - return app; + , reportOnly: secureCspReportOnly + })); + app.use(helmet.referrerPolicy({ policy: 'no-referrer' })); + app.use(helmet.featurePolicy({ features: { payment: ["'none'"], } })); + app.use(bodyParser.json({ type: ['json', 'application/csp-report'] })) + app.post('/report-violation', (req, res) => { + if (req.body) { + console.log('CSP Violation: ', req.body) + } else { + console.log('CSP Violation: No data received!') + } + res.status(204).end() + }) + } } - - if (env.settings.isEnabled('cors')) { - var allowOrigin = _get(env, 'extendedSettings.cors.allowOrigin') || '*'; - console.info('Enabled CORS, allow-origin:', allowOrigin); - app.use(function allowCrossDomain(req, res, next) { - res.header('Access-Control-Allow-Origin', allowOrigin); - res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With'); - - // intercept OPTIONS method - if ('OPTIONS' === req.method) { - res.send(200); - } else { - next(); - } - }); + } else { + console.info('Security settings: INSECURE_USE_HTTP=', insecureUseHttp, ', SECURE_HSTS_HEADER=', secureHstsHeader); + } + + app.set('view engine', 'ejs'); + // this allows you to render .html files as templates in addition to .ejs + app.engine('html', require('ejs').renderFile); + app.engine('appcache', require('ejs').renderFile); + app.set("views", path.join(__dirname, "views/")); + + let cacheBuster = 'developmentMode'; + if (process.env.NODE_ENV !== 'development') { + cacheBuster = fs.readFileSync(process.cwd() + '/tmp/cacheBusterToken').toString().trim(); + } + app.locals.cachebuster = cacheBuster; + + if (ctx.bootErrors && ctx.bootErrors.length > 0) { + app.get('*', require('./lib/server/booterror')(ctx)); + return app; + } + + if (env.settings.isEnabled('cors')) { + var allowOrigin = _get(env, 'extendedSettings.cors.allowOrigin') || '*'; + console.info('Enabled CORS, allow-origin:', allowOrigin); + app.use(function allowCrossDomain (req, res, next) { + res.header('Access-Control-Allow-Origin', allowOrigin); + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With'); + + // intercept OPTIONS method + if ('OPTIONS' === req.method) { + res.send(200); + } else { + next(); + } + }); + } + + /////////////////////////////////////////////////// + // api and json object variables + /////////////////////////////////////////////////// + var api = require('./lib/api/')(env, ctx); + var ddata = require('./lib/data/endpoints')(env, ctx); + + app.use(compression({ + filter: function shouldCompress (req, res) { + //TODO: return false here if we find a condition where we don't want to compress + // fallback to standard filter function + return compression.filter(req, res); } + })); - /////////////////////////////////////////////////// - // api and json object variables - /////////////////////////////////////////////////// - var api = require('./lib/api/')(env, ctx); - var ddata = require('./lib/data/endpoints')(env, ctx); - - app.use(compression({ - filter: function shouldCompress(req, res) { - //TODO: return false here if we find a condition where we don't want to compress - // fallback to standard filter function - return compression.filter(req, res); - } - })); + const clockviews = require('./lib/server/clocks.js')(env, ctx); + clockviews.setLocals(app.locals); - const clockviews = require('./lib/server/clocks.js')(env, ctx); - clockviews.setLocals(app.locals); - - app.use("/clock", clockviews); + app.use("/clock", clockviews); - app.get("/", (req, res) => { - res.render("index.html", { - locals: app.locals - }); + app.get("/", (req, res) => { + res.render("index.html", { + locals: app.locals }); - - var appPages = { - "/clock-color.html": "clock-color.html", - "/admin": "adminindex.html", - "/profile": "profileindex.html", - "/food": "foodindex.html", - "/bgclock.html": "bgclock.html", - "/report": "reportindex.html", - "/translations": "translationsindex.html", - "/clock.html": "clock.html" - }; - - Object.keys(appPages).forEach(function (page) { - app.get(page, (req, res) => { - res.render(appPages[page], { - locals: app.locals - }); - }); + }); + + var appPages = { + "/clock-color.html": "clock-color.html" + , "/admin": "adminindex.html" + , "/profile": "profileindex.html" + , "/food": "foodindex.html" + , "/bgclock.html": "bgclock.html" + , "/report": "reportindex.html" + , "/translations": "translationsindex.html" + , "/clock.html": "clock.html" + }; + + Object.keys(appPages).forEach(function(page) { + app.get(page, (req, res) => { + res.render(appPages[page], { + locals: app.locals + }); }); + }); - app.get("/appcache/*", (req, res) => { - res.render("nightscout.appcache", { - locals: app.locals - }); - }); - - app.use('/api/v1', bodyParser({ - limit: 1048576 * 50 - }), api); - - app.use('/api/v2/properties', ctx.properties); - app.use('/api/v2/authorization', ctx.authorization.endpoints); - app.use('/api/v2/ddata', ddata); - - // pebble data - app.get('/pebble', ctx.pebble); - - // expose swagger.json - app.get('/swagger.json', function (req, res) { - res.sendFile(__dirname + '/swagger.json'); + app.get("/appcache/*", (req, res) => { + res.render("nightscout.appcache", { + locals: app.locals }); - - // expose swagger.yaml - app.get('/swagger.yaml', function (req, res) { - res.sendFile(__dirname + '/swagger.yaml'); + }); + + app.use('/api/v1', bodyParser({ + limit: 1048576 * 50 + }), api); + + app.use('/api/v2/properties', ctx.properties); + app.use('/api/v2/authorization', ctx.authorization.endpoints); + app.use('/api/v2/ddata', ddata); + + // pebble data + app.get('/pebble', ctx.pebble); + + // expose swagger.json + app.get('/swagger.json', function(req, res) { + res.sendFile(__dirname + '/swagger.json'); + }); + + // expose swagger.yaml + app.get('/swagger.yaml', function(req, res) { + res.sendFile(__dirname + '/swagger.yaml'); + }); + + if (env.settings.isEnabled('dumps')) { + var heapdump = require('heapdump'); + app.get('/api/v2/dumps/start', function(req, res) { + var path = new Date().toISOString() + '.heapsnapshot'; + path = path.replace(/:/g, '-'); + console.info('writing dump to', path); + heapdump.writeSnapshot(path); + res.send('wrote dump to ' + path); }); + } + // app.get('/package.json', software); - /* - if (env.settings.isEnabled('dumps')) { - var heapdump = require('heapdump'); - app.get('/api/v2/dumps/start', function(req, res) { - var path = new Date().toISOString() + '.heapsnapshot'; - path = path.replace(/:/g, '-'); - console.info('writing dump to', path); - heapdump.writeSnapshot(path); - res.send('wrote dump to ' + path); - }); - } - */ - - //app.get('/package.json', software); - - // Allow static resources to be cached for week - var maxAge = 7 * 24 * 60 * 60 * 1000; - - if (process.env.NODE_ENV === 'development') { - maxAge = 10; - console.log('Development environment detected, setting static file cache age to 10 seconds'); + // Allow static resources to be cached for week + var maxAge = 7 * 24 * 60 * 60 * 1000; - app.get('/nightscout.appcache', function (req, res) { - res.sendStatus(404); - }); - } + if (process.env.NODE_ENV === 'development') { + maxAge = 1; + console.log('Development environment detected, setting static file cache age to 1 second'); - //TODO: JC - changed cache to 1 hour from 30d ays to bypass cache hell until we have a real solution - var staticFiles = express.static(env.static_files, { - maxAge: maxAge + app.get('/nightscout.appcache', function(req, res) { + res.sendStatus(404); }); + } - // serve the static content - app.use(staticFiles); + var staticFiles = express.static(env.static_files, { + maxAge + }); - const swaggerUiAssetPath = require("swagger-ui-dist").getAbsoluteFSPath(); - var swaggerFiles = express.static(swaggerUiAssetPath, { - maxAge: maxAge - }); + // serve the static content + app.use(staticFiles); - // serve the static content - app.use('/swagger-ui-dist', swaggerFiles); + // API docs - // if this is dev environment, package scripts on the fly - // if production, rely on postinstall script to run packaging for us + const swaggerUi = require('swagger-ui-express'); + const swaggerDocument = require('./swagger.json'); - app.locals.bundle = '/bundle'; + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); - if (process.env.NODE_ENV === 'development') { + app.use('/swagger-ui-dist', (req, res, next) => { + res.redirect(307, '/api-docs'); + }); - console.log('Development mode'); + // if this is dev environment, package scripts on the fly + // if production, rely on postinstall script to run packaging for us - app.locals.bundle = '/devbundle'; + app.locals.bundle = '/bundle'; - const webpack = require('webpack'); - var webpack_conf = require('./webpack.config'); - const middleware = require('webpack-dev-middleware'); - const compiler = webpack(webpack_conf); + if (process.env.NODE_ENV === 'development') { - app.use( - middleware(compiler, { - // webpack-dev-middleware options - publicPath: webpack_conf.output.publicPath, - lazy: false - }) - ); + console.log('Development mode'); - app.use(require("webpack-hot-middleware")(compiler, { - heartbeat: 1000 - })); - } + app.locals.bundle = '/devbundle'; - // Production bundling - var tmpFiles = express.static('tmp', { - maxAge: maxAge - }); + const webpack = require('webpack'); + var webpack_conf = require('./webpack.config'); + const middleware = require('webpack-dev-middleware'); + const compiler = webpack(webpack_conf); - // serve the static content - app.use('/bundle', tmpFiles); + app.use( + middleware(compiler, { + // webpack-dev-middleware options + publicPath: webpack_conf.output.publicPath + , lazy: false + }) + ); - if (process.env.NODE_ENV !== 'development') { - - console.log('Production environment detected, enabling Minify'); - - var minify = require('express-minify'); - var myCssmin = require('cssmin'); - - app.use(minify({ - js_match: /\.js/, - css_match: /\.css/, - sass_match: /scss/, - less_match: /less/, - stylus_match: /stylus/, - coffee_match: /coffeescript/, - json_match: /json/, - cssmin: myCssmin, - cache: __dirname + '/tmp', - onerror: undefined, - })); - - } - - // Handle errors with express's errorhandler, to display more readable error messages. - var errorhandler = require('errorhandler'); - //if (process.env.NODE_ENV === 'development') { - app.use(errorhandler()); - //} - return app; + app.use(require("webpack-hot-middleware")(compiler, { + heartbeat: 1000 + })); + } + + // Production bundling + var tmpFiles = express.static('tmp', { + maxAge: maxAge + }); + + // serve the static content + app.use('/bundle', tmpFiles); + + if (process.env.NODE_ENV !== 'development') { + + console.log('Production environment detected, enabling Minify'); + + var minify = require('express-minify'); + var myCssmin = require('cssmin'); + + app.use(minify({ + js_match: /\.js/ + , css_match: /\.css/ + , sass_match: /scss/ + , less_match: /less/ + , stylus_match: /stylus/ + , coffee_match: /coffeescript/ + , json_match: /json/ + , cssmin: myCssmin + , cache: __dirname + '/tmp' + , onerror: undefined + , })); + + } + + // Handle errors with express's errorhandler, to display more readable error messages. + var errorhandler = require('errorhandler'); + //if (process.env.NODE_ENV === 'development') { + app.use(errorhandler()); + //} + return app; } -module.exports = create; \ No newline at end of file +module.exports = create; diff --git a/env.js b/env.js index cb200d59de5..9114e7297fc 100644 --- a/env.js +++ b/env.js @@ -21,6 +21,17 @@ function config ( ) { * See README.md for info about all the supported ENV VARs */ env.DISPLAY_UNITS = readENV('DISPLAY_UNITS', 'mg/dl'); + + // be lenient at accepting the mmol input + if (env.DISPLAY_UNITS.toLowerCase().includes('mmol')) { + env.DISPLAY_UNITS = 'mmol'; + } else { + // also ensure the mg/dl is set with expected case + env.DISPLAY_UNITS = 'mg/dl'; + } + + console.log('Units set to', env.DISPLAY_UNITS ); + env.PORT = readENV('PORT', 1337); env.HOSTNAME = readENV('HOSTNAME', null); env.IMPORT_CONFIG = readENV('IMPORT_CONFIG', null); diff --git a/lib/api/devicestatus/index.js b/lib/api/devicestatus/index.js index 5c665b1ffc5..91702902fa3 100644 --- a/lib/api/devicestatus/index.js +++ b/lib/api/devicestatus/index.js @@ -1,17 +1,18 @@ 'use strict'; -var consts = require('../../constants'); +const consts = require('../../constants'); +const moment = require('moment'); -function configure (app, wares, ctx) { - var express = require('express'), - api = express.Router( ); +function configure (app, wares, ctx, env) { + var express = require('express') + , api = express.Router(); // invoke common middleware api.use(wares.sendJSONStatus); // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw( )); + api.use(wares.bodyParser.raw()); // json body types get handled as parsed json - api.use(wares.bodyParser.json( )); + api.use(wares.bodyParser.json()); // also support url-encoded content-type api.use(wares.bodyParser.urlencoded({ extended: true })); @@ -23,7 +24,21 @@ function configure (app, wares, ctx) { if (!q.count) { q.count = 10; } - ctx.devicestatus.list(q, function (err, results) { + + ctx.devicestatus.list(q, function(err, results) { + + // Support date de-normalization for older clients + if (env.settings.deNormalizeDates) { + results.forEach(function(e) { + // eslint-disable-next-line no-prototype-builtins + if (e.created_at && e.hasOwnProperty('utcOffset')) { + const d = moment(e.created_at).utcOffset(e.utcOffset); + e.created_at = d.toISOString(true); + delete e.utcOffset; + } + }); + } + return res.json(results); }); }); @@ -32,7 +47,7 @@ function configure (app, wares, ctx) { function doPost (req, res) { var obj = req.body; - ctx.devicestatus.create(obj, function (err, created) { + ctx.devicestatus.create(obj, function(err, created) { if (err) { res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); } else { @@ -48,7 +63,7 @@ function configure (app, wares, ctx) { * 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) { + function delete_records (req, res, next) { var query = req.query; if (!query.count) { query.count = 10 @@ -91,7 +106,7 @@ function configure (app, wares, ctx) { api.delete('/devicestatus/', ctx.authorization.isPermitted('api:devicestatus:delete'), delete_records); } - if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/) { + if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/ ) { config_authed(app, api, wares, ctx); } @@ -99,4 +114,3 @@ function configure (app, wares, ctx) { } module.exports = configure; - diff --git a/lib/api/entries/index.js b/lib/api/entries/index.js index 0280c51b34a..0c8b8fc1ef7 100644 --- a/lib/api/entries/index.js +++ b/lib/api/entries/index.js @@ -1,18 +1,19 @@ 'use strict'; -var _last = require('lodash/last'); -var _isNil = require('lodash/isNil'); -var _first = require('lodash/first'); -var _includes = require('lodash/includes'); +const _last = require('lodash/last'); +const _isNil = require('lodash/isNil'); +const _first = require('lodash/first'); +const _includes = require('lodash/includes'); +const moment = require('moment'); -var consts = require('../../constants'); -var es = require('event-stream'); -var braces = require('braces'); -var expand = braces.expand; +const consts = require('../../constants'); +const es = require('event-stream'); +const braces = require('braces'); +const expand = braces.expand; -var ID_PATTERN = /^[a-f\d]{24}$/; +const ID_PATTERN = /^[a-f\d]{24}$/; -function isId(value) { +function isId (value) { //TODO: why did we need tht length check? return value && ID_PATTERN.test(value) && value.length === 24; } @@ -31,10 +32,10 @@ function isId(value) { * @param Object ctx The global ctx with all modules, storage, and event buses * configured. */ -function configure(app, wares, ctx) { +function configure (app, wares, ctx, env) { // default storage biased towards entries. - var entries = ctx.entries; - var express = require('express') + const entries = ctx.entries; + const express = require('express') , api = express.Router(); // invoke common middleware @@ -59,17 +60,28 @@ function configure(app, wares, ctx) { * elements on the stream have a `type` field. * Generate a stream that ensures elements have a `type` field. */ - function force_typed_data(opts) { + function force_typed_data (opts) { /** * @function sync * Iterate over every element in the stream, enforcing some data type. */ - function sync(data, next) { + function sync (data, next) { // if element has no data type, but we know what the type should be if (!data.type && opts.type) { // bless absence with known type data.type = opts.type; } + + // Support date de-normalization for older clients + if (env.settings.deNormalizeDates) { + // eslint-disable-next-line no-prototype-builtins + if (data.dateString && data.hasOwnProperty('utcOffset')) { + const d = moment(data.dateString).utcOffset(data.utcOffset); + data.dateString = d.toISOString(true); + delete data.utcOffset; + } + } + // continue control flow to next element in the stream next(null, data); } @@ -79,7 +91,7 @@ function configure(app, wares, ctx) { // check for last modified from in-memory data - function ifModifiedSinceCTX(req, res, next) { + function ifModifiedSinceCTX (req, res, next) { var lastEntry = _last(ctx.ddata.sgvs); var lastEntryDate = null; @@ -96,7 +108,7 @@ function configure(app, wares, ctx) { console.log('CGM Entry request with If-Modified-Since: ', ifModifiedSince); - if (lastEntryDate.getTime() <= new Date(ifModifiedSince).getTime()) { + if (lastEntryDate.getTime() <= Date.parse(ifModifiedSince)) { console.log('Sending Not Modified'); res.status(304).send({ status: 304 @@ -116,23 +128,22 @@ function configure(app, wares, ctx) { * We expect a payload to be attached to `res.entries`. // Middleware to format any response involving entries. */ - function format_entries(req, res) { + function format_entries (req, res) { // deduce what type of records we might expect var type_params = { type: (req.query && req.query.find && req.query.find.type && req.query.find.type !== req.params.model) ? req.query.find.type : req.params.model }; - // prepare a stream of elements from some prepared payload - var output = es.readArray(res.entries || []); - // on other hand, if there's been some error, report that + + // f there's been some error, report that if (res.entries_err) { return res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', res.entries_err); } // IF-Modified-Since support - function compare(a, b) { + function compare (a, b) { var a_field = a.mills ? a.mills : a.date; var b_field = b.mills ? b.mills : b.date; @@ -148,6 +159,9 @@ function configure(app, wares, ctx) { var lastEntry = _first(res.entries); var lastEntryDate = null; + // prepare a stream of elements from some prepared payload + var output = es.readArray(res.entries || []); + if (!_isNil(lastEntry)) { if (lastEntry.mills) lastEntryDate = new Date(lastEntry.mills); if (!lastEntry.mills && lastEntry.date) lastEntryDate = new Date(lastEntry.date); @@ -166,9 +180,9 @@ function configure(app, wares, ctx) { return; } - function formatWithSeparator(data, separator) { - if (data === null || data.constructor !== Array || data.length == 0) return ""; - + function formatWithSeparator (data, separator) { + if (data === null || data.constructor !== Array || data.length == 0) return ""; + var outputdata = []; data.forEach(function(e) { var entry = { @@ -245,7 +259,7 @@ function configure(app, wares, ctx) { * into the configured storage layer, saving the results in mongodb. */ // middleware to process "uploads" of sgv data - function insert_entries(req, res, next) { + function insert_entries (req, res, next) { // list of incoming records var incoming = []; // Potentially a single json encoded body. @@ -266,7 +280,7 @@ function configure(app, wares, ctx) { * Sends stream elements into storage layer. * Configures the storage layer stream. */ - function persist(fn) { + function persist (fn) { if (req.persist_entries) { // store everything return entries.persist(fn); @@ -280,7 +294,7 @@ function configure(app, wares, ctx) { * Final callback store results on `res.entries`, after all I/O is done. * store results and move to the next middleware */ - function done(err, result) { + function done (err, result) { // assign payload res.entries = result; res.entries_err = err; @@ -298,7 +312,7 @@ function configure(app, wares, ctx) { * @param {String} model The name of the model to use if not found. * Sets `req.query.find.type` to your chosen model. */ - function prepReqModel(req, model) { + function prepReqModel (req, model) { var type = model || 'sgv'; if (!req.query.find) { req.query.find = { @@ -370,7 +384,6 @@ function configure(app, wares, ctx) { } }, format_entries); - /** * @module get#/entries * @route @@ -388,7 +401,7 @@ function configure(app, wares, ctx) { * Useful for understanding how REST api parameters translate into mongodb * queries. */ - function echo_query(req, res) { + function echo_query (req, res) { var query = req.query; // make a depth-wise copy of the original raw input var input = JSON.parse(JSON.stringify(query)); @@ -416,7 +429,7 @@ function configure(app, wares, ctx) { * This middleware executes the query, assigning the payload to results on * `res.entries`. */ - function query_models(req, res, next) { + function query_models (req, res, next) { var query = req.query; // If "?count=" is present, use that number to decided how many to return. @@ -427,7 +440,7 @@ function configure(app, wares, ctx) { // bias to entries, but allow expressing a preference var storage = req.storage || ctx.entries; // perform the query - storage.list(query, function payload(err, entries) { + storage.list(query, function payload (err, entries) { // assign payload res.entries = entries; res.entries_err = err; @@ -435,10 +448,10 @@ function configure(app, wares, ctx) { }); } - function count_records(req, res, next) { + function count_records (req, res, next) { var query = req.query; var storage = req.storage || ctx.entries; - storage.aggregate(query, function payload(err, entries) { + storage.aggregate(query, function payload (err, entries) { // assign payload res.entries = entries; res.entries_err = err; @@ -446,7 +459,7 @@ function configure(app, wares, ctx) { }); } - function format_results(req, res, next) { + function format_results (req, res, next) { res.json(res.entries); next(); } @@ -456,7 +469,7 @@ function configure(app, wares, ctx) { * Delete entries. 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) { + function delete_records (req, res, next) { // bias towards model, but allow expressing a preference if (!req.model) { req.model = ctx.entries; @@ -516,22 +529,22 @@ function configure(app, wares, ctx) { api.get('/echo/:echo/:model?/:spec?', echo_query); /** - * Prepare regexp patterns based on `prefix`, and `regex` parameters. - * Translates `/:prefix/:regex` strings into fancy mongo queries. - * @method prep_patterns - * @params String prefix - * @params String regex - * This performs bash style brace/glob pattern expansion in order to generate flexible series of regex patterns. - * Very useful in querying across days, but constrained hours of time. - * Consider the following examples: -``` -curl -s -g 'http://localhost:1337/api/v1/times/2015-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv -curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv -curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv - -``` - */ - function prep_patterns(req, res, next) { + * Prepare regexp patterns based on `prefix`, and `regex` parameters. + * Translates `/:prefix/:regex` strings into fancy mongo queries. + * @method prep_patterns + * @params String prefix + * @params String regex + * This performs bash style brace/glob pattern expansion in order to generate flexible series of regex patterns. + * Very useful in querying across days, but constrained hours of time. + * Consider the following examples: + ``` + curl -s -g 'http://localhost:1337/api/v1/times/2015-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv + curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv + curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv + + ``` + */ + function prep_patterns (req, res, next) { // initialize empty pattern list. var pattern = []; // initialize a basic prefix @@ -552,7 +565,7 @@ curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.js // create a single pattern with all inputs considered // expand the pattern using bash/glob style brace expansion to generate // an array of patterns. - + pattern = expand(pattern.join('')); if (pattern.length == 0) pattern = ['']; @@ -565,12 +578,12 @@ curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.js * RegExp with the prefix and suffix prepended, and appended, * respectively. */ - function iter_regex(prefix, suffix) { + function iter_regex (prefix, suffix) { /** * @function make * @returns RegExp Make a RegExp with configured prefix and suffix */ - function make(pat) { + function make (pat) { // concat the prefix, pattern, and suffix. pat = (prefix ? prefix : '') + pat + (suffix ? suffix : ''); // return RegExp. @@ -621,7 +634,7 @@ curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.js * Default is `dateString`, because that's the iso8601 field for sgv * entries. */ - function prep_pattern_field(req, res, next) { + function prep_pattern_field (req, res, next) { // If req.params.field from routed path parameter is available use it. if (req.params.field) { req.patternField = req.params.field; @@ -640,7 +653,7 @@ curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.js * the entries storage layer, because that's where sgv records are stored * by default. */ - function prep_storage(req, res, next) { + function prep_storage (req, res, next) { if (req.params.storage && _includes(['entries', 'treatments', 'devicestatus'], req.params.storage)) { req.storage = ctx[req.params.storage]; } else { @@ -667,31 +680,31 @@ curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.js }); /** - * @module get#/times/:prefix/:regex - * Allows searching for modal times of day across days and months. -``` -/api/v1/times/2015-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 -/api/v1/times/20{14..15}-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 -/api/v1/times/20{14..15}/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 -``` - * @routed - * @response 200 /definitions/Entries - */ + * @module get#/times/:prefix/:regex + * Allows searching for modal times of day across days and months. + ``` + /api/v1/times/2015-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 + /api/v1/times/20{14..15}-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 + /api/v1/times/20{14..15}/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 + ``` + * @routed + * @response 200 /definitions/Entries + */ api.get('/times/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, prep_patterns, query_models, format_entries); api.get('/count/:storage/where', prep_storage, count_records, format_results); /** - * @module get#/slice/:storage/:field/:type/:prefix/:regex - * @routed - * @response 200 /definitions/Entries - * Allows searching for modal times of day across days and months. - * Also allows specifying field to perform regexp on, the storage layer to - * use, as well as which type of model to look for. -``` -/api/v1/slice/entries/dateString/mbg/2015.json -``` - */ + * @module get#/slice/:storage/:field/:type/:prefix/:regex + * @routed + * @response 200 /definitions/Entries + * Allows searching for modal times of day across days and months. + * Also allows specifying field to perform regexp on, the storage layer to + * use, as well as which type of model to look for. + ``` + /api/v1/slice/entries/dateString/mbg/2015.json + ``` + */ api.get('/slice/:storage/:field/:type?/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, query_models, format_entries); /** diff --git a/lib/api/index.js b/lib/api/index.js index 3c9748c93e6..47a8a7bac3d 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -40,7 +40,7 @@ function create (env, ctx) { app.use(wares.extensions([ 'json', 'svg', 'csv', 'txt', 'png', 'html', 'tsv' ])); - var entriesRouter = require('./entries/')(app, wares, ctx); + var entriesRouter = require('./entries/')(app, wares, ctx, env); // Entries and settings app.all('/entries*', entriesRouter); app.all('/echo/*', entriesRouter); @@ -48,9 +48,9 @@ function create (env, ctx) { app.all('/slice/*', entriesRouter); app.all('/count/*', entriesRouter); - app.all('/treatments*', require('./treatments/')(app, wares, ctx)); + app.all('/treatments*', require('./treatments/')(app, wares, ctx, env)); app.all('/profile*', require('./profile/')(app, wares, ctx)); - app.all('/devicestatus*', require('./devicestatus/')(app, wares, ctx)); + app.all('/devicestatus*', require('./devicestatus/')(app, wares, ctx, env)); app.all('/notifications*', require('./notifications-api')(app, wares, ctx)); app.all('/activity*', require('./activity/')(app, wares, ctx)); diff --git a/lib/api/treatments/index.js b/lib/api/treatments/index.js index 0362829fc7b..5d527fce6ac 100644 --- a/lib/api/treatments/index.js +++ b/lib/api/treatments/index.js @@ -7,167 +7,175 @@ var _isArray = require('lodash/isArray'); var consts = require('../../constants'); var moment = require('moment'); -function configure(app, wares, ctx) { - var express = require('express') - , api = express.Router(); - - api.use(wares.compression()); - api.use(wares.bodyParser({ - limit: 1048576 * 50 - })); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw({ - limit: 1048576 - })); - // json body types get handled as parsed json - api.use(wares.bodyParser.json({ - limit: 1048576 - })); - // also support url-encoded content-type - api.use(wares.bodyParser.urlencoded({ - limit: 1048576 - , extended: true - })); - // invoke common middleware - api.use(wares.sendJSONStatus); - - api.use(ctx.authorization.isPermitted('api:treatments:read')); - - // List treatments available - api.get('/treatments', function(req, res) { - var ifModifiedSince = req.get('If-Modified-Since'); - ctx.treatments.list(req.query, function(err, results) { - var d1 = null; - - _forEach(results, function clean(t) { - t.carbs = Number(t.carbs); - t.insulin = Number(t.insulin); - - var d2 = null; - - if (t.hasOwnProperty('created_at')) { - d2 = new Date(t.created_at); - } else { - if (t.hasOwnProperty('timestamp')) { - d2 = new Date(t.timestamp); - } - } - - if (d2 == null) { return; } - - if (d1 == null || d2.getTime() > d1.getTime()) { - d1 = d2; - } - }); - - if (!_isNil(d1)) res.setHeader('Last-Modified', d1.toUTCString()); - - if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) { - res.status(304).send({ - status: 304 - , message: 'Not modified' - , type: 'internal' - }); - return; - } else { - return res.json(results); - } - }); - }); +function configure (app, wares, ctx, env) { + var express = require('express') + , api = express.Router(); + + api.use(wares.compression()); + api.use(wares.bodyParser({ + limit: 1048576 * 50 + })); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw({ + limit: 1048576 + })); + // json body types get handled as parsed json + api.use(wares.bodyParser.json({ + limit: 1048576 + })); + // also support url-encoded content-type + api.use(wares.bodyParser.urlencoded({ + limit: 1048576 + , extended: true + })); + // invoke common middleware + api.use(wares.sendJSONStatus); + + api.use(ctx.authorization.isPermitted('api:treatments:read')); + + // List treatments available + api.get('/treatments', function(req, res) { + var ifModifiedSince = req.get('If-Modified-Since'); + ctx.treatments.list(req.query, function(err, results) { + var d1 = null; + + const deNormalizeDates = env.settings.deNormalizeDates; + + _forEach(results, function clean (t) { + t.carbs = Number(t.carbs); + t.insulin = Number(t.insulin); + + // eslint-disable-next-line no-prototype-builtins + if (deNormalizeDates && t.hasOwnProperty('utcOffset')) { + const d = moment(t.created_at).utcOffset(t.utcOffset); + t.created_at = d.toISOString(true); + delete t.utcOffset; + } - function config_authed(app, api, wares, ctx) { + var d2 = null; - function post_response(req, res) { - var treatments = req.body; + if (t.hasOwnProperty('created_at')) { + d2 = new Date(t.created_at); + } else { + if (t.hasOwnProperty('timestamp')) { + d2 = new Date(t.timestamp); + } + } - if (!_isArray(treatments)) { - treatments = [treatments]; - }; + if (d2 == null) { return; } - ctx.treatments.create(treatments, function(err, created) { - if (err) { - console.log('Error adding treatment', err); - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); - } else { - console.log('Treatment created'); - res.json(created); - } - }); + if (d1 == null || d2.getTime() > d1.getTime()) { + d1 = d2; } + }); - api.post('/treatments/', wares.bodyParser({ - limit: 1048576 * 50 - }), ctx.authorization.isPermitted('api:treatments:create'), post_response); - - /** - * @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 - } + if (!_isNil(d1)) res.setHeader('Last-Modified', d1.toUTCString()); + + if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) { + res.status(304).send({ + status: 304 + , message: 'Not modified' + , type: 'internal' + }); + return; + } else { + return res.json(results); + } + }); + }); + + function config_authed (app, api, wares, ctx) { - console.log('Delete records with query: ', query); + function post_response (req, res) { + var treatments = req.body; - // 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); + if (!_isArray(treatments)) { + treatments = [treatments]; + }; - console.log('treatments records deleted'); + ctx.treatments.create(treatments, function(err, created) { + if (err) { + console.log('Error adding treatment', err); + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + console.log('Treatment created'); + res.json(created); + } + }); + } - return next(); - }); + api.post('/treatments/', wares.bodyParser({ + limit: 1048576 * 50 + }), ctx.authorization.isPermitted('api:treatments:create'), post_response); + + /** + * @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); - 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; - } + console.log('treatments records deleted'); - 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) { - var data = req.body; - ctx.treatments.save(data, function(err, created) { - if (err) { - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); - console.log('Error saving treatment'); - console.log(err); - } else { - res.json(created); - console.log('Treatment saved', data); - } - }); - }); + return next(); + }); } - if (app.enabled('api') && app.enabled('careportal')) { - config_authed(app, api, wares, ctx); - } + 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) { + var data = req.body; + ctx.treatments.save(data, function(err, created) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + console.log('Error saving treatment'); + console.log(err); + } else { + res.json(created); + console.log('Treatment saved', data); + } + }); + }); + } - return api; + if (app.enabled('api') && app.enabled('careportal')) { + config_authed(app, api, wares, ctx); + } + + return api; } module.exports = configure; - diff --git a/lib/client/browser-settings.js b/lib/client/browser-settings.js index 05ffed1bdf6..d7782170b2a 100644 --- a/lib/client/browser-settings.js +++ b/lib/client/browser-settings.js @@ -79,6 +79,9 @@ function init (client, serverSettings, $) { var showPluginsSettings = $('#show-plugins'); var hasPluginsToShow = false; + + const pluginPrefs = []; + client.plugins.eachEnabledPlugin(function each (plugin) { if (client.plugins.specialPlugins.indexOf(plugin.name) > -1) { //ignore these, they are always on for now @@ -89,10 +92,48 @@ function init (client, serverSettings, $) { dd.find('input').prop('checked', settings.showPlugins.indexOf(plugin.name) > -1); hasPluginsToShow = true; } + + if (plugin.getClientPrefs) { + const prefs = plugin.getClientPrefs(); + pluginPrefs.push({ + plugin + , prefs + }) + } }); showPluginsSettings.toggle(hasPluginsToShow); + const bs = $('.browserSettings'); + const toggleCheckboxes = []; + + if (pluginPrefs.length > 0) { + pluginPrefs.forEach(function(e) { + // Only show settings if plugin is visible + if (settings.showPlugins.indexOf(e.plugin.name) > -1) { + const label = e.plugin.label; + const dl = $('
'); + dl.append(`
${label}
`); + e.prefs.forEach(function(p) { + const id = e.plugin.name + "-" + p.id; + const label = p.label; + if (p.type == 'boolean') { + const html = $(`
`); + dl.append(html); + if (storage.get(id) == true) { + toggleCheckboxes.push(id); + } + } + }); + bs.append(dl); + } + }); + } + + toggleCheckboxes.forEach(function(cb) { + $('#' + cb).prop('checked', true); + }); + $('#editprofilelink').toggle(settings.isEnabled('iob') || settings.isEnabled('cob') || settings.isEnabled('bwp') || settings.isEnabled('basal')); } @@ -116,6 +157,20 @@ function init (client, serverSettings, $) { return checkedPlugins.join(' '); } + client.plugins.eachEnabledPlugin(function each (plugin) { + if (plugin.getClientPrefs) { + const prefs = plugin.getClientPrefs(); + + prefs.forEach(function(p) { + const id = plugin.name + "-" + p.id; + if (p.type == 'boolean') { + const val = $("#" + id).prop('checked'); + storage.set(id, val); + } + }); + } + }); + function storeInBrowser (data) { Object.keys(data).forEach(k => { storage.set(k, data[k]); @@ -198,6 +253,7 @@ function init (client, serverSettings, $) { var stored = storage.get('basalrender'); settings.extendedSettings.basal.render = stored !== null ? stored : settings.extendedSettings.basal.render; + } catch (err) { console.error(err); showLocalstorageError(); @@ -208,6 +264,30 @@ function init (client, serverSettings, $) { wireForm(); }; + init.loadPluginSettings = function loadPluginSettings (client) { + + client.plugins.eachEnabledPlugin(function each (plugin) { + if (plugin.getClientPrefs) { + const prefs = plugin.getClientPrefs(); + + if (!settings.extendedSettings[plugin.name]) { + settings.extendedSettings[plugin.name] = {}; + } + + const settingsBase = settings.extendedSettings[plugin.name]; + + prefs.forEach(function(p) { + const id = plugin.name + "-" + p.id; + const stored = storage.get(id); + if (stored !== null) { + settingsBase[p.id] = stored; + } + }); + } + }); + + } + return settings; } diff --git a/lib/client/careportal.js b/lib/client/careportal.js index 0d08980a29f..7b39ce6d2c4 100644 --- a/lib/client/careportal.js +++ b/lib/client/careportal.js @@ -7,7 +7,7 @@ var times = require('../times'); var Storages = require('js-storage'); function init (client, $) { - var careportal = { }; + var careportal = {}; var translate = client.translate; var storage = Storages.localStorage; @@ -18,7 +18,7 @@ function init (client, $) { careportal.events = _.map(careportal.allEventTypes, function each (event) { return _.pick(event, ['val', 'name']); }); - + var eventTime = $('#eventTimeValue'); var eventDate = $('#eventDateValue'); @@ -28,11 +28,11 @@ function init (client, $) { eventDate.val(time.format('YYYY-MM-DD')); } - function mergeDateAndTime ( ) { + function mergeDateAndTime () { return client.utils.mergeInputTime(eventTime.val(), eventDate.val()); } - function updateTime(ele, time) { + function updateTime (ele, time) { ele.attr('oldminutes', time.minutes()); ele.attr('oldhours', time.hours()); } @@ -49,9 +49,9 @@ function init (client, $) { inputMatrix[event.val] = _.pick(event, ['bg', 'insulin', 'carbs', 'protein', 'fat', 'prebolus', 'duration', 'percent', 'absolute', 'profile', 'split', 'reasons', 'targets']); }); - careportal.filterInputs = function filterInputs ( event ) { + careportal.filterInputs = function filterInputs (event) { var eventType = $('#eventType').val(); - + function displayType (enabled) { if (enabled) { return ''; @@ -59,28 +59,28 @@ function init (client, $) { return 'none'; } } - - function resetIfHidden(visible, id) { + + function resetIfHidden (visible, id) { if (!visible) { $(id).val(''); } } var reasons = inputMatrix[eventType]['reasons']; - $('#reasonLabel').css('display',displayType(reasons && reasons.length > 0)); - $('#targets').css('display',displayType(inputMatrix[eventType]['targets'])); - - $('#bg').css('display',displayType(inputMatrix[eventType]['bg'])); - $('#insulinGivenLabel').css('display',displayType(inputMatrix[eventType]['insulin'])); - $('#carbsGivenLabel').css('display',displayType(inputMatrix[eventType]['carbs'])); - $('#proteinGivenLabel').css('display',displayType(inputMatrix[eventType]['protein'])); - $('#fatGivenLabel').css('display',displayType(inputMatrix[eventType]['fat'])); - $('#durationLabel').css('display',displayType(inputMatrix[eventType]['duration'])); - $('#percentLabel').css('display',displayType(inputMatrix[eventType]['percent'] && $('#absolute').val() === '')); - $('#absoluteLabel').css('display',displayType(inputMatrix[eventType]['absolute'] && $('#percent').val() === '')); - $('#profileLabel').css('display',displayType(inputMatrix[eventType]['profile'])); - $('#preBolusLabel').css('display',displayType(inputMatrix[eventType]['prebolus'])); - $('#insulinSplitLabel').css('display',displayType(inputMatrix[eventType]['split'])); + $('#reasonLabel').css('display', displayType(reasons && reasons.length > 0)); + $('#targets').css('display', displayType(inputMatrix[eventType]['targets'])); + + $('#bg').css('display', displayType(inputMatrix[eventType]['bg'])); + $('#insulinGivenLabel').css('display', displayType(inputMatrix[eventType]['insulin'])); + $('#carbsGivenLabel').css('display', displayType(inputMatrix[eventType]['carbs'])); + $('#proteinGivenLabel').css('display', displayType(inputMatrix[eventType]['protein'])); + $('#fatGivenLabel').css('display', displayType(inputMatrix[eventType]['fat'])); + $('#durationLabel').css('display', displayType(inputMatrix[eventType]['duration'])); + $('#percentLabel').css('display', displayType(inputMatrix[eventType]['percent'] && $('#absolute').val() === '')); + $('#absoluteLabel').css('display', displayType(inputMatrix[eventType]['absolute'] && $('#percent').val() === '')); + $('#profileLabel').css('display', displayType(inputMatrix[eventType]['profile'])); + $('#preBolusLabel').css('display', displayType(inputMatrix[eventType]['prebolus'])); + $('#insulinSplitLabel').css('display', displayType(inputMatrix[eventType]['split'])); $('#reason').empty(); _.each(reasons, function eachReason (reason) { @@ -103,7 +103,7 @@ function init (client, $) { maybePrevent(event); }; - careportal.reasonable = function reasonable ( ) { + careportal.reasonable = function reasonable () { var eventType = $('#eventType').val(); var reasons = inputMatrix[eventType]['reasons']; var selected = $('#reason').val(); @@ -133,10 +133,10 @@ function init (client, $) { } }; - careportal.prepareEvents = function prepareEvents ( ) { + careportal.prepareEvents = function prepareEvents () { $('#eventType').empty(); - _.each(careportal.events, function eachEvent(event) { - $('#eventType').append(''); + _.each(careportal.events, function eachEvent (event) { + $('#eventType').append(''); }); $('#eventType').change(careportal.filterInputs); $('#reason').change(careportal.reasonable); @@ -148,7 +148,7 @@ function init (client, $) { careportal.adjustSplit(); }; - careportal.adjustSplit = function adjustSplit(event) { + careportal.adjustSplit = function adjustSplit (event) { if ($(this).attr('id') === 'insulinSplitNow') { var nowval = parseInt($('#insulinSplitNow').val()) || 0; $('#insulinSplitExt').val(100 - nowval); @@ -158,12 +158,12 @@ function init (client, $) { $('#insulinSplitNow').val(100 - extval); $('#insulinSplitExt').val(extval); } - + maybePrevent(event); }; - - careportal.resolveEventName = function resolveEventName(value) { - _.each(careportal.events, function eachEvent(e) { + + careportal.resolveEventName = function resolveEventName (value) { + _.each(careportal.events, function eachEvent (e) { if (e.val === value) { value = e.name; } @@ -171,9 +171,9 @@ function init (client, $) { return value; }; - careportal.prepare = function prepare ( ) { + careportal.prepare = function prepare () { $('#profile').empty(); - client.profilefunctions.listBasalProfiles().forEach(function (p) { + client.profilefunctions.listBasalProfiles().forEach(function(p) { $('#profile').append(''); }); careportal.prepareEvents(); @@ -195,31 +195,31 @@ function init (client, $) { setDateAndTime(); }; - function gatherData ( ) { + function gatherData () { var data = { enteredBy: $('#enteredBy').val() - , eventType: $('#eventType').val() - , glucose: $('#glucoseValue').val().replace(',','.') - , reason: $('#reason').val() - , targetTop: $('#targetTop').val().replace(',','.') - , targetBottom: $('#targetBottom').val().replace(',','.') - , glucoseType: $('#treatment-form').find('input[name=glucoseType]:checked').val() - , carbs: $('#carbsGiven').val() - , protein: $('#proteinGiven').val() - , fat: $('#fatGiven').val() - , insulin: $('#insulinGiven').val() - , duration: times.msecs(parse_duration($('#duration').val())).mins < 1 ? $('#duration').val() : times.msecs(parse_duration($('#duration').val())).mins - , percent: $('#percent').val() - , profile: $('#profile').val() - , preBolus: parseInt($('#preBolus').val()) - , notes: $('#notes').val() - , units: client.settings.units + , eventType: $('#eventType').val() + , glucose: $('#glucoseValue').val().replace(',', '.') + , reason: $('#reason').val() + , targetTop: $('#targetTop').val().replace(',', '.') + , targetBottom: $('#targetBottom').val().replace(',', '.') + , glucoseType: $('#treatment-form').find('input[name=glucoseType]:checked').val() + , carbs: $('#carbsGiven').val() + , protein: $('#proteinGiven').val() + , fat: $('#fatGiven').val() + , insulin: $('#insulinGiven').val() + , duration: times.msecs(parse_duration($('#duration').val())).mins < 1 ? $('#duration').val() : times.msecs(parse_duration($('#duration').val())).mins + , percent: $('#percent').val() + , profile: $('#profile').val() + , preBolus: parseInt($('#preBolus').val()) + , notes: $('#notes').val() + , units: client.settings.units }; - if (units == "mmol") { - data.targetTop = data.targetTop * 18; - data.targetBottom = data.targetBottom * 18; - } + if (units == "mmol") { + data.targetTop = data.targetTop * 18; + data.targetBottom = data.targetBottom * 18; + } //special handling for absolute to support temp to 0 var absolute = $('#absolute').val(); @@ -230,7 +230,7 @@ function init (client, $) { if ($('#othertime').is(':checked')) { data.eventTime = mergeDateAndTime().toDate(); } - + if (!inputMatrix[data.eventType].profile) { delete data.profile; } @@ -258,7 +258,59 @@ function init (client, $) { maybePrevent(event); }; - function buildConfirmText(data) { + function validateData (data) { + + let allOk = true; + let messages = []; + + console.log('Validating careportal entry: ', data.eventType); + + if (data.eventType == 'Temporary Target') { + if (isNaN(data.targetTop) || isNaN(data.targetBottom) || !data.targetBottom || !data.targetTop) { + console.log('Bottom or Top target missing'); + allOk = false; + messages.push("Please enter a valid value for both top and bottom target to save a Temporary Target"); + } else { + + let targetTop = data.targetTop; + let targetBottom = data.targetBottom; + + let minTarget = 4 * 18; + let maxTarget = 18 * 18; + + if (units == "mmol") { + targetTop = Math.round(targetTop / 18.0 * 10) / 10; + targetBottom = Math.round(targetBottom / 18.0 * 10) / 10; + minTarget = Math.round(minTarget / 18.0 * 10) / 10; + maxTarget = Math.round(maxTarget / 18.0 * 10) / 10; + } + + if (targetTop > maxTarget) { + allOk = false; + messages.push("Temporary target high is too high"); + } + + if (targetBottom < minTarget) { + allOk = false; + messages.push("Temporary target low is too low"); + } + + if (targetTop < targetBottom || targetBottom > targetTop) { + allOk = false; + messages.push("The low target must be lower than the high target and high target must be higher than the low target."); + } + + } + } + + return { + allOk + , messages + }; + + } + + function buildConfirmText (data) { var text = [ translate('Please verify that the data entered is correct') + ': ' , translate('Event Type') + ': ' + translate(careportal.resolveEventName(data.eventType)) @@ -271,22 +323,22 @@ function init (client, $) { } if (data.duration === 0 && data.eventType === 'Temporary Target') { - text[text.length - 1] += ' ' + translate('Cancel'); + text[text.length - 1] += ' ' + translate('Cancel'); } pushIf(data.glucose, translate('Blood Glucose') + ': ' + data.glucose); pushIf(data.glucose, translate('Measurement Method') + ': ' + translate(data.glucoseType)); pushIf(data.reason, translate('Reason') + ': ' + data.reason); - + var targetTop = data.targetTop; var targetBottom = data.targetBottom; - + if (units == "mmol") { - targetTop = Math.round(data.targetTop / 18.0 * 10) / 10; - targetBottom = Math.round(data.targetBottom / 18.0 * 10) / 10; - } - + targetTop = Math.round(data.targetTop / 18.0 * 10) / 10; + targetBottom = Math.round(data.targetBottom / 18.0 * 10) / 10; + } + pushIf(data.targetTop, translate('Target Top') + ': ' + targetTop); pushIf(data.targetBottom, translate('Target Bottom') + ': ' + targetBottom); @@ -307,13 +359,27 @@ function init (client, $) { return text.join('\n'); } - function confirmPost(data) { - if (window.confirm(buildConfirmText(data))) { - postTreatment(data); + function confirmPost (data) { + + const validation = validateData(data); + + if (!validation.allOk) { + + let messages = ""; + + validation.messages.forEach(function(m) { + messages += translate(m) + "\n"; + }); + + window.alert(messages); + } else { + if (window.confirm(buildConfirmText(data))) { + postTreatment(data); + } } } - function postTreatment(data) { + function postTreatment (data) { if (data.eventType === 'Combo Bolus') { data.enteredinsulin = data.insulin; data.insulin = data.enteredinsulin * data.splitNow / 100; @@ -322,9 +388,9 @@ function init (client, $) { $.ajax({ method: 'POST' - , url: '/api/v1/treatments/' - , headers: client.headers() - , data: data + , url: '/api/v1/treatments/' + , headers: client.headers() + , data: data }).done(function treatmentSaved (response) { console.info('treatment saved', response); }).fail(function treatmentSaveFail (response) { @@ -336,7 +402,7 @@ function init (client, $) { client.browserUtils.closeDrawer('#treatmentDrawer'); } - + careportal.dateTimeFocus = function dateTimeFocus (event) { $('#othertime').prop('checked', true); updateTime($(this), mergeDateAndTime()); @@ -384,4 +450,3 @@ function init (client, $) { } module.exports = init; - diff --git a/lib/client/clock-client.js b/lib/client/clock-client.js index ee067086169..2b4c063ff9f 100644 --- a/lib/client/clock-client.js +++ b/lib/client/clock-client.js @@ -65,6 +65,13 @@ client.render = function render (xhr) { if (m < 10) m = "0" + m; $('#clock').text(h + ":" + m); + var queryDict = {}; + location.search.substr(1).split("&").forEach(function(item) { queryDict[item.split("=")[0]] = item.split("=")[1] }); + + if (!window.serverSettings.settings.showClockClosebutton || !queryDict['showClockClosebutton']) { + $('#close').css('display', 'none'); + } + // defined in the template this is loaded into // eslint-disable-next-line no-undef if (clockFace == 'clock-color') { @@ -82,6 +89,11 @@ client.render = function render (xhr) { var green = 'rgba(134,207,70,1)'; var blue = 'rgba(78,143,207,1)'; + var darkRed = 'rgba(183,9,21,1)'; + var darkYellow = 'rgba(214,168,0,1)'; + var darkGreen = 'rgba(110,192,70,1)'; + var darkBlue = 'rgba(78,143,187,1)'; + var elapsedMins = Math.round(((now - last) / 1000) / 60); // Insert the BG stale time text. @@ -90,18 +102,28 @@ client.render = function render (xhr) { // Threshold background coloring. if (bgNum < bgLow) { $('body').css('background-color', red); + $('#close').css('border-color', darkRed); + $('#close').css('color', darkRed); } if ((bgLow <= bgNum) && (bgNum < bgTargetBottom)) { $('body').css('background-color', blue); + $('#close').css('border-color', darkBlue); + $('#close').css('color', darkBlue); } if ((bgTargetBottom <= bgNum) && (bgNum < bgTargetTop)) { $('body').css('background-color', green); + $('#close').css('border-color', darkGreen); + $('#close').css('color', darkGreen); } if ((bgTargetTop <= bgNum) && (bgNum < bgHigh)) { $('body').css('background-color', yellow); + $('#close').css('border-color', darkYellow); + $('#close').css('color', darkYellow); } if (bgNum >= bgHigh) { $('body').css('background-color', red); + $('#close').css('border-color', darkRed); + $('#close').css('color', darkRed); } // Restyle body bg, and make the "x minutes ago" visible too. @@ -113,6 +135,7 @@ client.render = function render (xhr) { } else { $('#staleTime').css('display', 'none'); $('body').css('color', 'white'); + $('#arrow').css('filter', 'brightness(100%)'); } } } diff --git a/lib/client/index.js b/lib/client/index.js index 311a49ce8cb..63b37d7c9ed 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -143,9 +143,12 @@ client.load = function load (serverSettings, callback) { client.plugins = require('../plugins/')({ settings: client.settings + , extendedSettings: client.settings.extendedSettings , language: language }).registerClientDefaults(); + browserSettings.loadPluginSettings(client); + client.utils = require('../utils')({ settings: client.settings , language: language diff --git a/lib/data/dataloader.js b/lib/data/dataloader.js index c9c24b2dd08..cc4426aa6ab 100644 --- a/lib/data/dataloader.js +++ b/lib/data/dataloader.js @@ -297,6 +297,7 @@ function loadSensorAndInsulinTreatments(ddata, ctx, callback) { async.parallel([ loadLatestSingle.bind(null, ddata, ctx, 'Sensor Start') ,loadLatestSingle.bind(null, ddata, ctx, 'Sensor Change') + ,loadLatestSingle.bind(null, ddata, ctx, 'Sensor Stop') ,loadLatestSingle.bind(null, ddata, ctx, 'Site Change') ,loadLatestSingle.bind(null, ddata, ctx, 'Insulin Change') ,loadLatestSingle.bind(null, ddata, ctx, 'Pump Battery Change') diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js index e3deca2c099..27b7a31ae95 100644 --- a/lib/plugins/openaps.js +++ b/lib/plugins/openaps.js @@ -17,6 +17,14 @@ function init (ctx) { var translate = ctx.language.translate; var firstPrefs = true; + openaps.getClientPrefs = function getClientPrefs() { + return ([{ + label: "Color prediction lines", + id: "colorPredictionLines", + type: "boolean" + }]); + } + openaps.getPrefs = function getPrefs (sbx) { function cleanList (value) { @@ -27,23 +35,34 @@ function init (ctx) { return _.isEmpty(list) || _.isEmpty(list[0]); } - var fields = cleanList(sbx.extendedSettings.fields); + const settings = sbx.extendedSettings || {}; + + var fields = cleanList(settings.fields); fields = isEmpty(fields) ? ['status-symbol', 'status-label', 'iob', 'meal-assist', 'rssi'] : fields; - var retroFields = cleanList(sbx.extendedSettings.retroFields); + var retroFields = cleanList(settings.retroFields); retroFields = isEmpty(retroFields) ? ['status-symbol', 'status-label', 'iob', 'meal-assist', 'rssi'] : retroFields; + if (typeof settings.colorPredictionLines == 'undefined') { + settings.colorPredictionLines = true; + } + var prefs = { fields: fields , retroFields: retroFields - , warn: sbx.extendedSettings.warn ? sbx.extendedSettings.warn : 30 - , urgent: sbx.extendedSettings.urgent ? sbx.extendedSettings.urgent : 60 - , enableAlerts: sbx.extendedSettings.enableAlerts + , warn: settings.warn ? settings.warn : 30 + , urgent: settings.urgent ? settings.urgent : 60 + , enableAlerts: settings.enableAlerts + , predIOBColor: settings.predIobColor ? settings.predIobColor : '#1e88e5' + , predCOBColor: settings.predCobColor ? settings.predCobColor : '#FB8C00FF' + , predACOBColor: settings.predAcobColor ? settings.predAcobColor : '#FB8C0080' + , predZTColor: settings.predZtColor ? settings.predZtColor : '#00d2d2' + , predUAMColor: settings.predUamColor ? settings.predUamColor : '#c9bd60' + , colorPredictionLines: settings.colorPredictionLines }; if (firstPrefs) { firstPrefs = false; - console.info('OpenAPS Prefs:', prefs); } return prefs; @@ -365,9 +384,18 @@ function init (ctx) { function toPoints (offset, forecastType) { return function toPoint (value, index) { + var colors = { + 'Values': '#ff00ff' + , 'IOB': prefs.predIOBColor + , 'Zero-Temp': prefs.predZTColor + , 'COB': prefs.predCOBColor + , 'Accel-COB': prefs.predACOBColor + , 'UAM': prefs.predUAMColor + } + return { mgdl: value - , color: '#ff00ff' + , color: prefs.colorPredictionLines ? colors[forecastType] : '#ff00ff' , mills: prop.lastPredBGs.moment.valueOf() + times.mins(5 * index).msecs + offset , noFade: true , forecastType: forecastType diff --git a/lib/report_plugins/daytoday.js b/lib/report_plugins/daytoday.js index 28eff349fd5..3445973402c 100644 --- a/lib/report_plugins/daytoday.js +++ b/lib/report_plugins/daytoday.js @@ -738,7 +738,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio var label = ' ' + treatment.carbs + ' g'; if (treatment.protein) label += ' / ' + treatment.protein + ' g'; if (treatment.fat) label += ' / ' + treatment.fat + ' g'; - label += ' (' + (treatment.carbs / ic).toFixed(2) + 'U)'; + label += ' (' + client.utils.toFixedMin((treatment.carbs / ic), 2) + 'U)'; context.append('rect') .attr('y', yCarbsScale(treatment.carbs)) @@ -758,6 +758,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio } if (treatment.insulin && options.insulin) { + var dataLabel = client.utils.toFixedMin(treatment.insulin,2)+ 'U'; context.append('rect') .attr('y', yInsulinScale(treatment.insulin)) .attr('height', chartHeight - yInsulinScale(treatment.insulin)) @@ -773,7 +774,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio //.attr('y', yInsulinScale(treatment.insulin)-10) .attr('transform', 'rotate(-45,' + (xScale2(treatment.mills) + padding.left - 2) + ',' + (padding.top + yInsulinScale(treatment.insulin)) + ')' + 'translate(' + (xScale2(treatment.mills) + padding.left + 10) + ',' + (padding.top + yInsulinScale(treatment.insulin)) + ')') - .text(Number(treatment.insulin).toFixed(2) + 'U'); + .text(dataLabel); } // process the rest diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js index 0fc988d8f2c..9e53c4f77ac 100644 --- a/lib/server/bootevent.js +++ b/lib/server/bootevent.js @@ -16,28 +16,28 @@ function boot (env, language) { // < 8 does not work, not supported // >= 8.15.1 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.2 works, supported and recommended - // >= 11.12.0 does work, not recommended, will not be supported. We only support Node LTS releases + // == 10.15.2 works, not fully supported and not recommended (Azure version) + // >= 10.16.0 works, supported and recommended + // == 11.x does not work, not supported + // >= 12.6.0 does work, not recommended, will not be supported. We only support Node LTS releases /////////////////////////////////////////////////// function checkNodeVersion (ctx, next) { var semver = require('semver'); var nodeVersion = process.version; - if ( semver.satisfies(nodeVersion, '^8.15.1') || semver.satisfies(nodeVersion, '^10.15.2')) { + if ( semver.satisfies(nodeVersion, '^8.15.1') || semver.satisfies(nodeVersion, '^10.16.0')) { //Latest Node 8 LTS and Latest Node 10 LTS are recommended and supported. //Require at least Node 8 LTS and Node 10 LTS without known security issues console.debug('Node LTS version ' + nodeVersion + ' is supported'); next(); } - else if ( semver.eq(nodeVersion, '10.14.1')) { + else if ( semver.eq(nodeVersion, '10.15.2')) { //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: Node version v10.15.2 and Microsoft Azure are not recommended.'); console.log('WARNING: Please migrate to another hosting provider. Your Node version is outdated and insecure'); next(); } - else if ( semver.satisfies(nodeVersion, '^11.12.0')) { + else if ( semver.satisfies(nodeVersion, '^12.6.0')) { //Latest Node version console.debug('Node version ' + nodeVersion + ' is not a LTS version. Not recommended. Not supported'); next(); diff --git a/lib/server/query.js b/lib/server/query.js index 557b176a4df..8279d5ad1e8 100644 --- a/lib/server/query.js +++ b/lib/server/query.js @@ -1,9 +1,10 @@ 'use strict'; -var traverse = require('traverse'); -var ObjectID = require('mongodb').ObjectID; +const traverse = require('traverse'); +const ObjectID = require('mongodb').ObjectID; +const moment = require('moment'); -var TWO_DAYS = 172800000; +const TWO_DAYS = 172800000; /** * @module query utilities * Assist in translating objects from query-string representation into @@ -62,7 +63,16 @@ function enforceDateFilter (query, opts) { let dateString = dateValue[key]; if (isNaN(dateString)) { dateString = dateString.replace(' ', '+'); // some clients don't excape the plus - dateValue[key] = new Date(dateString).toISOString(); + + const validDate = moment(dateString).isValid(); + + if (!validDate) { + console.error('API request using an invalid date:', dateString); + throw new Error('Cannot parse ' + dateString + ' as a valid ISO-8601 date'); + } + + const d = moment.parseZone(dateString); + dateValue[key] = d.toISOString(); } }); } diff --git a/lib/settings.js b/lib/settings.js index e4d15f99f63..2d16abd0f2d 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -3,7 +3,7 @@ var _ = require('lodash'); var levels = require('./levels'); -function init ( ) { +function init () { var settings = { units: 'mg/dL' @@ -41,12 +41,14 @@ function init ( ) { , bgTargetTop: 180 , bgTargetBottom: 80 , bgLow: 55 - }, - insecureUseHttp: false, - secureHstsHeader: true, - secureHstsHeaderIncludeSubdomains: false, - secureHstsHeaderPreload: false, - secureCsp: false + } + , insecureUseHttp: false + , secureHstsHeader: true + , secureHstsHeaderIncludeSubdomains: false + , secureHstsHeaderPreload: false + , secureCsp: false + , showClockClosebutton: true + , deNormalizeDates: false }; var valueMappers = { @@ -67,7 +69,8 @@ function init ( ) { , insecureUseHttp: mapTruthy , secureHstsHeader: mapTruthy , secureCsp: mapTruthy - + , showClockClosebutton: mapTruthy + , deNormalizeDates: mapTruthy }; function mapNumberArray (value) { @@ -77,7 +80,7 @@ function init ( ) { if (isNaN(value)) { var rawValues = value && value.split(' ') || []; - return _.map(rawValues, function (num) { + return _.map(rawValues, function(num) { return isNaN(num) ? null : Number(num); }); } else { @@ -153,18 +156,18 @@ function init ( ) { } function anyEnabled (features) { - return _.findIndex(features, function (feature) { + return _.findIndex(features, function(feature) { return enable.indexOf(feature) > -1; }) > -1; } - function prepareAlarmTypes ( ) { + function prepareAlarmTypes () { var alarmTypes = _.filter(getAndPrepare('alarmTypes'), function onlyKnownTypes (type) { return type === 'predict' || type === 'simple'; }); if (alarmTypes.length === 0) { - var thresholdWasSet = _.findIndex(wasSet, function (name) { + var thresholdWasSet = _.findIndex(wasSet, function(name) { return name.indexOf('bg') === 0; }) > -1; alarmTypes = thresholdWasSet ? ['simple'] : ['predict']; @@ -209,7 +212,7 @@ function init ( ) { adjustShownPlugins(); } - function verifyThresholds() { + function verifyThresholds () { var thresholds = settings.thresholds; if (thresholds.bgTargetBottom >= thresholds.bgTargetTop) { @@ -234,7 +237,7 @@ function init ( ) { } } - function adjustShownPlugins ( ) { + function adjustShownPlugins () { var showPluginsUnset = settings.showPlugins && 0 === settings.showPlugins.length; settings.showPlugins += ' delta direction upbat'; @@ -245,7 +248,7 @@ function init ( ) { if (showPluginsUnset) { //assume all enabled features are plugins and they should be shown for now //it would be better to use the registered plugins, but it's not loaded yet... - _.forEach(settings.enable, function showFeature(feature) { + _.forEach(settings.enable, function showFeature (feature) { if (isEnabled(feature)) { settings.showPlugins += ' ' + feature; } @@ -289,7 +292,7 @@ function init ( ) { var snoozeTime; if (notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh) { - snoozeTime = settings.alarmUrgentHighMins; + snoozeTime = settings.alarmUrgentHighMins; } else if (notify.eventName === 'high' && settings.alarmHigh) { snoozeTime = settings.alarmHighMins; } else if (notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow) { diff --git a/lib/utils.js b/lib/utils.js index 1d407ccda05..fe1778f8120 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -26,7 +26,7 @@ function init(ctx) { }; utils.toFixed = function toFixed(value) { - if (value === undefined || value === 0) { + if (!value) { return '0'; } else { var fixed = value.toFixed(2); @@ -34,6 +34,15 @@ function init(ctx) { } }; + utils.toFixedMin = function toFixedMin(value,digits) { + if (!value) { + return '0'; + } + var mult = Math.pow(10,digits); + var fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult + return String(fixed); + }; + // some helpers for input "date" utils.mergeInputTime = function mergeInputTime(timestring, datestring) { return moment(datestring + ' ' + timestring, 'YYYY-MM-D HH:mm'); @@ -70,4 +79,4 @@ function init(ctx) { return utils; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 18cca5119e9..f54cc8fa3b3 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.12.1", + "version": "0.12.3-dev", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3087,7 +3087,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3105,11 +3106,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3122,15 +3125,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3233,7 +3239,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3243,6 +3250,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3255,17 +3263,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3282,6 +3293,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3354,7 +3366,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3364,6 +3377,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3439,7 +3453,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3469,6 +3484,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3486,6 +3502,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3524,11 +3541,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -3819,6 +3838,14 @@ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, + "heapdump": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.14.tgz", + "integrity": "sha512-omSf+s4yDxHWsh8uNdzhDQFi9tkuBEaxn3pUnHetcEB4XD5L/bWDAFmtnSHP2HmtfnYqkaK6Y76TbhfSOPP7/A==", + "requires": { + "nan": "^2.13.2" + } + }, "helmet": { "version": "3.18.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.18.0.tgz", @@ -7712,8 +7739,7 @@ "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "optional": true + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "nanomatch": { "version": "1.2.13", @@ -7939,7 +7965,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7960,12 +7987,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7980,17 +8009,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -8107,7 +8139,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -8119,6 +8152,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8133,6 +8167,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8140,12 +8175,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -8164,6 +8201,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -8244,7 +8282,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -8256,6 +8295,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -8341,7 +8381,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -8377,6 +8418,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8396,6 +8438,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8439,12 +8482,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -10854,6 +10899,14 @@ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.22.1.tgz", "integrity": "sha512-KITbEqXkXrjGH12A0lpVZlH3uODFkwUh8d15My1YD4N0PSZDnIiC1iMFT6ryyuJxDYWZh0qezKpPqa5FRowngw==" }, + "swagger-ui-express": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.0.7.tgz", + "integrity": "sha512-ipXe53qDMjB2GlFcWARof15fMxX0n0wkwUturBpdovfJLaqod3WAqimwQGFXjwpWKA6hnxEPrd31yOzaYkP++A==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", diff --git a/package.json b/package.json index d62b8397336..5adf81960af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.12.2", + "version": "0.12.3", "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", @@ -53,7 +53,7 @@ } }, "engines": { - "node": "^10.16.0 || ^8.15.1", + "node": "^10.15.2 || ^8.15.1", "npm": "^6.4.1" }, "dependencies": { @@ -73,6 +73,7 @@ "express-minify": "^1.0.0", "file-loader": "^3.0.1", "flot": "^0.8.3", + "heapdump": "^0.3.14", "helmet": "^3.16.0", "jquery": "^3.3.1", "jquery-ui-bundle": "^1.12.1-migrate", @@ -102,6 +103,7 @@ "socket.io": "~2.1.1", "style-loader": "^0.23.1", "swagger-ui-dist": "^3.22.0", + "swagger-ui-express": "^4.0.7", "terser": "^3.17.0", "traverse": "^0.6.6", "webpack": "^4.29.6", diff --git a/swagger.json b/swagger.json index 512ed9515e8..1daf2c45a38 100755 --- a/swagger.json +++ b/swagger.json @@ -8,7 +8,7 @@ "info": { "title": "Nightscout API", "description": "Own your DData with the Nightscout API", - "version": "0.12.2", + "version": "0.12.3", "license": { "name": "AGPL 3", "url": "https://www.gnu.org/licenses/agpl.txt" @@ -907,7 +907,7 @@ }, "dateString": { "type": "string", - "description": "dateString, prefer ISO `8601`" + "description": "dateString, MUST be ISO `8601` format date parseable by Javascript Date()" }, "date": { "type": "number", diff --git a/swagger.yaml b/swagger.yaml index c7e47b4e7b1..4591239d8d8 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -4,7 +4,7 @@ servers: info: title: Nightscout API description: Own your DData with the Nightscout API - version: 0.12.2 + version: 0.12.3 license: name: AGPL 3 url: 'https://www.gnu.org/licenses/agpl.txt' @@ -679,7 +679,7 @@ components: description: 'sgv, mbg, cal, etc' dateString: type: string - description: 'dateString, prefer ISO `8601`' + description: 'dateString, MUST be ISO `8601` format date parseable by Javascript Date()' date: type: number description: Epoch @@ -723,7 +723,7 @@ components: description: 'Device type and hostname for example openaps://hostname' created_at: type: string - description: 'dateString, prefer ISO `8601`' + description: 'dateString, MUST be ISO `8601` format date parseable by Javascript Date()' openaps: type: string description: 'OpenAPS devicestatus record - TODO: Fill Out Details' @@ -744,7 +744,7 @@ components: properties: clock: type: string - description: 'dateString, prefer ISO `8601`' + description: 'dateString, MUST be ISO `8601` format date parseable by Javascript Date()' battery: $ref: '#/components/schemas/pumpbattery' reservoir: @@ -773,7 +773,7 @@ components: description: Is Pump Suspended timestamp: type: string - description: 'dateString, prefer ISO `8601`' + description: 'dateString, MUST be ISO `8601` format date parseable by Javascript Date()' uploader: properties: batteryVoltage: diff --git a/tests/api.entries.test.js b/tests/api.entries.test.js index 6f70c81593c..098b5c45663 100644 --- a/tests/api.entries.test.js +++ b/tests/api.entries.test.js @@ -19,7 +19,7 @@ describe('Entries REST api', function ( ) { 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.app.use('/', entries(self.app, self.wares, ctx, self.env)); self.archive = require('../lib/server/entries')(self.env, ctx); var creating = load('json'); diff --git a/tests/api.unauthorized.test.js b/tests/api.unauthorized.test.js index 8c804c4244f..5785069f77e 100644 --- a/tests/api.unauthorized.test.js +++ b/tests/api.unauthorized.test.js @@ -21,7 +21,7 @@ describe('authed REST api', function ( ) { var self = this; self.known_key = known; require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { - self.app.use('/', entries(self.app, self.wares, ctx)); + self.app.use('/', entries(self.app, self.wares, ctx, env)); self.archive = require('../lib/server/entries')(env, ctx); var creating = load('json'); diff --git a/tests/timeago.test.js b/tests/timeago.test.js index e47e9873d00..7b4a718ccd0 100644 --- a/tests/timeago.test.js +++ b/tests/timeago.test.js @@ -2,7 +2,7 @@ var should = require('should'); var levels = require('../lib/levels'); var times = require('../lib/times'); -describe('timeago', function ( ) { +describe('timeago', function() { var ctx = {}; ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); @@ -12,16 +12,16 @@ describe('timeago', function ( ) { var env = require('../env')(); - function freshSBX() { + function freshSBX () { //set extendedSettings right before calling withExtendedSettings, there's some strange test interference here - env.extendedSettings = {timeago: {enableAlerts: true}}; + env.extendedSettings = { timeago: { enableAlerts: true } }; var sbx = require('../lib/sandbox')().serverInit(env, ctx).withExtendedSettings(timeago); return sbx; } - it('Not trigger an alarm when data is current', function (done) { + it('Not trigger an alarm when data is current', function(done) { ctx.notifications.initRequests(); - ctx.ddata.sgvs = [{mills: Date.now(), mgdl: 100, type: 'sgv'}]; + ctx.ddata.sgvs = [{ mills: Date.now(), mgdl: 100, type: 'sgv' }]; var sbx = freshSBX(); timeago.checkNotifications(sbx); @@ -30,9 +30,9 @@ describe('timeago', function ( ) { done(); }); - it('Not trigger an alarm with future data', function (done) { + it('Not trigger an alarm with future data', function(done) { ctx.notifications.initRequests(); - ctx.ddata.sgvs = [{mills: Date.now() + times.mins(15).msecs, mgdl: 100, type: 'sgv'}]; + ctx.ddata.sgvs = [{ mills: Date.now() + times.mins(15).msecs, mgdl: 100, type: 'sgv' }]; var sbx = freshSBX(); timeago.checkNotifications(sbx); @@ -41,21 +41,27 @@ describe('timeago', function ( ) { done(); }); - it('should trigger a warning when data older than 15m', function (done) { + it('should trigger a warning when data older than 15m', function(done) { ctx.notifications.initRequests(); - ctx.ddata.sgvs = [{mills: Date.now() - times.mins(16).msecs, mgdl: 100, type: 'sgv'}]; + ctx.ddata.sgvs = [{ mills: Date.now() - times.mins(16).msecs, mgdl: 100, type: 'sgv' }]; var sbx = freshSBX(); timeago.checkNotifications(sbx); + + var currentTime = new Date().getTime(); + + // eslint-disable-next-line no-empty + while (currentTime + 500 >= new Date().getTime()) {} + var highest = ctx.notifications.findHighestAlarm('Time Ago'); highest.level.should.equal(levels.WARN); highest.message.should.equal('Last received: 16 mins ago\nBG Now: 100 mg/dl'); done(); }); - it('should trigger an urgent alarm when data older than 30m', function (done) { + it('should trigger an urgent alarm when data older than 30m', function(done) { ctx.notifications.initRequests(); - ctx.ddata.sgvs = [{mills: Date.now() - times.mins(31).msecs, mgdl: 100, type: 'sgv'}]; + ctx.ddata.sgvs = [{ mills: Date.now() - times.mins(31).msecs, mgdl: 100, type: 'sgv' }]; var sbx = freshSBX(); timeago.checkNotifications(sbx); @@ -70,55 +76,55 @@ describe('timeago', function ( ) { should.deepEqual( timeago.calcDisplay({ mills: now + times.mins(15).msecs }, now) - , {label: 'in the future', shortLabel: 'future'} + , { label: 'in the future', shortLabel: 'future' } ); //TODO: current behavior, we can do better //just a little in the future, pretend it's ok should.deepEqual( timeago.calcDisplay({ mills: now + times.mins(4).msecs }, now) - , {value: 1, label: 'min ago', shortLabel: 'm'} + , { value: 1, label: 'min ago', shortLabel: 'm' } ); should.deepEqual( timeago.calcDisplay(null, now) - , {label: 'time ago', shortLabel: 'ago'} + , { label: 'time ago', shortLabel: 'ago' } ); should.deepEqual( timeago.calcDisplay({ mills: now }, now) - , {value: 1, label: 'min ago', shortLabel: 'm'} + , { value: 1, label: 'min ago', shortLabel: 'm' } ); should.deepEqual( timeago.calcDisplay({ mills: now - 1 }, now) - , {value: 1, label: 'min ago', shortLabel: 'm'} + , { value: 1, label: 'min ago', shortLabel: 'm' } ); should.deepEqual( timeago.calcDisplay({ mills: now - times.sec(30).msecs }, now) - , {value: 1, label: 'min ago', shortLabel: 'm'} + , { value: 1, label: 'min ago', shortLabel: 'm' } ); should.deepEqual( timeago.calcDisplay({ mills: now - times.mins(30).msecs }, now) - , {value: 30, label: 'mins ago', shortLabel: 'm'} + , { value: 30, label: 'mins ago', shortLabel: 'm' } ); should.deepEqual( timeago.calcDisplay({ mills: now - times.hours(5).msecs }, now) - , {value: 5, label: 'hours ago', shortLabel: 'h'} + , { value: 5, label: 'hours ago', shortLabel: 'h' } ); should.deepEqual( timeago.calcDisplay({ mills: now - times.days(5).msecs }, now) - , {value: 5, label: 'days ago', shortLabel: 'd'} + , { value: 5, label: 'days ago', shortLabel: 'd' } ); should.deepEqual( timeago.calcDisplay({ mills: now - times.days(10).msecs }, now) - , {label: 'long ago', shortLabel: 'ago'} + , { label: 'long ago', shortLabel: 'ago' } ); }); -}); \ No newline at end of file +}); diff --git a/tests/utils.test.js b/tests/utils.test.js index be0b298290d..53cccf07507 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -15,6 +15,22 @@ describe('utils', function ( ) { utils.toFixed(5.499999999).should.equal('5.50'); }); + it('format numbers short', function () { + var undef; + utils.toFixedMin(3.345, 2).should.equal('3.35'); + utils.toFixedMin(5.499999999, 0).should.equal('5'); + utils.toFixedMin(5.499999999, 1).should.equal('5.5'); + utils.toFixedMin(5.499999999, 3).should.equal('5.5'); + utils.toFixedMin(123.45, -2).should.equal('100'); + utils.toFixedMin(-0.001, 2).should.equal('0'); + utils.toFixedMin(-2.47, 1).should.equal('-2.5'); + utils.toFixedMin(-2.44, 1).should.equal('-2.4'); + + utils.toFixedMin(undef, 2).should.equal('0'); + utils.toFixedMin(null, 2).should.equal('0'); + utils.toFixedMin('text', 2).should.equal('0'); + }); + it('merge date and time', function () { var result = utils.mergeInputTime('22:35', '2015-07-14'); result.hours().should.equal(22); diff --git a/views/clockviews/bgclock.css b/views/clockviews/bgclock.css index 5398a1bc9cc..6f97406a883 100644 --- a/views/clockviews/bgclock.css +++ b/views/clockviews/bgclock.css @@ -20,6 +20,18 @@ main { height: 100vh; } +#close { + position: absolute; + top: 10px; + right: 10px; + color: #333; + border-radius: 5px; + border: 2px solid #333; + padding: 5px; + width: 20px; + height: 20px; +} + .inner { width: 100%; -webkit-transform: translateY(-2%); diff --git a/views/clockviews/clock-color.css b/views/clockviews/clock-color.css index b5860736e8a..3777a86a119 100644 --- a/views/clockviews/clock-color.css +++ b/views/clockviews/clock-color.css @@ -20,6 +20,18 @@ main { height: 100vh; } +#close { + position: absolute; + top: 10px; + right: 10px; + color: grey; + border-radius: 5px; + border: 2px solid grey; + padding: 5px; + width: 20px; + height: 20px; +} + .inner { width: 100%; -webkit-transform: translateY(-5%); diff --git a/views/clockviews/clock.css b/views/clockviews/clock.css index 0b136ef518f..75b1b0cbafa 100644 --- a/views/clockviews/clock.css +++ b/views/clockviews/clock.css @@ -20,6 +20,18 @@ main { height: 100vh; } +#close { + position: absolute; + top: 10px; + right: 10px; + color: #333; + border-radius: 5px; + border: 2px solid #333; + padding: 5px; + width: 20px; + height: 20px; +} + .inner { width: 100%; -webkit-transform: translateY(-5%); @@ -51,4 +63,4 @@ main { #clock { display: none; -} \ No newline at end of file +} diff --git a/views/clockviews/shared.html b/views/clockviews/shared.html index 617ca9c9e1b..f7d5f78cd25 100644 --- a/views/clockviews/shared.html +++ b/views/clockviews/shared.html @@ -25,7 +25,7 @@ -
X
+
X
diff --git a/views/index.html b/views/index.html index 82aa12e8879..cf13cdb91a7 100644 --- a/views/index.html +++ b/views/index.html @@ -116,7 +116,7 @@
-