Revert "Revert to c1b2988eb0 (Merge branch 'hotfix/0.3.5')"

This reverts commit 1cc9c6127c.
This commit is contained in:
Jason Calabrese
2014-08-31 00:04:11 -07:00
parent 1cc9c6127c
commit 0404905e53
35 changed files with 876 additions and 174 deletions
+3
View File
@@ -8,5 +8,8 @@ my.env
*.env
static/bower_components/
.*.sw?
.DS_Store
.vagrant
/iisnode
/web.config
+1
View File
@@ -0,0 +1 @@
web: node server.js
+2
View File
@@ -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
+13
View File
@@ -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"
]
}
+11
View File
@@ -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..."
}'
+2
View File
@@ -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",
+3 -2
View File
@@ -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;
+2 -1
View File
@@ -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));
+47
View File
@@ -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;
+9 -3
View File
@@ -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);
});
}
+24
View File
@@ -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;
+38 -12
View File
@@ -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
+1
View File
@@ -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"
},
+5 -4
View File
@@ -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);
});
///////////////////////////////////////////////////
+27 -2
View File
@@ -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;
+1 -1
View File
@@ -136,7 +136,7 @@ body {
#bgButton,
#silenceBtn {
z-index: 999;
z-index: 99;
}
#bgButton {
+6
View File
@@ -60,6 +60,12 @@
"code": 59392,
"src": "mfglabs"
},
{
"uid": "55e2ff85b1c459c383f46da6e96014b0",
"css": "plus",
"code": 59403,
"src": "elusive"
},
{
"uid": "b59b3b618699b467541f631edd5a02ed",
"css": "volume",
+1
View File
@@ -10,3 +10,4 @@
.icon-battery-100:before { content: '\e808'; } /* '' */
.icon-cancel-circled:before { content: '\e809'; } /* '' */
.icon-volume:before { content: '\e80a'; } /* '' */
.icon-plus:before { content: '\e80b'; } /* '' */
File diff suppressed because one or more lines are too long
+1
View File
@@ -10,3 +10,4 @@
.icon-battery-100 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
+1
View File
@@ -21,3 +21,4 @@
.icon-battery-100 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-volume { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
+7 -6
View File
@@ -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');
}
}
*/
@@ -61,3 +61,4 @@
.icon-battery-100:before { content: '\e808'; } /* '' */
.icon-cancel-circled:before { content: '\e809'; } /* '' */
.icon-volume:before { content: '\e80a'; } /* '' */
.icon-plus:before { content: '\e80b'; } /* '' */
+1
View File
@@ -270,6 +270,7 @@ body {
<div title="Code: 0xe808" class="the-icons span3"><i class="icon-battery-100"></i> <span class="i-name">icon-battery-100</span><span class="i-code">0xe808</span></div>
<div title="Code: 0xe809" class="the-icons span3"><i class="icon-cancel-circled"></i> <span class="i-name">icon-cancel-circled</span><span class="i-code">0xe809</span></div>
<div title="Code: 0xe80a" class="the-icons span3"><i class="icon-volume"></i> <span class="i-name">icon-volume</span><span class="i-code">0xe80a</span></div>
<div title="Code: 0xe80b" class="the-icons span3"><i class="icon-plus"></i> <span class="i-name">icon-plus</span><span class="i-code">0xe80b</span></div>
</div>
</div>
<div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
Binary file not shown.
+1
View File
@@ -17,6 +17,7 @@
<glyph glyph-name="battery-100" unicode="&#xe808;" d="m365 194q-21 0-37 15t-15 37l0 209q0 21 15 36t37 15 36-15 15-36l0-209q0-21-15-37t-36-15z m-156 0q-21 0-37 15t-16 37l0 209q0 21 16 36t37 15 36-15 15-36l0-209q0-21-15-37t-36-15z m469 0q-22 0-37 15t-16 37l0 209q0 21 16 36t37 15 36-15 15-36l0-209q0-21-15-37t-36-15z m-157 0q-21 0-37 15t-15 37l0 209q0 21 15 36t37 15 37-15 15-36l0-209q0-21-15-37t-37-15z m364 312q44 0 74-30t31-73l0-104q0-44-31-75t-74-30q0-65-46-111t-110-45l-573 0q-65 0-110 45t-46 111l0 312q0 65 46 111t110 46l573 0q65 0 110-46t46-111z m-104-312l0 312q0 22-15 37t-37 16l-573 0q-21 0-37-16t-15-37l0-312q0-21 15-36t37-15l573 0q21 0 37 15t15 36z" horiz-adv-x="990" />
<glyph glyph-name="cancel-circled" unicode="&#xe809;" d="m420 770q174 0 297-123t123-297-123-297-297-123-297 123-123 297 123 297 297 123z m86-420l154 154-86 86-154-152-152 152-88-86 154-154-154-152 88-86 152 152 154-152 86 86z" horiz-adv-x="840" />
<glyph glyph-name="volume" unicode="&#xe80a;" d="m0 142l0 416 236 0 354 289 0-994-354 289-236 0z m652 35q73 74 73 176t-73 178l71 74q105-106 107-254 0-145-107-246z m118-119q123 119 123 295t-123 299l76 74q154-154 154-372t-154-372z" horiz-adv-x="1000" />
<glyph glyph-name="plus" unicode="&#xe80b;" d="m0 209l0 282 359 0 0 359 282 0 0-359 359 0 0-282-359 0 0-359-282 0 0 359-359 0z" horiz-adv-x="1000" />
</font>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+60 -1
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, maximum-scale=1, initial-scale=1, user-scalable=0" />
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="/images/logomobile.png">
<title>NightScout</title>
<link href="/images/round1.png" rel="icon" id="favicon" type="image/png" />
<link rel="stylesheet" type="text/css" href="/css/main.css?v=0.3.3" />
@@ -16,6 +18,7 @@
<a id="testAlarms" class="tip" original-title="Alarm Test / Smartphone Enable" href="#"><i class="icon-volume"></i></a>
<a id="hideToolbar" class="tip" original-title="Hides the toolbar" href="#"><i class="icon-angle-double-up"></i></a>
<a id="drawerToggle" class="tip" original-title="Settings" href="#"><i class="icon-cog"></i></a>
<a id="treatmentDrawerToggle" class="tip" original-title="Treatments" href="#"><i class="icon-plus"></i></a>
</div>
<h1 class="customTitle">Nightscout</h1>
</div>
@@ -68,6 +71,11 @@
<dd><input type="radio" name="units-browser" id="mgdl-browser" value="mg/dl" checked /><label for="mgdl-browser">mg/dL</label><br />
<dd><input type="radio" name="units-browser" id="mmol-browser" value="mmol" /><label for="mmol-browser">mmol/L</label>
</dl>
<dl class="toggle">
<dt>Enable Alarms <a class="tip" original-title="When enabled the an alarm will sound."><i class="icon-help-circled"></i></a></dt>
<dd><input type="checkbox" name="alarmhigh-browser" id="alarmhigh-browser" /><label for="alarmhigh-browser">High Alarm</label></dd>
<dd><input type="checkbox" name="alarmlow-browser" id="alarmlow-browser" /><label for="alarmlow-browser">Low Alarm</label></dd>
</dl>
<dl class="toggle">
<dt>Night Mode <a class="tip" original-title="When enabled the page will be dimmed from 10pm - 6am."><i class="icon-help-circled"></i></a></dt>
<dd><input type="checkbox" name="nightmode-browser" id="nightmode-browser" /><label for="nightmode-browser">Enable</label></dd>
@@ -76,6 +84,11 @@
<dt>Custom Title</dt>
<dd><input type="text" id="customTitle" value="Nightscout" /></dd>
</dl>
<dl class="radio">
<dt>Theme</dt>
<dd><input type="radio" name="theme-browser" id="theme-default-browser" value="default" checked /><label for="theme-default-browser">Default</label><br />
<dd><input type="radio" name="theme-browser" id="theme-colors-browser" value="colors" /><label for="theme-colors-browser">Colors</label>
</dl>
</fieldset>
<input type="submit" id="save" value="Save" />
@@ -102,6 +115,52 @@
</form>
</div>
<div id="treatmentDrawer">
<form id="treatment-form" onsubmit="return treatmentSubmit();">
<fieldset class="treatmentData">
<legend>Log a Treatment</legend>
<dl>
<dt>Entered By:</dt>
<dd><input type="text" id="enteredBy" value="" /></dd>
</dl>
<label for="eventType">Event Type:</label>
<select id="eventType">
<option value="BG Check">Blood Glucose Check</option>
<option value="Snack Bolus">Snack Bolus</option>
<option value="Meal Bolus">Meal Bolus</option>
<option value="Correction Bolus">Correction Bolus</option>
<option value="Food Correction">Food Correction</option>
<option value="Question">Note or Question to Parents</option>
</select>
<fieldset>
<legend>Glucose Reading</legend>
<input type="number" step="any" id="glucoseValue" />
<label><br>Measurement Method:<br></label>
<label for="meter">
<input type="radio" name="glucoseType" id="meter" value="Finger"/>
<span>Meter</span>
</label>
<label for="sensor">
<input type="radio" name="glucoseType" id="sensor" value="Sensor"/>
<span>Sensor<br></span>
</label>
</fieldset>
<label for="carbsGiven">Carbs Given (mass in grams):<br></label>
<input type="number" step="any" min="0" id="carbsGiven" />
<br>
<label for="insulinGiven"><br>Insulin Given (units):<br></label>
<input type="number" step="any" min="0" id="insulinGiven" />
<br>
<!-- Label and textarea -->
<label for="notes"><br>Additional Notes, Comments:<br></label>
<textarea id="notes"></textarea><br>
<button type="submit">Submit Form</button>
</fieldset>
<a href="/treatments.html" target="treatments">View all treatments</a>
</form>
</div>
<div class="audio alarms">
<audio src="/audio/alarm.mp3" preload="auto" loop="true" class="alarm mp3" type="audio/mp3"></audio>
<audio src="/audio/alarm2.mp3" preload="auto" loop="true" class="urgent alarm2 mp3" type="audio/mp3"></audio>
+241 -122
View File
@@ -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 = '&#8987;'; 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,41 +729,36 @@
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;
latestSGV = d[0][d[0].length - 1];
//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;
//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);
}
$('#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)
}
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) {
isInitialData = true;
@@ -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;
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)) + ')');
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 }
];
//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;
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 R1 = Math.sqrt(Math.min(carbs, insulin * CR)) / scale,
R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / scale,
R3 = R2 + 8 / scale;
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 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 }
];
var treatmentDots = focus.selectAll('treatment-dot')
.data(arc_data)
.enter()
.append('g')
.attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(treatment.y)) + ')');
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 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('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) + ')';
var arc = d3.svg.arc()
.innerRadius(function (d) {
return 5 * d.inner;
})
.text(function (d) { return d.element; })
.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('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;
}
+116 -8
View File
@@ -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({
+98
View File
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nightscout: Treatments</title>
<link href="/images/round1.png" rel="icon" id="favicon" type="image/png" />
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<style type="text/css">
table tr td {
white-space: nowrap;
}
</style>
<script src="/bower_components/angularjs/angular.min.js"></script>
<script type="text/javascript">
'use strict';
var app = angular.module('ns-treatments', []);
app.controller('TreatmentsController', function ($scope, $http, $timeout, visibility) {
var pageIsHidden = false;
function update() {
console.info("update called");
if (pageIsHidden) {
console.info('Not updating, since page is hidden');
} else {
console.info('Updating, since page is visible');
$http.get('/api/v1/treatments').success(function(treatments) {
console.info("got treatments", treatments);
$scope.treatments = treatments;
});
}
}
$scope.$on('visibilityChanged', function(event, isHidden) {
console.info('changed pageIsHidden to ' + isHidden);
pageIsHidden = isHidden;
if (!pageIsHidden) update();
});
function startUpdateCycle() {
update();
$timeout(startUpdateCycle, 20 * 1000);
}
startUpdateCycle();
$scope.glucoseDisplay = function(treatment) {
if (treatment.glucose)
return treatment.glucose + (treatment.glucoseType ? ' (' + treatment.glucoseType + ')' : '');
else
return '';
}
});
app.service('visibility', function visibility($rootScope) {
function visibilityChanged() {
$rootScope.$broadcast('visibilityChanged', !!(document.hidden || document.webkitHidden || document.mozHidden || document.msHidden));
}
document.addEventListener('webkitvisibilitychange', visibilityChanged);
});
</script>
</head>
<body ng-app="ns-treatments">
<div class="container" ng-controller="TreatmentsController">
<h3>Nightscout: Treatments</h3>
<table class="table table-striped table-hover">
<thead>
<tr>
<td>Time</td>
<td>Event Type</td>
<td>BG</td>
<td>Insulin</td>
<td>Carbs</td>
<td>Entered By</td>
<td>Notes</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="treatment in treatments">
<td>{{treatment.created_at | date:'short'}}</td>
<td>{{treatment.eventType}}</td>
<td>{{glucoseDisplay(treatment)}}</td>
<td>{{treatment.insulin | number: 2}}</td>
<td>{{treatment.carbs}}</td>
<td>{{treatment.enteredBy}}</td>
<td>{{treatment.notes}}</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
+16
View File
@@ -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
}
}
);
}
);
+125
View File
@@ -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;
}