From 19e54f189a1b5792b97210105dfbd69da658d80f Mon Sep 17 00:00:00 2001 From: John Costik Date: Mon, 25 Aug 2014 23:00:52 -0400 Subject: [PATCH 01/29] Added "Circles" to represent treatment entries, w/ mouse over --- static/css/main.css | 13 +++++++++++ static/js/client.js | 56 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/static/css/main.css b/static/css/main.css index e85fd2292c5..60fee88de0d 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -180,6 +180,19 @@ body { margin: 10px auto; } +div.tooltip { + position: absolute; + text-align: left; + width: fit-content; + height: fit-content; + padding: 2px; + font: 14px sans-serif; + background: white; + border: 0px; + border-radius: 8px; + pointer-events: none; +} + #silenceBtn, #silenceBtn * { font-size: 70%; } diff --git a/static/js/client.js b/static/js/client.js index 94072b49af9..d2db83075d7 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -42,7 +42,9 @@ if (browserSettings.units == "mmol") { var tickValues = [2.0, 3.0, 4.0, 6.0, 10.0, 15.0, 22.0]; } - + var div = d3.select("body").append("div") + .attr("class", "tooltip") + .style("opacity", 0); //TODO: get these from the config var targetTop = 180, targetBottom = 80; @@ -102,7 +104,7 @@ .domain(d3.extent(data, function (d) { return d.date; })); yScale = d3.scale.log() - .domain([scaleBg(30), scaleBg(420)]); + .domain([scaleBg(30), scaleBg(510)]); xScale2 = d3.time.scale() .domain(d3.extent(data, function (d) { return d.date; })); @@ -382,6 +384,52 @@ // add clipping path so that data stays within axis focusCircles.attr('clip-path', 'url(#clip)'); + + try { + // bind up the focus chart data to an array of circles + var treatCircles = focus.selectAll('rect').data(treatments); + + // if already existing then transition each circle to its new position + treatCircles.transition() + .duration(UPDATE_TRANS_MS) + .attr('x', function (d) { return xScale(new Date(d.created_at)); }) + .attr('y', function (d) { return yScale(500); }) + .attr("width", 15) + .attr("height", 15) + .attr("rx", 6) + .attr("ry", 6) + .attr('stroke-width', 2) + .attr('stroke', function (d) { return "white"; }) + .attr('fill', function (d) { return "grey"; }) + + + // if new circle then just display + treatCircles.enter().append('rect') + .attr('x', function (d) { return xScale(d.created_at); }) + .attr('y', function (d) { return yScale(500); }) + .attr('fill', function (d) { return "grey"; }) + .on("mouseover", function (d) { + div.transition() + .duration(200) + .style("opacity", .9); + div.html("Time: " + formatTime(d.created_at) + "
" + "Treatment type: " + d.eventType + "
" + "Carbs: " + d.carbs + "
" + + "Insulin: " + d.insulin + "
" + + "BG: " + d.glucose + "
" + + "Test method: " + d.glucoseType + "
" + + "Entered by: " + d.enteredBy + "
" + + "Notes: " + d.notes) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + }) + .on("mouseout", function (d) { + div.transition() + .duration(500) + .style("opacity", 0); + }); + + treatCircles.attr('clip-path', 'url(#clip)'); + } catch (err) + { } } // called for initial update and updates for resize @@ -760,6 +808,10 @@ }) treatments = d[3]; + treatments.forEach(function (d) { + + d.created_at = new Date(d.created_at); + }) if (!isInitialData) { isInitialData = true; initializeCharts(); From 911085921a0e293f97d383ced4b870ce8d98573f Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Mon, 25 Aug 2014 22:11:21 -0700 Subject: [PATCH 02/29] added confirm dialog back --- static/js/ui-utils.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js index ccee0b3b013..569df584abe 100644 --- a/static/js/ui-utils.js +++ b/static/js/ui-utils.js @@ -261,14 +261,26 @@ function treatmentSubmit(event) { var dataJson = JSON.stringify(data, null, " "); - var xhr = new XMLHttpRequest(); - xhr.open("POST", "/api/v1/treatments/", true); - xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - xhr.send(dataJson); - - browserStorage.set("enteredBy", data.enteredBy); - - closeTreatmentDrawer(); + var ok = window.confirm( + 'Please verify that the data entered is correct: ' + + '\nEntered By: ' + data.enteredBy + + '\nEvent type: ' + data.eventType + + '\nBlood glucose: ' + data.glucose + + '\nMethod: ' + data.glucoseType + + '\nCarbs Given: ' + data.carbs + + '\nInsulin Given: ' + data.insulin + + '\nNotes: ' + data.notes); + + if (ok) { + var xhr = new XMLHttpRequest(); + xhr.open("POST", "/api/v1/treatments/", true); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.send(dataJson); + + browserStorage.set("enteredBy", data.enteredBy); + + closeTreatmentDrawer(); + } if (event) { event.preventDefault(); From c55d7c0718e959a655b1398edc8066a90e004fe7 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Mon, 25 Aug 2014 22:15:24 -0700 Subject: [PATCH 03/29] minor clean up --- static/js/client.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/static/js/client.js b/static/js/client.js index d2db83075d7..2f709848493 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -428,8 +428,9 @@ }); treatCircles.attr('clip-path', 'url(#clip)'); - } catch (err) - { } + } catch (err) { + console.error(err); + } } // called for initial update and updates for resize @@ -805,13 +806,13 @@ data.forEach(function (d) { if (d.sgv < 39) d.color = "transparent"; - }) + }); treatments = d[3]; treatments.forEach(function (d) { + d.created_at = new Date(d.created_at); + }); - d.created_at = new Date(d.created_at); - }) if (!isInitialData) { isInitialData = true; initializeCharts(); From e9f0343ca43adc5a6ba0f5c7c93aadfa77646b0f Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Mon, 25 Aug 2014 22:18:45 -0700 Subject: [PATCH 04/29] revert changes to original drawTreatments, not used right now --- static/js/client.js | 148 ++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 87 deletions(-) diff --git a/static/js/client.js b/static/js/client.js index 2f709848493..f7f15c3f60f 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -344,10 +344,11 @@ d3.selectAll('.path').remove(); // add treatment bubbles - var bubbleSize = prevChartWidth < 400 ? 4 : (prevChartWidth < 600 ? 3 : 2); - focus.selectAll('circle') - .data(treatments) - .each(function (d) { drawTreatment(d, bubbleSize, true) }); + // + //var bubbleSize = prevChartWidth < 400 ? 4 : (prevChartWidth < 600 ? 3 : 2); + //focus.selectAll('circle') + // .data(treatments) + // .each(function (d) { drawTreatment(d, bubbleSize, true) }); // transition open-top line to correct location focus.select('.open-top') @@ -980,91 +981,64 @@ //draw a compact visualization of a treatment (carbs, insulin) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function drawTreatment(treatment, scale, showValues) { - - if (!treatment.CR) { - //plot a simple treatment point - console.info("plotting treatment", treatment); - var treatmentDots = focus.selectAll('treatment-dot') - .data(treatment) - .enter() - .append('g') - .attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(300)) + ')'); - - //TODO: some d3 magic to get the treatments to display and show a tooltip - } else { - var carbs = treatment.carbs; - var insulin = treatment.insulin; - var CR = treatment.CR; - - var R1 = Math.sqrt(Math.min(carbs, insulin * CR)) / scale, - R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / scale, - R3 = R2 + 8 / scale; - - var arc_data = [ - { 'element': '', 'color': '#9c4333', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': R1 }, - { 'element': '', 'color': '#d4897b', 'start': -1.5708, 'end': 1.5708, 'inner': R1, 'outer': R2 }, - { 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': R2, 'outer': R3 }, - { 'element': '', 'color': '#3d53b7', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': R1 }, - { 'element': '', 'color': '#5d72c9', 'start': 1.5708, 'end': 4.7124, 'inner': R1, 'outer': R2 }, - { 'element': '', 'color': 'transparent', 'start': 1.5708, 'end': 4.7124, 'inner': R2, 'outer': R3 } - ]; - - if (carbs < insulin * CR) arc_data[1].color = 'transparent'; - if (carbs > insulin * CR) arc_data[4].color = 'transparent'; - if (carbs > 0) arc_data[2].element = Math.round(carbs) + ' g'; - if (insulin > 0) arc_data[5].element = Math.round(insulin * 10) / 10 + ' U'; - - var arc = d3.svg.arc() - .innerRadius(function (d) { - return 5 * d.inner; - }) - .outerRadius(function (d) { - return 5 * d.outer; - }) - .endAngle(function (d) { - return d.start; - }) - .startAngle(function (d) { - return d.end; - }); - - var treatmentDots = focus.selectAll('treatment-dot') - .data(arc_data) - .enter() - .append('g') - .attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(treatment.y)) + ')'); - - var arcs = treatmentDots.append('path') + var carbs = treatment.carbs; + var insulin = treatment.insulin; + var CR = treatment.CR; + + var R1 = Math.sqrt(Math.min(carbs, insulin * CR)) / scale, + R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / scale, + R3 = R2 + 8 / scale; + + var arc_data = [ + { 'element': '', 'color': '#9c4333', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': R1 }, + { 'element': '', 'color': '#d4897b', 'start': -1.5708, 'end': 1.5708, 'inner': R1, 'outer': R2 }, + { 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': R2, 'outer': R3 }, + { 'element': '', 'color': '#3d53b7', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': R1 }, + { 'element': '', 'color': '#5d72c9', 'start': 1.5708, 'end': 4.7124, 'inner': R1, 'outer': R2 }, + { 'element': '', 'color': 'transparent', 'start': 1.5708, 'end': 4.7124, 'inner': R2, 'outer': R3 } + ]; + + if (carbs < insulin * CR) arc_data[1].color = 'transparent'; + if (carbs > insulin * CR) arc_data[4].color = 'transparent'; + if (carbs > 0) arc_data[2].element = Math.round(carbs) + ' g'; + if (insulin > 0) arc_data[5].element = Math.round(insulin * 10) / 10 + ' U'; + + var arc = d3.svg.arc() + .innerRadius(function (d) { return 5 * d.inner; }) + .outerRadius(function (d) { return 5 * d.outer; }) + .endAngle(function (d) { return d.start; }) + .startAngle(function (d) { return d.end; }); + + var treatmentDots = focus.selectAll('treatment-dot') + .data(arc_data) + .enter() + .append('g') + .attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(treatment.y)) + ')'); + + var arcs = treatmentDots.append('path') + .attr('class', 'path') + .attr('fill', function (d, i) { return d.color; }) + .attr('id', function (d, i) { return 's' + i; }) + .attr('d', arc); + + + // labels for carbs and insulin + if (showValues) { + var label = treatmentDots.append('g') .attr('class', 'path') - .attr('fill', function (d, i) { - return d.color; + .attr('id', 'label') + .style('fill', 'white'); + label.append('text') + .style('font-size', 30 / scale) + .style('font-family', 'Arial') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .attr('transform', function (d) { + d.outerRadius = d.outerRadius * 2.1; + d.innerRadius = d.outerRadius * 2.1; + return 'translate(' + arc.centroid(d) + ')'; }) - .attr('id', function (d, i) { - return 's' + i; - }) - .attr('d', arc); - - - // labels for carbs and insulin - if (showValues) { - var label = treatmentDots.append('g') - .attr('class', 'path') - .attr('id', 'label') - .style('fill', 'white'); - label.append('text') - .style('font-size', 30 / scale) - .style('font-family', 'Arial') - .attr('text-anchor', 'middle') - .attr('dy', '.35em') - .attr('transform', function (d) { - d.outerRadius = d.outerRadius * 2.1; - d.innerRadius = d.outerRadius * 2.1; - return 'translate(' + arc.centroid(d) + ')'; - }) - .text(function (d) { - return d.element; - }) - } + .text(function (d) { return d.element; }) } } From 19c3936c51f42b200524eb94bd8cc2e4432827e4 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sat, 30 Aug 2014 18:45:57 -0700 Subject: [PATCH 05/29] show the treatment circles at first load instead of waiting on the trnasistion --- static/js/client.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/static/js/client.js b/static/js/client.js index f7f15c3f60f..33ab33de0db 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -408,6 +408,12 @@ treatCircles.enter().append('rect') .attr('x', function (d) { return xScale(d.created_at); }) .attr('y', function (d) { return yScale(500); }) + .attr("width", 15) + .attr("height", 15) + .attr("rx", 6) + .attr("ry", 6) + .attr('stroke-width', 2) + .attr('stroke', function (d) { return "white"; }) .attr('fill', function (d) { return "grey"; }) .on("mouseover", function (d) { div.transition() From 8580b29ff3dbab1b4730320dc54fd5bb212dcf95 Mon Sep 17 00:00:00 2001 From: Paul Crowder Date: Fri, 29 Aug 2014 20:37:06 -0400 Subject: [PATCH 06/29] cherry-picked @paulisme's pushover integration and hooked it up for adding treatments; removed the push for high/low since it would have only worked if using the API, will discuss adding it to the websockets.emitAlarm with others --- env.js | 6 ++++++ lib/pushover.js | 19 +++++++++++++++++++ lib/treatments.js | 26 +++++++++++++++++++++++++- package.json | 1 + server.js | 3 ++- 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 lib/pushover.js diff --git a/env.js b/env.js index 8532a20fa6f..7e001aad35f 100644 --- a/env.js +++ b/env.js @@ -42,6 +42,11 @@ function config ( ) { shasum.update(process.env.API_SECRET); env.api_secret = shasum.digest('hex'); } + + // For pushing notifications to Pushover. + env.pushover_api_token = process.env.PUSHOVER_API_TOKEN; + env.pushover_user_key = process.env.PUSHOVER_USER_KEY || process.env.PUSHOVER_GROUP_KEY; + // TODO: clean up a bit // Some people prefer to use a json configuration file instead. // This allows a provided json config to override environment variables @@ -54,6 +59,7 @@ function config ( ) { env.settings_collection = DB_SETTINGS_COLLECTION; var STATIC_FILES = __dirname + '/static/'; env.static_files = process.env.NIGHTSCOUT_STATIC_FILES || STATIC_FILES; + return env; } module.exports = config; diff --git a/lib/pushover.js b/lib/pushover.js new file mode 100644 index 00000000000..ce30c40cb26 --- /dev/null +++ b/lib/pushover.js @@ -0,0 +1,19 @@ +'use strict'; + +var Pushover = require('pushover-notifications'); + +function init(env) { + if (env.pushover_api_token && env.pushover_user_key) { + return new Pushover({ + token: env.pushover_api_token, + user: env.pushover_user_key, + onerror: function (err) { + console.log(err); + } + }); + } else { + return null; + } +} + +module.exports = init; \ No newline at end of file diff --git a/lib/treatments.js b/lib/treatments.js index c7ba0e101f9..4b1ac96d46b 100644 --- a/lib/treatments.js +++ b/lib/treatments.js @@ -1,11 +1,35 @@ 'use strict'; -function configure (collection, storage) { +function configure (collection, storage, pushover) { function create (obj, fn) { obj.created_at = (new Date( )).toISOString( ); api( ).insert(obj, function (err, doc) { fn(null, doc); + + if (pushover) { + + var text = (obj.glucose ? 'Blood glucose: ' + obj.glucose + ' (' + obj.glucoseType + ')' : '') + + (obj.carbs ? '\nCarbs: ' + obj.carbs : '') + + (obj.insulin ? '\nInsulin: ' + obj.insulin : '')+ + (obj.enteredBy ? '\nEntered By: ' + obj.enteredBy : '') + + (obj.notes ? '\nNotes: ' + obj.notes : ''); + + var msg = { + expire: 14400, // 4 hours + message: text, + title: obj.eventType, + sound: 'persistent', + timestamp: new Date( ), + priority: 1, + retry: 30 + }; + + pushover.send( msg, function( err, result ) { + console.log(result); + }); + } + }); } diff --git a/package.json b/package.json index bb4510546c6..96f304011c2 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "express-extension-to-accept": "0.0.2", "mongodb": "^1.4.7", "moment": "2.8.1", + "pushover-notifications": "0.2.0", "sgvdata": "0.0.2", "socket.io": "^0.9.17" }, diff --git a/server.js b/server.js index 72b8dcf1bbc..d92816894a2 100644 --- a/server.js +++ b/server.js @@ -26,12 +26,13 @@ var store = require('./lib/storage')(env); var express = require('express'); +var pushover = require('./lib/pushover')(env); /////////////////////////////////////////////////// // api and json object variables /////////////////////////////////////////////////// var entries = require('./lib/entries')(env.mongo_collection, store); var settings = require('./lib/settings')(env.settings_collection, store); -var treatments = require('./lib/treatments')(env.treatments_collection, store); +var treatments = require('./lib/treatments')(env.treatments_collection, store, pushover); var api = require('./lib/api/')(env, entries, settings, treatments); var pebble = require('./lib/pebble'); /////////////////////////////////////////////////// From 54d339d47745f79d429377ce845a13a0dfa76525 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sat, 30 Aug 2014 23:11:40 -0700 Subject: [PATCH 07/29] better treatment notification sound --- lib/treatments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/treatments.js b/lib/treatments.js index 4b1ac96d46b..8bfe8d45b0f 100644 --- a/lib/treatments.js +++ b/lib/treatments.js @@ -19,7 +19,7 @@ function configure (collection, storage, pushover) { expire: 14400, // 4 hours message: text, title: obj.eventType, - sound: 'persistent', + sound: 'gamelan', timestamp: new Date( ), priority: 1, retry: 30 From 0404905e536d84248776866a74538cd7c538a3b8 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 31 Aug 2014 00:04:11 -0700 Subject: [PATCH 08/29] Revert "Revert to c1b2988eb0c548e4b01a93759fcfd65614514309 (Merge branch 'hotfix/0.3.5')" This reverts commit 1cc9c6127cb5e14ac72356e6814c078f04d8db7b. --- .gitignore | 5 +- Procfile | 1 + README.md | 2 + app.json | 13 + bin/post-treatment.sh | 11 + bower.json | 2 + env.js | 5 +- lib/api/index.js | 3 +- lib/api/treatments/index.js | 47 +++ lib/entries.js | 12 +- lib/treatments.js | 24 ++ lib/websocket.js | 50 ++- package.json | 1 + server.js | 9 +- static/css/drawer.css | 29 +- static/css/main.css | 2 +- static/glyphs/config.json | 6 + static/glyphs/css/fontello-codes.css | 3 +- static/glyphs/css/fontello-embedded.css | 15 +- static/glyphs/css/fontello-ie7-codes.css | 3 +- static/glyphs/css/fontello-ie7.css | 3 +- static/glyphs/css/fontello.css | 15 +- static/glyphs/demo.html | 1 + static/glyphs/font/fontello.eot | Bin 7140 -> 7240 bytes static/glyphs/font/fontello.svg | 1 + static/glyphs/font/fontello.ttf | Bin 6972 -> 7072 bytes static/glyphs/font/fontello.woff | Bin 4052 -> 4120 bytes static/images/large.png | Bin 0 -> 10952 bytes static/images/logomobile.png | Bin 0 -> 26537 bytes static/index.html | 61 +++- static/js/client.js | 375 +++++++++++++++-------- static/js/ui-utils.js | 124 +++++++- static/treatments.html | 98 ++++++ testing/convert-treatments.js | 16 + testing/populate.js | 125 ++++++++ 35 files changed, 882 insertions(+), 180 deletions(-) create mode 100644 Procfile create mode 100644 app.json create mode 100755 bin/post-treatment.sh create mode 100644 lib/api/treatments/index.js create mode 100644 lib/treatments.js create mode 100755 static/images/large.png create mode 100644 static/images/logomobile.png create mode 100644 static/treatments.html create mode 100644 testing/convert-treatments.js create mode 100644 testing/populate.js diff --git a/.gitignore b/.gitignore index 1f5dadc574a..2b70a086095 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,8 @@ my.env *.env static/bower_components/ .*.sw? +.DS_Store -.vagrant \ No newline at end of file +.vagrant +/iisnode +/web.config \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 00000000000..489b2700aca --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node server.js diff --git a/README.md b/README.md index e2e312c1b97..4941a409748 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ cgm-remote-monitor (a.k.a. NightScout) [![Dependency Status](https://david-dm.org/nightscout/cgm-remote-monitor.png)](https://david-dm.org/nightscout/cgm-remote-monitor) [![Gitter chat](https://badges.gitter.im/nightscout.png)](https://gitter.im/nightscout/public) +[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) + This acts as a web-based CGM (Continuous Glucose Montinor) to allow multiple caregivers to remotely view a patients glucose data in realtime. The server reads a MongoDB which is intended to be data diff --git a/app.json b/app.json new file mode 100644 index 00000000000..11b3f0cfa05 --- /dev/null +++ b/app.json @@ -0,0 +1,13 @@ +{ + "name": "CGM Remote Monitor", + "repository": "https://github.com/nightscout/cgm-remote-monitor", + "env": { + "MONGO_COLLECTION": { + "description": "The mongo collection to connect to.", + "value": "nightscout" + } + }, + "addons": [ + "mongolab" + ] +} diff --git a/bin/post-treatment.sh b/bin/post-treatment.sh new file mode 100755 index 00000000000..f67c90f5da1 --- /dev/null +++ b/bin/post-treatment.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +curl -H "Content-Type: application/json" -XPOST 'http://localhost:1337/api/v1/treatments/' -d '{ + "enteredBy": "Dad", + "eventType":"Site Change", + "glucoseValue": 322, + "glucoseType": "sensor", + "carbsGiven": 0, + "insulinGiven": 1.25, + "notes": "Argh..." +}' diff --git a/bower.json b/bower.json index 11cb3a30cde..6dbd1b93304 100644 --- a/bower.json +++ b/bower.json @@ -2,6 +2,8 @@ "name": "nightscout", "version": "0.3.5", "dependencies": { + "angularjs": "1.3.0-beta.19", + "bootstrap": "~3.2.0", "d3": "3.4.3", "jquery": "2.1.0", "jQuery-Storage-API": "~1.7.2", diff --git a/env.js b/env.js index 1ec8ab1d334..8532a20fa6f 100644 --- a/env.js +++ b/env.js @@ -23,9 +23,10 @@ function config ( ) { env.name = software.name; env.DISPLAY_UNITS = process.env.DISPLAY_UNITS || 'mg/dl'; env.PORT = process.env.PORT || 1337; - env.mongo = process.env.MONGO_CONNECTION || process.env.CUSTOMCONNSTR_mongo; - env.mongo_collection = process.env.CUSTOMCONNSTR_mongo_collection || 'entries'; + env.mongo = process.env.MONGO_CONNECTION || process.env.CUSTOMCONNSTR_mongo || process.env.MONGOLAB_URI; + env.mongo_collection = process.env.CUSTOMCONNSTR_mongo_collection || process.env.MONGO_COLLECTION || 'entries'; env.settings_collection = process.env.CUSTOMCONNSTR_mongo_settings_collection || 'settings'; + env.treatments_collection = process.env.CUSTOMCONNSTR_mongo_treatments_collection || 'treatments'; var shasum = crypto.createHash('sha1'); var useSecret = (process.env.API_SECRET && process.env.API_SECRET.length > 0); env.api_secret = null; diff --git a/lib/api/index.js b/lib/api/index.js index 7fce2bbe2a9..323eff38463 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -1,6 +1,6 @@ 'use strict'; -function create (env, entries, settings) { +function create (env, entries, settings, treatments) { var express = require('express'), app = express( ) ; @@ -29,6 +29,7 @@ function create (env, entries, settings) { // Entries and settings app.use('/', require('./entries/')(app, wares, entries)); app.use('/', require('./settings/')(app, wares, settings)); + app.use('/', require('./treatments/')(app, wares, treatments)); // Status app.use('/', require('./status')(app, wares)); diff --git a/lib/api/treatments/index.js b/lib/api/treatments/index.js new file mode 100644 index 00000000000..9dde12f9f33 --- /dev/null +++ b/lib/api/treatments/index.js @@ -0,0 +1,47 @@ +'use strict'; + +var consts = require('../../constants'); + +function configure (app, wares, treatments) { + var express = require('express'), + api = express.Router( ); + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw( )); + // json body types get handled as parsed json + api.use(wares.bodyParser.json( )); + // also support url-encoded content-type + api.use(wares.bodyParser.urlencoded({ extended: true })); + + // List settings available + api.get('/treatments/', function(req, res) { + treatments.list(function (err, profiles) { + return res.json(profiles); + }); + }); + + function config_authed (app, api, wares, treatments) { + + api.post('/treatments/', /*TODO: auth disabled for quick UI testing... wares.verifyAuthorization, */ function(req, res) { + var treatment = req.body; + treatments.create(treatment, function (err, created) { + if (err) + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + else + res.json(created); + }); + }); + + } + + if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/) { + config_authed(app, api, wares, treatments); + } + + return api; +} + +module.exports = configure; + diff --git a/lib/entries.js b/lib/entries.js index 4fa5ece23ba..3039d1628da 100644 --- a/lib/entries.js +++ b/lib/entries.js @@ -92,10 +92,16 @@ function entries (name, storage) { with_collection(function(err, collection) { if (err) { fn(err); return; } // potentially a batch insert - collection.insert(docs, function (err, created) { - // execute the callback - fn(err, created, docs); + var firstErr = null, + totalCreated = 0; + + docs.forEach(function(doc) { + collection.update(doc, doc, {upsert: true}, function (err, created) { + firstErr = firstErr || err; + totalCreated += created; + }); }); + fn(firstErr, totalCreated, docs); }); } diff --git a/lib/treatments.js b/lib/treatments.js new file mode 100644 index 00000000000..c7ba0e101f9 --- /dev/null +++ b/lib/treatments.js @@ -0,0 +1,24 @@ +'use strict'; + +function configure (collection, storage) { + + function create (obj, fn) { + obj.created_at = (new Date( )).toISOString( ); + api( ).insert(obj, function (err, doc) { + fn(null, doc); + }); + } + + function list (fn) { + return api( ).find({ }).sort({created_at: -1}).toArray(fn); + } + + function api ( ) { + return storage.pool.db.collection(collection); + } + + api.list = list; + api.create = create; + return api; +} +module.exports = configure; diff --git a/lib/websocket.js b/lib/websocket.js index 9ac00c2d7e8..d034bd3ec47 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -1,5 +1,5 @@ -function websocket (env, server, entries) { +function websocket (env, server, entries, treatments) { "use strict"; // CONSTANTS var ONE_HOUR = 3600000, @@ -24,8 +24,10 @@ var dir2Char = { var io; var watchers = 0; var now = new Date().getTime(); - var cgmData = []; - var patientData = []; + + var cgmData = [], + treatmentData = [], + patientData = []; function start ( ) { io = require('socket.io').listen(server); @@ -101,6 +103,7 @@ function update() { now = Date.now(); cgmData = []; + treatmentData = []; var earliest_data = now - TWO_DAYS; var q = { find: {"date": {"$gte": earliest_data}} }; entries.list(q, function (err, results) { @@ -114,8 +117,15 @@ function update() { cgmData.push(obj); } }); - // all done, do loadData - loadData( ); + treatments.list(function (err, results) { + treatmentData = results.map(function(treatment) { + var timestamp = new Date(treatment.timestamp || treatment.created_at); + treatment.x = timestamp.getTime(); + return treatment; + }); + // all done, do loadData + loadData( ); + }); }); return update; @@ -133,16 +143,21 @@ function emitAlarm(alarmType) { function loadData() { console.log('running loadData'); - var treatment = []; var mbg = []; - var actual = []; + var actual = [], + actualCurrent, + treatment = [], + errorCode; + if (cgmData) { actual = cgmData.slice(); actual.sort(function(a, b) { return a.x - b.x; }); + actualCurrent = actual.length > 0 ? actual[actual.length - 1].y : null; + // sgv less than or equal to 10 means error code // or warm up period code, so ignore actual = actual.filter(function (a) { @@ -150,6 +165,15 @@ function loadData() { }) } + if (treatmentData) { + treatment = treatmentData.slice(); + treatment.sort(function(a, b) { + return a.x - b.x; + }); + } + + if (actualCurrent && actualCurrent < 39) errorCode = actualCurrent; + var actualLength = actual.length - 1; if (actualLength > 1) { @@ -181,8 +205,8 @@ function loadData() { //TODO: need to consider when data being sent has less than the 2 day minimum // consolidate and send the data to the client - var shouldEmit = is_different(actual, predicted, mbg, treatment); - patientData = [actual, predicted, mbg, treatment]; + var shouldEmit = is_different(actual, predicted, mbg, treatment, errorCode); + patientData = [actual, predicted, mbg, treatment, errorCode]; console.log('patientData', patientData.length); if (shouldEmit) { emitData( ); @@ -195,16 +219,16 @@ function loadData() { avgLoss += 1 / size * Math.pow(log10(predicted[j].y / 120), 2); } - //console.log(alarms['urgent_alarm'].threshold); - //console.log(alarms['alarm'].threshold); if (avgLoss > alarms['urgent_alarm'].threshold) { emitAlarm('urgent_alarm'); } else if (avgLoss > alarms['alarm'].threshold) { emitAlarm('alarm'); + } else if (errorCode) { + emitAlarm('urgent_alarm'); } } } - function is_different (actual, predicted, mbg, treatment) { + function is_different (actual, predicted, mbg, treatment, errorCode) { if (patientData && patientData.length < 3) { return true; } @@ -213,12 +237,14 @@ function loadData() { , predicted: patientData[1].slice(-1).pop( ) , mbg: patientData[2].slice(-1).pop( ) , treatment: patientData[3].slice(-1).pop( ) + , errorCode: patientData.length >= 5 ? patientData[4] : 0 }; var last = { actual: actual.slice(-1).pop( ) , predicted: predicted.slice(-1).pop( ) , mbg: mbg.slice(-1).pop( ) , treatment: treatment.slice(-1).pop( ) + , errorCode: errorCode }; // textual diff of objects diff --git a/package.json b/package.json index f66930518bf..bb4510546c6 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "express": "^4.6.1", "express-extension-to-accept": "0.0.2", "mongodb": "^1.4.7", + "moment": "2.8.1", "sgvdata": "0.0.2", "socket.io": "^0.9.17" }, diff --git a/server.js b/server.js index 8d8e460fece..72b8dcf1bbc 100644 --- a/server.js +++ b/server.js @@ -31,7 +31,8 @@ var express = require('express'); /////////////////////////////////////////////////// var entries = require('./lib/entries')(env.mongo_collection, store); var settings = require('./lib/settings')(env.settings_collection, store); -var api = require('./lib/api/')(env, entries, settings); +var treatments = require('./lib/treatments')(env.treatments_collection, store); +var api = require('./lib/api/')(env, entries, settings, treatments); var pebble = require('./lib/pebble'); /////////////////////////////////////////////////// @@ -39,7 +40,6 @@ var pebble = require('./lib/pebble'); // setup http server /////////////////////////////////////////////////// var PORT = env.PORT; -var THIRTY_DAYS = 2592000; var app = express(); var appInfo = software.name + ' ' + software.version; @@ -57,7 +57,8 @@ app.get('/pebble', pebble(entries)); //app.get('/package.json', software); // define static server -var staticFiles = express.static(env.static_files, {maxAge: THIRTY_DAYS * 1000}); +//TODO: JC - changed cache to 1 hour from 30d ays to bypass cache hell until we have a real solution +var staticFiles = express.static(env.static_files, {maxAge: 60 * 60 * 1000}); // serve the static content app.use(staticFiles); @@ -76,7 +77,7 @@ store(function ready ( ) { // setup socket io for data and message transmission /////////////////////////////////////////////////// var websocket = require('./lib/websocket'); - var io = websocket(env, server, entries); + var io = websocket(env, server, entries, treatments); }); /////////////////////////////////////////////////// diff --git a/static/css/drawer.css b/static/css/drawer.css index c52cd0e3202..40a1fc8ca04 100644 --- a/static/css/drawer.css +++ b/static/css/drawer.css @@ -17,6 +17,31 @@ #drawer i { opacity: 0.6; } + +#treatmentDrawer { + background-color: #666; + border-left: 1px solid #999; + box-shadow: inset 4px 4px 5px 0px rgba(50, 50, 50, 0.75); + color: #eee; + display: none; + font-size: 16px; + height: calc(100% - 45px); + overflow-y: auto; + position: absolute; + margin-top: 45px; + right: -200px; + width: 300px; + top: 0; + z-index: 1; +} +#treatmentDrawer i { + opacity: 0.6; +} + +#treatmentDrawer a { + color: white; +} + #about { margin-top: 1em; } @@ -98,7 +123,7 @@ h1, legend, padding: 0; float: right; height: 44px; - width: 180px; + width: 190px; opacity: 0.75; vertical-align: middle; } @@ -118,7 +143,7 @@ h1, legend, #buttonbar a { float: left; text-decoration: none; - width: 44px; + width: 34px; } #buttonbar i { padding-left: 12px; diff --git a/static/css/main.css b/static/css/main.css index 24640817ae2..e85fd2292c5 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -136,7 +136,7 @@ body { #bgButton, #silenceBtn { - z-index: 999; + z-index: 99; } #bgButton { diff --git a/static/glyphs/config.json b/static/glyphs/config.json index 149750f077b..ebfdc8a85a4 100644 --- a/static/glyphs/config.json +++ b/static/glyphs/config.json @@ -60,6 +60,12 @@ "code": 59392, "src": "mfglabs" }, + { + "uid": "55e2ff85b1c459c383f46da6e96014b0", + "css": "plus", + "code": 59403, + "src": "elusive" + }, { "uid": "b59b3b618699b467541f631edd5a02ed", "css": "volume", diff --git a/static/glyphs/css/fontello-codes.css b/static/glyphs/css/fontello-codes.css index dcbfd4a0d0c..d38f9fe8e1e 100644 --- a/static/glyphs/css/fontello-codes.css +++ b/static/glyphs/css/fontello-codes.css @@ -9,4 +9,5 @@ .icon-battery-75:before { content: '\e807'; } /* '' */ .icon-battery-100:before { content: '\e808'; } /* '' */ .icon-cancel-circled:before { content: '\e809'; } /* '' */ -.icon-volume:before { content: '\e80a'; } /* '' */ \ No newline at end of file +.icon-volume:before { content: '\e80a'; } /* '' */ +.icon-plus:before { content: '\e80b'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/css/fontello-embedded.css b/static/glyphs/css/fontello-embedded.css index be4d85ca15a..c280a635980 100644 --- a/static/glyphs/css/fontello-embedded.css +++ b/static/glyphs/css/fontello-embedded.css @@ -1,15 +1,15 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?75577666'); - src: url('../font/fontello.eot?75577666#iefix') format('embedded-opentype'), - url('../font/fontello.svg?75577666#fontello') format('svg'); + src: url('../font/fontello.eot?87362083'); + src: url('../font/fontello.eot?87362083#iefix') format('embedded-opentype'), + url('../font/fontello.svg?87362083#fontello') format('svg'); font-weight: normal; font-style: normal; } @font-face { font-family: 'fontello'; - src: url('data:application/octet-stream;base64,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') format('woff'), - url('data:application/octet-stream;base64,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') format('truetype'); + src: url('data:application/octet-stream;base64,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') format('woff'), + url('data:application/octet-stream;base64,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sAAWsAQlsAQlIC5HI0cjYSCwBCNCsAZFKyCwYFBYILBAUVizAiADIBuzAiYDGllCQiMgR7AEQ7CAYmAgsAArIIqKYSCwAkNgZCOwA0NhZFBYsAJDYRuwA0NgWbADJbCAYmGwAiVGYTgjIDwjOBshICBGI0ewACsjYTghWbEmARQrLbA9LLAwKy6xJgEUKy2wPiywMSshIyAgPLAEI0IjOLEmARQrsARDLrAmKy2wPyywABUgR7AAI0KyAAEBFRQTLrAsKi2wQCywABUgR7AAI0KyAAEBFRQTLrAsKi2wQSyxAAEUE7AtKi2wQiywLyotsEMssAAWRSMgLiBGiiNhOLEmARQrLbBELLAII0KwQystsEUssgAAPCstsEYssgABPCstsEcssgEAPCstsEgssgEBPCstsEkssgAAPSstsEossgABPSstsEsssgEAPSstsEwssgEBPSstsE0ssgAAOSstsE4ssgABOSstsE8ssgEAOSstsFAssgEBOSstsFEssgAAOystsFIssgABOystsFMssgEAOystsFQssgEBOystsFUssgAAPistsFYssgABPistsFcssgEAPistsFgssgEBPistsFkssgAAOistsFossgABOistsFsssgEAOistsFwssgEBOistsF0ssDIrLrEmARQrLbBeLLAyK7A2Ky2wXyywMiuwNystsGAssAAWsDIrsDgrLbBhLLAzKy6xJgEUKy2wYiywMyuwNistsGMssDMrsDcrLbBkLLAzK7A4Ky2wZSywNCsusSYBFCstsGYssDQrsDYrLbBnLLA0K7A3Ky2waCywNCuwOCstsGkssDUrLrEmARQrLbBqLLA1K7A2Ky2wayywNSuwNystsGwssDUrsDgrLbBtLCuwCGWwAyRQeLABFTAtAAAAS7gAyFJYsQEBjlm5CAAIAGMgsAEjRLADI3CyBCgJRVJEsgoCByqxBgFEsSQBiFFYsECIWLEGA0SxJgGIUVi4BACIWLEGAURZWVlZuAH/hbAEjbEFAEQAAA==') format('truetype'); } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ @@ -17,7 +17,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?75577666#fontello') format('svg'); + src: url('../font/fontello.svg?87362083#fontello') format('svg'); } } */ @@ -62,4 +62,5 @@ .icon-battery-75:before { content: '\e807'; } /* '' */ .icon-battery-100:before { content: '\e808'; } /* '' */ .icon-cancel-circled:before { content: '\e809'; } /* '' */ -.icon-volume:before { content: '\e80a'; } /* '' */ \ No newline at end of file +.icon-volume:before { content: '\e80a'; } /* '' */ +.icon-plus:before { content: '\e80b'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/css/fontello-ie7-codes.css b/static/glyphs/css/fontello-ie7-codes.css index e18146c1ff6..c50afefb973 100644 --- a/static/glyphs/css/fontello-ie7-codes.css +++ b/static/glyphs/css/fontello-ie7-codes.css @@ -9,4 +9,5 @@ .icon-battery-75 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-battery-100 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file +.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file diff --git a/static/glyphs/css/fontello-ie7.css b/static/glyphs/css/fontello-ie7.css index b496691191e..79ca9ab5894 100644 --- a/static/glyphs/css/fontello-ie7.css +++ b/static/glyphs/css/fontello-ie7.css @@ -20,4 +20,5 @@ .icon-battery-75 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-battery-100 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file +.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file diff --git a/static/glyphs/css/fontello.css b/static/glyphs/css/fontello.css index 95edcbaa85d..8920cc801a7 100644 --- a/static/glyphs/css/fontello.css +++ b/static/glyphs/css/fontello.css @@ -1,10 +1,10 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?77167544'); - src: url('../font/fontello.eot?77167544#iefix') format('embedded-opentype'), - url('../font/fontello.woff?77167544') format('woff'), - url('../font/fontello.ttf?77167544') format('truetype'), - url('../font/fontello.svg?77167544#fontello') format('svg'); + src: url('../font/fontello.eot?48374311'); + src: url('../font/fontello.eot?48374311#iefix') format('embedded-opentype'), + url('../font/fontello.woff?48374311') format('woff'), + url('../font/fontello.ttf?48374311') format('truetype'), + url('../font/fontello.svg?48374311#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -14,7 +14,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?77167544#fontello') format('svg'); + src: url('../font/fontello.svg?48374311#fontello') format('svg'); } } */ @@ -60,4 +60,5 @@ .icon-battery-75:before { content: '\e807'; } /* '' */ .icon-battery-100:before { content: '\e808'; } /* '' */ .icon-cancel-circled:before { content: '\e809'; } /* '' */ -.icon-volume:before { content: '\e80a'; } /* '' */ \ No newline at end of file +.icon-volume:before { content: '\e80a'; } /* '' */ +.icon-plus:before { content: '\e80b'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/demo.html b/static/glyphs/demo.html index d59efc91be3..cd689710d4a 100644 --- a/static/glyphs/demo.html +++ b/static/glyphs/demo.html @@ -270,6 +270,7 @@

