diff --git a/README.md b/README.md index 88e3cab99b4..9641ec3da91 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,11 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm * `SHOW_PLUGINS` - enabled plugins that should have their visualizations shown, defaults to all enabled - * `LANGUAGE` (`en`) - language of Nighscout. If not available english is used + * `LANGUAGE` (`en`) - language of Nightscout. If not available english is used + * `SCALE_Y` (`log`) - The type of scaling used for the Y axis of the charts system wide. + * The default `log` (logarithmic) option will let you see more detail towards the lower range, while still showing the full CGM range. + * The `linear` option has equidistant tick marks, the range used is dynamic so that space at the top of chart isn't wasted. + * The `log-dynamic` is similar to the default `log` options, but uses the same dynamic range and the `linear` scale. ### Plugins @@ -241,7 +245,8 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `CAGE_WARN` (`48`) - If time since last `Site Change` matches `CAGE_WARN`, user will be alarmed to to change the cannula * `CAGE_URGENT` (`72`) - If time since last `Site Change` matches `CAGE_URGENT`, user will be issued a persistent warning of overdue change. * `treatmentnotify` (Treatment Notifications) - Generates notifications when a treatment has been entered and snoozes alarms minutes after a treatment. Default snooze is 10 minutes, and can be set using the `TREATMENTNOTIFY_SNOOZE_MINS` [extended setting](#extended-settings). - * `basal` (Basal Profile) - Adds the Basal pill visualization to display the basal rate for the current time. Also enables the `bwp` plugin to calculate correction temp basal suggestions. Uses the `basal` field from the [treatment profile](#treatment-profile). + * `basal` (Basal Profile) - Adds the Basal pill visualization to display the basal rate for the current time. Also enables the `bwp` plugin to calculate correction temp basal suggestions. Uses the `basal` field from the [treatment profile](#treatment-profile). Also uses the extended setting: + * `BASAL_RENDER` (`none`) - Possible values are `none`, `default`, or `icicle` (inverted) * `bridge` (Share2Nightscout bridge) - Glucose reading directly from the Share service, uses these extended settings: * `BRIDGE_USER_NAME` - Your user name for the Share service. * `BRIDGE_PASSWORD` - Your password for the Share service. @@ -250,6 +255,14 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `BRIDGE_FIRST_FETCH_COUNT` (`3`) - Changes max count during the very first update only. * `BRIDGE_MAX_FAILURES` (`3`) - How many failures before giving up. * `BRIDGE_MINUTES` (`1400`) - The time window to search for new data per update (default is one day in minutes). + * `mmconnect` (MiniMed Connect bridge) - Transfer real-time MiniMed Connect data from the Medtronic CareLink server into Nightscout ([read more](https://github.com/mddub/minimed-connect-to-nightscout)) + * `MMCONNECT_USER_NAME` - Your user name for CareLink Connect. + * `MMCONNECT_PASSWORD` - Your password for CareLink Connect. + * `MMCONNECT_INTERVAL` (`60000` *1 minute*) - Number of milliseconds to wait between requests to the CareLink server. + * `MMCONNECT_MAX_RETRY_DURATION` (`32`) - Maximum number of total seconds to spend retrying failed requests before giving up. + * `MMCONNECT_SGV_LIMIT` (`24`) - Maximum number of recent sensor glucose values to send to Nightscout on each request. + * `MMCONNECT_VERBOSE` - Set this to "true" to log CareLink request information to the console. + * `MMCONNECT_STORE_RAW_DATA` - Set this to "true" to store raw data returned from CareLink as `type: "carelink_raw"` database entries (useful for development). Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker). diff --git a/app.js b/app.js index fa807b81a5d..9618fde24ed 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,7 @@ var express = require('express'); var compression = require('compression'); +var bodyParser = require('body-parser'); function create (env, ctx) { /////////////////////////////////////////////////// // api and json object variables @@ -17,11 +18,12 @@ function create (env, ctx) { // fallback to standard filter function return compression.filter(req, res); }})); + // app.use(bodyParser({limit: 1048576 * 50, extended: true })); //if (env.api_secret) { // console.log("API_SECRET", env.api_secret); //} - app.use('/api/v1', api); + app.use('/api/v1', bodyParser({limit: 1048576 * 50 }), api); // pebble data diff --git a/bower.json b/bower.json index 0511964f203..57c271450a2 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.8.1", + "version": "0.8.2", "dependencies": { "jquery": "2.1.0", "jQuery-Storage-API": "~1.7.2", diff --git a/bundle/bundle.source.js b/bundle/bundle.source.js index 0ca7d7235eb..d78ecb3c868 100644 --- a/bundle/bundle.source.js +++ b/bundle/bundle.source.js @@ -10,6 +10,7 @@ , units: require('../lib/units')() , plugins: require('../lib/plugins/')().registerClientDefaults() , report_plugins: require('../lib/report_plugins/')() + , admin_plugins: require('../lib/admin_plugins/')() }; console.info('Nightscout bundle ready'); diff --git a/lib/admin_plugins/cleanstatusdb.js b/lib/admin_plugins/cleanstatusdb.js new file mode 100644 index 00000000000..86e61e987f8 --- /dev/null +++ b/lib/admin_plugins/cleanstatusdb.js @@ -0,0 +1,70 @@ +'use strict'; + +var cleanstatusdb = { + name: 'cleanstatusdb' + , label: 'Clean Mongo status database' + , pluginType: 'admin' +}; + +function init() { + return cleanstatusdb; +} + +module.exports = init; + +cleanstatusdb.actions = [ + { + name: 'Delete all documents from devicestatus collection' + , description: 'This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.' + , buttonLabel: 'Delete all documents' + , confirmText: 'Delete all documents from devicestatus collection?' + } + ]; + +cleanstatusdb.actions[0].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanstatusdb.name + '_0_status'); + + $status.hide().text(translate('Loading database ...')).fadeIn('slow'); + $.ajax('/api/v1/devicestatus.json?count=500', { + success: function (records) { + var recs = (records.length === 500 ? '500+' : records.length); + $status.hide().text(translate('Database contains %1 records',{ params: [recs] })).fadeIn('slow'); + }, + error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + } + }).done(function () { if (callback) { callback(); } }); +}; + +cleanstatusdb.actions[0].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanstatusdb.name + '_0_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + $.ajax({ + method: 'DELETE' + , url: '/api/v1/devicestatus/*' + , headers: { + 'api-secret': client.hashauth.hash() + } + }).done(function success () { + $status.hide().text(translate('All records removed ...')).fadeIn('slow'); + if (callback) { + callback(); + } + }).fail(function fail() { + $status.hide().text(translate('Error')).fadeIn('slow'); + if (callback) { + callback(); + } + }); +}; diff --git a/lib/admin_plugins/futureitems.js b/lib/admin_plugins/futureitems.js new file mode 100644 index 00000000000..ab888aff0a6 --- /dev/null +++ b/lib/admin_plugins/futureitems.js @@ -0,0 +1,172 @@ +'use strict'; + +var futureitems = { + name: 'futureitems' + , label: 'Remove future items from mongo database' + , pluginType: 'admin' +}; + +function init() { + return futureitems; +} + +module.exports = init; + +futureitems.actions = [ + { + name: 'Find and remove treatments in the future' + , description: 'This task find and remove treatments in the future.' + , buttonLabel: 'Remove treatments in the future' + } + , { + name: 'Find and remove entries in the future' + , description: 'This task find and remove CGM data in the future created by uploader with wrong date/time.' + , buttonLabel: 'Remove entries in the future' + } + ]; + +futureitems.actions[0].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_0_status'); + + function valueOrEmpty (value) { + return value ? value : ''; + } + + function showOneTreatment (tr, table) { + table.append($('').css('background-color','#0f0f0f') + .append($('').attr('width','20%').append(new Date(tr.created_at).toLocaleString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'))) + .append($('').attr('width','20%').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) : '')) + .append($('').attr('width','10%').attr('align','center').append(tr.glucose ? tr.glucose + ' ('+translate(tr.glucoseType)+')' : '')) + .append($('').attr('width','10%').attr('align','center').append(valueOrEmpty(tr.insulin))) + .append($('').attr('width','10%').attr('align','center').append(valueOrEmpty(tr.carbs))) + .append($('').attr('width','10%').append(valueOrEmpty(tr.enteredBy))) + .append($('').attr('width','20%').append(valueOrEmpty(tr.notes))) + ); + } + + function showTreatments(treatments, table) { + table.append($('').css('background','#040404') + .append($('').css('width','80px').attr('align','left').append(translate('Time'))) + .append($('').css('width','150px').attr('align','left').append(translate('Event Type'))) + .append($('').css('width','150px').attr('align','left').append(translate('Blood Glucose'))) + .append($('').css('width','50px').attr('align','left').append(translate('Insulin'))) + .append($('').css('width','50px').attr('align','left').append(translate('Carbs'))) + .append($('').css('width','150px').attr('align','left').append(translate('Entered By'))) + .append($('').css('width','300px').attr('align','left').append(translate('Notes'))) + ); + for (var t=0; t').css('margin-top','10px'); + $('#admin_' + futureitems.name + '_0_html').append(table); + showTreatments(records, table); + futureitems.actions[0].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); + }, + error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + futureitems.treatmentrecords = []; + } + }).done(function () { if (callback) { callback(); } }); +}; + +futureitems.actions[0].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_0_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + function deleteRecordById (_id) { + $.ajax({ + method: 'DELETE' + , url: '/api/v1/treatments/' + _id + , headers: { + 'api-secret': client.hashauth.hash() + } + }).done(function success () { + $status.text(translate('Record %1 removed ...', { params: [_id] })); + }).fail(function fail() { + $status.text(translate('Error removing record %1', { params: [_id] })); + }); + } + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + for (var i = 0; i < futureitems.treatmentrecords.length; i++) { + deleteRecordById(futureitems.treatmentrecords[i]._id); + } + $('#admin_' + futureitems.name + '_0_html').html(''); + + if (callback) { + callback(); + } +}; + +futureitems.actions[1].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_1_status'); + + $status.hide().text(translate('Loading database ...')).fadeIn('slow'); + var now = new Date().getTime(); + $.ajax('/api/v1/entries.json?&find[date][$gte]=' + now, { + success: function (records) { + futureitems.entriesrecords = records; + $status.hide().text(translate('Database contains %1 future records',{ params: [records.length] })).fadeIn('slow'); + futureitems.actions[1].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); + }, + error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + futureitems.entriesrecords = []; + } + }).done(function () { if (callback) { callback(); } }); +}; + +futureitems.actions[1].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_1_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + function deteleteRecordById (_id) { + $.ajax({ + method: 'DELETE' + , url: '/api/v1/entries/' + _id + , headers: { + 'api-secret': client.hashauth.hash() + } + }).done(function success () { + $status.text(translate('Record %1 removed ...', { params: [_id] })); + }).fail(function fail() { + $status.text(translate('Error removing record %1', { params: [_id] })); + }); + } + + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + for (var i = 0; i < futureitems.entriesrecords.length; i++) { + deteleteRecordById(futureitems.entriesrecords[i]._id); + } + + if (callback) { + callback(); + } +}; diff --git a/lib/admin_plugins/index.js b/lib/admin_plugins/index.js new file mode 100644 index 00000000000..f7a5c74133d --- /dev/null +++ b/lib/admin_plugins/index.js @@ -0,0 +1,79 @@ +'use strict'; + +var _ = require('lodash'); + +function init() { + var allPlugins = [ + require('./cleanstatusdb')() + , require('./futureitems')() + ]; + + function plugins(name) { + if (name) { + return _.find(allPlugins, {name: name}); + } else { + return plugins; + } + } + + plugins.eachPlugin = function eachPlugin(f) { + _.each(allPlugins, f); + }; + + plugins.createHTML = function createHTML(client) { + var translate = client.translate; + plugins.eachPlugin(function addHtml(p) { + var fs = $('
'); + $('#admin_placeholder').append(fs); + fs.append($('').append(translate(p.label))); + for (var i = 0; i < p.actions.length; i++) { + if (i !== 0) { + fs.append('
'); + } + var a = p.actions[i]; + // add main plugin html + fs.append($('').css('text-decoration','underline').append(translate(a.name))); + fs.append('
'); + fs.append($('').append(translate(a.description))); + fs.append($('
').attr('id','admin_' + p.name + '_' + i + '_html')); + fs.append($('