diff --git a/README.md b/README.md
index 88e3cab99b4..fff1d1180a6 100644
--- a/README.md
+++ b/README.md
@@ -250,6 +250,13 @@ 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 any truthy value to log CareLink request information to the console.
Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker).
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 = $(' |