icon-battery-1000xe808
icon-cancel-circled0xe809
icon-volume0xe80a
+
icon-plus0xe80b
diff --git a/static/glyphs/font/fontello.eot b/static/glyphs/font/fontello.eot index 1d70fcab91223f70194591867a4b02dce8d3496e..25039a0de45c4c8a0b9d4f375828c4bb0a09e27c 100644 GIT binary patch delta 463 zcmX|-%P#{_6voe;J2SLn$|PM-B^6>L@rq}NM_7o&W)@9YkV(~4b(m;L#llvs)GV|T z3E~fsl-OBWSP-?ctwf{?v18fsk#KKL&i#E~&bjxV*Z#W!X(~u`l9BpF`6|Af8Qb3J zCZcylIAR%gAy{`!#3m8bkPLEuo`YCXj7|%LcHtJEh z?#5}3>rRmkG9@1nR+Sw$&P!Y;g<8j&1xB(YaFO^dG3K2hQB*_>0j{F`tO|cyuh#Ux znaKjoEa%E^Pk)DUj+vMmY7o2eD|w-M_>&Y~fTdW*I;M_UX*A3=F&?K)y;wZb^mK`8Xhxf!6{^806$9Ckj62 z11jdt0P>}B6DtZBg}LN_{1zZzAulmE^~_u;86bZN(2}zS`Nbt)@1EZVbVCVHetSVt zYQdz1v;G6MJ^}K>7#Nu+PM*PN&&kCc^goLsiaBud14c_muFX()IT^Vaxq%wC2v$!v6;fag1R4{#IZEgqCjg+#ULpVh diff --git a/static/glyphs/font/fontello.svg b/static/glyphs/font/fontello.svg index ed019eabf63..177af8dc21d 100644 --- a/static/glyphs/font/fontello.svg +++ b/static/glyphs/font/fontello.svg @@ -17,6 +17,7 @@ + \ No newline at end of file diff --git a/static/glyphs/font/fontello.ttf b/static/glyphs/font/fontello.ttf index 3b68bf5eadeb581a75d9bc46fb0207a3f43bf176..fb8152f39f6e4db8d5621f32a0daf767cacfdc84 100644 GIT binary patch delta 522 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 Am;e9( diff --git a/static/images/large.png b/static/images/large.png new file mode 100755 index 0000000000000000000000000000000000000000..6e7f04cc596622cc22d3827e9506fe4c10a0dd10 GIT binary patch literal 10952 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 R?+R6bw77y;g@|Fm{{X%XvMT@p literal 0 HcmV?d00001 diff --git a/static/images/logomobile.png b/static/images/logomobile.png new file mode 100644 index 0000000000000000000000000000000000000000..b011d9faf24d744fc366a4be72a3196ce1a46e8e GIT binary patch literal 26537 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�hBc% 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 literal 0 HcmV?d00001 diff --git a/static/index.html b/static/index.html index 816d4b0bcab..ce43379664f 100644 --- a/static/index.html +++ b/static/index.html @@ -1,7 +1,9 @@ - + + + NightScout @@ -16,6 +18,7 @@ +

