diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 23cc61e5ef7..3c9fc778c1b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,6 @@ name: CI test -on: [push] +on: [push, pull_request] jobs: build: @@ -28,5 +28,7 @@ jobs: sudo apt-get install -y --allow-downgrades mongodb-org=3.6.14 mongodb-org-server=3.6.14 mongodb-org-shell=3.6.14 mongodb-org-mongos=3.6.14 mongodb-org-tools=3.6.14 - name: Start MongoDB run: sudo systemctl start mongod - - name: Run tests + - name: Run Tests run: npm run-script test-ci + - name: Send Coverage + run: npm run-script coverage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8e87e2db4a..7beea2d51a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ [coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg [coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master [discord-img]: https://img.shields.io/discord/629952586895851530?label=discord%20chat -[discord-url]: https://discordapp.com/channels/629952586895851530/629952669967974410 +[discord-url]: https://discord.gg/rTKhrqz ## Installation for development @@ -181,6 +181,7 @@ Also if you can't code, it's possible to contribute by improving the documentati [@unsoluble]: https://github.com/unsoluble [@viderehh]: https://github.com/viderehh [@OpossumGit]: https://github.com/OpossumGit +[@Bartlomiejsz]: https://github.com/Bartlomiejsz | Contribution area | List of contributors | | ------------------------------------- | ---------------------------------- | @@ -252,7 +253,7 @@ Languages with less than 90% coverage will be removed in a future Nightscout ver | 한국어 (`ko`)|Please volunteer|Needs attention: 80.6%| | Norsk (Bokmål) (`nb`)|Please volunteer|OK| | Nederlands (`nl`)|[@PieterGit]|OK| -| Polski (`pl`)|Please volunteer|OK| +| Polski (`pl`)|[@Bartlomiejsz]|OK| | Português (Brasil) (`pt`)|Please volunteer|OK| | Română (`ro`)|Please volunteer|OK| | Русский (`ru`)|[@apanasef]|OK| diff --git a/README.md b/README.md index f4a0452dd9d..8586e0b5a3b 100644 --- a/README.md +++ b/README.md @@ -49,23 +49,24 @@ Community maintained fork of the - [Install](#install) - [Supported configurations:](#supported-configurations) - - [Minimum browser requirements for viewing the site:](#minimum-browser-requirements-for-viewing-the-site) + - [Recommended minimum browser versions for using Nightscout:](#recommended-minimum-browser-versions-for-using-nightscout) - [Windows installation software requirements:](#windows-installation-software-requirements) - [Installation notes for users with nginx or Apache reverse proxy for SSL/TLS offloading:](#installation-notes-for-users-with-nginx-or-apache-reverse-proxy-for-ssltls-offloading) - [Installation notes for Microsoft Azure, Windows:](#installation-notes-for-microsoft-azure-windows) +- [Development](#development) - [Usage](#usage) - [Updating my version?](#updating-my-version) - - [What is my mongo string?](#what-is-my-mongo-string) - [Configure my uploader to match](#configure-my-uploader-to-match) - [Nightscout API](#nightscout-api) - [Example Queries](#example-queries) - [Environment](#environment) - [Required](#required) - - [Features/Labs](#featureslabs) + - [Features](#features) - [Alarms](#alarms) - [Core](#core) - [Predefined values for your browser settings (optional)](#predefined-values-for-your-browser-settings-optional) - [Predefined values for your server settings (optional)](#predefined-values-for-your-server-settings-optional) + - [Views](#views) - [Plugins](#plugins) - [Default Plugins](#default-plugins) - [`delta` (BG Delta)](#delta-bg-delta) @@ -97,7 +98,7 @@ Community maintained fork of the - [`openaps` (OpenAPS)](#openaps-openaps) - [`loop` (Loop)](#loop-loop) - [`override` (Override Mode)](#override-override-mode) - - [`xdripjs` (xDrip-js)](#xdripjs-xdripjs) + - [`xdripjs` (xDrip-js)](#xdripjs-xdrip-js) - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) - [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow) - [`speech` (Speech)](#speech-speech) @@ -109,6 +110,7 @@ Community maintained fork of the - [Setting environment variables](#setting-environment-variables) - [Vagrant install](#vagrant-install) - [More questions?](#more-questions) + - [Browser testing suite provided by](#browser-testing-suite-provided-by) - [License](#license) @@ -583,7 +585,7 @@ For remote overrides, the following extended settings must be configured: Treatment Profile Fields: * `timezone` (Time Zone) - time zone local to the patient. *Should be set.* - * `units` (Profile Units) - blood glucose units used in the profile, either "mgdl" or "mmol" + * `units` (Profile Units) - blood glucose units used in the profile, either "mg/dl" or "mmol" * `dia` (Insulin duration) - value should be the duration of insulin action to use in calculating how much insulin is left active. Defaults to 3 hours. * `carbs_hr` (Carbs per Hour) - The number of carbs that are processed per hour, for more information see [#DIYPS](http://diyps.org/2014/05/29/determining-your-carbohydrate-absorption-rate-diyps-lessons-learned/). * `carbratio` (Carb Ratio) - grams per unit of insulin. diff --git a/app.js b/app.js index 7c6f67b1ee1..b718e3772e9 100644 --- a/app.js +++ b/app.js @@ -131,36 +131,53 @@ function create (env, ctx) { } })); - const clockviews = require('./lib/server/clocks.js')(env, ctx); - clockviews.setLocals(app.locals); - - app.use("/clock", clockviews); - - app.get("/", (req, res) => { - res.render("index.html", { - locals: app.locals - }); - }); - var appPages = { - "/clock-color.html": "clock-color.html" - , "/admin": "adminindex.html" - , "/profile": "profileindex.html" - , "/food": "foodindex.html" - , "/bgclock.html": "bgclock.html" - , "/report": "reportindex.html" - , "/translations": "translationsindex.html" - , "/clock.html": "clock.html" + "/": { + file: "index.html" + , type: "index" + } + , "/admin": { + file: "adminindex.html" + , title: 'Admin Tools' + , type: 'admin' + } + , "/food": { + file: "foodindex.html" + , title: 'Food Editor' + , type: 'food' + } + , "/profile": { + file: "profileindex.html" + , title: 'Profile Editor' + , type: 'profile' + } + , "/report": { + file: "reportindex.html" + , title: 'Nightscout reporting' + , type: 'report' + } + , "/translations": { + file: "translationsindex.html" + , title: 'Nightscout translations' + , type: 'translations' + } }; Object.keys(appPages).forEach(function(page) { app.get(page, (req, res) => { - res.render(appPages[page], { - locals: app.locals + res.render(appPages[page].file, { + locals: app.locals, + title: appPages[page].title ? appPages[page].title : '', + type: appPages[page].type ? appPages[page].type : '', }); }); }); + const clockviews = require('./lib/server/clocks.js')(env, ctx); + clockviews.setLocals(app.locals); + + app.use("/clock", clockviews); + app.get("/appcache/*", (req, res) => { res.render("nightscout.appcache", { locals: app.locals diff --git a/bundle/bundle.reports.source.js b/bundle/bundle.reports.source.js index c07368543b4..27d67e9fb82 100644 --- a/bundle/bundle.reports.source.js +++ b/bundle/bundle.reports.source.js @@ -1,10 +1,11 @@ import './bundle.source'; window.Nightscout.report_plugins = require('../lib/report_plugins/')(); +window.Nightscout.predictions = require('../lib/report/predictions'); console.info('Nightscout report bundle ready'); // Needed for Hot Module Replacement if(typeof(module.hot) !== 'undefined') { - module.hot.accept() // eslint-disable-line no-undef + module.hot.accept() } diff --git a/bundle/bundle.source.js b/bundle/bundle.source.js index d554744e6e4..db61af947bf 100644 --- a/bundle/bundle.source.js +++ b/bundle/bundle.source.js @@ -32,5 +32,5 @@ console.info('Nightscout bundle ready'); // Needed for Hot Module Replacement if(typeof(module.hot) !== 'undefined') { - module.hot.accept() // eslint-disable-line no-undef + module.hot.accept() } diff --git a/ci.test.env b/ci.test.env index c57e5eeb0c4..f5e240f8381 100644 --- a/ci.test.env +++ b/ci.test.env @@ -4,4 +4,5 @@ HOSTNAME=localhost INSECURE_USE_HTTP=true PORT=1337 NODE_ENV=production -CI=true \ No newline at end of file +CI=true +CODACY_PROJECT_TOKEN=cff7ab3377d6434a9355fd051dbb4595 \ No newline at end of file diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md index 4e298df4b74..87117affd46 100644 --- a/docs/plugins/alexa-plugin.md +++ b/docs/plugins/alexa-plugin.md @@ -10,6 +10,7 @@ - [Create a new Alexa skill](#create-a-new-alexa-skill) - [Define the interaction model](#define-the-interaction-model) - [Point your skill at your site](#point-your-skill-at-your-site) + - [Do you use Authentication Roles?](#do-you-use-authentication-roles) - [Test your skill out with the test tool](#test-your-skill-out-with-the-test-tool) - [What questions can you ask it?](#what-questions-can-you-ask-it) - [Activate the skill on your Echo or other device](#activate-the-skill-on-your-echo-or-other-device) @@ -75,10 +76,20 @@ Now you need to point your skill at *your* Nightscout site. 1. In the left-hand menu for your skill, there's an option called "Endpoint". Click it. 1. Under "Service Endpoint Type", select "HTTPS". 1. You only need to set up the Default Region. In the box that says "Enter URI...", put in `https://{yourdomain}/api/v1/alexa`. (So if your Nightscout site is at `mynightscoutsite.herokuapp.com`, you'll enter `https://mynightscoutsite.herokuapp.com/api/v1/alexa` in the box.) + - If you use Authentication Roles, you'll need to add a bit to the end of your URL. See [the section](#do-you-use-authentication-roles) below. 1. In the dropdown under the previous box, select "My development endpoint is a sub-domain of a domain that has a wildcard certificate from a certificate authority". 1. Click the "Save Endpoints" button at the top. 1. You should see a success message pop up when the save succeeds. +### Do you use Authentication Roles? ### + +If you use Authentication Roles, you will need to add a token to the end of your Nightscout URL when configuring your Endpoint. + +1. In your Nightscout Admin Tools, add a new subject and give it the "readable" role. + - If you **really** would like to be super specific, you could create a new role and set the permissions to `api:*:read`. +1. After the new subject is created, copy the "Access Token" value for the new row in your subject table (**don't** copy the link, just copy the text). +1. At the end of your Nighscout URL, add `?token={yourtoken}`, where `{yourtoken}` is the Access Token you just copied. Your new URL should look like `https://{yourdomain}/api/v1/googlehome?token={yourtoken}`. + ### Test your skill out with the test tool Click on the "Test" tab on the top menu. This will take you to the page where you can test your new skill. diff --git a/docs/plugins/alexa-templates/en-us.json b/docs/plugins/alexa-templates/en-us.json index 4cb10aa0643..79cc1baa977 100644 --- a/docs/plugins/alexa-templates/en-us.json +++ b/docs/plugins/alexa-templates/en-us.json @@ -74,6 +74,16 @@ { "name": "LIST_OF_METRICS", "values": [ + { + "name": { + "value": "delta", + "synonyms": [ + "blood glucose delta", + "blood sugar delta", + "bg delta" + ] + } + }, { "name": { "value": "uploader battery", @@ -162,6 +172,67 @@ "raw blood glucose" ] } + }, + { + "name": { + "value": "cgm noise" + } + }, + { + "name": { + "value": "cgm tx age", + "synonyms": [ + "tx age", + "transmitter age", + "cgm transmitter age" + ] + } + }, + { + "name": { + "value": "cgm tx status", + "synonyms": [ + "tx status", + "transmitter status", + "cgm transmitter status" + ] + } + }, + { + "name": { + "value": "cgm battery", + "synonyms": [ + "cgm battery level", + "cgm battery levels", + "cgm batteries", + "cgm transmitter battery", + "cgm transmitter battery level", + "cgm transmitter battery levels", + "cgm transmitter batteries", + "transmitter battery", + "transmitter battery level", + "transmitter battery levels", + "transmitter batteries" + ] + } + }, + { + "name": { + "value": "cgm session age", + "synonyms": [ + "session age" + ] + } + }, + { + "name": { + "value": "cgm status" + } + }, + { + "name": { + "value": "cgm mode" + } } ] } diff --git a/docs/plugins/google-home-templates/en-us.zip b/docs/plugins/google-home-templates/en-us.zip index 6a8498b0b19..d8ada2a834a 100644 Binary files a/docs/plugins/google-home-templates/en-us.zip and b/docs/plugins/google-home-templates/en-us.zip differ diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md index ccffb81f404..8e43549a2cf 100644 --- a/docs/plugins/googlehome-plugin.md +++ b/docs/plugins/googlehome-plugin.md @@ -6,6 +6,7 @@ - [Overview](#overview) - [Activate the Nightscout Google Home Plugin](#activate-the-nightscout-google-home-plugin) - [Create Your DialogFlow Agent](#create-your-dialogflow-agent) + - [Do you use Authentication Roles?](#do-you-use-authentication-roles) - [What questions can you ask it?](#what-questions-can-you-ask-it) - [Updating your agent with new features](#updating-your-agent-with-new-features) - [Adding support for additional languages](#adding-support-for-additional-languages) @@ -56,6 +57,7 @@ To add Google Home support for your Nightscout site, here's what you need to do: 1. After the import finishes, click the "DONE" button followed by the "SAVE" button. 1. In the navigation pane on the left, click on "Fulfillment". 1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` + - If you use Authentication Roles, you'll need to add a bit to the end of your URL. See [the section](#do-you-use-authentication-roles) below. 1. Scroll down to the bottom of the page and click the "SAVE" button. 1. Click on "Integrations" in the navigation pane. 1. Click on "INTEGRATION SETTINGS" for "Google Assistant". @@ -65,6 +67,15 @@ To add Google Home support for your Nightscout site, here's what you need to do: That's it! Now try asking Google "Hey Google, ask *your Action's name* how am I doing?" +### Do you use Authentication Roles? ### + +If you use Authentication Roles, you will need to add a token to the end of your Nightscout URL when configuring your Webhook. + +1. In your Nightscout Admin Tools, add a new subject and give it the "readable" role. + - If you **really** would like to be super specific, you could create a new role and set the permissions to `api:*:read`. +1. After the new subject is created, copy the "Access Token" value for the new row in your subject table (**don't** copy the link, just copy the text). +1. At the end of your Nighscout URL, add `?token=YOUR-TOKEN`, where `YOUR-TOKEN` is the Access Token you just copied. Your new URL should look like `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome?token=YOUR-TOKEN`. + ### What questions can you ask it? See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. diff --git a/docs/plugins/interacting-with-virtual-assistants.md b/docs/plugins/interacting-with-virtual-assistants.md index 984a876f21c..3fe67bee2fb 100644 --- a/docs/plugins/interacting-with-virtual-assistants.md +++ b/docs/plugins/interacting-with-virtual-assistants.md @@ -43,6 +43,7 @@ This list is not meant to be comprehensive, nor does it include every way you ca - "Alexa, ask Nightscout what is my basal" - "Alexa, ask Nightscout what is my current basal" - "Alexa, ask Nightscout what is my cob" +- "Alexa, ask Nightscout what is my delta" - "Alexa, ask Nightscout what is Charlie's carbs on board" - "Alexa, ask Nightscout what is Sophie's carbohydrates on board" - "Alexa, ask Nightscout what is Harper's loop forecast" @@ -51,6 +52,15 @@ This list is not meant to be comprehensive, nor does it include every way you ca - "Alexa, ask Nightscout what is Arden's raw bg" - "Alexa, ask Nightscout what is Dana's raw blood glucose" +*CGM Info:* (when using the [`xdripjs` plugin](/README.md#xdripjs-xdrip-js)) + +- "Alexa, ask Nightscout what's my CGM status" +- "Alexa, ask Nightscout what's my CGM session age" +- "Alexa, ask Nightscout what's my CGM transmitter age" +- "Alexa, ask Nightscout what's my CGM mode" +- "Alexa, ask Nightscout what's my CGM noise" +- "Alexa, ask Nightscout what's my CGM battery" + *Insulin Remaining:* - "Alexa, ask Nightscout how much insulin do I have left" diff --git a/lib/api/activity/index.js b/lib/api/activity/index.js index d88019ab936..c42e73570c6 100644 --- a/lib/api/activity/index.js +++ b/lib/api/activity/index.js @@ -43,17 +43,17 @@ function configure(app, wares, ctx) { var d2 = null; - if (t.hasOwnProperty('created_at')) { + if (Object.prototype.hasOwnProperty.call(t, 'created_at')) { d2 = new Date(t.created_at); } else { - if (t.hasOwnProperty('timestamp')) { + if (Object.prototype.hasOwnProperty.call(t, 'timestamp')) { d2 = new Date(t.timestamp); } } if (d2 == null) { return; } - if (d1 == null || d2.getTime() > d1.getTime()) { + if (d1 == null || d2.getTime() > d1.getTime()) { d1 = d2; } }); @@ -80,7 +80,7 @@ function configure(app, wares, ctx) { if (!_isArray(activity)) { activity = [activity]; - }; + } ctx.activity.create(activity, function(err, created) { if (err) { diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js index 337ec00f732..2a5fd4ef6cd 100644 --- a/lib/api/alexa/index.js +++ b/lib/api/alexa/index.js @@ -1,10 +1,8 @@ 'use strict'; var moment = require('moment'); -var _each = require('lodash/each'); function configure (app, wares, ctx, env) { - var entries = ctx.entries; var express = require('express') , api = express.Router( ); var translate = ctx.language.translate; @@ -16,30 +14,7 @@ function configure (app, wares, ctx, env) { // json body types get handled as parsed json api.use(wares.bodyParser.json()); - ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.virtAsst) { - if (plugin.virtAsst.intentHandlers) { - console.log('Alexa: Plugin ' + plugin.name + ' supports Virtual Assistants'); - _each(plugin.virtAsst.intentHandlers, function (route) { - if (route) { - ctx.alexa.configureIntentHandler(route.intent, route.intentHandler, route.metrics); - } - }); - } - if (plugin.virtAsst.rollupHandlers) { - console.log('Alexa: Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); - _each(plugin.virtAsst.rollupHandlers, function (route) { - console.log('Route'); - console.log(route); - if (route) { - ctx.alexa.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); - } - }); - } - } else { - console.log('Alexa: Plugin ' + plugin.name + ' does not support Virtual Assistants'); - } - }); + ctx.virtAsstBase.setupVirtAsstHandlers(ctx.alexa); api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { console.log('Incoming request from Alexa'); @@ -53,77 +28,40 @@ function configure (app, wares, ctx, env) { } switch (req.body.request.type) { - case 'IntentRequest': - onIntent(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); + case 'SessionEndedRequest': + onSessionEnded(function () { + res.json(''); next( ); }); break; case 'LaunchRequest': - onLaunch(req.body.request.intent, function (title, response) { - res.json(ctx.alexa.buildSpeechletResponse(title, response, '', 'true')); - next( ); - }); - break; - case 'SessionEndedRequest': - onSessionEnded(req.body.request.intent, function (alexaResponse) { - res.json(alexaResponse); + if (!req.body.request.intent) { + onLaunch(function () { + res.json(ctx.alexa.buildSpeechletResponse( + translate('virtAsstTitleLaunch'), + translate('virtAsstLaunch'), + translate('virtAsstLaunch'), + false + )); + next( ); + }); + break; + } + // if intent is set then fallback to IntentRequest + case 'IntentRequest': // eslint-disable-line no-fallthrough + onIntent(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', true)); next( ); }); break; } }); - ctx.alexa.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { - entries.list({count: 1}, function (err, records) { - var direction; - if (translate(records[0].direction)) { - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('virtAsstStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time)) - ] - }); - - callback(null, {results: status, priority: -1}); - }); - }, 'BG Status'); - - ctx.alexa.configureIntentHandler('MetricNow', function (callback, slots, sbx, locale) { - entries.list({count: 1}, function(err, records) { - var direction; - if(translate(records[0].direction)){ - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('virtAsstStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time))] - }); - - callback(translate('virtAsstTitleCurrentBG'), status); - }); - }, ['bg', 'blood glucose', 'number']); - - ctx.alexa.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { - ctx.alexa.getRollup('Status', sbx, slots, locale, function (status) { - callback(translate('virtAsstTitleFullStatus'), status); - }); - }); - + ctx.virtAsstBase.setupMutualIntents(ctx.alexa); - function onLaunch(intent, next) { + function onLaunch(next) { console.log('Session launched'); - console.log(JSON.stringify(intent)); - handleIntent(intent.name, intent.slots, next); + next( ); } function onIntent(intent, next) { @@ -132,8 +70,9 @@ function configure (app, wares, ctx, env) { handleIntent(intent.name, intent.slots, next); } - function onSessionEnded() { + function onSessionEnded(next) { console.log('Session ended'); + next( ); } function handleIntent(intentName, slots, next) { @@ -154,6 +93,7 @@ function configure (app, wares, ctx, env) { metric = slots.metric.resolutions.resolutionsPerAuthority[0].values[0].value.name; } else { next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); + return; } } @@ -161,8 +101,10 @@ function configure (app, wares, ctx, env) { if (handler){ var sbx = initializeSandbox(); handler(next, slots, sbx); + return; } else { next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); + return; } } @@ -176,4 +118,4 @@ function configure (app, wares, ctx, env) { return api; } -module.exports = configure; \ No newline at end of file +module.exports = configure; diff --git a/lib/api/devicestatus/index.js b/lib/api/devicestatus/index.js index 91702902fa3..25e226efef6 100644 --- a/lib/api/devicestatus/index.js +++ b/lib/api/devicestatus/index.js @@ -30,8 +30,7 @@ function configure (app, wares, ctx, env) { // 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')) { + if (e.created_at && Object.prototype.hasOwnProperty.call(e, 'utcOffset')) { const d = moment(e.created_at).utcOffset(e.utcOffset); e.created_at = d.toISOString(true); delete e.utcOffset; @@ -106,7 +105,7 @@ function configure (app, wares, ctx, env) { api.delete('/devicestatus/', ctx.authorization.isPermitted('api:devicestatus:delete'), delete_records); } - if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/ ) { + if (app.enabled('api')) { config_authed(app, api, wares, ctx); } diff --git a/lib/api/entries/index.js b/lib/api/entries/index.js index faf922ff0e8..5dd05b419fb 100644 --- a/lib/api/entries/index.js +++ b/lib/api/entries/index.js @@ -14,8 +14,7 @@ const expand = braces.expand; const ID_PATTERN = /^[a-f\d]{24}$/; function isId (value) { - //TODO: why did we need tht length check? - return value && ID_PATTERN.test(value) && value.length === 24; + return ID_PATTERN.test(value); } /** @@ -74,8 +73,7 @@ function configure (app, wares, ctx, env) { // Support date de-normalization for older clients if (env.settings.deNormalizeDates) { - // eslint-disable-next-line no-prototype-builtins - if (data.dateString && data.hasOwnProperty('utcOffset')) { + if (data.dateString && Object.prototype.hasOwnProperty.call(data, 'utcOffset')) { const d = moment(data.dateString).utcOffset(data.utcOffset); data.dateString = d.toISOString(true); delete data.utcOffset; diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js index 2b2caa2a378..b44715b25eb 100644 --- a/lib/api/googlehome/index.js +++ b/lib/api/googlehome/index.js @@ -1,10 +1,8 @@ 'use strict'; var moment = require('moment'); -var _each = require('lodash/each'); function configure (app, wares, ctx, env) { - var entries = ctx.entries; var express = require('express') , api = express.Router( ); var translate = ctx.language.translate; @@ -16,30 +14,7 @@ function configure (app, wares, ctx, env) { // json body types get handled as parsed json api.use(wares.bodyParser.json()); - ctx.plugins.eachEnabledPlugin(function each(plugin){ - if (plugin.virtAsst) { - if (plugin.virtAsst.intentHandlers) { - console.log('Google Home: Plugin ' + plugin.name + ' supports Virtual Assistants'); - _each(plugin.virtAsst.intentHandlers, function (route) { - if (route) { - ctx.googleHome.configureIntentHandler(route.intent, route.intentHandler, route.metrics); - } - }); - } - if (plugin.virtAsst.rollupHandlers) { - console.log('Google Home: Plugin ' + plugin.name + ' supports rollups for Virtual Assistants'); - _each(plugin.virtAsst.rollupHandlers, function (route) { - console.log('Route'); - console.log(route); - if (route) { - ctx.googleHome.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); - } - }); - } - } else { - console.log('Google Home: Plugin ' + plugin.name + ' does not support Virtual Assistants'); - } - }); + ctx.virtAsstBase.setupVirtAsstHandlers(ctx.googleHome); api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { console.log('Incoming request from Google Home'); @@ -58,57 +33,16 @@ function configure (app, wares, ctx, env) { handler(function (title, response) { res.json(ctx.googleHome.buildSpeechletResponse(response, false)); next( ); + return; }, req.body.queryResult.parameters, sbx); } else { - res.json(ctx.googleHome.buildSpeechletResponse('I\'m sorry. I don\'t know what you\'re asking for. Could you say that again?', true)); + res.json(ctx.googleHome.buildSpeechletResponse(translate('virtAsstUnknownIntentText'), true)); next( ); + return; } }); - ctx.googleHome.addToRollup('Status', function bgRollupHandler(slots, sbx, callback) { - entries.list({count: 1}, function (err, records) { - var direction; - if (translate(records[0].direction)) { - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('virtAsstStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time)) - ] - }); - - callback(null, {results: status, priority: -1}); - }); - }, 'BG Status'); - - ctx.googleHome.configureIntentHandler('MetricNow', function (callback, slots, sbx, locale) { - entries.list({count: 1}, function(err, records) { - var direction; - if(translate(records[0].direction)){ - direction = translate(records[0].direction); - } else { - direction = records[0].direction; - } - var status = translate('virtAsstStatus', { - params: [ - sbx.scaleMgdl(records[0].sgv), - direction, - moment(records[0].date).from(moment(sbx.time))] - }); - - callback(translate('virtAsstTitleCurrentBG'), status); - }); - }, ['bg', 'blood glucose', 'number']); - - ctx.googleHome.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { - ctx.googleHome.getRollup('Status', sbx, slots, locale, function (status) { - callback(translate('virtAsstTitleFullStatus'), status); - }); - }); + ctx.virtAsstBase.setupMutualIntents(ctx.googleHome); function initializeSandbox() { var sbx = require('../../sandbox')(); @@ -120,4 +54,4 @@ function configure (app, wares, ctx, env) { return api; } -module.exports = configure; \ No newline at end of file +module.exports = configure; diff --git a/lib/api/status.js b/lib/api/status.js index a6ab2e82750..dc8d97bcdd3 100644 --- a/lib/api/status.js +++ b/lib/api/status.js @@ -15,7 +15,7 @@ function configure (app, wares, env, ctx) { // Status badge/text/json api.get('/status', function (req, res) { - var authToken = req.query.token || req.query.secret || ''; + var authToken = req.query.token || req.query.secret || ''; var date = new Date(); var info = { status: 'ok' diff --git a/lib/api/treatments/index.js b/lib/api/treatments/index.js index 5d527fce6ac..b30c297143b 100644 --- a/lib/api/treatments/index.js +++ b/lib/api/treatments/index.js @@ -45,8 +45,7 @@ function configure (app, wares, ctx, env) { t.carbs = Number(t.carbs); t.insulin = Number(t.insulin); - // eslint-disable-next-line no-prototype-builtins - if (deNormalizeDates && t.hasOwnProperty('utcOffset')) { + if (deNormalizeDates && Object.prototype.hasOwnProperty.call(t, 'utcOffset')) { const d = moment(t.created_at).utcOffset(t.utcOffset); t.created_at = d.toISOString(true); delete t.utcOffset; @@ -54,10 +53,10 @@ function configure (app, wares, ctx, env) { var d2 = null; - if (t.hasOwnProperty('created_at')) { + if (Object.prototype.hasOwnProperty.call(t, 'created_at')) { d2 = new Date(t.created_at); } else { - if (t.hasOwnProperty('timestamp')) { + if (Object.prototype.hasOwnProperty.call(t, 'timestamp')) { d2 = new Date(t.timestamp); } } @@ -91,7 +90,7 @@ function configure (app, wares, ctx, env) { if (!_isArray(treatments)) { treatments = [treatments]; - }; + } ctx.treatments.create(treatments, function(err, created) { if (err) { diff --git a/lib/api3/const.json b/lib/api3/const.json index 1c3dfd873ee..fd874a1175a 100644 --- a/lib/api3/const.json +++ b/lib/api3/const.json @@ -15,6 +15,7 @@ "UNAUTHORIZED": 401, "FORBIDDEN": 403, "NOT_FOUND": 404, + "NOT_ACCEPTABLE": 406, "GONE": 410, "PRECONDITION_FAILED": 412, "UNPROCESSABLE_ENTITY": 422, @@ -40,6 +41,7 @@ "HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT", "HTTP_403_MISSING_PERMISSION": "Missing permission {0}", "HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS", + "HTTP_406_UNSUPPORTED_FORMAT": "Unsupported output format requested", "HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document", "HTTP_500_INTERNAL_ERROR": "Internal Server Error", "STORAGE_ERROR": "Database error", diff --git a/lib/api3/doc/formats.md b/lib/api3/doc/formats.md new file mode 100644 index 00000000000..5a01a802f3e --- /dev/null +++ b/lib/api3/doc/formats.md @@ -0,0 +1,88 @@ +# APIv3: Output formats + +### Choosing output format +In APIv3, the standard content type is JSON for both HTTP request and HTTP response. +However, in HTTP response, the response content type can be changed to XML or CSV +for READ, SEARCH, and HISTORY operations. + +The response content type can be requested in one of the following ways: +- add a file type extension to the URL, eg. + `/api/v3/entries.csv?...` + or `/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895.xml?...` +- set `Accept` HTTP request header to `text/csv` or `application/xml` + +The server replies with `406 Not Acceptable` HTTP status in case of not supported content type. + + +### JSON + +Default content type is JSON, output can look like this: + +``` +[ + { + "type":"sgv", + "sgv":"171", + "dateString":"2014-07-19T02:44:15.000-07:00", + "date":1405763055000, + "device":"dexcom", + "direction":"Flat", + "identifier":"5c5a2404e0196f4d3d9a718a", + "srvModified":1405763055000, + "srvCreated":1405763055000 + }, + { + "type":"sgv", + "sgv":"176", + "dateString":"2014-07-19T03:09:15.000-07:00", + "date":1405764555000, + "device":"dexcom", + "direction":"Flat", + "identifier":"5c5a2404e0196f4d3d9a7187", + "srvModified":1405764555000, + "srvCreated":1405764555000 + } +] +``` + +### XML + +Sample output: + +``` + + + + sgv + 171 + 2014-07-19T02:44:15.000-07:00 + 1405763055000 + dexcom + Flat + 5c5a2404e0196f4d3d9a718a + 1405763055000 + 1405763055000 + + + sgv + 176 + 2014-07-19T03:09:15.000-07:00 + 1405764555000 + dexcom + Flat + 5c5a2404e0196f4d3d9a7187 + 1405764555000 + 1405764555000 + + +``` + +### CSV + +Sample output: + +``` +type,sgv,dateString,date,device,direction,identifier,srvModified,srvCreated +sgv,171,2014-07-19T02:44:15.000-07:00,1405763055000,dexcom,Flat,5c5a2404e0196f4d3d9a718a,1405763055000,1405763055000 +sgv,176,2014-07-19T03:09:15.000-07:00,1405764555000,dexcom,Flat,5c5a2404e0196f4d3d9a7187,1405764555000,1405764555000 +``` diff --git a/lib/api3/generic/create/insert.js b/lib/api3/generic/create/insert.js index 4ac80a37e94..b643818569a 100644 --- a/lib/api3/generic/create/insert.js +++ b/lib/api3/generic/create/insert.js @@ -3,6 +3,7 @@ const apiConst = require('../../const.json') , security = require('../../security') , validate = require('./validate.js') + , path = require('path') ; /** @@ -33,7 +34,7 @@ async function insert (opCtx, doc) { throw new Error('empty identifier'); res.setHeader('Last-Modified', now.toUTCString()); - res.setHeader('Location', `${req.baseUrl}${req.path}/${identifier}`); + res.setHeader('Location', path.posix.join(req.baseUrl, req.path, identifier)); res.status(apiConst.HTTP.CREATED).send({ }); ctx.bus.emit('storage-socket-create', { colName: col.colName, doc }); diff --git a/lib/api3/generic/history/operation.js b/lib/api3/generic/history/operation.js index 0929a09cc4f..5151c8b2749 100644 --- a/lib/api3/generic/history/operation.js +++ b/lib/api3/generic/history/operation.js @@ -1,6 +1,7 @@ 'use strict'; const dateTools = require('../../shared/dateTools') + , renderer = require('../../shared/renderer') , apiConst = require('../../const.json') , security = require('../../security') , opTools = require('../../shared/operationTools') @@ -53,7 +54,8 @@ async function history (opCtx, fieldsProjector) { _.each(result, fieldsProjector.applyProjection); - res.status(apiConst.HTTP.OK).send(result); + res.status(apiConst.HTTP.OK); + renderer.render(res, result); } } diff --git a/lib/api3/generic/read/operation.js b/lib/api3/generic/read/operation.js index 04d6f03bc70..c2e65a4afcc 100644 --- a/lib/api3/generic/read/operation.js +++ b/lib/api3/generic/read/operation.js @@ -4,6 +4,7 @@ const apiConst = require('../../const.json') , security = require('../../security') , opTools = require('../../shared/operationTools') , dateTools = require('../../shared/dateTools') + , renderer = require('../../shared/renderer') , FieldsProjector = require('../../shared/fieldsProjector') ; @@ -48,7 +49,8 @@ async function read (opCtx) { fieldsProjector.applyProjection(doc); - res.status(apiConst.HTTP.OK).send(doc); + res.status(apiConst.HTTP.OK); + renderer.render(res, doc); } diff --git a/lib/api3/generic/search/operation.js b/lib/api3/generic/search/operation.js index 074f864d58a..c24f978dc27 100644 --- a/lib/api3/generic/search/operation.js +++ b/lib/api3/generic/search/operation.js @@ -3,6 +3,7 @@ const apiConst = require('../../const.json') , security = require('../../security') , opTools = require('../../shared/operationTools') + , renderer = require('../../shared/renderer') , input = require('./input') , _each = require('lodash/each') , FieldsProjector = require('../../shared/fieldsProjector') @@ -49,7 +50,8 @@ async function search (opCtx) { _each(result, fieldsProjector.applyProjection); - res.status(apiConst.HTTP.OK).send(result); + res.status(apiConst.HTTP.OK); + renderer.render(res, result); } } diff --git a/lib/api3/generic/update/replace.js b/lib/api3/generic/update/replace.js index ca490b31136..fdf803ed16f 100644 --- a/lib/api3/generic/update/replace.js +++ b/lib/api3/generic/update/replace.js @@ -3,6 +3,7 @@ const apiConst = require('../../const.json') , security = require('../../security') , validate = require('./validate.js') + , path = require('path') ; /** @@ -38,7 +39,7 @@ async function replace (opCtx, doc, storageDoc, options) { res.setHeader('Last-Modified', now.toUTCString()); if (storageDoc.identifier !== doc.identifier || isDeduplication) { - res.setHeader('Location', `${req.baseUrl}${req.path}/${doc.identifier}`); + res.setHeader('Location', path.posix.join(req.baseUrl, req.path, doc.identifier)); } res.status(apiConst.HTTP.NO_CONTENT).send({ }); diff --git a/lib/api3/index.js b/lib/api3/index.js index 5b1799c78fd..4bfe07a35fe 100644 --- a/lib/api3/index.js +++ b/lib/api3/index.js @@ -2,6 +2,7 @@ const express = require('express') , bodyParser = require('body-parser') + , renderer = require('./shared/renderer') , StorageSocket = require('./storageSocket') , apiConst = require('./const.json') , security = require('./security') @@ -47,6 +48,8 @@ function configure (env, ctx) { } }); + app.use(renderer.extension2accept); + // we don't need these here app.set('etag', false); app.set('x-powered-by', false); // this seems to be unreliable @@ -74,7 +77,7 @@ function configure (env, ctx) { app.get('/version', require('./specific/version')(app, ctx, env)); - if (app.get('env') === 'development' || app.get('ci')) { // for development and testing purposes only + if (app.get('env') === 'development' || app.get('ci')) { // for development and testing purposes only app.get('/test', async function test (req, res) { try { diff --git a/lib/api3/shared/renderer.js b/lib/api3/shared/renderer.js new file mode 100644 index 00000000000..a3588819a72 --- /dev/null +++ b/lib/api3/shared/renderer.js @@ -0,0 +1,99 @@ +'use strict'; + +const apiConst = require('../const.json') + , mime = require('mime') + , url = require('url') + , opTools = require('./operationTools') + , EasyXml = require('easyxml') + , csvStringify = require('csv-stringify') + ; + + +/** + * Middleware that converts url's extension to Accept HTTP request header + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +function extension2accept (req, res, next) { + + const pathSplit = req.path.split('.'); + + if (pathSplit.length < 2) + return next(); + + const pathBase = pathSplit[0] + , extension = pathSplit.slice(1).join('.'); + + if (!extension) + return next(); + + const mimeType = mime.getType(extension); + if (!mimeType) + return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_ACCEPTABLE, apiConst.MSG.HTTP_406_UNSUPPORTED_FORMAT); + + req.extToAccept = { + url: req.url, + accept: req.headers.accept + }; + + req.headers.accept = mimeType; + const parsed = url.parse(req.url); + parsed.pathname = pathBase; + req.url = url.format(parsed); + + next(); +} + + +/** + * Sends data to output using the client's desired format + * @param {Object} res + * @param {any} data + */ +function render (res, data) { + res.format({ + 'json': () => res.send(data), + 'csv': () => renderCsv(res, data), + 'xml': () => renderXml(res, data), + 'default': () => + opTools.sendJSONStatus(res, apiConst.HTTP.NOT_ACCEPTABLE, apiConst.MSG.HTTP_406_UNSUPPORTED_FORMAT) + }); +} + + +/** + * Format data to output as .csv + * @param {Object} res + * @param {any} data + */ +function renderCsv (res, data) { + const csvSource = Array.isArray(data) ? data : [data]; + csvStringify(csvSource, { + header: true + }, + function csvStringified (err, output) { + res.send(output); + }); +} + + +/** + * Format data to output as .xml + * @param {Object} res + * @param {any} data + */ +function renderXml (res, data) { + const serializer = new EasyXml({ + rootElement: 'item', + dateFormat: 'ISO', + manifest: true + }); + res.send(serializer.render(data)); +} + + +module.exports = { + extension2accept, + render +}; \ No newline at end of file diff --git a/lib/api3/swagger.js b/lib/api3/swagger.js index 2d434e97f53..ff965061c87 100644 --- a/lib/api3/swagger.js +++ b/lib/api3/swagger.js @@ -10,7 +10,7 @@ function setupSwaggerUI (app) { const serveSwaggerDef = function serveSwaggerDef (req, res) { res.sendFile(__dirname + '/swagger.yaml'); }; - app.get('/swagger.yaml', serveSwaggerDef); + app.get('/swagger', serveSwaggerDef); const swaggerUiAssetPath = require('swagger-ui-dist').getAbsoluteFSPath(); const swaggerFiles = express.static(swaggerUiAssetPath); diff --git a/lib/api3/swagger.yaml b/lib/api3/swagger.yaml index 17db893e0ef..5cbb2a05544 100644 --- a/lib/api3/swagger.yaml +++ b/lib/api3/swagger.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 servers: - url: '/api/v3' info: - version: '3.0.1' + version: "3.0.1" title: Nightscout API contact: name: NS development discussion channel @@ -135,6 +135,8 @@ paths: $ref: '#/components/responses/403Forbidden' 404: $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' ###################################################################################### @@ -240,6 +242,8 @@ paths: $ref: '#/components/responses/403Forbidden' 404: $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' 410: $ref: '#/components/responses/410Gone' @@ -459,6 +463,8 @@ paths: $ref: '#/components/responses/403Forbidden' 404: $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' ###################################################################################### @@ -518,6 +524,8 @@ paths: $ref: '#/components/responses/403Forbidden' 404: $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' ###################################################################################### @@ -888,6 +896,9 @@ components: 404NotFound: description: The collection or document specified was not found. + 406NotAcceptable: + description: The requested content type (in `Accept` header) is not supported. + 412PreconditionFailed: description: The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header). @@ -903,6 +914,12 @@ components: application/json: schema: $ref: '#/components/schemas/DocumentArray' + text/csv: + schema: + $ref: '#/components/schemas/DocumentArray' + application/xml: + schema: + $ref: '#/components/schemas/DocumentArray' search204: description: Successful operation - no documents matching the filtering criteria @@ -913,6 +930,12 @@ components: application/json: schema: $ref: '#/components/schemas/Document' + text/csv: + schema: + $ref: '#/components/schemas/Document' + application/xml: + schema: + $ref: '#/components/schemas/Document' headers: 'Last-Modified': $ref: '#/components/schemas/headerLastModified' @@ -924,6 +947,12 @@ components: application/json: schema: $ref: '#/components/schemas/DocumentArray' + text/csv: + schema: + $ref: '#/components/schemas/DocumentArray' + application/xml: + schema: + $ref: '#/components/schemas/DocumentArray' headers: 'Last-Modified': $ref: '#/components/schemas/headerLastModifiedMaximum' @@ -1417,6 +1446,8 @@ components: Document: description: Single document + xml: + name: 'item' type: object oneOf: - $ref: '#/components/schemas/DeviceStatus' @@ -1483,6 +1514,8 @@ components: DocumentArray: type: object + xml: + name: 'items' oneOf: - $ref: '#/components/schemas/DeviceStatusArray' - $ref: '#/components/schemas/EntryArray' diff --git a/lib/authorization/index.js b/lib/authorization/index.js index feaed739b42..b81578e0dca 100644 --- a/lib/authorization/index.js +++ b/lib/authorization/index.js @@ -186,7 +186,7 @@ function init (env, ctx) { authorization.isPermitted = function isPermitted (permission, opts) { - opts = mkopts(opts); + mkopts(opts); authorization.seenPermissions = _.chain(authorization.seenPermissions) .push(permission) .sort() diff --git a/lib/authorization/storage.js b/lib/authorization/storage.js index 3a4c4490876..c032018d170 100644 --- a/lib/authorization/storage.js +++ b/lib/authorization/storage.js @@ -24,7 +24,7 @@ function init (env, ctx) { function create (collection) { function doCreate(obj, fn) { - if (!obj.hasOwnProperty('created_at')) { + if (!Object.prototype.hasOwnProperty.call(obj, 'created_at')) { obj.created_at = (new Date()).toISOString(); } collection.insert(obj, function (err, doc) { @@ -211,14 +211,14 @@ function init (env, ctx) { if (!accessToken) return null; var split_token = accessToken.split('-'); - var prefix = split_token ? _.last(split_token) : ''; + var prefix = split_token ? _.last(split_token) : ''; if (prefix.length < 16) { return null; } return _.find(storage.subjects, function matches (subject) { - return subject.accessTokenDigest.indexOf(accessToken) === 0 || subject.digest.indexOf(prefix) === 0; + return subject.accessTokenDigest.indexOf(accessToken) === 0 || subject.digest.indexOf(prefix) === 0; }); }; diff --git a/lib/bus.js b/lib/bus.js index 5828130fa92..65faeea21a6 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -1,11 +1,11 @@ 'use strict'; - var Stream = require('stream'); function init (settings) { var beats = 0; var started = new Date( ); var interval = settings.heartbeat * 1000; + let busInterval; var stream = new Stream; @@ -24,9 +24,15 @@ function init (settings) { stream.emit('tick', ictus( )); } + stream.teardown = function ( ) { + console.log('Initiating server teardown'); + clearInterval(busInterval); + stream.emit('teardown'); + }; + stream.readable = true; stream.uptime = repeat; - setInterval(repeat, interval); + busInterval = setInterval(repeat, interval); return stream; } module.exports = init; diff --git a/lib/client/browser-utils.js b/lib/client/browser-utils.js index 4f920588f80..634afb2ab40 100644 --- a/lib/client/browser-utils.js +++ b/lib/client/browser-utils.js @@ -51,7 +51,7 @@ function init ($) { function queryParms () { var params = {}; - if (location.search) { + if ((typeof location !== 'undefined') && location.search) { location.search.substr(1).split('&').forEach(function(item) { // eslint-disable-next-line no-useless-escape params[item.split('=')[0]] = item.split('=')[1].replace(/[_\+]/g, ' '); diff --git a/lib/client/chart.js b/lib/client/chart.js index a5db09f416a..71d68bbc761 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -273,7 +273,7 @@ function init (client, d3, $) { chart.createAdjustedRange = function() { var adjustedRange = chart.createBrushedRange(); - adjustedRange[1] = new Date(adjustedRange[1].getTime() + client.forecastTime); + adjustedRange[1] = new Date(Math.max(adjustedRange[1].getTime(), client.forecastTime)); return adjustedRange; } @@ -609,6 +609,8 @@ function init (client, d3, $) { var currentBrushExtent = scrollBrushExtent; var currentRange = scrollRange; + chart.setForecastTime(); + chart.xScale.domain(currentRange); focusYDomain = dynamicDomainOrElse(focusYDomain); @@ -663,6 +665,10 @@ function init (client, d3, $) { renderer.addTreatmentProfiles(client); renderer.drawTreatments(client); + + // console.log('Redrawing brush due to update: ', currentBrushExtent); + + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); } chart.scroll = function scroll (nowDate) { @@ -704,21 +710,28 @@ function init (client, d3, $) { if (client.sbx.pluginBase.forecastPoints) { var shownForecastPoints = chart.getForecastData(); - var focusHoursAheadMills = chart.getMaxForecastMills(); + // Get maximum time we will allow projected forward in time + // based on the number of hours the user has selected to show. + var maxForecastMills = chart.getMaxForecastMills(); var selectedRange = chart.createBrushedRange(); var to = selectedRange[1].getTime(); - var maxForecastMills = to + times.mins(30).msecs; + // Default min forecast projection times to the default amount of time to forecast + var minForecastMills = to + client.defaultForecastTime; + var availForecastMills = 0; + + // Determine what the maximum forecast time is that is available in the forecast data if (shownForecastPoints.length > 0) { - maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); + availForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); } - maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills); - - var lastSGVMills = client.sbx.lastSGVMills(); + // Limit the amount shown to the maximum time allowed to be projected forward based + // on the number of hours the user has selected to show + var forecastMills = Math.min(availForecastMills, maxForecastMills); - client.forecastTime = ((maxForecastMills > 0) && lastSGVMills) ? maxForecastMills - lastSGVMills : client.defaultForecastTime; + // Don't allow the forecast time to go below the minimum forecast time + client.forecastTime = Math.max(forecastMills, minForecastMills); } }; diff --git a/lib/client/clock-client.js b/lib/client/clock-client.js index 324a5168693..e6eec7190c6 100644 --- a/lib/client/clock-client.js +++ b/lib/client/clock-client.js @@ -107,8 +107,7 @@ client.render = function render (xhr) { if (m < 10) m = "0" + m; $('#clock').text(h + ":" + m); - // defined in the template this is loaded into - // eslint-disable-next-line no-undef + /* global clockFace */ if (clockFace === 'clock-color') { var bgHigh = window.serverSettings.settings.thresholds.bgHigh; diff --git a/lib/client/d3locales.js b/lib/client/d3locales.js index afb04cbad5c..1af15ba3fb6 100644 --- a/lib/client/d3locales.js +++ b/lib/client/d3locales.js @@ -142,6 +142,21 @@ d3locales.it_IT = { shortMonths: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'] }; +d3locales.pl_PL = { + decimal: '.', + thousands: ',', + grouping: [3], + currency: ['', 'zł'], + dateTime: '%a %b %e %X %Y', + date: '%d.%m.%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], // unused + days: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'], + shortDays: ['Nie', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'So'], + months: ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'], + shortMonths: ['Sty', 'Lu', 'Mar', 'Kw', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Pa', 'Lis', 'Gru'] +}; + d3locales.pt_BR = { decimal: ',', thousands: '.', @@ -212,6 +227,7 @@ d3locales.locale = function locale (language) { , fr: 'fr_FR' , he: 'he_IL' , it: 'it_IT' + , pl: 'pl_PL' , pt: 'pt_BR' , ro: 'ro_RO' , ru: 'ru_RU' diff --git a/lib/client/index.js b/lib/client/index.js index 469009a9a94..5c611306d9f 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -64,8 +64,7 @@ client.init = function init (callback) { console.log('Application appears to be online'); $('#centerMessagePanel').hide(); client.load(serverSettings, callback); - // eslint-disable-next-line no-unused-vars - }).fail(function fail (jqXHR, textStatus, errorThrown) { + }).fail(function fail (jqXHR) { // check if we couldn't reach the server at all, show offline message if (!jqXHR.readyState) { @@ -136,7 +135,8 @@ client.load = function load (serverSettings, callback) { client.now = Date.now(); client.ddata = require('../data/ddata')(); - client.forecastTime = client.defaultForecastTime = times.mins(30).msecs; + client.defaultForecastTime = times.mins(30).msecs; + client.forecastTime = client.now + client.defaultForecastTime; client.entries = []; client.ticks = require('./ticks'); @@ -774,7 +774,8 @@ client.load = function load (serverSettings, callback) { function updateClock () { updateClockDisplay(); - var interval = (60 - (new Date()).getSeconds()) * 1000 + 5; + // Update at least every 15 seconds + var interval = Math.min(15 * 1000, (60 - (new Date()).getSeconds()) * 1000 + 5); setTimeout(updateClock, interval); updateTimeAgo(); @@ -959,7 +960,7 @@ client.load = function load (serverSettings, callback) { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Client-side code to connect to server and handle incoming data //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // eslint-disable-next-line no-undef + /* global io */ client.socket = socket = io.connect(); socket.on('dataUpdate', dataUpdate); diff --git a/lib/client/renderer.js b/lib/client/renderer.js index f058d3de860..7ea154ff0fd 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -542,23 +542,26 @@ function init (client, d3) { // these used to be semicircles from 1.5708 to 4.7124, but that made the tooltip target too big { 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R3 } , { 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R4 } - ]; + ] + , arc_data_1_elements = []; arc_data[0].outlineOnly = !treatment.carbs; arc_data[2].outlineOnly = !treatment.insulin; if (treatment.carbs > 0) { - arc_data[1].element = Math.round(treatment.carbs) + ' g'; + arc_data_1_elements.push(Math.round(treatment.carbs) + ' g'); } if (treatment.protein > 0) { - arc_data[1].element = arc_data[1].element + " / " + Math.round(treatment.protein) + ' g'; + arc_data_1_elements.push(Math.round(treatment.protein) + ' g'); } if (treatment.fat > 0) { - arc_data[1].element = arc_data[1].element + " / " + Math.round(treatment.fat) + ' g'; + arc_data_1_elements.push(Math.round(treatment.fat) + ' g'); } + arc_data[1].element = arc_data_1_elements.join(' / '); + if (treatment.foodType) { arc_data[1].element = arc_data[1].element + " " + treatment.foodType; } @@ -1007,7 +1010,7 @@ function init (client, d3) { }; renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio) { - if (!treatment.carbs && !treatment.insulin) { + if (!treatment.carbs && !treatment.protein && !treatment.fat && !treatment.insulin) { return; } diff --git a/lib/data/calcdelta.js b/lib/data/calcdelta.js index e3e0fde7052..c991bd8d602 100644 --- a/lib/data/calcdelta.js +++ b/lib/data/calcdelta.js @@ -75,7 +75,7 @@ module.exports = function calcDelta (oldData, newData) { var result = []; l = newArray.length; for (var j = 0; j < l; j++) { - if (!seen.hasOwnProperty(newArray[j].mills)) { + if (!Object.prototype.hasOwnProperty.call(seen, newArray[j].mills)) { result.push(newArray[j]); } } @@ -94,12 +94,12 @@ module.exports = function calcDelta (oldData, newData) { var changesFound = false; for (var array in compressibleArrays) { - if (compressibleArrays.hasOwnProperty(array)) { + if (Object.prototype.hasOwnProperty.call(compressibleArrays, array)) { var a = compressibleArrays[array]; - if (newData.hasOwnProperty(a)) { + if (Object.prototype.hasOwnProperty.call(newData, a)) { // if previous data doesn't have the property (first time delta?), just assign data over - if (!oldData.hasOwnProperty(a)) { + if (!Object.prototype.hasOwnProperty.call(oldData, a)) { delta[a] = newData[a]; changesFound = true; continue; @@ -125,9 +125,9 @@ module.exports = function calcDelta (oldData, newData) { var changesFound = false; for (var object in skippableObjects) { - if (skippableObjects.hasOwnProperty(object)) { + if (Object.prototype.hasOwnProperty.call(skippableObjects, object)) { var o = skippableObjects[object]; - if (newData.hasOwnProperty(o)) { + if (Object.prototype.hasOwnProperty.call(newData, o)) { if (JSON.stringify(newData[o]) !== JSON.stringify(oldData[o])) { //console.log('delta changes found on', o); changesFound = true; diff --git a/lib/data/dataloader.js b/lib/data/dataloader.js index cc4426aa6ab..1c00f998d73 100644 --- a/lib/data/dataloader.js +++ b/lib/data/dataloader.js @@ -11,8 +11,7 @@ var ONE_DAY = 86400000, function uniq(a) { var seen = {}; return a.filter(function(item) { - // eslint-disable-next-line no-prototype-builtins - return seen.hasOwnProperty(item.mills) ? false : (seen[item.mills] = true); + return Object.prototype.hasOwnProperty.call(seen, item.mills) ? false : (seen[item.mills] = true); }); } @@ -193,7 +192,6 @@ function loadActivity(ddata, ctx, callback) { } }; - var activity = []; ctx.activity.list(q, function(err, results) { if (err) { diff --git a/lib/language.js b/lib/language.js index 87fb871e1d1..c105c5239d7 100644 --- a/lib/language.js +++ b/lib/language.js @@ -535,7 +535,7 @@ function init() { ,nb: 'Siste 2 dager' ,he: 'יומיים אחרונים' ,pl: 'Ostatnie 2 dni' - ,ru: 'Последние 2 дня' + ,ru: 'Прошедшие 2 дня' ,sk: 'Posledné 2 dni' ,nl: 'Afgelopen 2 dagen' ,ko: '지난 2일' @@ -560,7 +560,7 @@ function init() { ,nb: 'Siste 3 dager' ,he: 'שלושה ימים אחרונים' ,pl: 'Ostatnie 3 dni' - ,ru: 'Последние 3 дня' + ,ru: 'Прошедшие 3 дня' ,sk: 'Posledné 3 dni' ,nl: 'Afgelopen 3 dagen' ,ko: '지난 3일' @@ -585,7 +585,7 @@ function init() { ,nb: 'Siste uke' ,he: 'שבוע אחרון' ,pl: 'Ostatni tydzień' - ,ru: 'Последняя неделя' + ,ru: 'Прошедшая неделя' ,sk: 'Posledný týždeň' ,nl: 'Afgelopen week' ,ko: '지난주' @@ -610,7 +610,7 @@ function init() { ,nb: 'Siste 2 uker' ,he: 'שבועיים אחרונים' ,pl: 'Ostatnie 2 tygodnie' - ,ru: 'Последние 2 недели' + ,ru: 'Прошедшие 2 недели' ,sk: 'Posledné 2 týždne' ,nl: 'Afgelopen 2 weken' ,ko: '지난 2주' @@ -635,7 +635,7 @@ function init() { ,nb: 'Siste måned' ,he: 'חודש אחרון' ,pl: 'Ostatni miesiąc' - ,ru: 'Последний месяц' + ,ru: 'Прошедший месяц' ,sk: 'Posledný mesiac' ,nl: 'Afgelopen maand' ,ko: '지난달' @@ -660,7 +660,7 @@ function init() { ,nb: 'Siste 3 måneder' ,he: 'שלושה חודשים אחרונים' ,pl: 'Ostatnie 3 miesiące' - ,ru: 'Последние 3 месяца' + ,ru: 'Прошедшие 3 месяца' ,sk: 'Posledné 3 mesiace' ,nl: 'Afgelopen 3 maanden' ,ko: '지난 3달' @@ -685,7 +685,7 @@ function init() { ,nb: 'between' ,he: 'between' ,pl: 'between' - ,ru: 'between' + ,ru: 'между' ,sk: 'between' ,nl: 'between' ,ko: 'between' @@ -710,7 +710,7 @@ function init() { ,nb: 'around' ,he: 'around' ,pl: 'around' - ,ru: 'around' + ,ru: 'около' ,sk: 'around' ,nl: 'around' ,ko: 'around' @@ -735,7 +735,7 @@ function init() { ,nb: 'and' ,he: 'and' ,pl: 'and' - ,ru: 'and' + ,ru: 'и' ,sk: 'and' ,nl: 'and' ,ko: 'and' @@ -894,7 +894,7 @@ function init() { } ,'Notes contain' : { cs: 'Poznámky obsahují' - ,de: 'Erläuterungen' + ,de: 'Notizen enthalten' ,he: 'ההערות מכילות' ,es: 'Contenido de las notas' ,fr: 'Notes contiennent' @@ -994,7 +994,7 @@ function init() { } ,'Display' : { cs: 'Zobraz' - ,de: 'Darstellen' + ,de: 'Anzeigen' ,es: 'Visualizar' ,fr: 'Afficher' ,el: 'Εμφάνιση' @@ -1135,7 +1135,7 @@ function init() { ,nb: 'Vises ikke' ,he: 'לא מוצג' ,pl: 'Nie jest wyświetlany' - ,ru: 'Не отражено' + ,ru: 'Не показано' ,sk: 'Nie je zobrazené' ,nl: 'Niet weergegeven' ,ko: '출력되지 않음' @@ -1219,7 +1219,7 @@ function init() { } ,'Portion' : { cs: 'Porce' - ,de: 'Portion' + ,de: 'Abschnitt' ,es: 'Porción' ,fr: 'Portion' ,el: 'Μερίδα' @@ -1260,7 +1260,7 @@ function init() { ,nb: 'Størrelse' ,he: 'גודל' ,pl: 'Rozmiar' - ,ru: 'Размер' + ,ru: 'Объем' ,sk: 'Veľkosť' ,nl: 'Grootte' ,ko: '크기' @@ -1396,7 +1396,7 @@ function init() { } ,'Week to week' : { cs: 'Week to week' - ,de: 'Week to week' + ,de: 'Woche zu Woche' ,es: 'Week to week' ,fr: 'Week to week' ,el: 'Week to week' @@ -1410,7 +1410,7 @@ function init() { ,fi: 'Week to week' ,nb: 'Week to week' ,he: 'Week to week' - ,pl: 'Week to week' + ,pl: 'Tydzień po tygodniu' ,ru: 'По неделям' ,sk: 'Week to week' ,nl: 'Week to week' @@ -1526,6 +1526,7 @@ function init() { ,fi: 'netIOB tilasto' ,bg: 'netIOB татистика' ,hr: 'netIOB statistika' + , pl: 'Statystyki netIOP' ,ru: 'статистика нетто активн инс netIOB' ,tr: 'netIOB istatistikleri' } @@ -1537,6 +1538,7 @@ function init() { ,bg: 'временните базали трябва да са показани за да се покаже тази това' ,hr: 'temp bazali moraju biti prikazani kako bi se vidio ovaj izvještaj' ,he: 'חובה לאפשר רמה בזלית זמנית כדי לרות דוח זה' + , pl: 'Tymczasowa dawka podstawowa jest wymagana aby wyświetlić ten raport' ,ru: 'для этого отчета требуется прорисовка врем базалов' ,tr: 'Bu raporu görüntülemek için geçici bazal oluşturulmalıdır' } @@ -1642,7 +1644,7 @@ function init() { } ,'Period' : { cs: 'Období' - ,de: 'Periode' + ,de: 'Zeitabschnitt' ,es: 'Periodo' ,fr: 'Période' ,el: 'Περίοδος' @@ -2017,7 +2019,7 @@ function init() { } ,'Total per day' : { cs: 'dní celkem' - ,de: 'Gesamttage' + ,de: 'Gesamt pro Tag' ,es: 'Total de días' ,fr: 'Total journalier' ,el: 'ημέρες συνολικά' @@ -2057,7 +2059,7 @@ function init() { ,nb: 'Generelt' ,he: 'סך הכל' ,pl: 'Ogółem' - ,ru: 'Всего' + ,ru: 'Суммарно' ,sk: 'Súhrn' ,nl: 'Totaal' ,ko: '전체' @@ -2257,7 +2259,7 @@ function init() { ,nb: 'Beregnet HbA1c' ,he: 'משוער A1c' ,pl: 'HbA1c przewidywany' - ,ru: 'Ожидаемый HbA1c' + ,ru: 'Ожидаемый HbA1c*' ,sk: 'Odhadované HbA1C*' ,nl: 'Geschatte HbA1C' ,ko: '예상 당화혈 색소' @@ -2410,7 +2412,7 @@ function init() { ,nb: 'Feil: Database kan ikke leses' ,he: 'שגיאה: לא ניתן לטעון בסיס נתונים' ,pl: 'Błąd, baza danych nie może być załadowana' - ,ru: 'Не удалось загрузить базу данных' + ,ru: 'Ошибка: Не удалось загрузить базу данных' ,sk: 'Chyba pri načítaní databázy' ,nl: 'FOUT: Database niet geladen' ,ko: '에러: 데이터베이스 로드 실패' @@ -2560,7 +2562,7 @@ function init() { ,fi: 'GI' ,nb: 'GI' ,pl: 'IG' - ,ru: 'ГИ' + ,ru: 'гл индекс ГИ' ,sk: 'GI' ,nl: 'Glycemische index ' ,ko: '혈당 지수' @@ -2710,7 +2712,7 @@ function init() { ,fi: 'API-avaimen tulee olla ainakin 12 merkin mittainen' ,nb: 'Din API nøkkel må være minst 12 tegn lang' ,pl: 'Twój poufny klucz API musi zawierać co majmniej 12 znaków' - ,ru: 'Ваш пароль API должен быть не менее 12 знаков' + ,ru: 'Ваш пароль API должен иметь не менее 12 знаков' ,sk: 'Vaše API heslo musí mať najmenej 12 znakov' ,nl: 'Uw API wachtwoord dient tenminste 12 karakters lang te zijn' ,ko: 'API secret는 최소 12자 이상이여야 합니다.' @@ -2838,7 +2840,7 @@ function init() { ,fi: 'Muokkaa ruokia' ,nb: 'Mat editor' ,pl: 'Edytor posiłków' - ,ru: 'Редактор епродуктов' + ,ru: 'Редактор продуктов' ,sk: 'Editor jedál' ,nl: 'Voeding beheer' ,ko: '음식 편집' @@ -3023,9 +3025,13 @@ function init() { } ,'Your API secret or token' : { fi: 'API salaisuus tai avain' + , pl: 'Twój hash API lub token' + ,ru: 'Ваш пароль API или код доступа ' } ,'Remember this device. (Do not enable this on public computers.)' : { fi: 'Muista tämä laite (Älä valitse julkisilla tietokoneilla)' + , pl: 'Zapamiętaj to urządzenie (Nie używaj tej opcji korzystając z publicznych komputerów.)' + ,ru: 'Запомнить это устройство (Не применяйте в общем доступе)' } ,'Treatments' : { cs: 'Ošetření' @@ -3146,7 +3152,7 @@ function init() { ,nb: 'Lagt inn av' ,he: 'הוזן על-ידי' ,pl: 'Wprowadzono przez' - ,ru: 'Введено от' + ,ru: 'Внесено через' ,sk: 'Zadal' ,nl: 'Ingevoerd door' ,ko: '입력 내용' @@ -3455,7 +3461,7 @@ function init() { } ,'Add from database' : { cs: 'Přidat z databáze' - ,de: 'Ergänzt aus Datenbank' + ,de: 'Ergänze aus Datenbank' ,es: 'Añadir desde la base de datos' ,fr: 'Ajouter à partir de la base de données' ,el: 'Επιλογή από τη Βάση Δεδομένων' @@ -3496,7 +3502,7 @@ function init() { ,fi: 'Käytä hiilihydraattikorjausta laskennassa' ,nb: 'Bruk karbohydratkorrigering i beregning' ,pl: 'Użyj wartość węglowodanów w obliczeniach korekty' - ,ru: 'Пользуйтесь коррекцией на углеводы при расчете' + ,ru: 'Пользоваться коррекцией на углеводы при расчете' ,sk: 'Použite korekciu na sacharidy' ,nl: 'Gebruik KH correctie in berekening' ,ko: '계산에 보정된 탄수화물을 사용하세요.' @@ -3521,7 +3527,7 @@ function init() { ,fi: 'Käytä aktiivisia hiilihydraatteja laskennassa' ,nb: 'Benytt aktive karbohydrater i beregning' ,pl: 'Użyj COB do obliczenia korekty' - ,ru: 'Учитывайте активные углеводы COB при расчете' + ,ru: 'Учитывать активные углеводы COB при расчете' ,sk: 'Použite korekciu na COB' ,nl: 'Gebruik ingenomen KH in berekening' ,ko: '계산에 보정된 COB를 사용하세요.' @@ -3546,7 +3552,7 @@ function init() { ,fi: 'Käytä aktiviivista insuliinia laskennassa' ,nb: 'Bruk aktivt insulin i beregningen' ,pl: 'Użyj IOB w obliczeniach' - ,ru: 'Учитывайте активный инсулин IOB при расчете' + ,ru: 'Учитывать активный инсулин IOB при расчете' ,sk: 'Použite IOB vo výpočte' ,nl: 'Gebruik IOB in berekening' ,ko: '계산에 IOB를 사용하세요.' @@ -3621,7 +3627,7 @@ function init() { ,fi: 'Syötä insuliinikorjaus' ,nb: 'Task inn insulinkorrigering' ,pl: 'Wprowadź wartość korekty w leczeniu' - ,ru: 'Введите коррекцию инсулина в лечение' + ,ru: 'Внести коррекцию инсулина в лечение' ,sk: 'Zadajte korekciu inzulínu do ošetrenia' ,nl: 'Voer insuline correctie toe aan behandeling' ,ko: '대처를 위해 보정된 인슐린을 입력하세요.' @@ -3731,7 +3737,7 @@ function init() { ,'60 minutes earlier' : { cs: '60 min předem' ,he: 'שישים דקות מוקדם יותר' - ,de: '60 Min. früher' + ,de: '60 Minuten früher' ,es: '60 min antes' ,fr: '60 min plus tôt' ,el: '60 λεπτά πριν' @@ -3756,7 +3762,7 @@ function init() { ,'45 minutes earlier' : { cs: '45 min předem' ,he: 'ארבעים דקות מוקדם יותר' - ,de: '45 Min. früher' + ,de: '45 Minuten früher' ,es: '45 min antes' ,fr: '45 min plus tôt' ,el: '45 λεπτά πριν' @@ -3781,7 +3787,7 @@ function init() { ,'30 minutes earlier' : { cs: '30 min předem' ,he: 'שלושים דקות מוקדם יותר' - ,de: '30 Min früher' + ,de: '30 Minuten früher' ,es: '30 min antes' ,fr: '30 min plus tôt' ,el: '30 λεπτά πριν' @@ -3806,7 +3812,7 @@ function init() { ,'20 minutes earlier' : { cs: '20 min předem' ,he: 'עשרים דקות מוקדם יותר' - ,de: '20 Min. früher' + ,de: '20 Minuten früher' ,es: '20 min antes' ,fr: '20 min plus tôt' ,el: '20 λεπτά πριν' @@ -3831,7 +3837,7 @@ function init() { ,'15 minutes earlier' : { cs: '15 min předem' ,he: 'חמש עשרה דקות מוקדם יותר' - ,de: '15 Min. früher' + ,de: '15 Minuten früher' ,es: '15 min antes' ,fr: '15 min plus tôt' ,el: '15 λεπτά πριν' @@ -3879,7 +3885,7 @@ function init() { } ,'15 minutes later' : { cs: '15 min po' - ,de: '15 Min. später' + ,de: '15 Minuten später' ,es: '15 min más tarde' ,fr: '15 min plus tard' ,el: '15 λεπτά αργότερα' @@ -3904,7 +3910,7 @@ function init() { } ,'20 minutes later' : { cs: '20 min po' - ,de: '20 Min. später' + ,de: '20 Minuten später' ,es: '20 min más tarde' ,fr: '20 min plus tard' ,el: '20 λεπτά αργότερα' @@ -3929,7 +3935,7 @@ function init() { } ,'30 minutes later' : { cs: '30 min po' - ,de: '30 Min. später' + ,de: '30 Minuten später' ,es: '30 min más tarde' ,fr: '30 min plus tard' ,el: '30 λεπτά αργότερα' @@ -3954,7 +3960,7 @@ function init() { } ,'45 minutes later' : { cs: '45 min po' - ,de: '45 Min. später' + ,de: '45 Minuten später' ,es: '45 min más tarde' ,fr: '45 min plus tard' ,el: '45 λεπτά αργότερα' @@ -3979,7 +3985,7 @@ function init() { } ,'60 minutes later' : { cs: '60 min po' - ,de: '60 Min. später' + ,de: '60 Minuten später' ,es: '60 min más tarde' ,fr: '60 min plus tard' ,el: '60 λεπτά αργότερα' @@ -4030,7 +4036,7 @@ function init() { ,'RETRO MODE' : { cs: 'V MINULOSTI' ,he: 'מצב רטרו' - ,de: 'RETRO MODUS' + ,de: 'Retro-Modus' ,es: 'Modo Retrospectivo' ,fr: 'MODE RETROSPECTIF' ,el: 'Αναδρομική Λειτουργία' @@ -4197,7 +4203,7 @@ function init() { ,fi: 'Lisää ruoka tietokannasta' ,nb: 'Legg til mat fra din database' ,pl: 'Dodaj posiłek z twojej bazy danych' - ,ru: 'Добавьте продукт из вашей базы данных' + ,ru: 'Добавить продукт из вашей базы данных' ,sk: 'Pridať jedlo z Vašej databázy' ,nl: 'Voeg voeding toe uit uw database' ,ko: '데이터베이스에서 음식을 추가하세요.' @@ -4222,7 +4228,7 @@ function init() { ,fi: 'Lataa tietokanta uudelleen' ,nb: 'Last inn databasen på nytt' ,pl: 'Odśwież bazę danych' - ,ru: 'Перезагрузите базу данных' + ,ru: 'Перезагрузить базу данных' ,sk: 'Obnoviť databázu' ,nl: 'Database opnieuw laden' ,ko: '데이터베이스 재로드' @@ -4247,7 +4253,7 @@ function init() { ,fi: 'Lisää' ,nb: 'Legg til' ,pl: 'Dodaj' - ,ru: 'Добавьте' + ,ru: 'Добавить' ,sk: 'Pridať' ,nl: 'Toevoegen' ,ko: '추가' @@ -4762,7 +4768,7 @@ function init() { ,sv: 'Kanylålder (CAGE)' ,pl: 'Czas wkłucia (CAGE)' ,pt: 'Idade da Cânula (ICAT)' - ,ru: 'Канюля отработала' + ,ru: 'Катетер проработал' ,sk: 'Zavedenie kanyly (CAGE)' ,nl: 'Canule leeftijd (CAGE)' ,ko: '캐뉼라 사용기간' @@ -5047,7 +5053,7 @@ function init() { ,nb: 'Logg en hendelse' ,he: 'הזן רשומה' ,pl: 'Wprowadź leczenie' - ,ru: 'Журнал лечения' + ,ru: 'Записать лечение' ,sk: 'Záznam ošetrenia' ,nl: 'Registreer een behandeling' ,ko: 'Treatment 로그' @@ -5056,7 +5062,7 @@ function init() { } ,'BG Check' : { cs: 'Kontrola glykémie' - ,de: 'BG-Prüfung' + ,de: 'BG-Messung' ,es: 'Control de glucemia' ,fr: 'Contrôle glycémie' ,el: 'Έλεγχος Γλυκόζης' @@ -5272,7 +5278,7 @@ function init() { ,nb: 'Pumpebytte' ,he: 'החלפת צינורית משאבה' ,pl: 'Zmiana miejsca wkłucia pompy' - ,ru: 'Смена места катетора помпы' + ,ru: 'Смена катетера помпы' ,sk: 'Výmena setu' ,nl: 'Nieuwe pomp infuus' ,ko: '펌프 위치 변경' @@ -5322,7 +5328,7 @@ function init() { ,nb: 'CGM Sensor Stop' ,he: 'CGM Sensor Stop' ,pl: 'CGM Sensor Stop' - ,ru: 'Остановка сенсора' + ,ru: 'Стоп сенсор' ,sk: 'CGM Sensor Stop' ,nl: 'CGM Sensor Stop' ,ko: 'CGM Sensor Stop' @@ -5372,7 +5378,7 @@ function init() { ,nb: 'Dexcom sensor start' ,he: 'אתחול חיישן סוכר של דקסקום' ,pl: 'Start sensora DEXCOM' - ,ru: 'Старт сенсора Декском' + ,ru: 'Старт сенсора' ,sk: 'Spustenie senzoru DEXCOM' ,nl: 'Dexcom sensor start' ,ko: 'Dexcom 센서 시작' @@ -5546,7 +5552,7 @@ function init() { ,nb: 'Insulin' ,he: 'אינסולין שניתן' ,pl: 'Podana insulina' - ,ru: 'Введенный инсулин' + ,ru: 'Введен инсулин' ,sk: 'Podaný inzulín' ,nl: 'Toegediende insuline' ,ko: '인슐린 요구량' @@ -5605,7 +5611,7 @@ function init() { } ,'View all treatments' : { cs: 'Zobraz všechny ošetření' - ,de: 'Zeige alle Eingaben' + ,de: 'Zeige alle Behandlungen' ,es: 'Visualizar todos los tratamientos' ,fr: 'Voir tous les traitements' ,el: 'Προβολή όλων των ενεργειών' @@ -5711,7 +5717,7 @@ function init() { ,nb: 'Når aktivert er alarmer aktive' ,he: 'כשמופעל התראות יכולות להישמע.' ,pl: 'Sygnalizacja dzwiękowa przy włączonym alarmie' - ,ru: 'При активации сигналы слышны' + ,ru: 'При активации может звучать сигнал' ,sk: 'Pri aktivovanom alarme znie zvuk ' ,nl: 'Als ingeschakeld kan alarm klinken' ,ko: '알림을 활성화 하면 알람이 울립니다.' @@ -5878,7 +5884,7 @@ function init() { ,'mins' : { cs: 'min' ,he: 'דקות' - ,de: 'min' + ,de: 'Minuten' ,es: 'min' ,fr: 'mins' ,el: 'λεπτά' @@ -5996,8 +6002,8 @@ function init() { ,dk: 'Vis rå BS data' ,fi: 'Näytä raaka VS tieto' ,nb: 'Vis rådata' - ,pl: 'Wyświetl surowe dane RAW' - ,ru: 'Показывать необработанные RAW данные' + ,pl: 'Wyświetl surowe dane BG' + ,ru: 'Показывать необработанные данные RAW' ,sk: 'Zobraziť RAW dáta' ,nl: 'Laat ruwe data zien' ,ko: 'Raw 혈당 데이터 보기' @@ -6138,7 +6144,7 @@ function init() { ,'Theme' : { cs: 'Téma' ,he: 'נושא' - ,de: 'Thema' + ,de: 'Aussehen' ,es: 'Tema' ,fr: 'Thème' ,el: 'Θέμα απεικόνισης' @@ -6464,7 +6470,7 @@ function init() { ,fi: 'minuutti sitten' ,nb: 'minutter siden' ,pl: 'minuta temu' - ,ru: 'мин. назад' + ,ru: 'мин назад' ,sk: 'min. pred' ,nl: 'm geleden' ,ko: '분 전' @@ -6579,7 +6585,7 @@ function init() { ,'Clean' : { cs: 'Čistý' ,he: 'נקה' - ,de: 'Rein' + ,de: 'Löschen' ,es: 'Limpio' ,fr: 'Propre' ,el: 'Καθαρισμός' @@ -6682,7 +6688,7 @@ function init() { } ,'Treatment type' : { cs: 'Typ ošetření' - ,de: 'Eingabe-Typ' + ,de: 'Behandlungstyp' ,es: 'Tipo de tratamiento' ,fr: 'Type de traitement' ,el: 'Τύπος Ενέργειας' @@ -7366,7 +7372,7 @@ function init() { ,fi: 'Etsi ja poista tapahtumat' ,pl: 'Znajdź i usuń wpisy z przyszłości' ,pt: 'Encontrar e remover entradas futuras' - ,ru: 'Найти и удалить данные с сенсора из будущего' + ,ru: 'Найти и удалить данные сенсора из будущего' ,sk: 'Nájsť a odstrániť CGM dáta v budúcnosti' ,nl: 'Zoek en verwijder behandelingen met datum in de toekomst' ,ko: '미래에 입력을 검색하고 지우세요.' @@ -7391,7 +7397,7 @@ function init() { ,fi: 'Tämä työkalu etsii ja poistaa sensorimerkinnät joiden aikamerkintä sijaitsee tulevaisuudessa.' ,pl: 'To narzędzie odnajduje i usuwa dane CGM utworzone przez uploader w przyszłości - ze złą datą/czasem.' ,pt: 'Este comando procura e remove dados de sensor futuros criados por um uploader com data ou horário errados.' - ,ru: 'Эта опция найдет и удалит данные с сенсора созданные загрузчиком с неверными датой/временем' + ,ru: 'Эта опция найдет и удалит данные сенсора созданные загрузчиком с неверными датой/временем' ,sk: 'Táto úloha nájde a odstráni CGM dáta v budúcnosti vzniknuté zle nastaveným časom uploaderu.' ,nl: 'Dit commando zoekt en verwijdert behandelingen met datum in de toekomst' ,ko: '이 작업은 잘못된 날짜/시간으로 업로드 되어 생성된 미래의 CGM 데이터를 검색하고 지우는 것입니다.' @@ -7602,6 +7608,8 @@ function init() { ,'%1 records deleted' : { hr: 'obrisano %1 zapisa' ,de: '%1 Einträge gelöscht' + , pl: '%1 rekordów zostało usuniętych' + ,ru: '% записей удалено' } ,'Clean Mongo status database' : { cs: 'Vyčištění Mongo databáze statusů' @@ -7665,7 +7673,7 @@ function init() { ,hr: 'Ovo briše sve zapise o statusima. Korisno kada se status baterije uploadera ne osvježava ispravno.' ,it: 'Questa attività elimina tutti i documenti dalla collezione "devicestatus". Utile quando lo stato della batteria uploader/xdrip non si aggiorna.' ,fi: 'Tämä työkalu poistaa kaikki tiedot statustietokannasta, mikä korjaa tilanteen, jossa puhelimen akun lataustilanne ei näy oikein.' - ,pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji devicestatus. Potrzebne jest wtedy, gdy status baterii uploadera nie jest aktualizowany' + ,pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji devicestatus. Przydatne, gdy status baterii uploadera nie jest aktualizowany.' ,pt: 'Este comando remove todos os documentos da coleção devicestatus. Útil quando o status da bateria do uploader não é atualizado corretamente.' ,ru: 'Эта опция удаляет все документы из коллекции статус устройства. Полезно когда состояние батвреи загрузчика не обновляется' ,sk: 'Táto úloha vymaže všetky záznamy z kolekcie "devicestatus". Je to vhodné keď sa stav batérie nezobrazuje správne.' @@ -7691,7 +7699,7 @@ function init() { ,fi: 'Poista kaikki tiedot' ,pl: 'Usuń wszystkie dokumenty' ,pt: 'Apagar todos os documentos' - ,ru: 'Стереть все документы' + ,ru: 'Удалить все документы' ,sk: 'Zmazať všetky záznamy' ,nl: 'Verwijder alle documenten' ,ko: '모든 문서들을 지우세요' @@ -7715,7 +7723,7 @@ function init() { ,fi: 'Poista tiedot statustietokannasta?' ,pl: 'Czy na pewno usunąć wszystkie dokumenty z kolekcji devicestatus?' ,pt: 'Apagar todos os documentos da coleção devicestatus?' - ,ru: 'Стереть все документы коллекции статус устройства?' + ,ru: 'Удалить все документы коллекции статус устройства?' ,sk: 'Zmazať všetky záznamy z kolekcie "devicestatus"?' ,ko: 'devicestatus 수집의 모든 문서들을 지우세요.' ,tr: 'Tüm Devicestatus koleksiyon belgeleri silinsin mi?' @@ -7772,69 +7780,94 @@ function init() { } ,'Delete all documents from devicestatus collection older than 30 days' : { hr: 'Obriši sve statuse starije od 30 dana' - ,ru: 'Удалить все записи коллекции devicestatus' + ,ru: 'Удалить все записи коллекции devicestatus старше 30 дней' ,de: 'Alle Dokumente der Gerätestatus-Sammlung löschen, die älter als 30 Tage sind' + , pl: 'Usuń wszystkie dokumenty z kolekcji devicestatus starsze niż 30 dni' } ,'Number of Days to Keep:' : { hr: 'Broj dana za sačuvati:' ,ru: 'Оставить дней' ,de: 'Daten löschen, die älter sind (in Tagen) als:' + , pl: 'Ilość dni do zachowania:' } ,'This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo uklanja sve statuse starije od 30 dana. Korisno kada se status baterije uploadera ne osvježava ispravno.' + , pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji devicestatus starsze niż 30 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.' ,ru: 'Это удалит все документы коллекции devicestatus которым более 30 дней. Полезно, когда статус батареи не обновляется или обновляется неверно.' ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Gerätestatus-Sammlung, die älter sind als 30 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' } ,'Delete old documents from devicestatus collection?' : { hr: 'Obriši stare statuse' ,de: 'Alte Dokumente aus der Gerätestatus-Sammlung entfernen?' - + , pl: 'Czy na pewno chcesz usunąć stare dokumenty z kolekcji devicestatus?' + ,ru: 'Удалить старыые документы коллекции devicestatus' } ,'Clean Mongo entries (glucose entries) database' : { hr: 'Obriši GUK zapise iz baze' ,de: 'Mongo-Einträge (Glukose-Einträge) Datenbank bereinigen' + , pl: 'Wyczyść bazę wpisów (wpisy glukozy) Mongo' + ,ru: 'Очистить записи данных в базе Mongo' } ,'Delete all documents from entries collection older than 180 days' : { hr: 'Obriši sve zapise starije od 180 dana' ,de: 'Alle Dokumente aus der Einträge-Sammlung löschen, die älter sind als 180 Tage' + , pl: 'Usuń wszystkie dokumenty z kolekcji wpisów starsze niż 180 dni' + ,ru: 'Удалить все документы коллекции entries старше 180 дней ' } ,'This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo briše sve zapise starije od 180 dana. Korisno kada se status baterije uploadera ne osvježava.' ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Einträge-Sammlung, die älter sind als 180 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' + , pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji wpisów starsze niż 180 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.' + ,ru: 'Это удалит все документы коллекции entries старше 180 дней. Полезно, когда статус батареи загрузчика должным образом не обновляется' } ,'Delete old documents' : { hr: 'Obriši stare zapise' ,de: 'Alte Dokumente löschen' + , pl: 'Usuń stare dokumenty' + ,ru: 'Удалить старые документы' } ,'Delete old documents from entries collection?' : { hr: 'Obriši stare zapise?' ,de: 'Alte Dokumente aus der Einträge-Sammlung entfernen?' + , pl: 'Czy na pewno chcesz usunąć stare dokumenty z kolekcji wpisów?' + ,ru: 'Удалить старые документы коллекции entries?' } ,'%1 is not a valid number' : { hr: '%1 nije valjan broj' ,de: '%1 ist keine gültige Zahl' ,he: 'זה לא מיספר %1' + , pl: '%1 nie jest poprawną liczbą' + ,ru: '% не является допустимым значением' } ,'%1 is not a valid number - must be more than 2' : { hr: '%1 nije valjan broj - mora biti veći od 2' ,de: '%1 ist keine gültige Zahl - Eingabe muss größer als 2 sein' + , pl: '%1 nie jest poprawną liczbą - musi być większe od 2' + ,ru: '% не является допустимым значением - должно быть больше 2' } ,'Clean Mongo treatments database' : { hr: 'Obriši tretmane iz baze' ,de: 'Mongo-Behandlungsdatenbank bereinigen' + , pl: 'Wyczyść bazę leczenia Mongo' + ,ru: 'Очистить базу лечения Mongo' } ,'Delete all documents from treatments collection older than 180 days' : { hr: 'Obriši tretmane starije od 180 dana iz baze' ,de: 'Alle Dokumente aus der Behandlungs-Sammlung löschen, die älter sind als 180 Tage' + , pl: 'Usuń wszystkie dokumenty z kolekcji leczenia starsze niż 180 dni' + ,ru: 'Удалить все документы коллекции treatments старше 180 дней' } ,'This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.' : { hr: 'Ovo briše sve tretmane starije od 180 dana iz baze. Korisno kada se status baterije uploadera ne osvježava.' ,de: 'Diese Aufgabe entfernt alle Dokumente aus der Behandlungs-Sammlung, die älter sind als 180 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.' + , pl: 'To narzędzie usuwa wszystkie dokumenty z kolekcji leczenia starsze niż 180 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.' + ,ru: 'Это удалит все документы коллекции treatments старше 180 дней. Полезно, когда статус батареи загрузчика не обновляется должным образом' } ,'Delete old documents from treatments collection?' : { hr: 'Obriši stare tretmane?' - ,ru: 'Удалить старые документы из коллекции лечения?' + ,ru: 'Удалить старые документы из коллекции treatments?' ,de: 'Alte Dokumente aus der Behandlungs-Sammlung entfernen?' + , pl: 'Czy na pewno chcesz usunąć stare dokumenty z kolekcji leczenia?' } ,'Admin Tools' : { cs: 'Nástroje pro správu' @@ -8115,7 +8148,7 @@ function init() { ,fi: 'Basaalin määrä' ,pl: 'Dawka podstawowa' ,pt: 'Valor da basal' - ,ru: 'величина временного базалал' + ,ru: 'величина временного базала' ,sk: 'Hodnota bazálu' ,nl: 'Basaal snelheid' ,ko: 'Basal' @@ -8304,7 +8337,7 @@ function init() { ,hr: 'Naslov' ,pl: 'Nazwa' ,pt: 'Título' - ,ru: 'Наименование' + ,ru: 'Название' ,sk: 'Názov' ,nl: 'Titel' ,ko: '제목' @@ -8522,7 +8555,7 @@ function init() { ,hr: 'Predstavlja uobičajeno trajanje djelovanje inzulina. Varira po vrstama inzulina i osobama. Tipično je to 3-4 sata za inzuline u pumpama za većinu osoba. Ponekad se naziva i vijek inzulina' ,pl: 'Odzwierciedla czas działania insuliny. Może różnić się w zależności od chorego i rodzaju insuliny. Zwykle są to 3-4 godziny dla insuliny podawanej pompą u większości chorych. Inna nazwa to czas trwania insuliny.' ,pt: 'Representa a tempo típico durante o qual a insulina tem efeito. Varia de acordo com o paciente e tipo de insulina. Tipicamente 3-4 horas para a maioria das insulinas usadas em bombas e dos pacientes. Algumas vezes chamada de tempo de vida da insulina' - ,ru: 'Представляет типичную продолжительность действия инсулина. Зависит от пациента и от типа инсулина. Обычно 3-4 часа для большинства помповых инсулинов и большинства пациентов' + ,ru: 'Отражает типичную продолжительность действия инсулина. Зависит от пациента и от типа инсулина. Обычно 3-4 часа для большинства помповых инсулинов и большинства пациентов' ,sk: 'Predstavuje typickú dobu počas ktorej inzulín pôsobí. Býva rôzna od pacienta a od typu inzulínu. Zvyčajne sa pohybuje medzi 3-4 hodinami u pacienta s pumpou.' ,nl: 'Geeft de werkingsduur van de insuline in het lichaam aan. Dit verschilt van patient tot patient er per soort insuline, algemeen gemiddelde is 3 tot 4 uur. ' ,ko: '인슐린이 작용하는 지속시간을 나타냅니다. 사람마다 그리고 인슐린 종류에 따라 다르고 일반적으로 3~4시간간 동안 지속되며 인슐린 작용 시간(Insulin lifetime)이라고 불리기도 합니다.' @@ -8761,7 +8794,7 @@ function init() { ,hr: 'Bazali [jedinica/sat]' ,pl: 'Dawka podstawowa [j/h]' ,pt: 'Taxas de basal [unidades/hora]' - ,ru: 'Базал ед/час' + ,ru: 'Базал [unit/hour]' ,sk: 'Bazál [U/hod]' ,nl: 'Basaal snelheid [eenheden/uur]' ,ko: 'Basal 비율[unit/hour]' @@ -8858,7 +8891,7 @@ function init() { ,hr: 'Iscrtaj bazale' ,pl: 'Zmiana dawki bazowej' ,pt: 'Renderizar basal' - ,ru: 'отрисовать базал' + ,ru: 'Отображать базал' ,sk: 'Zobrazenie bazálu' ,nl: 'Toon basaal' ,ko: 'Basal 사용하기' @@ -8906,7 +8939,7 @@ function init() { ,hr: 'Izračun je u ciljanom rasponu.' ,pl: 'Obliczenie mieści się w zakresie docelowym' ,pt: 'O cálculo está dentro da meta' - ,ru: 'Расчет в целевом диапазоне ' + ,ru: 'Расчет в целевом диапазоне' ,sk: 'Výpočet je v cieľovom rozsahu.' ,nl: 'Berekening valt binnen doelwaards' ,ko: '계산은 목표 범위 안에 있습니다.' @@ -9026,7 +9059,7 @@ function init() { ,hr: 'Vremenski rasponi donje ciljane i gornje ciljane vrijednosti nisu ispravni. Vrijednosti vraćene na zadano.' ,pl: 'Zakres czasu w docelowo niskim i wysokim przedziale nie są dopasowane. Przywrócono wartości domyślne' ,pt: 'Os intervalos de tempo da meta inferior e da meta superior não conferem. Os valores padrão serão restaurados.' - ,ru: 'Диапазон времени нижних и верхних целевых значений не совпадают. Восстановлены значения по умолчанию' + ,ru: 'Диапазоны времени нижних и верхних целевых значений не совпадают. Восстановлены значения по умолчанию' ,sk: 'Časové rozsahy pre cieľové glykémie sa nezhodujú. Hodnoty nastavené na východzie.' ,ko: '설정한 저혈당과 고혈당의 시간 범위와 일치하지 않습니다. 값은 초기 설정값으로 다시 저장 될 것입니다.' ,it: 'Intervalli di tempo della glicemia obiettivo inferiore e superiore non corretti. Valori ripristinati a quelli standard.' @@ -9136,7 +9169,7 @@ function init() { ,nb: 'IKH' ,fr: 'I:C' ,ro: 'ICR' - ,de: 'I:KH' + ,de: 'IE:KH' ,dk: 'I:C' ,es: 'I:C' ,sv: 'I:C' @@ -9169,7 +9202,7 @@ function init() { ,pl: 'ISF' ,pt: 'ISF' ,nl: 'ISF' - ,ru: 'Чувствительность к инсулину ISF' + ,ru: 'Чувств к инс ISF' ,sk: 'ISF' ,ko: 'ISF' ,it: 'ISF' @@ -9192,7 +9225,7 @@ function init() { ,hr: 'Dual bolus' ,fi: 'Yhdistelmäbolus' ,pt: 'Bolus duplo' - ,ru: 'Комбинированный болюс' + ,ru: 'Комбинир болюс' ,sk: 'Kombinovaný bolus' ,nl: 'Pizza Bolus' ,ko: 'Combo Bolus' @@ -9506,7 +9539,7 @@ function init() { ,bg: 'Да променя ли времето на ВХ с %1?' ,hr: 'Promijeni vrijeme UGH na %1?' ,fi: 'Muuta hiilihydraattien aika? Uusi: %1' - ,ru: 'Изменить время подачи углеводов на % ?' + ,ru: 'Изменить время приема углеводов на % ?' ,sk: 'Zmeniť čas sacharidov na %1 ?' ,pl: 'Zmień czas węglowodanów na %1 ?' ,pt: 'Alterar horário do carboidrato para %1 ?' @@ -9698,7 +9731,7 @@ function init() { ,sv: 'Fel profilinställning.\nIngen profil vald för vald tid.\nOmdirigerar för att skapa ny profil.' ,nb: 'Feil profilinstilling.\nIngen profil valgt for valgt tid.\nVideresender for å lage ny profil.' ,fi: 'Väärä profiiliasetus tai profiilia ei löydy.\nSiirrytään profiilin muokkaamiseen uuden profiilin luontia varten.' - ,ru: 'Неверные настройки профиля. Для отображаемого времени не определен профиль. Переход к редактору профиля для создания нового' + ,ru: 'Переход к редактору профиля для создания нового' ,sk: 'Zle nastavený profil.\nK zobrazenému času nieje definovaný žiadny profil.\nPresmerovávam na vytvorenie profilu.' ,pl: 'Złe ustawienia profilu.\nDla podanego czasu nie zdefiniowano profilu.\nPrzekierowuję do edytora profili aby utworzyć nowy.' ,pt: 'Configuração de perfil incorreta. \nNão há perfil definido para mostrar o horário. \nRedirecionando para o editor de perfil para criar um perfil novo.' @@ -9726,11 +9759,11 @@ function init() { ,nl: 'Pomp' ,ko: '펌프' ,fi: 'Pumppu' + , pl: 'Pompa' ,pt: 'Bomba' ,it: 'Pompa' ,tr: 'Pompa' ,zh_cn: '胰岛素泵' - ,pl: 'Pompa' } ,'Sensor Age' : { cs: 'Stáří senzoru (SAGE)' @@ -9746,16 +9779,16 @@ function init() { ,bg: 'Възраст на сензора (ВС)' ,hr: 'Starost senzora' ,ro: 'Vechimea senzorului' - ,ru: 'Сенсор отработал' + ,ru: 'Сенсор работает' ,nl: 'Sensor leeftijd' ,ko: '센서 사용 기간' ,fi: 'Sensorin ikä' + , pl: 'Wiek sensora' ,pt: 'Idade do sensor' ,it: 'SAGE - Durata Sensore' ,tr: '(SAGE) Sensör yaşı ' ,zh_cn: '探头使用时间(SAGE)' ,zh_tw: '探頭使用時間(SAGE)' - ,pl: 'Wiek sensora' } ,'Insulin Age' : { cs: 'Stáří inzulínu (IAGE)' @@ -9771,16 +9804,16 @@ function init() { ,bg: 'Възраст на инсулина (ВИ)' ,hr: 'Starost inzulina' ,ro: 'Vechimea insulinei' - ,ru: 'инсулин отработал' + ,ru: 'Инсулин работает' ,ko: '인슐린 사용 기간' ,fi: 'Insuliinin ikä' + , pl: 'Wiek insuliny' ,pt: 'Idade da insulina' ,it: 'IAGE - Durata Insulina' ,nl: 'Insuline ouderdom (IAGE)' ,tr: '(IAGE) İnsülin yaşı' ,zh_cn: '胰岛素使用时间(IAGE)' ,zh_tw: '胰島素使用時間(IAGE)' - ,pj: 'Wiek insuliny' } ,'Temporary target' : { cs: 'Dočasný cíl' @@ -9800,6 +9833,7 @@ function init() { ,nl: 'Tijdelijk doel' ,ko: '임시 목표' ,fi: 'Tilapäinen tavoite' + , pl: 'Tymczasowy cel' ,pt: 'Meta temporária' ,it: 'Obiettivo Temporaneo' ,tr: 'Geçici hedef' @@ -9823,11 +9857,11 @@ function init() { ,ru: 'Причина' ,ko: '근거' ,fi: 'Syy' + , pl: 'Powód' ,pt: 'Razão' ,it: 'Ragionare' ,tr: 'Neden' // Gerekçe ,zh_cn: '原因' - ,pl: 'Powód' } ,'Eating soon' : { cs: 'Následuje jídlo' @@ -9836,21 +9870,21 @@ function init() { ,fr: 'Repas sous peu' ,sv: 'Snart matdags' ,nb: 'Snart tid for mat' - ,de: 'Bald essend' + ,de: 'Bald essen' ,dk: 'Spiser snart' ,es: 'Comer pronto' ,bg: 'Ядене скоро' ,hr: 'Uskoro jelo' ,ro: 'Mâncare în curând' - ,ru: 'Приближается прием пищи' + ,ru: 'Ожидаемый прием пищи' ,nl: 'Binnenkort eten' ,ko: '편집 중' ,fi: 'Syödään pian' + , pl: 'Zjedz wkrótce' ,pt: 'Refeição em breve' ,it: 'Mangiare prossimamente' ,tr: 'Yakında yemek' // Kısa zamanda yemek yenecek ,zh_cn: '接近用餐时间' - ,pl: 'Zjedz wkrótce' } ,'Top' : { cs: 'Horní' @@ -9980,7 +10014,7 @@ function init() { ,ro: 'Bazala obișnuită:' ,el: 'Βασική Ινσουλίνη' ,es: 'Insulina basal básica' - ,ru: 'Основной базальный инсулин' + ,ru: 'Профильный базальный инсулин' ,sv: 'Basalinsulin:' ,nb: 'Basalinsulin:' ,hr: 'Osnovni bazal:' @@ -10012,13 +10046,13 @@ function init() { ,fi: 'Positiivinen tilapäisbasaali:' ,de: 'Positives temporäres Basal Insulin:' ,dk: 'Positiv midlertidig basalinsulin:' + , pl: 'Zwiększona bazowa dawka insuliny' ,pt: 'Insulina basal temporária positiva:' ,sk: 'Pozitívny dočasný bazálny inzulín:' ,it: 'Insulina basale temp positiva:' ,nl: 'Extra tijdelijke basaal insuline' ,tr: 'Pozitif geçici bazal insülin:' ,zh_cn: '实际临时基础率胰岛素' - , pl: 'Zwiększona bazowa dawka insuliny' } ,'Negative temp basal insulin:' : { cs:'Negativní dočasný bazální inzulín:' @@ -10036,13 +10070,13 @@ function init() { ,fi: 'Negatiivinen tilapäisbasaali:' ,de: 'Negatives temporäres Basal Insulin:' ,dk: 'Negativ midlertidig basalinsulin:' + , pl: 'Zmniejszona bazowa dawka insuliny' ,pt: 'Insulina basal temporária negativa:' ,sk: 'Negatívny dočasný bazálny inzulín:' ,it: 'Insulina basale Temp negativa:' ,nl: 'Negatieve tijdelijke basaal insuline' ,tr: 'Negatif geçici bazal insülin:' ,zh_cn: '其余临时基础率胰岛素' - , pl: 'Zmniejszona bazowa dawka insuliny' } ,'Total basal insulin:' : { cs: 'Celkový bazální inzulín:' @@ -10060,13 +10094,13 @@ function init() { ,fi: 'Basaali yhteensä:' ,de: 'Gesamt Basal Insulin:' ,dk: 'Total daglig basalinsulin:' + , pl: 'Całkowita ilość bazowej dawki insuliny' ,pt: 'Insulina basal total:' ,sk: 'Celkový bazálny inzulín:' ,it: 'Insulina Basale Totale:' ,nl: 'Totaal basaal insuline' ,tr: 'Toplam bazal insülin:' ,zh_cn: '基础率胰岛素合计' - , pl: 'Całkowita ilość bazowej dawki insuliny' } ,'Total daily insulin:' : { cs:'Celkový denní inzulín:' @@ -10084,13 +10118,13 @@ function init() { ,de: 'Gesamtes tägliches Insulin:' ,dk: 'Total dagsdosis insulin' ,es: 'Total Insulina diaria:' + ,pl: 'Całkowita dzienna ilość insuliny' ,pt: 'Insulina diária total:' ,sk: 'Celkový denný inzulín:' ,it: 'Totale giornaliero d\'insulina:' ,nl: 'Totaal dagelijkse insuline' ,tr: 'Günlük toplam insülin:' ,zh_cn: '每日胰岛素合计' - ,pl: 'Całkowita dzienna ilość insuliny' } ,'Unable to %1 Role' : { // PUT or POST cs: 'Chyba volání %1 Role:' @@ -10106,6 +10140,7 @@ function init() { ,fi: '%1 operaatio roolille opäonnistui' ,de: 'Unpassend zu %1 Rolle' ,dk: 'Kan ikke slette %1 rolle' + ,pl: 'Nie można %1 roli' ,pt: 'Função %1 não foi possível' ,sk: 'Chyba volania %1 Role' ,ko: '%1로 비활성' @@ -10113,7 +10148,6 @@ function init() { ,nl: 'Kan %1 rol niet verwijderen' ,tr: '%1 Rolü yapılandırılamadı' ,zh_cn: '%1角色不可用' - ,pl: 'Nie można %1 roli' } ,'Unable to delete Role' : { cs: 'Nelze odstranit Roli:' @@ -10152,6 +10186,7 @@ function init() { ,fi: 'Tietokanta sisältää %1 roolia' ,de: 'Datenbank enthält %1 Rollen' ,dk: 'Databasen indeholder %1 roller' + , pl: 'Baza danych zawiera %1 ról' ,pt: 'Banco de dados contém %1 Funções' ,sk: 'Databáza obsahuje %1 rolí' ,ko: '데이터베이스가 %1 포함' @@ -10159,7 +10194,6 @@ function init() { ,nl: 'Database bevat %1 rollen' ,tr: 'Veritabanı %1 rol içerir' ,zh_cn: '数据库包含%1个角色' - , pl: 'Baza danych zawiera %1 ról' } ,'Edit Role' : { cs:'Editovat roli' @@ -10175,6 +10209,7 @@ function init() { ,de: 'Rolle editieren' ,dk: 'Rediger rolle' ,es: 'Editar Rol' + ,pl: 'Edycja roli' ,pt: 'Editar Função' ,sk: 'Editovať rolu' ,ko: '편집 모드' @@ -10182,7 +10217,6 @@ function init() { ,nl: 'Pas rol aan' ,tr: 'Rolü düzenle' ,zh_cn: '编辑角色' - ,pl: 'Edycja roli' } ,'admin, school, family, etc' : { cs: 'administrátor, škola, rodina atd...' @@ -10198,6 +10232,7 @@ function init() { ,fi: 'ylläpitäjä, koulu, perhe jne' ,de: 'Administrator, Schule, Familie, etc' ,dk: 'Administrator, skole, familie, etc' + ,pl: 'administrator, szkoła, rodzina, itp' ,pt: 'Administrador, escola, família, etc' ,sk: 'administrátor, škola, rodina atď...' ,ko: '관리자, 학교, 가족 등' @@ -10205,7 +10240,6 @@ function init() { ,nl: 'admin, school, familie, etc' ,tr: 'yönetici, okul, aile, vb' ,zh_cn: '政府、学校、家庭等' - ,pl: 'administrator, szkoła, rodzina, itp' } ,'Permissions' : { cs: 'Oprávnění' @@ -10221,6 +10255,7 @@ function init() { ,de: 'Berechtigungen' ,dk: 'Rettigheder' ,es: 'Permisos' + ,pl: 'Uprawnienia' ,pt: 'Permissões' ,sk: 'Oprávnenia' ,ko: '허가' @@ -10228,7 +10263,6 @@ function init() { ,nl: 'Rechten' ,tr: 'İzinler' ,zh_cn: '权限' - ,pl: 'Uprawnienia' } ,'Are you sure you want to delete: ' : { cs: 'Opravdu vymazat: ' @@ -10241,9 +10275,10 @@ function init() { ,sv: 'Är du säker att du vill ta bort:' ,nb: 'Er du sikker på at du vil slette:' ,fi: 'Oletko varmat että haluat tuhota: ' - ,de: ' Sind sie sicher, das Sie löschen wollen:' + ,de: 'Sind sie sicher, das Sie löschen wollen:' ,dk: 'Er du sikker på at du vil slette:' ,es: 'Seguro que quieres eliminarlo:' + ,pl: 'Jesteś pewien, że chcesz usunąć:' ,pt: 'Tem certeza de que deseja apagar:' ,sk: 'Naozaj zmazať:' ,ko: '정말로 삭제하시겠습니까: ' @@ -10251,7 +10286,6 @@ function init() { ,nl: 'Weet u het zeker dat u wilt verwijderen?' ,tr: 'Silmek istediğinizden emin misiniz:' ,zh_cn: '你确定要删除:' - ,pl: 'Jesteś pewien, że chcesz usunąć:' } ,'Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.' : { cs: 'Každá role má 1 nebo více oprávnění. Oprávnění * je zástupný znak, oprávnění jsou hiearchie používající : jako oddělovač.' @@ -10266,6 +10300,7 @@ function init() { ,fi: 'Jokaisella roolilla on yksi tai useampia oikeuksia. * on jokeri (tunnistuu kaikkina oikeuksina), oikeudet ovat hierarkia joka käyttää : merkkiä erottimena.' ,de: 'Jede Rolle hat eine oder mehrere Berechtigungen. Die * Berechtigung ist ein Platzhalter, Berechtigungen sind hierachrchisch mit : als Separator.' ,es: 'Cada Rol tiene uno o más permisos. El permiso * es un marcador de posición y los permisos son jerárquicos con : como separador.' + , pl: 'Każda rola będzie mieć 1 lub więcej uprawnień. Symbol * uprawnia do wszystkiego. Uprawnienia są hierarchiczne, używając : jako separatora.' ,pt: 'Cada função terá uma ou mais permissões. A permissão * é um wildcard, permissões são uma hierarquia utilizando * como um separador.' ,sk: 'Každá rola má 1 alebo viac oprávnení. Oprávnenie * je zástupný znak, oprávnenia sú hierarchie používajúce : ako oddelovač.' ,ko: '각각은 1 또는 그 이상의 허가를 가지고 있습니다. 허가는 예측이 안되고 구분자로 : 사용한 계층이 있습니다' @@ -10273,7 +10308,6 @@ function init() { ,nl: 'Elke rol heeft mintens 1 machtiging. De * machtiging is een wildcard, machtigingen hebben een hyrarchie door gebruik te maken van : als scheidingsteken.' ,tr: 'Her rolün bir veya daha fazla izni vardır.*izni bir yer tutucudur ve izinler ayırıcı olarak : ile hiyerarşiktir.' ,zh_cn: '每个角色都具有一个或多个权限。权限设置时使用*作为通配符,层次结构使用:作为分隔符。' - , pl: 'Każda rola będzie mieć 1 lub więcej uprawnień. Symbol * uprawnia do wszystkiego. Uprawnienia są hierarchiczne, używając : jako separatora.' } ,'Add new Role' : { cs: 'Přidat novou roli' @@ -10490,7 +10524,7 @@ function init() { ,hr: 'Baza sadrži %1 subjekata' ,fr: 'La base de données contient %1 utilisateurs' ,fi: 'Tietokanta sisältää %1 käyttäjää' - ,ru: 'База данных содержит %1 субъект(а/ов)' + ,ru: 'База данных содержит %1 субъекта/ов' ,ro: 'Baza de date are %1 subiecți' ,sv: 'Databasen innehåller %1 ämnen' ,nb: 'Databasen inneholder %1 ressurser' @@ -10724,7 +10758,7 @@ function init() { ,sv: 'Tyst i %1 minuter' ,nb: 'Stille i %1 minutter' ,fi: 'Hiljennä %1 minuutiksi' - ,de: 'Inaktivität für %1 Minuten' + ,de: 'Ruhe für %1 Minuten' ,dk: 'Stilhed i %1 minutter' ,pt: 'Silencir por %1 minutos' ,es: 'Silenciado por %1 minutos' @@ -10929,7 +10963,7 @@ function init() { ,dk: 'Aktiv midlertidig basal start' ,ro: 'Start bazală temporară activă' ,fr: 'Début du débit basal temporaire' - ,ru: 'Старт активного временного базала' + ,ru: 'Старт актуального временного базала' ,sk: 'Štart dočasného bazálu' ,sv: 'Aktiv tempbasal start' ,nb: 'Aktiv midlertidig basal start' @@ -10952,7 +10986,7 @@ function init() { ,dk: 'Aktiv midlertidig basal varighed' ,ro: 'Durata bazalei temporare active' ,fr: 'Durée du débit basal temporaire' - ,ru: 'Длительность активного временного базала' + ,ru: 'Длительность актуального временного базала' ,sk: 'Trvanie dočasného bazálu' ,sv: 'Aktiv tempbasal varaktighetstid' ,nb: 'Aktiv midlertidig basal varighet' @@ -10975,7 +11009,7 @@ function init() { ,dk: 'Resterende tid for aktiv midlertidig basal' ,ro: 'Rest de bazală temporară activă' ,fr: 'Durée restante de débit basal temporaire' - ,ru: 'Остаток акивного временного базала' + ,ru: 'Остается актуального временного базала' ,sk: 'Zostatok dočasného bazálu' ,sv: 'Återstående tempbasaltid' ,nb: 'Gjenstående midlertidig basal tid' @@ -11044,7 +11078,7 @@ function init() { ,dk: 'Aktiv kombibolus start' ,ro: 'Start bolus combinat activ' ,fr: 'Début de Bolus Duo/Combo' - ,ru: 'Старт активного комбо болюса' + ,ru: 'Старт активного комбинир болюса' ,sv: 'Aktiv kombobolus start' ,nb: 'Kombinasjonsbolus start' ,fi: 'Aktiivisen yhdistelmäboluksen alku' @@ -11067,7 +11101,7 @@ function init() { ,dk: 'Aktiv kombibolus varighed' ,ro: 'Durată bolus combinat activ' ,fr: 'Durée du Bolus Duo/Combo' - ,ru: 'Длительность активного комбо болюса' + ,ru: 'Длительность активного комбинир болюса' ,sv: 'Aktiv kombibolus varaktighet' ,es: 'Duración del Combo-Bolo activo' ,nb: 'Kombinasjonsbolus varighet' @@ -11090,7 +11124,7 @@ function init() { ,dk: 'Resterende aktiv kombibolus' ,ro: 'Rest de bolus combinat activ' ,fr: 'Activité restante du Bolus Duo/Combo' - ,ru: 'Остаток активного комбо болюса' + ,ru: 'Остается активного комбинир болюса' ,sv: 'Återstående aktiv kombibolus' ,es: 'Restante Combo-Bolo activo' ,nb: 'Gjenstående kombinasjonsbolus' @@ -11120,7 +11154,7 @@ function init() { ,pt: 'Diferença de glicemia' ,es: 'Diferencia de glucemia' ,sk: 'Zmena glykémie' - ,bg: 'Изменение КЗ' + ,bg: 'Дельта ГК' ,hr: 'GUK razlika' ,ko: '혈당 차이' ,it: 'BG Delta' @@ -11355,7 +11389,7 @@ function init() { ,fr: 'Vérifier la glycémie, bolus nécessaire ?' ,bg: 'Провери КЗ, не е ли време за болус?' ,hr: 'Provjeri GUK, vrijeme je za bolus?' - ,ru: 'Проверьте СК, дать болюс?' + ,ru: 'Проверьте ГК, дать болюс?' ,sv: 'Kontrollera BS, dags att ge bolus?' ,nb: 'Sjekk blodsukker, på tide med bolus?' ,fi: 'Tarkista VS, aika bolustaa?' @@ -11545,13 +11579,13 @@ function init() { ,nb: 'Insulin tilsvarende %1U mer enn det trengs for å nå lavt mål, karbohydrater ikke medregnet' ,nl: 'Insulineoverschot van %1U om laag doel te behalen (excl. koolhydraten)' ,fi: 'Liikaa insuliinia: %1U enemmän kuin tarvitaan tavoitteeseen pääsyyn (huomioimatta hiilihydraatteja)' + , pl: 'Nadmiar insuliny, %1J więcej niż potrzeba, aby osiągnąć cel dolnej granicy, nie biorąc pod uwagę węglowodanów' ,pt: 'Excesso de insulina equivalente a %1U além do necessário para atingir a meta inferior, sem levar em conta carboidratos' ,sk: 'Nadbytok inzulínu o %1U viac ako je potrebné na dosiahnutie spodnej cieľovej hranice. Neráta sa so sacharidmi.' ,ko: '낮은 혈당 목표에 도달하기 위해 필요한 인슐린양보다 %1U의 인슐린 양이 초과 되었고 탄수화물 양이 초과되지 않았습니다.' ,it: 'L\'eccesso d\'insulina equivalente %1U più che necessari per raggiungere l\'obiettivo basso, non rappresentano i carboidrati.' ,tr: 'Fazla insülin: Karbonhidratları dikkate alınmadan, alt hedefe ulaşmak için gerekenden %1U\'den daha fazla' //??? ,zh_cn: '胰岛素超过至血糖下限目标所需剂量%1单位,不计算碳水化合物' - , pl: 'Nadmiar insuliny, %1J więcej niż potrzeba, aby osiągnąć cel dolnej granicy, nie biorąc pod uwagę węglowodanów' } ,'Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS' : { cs:'Nadbytek inzulínu: o %1U více, než na dosažení spodní hranice cíle. UJISTĚTE SE, ŽE JE TO POKRYTO SACHARIDY' @@ -11562,7 +11596,7 @@ function init() { ,fr: 'Insuline en excès: %1U de plus que nécessaire pour atteindre la cible inférieure, ASSUREZ UN APPORT SUFFISANT DE GLUCIDES' ,bg: 'Излишният инсулин %1U е повече от необходимия за достигане до долната граница, ПРОВЕРИ ДАЛИ IOB СЕ ПОКРИВА ОТ ВЪГЛЕХИДРАТИТЕ' ,hr: 'Višak inzulina je %1U više nego li je potrebno da se postigne donja ciljana granica, OBAVEZNO POKRIJTE SA UGH' - ,ru: 'Избыток инсулина, равного %1U, необходимого для достижения нижнего целевого значения, ПОКРОЙТЕ IOB ИНСУЛИН В ОРГАНИЗМЕ УГЛЕВОДАМИ' + ,ru: 'Избыток инсулина, равного %1U, необходимого для достижения нижнего целевого значения, ПОКРОЙТЕ АКТИВН IOB ИНСУЛИН УГЛЕВОДАМИ' ,sv: 'Överskott av insulin motsvarande %1U mer än nödvändigt för att nå lågt målvärde, SÄKERSTÄLL ATT IOB TÄCKS AV KOLHYDRATER' ,es: 'Exceso de insulina en %1U más de la necesaria para alcanzar objetivo inferior. ASEGÚRESE QUE LA INSULINA ACTIVA IOB ESTA CUBIERTA POR CARBOHIDRATOS' ,nb: 'Insulin tilsvarende %1U mer enn det trengs for å nå lavt mål, PASS PÅ AT AKTIVT INSULIN ER DEKKET OPP MED KARBOHYDRATER' @@ -11597,7 +11631,7 @@ function init() { ,it: 'Riduzione 1U% necessaria d\'insulina attiva per raggiungere l\'obiettivo basso, troppa basale?' ,tr: 'Alt KŞ hedefi için %1U aktif insülin azaltılmalı, bazal oranı çok mu yüksek?' ,zh_cn: '活性胰岛素已可至血糖下限目标,需减少%1单位,基础率过高?' - , pl: '%1J potrzebnej redukcji w aktywnej insulinie, aby osiągnąć niski cel dolnej granicy, Za duża dawka podstawowa ?' + ,pl: '%1J potrzebnej redukcji w aktywnej insulinie, aby osiągnąć niski cel dolnej granicy, Za duża dawka podstawowa ?' } ,'basal adjustment out of range, give carbs?' : { cs:'úprava změnou bazálu není možná. Podat sacharidy?' @@ -11608,7 +11642,7 @@ function init() { ,fr: 'ajustement de débit basal hors de limites, prenez des glucides?' ,bg: 'Корекция на базала не е възможна, добавка на въглехидрати? ' ,hr: 'prilagodba bazala je izvan raspona, dodati UGH?' - ,ru: 'Корректировка базы вне диапазона, добавить углеводов?' + ,ru: 'Корректировка базала вне диапазона, добавить углеводов?' ,sv: 'basaländring utanför gräns, ge kolhydrater?' ,es: 'ajuste basal fuera de rango, dar carbohidratos?' ,nb: 'basaljustering utenfor tillatt område, gi karbohydrater?' @@ -11631,7 +11665,7 @@ function init() { ,fr: 'ajustement de débit basal hors de limites, prenez un bolus?' ,bg: 'Корекция на базала не е възможна, добавка на болус? ' ,hr: 'prilagodna bazala je izvan raspona, dati bolus?' - ,ru: 'Корректировка базы вне диапазона, добавить болюс?' + ,ru: 'Корректировка базала вне диапазона, добавить болюс?' ,sv: 'basaländring utanför gräns, ge bolus?' ,nb: 'basaljustering utenfor tillatt område, gi bolus?' ,fi: 'säätö liian suuri, anna bolus?' @@ -11702,7 +11736,7 @@ function init() { ,fr: 'Glycémie cible projetée %1 ' ,bg: 'Предполагаемата КЗ %1 в граници' ,hr: 'Procjena GUK %1 cilja' - ,ru: 'Расчетная гликемия %1' + ,ru: 'Расчетная целевая гликемия %1' ,sv: 'Önskat BS %1 mål' ,nb: 'Ønsket BS %1 mål' ,fi: 'Laskettu VS %1 tavoitteen' @@ -11771,7 +11805,7 @@ function init() { ,fr: 'ou ajuster le débit basal' ,bg: 'или корекция на базала' ,hr: 'ili prilagodba bazala' - ,ru: 'или корректировать базу' + ,ru: 'или корректировать базал' ,sv: 'eller justera basal' ,nb: 'eller justere basal' ,fi: 'tai säädä basaalia' @@ -11886,7 +11920,7 @@ function init() { ,bg: 'Времето за смяна на сет просрочено' ,hr: 'Prošao rok za zamjenu kanile!' ,fr: 'Dépassement de date de changement de canule!' - ,ru: 'Срок замены катетера истек' + ,ru: 'Срок замены катетера помпы истек' ,sv: 'Infusionsset, bytestid överskriden' ,nb: 'Byttetid for infusjonssett overskredet' ,fi: 'Kanyylin ikä yli määräajan!' @@ -11909,7 +11943,7 @@ function init() { ,ro: 'Este vremea să schimbați canula' ,bg: 'Време за смяна на сет' ,hr: 'Vrijeme za zamjenu kanile' - ,ru: 'Пора заменить катетер' + ,ru: 'Пора заменить катетер помпы' ,sv: 'Dags att byta infusionsset' ,nb: 'På tide å bytte infusjonssett' ,fi: 'Aika vaihtaa kanyyli' @@ -11932,7 +11966,7 @@ function init() { ,fr: 'Changement de canule bientòt' ,bg: 'Смени сета скоро' ,hr: 'Zamijena kanile uskoro' - ,ru: 'Приближается время замены катетера' + ,ru: 'Приближается время замены катетера помпы' ,sv: 'Byt infusionsset snart' ,nb: 'Bytt infusjonssett snart' ,fi: 'Vaihda kanyyli pian' @@ -11954,7 +11988,7 @@ function init() { ,ro: 'Vechimea canulei în ore: %1' ,bg: 'Сетът е на %1 часове' ,hr: 'Staros kanile %1 sati' - ,ru: 'Катетер отработал %1 час' + ,ru: 'Катетер помпы работает %1 час' ,sv: 'Infusionsset tid %1 timmar' ,nb: 'infusjonssett alder %1 timer' ,fi: 'Kanyylin ikä %1 tuntia' @@ -12002,7 +12036,7 @@ function init() { ,bg: 'ВС' ,hr: 'Starost kanile' ,fr: 'CAGE' - ,ru: 'ОтрабКат' + ,ru: 'КатПомп' ,sv: 'Nål' ,nb: 'Nål alder' ,fi: 'KIKÄ' @@ -12026,7 +12060,7 @@ function init() { ,bg: 'АВХ' ,hr: 'Aktivni UGH' ,fr: 'COB' - ,ru: 'Активн углеводы COB' + ,ru: 'АктУгл COB' ,sv: 'COB' ,nb: 'Aktive karbohydrater' ,fi: 'AH' @@ -12049,7 +12083,7 @@ function init() { ,fr: 'Derniers glucides' ,bg: 'Последни ВХ' ,hr: 'Posljednji UGH' - ,ru: 'Новые углеводы' + ,ru: 'Прошлые углеводы' ,sv: 'Senaste kolhydrater' ,nb: 'Siste karbohydrater' ,fi: 'Viimeisimmät hiilihydraatit' @@ -12208,7 +12242,7 @@ function init() { ,de: 'IOB' ,dk: 'IOB' ,ro: 'IOB' - ,ru: 'Активный Инсулин IOB' + ,ru: 'АктИнс IOB' ,fr: 'IOB' ,bg: 'АИ' ,hr: 'Aktivni inzulin' @@ -12231,7 +12265,7 @@ function init() { ,de: 'Careportal IOB' ,dk: 'IOB i Careportal' ,ro: 'IOB în Careportal' - ,ru: 'Активн Инс на портале назначений' + ,ru: 'АктИнс на портале лечения' ,fr: 'Careportal IOB' ,bg: 'АИ от Кеърпортал' ,hr: 'Careportal IOB' @@ -12277,7 +12311,7 @@ function init() { ,de: 'Basal IOB' ,dk: 'Basal IOB' ,ro: 'IOB bazală' - ,ru: 'Активн Базал IOB' + ,ru: 'АктуальнБазал IOB' ,fr: 'IOB du débit basal' ,bg: 'Базален АИ' ,hr: 'Bazalni aktivni inzulin' @@ -12324,20 +12358,20 @@ function init() { ,dk: 'Gammel data, kontrollere uploader?' ,ro: 'Date învechite, verificați uploaderul!' ,fr: 'Valeurs trop anciennes, vérifier l\'uploadeur' - ,ru: 'Устаревшие данные, проверьте загрузчик' + ,ru: 'Старые данные, проверьте загрузчик' ,bg: 'Стари данни, провери телефона' ,hr: 'Nedostaju podaci, provjera opreme?' ,es: 'Datos desactualizados, controlar la subida?' ,sv: 'Gammal data, kontrollera rigg?' ,nb: 'Gamle data, sjekk rigg?' ,fi: 'Tiedot vanhoja, tarkista lähetin?' + , pl: 'Dane są nieaktualne, sprawdź urządzenie transmisyjne.' ,pt: 'Dados antigos, verificar uploader?' ,sk: 'Zastaralé dáta, skontrolujte uploader' ,ko: '오래된 데이터입니다. 확인해 보시겠습니까?' ,it: 'dati non aggiornati, controllare il telefono?' ,nl: 'Geen data, controleer uploader' ,zh_cn: '数据过期,检查一下设备?' - , pl: 'Dane są nieaktualne, sprawdź urządzenie transmisyjne.' ,tr: 'Veri güncel değil, vericiyi kontrol et?' } ,'Last received:' : { @@ -12347,7 +12381,7 @@ function init() { ,dk: 'Senest modtaget:' ,fr: 'Dernière réception:' ,ro: 'Ultimile date:' - ,ru: 'Предыдущий полученный' + ,ru: 'Получено:' ,bg: 'Последно получени' ,hr: 'Zadnji podaci od:' ,sv: 'Senast mottagen:' @@ -12370,7 +12404,7 @@ function init() { ,dk: '%1m siden' ,ro: 'acum %1 minute' ,fr: 'il y a %1 min' - ,ru: 'мин назад' + ,ru: '% мин назад' ,bg: 'преди %1 мин.' ,hr: 'prije %1m' ,sv: '%1m sedan' @@ -12393,7 +12427,7 @@ function init() { ,dk: '%1t siden' ,ro: 'acum %1 ore' ,fr: '%1 heures plus tôt' - ,ru: 'час назад' + ,ru: '% час назад' ,bg: 'преди %1 час' ,hr: 'prije %1 sati' ,sv: '%1h sedan' @@ -12416,7 +12450,7 @@ function init() { ,dk: '%1d siden' ,ro: 'acum %1 zile' ,fr: '%1 jours plus tôt' - ,ru: 'дн назад' + ,ru: '% дн назад' ,bg: 'преди %1 ден' ,hr: 'prije %1 dana' ,sv: '%1d sedan' @@ -12461,7 +12495,7 @@ function init() { ,de:'SAGE' ,dk: 'Sensoralder' ,ro: 'VS' - ,ru: 'Сенсор проработал' + ,ru: 'Сенсор работает' ,fr: 'SAGE' ,bg: 'ВС' ,hr: 'Starost senzora' @@ -12555,7 +12589,7 @@ function init() { ,dk: 'Sensoralder %1 dage %2 timer' ,ro: 'Senzori vechi de %1 zile și %2 ore' ,fr: 'Âge su senseur %1 jours et %2 heures' - ,ru: 'Сенсор отработал % дн % час' + ,ru: 'Сенсор работает %1 дн %2 час' ,bg: 'Сензорът е на %1 дни %2 часа ' ,hr: 'Starost senzora %1 dana i %2 sati' ,sv: 'Sensorålder %1 dagar %2 timmar' @@ -12578,7 +12612,7 @@ function init() { ,dk: 'Sensor isat' ,ro: 'Inserția senzorului' ,fr: 'Insertion du senseur' - ,ru: 'Установка сенсора' + ,ru: 'Сенсор установлен' ,bg: 'Поставяне на сензора' ,hr: 'Postavljanje senzora' ,sv: 'Sensor insättning' @@ -12607,6 +12641,7 @@ function init() { ,sv: 'Sensorstart' ,nb: 'Sensorstart' ,fi: 'Sensorin Aloitus' + ,pl: 'Uruchomienie sensora' ,pt: 'Início de sensor' ,es: 'Inicio del sensor' ,sk: 'Štart senzoru' @@ -12614,7 +12649,6 @@ function init() { ,it: 'SAGE - partenza sensore' ,nl: 'Sensor start' ,zh_cn: '启动探头' - ,pl: 'Uruchom sensor' ,tr: 'Sensör başlatma' } ,'days' : { @@ -12677,7 +12711,7 @@ function init() { ,sv: 'För att se denna rapport, klicka på "Visa"' ,bg: 'За да видите тази статистика, натиснете ПОКАЖИ' ,hr: 'Za prikaz ovog izvješća, pritisnite PRIKAŽI na ovom prozoru' - , pl: 'Aby wyświetlić ten raport, naciśnij przycisk POKAŻ w tym widoku' + ,pl: 'Aby wyświetlić ten raport, naciśnij przycisk POKAŻ w tym widoku' ,tr: 'Bu raporu görmek için bu görünümde GÖSTER düğmesine basın.' } ,'AR2 Forecast' : { @@ -12727,7 +12761,7 @@ function init() { ,dk: 'Midlertidigt mål' ,fr: 'Cible temporaire' ,ro: 'Țintă temporară' - ,ru: 'промежуточная цель' + ,ru: 'Временная цель' ,fi: 'Tilapäinen tavoite' ,es: 'Objetivo temporal' ,ko: '임시목표' @@ -12747,7 +12781,7 @@ function init() { ,dk: 'Afslut midlertidigt mål' ,fr: 'Effacer la cible temporaire' ,ro: 'Renunțare la ținta temporară' - ,ru: 'отмена промежуточной цели' + ,ru: 'Отмена временной цели' ,fi: 'Peruuta tilapäinen tavoite' ,es: 'Objetivo temporal cancelado' ,ko: '임시목표취소' @@ -12787,7 +12821,7 @@ function init() { ,dk: 'Profiler' ,fr: 'Profils' ,ro: 'Profile' - ,ru: 'профили' + ,ru: 'Профили' ,fi: 'Profiilit' ,ko: '프로파일' ,it: 'Profili' @@ -12809,7 +12843,7 @@ function init() { ,it: 'Tempo in fluttuazione' ,ro: 'Timp în fluctuație' ,es: 'Tiempo fluctuando' - ,ru: 'время флуктуаций' + ,ru: 'Время флуктуаций' ,nl: 'Tijd met fluctuaties' ,zh_cn: '波动时间' ,sv: 'Tid i fluktation' @@ -12829,7 +12863,7 @@ function init() { ,it: 'Tempo in rapida fluttuazione' ,ro: 'Timp în fluctuație rapidă' ,es: 'Tiempo fluctuando rápido' - ,ru: 'время быстрых флуктуаций' + ,ru: 'Время быстрых флуктуаций' ,nl: 'Tijd met grote fluctuaties' ,zh_cn: '快速波动时间' ,sv: 'Tid i snabb fluktation' @@ -12868,7 +12902,7 @@ function init() { ,fr: 'Filtrer par heures' ,ro: 'Filtrare pe ore' ,es: 'Filtrar por horas' - ,ru: 'почасовой фильтр' + ,ru: 'Почасовая фильтрация' ,nl: 'Filter op uren' ,zh_cn: '按小时过滤' ,sv: 'Filtrera per timme' @@ -12887,7 +12921,7 @@ function init() { ,it: 'Tempo in fluttuazione e Tempo in rapida fluttuazione misurano la % di tempo durante il periodo esaminato, durante il quale la glicemia stà variando velocemente o rapidamente. Bassi valori sono migliori.' ,ro: 'Timpul în fluctuație și timpul în fluctuație rapidă măsoară procentul de timp, din perioada examinată, în care glicemia din sânge a avut o variație relativ rapidă sau rapidă. Valorile mici sunt de preferat.' ,es: 'Tiempo en fluctuación y Tiempo en fluctuación rápida miden el % de tiempo del período exáminado, durante la cual la glucosa en sangre ha estado cambiando relativamente rápido o rápidamente. Valores más bajos son mejores.' - ,ru: 'время флуктуаций и время быстрых флуктуаций означает % времени в рассматриваемый период в течение которого СК менялся относительно быстро или просто быстро. Более низкие значения предпочтительней' + ,ru: 'Время флуктуаций и время быстрых флуктуаций означает % времени в рассматриваемый период в течение которого ГК менялась относительно быстро или просто быстро. Более низкие значения предпочтительней' ,nl: 'Tijd met fluctuaties of grote fluctuaties in % van de geevalueerde periode, waarbij de bloed glucose relatief snel wijzigde.Lagere waarden zijn beter.' ,zh_cn: '在检查期间血糖波动时间和快速波动时间占的时间百分比,在此期间血糖相对快速或快速地变化。百分比值越低越好。' ,sv: 'Tid i fluktuation och tid i snabb fluktuation mäter% av tiden under den undersökta perioden, under vilken blodsockret har förändrats relativt snabbt eller snabbt. Lägre värden är bättre' @@ -12905,7 +12939,7 @@ function init() { ,ko: '전체 일일 변동 평균은 조사된 기간동안 전체 혈당 절대값의 합을 전체 일수로 나눈 값입니다. 낮을수록 좋습니다.' ,it: 'Media Totale Giornaliera Variazioni è la somma dei valori assoluti di tutte le escursioni glicemiche per il periodo esaminato, diviso per il numero di giorni. Bassi valori sono migliori.' ,ro: 'Schimbarea medie totală zilnică este suma valorilor absolute ale tuturor excursiilor glicemice din perioada examinată, împărțite la numărul de zile. Valorile mici sunt de preferat.' - ,ru: 'усредненное ежедневное изменение это сумма абсолютных величин всех отклонений СК в рассматриваемый период, деленное на количество дней. Меньшая величина предпочтительней' + ,ru: 'Усредненное ежедневное изменение это сумма абсолютных величин всех отклонений ГК в рассматриваемый период, деленная на количество дней. Меньшая величина предпочтительней' ,es: 'El cambio medio diario total es la suma de los valores absolutos de todas las glucémias en el período examinado, dividido por el número de días. Mejor valores bajos.' ,nl: 'Gemiddelde veranderingen per dag is een som van alle waardes die uitschieten over de bekeken periode, gedeeld door het aantal dagen in deze periode. Lager is beter.' ,zh_cn: '平均每日总变化是检查期间所有血糖偏移的绝对值之和除以天数。越低越好' @@ -12924,7 +12958,7 @@ function init() { ,ko: '시간당 변동 평균은 조사된 기간 동안 전체 혈당 절대값의 합을 기간의 시간으로 나눈 값입니다.낮을수록 좋습니다.' ,it: 'Media Oraria Variazioni è la somma del valore assoluto di tutte le escursioni glicemiche per il periodo esaminato, diviso per il numero di ore. Bassi valori sono migliori.' ,ro: 'Variația media orară este suma valorilor absolute ale tuturor excursiilor glicemice din perioada examinată, împărțite la numărul de ore din aceeași perioadă. Valorile mici sunt de preferat.' - ,ru: 'усредненное часовое изменение это сумма абсолютных величин всех отклонений СК в рассматриваемый период, деленное на количество часов в этот период. Более низкое предпочтительней' + ,ru: 'Усредненное часовое изменение это сумма абсолютных величин всех отклонений ГК в рассматриваемый период, деленная на количество часов в этот период. Более низкое предпочтительней' ,es: 'El cambio medio por hora, es la suma del valor absoluto de todas las glucemias para el período examinado, dividido por el número de horas en el período. Más bajo es mejor.' ,nl: 'Gemiddelde veranderingen per uur is een som van alle waardes die uitschieten over de bekeken periode, gedeeld door het aantal uur in deze periode. Lager is beter.' ,zh_cn: '平均每小时变化是检查期间所有血糖偏移的绝对值之和除以该期间的小时数。 越低越好' @@ -12945,7 +12979,7 @@ function init() { ,es: 'Variabilidad de la glucosa en sangre y el estado glucémico del paciente es un valor diseñado por Dexcom, más detalles en 15 * 1000) { // Looks like we've been hibernating - lastSuspendTime = now; + if (delta > 20 * 1000) { // Looks like we've been hibernating + lastRecoveryTimeFromSuspend = now; } + var timeSinceLastRecovered = now.getTime() - lastRecoveryTimeFromSuspend.getTime(); + return timeSinceLastRecovered < (10 * 1000); + } - var timeSinceLastSuspended = now.getTime() - lastSuspendTime.getTime(); + // Assume server never hibernates, or if it does, it's alarm-worthy + return false; - return timeSinceLastSuspended < (10 * 1000); - } else if (sbx.runtimeEnvironment === 'server') { - return delta > 2 * heartbeatMs; - } else { - console.error('Cannot detect hibernation, because runtimeEnvironment is not detected from sbx.runtimeEnvironment:', sbx.runtimeEnvironment); - return false; - } } if (isHibernationDetected()) { diff --git a/lib/plugins/virtAsstBase.js b/lib/plugins/virtAsstBase.js new file mode 100644 index 00000000000..e0d103672a2 --- /dev/null +++ b/lib/plugins/virtAsstBase.js @@ -0,0 +1,111 @@ +'use strict'; + +var moment = require('moment'); +var _each = require('lodash/each'); + +function init(env, ctx) { + function virtAsstBase() { + return virtAsstBase; + } + + var entries = ctx.entries; + var translate = ctx.language.translate; + + virtAsstBase.setupMutualIntents = function (configuredPlugin) { + // full status + configuredPlugin.addToRollup('Status', function (slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time)) + ] + }); + + callback(null, {results: status, priority: -1}); + }); + }, 'BG Status'); + + configuredPlugin.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { + configuredPlugin.getRollup('Status', sbx, slots, locale, function (status) { + callback(translate('virtAsstTitleFullStatus'), status); + }); + }); + + // blood sugar and direction + configuredPlugin.configureIntentHandler('MetricNow', function (callback, slots, sbx) { + entries.list({count: 1}, function(err, records) { + var direction; + if(translate(records[0].direction)){ + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time))] + }); + + callback(translate('virtAsstTitleCurrentBG'), status); + }); + }, ['bg', 'blood glucose', 'number']); + + // blood sugar delta + configuredPlugin.configureIntentHandler('MetricNow', function (callback, slots, sbx) { + if (sbx.properties.delta && sbx.properties.delta.display) { + entries.list({count: 2}, function(err, records) { + callback( + translate('virtAsstTitleDelta'), + translate('virtAsstDelta', { + params: [ + sbx.properties.delta.display == '+0' ? '0' : sbx.properties.delta.display, + moment(records[0].date).from(moment(sbx.time)), + moment(records[1].date).from(moment(sbx.time)) + ] + }) + ); + }); + } else { + callback(translate('virtAsstTitleDelta'), translate('virtAsstUnknown')); + } + }, ['delta']); + }; + + virtAsstBase.setupVirtAsstHandlers = function (configuredPlugin) { + ctx.plugins.eachEnabledPlugin(function (plugin){ + if (plugin.virtAsst) { + if (plugin.virtAsst.intentHandlers) { + console.log('Plugin "' + plugin.name + '" supports Virtual Assistants'); + _each(plugin.virtAsst.intentHandlers, function (route) { + if (route) { + configuredPlugin.configureIntentHandler(route.intent, route.intentHandler, route.metrics); + } + }); + } + if (plugin.virtAsst.rollupHandlers) { + console.log('Plugin "' + plugin.name + '" supports rollups for Virtual Assistants'); + _each(plugin.virtAsst.rollupHandlers, function (route) { + if (route) { + configuredPlugin.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); + } + }); + } + } else { + console.log('Plugin "' + plugin.name + '" does not support Virtual Assistants'); + } + }); + }; + + return virtAsstBase; +} + +module.exports = init; diff --git a/lib/plugins/xdripjs.js b/lib/plugins/xdripjs.js index a36a42de2e0..dc44aad2988 100644 --- a/lib/plugins/xdripjs.js +++ b/lib/plugins/xdripjs.js @@ -9,6 +9,7 @@ function init(ctx) { var utils = require('../utils')(ctx); var firstPrefs = true; var lastStateNotification = null; + var translate = ctx.language.translate; var sensorState = { name: 'xdripjs' @@ -321,6 +322,115 @@ function init(ctx) { } }; + function virtAsstGenericCGMHandler(translateItem, field, next, sbx) { + var response; + if (sbx.properties.sensorState && sbx.properties.sensorState[field]) { + response = translate('virtAsstCGM'+translateItem, { + params:[ + sbx.properties.sensorState[field] + , moment(sbx.properties.sensorState.lastStateTime).from(moment(sbx.time)) + ] + }); + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGM'+translateItem), response); + } + + sensorState.virtAsst = { + intentHandlers: [ + { + intent: 'MetricNow' + , metrics: ['cgm mode'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Mode', 'lastMode', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm status'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Status', 'lastStateString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm session age'] + , intentHandler: function(next, slots, sbx){ + var response; + // session start is only valid if in a session + if (sbx.properties.sensorState && sbx.properties.sensorState.lastSessionStart) { + if (sbx.properties.sensorState.lastState != 0x1) { + var duration = moment.duration(moment().diff(moment(sbx.properties.sensorState.lastSessionStart))); + response = translate('virtAsstCGMSessAge', { + params: [ + duration.days(), + duration.hours() + ] + }); + } else { + response = translate('virtAsstCGMSessNotStarted'); + } + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGMSessAge'), response); + } + } + , { + intent: 'MetricNow' + , metrics: ['cgm tx status'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('TxStatus', 'lastTxStatusString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm tx age'] + , intentHandler: function(next, slots, sbx){ + next( + translate('virtAsstTitleCGMTxAge'), + (sbx.properties.sensorState && sbx.properties.sensorState.lastTxActivation) + ? translate('virtAsstCGMTxAge', {params:[moment().diff(moment(sbx.properties.sensorState.lastTxActivation), 'days')]}) + : translate('virtAsstUnknown') + ); + } + } + , { + intent: 'MetricNow' + , metrics: ['cgm noise'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Noise', 'lastNoiseString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm battery'] + , intentHandler: function(next, slots, sbx){ + var response; + var sensor = sbx.properties.sensorState; + if (sensor && (sensor.lastVoltageA || sensor.lastVoltageB)) { + if (sensor.lastVoltageA && sensor.lastVoltageB) { + response = translate('virtAsstCGMBattTwo', { + params:[ + (sensor.lastVoltageA / 100) + , (sensor.lastVoltageB / 100) + , moment(sensor.lastBatteryTimestamp).from(moment(sbx.time)) + ] + }); + } else { + var finalValue = sensor.lastVoltageA ? sensor.lastVoltageA : sensor.lastVoltageB; + response = translate('virtAsstCGMBattOne', { + params:[ + (finalValue / 100) + , moment(sensor.lastBatteryTimestamp).from(moment(sbx.time)) + ] + }); + } + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGMBatt'), response); + } + } + ] + }; + return sensorState; } diff --git a/lib/profilefunctions.js b/lib/profilefunctions.js index 5826e6108b4..8bd251d4820 100644 --- a/lib/profilefunctions.js +++ b/lib/profilefunctions.js @@ -163,7 +163,7 @@ function init (profileData) { profile.getUnits = function getUnits (spec_profile) { var pu = profile.getCurrentProfile(null, spec_profile)['units'] + ' '; if (pu.toLowerCase().includes('mmol')) return 'mmol'; - return 'mgdl'; + return 'mg/dl'; }; profile.getTimezone = function getTimezone (spec_profile) { diff --git a/lib/report/predictions.js b/lib/report/predictions.js new file mode 100644 index 00000000000..8e341d0b666 --- /dev/null +++ b/lib/report/predictions.js @@ -0,0 +1,33 @@ +var predictions = { + offset: 0, + backward: function () { + this.offset -= 5; + this.updateOffsetHtml(); + }, + forward: function () { + this.offset += 5; + this.updateOffsetHtml(); + }, + moreBackward: function () { + this.offset -= 30; + this.updateOffsetHtml(); + }, + moreForward: function () { + this.offset += 30; + this.updateOffsetHtml(); + }, + reset: function () { + this.offset = 0; + this.updateOffsetHtml(); + }, + updateOffsetHtml: function () { + $('#rp_predictedOffset').html(this.offset); + } +}; + +$(document).on('change', '#rp_optionspredicted', function() { + $('#rp_predictedSettings').toggle(this.checked); + predictions.reset(); +}); + +module.exports = predictions; diff --git a/lib/report_plugins/daytoday.js b/lib/report_plugins/daytoday.js index f4b5da7cf45..6728a5e1860 100644 --- a/lib/report_plugins/daytoday.js +++ b/lib/report_plugins/daytoday.js @@ -23,17 +23,17 @@ daytoday.html = function html (client) { '

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

' + '' + translate('To see this report, press SHOW while in this view') + '
' + translate('Display') + ': ' + - '' + translate('Insulin') + '' + - '' + translate('Carbs') + '' + - '' + translate('Basal rate') + '' + - '' + translate('Notes') + - '' + translate('Food') + - '' + translate('Raw') + '' + - '' + translate('IOB') + '' + - '' + translate('COB') + '' + - '' + translate('Predictions') + '' + - '' + translate('OpenAPS') + '' + - '' + translate('Insulin distribution') + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ' ' + translate('Size') + ' ' + '
' + translate('Scale') + ': ' + - '' + - translate('Linear') + - '' + - translate('Logarithmic') + + '' + + '' + '' + '
' + '
' + @@ -362,16 +362,14 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio for (var treatmentsIndex = 0; treatmentsIndex < treatmentsTimestamps.length; treatmentsIndex++) { var timestamp = treatmentsTimestamps[treatmentsIndex]; // TODO refactor code so this is set here - now set as global in file loaded by the browser - // eslint-disable-next-line no-undef - var predictedIndex = findPredicted(predictions, timestamp, predictedOffset); // Find predictions offset before or after timestamp + var predictedIndex = findPredicted(predictions, timestamp, Nightscout.predictions.offset); // Find predictions offset before or after timestamp if (predictedIndex != null) { entry = predictions[predictedIndex]; // Start entry var d = moment(entry.startDate); var end = moment().endOf('day'); if (options.predictedTruncate) { - // eslint-disable-next-line no-undef - if (predictedOffset >= 0) { + if (Nightscout.predictions.offset >= 0) { // If we are looking forward we want to stop at the next treatment if (treatmentsIndex < treatmentsTimestamps.length - 1) { end = moment(treatmentsTimestamps[treatmentsIndex + 1]); @@ -850,6 +848,26 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio .attr('y', yScale2(client.utils.scaleMgdl(306)) + padding.top) .attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left) .text(treatment.notes); + } else if (treatment.eventType === 'Temporary Override' && treatment.duration ) { + // Loop Overrides with duration + context.append('rect') + .attr('x', xScale2(treatment.mills) + padding.left) + .attr('y', yScale2(client.utils.scaleMgdl(432)) + padding.top) + .attr('width', xScale2(treatment.mills + times.mins(treatment.duration).msecs) - xScale2(treatment.mills)) + .attr('height', yScale2(client.utils.scaleMgdl(396)) - yScale2(client.utils.scaleMgdl(432))) + .attr('stroke-width', 1) + .attr('opacity', .2) + .attr('stroke', 'white') + .attr('fill', 'black'); + context.append('text') + .style('font-size', '12px') + .style('font-weight', 'bold') + .attr('fill', 'black') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .attr('y', yScale2(client.utils.scaleMgdl(414)) + padding.top) + .attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left) + .text(treatment.reason); } else if (!treatment.duration) { // other treatments without duration context.append('circle') diff --git a/lib/report_plugins/loopalyzer.js b/lib/report_plugins/loopalyzer.js index 2d53fa251a3..c37c58f635f 100644 --- a/lib/report_plugins/loopalyzer.js +++ b/lib/report_plugins/loopalyzer.js @@ -318,10 +318,12 @@ loopalyzer.fillNanWithTreatments = function(array, treatments) { var stop = index; // Now move left and right until we find real numbers, so not NaN - // eslint-disable-next-line no-empty - while (start-- >= 0 && isNaN(array[start])) {} - // eslint-disable-next-line no-empty - while (stop++ < array.length && isNaN(array[stop])) {} + while (start >= 0 && isNaN(array[start])) { + start--; + } + while (stop < array.length && isNaN(array[stop])) { + stop++; + } // var gap = stop - start; // if (isNaN(array[start]) || isNaN(array[stop]) || gap > interpolationGap || (gap < interpolationGap && array[start]= interpolationGap || array[start]==0)) ) { @@ -732,8 +734,7 @@ loopalyzer.renderProfilesTable = function(datastoreProfiles, daysToShow, client) if (store) { for (var key in store) { if (laDebug) console.log('profile ' + key); - // eslint-disable-next-line no-prototype-builtins - if (store.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(store, key)) { var defaultProfile = store[key]; newEntry.profileName = key; if (defaultProfile.basal) newEntry.basal = defaultProfile.basal; diff --git a/lib/report_plugins/profiles.js b/lib/report_plugins/profiles.js index 580367891de..4aea2d040a8 100644 --- a/lib/report_plugins/profiles.js +++ b/lib/report_plugins/profiles.js @@ -30,8 +30,7 @@ profiles.css = ' height: 100%;' + '}'; -// eslint-disable-next-line no-unused-vars -profiles.report = function report_profiles (datastorage, sorteddaystoshow, options) { +profiles.report = function report_profiles (datastorage) { var Nightscout = window.Nightscout; var client = Nightscout.client; var translate = client.translate; diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js index 2bb63ad78f0..f5702efc4fe 100644 --- a/lib/server/bootevent.js +++ b/lib/server/bootevent.js @@ -179,6 +179,10 @@ function boot (env, language) { ctx.dataloader = require('../data/dataloader')(env, ctx); ctx.notifications = require('../notifications')(env, ctx); + if (env.settings.isEnabled('alexa') || env.settings.isEnabled('googlehome')) { + ctx.virtAsstBase = require('../plugins/virtAsstBase')(env, ctx); + } + if (env.settings.isEnabled('alexa')) { ctx.alexa = require('../plugins/alexa')(env, ctx); } @@ -247,7 +251,7 @@ function boot (env, language) { return next(); } - ctx.bridge = require('../plugins/bridge')(env); + ctx.bridge = require('../plugins/bridge')(env, ctx.bus); if (ctx.bridge) { ctx.bridge.startEngine(ctx.entries); } @@ -259,7 +263,7 @@ function boot (env, language) { return next(); } - ctx.mmconnect = require('../plugins/mmconnect').init(env, ctx.entries, ctx.devicestatus); + ctx.mmconnect = require('../plugins/mmconnect').init(env, ctx.entries, ctx.devicestatus, ctx.bus); if (ctx.mmconnect) { ctx.mmconnect.run(); } diff --git a/lib/server/clocks.js b/lib/server/clocks.js index 282acd01951..56bce7b6f13 100644 --- a/lib/server/clocks.js +++ b/lib/server/clocks.js @@ -3,8 +3,7 @@ const express = require('express'); const path = require('path'); -// eslint-disable-next-line no-unused-vars -function clockviews(env, ctx) { +function clockviews() { const app = new express(); let locals = {}; diff --git a/lib/server/treatments.js b/lib/server/treatments.js index c02bd50f317..580e5cf0c45 100644 --- a/lib/server/treatments.js +++ b/lib/server/treatments.js @@ -25,8 +25,7 @@ function storage (env, ctx) { errs.push(err); callback(err, docs) }); - // eslint-disable-next-line no-unused-vars - }, function (err, docs) { + }, function () { errs = _.compact(errs); done(errs.length > 0 ? errs : null, allDocs); }); diff --git a/lib/server/websocket.js b/lib/server/websocket.js index afced8bfb37..899d9326cc2 100644 --- a/lib/server/websocket.js +++ b/lib/server/websocket.js @@ -34,8 +34,7 @@ function init (env, ctx, server) { }; // This is little ugly copy but I was unable to pass testa after making module from status and share with /api/v1/status - // eslint-disable-next-line no-unused-vars - function status (profile) { + function status () { var versionNum = 0; var verParse = /(\d+)\.(\d+)\.(\d+)*/.exec(env.version); if (verParse) { @@ -73,6 +72,13 @@ function init (env, ctx, server) { 'browser client etag': true, 'browser client gzip': false }); + + ctx.bus.on('teardown', function serverTeardown () { + Object.keys(io.sockets.sockets).forEach(function(s) { + io.sockets.sockets[s].disconnect(true); + }); + io.close(); + }); } function verifyAuthorization (message, callback) { @@ -437,15 +443,9 @@ function init (env, ctx, server) { socketAuthorization = authorization; clientType = message.client; history = message.history || 48; //default history is 48 hours - var from = message.from; if (socketAuthorization.read) { socket.join('DataReceivers'); - var msecHistory = times.hours(history).msecs; - // if `from` is received, it's a reconnection and full data is not needed - if (from && from > 0) { - msecHistory = Math.min(new Date().getTime() - from, msecHistory); - } if (lastData && lastData.dataWithRecentStatuses) { let data = lastData.dataWithRecentStatuses(); diff --git a/lib/settings.js b/lib/settings.js index c2497ed66d5..d1f18b0e9c5 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -290,34 +290,40 @@ function init () { return enabled; } - function isAlarmEventEnabled (notify) { - var enabled = false; + function isUrgentHighAlarmEnabled(notify) { + return notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh; + } - if ('high' !== notify.eventName && 'low' !== notify.eventName) { - enabled = true; - } else if (notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh) { - enabled = true; - } else if (notify.eventName === 'high' && settings.alarmHigh) { - enabled = true; - } else if (notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow) { - enabled = true; - } else if (notify.eventName === 'low' && settings.alarmLow) { - enabled = true; - } + function isHighAlarmEnabled(notify) { + return notify.eventName === 'high' && settings.alarmHigh; + } - return enabled; + function isUrgentLowAlarmEnabled(notify) { + return notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow; + } + + function isLowAlarmEnabled(notify) { + return notify.eventName === 'low' && settings.alarmLow; + } + + function isAlarmEventEnabled (notify) { + return ('high' !== notify.eventName && 'low' !== notify.eventName) + || isUrgentHighAlarmEnabled(notify) + || isHighAlarmEnabled(notify) + || isUrgentLowAlarmEnabled(notify) + || isLowAlarmEnabled(notify); } function snoozeMinsForAlarmEvent (notify) { var snoozeTime; - if (notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh) { + if (isUrgentHighAlarmEnabled(notify)) { snoozeTime = settings.alarmUrgentHighMins; - } else if (notify.eventName === 'high' && settings.alarmHigh) { + } else if (isHighAlarmEnabled(notify)) { snoozeTime = settings.alarmHighMins; - } else if (notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow) { + } else if (isUrgentLowAlarmEnabled(notify)) { snoozeTime = settings.alarmUrgentLowMins; - } else if (notify.eventName === 'low' && settings.alarmLow) { + } else if (isLowAlarmEnabled(notify)) { snoozeTime = settings.alarmLowMins; } else if (notify.level === levels.URGENT) { snoozeTime = settings.alarmUrgentMins; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 7d3a1730a7d..fefff4919b9 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "13.0.1", + "version": "13.0.2-dev", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2805,6 +2805,23 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" }, + "codacy-coverage": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/codacy-coverage/-/codacy-coverage-3.4.0.tgz", + "integrity": "sha512-A0ats3/gZtOw76muu++HZ6QrInztWjjLefkLJmmBpjPfyn6nNwNLoApmGmj3F3dfgl2+o6u5GwPnUBkKdfKXTQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.x", + "commander": "^2.x", + "jacoco-parse": "^2.x", + "joi": "^13.x", + "lcov-parse": "^1.x", + "lodash": "^4.17.4", + "log-driver": "^1.x", + "request": "^2.88.0", + "request-promise": "^4.x" + } + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -3179,6 +3196,17 @@ "cssom": "0.3.x" } }, + "csv-parse": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.8.3.tgz", + "integrity": "sha512-0GPxubzYzSn08lhNTWDCkcDKn8krmw0WuscqB2RrW6sugGGskbwaaEz7PCFFwbQ0phNGTTieiyfzzu3S/jZZ7Q==", + "dev": true + }, + "csv-stringify": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.3.5.tgz", + "integrity": "sha512-bFbaJqz7LcwnTzdryyJuhR6Pys2deU8+z7O8N0JBnNGm7vnJVr3K0n68bhb+rlMpwCmDbUtinr8yq5I2RlPMqw==" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -3684,6 +3712,15 @@ "stream-shift": "^1.0.0" } }, + "easyxml": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/easyxml/-/easyxml-2.0.1.tgz", + "integrity": "sha1-7qCShCyREwCox4GRPL5b04pQcRw=", + "requires": { + "elementtree": "^0.1.6", + "inflect": "^0.3.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -3716,6 +3753,21 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.275.tgz", "integrity": "sha512-/YWtW/VapMnuYA1lNOaa1F4GhR1LBf+CUTp60lzDPEEh0XOzyOAyULyYZVF9vziZ3qSbTqCwmKwsyRXp66STbw==" }, + "elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha1-mskb5uUvtuYkTE5UpKw+2K6OKcA=", + "requires": { + "sax": "1.1.4" + }, + "dependencies": { + "sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha1-dLbTPJrh4AFRDxeakRaFiPGu2qk=" + } + } + }, "elliptic": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz", @@ -5508,6 +5560,12 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==", + "dev": true + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -5699,6 +5757,11 @@ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" }, + "inflect": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/inflect/-/inflect-0.3.0.tgz", + "integrity": "sha1-gdDqqja1CmAjC3UQBIs5xBQv5So=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6000,6 +6063,15 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" }, + "isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "dev": true, + "requires": { + "punycode": "2.x.x" + } + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6249,6 +6321,27 @@ "handlebars": "^4.1.2" } }, + "jacoco-parse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jacoco-parse/-/jacoco-parse-2.0.1.tgz", + "integrity": "sha512-YGhIb2iXuQ4/zNh2zgHd6Z6dqlYwLYH1wfsxtTNQ+jnHH9PhhuMwqOFihXymSI41trxok48LdKkSeDIWs28tYg==", + "dev": true, + "requires": { + "mocha": "^5.2.0", + "xml2js": "^0.4.9" + } + }, + "joi": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", + "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", + "dev": true, + "requires": { + "hoek": "5.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + } + }, "jquery": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", @@ -6455,6 +6548,12 @@ "invert-kv": "^2.0.0" } }, + "lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha1-6w1GtUER68VhrLTECO+TY73I9+A=", + "dev": true + }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -6578,6 +6677,12 @@ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9594,6 +9699,29 @@ } } }, + "request-promise": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.5.tgz", + "integrity": "sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + } + } + }, "request-promise-core": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", @@ -11202,6 +11330,23 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "dev": true, + "requires": { + "hoek": "6.x.x" + }, + "dependencies": { + "hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==", + "dev": true + } + } + }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -12117,6 +12262,22 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true + }, "xmlhttprequest-ssl": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", diff --git a/package.json b/package.json index 32ee90d2a33..0abbae897b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "13.0.1", + "version": "13.0.2-dev", "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", @@ -28,16 +28,17 @@ "scripts": { "start": "node server.js", "test": "env-cmd ./my.test.env mocha --exit tests/*.test.js", - "test-ci": "env-cmd ./ci.test.env mocha --exit tests/*.test.js", + "test-ci": "env-cmd ./ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --exit tests/*.test.js", "env": "env", "postinstall": "webpack --mode production --config webpack.config.js && npm run-script update-buster", "bundle": "webpack --mode production --config webpack.config.js && npm run-script update-buster", "bundle-dev": "webpack --mode development --config webpack.config.js && npm run-script update-buster", "bundle-analyzer": "webpack --mode development --config webpack.config.js --profile --json > stats.json && webpack-bundle-analyzer stats.json", "update-buster": "node bin/generateCacheBuster.js >tmp/cacheBusterToken", - "coverage": "env-cmd ./test.env nyc mocha --exit tests/*.test.js", + "coverage": "cat ./coverage/lcov.info | env-cmd ./ci.test.env codacy-coverage", "dev": "env-cmd ./my.env nodemon server.js 0.0.0.0", - "prod": "env-cmd ./my.prod.env node server.js 0.0.0.0" + "prod": "env-cmd ./my.prod.env node server.js 0.0.0.0", + "lint": "eslint lib" }, "main": "server.js", "config": { @@ -71,7 +72,9 @@ "compression": "^1.7.4", "css-loader": "^1.0.1", "cssmin": "^0.4.3", + "csv-stringify": "^5.3.5", "d3": "^5.12.0", + "easyxml": "^2.0.1", "ejs": "^2.6.2", "errorhandler": "^1.5.1", "event-stream": "3.3.4", @@ -121,6 +124,8 @@ "devDependencies": { "babel-eslint": "^10.0.3", "benv": "^3.3.0", + "codacy-coverage": "^3.4.0", + "csv-parse": "^4.8.3", "env-cmd": "^8.0.2", "eslint": "^6.2.1", "eslint-loader": "^2.2.1", @@ -133,7 +138,8 @@ "terser-webpack-plugin": "^1.4.1", "webpack-bundle-analyzer": "^3.4.1", "webpack-dev-middleware": "^3.7.2", - "webpack-hot-middleware": "^2.25.0" + "webpack-hot-middleware": "^2.25.0", + "xml2js": "^0.4.23" }, "browserslist": "> 0.25%, not dead" } diff --git a/server.js b/server.js index df99724747a..f4350bbbe00 100644 --- a/server.js +++ b/server.js @@ -54,6 +54,12 @@ require('./lib/server/bootevent')(env, language).boot(function booted (ctx) { return; } + ctx.bus.on('teardown', function serverTeardown () { + server.close(); + clearTimeout(sendStartupAllClearTimer); + ctx.store.client.close(); + }); + /////////////////////////////////////////////////// // setup socket io for data and message transmission /////////////////////////////////////////////////// @@ -68,7 +74,7 @@ require('./lib/server/bootevent')(env, language).boot(function booted (ctx) { }); //after startup if there are no alarms send all clear - setTimeout(function sendStartupAllClear () { + let sendStartupAllClearTimer = setTimeout(function sendStartupAllClear () { var alarm = ctx.notifications.findHighestAlarm(); if (!alarm) { ctx.bus.emit('notification', { diff --git a/static/css/drawer.css b/static/css/drawer.css index f9e6147fe2d..96e1375ebd8 100644 --- a/static/css/drawer.css +++ b/static/css/drawer.css @@ -207,34 +207,49 @@ h1, legend, } #toolbar { - background: url(/images/logo2.png) no-repeat 3px 3px #333; - border-bottom: 1px solid #999; - top: 0; - margin: 0; - height: 44px; text-shadow: 0 0 5px black; + display: flex; + height: 44px; + margin: 0 0 10px; + padding: 0 15px 0 40px; + position: relative; + align-items: center; + background: url(/images/logo2.png) no-repeat 3px center #333; + border-bottom: 1px solid #999; + justify-content: space-between; } #toolbar .customTitle { color: #ccc; font-size: 16px; - margin-top: 0; - margin-left: 42px; - padding-top: 10px; - padding-right: 150px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } + +#toolbar .button-close { + color: #404040; + text-align: center; + text-shadow: none; + height: 20px; + width: 20px; + padding: 5px; + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + background: grey; + border: 2px solid #404040; + border-radius: 5px; +} + +#toolbar .button-close + #buttonbar { + margin-right: 40px; +} + #buttonbar { - margin-right: 50px; - padding-right: 15px; - height: 44px; opacity: 0.75; vertical-align: middle; - position: absolute; - right: 0; - z-index: 500; } #buttonbar a, @@ -244,14 +259,17 @@ h1, legend, height: 44px; line-height: 44px; } + #buttonbar .selected { color: red; } + #buttonbar a { float: left; text-decoration: none; width: 34px; } + #buttonbar i { padding-left: 12px; } diff --git a/static/css/main.css b/static/css/main.css index 7c2e25837cb..8e6882dde9d 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -427,12 +427,30 @@ a, a:visited, a:link { display: none; } -#authorizationstatus a { +.toolbar-title { + text-align: left; + padding: 0 10px; +} + +.toolbar-title h2 { + margin: 0; +} + +.page-content { + padding: 10px; +} + +.authentication-status { + padding: 10px; + border-top: 1px solid #bdbdbd; +} + +.authentication-status a { color: #2196f3; text-decoration: underline; } -#authorizationstatus .small { +.authentication-status .small { font-size: 12px; } diff --git a/static/css/report.css b/static/css/report.css index afe046f00d9..6994eb96b6c 100644 --- a/static/css/report.css +++ b/static/css/report.css @@ -24,26 +24,32 @@ body { #tabnav { text-align: left; - margin: 1em 0 1em 0; + margin: 1em 0 0; font: bold 11pt verdana, arial, sans-serif; border-bottom: 1px solid #6c6; list-style-type: none; - padding: 3px 10px 3px 10px; + padding: 0 0 0 15px; } -#tabnav li{ - display: inline; - padding: 3px 4px; - border: 1px solid #6c6; +#tabnav li { + display: inline-block; + padding: 10px 15px; + border-top: 1px solid #6c6; + border-right: 1px solid #6c6; + border-bottom: none; background-color: #cfc; color: #666; - margin-right: 0; + margin: 0 0 -1px 0; text-decoration: none; - border-bottom: none; +} + +#tabnav li:first-child { + border-left: 1px solid #6c6; } #tabnav .selected { background: #fff; + border-bottom: 1px solid #fff; } #tabnav li:hover { @@ -66,4 +72,41 @@ body { float: right; min-width: 150px; max-width: 400px; +} + +main { + padding: 15px; +} + +input[type=date], +input[type=text], +input[type=number], +select { + font: 13px verdana, arial, sans-serif; +} + +label { + display: inline-flex; + justify-content: flex-start; + align-items: center; + margin-right: 7px; +} + +#rp_to { + margin-right: 10px; +} + +.presetdates { + display: inline-block; + margin-right: 8px; +} + +#rp_show { + background-color: #cfc; + border: 1px solid #6c6; + color: #666; + padding: 10px; + font-weight: bold; + font-size: 12pt; + text-transform: uppercase; } \ No newline at end of file diff --git a/static/css/translations.css b/static/css/translations.css index 8408cf29f45..b1fae30e253 100644 --- a/static/css/translations.css +++ b/static/css/translations.css @@ -1,5 +1,16 @@ -td,th { +body { + color: black; + font-family: 'Open Sans', Helvetica, Arial, sans-serif; + background-color: white; +} + +th, +td { vertical-align: top; text-align: left; border: 1px solid } + +#translations { + padding: 15px; +} diff --git a/static/report/js/predictions.js b/static/report/js/predictions.js deleted file mode 100644 index 336483a0dce..00000000000 --- a/static/report/js/predictions.js +++ /dev/null @@ -1,40 +0,0 @@ - -var predictedOffset = 0; - -function predictForward() { - predictedOffset += 5; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -function predictMoreForward() { - predictedOffset += 30; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -function predictBackward() { - predictedOffset -= 5; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -function predictMoreBackward() { - predictedOffset -= 30; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -function predictResetToZero() { - predictedOffset = 0; - $("#rp_predictedOffset").html(predictedOffset); - $("#rp_show").click(); -} - -$(document).on('change', '#rp_optionspredicted', function() { - if (this.checked) - $("#rp_predictedSettings").show(); - else - $("#rp_predictedSettings").hide(); - predictResetToZero(); -}); diff --git a/swagger.json b/swagger.json index dce7854e05b..5ff1e1c251e 100755 --- a/swagger.json +++ b/swagger.json @@ -8,7 +8,7 @@ "info": { "title": "Nightscout API", "description": "Own your DData with the Nightscout API", - "version": "13.0.1", + "version": "13.0.2-dev", "license": { "name": "AGPL 3", "url": "https://www.gnu.org/licenses/agpl.txt" diff --git a/swagger.yaml b/swagger.yaml index a08f701d7a5..4b2b13a8276 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: 13.0.1 + version: 13.0.2-dev license: name: AGPL 3 url: 'https://www.gnu.org/licenses/agpl.txt' diff --git a/tests/admintools.test.js b/tests/admintools.test.js index 0b95acdf6c1..2fe6169f59a 100644 --- a/tests/admintools.test.js +++ b/tests/admintools.test.js @@ -66,7 +66,7 @@ var someData = { describe('admintools', function ( ) { var self = this; - this.timeout(30000); // TODO: see why this test takes longer on Travis to complete + this.timeout(45000); // TODO: see why this test takes longer on CI to complete before(function (done) { benv.setup(function() { diff --git a/tests/api.alexa.test.js b/tests/api.alexa.test.js new file mode 100644 index 00000000000..8c2f5916cf6 --- /dev/null +++ b/tests/api.alexa.test.js @@ -0,0 +1,96 @@ +'use strict'; + +var request = require('supertest'); +var language = require('../lib/language')(); +const bodyParser = require('body-parser'); + +require('should'); + +describe('Alexa REST api', function ( ) { + const apiRoot = require('../lib/api/root'); + const api = require('../lib/api/'); + before(function (done) { + var env = require('../env')( ); + env.settings.enable = ['alexa']; + env.settings.authDefaultRoles = 'readable'; + env.api_secret = 'this is my long pass phrase'; + this.wares = require('../lib/middleware/')(env); + this.app = require('express')( ); + this.app.enable('api'); + var self = this; + require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { + self.app.use('/api', bodyParser({ + limit: 1048576 * 50 + }), apiRoot(env, ctx)); + + self.app.use('/api/v1', bodyParser({ + limit: 1048576 * 50 + }), api(env, ctx)); + done( ); + }); + }); + + it('Launch Request', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "LaunchRequest", + "locale": "en-US" + } + }) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + + const launchText = 'What would you like to check on Nightscout?'; + + res.body.response.outputSpeech.text.should.equal(launchText); + res.body.response.reprompt.outputSpeech.text.should.equal(launchText); + res.body.response.shouldEndSession.should.equal(false); + done( ); + }); + }); + + it('Launch Request With Intent', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "LaunchRequest", + "locale": "en-US", + "intent": { + "name": "UNKNOWN" + } + } + }) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + + const unknownIntentText = 'I\'m sorry, I don\'t know what you\'re asking for.'; + + res.body.response.outputSpeech.text.should.equal(unknownIntentText); + res.body.response.shouldEndSession.should.equal(true); + done( ); + }); + }); + + it('Session Ended', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "SessionEndedRequest", + "locale": "en-US" + } + }) + .expect(200) + .end(function (err) { + if (err) return done(err); + + done( ); + }); + }); +}); + diff --git a/tests/api3.basic.test.js b/tests/api3.basic.test.js index 8e51b343585..fc7a885269f 100644 --- a/tests/api3.basic.test.js +++ b/tests/api3.basic.test.js @@ -19,7 +19,7 @@ describe('Basic REST API3', function() { after(function after () { - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/api3.create.test.js b/tests/api3.create.test.js index cbd17a3e826..ba0cce83ba3 100644 --- a/tests/api3.create.test.js +++ b/tests/api3.create.test.js @@ -72,7 +72,7 @@ describe('API3 CREATE', function() { after(() => { - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/api3.delete.test.js b/tests/api3.delete.test.js index 203d32edce8..7cee15410a0 100644 --- a/tests/api3.delete.test.js +++ b/tests/api3.delete.test.js @@ -28,7 +28,7 @@ describe('API3 UPDATE', function() { after(() => { - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/api3.generic.workflow.test.js b/tests/api3.generic.workflow.test.js index acebe39555a..f94238d6747 100644 --- a/tests/api3.generic.workflow.test.js +++ b/tests/api3.generic.workflow.test.js @@ -9,10 +9,8 @@ describe('Generic REST API3', function() { , instance = require('./fixtures/api3/instance') , authSubject = require('./fixtures/api3/authSubject') , opTools = require('../lib/api3/shared/operationTools') - , utils = require('./fixtures/api3/utils') ; - utils.randomString('32', 'aA#'); // let's have a brand new identifier for your testing document self.urlLastModified = '/api/v3/lastModified'; self.historyTimestamp = 0; @@ -46,7 +44,7 @@ describe('Generic REST API3', function() { after(() => { - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/api3.patch.test.js b/tests/api3.patch.test.js index 38850b46ad5..3967e53cf12 100644 --- a/tests/api3.patch.test.js +++ b/tests/api3.patch.test.js @@ -51,7 +51,7 @@ describe('API3 PATCH', function() { after(() => { - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/api3.read.test.js b/tests/api3.read.test.js index b18b0225bb9..de29b26d000 100644 --- a/tests/api3.read.test.js +++ b/tests/api3.read.test.js @@ -38,7 +38,7 @@ describe('API3 READ', function() { after(() => { - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/api3.renderer.test.js b/tests/api3.renderer.test.js new file mode 100644 index 00000000000..70401897025 --- /dev/null +++ b/tests/api3.renderer.test.js @@ -0,0 +1,268 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 output renderers', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + , _ = require('lodash') + , xml2js = require('xml2js') + , csvParse = require('csv-parse/lib/sync') + ; + + self.historyFrom = (new Date()).getTime() - 1000; // starting timestamp for HISTORY operations + + self.doc1 = testConst.SAMPLE_ENTRIES[0]; + self.doc1.date = (new Date()).getTime() - (5 * 60 * 1000); + self.doc1.identifier = opTools.calculateIdentifier(self.doc1); + + self.doc2 = testConst.SAMPLE_ENTRIES[1]; + self.doc2.date = (new Date()).getTime(); + self.doc2.identifier = opTools.calculateIdentifier(self.doc2); + + self.xmlParser = new xml2js.Parser({ + explicitArray: false + }); + + self.csvParserOptions = { + columns: true, + skip_empty_lines: true + }; + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/entries'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage); + + self.subject = authResult.subject; + self.token = authResult.token; + }); + + + after(() => { + self.instance.server.close(); + }); + + + /** + * Checks if all properties from obj1 are string identical in obj2 + * (comparison of properties is made using toString()) + * @param {Object} obj1 + * @param {Object} obj2 + */ + self.checkProps = function checkProps (obj1, obj2) { + for (let propName in obj1) { + obj1[propName].toString().should.eql(obj2[propName].toString()); + } + }; + + + /** + * Checks if all objects from arrModel exist in arr + * (with string identical properties) + * @param arrModel + * @param arr + */ + self.checkItems = function checkItems (arrModel, arr) { + for (let itemModel of arrModel) { + const item = _.find(arr, (doc) => doc.identifier === itemModel.identifier); + item.should.not.be.empty(); + self.checkProps(itemModel, item); + } + }; + + + /** + * Checks if given text is valid XML. + * Next checks if all objects from arrModel exist in parsed array + * (with string identical properties) + * @param arrModel + * @param xmlText + * @returns {Promise} + */ + self.checkXmlItems = async function checkXmlItems (arrModel, xmlText) { + xmlText.should.startWith(''); + + const xml = await self.xmlParser.parseStringPromise(xmlText); + xml.items.should.not.be.empty(); + let items = xml.items.item; + items.should.be.Array(); + items.length.should.be.aboveOrEqual(arrModel.length); + + self.checkItems(arrModel, items); + }; + + + /** + * Checks if given text is valid CSV. + * Next checks if all objects from arrModel exist in parsed array + * (with string identical properties) + * @param arrModel + * @param csvText + * @returns {Promise} + */ + self.checkCsvItems = async function checkXmlItems (arrModel, csvText) { + csvText.should.not.be.empty(); + + const items = csvParse(csvText, self.csvParserOptions); + items.should.be.Array(); + items.length.should.be.aboveOrEqual(arrModel.length); + + self.checkItems(arrModel, items); + }; + + + it('should create 2 mock documents', async () => { + + async function createDoc (doc) { + + let res = await self.instance.post(`${self.url}?token=${self.token.create}`) + .send(doc) + .expect(201); + + res.body.should.be.empty(); + + res = await self.instance.get(`${self.url}/${doc.identifier}?token=${self.token.read}`) + .expect(200); + return res.body; + } + + self.doc1json = await createDoc(self.doc1); + self.doc2json = await createDoc(self.doc2); + }); + + + it('READ/SEARCH/HISTORY should not accept unsupported content type', async () => { + + async function check406 (request) { + const res = await request + .expect(406); + res.body.message.should.eql('Unsupported output format requested'); + } + + await check406(self.instance.get(`${self.url}/${self.doc1.identifier}.ttf?fields=_all&token=${self.token.read}`)); + await check406(self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all&token=${self.token.read}`) + .set('Accept', 'font/ttf')); + + await check406(self.instance.get(`${self.url}.ttf?fields=_all&token=${self.token.read}`)); + await check406(self.instance.get(`${self.url}?fields=_all&token=${self.token.read}`) + .set('Accept', 'font/ttf')); + + await check406(self.instance.get(`${self.url}/history/${self.doc1.date}.ttf?token=${self.token.read}`)); + await check406(self.instance.get(`${self.url}/history/${self.doc1.date}?token=${self.token.read}`) + .set('Accept', 'font/ttf')); + }); + + + it('READ should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}/${self.doc1.identifier}.xml?fields=_all&token=${self.token.read}`) + .expect(200); + + res.text.should.startWith(''); + + const xml = await self.xmlParser.parseStringPromise(res.text); + xml.item.should.not.be.empty(); + self.checkProps(self.doc1, xml.item); + + let res2 = await self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all&token=${self.token.read}`) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.eql(res2.text); + }); + + + it('READ should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}/${self.doc1.identifier}.csv?fields=_all&token=${self.token.read}`) + .expect(200); + + await self.checkCsvItems([self.doc1], res.text); + + let res2 = await self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all&token=${self.token.read}`) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.eql(res2.text); + }); + + + it('SEARCH should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}.xml?token=${self.token.read}&date$gte=${self.doc1.date}`) + .expect(200); + + await self.checkXmlItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}?token=${self.token.read}&date$gte=${self.doc1.date}`) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('SEARCH should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}.csv?token=${self.token.read}&date$gte=${self.doc1.date}`) + .expect(200); + + await self.checkCsvItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}?token=${self.token.read}&date$gte=${self.doc1.date}`) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('HISTORY should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}/history/${self.historyFrom}.xml?token=${self.token.read}`) + .expect(200); + + await self.checkXmlItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}/history/${self.historyFrom}?token=${self.token.read}`) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('HISTORY should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}/history/${self.historyFrom}.csv?token=${self.token.read}`) + .expect(200); + + await self.checkCsvItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}/history/${self.historyFrom}?token=${self.token.read}`) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('should remove mock documents', async () => { + + async function deleteDoc (identifier) { + await self.instance.delete(`${self.url}/${identifier}?token=${self.token.delete}`) + .query({ 'permanent': 'true' }) + .expect(204); + } + + await deleteDoc(self.doc1.identifier); + await deleteDoc(self.doc2.identifier); + }); +}); + diff --git a/tests/api3.search.test.js b/tests/api3.search.test.js index dae0ebaaf34..af109a18451 100644 --- a/tests/api3.search.test.js +++ b/tests/api3.search.test.js @@ -65,7 +65,7 @@ describe('API3 SEARCH', function() { after(() => { - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/api3.security.test.js b/tests/api3.security.test.js index 4cdc8e22b21..0e88e9fae19 100644 --- a/tests/api3.security.test.js +++ b/tests/api3.security.test.js @@ -7,7 +7,7 @@ const request = require('supertest') , moment = require('moment') ; require('should'); - + describe('Security of REST API3', function() { const self = this , instance = require('./fixtures/api3/instance') @@ -26,10 +26,10 @@ describe('Security of REST API3', function() { self.token = authResult.token; }); - + after(() => { - self.http.server.close(); - self.https.server.close(); + self.http.ctx.bus.teardown(); + self.https.ctx.bus.teardown(); }); diff --git a/tests/api3.socket.test.js b/tests/api3.socket.test.js index 5c2a5cf6461..9560c200c65 100644 --- a/tests/api3.socket.test.js +++ b/tests/api3.socket.test.js @@ -49,7 +49,7 @@ describe('Socket.IO in REST API3', function() { if(self.instance && self.instance.clientSocket && self.instance.clientSocket.connected) { self.instance.clientSocket.disconnect(); } - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/api3.update.test.js b/tests/api3.update.test.js index 403aadb022e..35995d27236 100644 --- a/tests/api3.update.test.js +++ b/tests/api3.update.test.js @@ -51,7 +51,7 @@ describe('API3 UPDATE', function() { after(() => { - self.instance.server.close(); + self.instance.ctx.bus.teardown(); }); diff --git a/tests/expressextensions.test.js b/tests/expressextensions.test.js new file mode 100644 index 00000000000..b65ebd8952b --- /dev/null +++ b/tests/expressextensions.test.js @@ -0,0 +1,33 @@ +'use strict'; + +require('should'); + +var extensionsMiddleware = require('../lib/middleware/express-extension-to-accept.js'); + +var acceptJsonRequests = extensionsMiddleware(['json']); + +describe('Express extension middleware', function ( ) { + + it('Valid json request should be given accept header for application/json', function () { + var entriesRequest = { + path: '/api/v1/entries.json', + url: '/api/v1/entries.json', + headers: {} + }; + + acceptJsonRequests(entriesRequest, {}, () => {}); + entriesRequest.headers.accept.should.equal('application/json'); + }); + + it('Invalid json request should NOT be given accept header', function () { + var invalidEntriesRequest = { + path: '/api/v1/entriesXjson', + url: '/api/v1/entriesXjson', + headers: {} + }; + + acceptJsonRequests(invalidEntriesRequest, {}, () => {}); + should(invalidEntriesRequest.headers.accept).not.be.ok; + }); + +}); diff --git a/tests/reports.test.js b/tests/reports.test.js index 7d5a0eb7009..2546bbbf5e8 100644 --- a/tests/reports.test.js +++ b/tests/reports.test.js @@ -268,7 +268,7 @@ describe('reports', function ( ) { result.indexOf('50 g').should.be.greaterThan(-1); // daytoday result.indexOf('TDD average: 2.9U').should.be.greaterThan(-1); // daytoday result.indexOf('0%100%0%2').should.be.greaterThan(-1); //dailystats - //TODO FIXME result.indexOf('td class="tdborder" style="background-color:#8f8">Normal: 64.7%6').should.be.greaterThan(-1); // distribution + result.indexOf('In Range: 47.6%10').should.be.greaterThan(-1); // distribution result.indexOf('16 (100%)').should.be.greaterThan(-1); // hourlystats result.indexOf('
').should.be.greaterThan(-1); //success result.indexOf('CAL: Scale: 1.10 Intercept: 31102 Slope: 776.91').should.be.greaterThan(-1); //calibrations diff --git a/tests/timeago.test.js b/tests/timeago.test.js index c0c88b8a1b1..66306a3d154 100644 --- a/tests/timeago.test.js +++ b/tests/timeago.test.js @@ -43,33 +43,6 @@ describe('timeago', function() { done(); }); - it('should suspend alarms due to hibernation when 2 heartbeats are skipped on server', function() { - ctx.ddata.sgvs = [{ mills: Date.now() - times.mins(16).msecs, mgdl: 100, type: 'sgv' }]; - - var sbx = freshSBX() - var status = timeago.checkStatus(sbx); - // By default (no hibernation detected) a warning should be given - // we force no hibernation by checking status twice - status = timeago.checkStatus(sbx); - should.equal(status, 'warn'); - - // 10ms more than suspend-threshold to prevent flapping tests - var timeoutMs = 2 * ctx.settings.heartbeat * 1000 + 100; - return new Promise(function(resolve, reject) { - setTimeout(function() { - status = timeago.checkStatus(sbx); - // Because hibernation should now be detected, no warning should be given - should.equal(status, 'current'); - - // We immediately ask status again, so hibernation should not be detected anymore, - // and we should receive a warning again - status = timeago.checkStatus(sbx); - should.equal(status, 'warn'); - - resolve() - }, timeoutMs) - }) - }); it('should trigger a warning when data older than 15m', function(done) { ctx.notifications.initRequests(); @@ -80,9 +53,6 @@ describe('timeago', function() { var currentTime = new Date().getTime(); - // eslint-disable-next-line no-empty - while (currentTime + 500 >= new Date().getTime()) {} - var highest = ctx.notifications.findHighestAlarm('Time Ago'); highest.level.should.equal(levels.WARN); highest.message.should.equal('Last received: 16 mins ago\nBG Now: 100 mg/dl'); diff --git a/tests/units.test.js b/tests/units.test.js index b6e8a9faa8f..2fbef0c4d3e 100644 --- a/tests/units.test.js +++ b/tests/units.test.js @@ -13,4 +13,20 @@ describe('units', function ( ) { units.mgdlToMMOL(180).should.equal('10.0'); }); + it('should convert 5.5 to 99', function () { + units.mmolToMgdl(5.5).should.equal(99); + }); + + it('should convert 10.0 to 180', function () { + units.mmolToMgdl(10.0).should.equal(180); + }); + + it('should convert 5.5 mmol and then convert back to 5.5 mmol', function () { + units.mgdlToMMOL(units.mmolToMgdl(5.5)).should.equal('5.5'); + }); + + it('should convert 99 mgdl and then convert back to 99 mgdl', function () { + units.mmolToMgdl(units.mgdlToMMOL(99)).should.equal(99); + }); + }); diff --git a/views/adminindex.html b/views/adminindex.html index 6108d1b3978..c9bb631cfeb 100644 --- a/views/adminindex.html +++ b/views/adminindex.html @@ -32,23 +32,12 @@ <% include preloadCSS %> - - -
X
- -
-

Nightscout

-
- -
-

Admin Tools

-
+ + <%- include('partials/toolbar') %>
- -
- Authentication status: - + + <%- include('partials/authentication-status') %> diff --git a/views/foodindex.html b/views/foodindex.html index d42eb16a50b..0cbb6a3ebf6 100644 --- a/views/foodindex.html +++ b/views/foodindex.html @@ -32,22 +32,9 @@ <% include preloadCSS %> - + + <%- include('partials/toolbar') %> -
X
- - -
-
- Status: Not loaded -
-

Nightscout

-
-

Food Editor

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

Food Editor

- Authentication status: -
- + + <%- include('partials/authentication-status') %> + diff --git a/views/index.html b/views/index.html index b80e11ddd42..153e187b7f7 100644 --- a/views/index.html +++ b/views/index.html @@ -117,17 +117,8 @@
-
- -
Nightscout
-
+ <%- include('partials/toolbar') %> +
@@ -277,8 +268,7 @@ Reset, and use defaults
-
- + <%- include('partials/authentication-status') %>
About diff --git a/views/partials/authentication-status.ejs b/views/partials/authentication-status.ejs new file mode 100644 index 00000000000..db969c906b1 --- /dev/null +++ b/views/partials/authentication-status.ejs @@ -0,0 +1,3 @@ +
+ Authentication status: +
\ No newline at end of file diff --git a/views/partials/toolbar.ejs b/views/partials/toolbar.ejs new file mode 100644 index 00000000000..d3c8a7613fb --- /dev/null +++ b/views/partials/toolbar.ejs @@ -0,0 +1,34 @@ +
+

Nightscout

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

<%= title %>

+
+<% } %> \ No newline at end of file diff --git a/views/profileindex.html b/views/profileindex.html index d64f1300917..55a98f19dc4 100644 --- a/views/profileindex.html +++ b/views/profileindex.html @@ -32,19 +32,8 @@ <% include preloadCSS %> - -
X
- -
-
- Status: Not loaded -
-

Nightscout

-
- -
-

Profile Editor

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

Profile Editor

- Authentication status: - -

Status: Not loaded

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

Nightscout reporting

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

+
-
- Authentication status: + <%- include('partials/authentication-status') %> - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/views/translationsindex.html b/views/translationsindex.html index 4145f3d1db0..6543bb06259 100644 --- a/views/translationsindex.html +++ b/views/translationsindex.html @@ -23,19 +23,18 @@ + <% include preloadCSS %> - -
X
+ + <%- include('partials/toolbar') %> -

Nightscout translations

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