diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000000..ff3059c3f09 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ede1c7e88c8..e2e71049f4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,7 @@ Nightscout is a Node.js application. The basic installation of the software for We develop on the `dev` branch. All new pull requests should be targeted to `dev`. The `master` branch is only used for distributing the latest version of the tested sources. -You can get the dev branch checked out using `git checkout dev`. +You can get the `dev` branch checked out using `git checkout dev`. Once checked out, install the dependencies using `npm install`, then copy the included `my.env.template`file to `my.env` and edit the file to include your settings (like the Mongo URL). Leave the `NODE_ENV=development` line intact. Once set, run the site using `npm run dev`. This will start Nigthscout in the development mode, with different code packaging rules and automatic restarting of the server using nodemon, when you save changed files on disk. The client also hot-reloads new code in, but it's recommended to reload the the website after changes due to the way the plugin sandbox works. @@ -119,8 +119,9 @@ We assume all new Pull Requests are at least smoke tested by the author and all Please include a description of what the features do and rationalize why the changes are needed. If you add any new NPM module dependencies, you have to rationalize why there are needed - we prefer pull requests that reduce dependencies, not add them. +Before releasing a a new version, we check with `npm audit` if our dependencies don't have known security issues. -When adding new features that add confugration options, please ensure the `README` document is amended with information on the new configuration. +When adding new features that add configuration options, please ensure the `README` document is amended with information on the new configuration. ## Bug fixing diff --git a/README.md b/README.md index fd85b987107..917be293303 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,9 @@ If you plan to use Nightscout, we recommend using [Heroku](http://www.nightscout - Linux based install (Debian, Ubuntu, Raspbian) install with own Node.JS and MongoDB install (see software requirements below) - Windows based install with own Node.JS and MongoDB install (see software requirements below) -## Minimum browser requirements for viewing the site: +## Recommended minimum browser versions for using Nightscout: + +Older versions of the browsers might work, but are untested. - Android 4 - Chrome 68 @@ -170,9 +172,9 @@ Wanna help with development, or just see how Nigthscout works? Great! See [CONTR # Usage The data being uploaded from the server to the client is from a -MongoDB server such as [mongolab][mongodb]. +MongoDB server such as [mLab][mLab]. -[mongodb]: https://mongolab.com +[mLab]: https://mlab.com/ [autoconfigure]: https://nightscout.github.io/pages/configure/ [mongostring]: https://nightscout.github.io/pages/mongostring/ @@ -200,7 +202,7 @@ The server status and settings are available from `/api/v1/status.json`. By default the `/entries` and `/treatments` APIs limit results to the the most recent 10 values from the last 2 days. You can get many more results, by using the `count`, `date`, `dateString`, and `created_at` parameters, depending on the type of data you're looking for. -Once you've installed Nightscout, you can access API documentation by loading `/api-docs` URL in your instance. +Once you've installed Nightscout, you can access API documentation by loading `/api-docs/` URL in your instance. #### Example Queries @@ -213,7 +215,7 @@ Once you've installed Nightscout, you can access API documentation by loading `/ * Boluses over 2U: `http://localhost:1337/api/v1/treatments.json?find[insulin][$gte]=2` The API is Swagger enabled, so you can generate client code to make working with the API easy. -To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.html or review [swagger.yaml](swagger.yaml). +To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or review [swagger.yaml](swagger.yaml). ## Environment diff --git a/app.js b/app.js index 05f1d8ff694..5b6f49b9708 100644 --- a/app.js +++ b/app.js @@ -18,12 +18,12 @@ function create (env, ctx) { if (!insecureUseHttp) { console.info('Redirecting http traffic to https because INSECURE_USE_HTTP=', insecureUseHttp); app.use((req, res, next) => { - if (req.header('x-forwarded-proto') == 'https' || req.secure) { + if (req.header('x-forwarded-proto') === 'https' || req.secure) { next(); } else { res.redirect(307, `https://${req.header('host')}${req.url}`); } - }) + }); if (secureHstsHeader) { // Add HSTS (HTTP Strict Transport Security) header console.info('Enabled SECURE_HSTS_HEADER (HTTP Strict Transport Security)'); const helmet = require('helmet'); @@ -61,7 +61,7 @@ function create (env, ctx) { })); app.use(helmet.referrerPolicy({ policy: 'no-referrer' })); app.use(helmet.featurePolicy({ features: { payment: ["'none'"], } })); - app.use(bodyParser.json({ type: ['json', 'application/csp-report'] })) + app.use(bodyParser.json({ type: ['json', 'application/csp-report'] })); app.post('/report-violation', (req, res) => { if (req.body) { console.log('CSP Violation: ', req.body) @@ -84,7 +84,11 @@ function create (env, ctx) { let cacheBuster = 'developmentMode'; if (process.env.NODE_ENV !== 'development') { - cacheBuster = fs.readFileSync(process.cwd() + '/tmp/cacheBusterToken').toString().trim(); + if (fs.existsSync(process.cwd() + '/tmp/cacheBusterToken')) { + cacheBuster = fs.readFileSync(process.cwd() + '/tmp/cacheBusterToken').toString().trim(); + } else { + cacheBuster = fs.readFileSync(__dirname + '/tmp/cacheBusterToken').toString().trim(); + } } app.locals.cachebuster = cacheBuster; @@ -254,9 +258,16 @@ function create (env, ctx) { } // Production bundling - var tmpFiles = express.static('tmp', { - maxAge: maxAge - }); + var tmpFiles; + if (fs.existsSync(process.cwd() + '/tmp/cacheBusterToken')) { + tmpFiles = express.static('tmp', { + maxAge: maxAge + }); + } else { + tmpFiles = express.static(__dirname + '/tmp', { + maxAge: maxAge + }); + } // serve the static content app.use('/bundle', tmpFiles); diff --git a/lib/client/browser-settings.js b/lib/client/browser-settings.js index d7782170b2a..4f990eb57f3 100644 --- a/lib/client/browser-settings.js +++ b/lib/client/browser-settings.js @@ -104,7 +104,7 @@ function init (client, serverSettings, $) { showPluginsSettings.toggle(hasPluginsToShow); - const bs = $('.browserSettings'); + const bs = $('#browserSettings'); const toggleCheckboxes = []; if (pluginPrefs.length > 0) { diff --git a/lib/client/careportal.js b/lib/client/careportal.js index 7b39ce6d2c4..4a89ffcebc8 100644 --- a/lib/client/careportal.js +++ b/lib/client/careportal.js @@ -265,7 +265,7 @@ function init (client, $) { console.log('Validating careportal entry: ', data.eventType); - if (data.eventType == 'Temporary Target') { + if (data.duration !== 0 && data.eventType == 'Temporary Target') { if (isNaN(data.targetTop) || isNaN(data.targetBottom) || !data.targetBottom || !data.targetTop) { console.log('Bottom or Top target missing'); allOk = false; diff --git a/lib/client/clock-client.js b/lib/client/clock-client.js index 2b4c063ff9f..18b5116f94d 100644 --- a/lib/client/clock-client.js +++ b/lib/client/clock-client.js @@ -11,10 +11,27 @@ client.settings = browserSettings(client, window.serverSettings, $); client.query = function query () { console.log('query'); - $.ajax('/api/v1/entries.json?count=3', { + var parts = (location.search || '?').substring(1).split('&'); + var token = ''; + parts.forEach(function (val) { + if (val.startsWith('token=')) { + token = val.substring('token='.length); + } + }); + + var secret = localStorage.getItem('apisecrethash'); + var src = '/api/v1/entries.json?count=3&t=' + new Date().getTime(); + + if (secret) { + src += '&secret=' + secret; + } else if (token) { + src += '&token=' + token; + } + + $.ajax(src, { success: client.render }); -} +}; client.render = function render (xhr) { console.log('got data', xhr); @@ -28,11 +45,27 @@ client.render = function render (xhr) { } }); + let $errorMessage = $('#errorMessage'); + + // If no one measured value found => show "-?-" + if (!rec) { + if (!$errorMessage.length) { + $('#arrowDiv').append('
-?-
'); + $('#arrow').hide(); + } else { + $errorMessage.show(); + } + return; + } else { + $errorMessage.length && $errorMessage.hide(); + $('#arrow').show(); + } + let last = new Date(rec.date); let now = new Date(); // Convert BG to mmol/L if necessary. - if (window.serverSettings.settings.units == 'mmol') { + if (window.serverSettings.settings.units === 'mmol') { var displayValue = window.Nightscout.units.mgdlToMMOL(rec.sgv); } else { displayValue = rec.sgv; @@ -42,7 +75,7 @@ client.render = function render (xhr) { $('#bgnow').html(displayValue); // Insert the trend arrow. - $('#arrow').attr('src', '/images/' + rec.direction + '.svg'); + $('#arrow').attr('src', '/images/' + (!rec.direction || rec.direction === 'NOT COMPUTABLE' ? 'NONE' : rec.direction) + '.svg'); // Time before data considered stale. let staleMinutes = 13; @@ -52,13 +85,13 @@ client.render = function render (xhr) { $('#bgnow').toggleClass('stale', (now - last > threshold)); // Generate and insert the clock. - let timeDivisor = (client.settings.timeFormat) ? client.settings.timeFormat : 12; + let timeDivisor = parseInt(client.settings.timeFormat ? client.settings.timeFormat : 12, 10); let today = new Date() , h = today.getHours() % timeDivisor; - if (timeDivisor == 12) { - h = (h == 0) ? 12 : h; // In the case of 00:xx, change to 12:xx for 12h time + if (timeDivisor === 12) { + h = (h === 0) ? 12 : h; // In the case of 00:xx, change to 12:xx for 12h time } - if (timeDivisor == 24) { + if (timeDivisor === 24) { h = (h < 10) ? ("0" + h) : h; // Pad the hours with a 0 in 24h time } let m = today.getMinutes(); @@ -67,14 +100,14 @@ client.render = function render (xhr) { var queryDict = {}; location.search.substr(1).split("&").forEach(function(item) { queryDict[item.split("=")[0]] = item.split("=")[1] }); - + if (!window.serverSettings.settings.showClockClosebutton || !queryDict['showClockClosebutton']) { $('#close').css('display', 'none'); } // defined in the template this is loaded into // eslint-disable-next-line no-undef - if (clockFace == 'clock-color') { + if (clockFace === 'clock-color') { var bgHigh = window.serverSettings.settings.thresholds.bgHigh; var bgLow = window.serverSettings.settings.thresholds.bgLow; @@ -138,12 +171,12 @@ client.render = function render (xhr) { $('#arrow').css('filter', 'brightness(100%)'); } } -} +}; client.init = function init () { console.log('init'); client.query(); setInterval(client.query, 1 * 60 * 1000); -} +}; module.exports = client; diff --git a/lib/client/index.js b/lib/client/index.js index 63b37d7c9ed..979348c0cb5 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1,5 +1,4 @@ 'use strict'; -'use strict'; var _ = require('lodash'); var $ = (global && global.$) || require('jquery'); @@ -64,7 +63,7 @@ client.init = function init (callback) { }).fail(function fail (jqXHR, textStatus, errorThrown) { // check if we couldn't reach the server at all, show offline message - if (jqXHR.readyState == 0) { + if (!jqXHR.readyState) { console.log('Application appears to be OFFLINE'); $('#loadingMessageText').html('Connecting to Nightscout server failed, retrying every 2 seconds'); window.setTimeout(window.Nightscout.client.init(), 2000); @@ -76,7 +75,20 @@ client.init = function init (callback) { console.log('Already tried to get settings after auth, but failed'); } else { client.settingsFailed = true; - language.set('en'); + + // detect browser language + var lang = Storages.localStorage.get('language') || (navigator.language || navigator.userLanguage).toLowerCase(); + if (lang !== 'zh_cn' && lang !== 'zh-cn' && lang !== 'zh_tw' && lang !== 'zh-tw') { + lang = lang.substring(0, 2); + } else { + lang = lang.replace('-', '_'); + } + if (language.languages.find(l => l.code === lang)) { + language.set(lang); + } else { + language.set('en'); + } + client.translate = language.translate; // auth failed, hide loader and request for key $('#centerMessagePanel').hide(); @@ -993,7 +1005,7 @@ client.load = function load (serverSettings, callback) { socket.on('notification', function(notify) { console.log('notification from server:', notify); - if (notify.timestamp && previousNotifyTimestamp != notify.timestamp) { + if (notify.timestamp && previousNotifyTimestamp !== notify.timestamp) { previousNotifyTimestamp = notify.timestamp; client.plugins.visualizeAlarm(client.sbx, notify, notify.title + ' ' + notify.message); } else { diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 5b1c36ad83b..ccbac62d72b 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -53,7 +53,7 @@ function init (client, d3) { // get the desired opacity for context chart based on the brush extent renderer.highlightBrushPoints = function highlightBrushPoints (data) { - if (data.mills >= chart().brush.extent()[0].getTime() && data.mills <= chart().brush.extent()[1].getTime()) { + if (client.latestSGV && data.mills >= chart().brush.extent()[0].getTime() && data.mills <= chart().brush.extent()[1].getTime()) { return chart().futureOpacity(data.mills - client.latestSGV.mills); } else { return 0.5; @@ -111,7 +111,7 @@ function init (client, d3) { return d.type === 'forecast' ? 'none' : d.color; }) .attr('opacity', function(d) { - return d.noFade ? 100 : chart().futureOpacity(d.mills - client.latestSGV.mills); + return d.noFade || !client.latestSGV ? 100 : chart().futureOpacity(d.mills - client.latestSGV.mills); }) .attr('stroke-width', function(d) { return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 2 : 0; @@ -173,13 +173,21 @@ function init (client, d3) { renderer.addTreatmentCircles = function addTreatmentCircles () { function treatmentTooltip (d) { + var targetBottom = d.targetBottom; + var targetTop = d.targetTop; + + if (client.settings.units === 'mmol') { + targetBottom = Math.round(targetBottom / 18.0 * 10) / 10; + targetTop = Math.round(targetTop / 18.0 * 10) / 10; + } + return '' + translate('Time') + ': ' + client.formatTime(new Date(d.mills)) + '
' + (d.eventType ? '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
' : '') + (d.reason ? '' + translate('Reason') + ': ' + translate(d.reason) + '
' : '') + (d.glucose ? '' + translate('BG') + ': ' + d.glucose + (d.glucoseType ? ' (' + translate(d.glucoseType) + ')' : '') + '
' : '') + (d.enteredBy ? '' + translate('Entered By') + ': ' + d.enteredBy + '
' : '') + - (d.targetTop ? '' + translate('Target Top') + ': ' + d.targetTop + '
' : '') + - (d.targetBottom ? '' + translate('Target Bottom') + ': ' + d.targetBottom + '
' : '') + + (d.targetTop ? '' + translate('Target Top') + ': ' + targetTop + '
' : '') + + (d.targetBottom ? '' + translate('Target Bottom') + ': ' + targetBottom + '
' : '') + (d.duration ? '' + translate('Duration') + ': ' + Math.round(d.duration) + ' min
' : '') + (d.notes ? '' + translate('Notes') + ': ' + d.notes : ''); } @@ -517,6 +525,14 @@ function init (client, d3) { } function treatmentTooltip () { + var glucose = treatment.glucose; + if (client.settings.units != client.ddata.profile.data[0].units) { + glucose *= (client.settings.units === 'mmol' ? 0.055 : 18); + var decimals = (client.settings.units === 'mmol' ? 10 : 1); + + glucose = Math.round(glucose * decimals) / decimals; + } + client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); client.tooltip.html('' + translate('Time') + ': ' + client.formatTime(new Date(treatment.mills)) + '
' + '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '
' + (treatment.carbs ? '' + translate('Carbs') + ': ' + treatment.carbs + '
' : '') + @@ -525,7 +541,7 @@ function init (client, d3) { (treatment.absorptionTime > 0 ? '' + translate('Absorption Time') + ': ' + (Math.round(treatment.absorptionTime / 60.0 * 10) / 10) + 'h' + '
' : '') + (treatment.insulin ? '' + translate('Insulin') + ': ' + treatment.insulin + '
' : '') + (treatment.enteredinsulin ? '' + translate('Combo Bolus') + ': ' + treatment.enteredinsulin + 'U, ' + treatment.splitNow + '% : ' + treatment.splitExt + '%, ' + translate('Duration') + ': ' + treatment.duration + '
' : '') + - (treatment.glucose ? '' + translate('BG') + ': ' + treatment.glucose + (treatment.glucoseType ? ' (' + translate(treatment.glucoseType) + ')' : '') + '
' : '') + + (treatment.glucose ? '' + translate('BG') + ': ' + glucose + (treatment.glucoseType ? ' (' + translate(treatment.glucoseType) + ')' : '') + '
' : '') + (treatment.enteredBy ? '' + translate('Entered By') + ': ' + treatment.enteredBy + '
' : '') + (treatment.notes ? '' + translate('Notes') + ': ' + treatment.notes : '') + boluscalcTooltip(treatment) @@ -897,7 +913,7 @@ function init (client, d3) { , treatments: treatmentCount }, client.sbx.data.profile.getCarbRatio(new Date())); }); - } + }; renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio) { if (!treatment.carbs && !treatment.insulin) { @@ -920,7 +936,7 @@ function init (client, d3) { var arc = prepareArc(treatment, radius); var treatmentDots = appendTreatments(treatment, arc); appendLabels(treatmentDots, arc, opts); - } + }; renderer.addBasals = function addBasals (client) { @@ -938,7 +954,7 @@ function init (client, d3) { var lastbasal = 0; if (!profile.activeProfileToTime(from)) { - window.alert(translate('Wrong profile setting.\nNo profile defined to displayed time.\nRedirecting to profile editor to create new profile.')); + window.alert(translate('Redirecting you to the Profile Editor to create a new profile.')); try { window.location.href = '/profile'; } catch (err) { @@ -1009,7 +1025,7 @@ function init (client, d3) { .attr('stroke', '#0099ff') .attr('stroke-width', 1) .attr('fill', 'none') - .attr('d', valueline(linedata)) + .attr('d', valueline(linedata)); g.append('path') .attr('class', 'line notempline') @@ -1017,7 +1033,7 @@ function init (client, d3) { .attr('stroke-width', 1) .attr('stroke-dasharray', ('3, 3')) .attr('fill', 'none') - .attr('d', valueline(notemplinedata)) + .attr('d', valueline(notemplinedata)); g.append('path') .attr('class', 'area basalarea') diff --git a/lib/hashauth.js b/lib/hashauth.js index d39060a2e2b..0840381a24c 100644 --- a/lib/hashauth.js +++ b/lib/hashauth.js @@ -55,7 +55,7 @@ hashauth.init = function init(client, $) { }); return hashauth; }; - + hashauth.removeAuthentication = function removeAuthentication(event) { Storages.localStorage.remove('apisecrethash'); @@ -74,16 +74,18 @@ hashauth.init = function init(client, $) { } return false; }; - + hashauth.requestAuthentication = function requestAuthentication (eventOrNext) { var translate = client.translate; hashauth.injectHtml(); $( '#requestauthenticationdialog' ).dialog({ - width: 350 - , height: 240 + width: 400 + , height: 270 + , closeText: '' , buttons: [ { - text: translate('Update') + id: 'requestauthenticationdialog-btn' + , text: translate('Update') , click: function() { var dialog = this; hashauth.processSecret($('#apisecret').val(), $('#storeapisecret').is(':checked'), function done (close) { @@ -102,9 +104,9 @@ hashauth.init = function init(client, $) { } ] , open: function open ( ) { - $('#requestauthenticationdialog').keypress(function pressed (e) { + $('#apisecret').off('keyup').on('keyup' ,function pressed (e) { if (e.keyCode === $.ui.keyCode.ENTER) { - $(this).parent().find('button.ui-button-text-only').trigger('click'); + $('#requestauthenticationdialog-btn').trigger('click'); } }); $('#apisecret').val('').focus(); @@ -117,7 +119,7 @@ hashauth.init = function init(client, $) { } return false; }; - + hashauth.processSecret = function processSecret(apisecret, storeapisecret, callback) { var translate = client.translate; @@ -170,9 +172,9 @@ hashauth.init = function init(client, $) { var html = '