Nightscout

@@ -68,6 +71,11 @@

Nightscout


+
+
Enable Alarms
+
+
+
Night Mode
@@ -76,6 +84,11 @@

Nightscout

Custom Title
+
+
Theme
+

+
+
@@ -102,6 +115,52 @@

Nightscout

+
+
+
+ Log a Treatment +
+
Entered By:
+
+
+ + + +
+ Glucose Reading + + + + +
+ + +
+ + +
+ + +
+ +
+ View all treatments +
+
+
diff --git a/static/js/client.js b/static/js/client.js index 10ad7b3758e..94072b49af9 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -1,8 +1,8 @@ (function () { "use strict"; - var retrospectivePredictor = true, - latestSGV, + var latestSGV, + errorCode, treatments, padding = { top: 20, right: 10, bottom: 30, left: 10 }, opacity = {current: 1, DAY: 1, NIGHT: 0.5}, @@ -23,7 +23,9 @@ clip, TWENTY_FIVE_MINS_IN_MS = 1500000, THIRTY_MINS_IN_MS = 1800000, + FORTY_MINS_IN_MS = 2400000, FORTY_TWO_MINS_IN_MS = 2520000, + SIXTY_MINS_IN_MS = 3600000, FOCUS_DATA_RANGE_MS = 12600000, // 3.5 hours of actual data FORMAT_TIME = '%I:%M%', //alternate format '%H:%M' audio = document.getElementById('audio'), @@ -41,6 +43,14 @@ var tickValues = [2.0, 3.0, 4.0, 6.0, 10.0, 15.0, 22.0]; } + //TODO: get these from the config + var targetTop = 180, + targetBottom = 80; + + var futureOpacity = d3.scale.linear( ) + .domain([TWENTY_FIVE_MINS_IN_MS, SIXTY_MINS_IN_MS]) + .range([0.8, 0.1]); + // create svg and g to contain the chart contents var charts = d3.select('#chartContainer').append('svg') .append('g') @@ -135,7 +145,7 @@ // get the desired opacity for context chart based on the brush extent function highlightBrushPoints(data) { if (data.date.getTime() >= brush.extent()[0].getTime() && data.date.getTime() <= brush.extent()[1].getTime()) { - return 1; + return futureOpacity(data.date - latestSGV.x); } else { return 0.5; } @@ -211,15 +221,15 @@ } var element = document.getElementById('bgButton').hidden == ''; - var nowDate = new Date(brushExtent[1] - THIRTY_MINS_IN_MS); // predict for retrospective data - if (retrospectivePredictor && brushExtent[1].getTime() - THIRTY_MINS_IN_MS < now && element != true) { + if (brushExtent[1].getTime() - THIRTY_MINS_IN_MS < now && element != true) { // filter data for -12 and +5 minutes from reference time for retrospective focus data prediction var nowData = data.filter(function(d) { return d.date.getTime() >= brushExtent[1].getTime() - FORTY_TWO_MINS_IN_MS && - d.date.getTime() <= brushExtent[1].getTime() - TWENTY_FIVE_MINS_IN_MS + d.date.getTime() <= brushExtent[1].getTime() - TWENTY_FIVE_MINS_IN_MS && + d.color != 'none'; }); if (nowData.length > 1) { var prediction = predictAR(nowData); @@ -238,18 +248,69 @@ $('#currentTime') .text(formatTime(new Date(brushExtent[1] - THIRTY_MINS_IN_MS))) .css('text-decoration','line-through'); - } else if (retrospectivePredictor) { + + $('#lastEntry').text("RETRO").removeClass('current'); + + $('.container #noButton .currentBG').css({color: 'grey'}); + $('.container #noButton .currentDirection').css({color: 'grey'}); + + } else { // if the brush comes back into the current time range then it should reset to the current time and sg + var nowData = data.filter(function(d) { + return d.color != 'none'; + }); + nowData = [nowData[nowData.length - 2], nowData[nowData.length - 1]]; + var prediction = predictAR(nowData); + focusData = focusData.concat(prediction); var dateTime = new Date(now); nowDate = dateTime; $('#currentTime') .text(formatTime(dateTime)) - .css('text-decoration',''); - $('.container .currentBG') - .text(scaleBg(latestSGV.y)) - .css('text-decoration',''); - $('.container .currentDirection') - .html(latestSGV.direction); + .css('text-decoration', ''); + + if (errorCode) { + var errorDisplay; + + switch (parseInt(errorCode)) { + case 0: errorDisplay = '??0'; break; //None + case 1: errorDisplay = '?SN'; break; //SENSOR_NOT_ACTIVE + case 2: errorDisplay = '??2'; break; //MINIMAL_DEVIATION + case 3: errorDisplay = '?NA'; break; //NO_ANTENNA + case 5: errorDisplay = '?NC'; break; //SENSOR_NOT_CALIBRATED + case 6: errorDisplay = '?CD'; break; //COUNTS_DEVIATION + case 7: errorDisplay = '??7'; break; //? + case 8: errorDisplay = '??8'; break; //? + case 9: errorDisplay = '⌛'; break; //ABSOLUTE_DEVIATION + case 10: errorDisplay = '???'; break; //POWER_DEVIATION + case 12: errorDisplay = '?RF'; break; //BAD_RF + default: errorDisplay = '?' + parseInt(errorCode) + '?'; break; + } + + $('#lastEntry').text("CGM ERROR").removeClass('current').addClass("urgent"); + + $('.container .currentBG').html(errorDisplay) + .css('text-decoration', ''); + $('.container .currentDirection').html('✖'); + + var color = sgvToColor(errorCode); + $('.container #noButton .currentBG').css({color: color}); + $('.container #noButton .currentDirection').css({color: color}); + + } else { + + var secsSinceLast = (Date.now() - new Date(latestSGV.x).getTime()) / 1000; + $('#lastEntry').text(timeAgo(secsSinceLast)).toggleClass('current', secsSinceLast < 10 * 60); + + $('.container .currentBG') + .text(scaleBg(latestSGV.y)) + .css('text-decoration', ''); + $('.container .currentDirection') + .html(latestSGV.direction); + + var color = sgvToColor(latestSGV.y); + $('.container #noButton .currentBG').css({color: color}); + $('.container #noButton .currentDirection').css({color: color}); + } } xScale.domain(brush.extent()); @@ -271,6 +332,7 @@ .attr('cx', function (d) { return xScale(d.date); }) .attr('cy', function (d) { return yScale(d.sgv); }) .attr('fill', function (d) { return d.color; }) + .attr('opacity', function (d) { return futureOpacity(d.date - latestSGV.x); }) .attr('r', 3); focusCircles.exit() @@ -556,18 +618,18 @@ .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale2(dataRange[0])) - .attr('y1', yScale2(scaleBg(180))) + .attr('y1', yScale2(scaleBg(targetTop))) .attr('x2', xScale2(dataRange[1])) - .attr('y2', yScale2(scaleBg(180))); + .attr('y2', yScale2(scaleBg(targetTop))); // transition low line to correct location context.select('.low-line') .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale2(dataRange[0])) - .attr('y1', yScale2(scaleBg(80))) + .attr('y1', yScale2(scaleBg(targetBottom))) .attr('x2', xScale2(dataRange[1])) - .attr('y2', yScale2(scaleBg(80))); + .attr('y2', yScale2(scaleBg(targetBottom))); } } @@ -653,7 +715,6 @@ socket.on('now', function (d) { now = d; var dateTime = new Date(now); - // lixgbg old: $('#currentTime').text(d3.time.format('%I:%M%p')(dateTime)); $('#currentTime').text(formatTime(dateTime)); // Dim the screen by reducing the opacity when at nighttime @@ -668,40 +729,35 @@ socket.on('sgv', function (d) { if (d.length > 1) { + errorCode = d.length >= 5 ? d[4] : undefined; + // change the next line so that it uses the prediction if the signal gets lost (max 1/2 hr) if (d[0].length) { - var current = d[0][d[0].length - 1]; - latestSGV = current; - var secsSinceLast = (Date.now() - new Date(current.x).getTime()) / 1000; - var currentBG = current.y; - - //TODO: currently these are filtered on the server - //TODO: use icons for these magic values - switch (current.y) { - case 0: currentBG = '??0'; break; //None - case 1: currentBG = '?SN'; break; //SENSOR_NOT_ACTIVE - case 2: currentBG = '??2'; break; //MINIMAL_DEVIATION - case 3: currentBG = '?NA'; break; //NO_ANTENNA - case 5: currentBG = '?NC'; break; //SENSOR_NOT_CALIBRATED - case 6: currentBG = '?CD'; break; //COUNTS_DEVIATION - case 7: currentBG = '??7'; break; //? - case 8: currentBG = '??8'; break; //? - case 9: currentBG = '?AD'; break; //ABSOLUTE_DEVIATION - case 10: currentBG = '?PD'; break; //POWER_DEVIATION - case 12: currentBG = '?RF'; break; //BAD_RF - default: - currentBG = scaleBg(currentBG); - break; - } + latestSGV = d[0][d[0].length - 1]; - $('#lastEntry').text(timeAgo(secsSinceLast)).toggleClass('current', secsSinceLast < 10 * 60); - $('.container .currentBG').text(currentBG); - $('.container .currentDirection').html(current.direction); - $('.container .current').toggleClass('high', current.y > 180).toggleClass('low', current.y < 70) + //TODO: alarmHigh/alarmLow probably shouldn't be here + if (browserSettings.alarmHigh) { + $('.container .current').toggleClass('high', latestSGV.y > 180); + } + if (browserSettings.alarmLow) { + $('.container .current').toggleClass('low', latestSGV.y < 70); + } } - data = d[0].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), direction: obj.direction, color: 'grey'} }); - data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'blue'} })); + data = d[0].map(function (obj) { + return { date: new Date(obj.x), sgv: scaleBg(obj.y), direction: obj.direction, color: sgvToColor(obj.y)} + }); + // TODO: This is a kludge to advance the time as data becomes stale by making old predictor clear (using color = 'none') + // This shouldn't have to be sent and can be fixed by using xScale.domain([x0,x1]) function with + // 2 days before now as x0 and 30 minutes from now for x1 for context plot, but this will be + // required to happen when "now" event is sent from websocket.js every minute. When fixed, + // remove all "color != 'none'" code + data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'none'} })); data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'red'} })); + + data.forEach(function (d) { + if (d.sgv < 39) + d.color = "transparent"; + }) treatments = d[3]; if (!isInitialData) { @@ -713,6 +769,22 @@ } } }); + + function sgvToColor(sgv) { + var color = 'grey'; + + if (browserSettings.theme == "colors") { + if (sgv > targetTop) { + color = 'yellow'; + } else if (sgv >= targetBottom && sgv <= targetTop) { + color = '#4cff00'; + } else if (sgv < targetBottom) { + color = 'red'; + } + } + + return color; + } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -722,16 +794,20 @@ console.log('Client connected to server.') }); socket.on('alarm', function () { - console.log("Alarm raised!"); - currentAlarmType = 'alarm'; - generateAlarm(alarmSound); + if (browserSettings.alarmHigh) { + console.log("Alarm raised!"); + currentAlarmType = 'alarm'; + generateAlarm(alarmSound); + } brushInProgress = false; updateChart(false); }); socket.on('urgent_alarm', function () { - console.log("Urgent alarm raised!"); - currentAlarmType = 'urgent_alarm'; - generateAlarm(urgentAlarmSound); + if (browserSettings.alarmLow) { + console.log("Urgent alarm raised!"); + currentAlarmType = 'urgent_alarm'; + generateAlarm(urgentAlarmSound); + } brushInProgress = false; updateChart(false); }); @@ -745,11 +821,11 @@ $('#testAlarms').click(function(event) { d3.select('.audio.alarms audio').each(function (data, i) { - var audio = this; - playAlarm(audio); - setTimeout(function() { - audio.pause(); - }, 4000); + var audio = this; + playAlarm(audio); + setTimeout(function() { + audio.pause(); + }, 4000); }); event.preventDefault(); }); @@ -758,9 +834,9 @@ alarmInProgress = true; var selector = '.audio.alarms audio.' + file; d3.select(selector).each(function (d, i) { - var audio = this; - playAlarm(audio); - $(this).addClass('playing'); + var audio = this; + playAlarm(audio); + $(this).addClass('playing'); }); var element = document.getElementById('bgButton'); element.hidden = ''; @@ -789,9 +865,9 @@ element = document.getElementById('noButton'); element.hidden = ''; d3.select('audio.playing').each(function (d, i) { - var audio = this; - audio.pause(); - $(this).removeClass('playing'); + var audio = this; + audio.pause(); + $(this).removeClass('playing'); }); $(".time").show(); @@ -839,9 +915,9 @@ } if (parts.value) - return parts.value + ' ' + parts.label + ' ago'; + return parts.value + ' ' + parts.label + ' ago'; else - return parts.label; + return parts.label; } @@ -851,64 +927,91 @@ //draw a compact visualization of a treatment (carbs, insulin) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function drawTreatment(treatment, scale, showValues) { - var carbs = treatment.carbs; - var insulin = treatment.insulin; - var CR = treatment.CR; - - var R1 = Math.sqrt(Math.min(carbs, insulin * CR)) / scale, - R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / scale, - R3 = R2 + 8 / scale; - - var arc_data = [ - { 'element': '', 'color': '#9c4333', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': R1 }, - { 'element': '', 'color': '#d4897b', 'start': -1.5708, 'end': 1.5708, 'inner': R1, 'outer': R2 }, - { 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': R2, 'outer': R3 }, - { 'element': '', 'color': '#3d53b7', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': R1 }, - { 'element': '', 'color': '#5d72c9', 'start': 1.5708, 'end': 4.7124, 'inner': R1, 'outer': R2 }, - { 'element': '', 'color': 'transparent', 'start': 1.5708, 'end': 4.7124, 'inner': R2, 'outer': R3 } - ]; - - if (carbs < insulin * CR) arc_data[1].color = 'transparent'; - if (carbs > insulin * CR) arc_data[4].color = 'transparent'; - if (carbs > 0) arc_data[2].element = Math.round(carbs) + ' g'; - if (insulin > 0) arc_data[5].element = Math.round(insulin * 10) / 10 + ' U'; - - var arc = d3.svg.arc() - .innerRadius(function (d) { return 5 * d.inner; }) - .outerRadius(function (d) { return 5 * d.outer; }) - .endAngle(function (d) { return d.start; }) - .startAngle(function (d) { return d.end; }); - - var treatmentDots = focus.selectAll('treatment-dot') - .data(arc_data) - .enter() - .append('g') - .attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(treatment.y)) + ')'); - - var arcs = treatmentDots.append('path') - .attr('class', 'path') - .attr('fill', function (d, i) { return d.color; }) - .attr('id', function (d, i) { return 's' + i; }) - .attr('d', arc); - - - // labels for carbs and insulin - if (showValues) { - var label = treatmentDots.append('g') + + if (!treatment.CR) { + //plot a simple treatment point + console.info("plotting treatment", treatment); + var treatmentDots = focus.selectAll('treatment-dot') + .data(treatment) + .enter() + .append('g') + .attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(300)) + ')'); + + //TODO: some d3 magic to get the treatments to display and show a tooltip + } else { + var carbs = treatment.carbs; + var insulin = treatment.insulin; + var CR = treatment.CR; + + var R1 = Math.sqrt(Math.min(carbs, insulin * CR)) / scale, + R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / scale, + R3 = R2 + 8 / scale; + + var arc_data = [ + { 'element': '', 'color': '#9c4333', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': R1 }, + { 'element': '', 'color': '#d4897b', 'start': -1.5708, 'end': 1.5708, 'inner': R1, 'outer': R2 }, + { 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': R2, 'outer': R3 }, + { 'element': '', 'color': '#3d53b7', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': R1 }, + { 'element': '', 'color': '#5d72c9', 'start': 1.5708, 'end': 4.7124, 'inner': R1, 'outer': R2 }, + { 'element': '', 'color': 'transparent', 'start': 1.5708, 'end': 4.7124, 'inner': R2, 'outer': R3 } + ]; + + if (carbs < insulin * CR) arc_data[1].color = 'transparent'; + if (carbs > insulin * CR) arc_data[4].color = 'transparent'; + if (carbs > 0) arc_data[2].element = Math.round(carbs) + ' g'; + if (insulin > 0) arc_data[5].element = Math.round(insulin * 10) / 10 + ' U'; + + var arc = d3.svg.arc() + .innerRadius(function (d) { + return 5 * d.inner; + }) + .outerRadius(function (d) { + return 5 * d.outer; + }) + .endAngle(function (d) { + return d.start; + }) + .startAngle(function (d) { + return d.end; + }); + + var treatmentDots = focus.selectAll('treatment-dot') + .data(arc_data) + .enter() + .append('g') + .attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(treatment.y)) + ')'); + + var arcs = treatmentDots.append('path') .attr('class', 'path') - .attr('id', 'label') - .style('fill', 'white'); - label.append('text') - .style('font-size', 30 / scale) - .style('font-family', 'Arial') - .attr('text-anchor', 'middle') - .attr('dy', '.35em') - .attr('transform', function (d) { - d.outerRadius = d.outerRadius * 2.1; - d.innerRadius = d.outerRadius * 2.1; - return 'translate(' + arc.centroid(d) + ')'; + .attr('fill', function (d, i) { + return d.color; + }) + .attr('id', function (d, i) { + return 's' + i; }) - .text(function (d) { return d.element; }) + .attr('d', arc); + + + // labels for carbs and insulin + if (showValues) { + var label = treatmentDots.append('g') + .attr('class', 'path') + .attr('id', 'label') + .style('fill', 'white'); + label.append('text') + .style('font-size', 30 / scale) + .style('font-family', 'Arial') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .attr('transform', function (d) { + d.outerRadius = d.outerRadius * 2.1; + d.innerRadius = d.outerRadius * 2.1; + return 'translate(' + arc.centroid(d) + ')'; + }) + .text(function (d) { + return d.element; + }) + } } } @@ -922,6 +1025,8 @@ var BG_REF = 140; var BG_MIN = 36; var BG_MAX = 400; + // these are the one sigma limits for the first 13 prediction interval uncertainties (65 minutes) + var CONE = [0.020, 0.041, 0.061, 0.081, 0.099, 0.116, 0.132, 0.146, 0.159, 0.171, 0.182, 0.192, 0.201]; if (actual.length < 2) { var y = [Math.log(actual[0].sgv / BG_REF), Math.log(actual[0].sgv / BG_REF)]; } else { @@ -932,17 +1037,31 @@ y = [Math.log(actual[0].sgv / BG_REF), Math.log(actual[0].sgv / BG_REF)]; } } - var n = 20; var AR = [-0.723, 1.716]; var dt = actual[1].date.getTime(); - for (var i = 0; i <= n; i++) { + var predictedColor = 'blue'; + if (browserSettings.theme == "colors") { + predictedColor = 'cyan'; + } + for (var i = 0; i < CONE.length; i++) { y = [y[1], AR[0] * y[0] + AR[1] * y[1]]; dt = dt + FIVE_MINUTES; - predicted[i] = { - date: new Date(dt+3000), - sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp(y[1])))), - color: 'blue' + // Add 2000 ms so not same point as SG + predicted[i * 2] = { + date: new Date(dt + 2000), + sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp((y[1] - 2 * CONE[i]))))), + color: predictedColor + }; + // Add 4000 ms so not same point as SG + predicted[i * 2 + 1] = { + date: new Date(dt + 4000), + sgv: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp((y[1] + 2 * CONE[i]))))), + color: predictedColor }; + predicted.forEach(function (d) { + if (d.sgv < BG_MIN) + d.color = "transparent"; + }) } return predicted; } diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js index eedfd9ebf0d..ccee0b3b013 100644 --- a/static/js/ui-utils.js +++ b/static/js/ui-utils.js @@ -1,9 +1,13 @@ var drawerIsOpen = false; +var treatmentDrawerIsOpen = false; var browserStorage = $.localStorage; var defaultSettings = { "units": "mg/dl", - "nightMode": false -} + "alarmHigh": true, + "alarmLow": true, + "nightMode": false, + "theme": "default" +}; var app = {}; $.ajax("/api/v1/status.json", { @@ -26,29 +30,42 @@ $.ajax("/api/v1/status.json", { function getBrowserSettings(storage) { var json = {}; try { - json = { + var json = { "units": storage.get("units"), + "alarmHigh": storage.get("alarmHigh"), + "alarmLow": storage.get("alarmLow"), "nightMode": storage.get("nightMode"), - "customTitle": storage.get("customTitle") + "customTitle": storage.get("customTitle"), + "theme": storage.get("theme") }; // Default browser units to server units if undefined. json.units = setDefault(json.units, serverSettings.units); - //console.log("browserSettings.units: " + json.units); if (json.units == "mmol") { $("#mmol-browser").prop("checked", true); } else { $("#mgdl-browser").prop("checked", true); } + json.alarmHigh = setDefault(json.alarmHigh, defaultSettings.alarmHigh); + $("#alarmhigh-browser").prop("checked", json.alarmHigh); + json.alarmLow = setDefault(json.alarmLow, defaultSettings.alarmLow); + $("#alarmlow-browser").prop("checked", json.alarmLow); + json.nightMode = setDefault(json.nightMode, defaultSettings.nightMode); $("#nightmode-browser").prop("checked", json.nightMode); if (json.customTitle) { - $("h1.customTitle").html(json.customTitle); + $("h1.customTitle").text(json.customTitle); $("input#customTitle").prop("value", json.customTitle); document.title = "Nightscout: " + json.customTitle; } + + if (json.theme == "colors") { + $("#theme-colors-browser").prop("checked", true); + } else { + $("#theme-default-browser").prop("checked", true); + } } catch(err) { showLocalstorageError(); @@ -84,13 +101,24 @@ function jsonIsNotEmpty(json) { } function storeInBrowser(json, storage) { if (json.units) storage.set("units", json.units); + if (json.alarmHigh == true) { + storage.set("alarmHigh", true) + } else { + storage.set("alarmHigh", false) + } + if (json.alarmLow == true) { + storage.set("alarmLow", true) + } else { + storage.set("alarmLow", false) + } if (json.nightMode == true) { storage.set("nightMode", true) } else { storage.set("nightMode", false) } if (json.customTitle) storage.set("customTitle", json.customTitle); - event.preventDefault(); + if (json.theme) storage.set("theme", json.theme); + event.preventDefault(); } function storeOnServer(json) { if (jsonIsNotEmpty(json)) { @@ -132,6 +160,7 @@ function closeDrawer(callback) { }); drawerIsOpen = false; } + function openDrawer() { drawerIsOpen = true; $("#container").animate({marginLeft: "-200px"}, 300); @@ -140,6 +169,30 @@ function openDrawer() { $("#drawer").animate({right: "0"}, 300); } +function closeTreatmentDrawer(callback) { + $("#container").animate({marginLeft: "0px"}, 400, callback); + $("#chartContainer").animate({marginLeft: "0px"}, 400); + $("#treatmentDrawer").animate({right: "-300px"}, 400, function() { + $("#treatmentDrawer").css("display", "none"); + }); + treatmentDrawerIsOpen = false; +} +function openTreatmentDrawer() { + treatmentDrawerIsOpen = true; + $("#container").animate({marginLeft: "-300px"}, 400); + $("#chartContainer").animate({marginLeft: "-300px"}, 400); + $("#treatmentDrawer").css("display", "block"); + $("#treatmentDrawer").animate({right: "0"}, 400); + + $('#enteredBy').val(browserStorage.get("enteredBy") || ''); + $('#eventType').val('BG Check'); + $('#glucoseValue').val(''); + $('#meter').prop('checked', true) + $('#carbsGiven').val(''); + $('#insulinGiven').val(''); + $('#notes').val(''); +} + function closeNotification() { var notify = $("#notification"); @@ -195,6 +248,33 @@ function stretchStatusForToolbar(toolbarState){ } } +function treatmentSubmit(event) { + + var data = new Object(); + data.enteredBy = document.getElementById("enteredBy").value; + data.eventType = document.getElementById("eventType").value; + data.glucose = document.getElementById("glucoseValue").value; + data.glucoseType = $('#treatment-form input[name=glucoseType]:checked').val(); + data.carbs = document.getElementById("carbsGiven").value; + data.insulin = document.getElementById("insulinGiven").value; + data.notes = document.getElementById("notes").value; + + var dataJson = JSON.stringify(data, null, " "); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", "/api/v1/treatments/", true); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.send(dataJson); + + browserStorage.set("enteredBy", data.enteredBy); + + closeTreatmentDrawer(); + + if (event) { + event.preventDefault(); + } +} + var querystring = getQueryParms(); // var serverSettings = getServerSettings(); @@ -222,6 +302,12 @@ Dropdown.prototype.open = function (e) { $("#drawerToggle").click(function(event) { + //close other drawers + if(treatmentDrawerIsOpen) { + closeTreatmentDrawer(); + treatmentDrawerIsOpen = false; + } + if(drawerIsOpen) { closeDrawer(); drawerIsOpen = false; @@ -232,6 +318,25 @@ $("#drawerToggle").click(function(event) { event.preventDefault(); }); +$("#treatmentDrawerToggle").click(function(event) { + //close other drawers + if(drawerIsOpen) { + closeDrawer(); + drawerIsOpen = false; + } + + if(treatmentDrawerIsOpen) { + closeTreatmentDrawer(); + treatmentDrawerIsOpen = false; + } else { + openTreatmentDrawer(); + treatmentDrawerIsOpen = true; + } + event.preventDefault(); +}); + +$("#treatmentDrawer button").click(treatmentSubmit); + $("#notification").click(function(event) { closeNotification(); event.preventDefault(); @@ -255,8 +360,11 @@ $("#showToolbar").find("a").click(function(event) { $("input#save").click(function() { storeInBrowser({ "units": $("input:radio[name=units-browser]:checked").val(), + "alarmHigh": $("#alarmhigh-browser").prop("checked"), + "alarmLow": $("#alarmlow-browser").prop("checked"), "nightMode": $("#nightmode-browser").prop("checked"), - "customTitle": $("input#customTitle").prop("value") + "customTitle": $("input#customTitle").prop("value"), + "theme": $("input:radio[name=theme-browser]:checked").val() }, browserStorage); storeOnServer({ diff --git a/static/treatments.html b/static/treatments.html new file mode 100644 index 00000000000..24761c4d7af --- /dev/null +++ b/static/treatments.html @@ -0,0 +1,98 @@ + + + + + Nightscout: Treatments + + + + + + + + +
+

Nightscout: Treatments

+ + + + + + + + + + + + + + + + + + + + + + + +
TimeEvent TypeBGInsulinCarbsEntered ByNotes
{{treatment.created_at | date:'short'}}{{treatment.eventType}}{{glucoseDisplay(treatment)}}{{treatment.insulin | number: 2}}{{treatment.carbs}}{{treatment.enteredBy}}{{treatment.notes}}
+
+ + + diff --git a/testing/convert-treatments.js b/testing/convert-treatments.js new file mode 100644 index 00000000000..1ef65786be9 --- /dev/null +++ b/testing/convert-treatments.js @@ -0,0 +1,16 @@ +db.treatments.find().forEach( + function (elem) { + db.treatments.update( + { + _id: elem._id + }, + { + $set: { + glucose: elem.glucoseValue, + insulin: elem.insulinGiven, + carbs: elem.carbsGiven + } + } + ); + } +); diff --git a/testing/populate.js b/testing/populate.js new file mode 100644 index 00000000000..f4fff19ae6e --- /dev/null +++ b/testing/populate.js @@ -0,0 +1,125 @@ +'use strict'; +/////////////////////////////////////////////////// +// This script is intended to be run as a cron job +// every n-minutes or whatever the equiv is on windows +// +// Author: John A. [euclidjda](https://github.com/euclidjda) +// Source: https://gist.github.com/euclidjda/4ae207a89921f21382a9 +/////////////////////////////////////////////////// + +/////////////////////////////////////////////////// +// DB Connection setup and utils +/////////////////////////////////////////////////// + +var mongodb = require('mongodb'); +var software = require('./package.json'); +var env = require('./env')( ); + +main(); + +function main( ) { + + var MongoClient = mongodb.MongoClient; + + MongoClient.connect(env.mongo, function connected (err, db) { + + console.log("Connected to mongo, ERROR: %j", err); + if (err) { throw err; } + populate_collection( db ); + + }); + +} + +function populate_collection( db ) { + + //console.log( 'mongo = ' + env.mongo ); + //console.log( 'collection = ' + env.mongo_collection ); + + var cgm_collection = db.collection( env.mongo_collection ); + + var new_cgm_record = get_cgm_record(); + + cgm_collection.insert( new_cgm_record, function(err,created) { + + // TODO: Error checking + process.exit( 0 ); + + } ); + + +} + +function get_cgm_record( ) { + + var dateobj = new Date(); + var datemil = dateobj.getTime(); + var datesec = datemil / 1000; + var datestr = getDateString( dateobj ); + + // We put the time in a range from -1 to +1 for every thiry minute period + var range = (datesec % 1800)/900 - 1.0; + + // The we push through a COS function and scale between 40 and 400 (so it is like a bg level) + var sgv = Math.floor(360*(Math.cos( 10.0 * range / 3.14 ) / 2 + 0.5)) + 40; + var dir = range > 0.0 ? "FortyFiveDown" : "FortyFiveUp"; + + console.log( 'Writing Record: '); + console.log( 'sgv = ' + sgv ); + console.log( 'date = ' + datemil ); + console.log( 'dir = ' + dir ); + console.log( 'str = ' + datestr ); + + var mondo_db = null; + var doc = { 'device' :'dexcom' , + 'date' : datemil , + 'sgv' : sgv , + 'direction' : dir , + 'dateString' : datestr }; + + + return doc; +} + +function getDateString( d ) { + + // How I wish js had strftime. This would be one line of code! + + var month = d.getMonth(); + var day = d.getDay(); + var year = d.getFullYear(); + + if (month < 10 ) month = '0'+month; + if (day < 10 ) day = '0'+day; + + var hour = d.getHours(); + var min = d.getMinutes(); + var sec = d.getSeconds(); + + var ampm = 'PM'; + + if (hour < 12) + { + ampm = "AM"; + } + else + { + ampm = "PM"; + } + + if (hour == 0) + { + hour = 12; + } + if (hour > 12) + { + hour = hour - 12; + } + + if (hour < 10) hour = '0' + hour; + if (min < 10) min = '0' + min; + if (sec < 10) sec = '0' + sec; + + return month + '/' + day + '/' + year + ' ' + hour + ':' + min + ':' + sec + ' ' + ampm; + +} From b63e7be2767429088f3ce8cb34d032593130ce5e Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 31 Aug 2014 10:11:39 -0700 Subject: [PATCH 09/29] tighen up care-portal so that the button is visible on even a 3.5 inch iPhone display; added some additional event types --- static/css/drawer.css | 28 ++++++++++++++++++++++++++- static/index.html | 45 +++++++++++++++++++++++++------------------ static/js/ui-utils.js | 2 +- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/static/css/drawer.css b/static/css/drawer.css index 40a1fc8ca04..75771971d9c 100644 --- a/static/css/drawer.css +++ b/static/css/drawer.css @@ -42,6 +42,32 @@ color: white; } +#treatmentDrawer input { + box-sizing: border-box; +} + +#treatmentDrawer label.left-column { + display: block; + width: 100%; +} + +#treatmentDrawer label.left-column span { + width: 110px; + display: inline-block; +} + +#treatmentDrawer label.left-column input, #treatmentDrawer label.left-column select { + width: 140px; +} + +#treatmentDrawer #glucoseValue { + width: 230px; +} + +#treatmentDrawer #notes { + width: 250px; +} + #about { margin-top: 1em; } @@ -117,7 +143,7 @@ h1, legend, font-size: 16px; margin-top: 0; margin-left: 42px; - padding: 10px; + padding-top: 10px; } #buttonbar { padding: 0; diff --git a/static/index.html b/static/index.html index ce43379664f..76509a0b498 100644 --- a/static/index.html +++ b/static/index.html @@ -119,19 +119,24 @@

Nightscout

Log a Treatment -
-
Entered By:
-
-
- - + +
Glucose Reading @@ -146,12 +151,14 @@

Nightscout

Sensor
- - -
- - -
+ +
diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js index 569df584abe..4288494f9c3 100644 --- a/static/js/ui-utils.js +++ b/static/js/ui-utils.js @@ -186,7 +186,7 @@ function openTreatmentDrawer() { $('#enteredBy').val(browserStorage.get("enteredBy") || ''); $('#eventType').val('BG Check'); - $('#glucoseValue').val(''); + $('#glucoseValue').val('').attr('placeholder', 'Value in ' + browserSettings.units); $('#meter').prop('checked', true) $('#carbsGiven').val(''); $('#insulinGiven').val(''); From 601662d19399e492099292fce9c12de5c5f2bb7c Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 31 Aug 2014 23:02:09 -0700 Subject: [PATCH 10/29] first pass at devicestatus api/storage current only used in the /pebble endpoint --- bin/post-devicestatus.sh | 5 ++++ env.js | 2 ++ lib/api/devicestatus/index.js | 47 +++++++++++++++++++++++++++++++++++ lib/api/index.js | 3 ++- lib/devicestatus.js | 34 +++++++++++++++++++++++++ lib/pebble.js | 15 +++++++++-- server.js | 5 ++-- 7 files changed, 106 insertions(+), 5 deletions(-) create mode 100755 bin/post-devicestatus.sh create mode 100644 lib/api/devicestatus/index.js create mode 100644 lib/devicestatus.js diff --git a/bin/post-devicestatus.sh b/bin/post-devicestatus.sh new file mode 100755 index 00000000000..e53c0782a2e --- /dev/null +++ b/bin/post-devicestatus.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +curl -H "Content-Type: application/json" -XPOST 'http://localhost:1337/api/v1/devicestatus/' -d '{ + "uploaderBattery": 55 +}' diff --git a/env.js b/env.js index 7e001aad35f..c851aaa7650 100644 --- a/env.js +++ b/env.js @@ -27,6 +27,8 @@ function config ( ) { env.mongo_collection = process.env.CUSTOMCONNSTR_mongo_collection || process.env.MONGO_COLLECTION || 'entries'; env.settings_collection = process.env.CUSTOMCONNSTR_mongo_settings_collection || 'settings'; env.treatments_collection = process.env.CUSTOMCONNSTR_mongo_treatments_collection || 'treatments'; + env.devicestatus_collection = process.env.CUSTOMCONNSTR_mongo_devicestatus_collection || 'devicestatus'; + var shasum = crypto.createHash('sha1'); var useSecret = (process.env.API_SECRET && process.env.API_SECRET.length > 0); env.api_secret = null; diff --git a/lib/api/devicestatus/index.js b/lib/api/devicestatus/index.js new file mode 100644 index 00000000000..af67dc7b64e --- /dev/null +++ b/lib/api/devicestatus/index.js @@ -0,0 +1,47 @@ +'use strict'; + +var consts = require('../../constants'); + +function configure (app, wares, devicestatus) { + var express = require('express'), + api = express.Router( ); + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.bodyParser.raw( )); + // json body types get handled as parsed json + api.use(wares.bodyParser.json( )); + // also support url-encoded content-type + api.use(wares.bodyParser.urlencoded({ extended: true })); + + // List settings available + api.get('/devicestatus/', function(req, res) { + devicestatus.list(function (err, profiles) { + return res.json(profiles); + }); + }); + + function config_authed (app, api, wares, devicestatus) { + + api.post('/devicestatus/', /*TODO: auth disabled for quick UI testing... wares.verifyAuthorization, */ function(req, res) { + var obj = req.body; + devicestatus.create(obj, function (err, created) { + if (err) + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + else + res.json(created); + }); + }); + + } + + if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/) { + config_authed(app, api, wares, devicestatus); + } + + return api; +} + +module.exports = configure; + diff --git a/lib/api/index.js b/lib/api/index.js index 323eff38463..a1e0546628b 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -1,6 +1,6 @@ 'use strict'; -function create (env, entries, settings, treatments) { +function create (env, entries, settings, treatments, devicestatus) { var express = require('express'), app = express( ) ; @@ -30,6 +30,7 @@ function create (env, entries, settings, treatments) { app.use('/', require('./entries/')(app, wares, entries)); app.use('/', require('./settings/')(app, wares, settings)); app.use('/', require('./treatments/')(app, wares, treatments)); + app.use('/', require('./devicestatus/')(app, wares, devicestatus)); // Status app.use('/', require('./status')(app, wares)); diff --git a/lib/devicestatus.js b/lib/devicestatus.js new file mode 100644 index 00000000000..eeaffb5515c --- /dev/null +++ b/lib/devicestatus.js @@ -0,0 +1,34 @@ +'use strict'; + +function configure (collection, storage) { + + function create (obj, fn) { + obj.created_at = (new Date( )).toISOString( ); + api( ).insert(obj, function (err, doc) { + fn(null, doc); + }); + } + + function tail(fn) { + return api( ).find({ }).sort({created_at: -1}).limit(1).toArray(function(err, entries) { + if (entries && entries.length > 0) + fn(err, entries[0]); + else + fn(err, null); + }); + } + + function list (fn) { + return api( ).find({ }).sort({created_at: -1}).toArray(fn); + } + + function api ( ) { + return storage.pool.db.collection(collection); + } + + api.list = list; + api.create = create; + api.tail = tail; + return api; +} +module.exports = configure; diff --git a/lib/pebble.js b/lib/pebble.js index 2eaef9c7379..2dc7f7fe6d2 100644 --- a/lib/pebble.js +++ b/lib/pebble.js @@ -22,6 +22,7 @@ function directionToTrend (direction) { function pebble (req, res) { var FORTY_MINUTES = 2400000; var cgmData = [ ]; + var uploaderBattery; function requestMetric() { var units = req.query.units; @@ -60,6 +61,7 @@ function pebble (req, res) { // obj.y = element.sgv; // obj.x = element.date; obj.datetime = element.date; + obj.battery = "" + uploaderBattery; // obj.date = element.date.toString( ); cgmData.push(obj); } @@ -70,11 +72,20 @@ function pebble (req, res) { res.end( ); // collection.db.close(); } - req.entries.list({count: 2}, get_latest); + req.devicestatus.tail(function(err, value) { + if (!err && value) { + uploaderBattery = value.uploaderBattery; + } else { + console.error("req.devicestatus.tail", err); + } + + req.entries.list({count: 2}, get_latest); + }); } -function configure (entries) { +function configure (entries, devicestatus) { function middle (req, res, next) { req.entries = entries; + req.devicestatus = devicestatus; next( ); } return [middle, pebble]; diff --git a/server.js b/server.js index d92816894a2..282e9e6bf77 100644 --- a/server.js +++ b/server.js @@ -33,7 +33,8 @@ var pushover = require('./lib/pushover')(env); var entries = require('./lib/entries')(env.mongo_collection, store); var settings = require('./lib/settings')(env.settings_collection, store); var treatments = require('./lib/treatments')(env.treatments_collection, store, pushover); -var api = require('./lib/api/')(env, entries, settings, treatments); +var devicestatus = require('./lib/devicestatus')(env.devicestatus_collection, store); +var api = require('./lib/api/')(env, entries, settings, treatments, devicestatus); var pebble = require('./lib/pebble'); /////////////////////////////////////////////////// @@ -53,7 +54,7 @@ app.enable('trust proxy'); // Allows req.secure test on heroku https connections app.use('/api/v1', api); // pebble data -app.get('/pebble', pebble(entries)); +app.get('/pebble', pebble(entries, devicestatus)); //app.get('/package.json', software); From c1184f5ccc1f1473f7a7ad9424726af5c5f71eea Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 31 Aug 2014 23:39:15 -0700 Subject: [PATCH 11/29] don't return a string the value 'undefined' if there is no battery info --- bin/post-devicestatus.sh | 3 ++- lib/pebble.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/post-devicestatus.sh b/bin/post-devicestatus.sh index e53c0782a2e..058fffec1a6 100755 --- a/bin/post-devicestatus.sh +++ b/bin/post-devicestatus.sh @@ -1,5 +1,6 @@ #!/bin/sh -curl -H "Content-Type: application/json" -XPOST 'http://localhost:1337/api/v1/devicestatus/' -d '{ +#curl -H "Content-Type: application/json" -XPOST 'http://localhost:1337/api/v1/devicestatus/' -d '{ +curl -H "Content-Type: application/json" -XPOST 'http://ns-dev2.cbrese.com/api/v1/devicestatus/' -d '{ "uploaderBattery": 55 }' diff --git a/lib/pebble.js b/lib/pebble.js index 2dc7f7fe6d2..dda8f2d0f56 100644 --- a/lib/pebble.js +++ b/lib/pebble.js @@ -61,7 +61,7 @@ function pebble (req, res) { // obj.y = element.sgv; // obj.x = element.date; obj.datetime = element.date; - obj.battery = "" + uploaderBattery; + obj.battery = uploaderBattery ? "" + uploaderBattery : undefined; // obj.date = element.date.toString( ); cgmData.push(obj); } From 2a44a1341b0a28d6873bfe65e9fda7dd3abbccc2 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 31 Aug 2014 23:40:12 -0700 Subject: [PATCH 12/29] removed debug url --- bin/post-devicestatus.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/post-devicestatus.sh b/bin/post-devicestatus.sh index 058fffec1a6..e53c0782a2e 100755 --- a/bin/post-devicestatus.sh +++ b/bin/post-devicestatus.sh @@ -1,6 +1,5 @@ #!/bin/sh -#curl -H "Content-Type: application/json" -XPOST 'http://localhost:1337/api/v1/devicestatus/' -d '{ -curl -H "Content-Type: application/json" -XPOST 'http://ns-dev2.cbrese.com/api/v1/devicestatus/' -d '{ +curl -H "Content-Type: application/json" -XPOST 'http://localhost:1337/api/v1/devicestatus/' -d '{ "uploaderBattery": 55 }' From 790b53700777fbf0ea70102a00dea3f5cee45d24 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 31 Aug 2014 23:56:43 -0700 Subject: [PATCH 13/29] clean up tooltip --- static/js/client.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/static/js/client.js b/static/js/client.js index 33ab33de0db..4a498a6d5dd 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -419,12 +419,13 @@ div.transition() .duration(200) .style("opacity", .9); - div.html("Time: " + formatTime(d.created_at) + "
" + "Treatment type: " + d.eventType + "
" + "Carbs: " + d.carbs + "
" + - "Insulin: " + d.insulin + "
" + - "BG: " + d.glucose + "
" + - "Test method: " + d.glucoseType + "
" + - "Entered by: " + d.enteredBy + "
" + - "Notes: " + d.notes) + div.html("Time: " + formatTime(d.created_at) + "
" + "Treatment type: " + d.eventType + "
" + + (d.carbs ? "Carbs: " + d.carbs + "
" : '') + + (d.insulin ? "Insulin: " + d.insulin + "
" : '') + + (d.glucose ? "BG: " + d.glucose + (d.glucoseType ? ' (' + d.glucoseType + ')': '') + "
" : '') + + (d.enteredBy ? "Entered by: " + d.enteredBy + "
" : '') + + (d.notes ? "Notes: " + d.notes : '') + ) .style("left", (d3.event.pageX) + "px") .style("top", (d3.event.pageY - 28) + "px"); }) From 767497b61f8aea9e1b13ea054e502ae01e5e610b Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Mon, 1 Sep 2014 00:18:01 -0700 Subject: [PATCH 14/29] clean up indents --- static/js/client.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/static/js/client.js b/static/js/client.js index 4a498a6d5dd..612d57354eb 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -416,9 +416,7 @@ .attr('stroke', function (d) { return "white"; }) .attr('fill', function (d) { return "grey"; }) .on("mouseover", function (d) { - div.transition() - .duration(200) - .style("opacity", .9); + div.transition().duration(200).style("opacity", .9); div.html("Time: " + formatTime(d.created_at) + "
" + "Treatment type: " + d.eventType + "
" + (d.carbs ? "Carbs: " + d.carbs + "
" : '') + (d.insulin ? "Insulin: " + d.insulin + "
" : '') + @@ -426,8 +424,8 @@ (d.enteredBy ? "Entered by: " + d.enteredBy + "
" : '') + (d.notes ? "Notes: " + d.notes : '') ) - .style("left", (d3.event.pageX) + "px") - .style("top", (d3.event.pageY - 28) + "px"); + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); }) .on("mouseout", function (d) { div.transition() From 664bed8b2d830e7f7b6080bc746aa2485a9c0448 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Mon, 1 Sep 2014 00:18:37 -0700 Subject: [PATCH 15/29] renamed tail to last since it's a single value --- lib/devicestatus.js | 4 ++-- lib/pebble.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/devicestatus.js b/lib/devicestatus.js index eeaffb5515c..fb21d1c2490 100644 --- a/lib/devicestatus.js +++ b/lib/devicestatus.js @@ -9,7 +9,7 @@ function configure (collection, storage) { }); } - function tail(fn) { + function last(fn) { return api( ).find({ }).sort({created_at: -1}).limit(1).toArray(function(err, entries) { if (entries && entries.length > 0) fn(err, entries[0]); @@ -28,7 +28,7 @@ function configure (collection, storage) { api.list = list; api.create = create; - api.tail = tail; + api.last = last; return api; } module.exports = configure; diff --git a/lib/pebble.js b/lib/pebble.js index dda8f2d0f56..d69d3af6051 100644 --- a/lib/pebble.js +++ b/lib/pebble.js @@ -72,7 +72,7 @@ function pebble (req, res) { res.end( ); // collection.db.close(); } - req.devicestatus.tail(function(err, value) { + req.devicestatus.last(function(err, value) { if (!err && value) { uploaderBattery = value.uploaderBattery; } else { From 8d324bd44a3a4116e336d3d3a1f321a40d8bf169 Mon Sep 17 00:00:00 2001 From: jimsiff Date: Thu, 4 Sep 2014 11:08:19 -0700 Subject: [PATCH 16/29] Update index.html --- static/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/static/index.html b/static/index.html index 76509a0b498..cde452ae8cb 100644 --- a/static/index.html +++ b/static/index.html @@ -134,6 +134,7 @@

Nightscout

+ From 9ff0095f6d9828065d44741bc1be7e222e1530aa Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Fri, 5 Sep 2014 01:27:48 -0700 Subject: [PATCH 17/29] fixed bug that caused SGV's to be transparent when units set to mmol --- static/js/client.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/static/js/client.js b/static/js/client.js index 612d57354eb..5f5a32e3c97 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -40,8 +40,9 @@ // Tick Values var tickValues = [40, 60, 80, 120, 180, 300, 400]; if (browserSettings.units == "mmol") { - var tickValues = [2.0, 3.0, 4.0, 6.0, 10.0, 15.0, 22.0]; + tickValues = [2.0, 3.0, 4.0, 6.0, 10.0, 15.0, 22.0]; } + var div = d3.select("body").append("div") .attr("class", "tooltip") .style("opacity", 0); @@ -401,7 +402,7 @@ .attr("ry", 6) .attr('stroke-width', 2) .attr('stroke', function (d) { return "white"; }) - .attr('fill', function (d) { return "grey"; }) + .attr('fill', function (d) { return "grey"; }); // if new circle then just display @@ -799,18 +800,18 @@ } } data = d[0].map(function (obj) { - return { date: new Date(obj.x), sgv: scaleBg(obj.y), direction: obj.direction, color: sgvToColor(obj.y)} + return { date: new Date(obj.x), y: obj.y, sgv: scaleBg(obj.y), direction: obj.direction, color: sgvToColor(obj.y)} }); // TODO: This is a kludge to advance the time as data becomes stale by making old predictor clear (using color = 'none') // This shouldn't have to be sent and can be fixed by using xScale.domain([x0,x1]) function with // 2 days before now as x0 and 30 minutes from now for x1 for context plot, but this will be // required to happen when "now" event is sent from websocket.js every minute. When fixed, // remove all "color != 'none'" code - data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'none'} })); - data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'red'} })); + data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), y: obj.y, sgv: scaleBg(obj.y), color: 'none'} })); + data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), y: obj.y, sgv: scaleBg(obj.y), color: 'red'} })); data.forEach(function (d) { - if (d.sgv < 39) + if (d.y < 39) d.color = "transparent"; }); @@ -1054,9 +1055,9 @@ var ONE_MINUTE = 60 * 1000; var FIVE_MINUTES = 5 * ONE_MINUTE; var predicted = []; - var BG_REF = 140; - var BG_MIN = 36; - var BG_MAX = 400; + var BG_REF = scaleBg(140); + var BG_MIN = scaleBg(36); + var BG_MAX = scaleBg(400); // these are the one sigma limits for the first 13 prediction interval uncertainties (65 minutes) var CONE = [0.020, 0.041, 0.061, 0.081, 0.099, 0.116, 0.132, 0.146, 0.159, 0.171, 0.182, 0.192, 0.201]; if (actual.length < 2) { From 7b1080f3748ff7ceea30b0acdda7caa55dc2dde2 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Fri, 5 Sep 2014 01:38:41 -0700 Subject: [PATCH 18/29] bump version fixes #131 --- bower.json | 2 +- package.json | 2 +- static/index.html | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bower.json b/bower.json index 6dbd1b93304..8a91092f296 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.3.5", + "version": "0.4.0", "dependencies": { "angularjs": "1.3.0-beta.19", "bootstrap": "~3.2.0", diff --git a/package.json b/package.json index 96f304011c2..cb4f165e5dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Nightscout", - "version": "0.3.5", + "version": "0.4.0", "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": "MIT", "author": "Nightscout Team", diff --git a/static/index.html b/static/index.html index 76509a0b498..bb3a92f5ad8 100644 --- a/static/index.html +++ b/static/index.html @@ -6,9 +6,9 @@ NightScout - + - + @@ -178,8 +178,8 @@

Nightscout

- - + + From 5581bd0839a7104e568d33a4b1cefcf1a423a726 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sat, 6 Sep 2014 10:08:57 -0700 Subject: [PATCH 19/29] added -dev suffix to version closes #131 --- bower.json | 2 +- package.json | 2 +- static/index.html | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bower.json b/bower.json index 8a91092f296..6d7a96c61be 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.4.0", + "version": "0.4.0-dev", "dependencies": { "angularjs": "1.3.0-beta.19", "bootstrap": "~3.2.0", diff --git a/package.json b/package.json index cb4f165e5dd..0e60f1a9d7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Nightscout", - "version": "0.4.0", + "version": "0.4.0-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": "MIT", "author": "Nightscout Team", diff --git a/static/index.html b/static/index.html index bb3a92f5ad8..4228661d5b3 100644 --- a/static/index.html +++ b/static/index.html @@ -6,9 +6,9 @@ NightScout - + - + @@ -178,8 +178,8 @@

Nightscout

- - + + From 9df56fdc6be7e6793c7b5edc29fdfaabfa219dd0 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 7 Sep 2014 11:01:46 -0700 Subject: [PATCH 20/29] require careportal to be enabled --- env.js | 2 ++ lib/api/index.js | 6 ++++++ lib/api/status.js | 1 + lib/api/treatments/index.js | 4 ++-- static/css/drawer.css | 3 +-- static/js/ui-utils.js | 4 +++- 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/env.js b/env.js index c851aaa7650..cc9e7be59ce 100644 --- a/env.js +++ b/env.js @@ -29,6 +29,8 @@ function config ( ) { env.treatments_collection = process.env.CUSTOMCONNSTR_mongo_treatments_collection || 'treatments'; env.devicestatus_collection = process.env.CUSTOMCONNSTR_mongo_devicestatus_collection || 'devicestatus'; + env.enable = process.env.ENABLE || ''; + var shasum = crypto.createHash('sha1'); var useSecret = (process.env.API_SECRET && process.env.API_SECRET.length > 0); env.api_secret = null; diff --git a/lib/api/index.js b/lib/api/index.js index a1e0546628b..0ba9c346036 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -18,6 +18,12 @@ function create (env, entries, settings, treatments, devicestatus) { app.enable('api'); } + env.enable.split(' ').forEach(function(value) { + var enable = value.trim(); + console.info("enabling feature:", enable); + app.enable(enable); + }); + app.set('title', [app.get('name'), 'API', app.get('version')].join(' ')); // Start setting up routes diff --git a/lib/api/status.js b/lib/api/status.js index 6e524f5cf1f..4fd8635ecde 100644 --- a/lib/api/status.js +++ b/lib/api/status.js @@ -12,6 +12,7 @@ function configure (app, wares) { api.get('/status', function (req, res, next) { var info = { status: 'ok' , apiEnabled: app.enabled('api') + , careportalEnabled: app.enabled('careportal') , units: app.get('units') , version: app.get('version') , name: app.get('name')}; diff --git a/lib/api/treatments/index.js b/lib/api/treatments/index.js index 9dde12f9f33..f60f3bd0b2d 100644 --- a/lib/api/treatments/index.js +++ b/lib/api/treatments/index.js @@ -24,7 +24,7 @@ function configure (app, wares, treatments) { function config_authed (app, api, wares, treatments) { - api.post('/treatments/', /*TODO: auth disabled for quick UI testing... wares.verifyAuthorization, */ function(req, res) { + api.post('/treatments/', /*TODO: auth disabled for now, need to get login figured out... wares.verifyAuthorization, */ function(req, res) { var treatment = req.body; treatments.create(treatment, function (err, created) { if (err) @@ -36,7 +36,7 @@ function configure (app, wares, treatments) { } - if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/) { + if (app.enabled('api') && app.enabled('careportal')) { config_authed(app, api, wares, treatments); } diff --git a/static/css/drawer.css b/static/css/drawer.css index 75771971d9c..7b7810b5610 100644 --- a/static/css/drawer.css +++ b/static/css/drawer.css @@ -146,10 +146,9 @@ h1, legend, padding-top: 10px; } #buttonbar { - padding: 0; + padding-right: 10px; float: right; height: 44px; - width: 190px; opacity: 0.75; vertical-align: middle; } diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js index 4288494f9c3..225ddb193b3 100644 --- a/static/js/ui-utils.js +++ b/static/js/ui-utils.js @@ -15,7 +15,8 @@ $.ajax("/api/v1/status.json", { app = { "name": xhr.name, "version": xhr.version, - "apiEnabled": xhr.apiEnabled + "apiEnabled": xhr.apiEnabled, + "careportalEnabled": xhr.careportalEnabled } } }).done(function() { @@ -24,6 +25,7 @@ $.ajax("/api/v1/status.json", { if (app.apiEnabled) { $(".serverSettings").show(); } + $("#treatmentDrawerToggle").toggle(app.careportalEnabled); }); From e52ea556f303d8dc9eab6718ad9507577dd2bdbc Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 7 Sep 2014 17:04:34 -0700 Subject: [PATCH 21/29] clean up the setting of env vars and better support azure weirdness --- env.js | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/env.js b/env.js index cc9e7be59ce..2901a6a7dad 100644 --- a/env.js +++ b/env.js @@ -21,35 +21,40 @@ function config ( ) { env.version = software.version; env.name = software.name; - env.DISPLAY_UNITS = process.env.DISPLAY_UNITS || 'mg/dl'; - env.PORT = process.env.PORT || 1337; - env.mongo = process.env.MONGO_CONNECTION || process.env.CUSTOMCONNSTR_mongo || process.env.MONGOLAB_URI; - env.mongo_collection = process.env.CUSTOMCONNSTR_mongo_collection || process.env.MONGO_COLLECTION || 'entries'; - env.settings_collection = process.env.CUSTOMCONNSTR_mongo_settings_collection || 'settings'; - env.treatments_collection = process.env.CUSTOMCONNSTR_mongo_treatments_collection || 'treatments'; - env.devicestatus_collection = process.env.CUSTOMCONNSTR_mongo_devicestatus_collection || 'devicestatus'; + + env.DISPLAY_UNITS = readENV('DISPLAY_UNITS', 'mg/dl'); + env.PORT = readENV('PORT', 1337); + env.mongo = readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI'); + env.mongo_collection = readENV('MONGO_COLLECTION', 'entries'); + env.settings_collection = readENV('MONGO_SETTINGS_COLLECTION', 'settings'); + env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); + env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); env.enable = process.env.ENABLE || ''; var shasum = crypto.createHash('sha1'); - var useSecret = (process.env.API_SECRET && process.env.API_SECRET.length > 0); + + ///////////////////////////////////////////////////////////////// + // A little ugly, but we don't want to read the secret into a var + ///////////////////////////////////////////////////////////////// + var useSecret = (readENV('API_SECRET') && readENV('API_SECRET').length > 0); env.api_secret = null; // if a passphrase was provided, get the hex digest to mint a single token if (useSecret) { - if (process.env.API_SECRET.length < consts.MIN_PASSPHRASE_LENGTH) { + if (readENV('API_SECRET').length < consts.MIN_PASSPHRASE_LENGTH) { var msg = ["API_SECRET should be at least", consts.MIN_PASSPHRASE_LENGTH, "characters"]; var err = new Error(msg.join(' ')); // console.error(err); throw err; process.exit(1); } - shasum.update(process.env.API_SECRET); + shasum.update(readENV('API_SECRET')); env.api_secret = shasum.digest('hex'); } // For pushing notifications to Pushover. - env.pushover_api_token = process.env.PUSHOVER_API_TOKEN; - env.pushover_user_key = process.env.PUSHOVER_USER_KEY || process.env.PUSHOVER_GROUP_KEY; + env.pushover_api_token = readENV('PUSHOVER_API_TOKEN'); + env.pushover_user_key = readENV('PUSHOVER_USER_KEY') || readENV('PUSHOVER_GROUP_KEY'); // TODO: clean up a bit // Some people prefer to use a json configuration file instead. @@ -61,9 +66,19 @@ function config ( ) { env.mongo = DB_URL; env.mongo_collection = DB_COLLECTION; env.settings_collection = DB_SETTINGS_COLLECTION; - var STATIC_FILES = __dirname + '/static/'; - env.static_files = process.env.NIGHTSCOUT_STATIC_FILES || STATIC_FILES; + env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/'); return env; } + +function readENV(varName, defaultValue) { + //for some reason Azure uses this prefix, maybe there is a good reason + var value = process.env['CUSTOMCONNSTR_' + varName] + || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] + || process.env[varName] + || process.env[varName.toLowerCase()]; + + return value || defaultValue; +} + module.exports = config; From ff2dc98a0db1ad3e8035a776b17794a26777f124 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 7 Sep 2014 17:09:27 -0700 Subject: [PATCH 22/29] lower case the ENABLE env var value to prevent support pain --- lib/api/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/index.js b/lib/api/index.js index 0ba9c346036..4bca9f90c73 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -18,7 +18,7 @@ function create (env, entries, settings, treatments, devicestatus) { app.enable('api'); } - env.enable.split(' ').forEach(function(value) { + env.enable.toLowerCase().split(' ').forEach(function(value) { var enable = value.trim(); console.info("enabling feature:", enable); app.enable(enable); From 8132ca5763265e34795d5aeddcadb3aae34131b1 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 7 Sep 2014 23:20:55 -0700 Subject: [PATCH 23/29] better formatting for pushover message --- lib/treatments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/treatments.js b/lib/treatments.js index 8bfe8d45b0f..f232834f629 100644 --- a/lib/treatments.js +++ b/lib/treatments.js @@ -9,7 +9,7 @@ function configure (collection, storage, pushover) { if (pushover) { - var text = (obj.glucose ? 'Blood glucose: ' + obj.glucose + ' (' + obj.glucoseType + ')' : '') + + var text = '\n' + (obj.glucose ? 'BG: ' + obj.glucose + ' (' + obj.glucoseType + ')' : '') + (obj.carbs ? '\nCarbs: ' + obj.carbs : '') + (obj.insulin ? '\nInsulin: ' + obj.insulin : '')+ (obj.enteredBy ? '\nEntered By: ' + obj.enteredBy : '') + From 5532c0b94bd72bd3a260eb04449da55b630873a2 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Sun, 7 Sep 2014 23:24:37 -0700 Subject: [PATCH 24/29] get rid of extra line --- lib/treatments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/treatments.js b/lib/treatments.js index f232834f629..4647d52433c 100644 --- a/lib/treatments.js +++ b/lib/treatments.js @@ -9,7 +9,7 @@ function configure (collection, storage, pushover) { if (pushover) { - var text = '\n' + (obj.glucose ? 'BG: ' + obj.glucose + ' (' + obj.glucoseType + ')' : '') + + var text = (obj.glucose ? 'BG: ' + obj.glucose + ' (' + obj.glucoseType + ')' : '') + (obj.carbs ? '\nCarbs: ' + obj.carbs : '') + (obj.insulin ? '\nInsulin: ' + obj.insulin : '')+ (obj.enteredBy ? '\nEntered By: ' + obj.enteredBy : '') + From 6a345083f412a6a0f2500c1d737ecd1ac2e9afad Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Tue, 9 Sep 2014 14:56:11 -0700 Subject: [PATCH 25/29] fixed bug that caused the ENABLE env to not be read if set as a Connection String in Azure --- env.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.js b/env.js index 2901a6a7dad..52265bf777c 100644 --- a/env.js +++ b/env.js @@ -30,7 +30,7 @@ function config ( ) { env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); - env.enable = process.env.ENABLE || ''; + env.enable = readENV('ENABLE'); var shasum = crypto.createHash('sha1'); From a95785562423713a75bebbf29ddf29677bcee4b0 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Tue, 9 Sep 2014 15:02:44 -0700 Subject: [PATCH 26/29] make sure env.enable is set before trying to use it --- lib/api/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/api/index.js b/lib/api/index.js index 4bca9f90c73..ff696802710 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -18,11 +18,13 @@ function create (env, entries, settings, treatments, devicestatus) { app.enable('api'); } - env.enable.toLowerCase().split(' ').forEach(function(value) { - var enable = value.trim(); - console.info("enabling feature:", enable); - app.enable(enable); - }); + if (env.enable) { + env.enable.toLowerCase().split(' ').forEach(function (value) { + var enable = value.trim(); + console.info("enabling feature:", enable); + app.enable(enable); + }); + } app.set('title', [app.get('name'), 'API', app.get('version')].join(' ')); From 08f7fcc777247cb0a4e3502604bb61f7011ce9d8 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Tue, 9 Sep 2014 15:15:28 -0700 Subject: [PATCH 27/29] need to use scaleBG for treatment bubble postions for use with mmol fixes #146 --- static/js/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/client.js b/static/js/client.js index 5f5a32e3c97..692ca87d130 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -395,7 +395,7 @@ treatCircles.transition() .duration(UPDATE_TRANS_MS) .attr('x', function (d) { return xScale(new Date(d.created_at)); }) - .attr('y', function (d) { return yScale(500); }) + .attr('y', function (d) { return yScale(scaleBg(500)); }) .attr("width", 15) .attr("height", 15) .attr("rx", 6) @@ -408,7 +408,7 @@ // if new circle then just display treatCircles.enter().append('rect') .attr('x', function (d) { return xScale(d.created_at); }) - .attr('y', function (d) { return yScale(500); }) + .attr('y', function (d) { return yScale(scaleBg(500)); }) .attr("width", 15) .attr("height", 15) .attr("rx", 6) From 7ec8ffdbd2bdcab243b6b4695e72666373dee95e Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Tue, 9 Sep 2014 15:59:05 -0700 Subject: [PATCH 28/29] the careportal is only enabled if the api is also enabled --- lib/api/status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/status.js b/lib/api/status.js index 4fd8635ecde..76e1350dacf 100644 --- a/lib/api/status.js +++ b/lib/api/status.js @@ -12,7 +12,7 @@ function configure (app, wares) { api.get('/status', function (req, res, next) { var info = { status: 'ok' , apiEnabled: app.enabled('api') - , careportalEnabled: app.enabled('careportal') + , careportalEnabled: app.enabled('api') && app.enabled('careportal') , units: app.get('units') , version: app.get('version') , name: app.get('name')}; From 6c038f630bd3ca54f8446f93bd3e1fb7d3ab86a0 Mon Sep 17 00:00:00 2001 From: Jason Calabrese Date: Tue, 9 Sep 2014 22:44:12 -0700 Subject: [PATCH 29/29] prepare versions for release --- bower.json | 2 +- package.json | 2 +- static/index.html | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bower.json b/bower.json index 6d7a96c61be..8a91092f296 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "nightscout", - "version": "0.4.0-dev", + "version": "0.4.0", "dependencies": { "angularjs": "1.3.0-beta.19", "bootstrap": "~3.2.0", diff --git a/package.json b/package.json index 0e60f1a9d7b..cb4f165e5dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Nightscout", - "version": "0.4.0-dev", + "version": "0.4.0", "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": "MIT", "author": "Nightscout Team", diff --git a/static/index.html b/static/index.html index 4324f1c5aa6..88e3ddf5fac 100644 --- a/static/index.html +++ b/static/index.html @@ -6,9 +6,9 @@ NightScout - + - + @@ -179,8 +179,8 @@

Nightscout

- - + +