diff --git a/README.md b/README.md index 8c45a3a6858..e2e312c1b97 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ cgm-remote-monitor (a.k.a. NightScout) ====================================== - + [![Build Status](https://travis-ci.org/nightscout/cgm-remote-monitor.png)](https://travis-ci.org/nightscout/cgm-remote-monitor) [![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) diff --git a/bower.json b/bower.json index 634093da1f6..5461e62435a 100644 --- a/bower.json +++ b/bower.json @@ -3,6 +3,12 @@ "version": "0.0.1", "dependencies": { "d3": "3.4.3", + "jquery": "2.1.0", + "jQuery-Storage-API": "~1.7.2", + "tipsy-jmalonzo": "~1.0.1", + "jsSHA": "~1.5.0" + }, + "resolutions": { "jquery": "2.1.0" } } diff --git a/deploy.sh b/deploy.sh index 91801faae9e..4bc630de4ab 100755 --- a/deploy.sh +++ b/deploy.sh @@ -2,7 +2,7 @@ # ---------------------- # KUDU Deployment Script -# Version: 0.1.10 +# Version: 0.1.11 # ---------------------- # Helpers @@ -100,32 +100,22 @@ selectNodeVersion () { echo Handling node.js deployment. -# 1. Select node version +# 1. KuduSync +if [[ "$IN_PLACE_DEPLOYMENT" -ne "1" ]]; then + "$KUDU_SYNC_CMD" -v 50 -f "$DEPLOYMENT_SOURCE" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;deploy.sh" + exitWithMessageOnError "Kudu Sync failed" +fi + +# 2. Select node version selectNodeVersion -# 2. Install npm packages -if [ -e "$DEPLOYMENT_SOURCE/package.json" ]; then - # cd "$DEPLOYMENT_SOURCE" - eval $NPM_CMD set ca "" +# 3. Install npm packages +if [ -e "$DEPLOYMENT_TARGET/package.json" ]; then + cd "$DEPLOYMENT_TARGET" eval $NPM_CMD install --production exitWithMessageOnError "npm failed" - # cd - > /dev/null + cd - > /dev/null fi -if [ -e "$DEPLOYMENT_SOURCE/bower.json" ]; then - # cd "$DEPLOYMENT_SOURCE" - ./node_modules/.bin/bower install - exitWithMessageOnError "bower failed" - # cd - > /dev/null -fi - -# 3. KuduSync -# if [[ "$IN_PLACE_DEPLOYMENT" -ne "1" ]]; then -# "$KUDU_SYNC_CMD" -v 50 -f "$DEPLOYMENT_SOURCE" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;deploy.sh" -# exitWithMessageOnError "Kudu Sync failed" -# fi -# 3. KuduSync to Target -"$KUDU_SYNC_CMD" -v 500 -f "$DEPLOYMENT_SOURCE/" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;deploy.sh" -exitWithMessageOnError "Kudu Sync to Target failed" ################################################################################################################################## diff --git a/env.js b/env.js index cb2419c3316..1ec8ab1d334 100644 --- a/env.js +++ b/env.js @@ -17,7 +17,11 @@ function config ( ) { * static files over http. Default value is the included `static` * directory. */ + var software = require('./package.json'); + 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; env.mongo_collection = process.env.CUSTOMCONNSTR_mongo_collection || 'entries'; diff --git a/lib/api/entries/index.js b/lib/api/entries/index.js index 69d1f3c22b6..42e07854e1e 100644 --- a/lib/api/entries/index.js +++ b/lib/api/entries/index.js @@ -104,9 +104,10 @@ function configure (app, wares, entries) { es.pipeline(inputs( ), persist(done)); } - api.get('/', function(req, res, next) { + api.get('/entries', function(req, res, next) { // If "?count=" is present, use that number to decided how many to return. var query = req.query; + if (!query.count) { query.count = 10 }; entries.list(query, function(err, entries) { res.entries = entries; res.entries_err = err; @@ -115,7 +116,7 @@ function configure (app, wares, entries) { return; }, format_entries); - api.get('/current', function(req, res, next) { + api.get('/entries/current', function(req, res, next) { entries.list({count: 1}, function(err, records) { res.entries = records; res.entries_err = err; @@ -127,7 +128,7 @@ function configure (app, wares, entries) { // Allow previewing your post content, just echos everything you // posted back out. - api.post('/preview', function (req, res, next) { + api.post('/entries/preview', function (req, res, next) { req.persist_entries = false; next( ); return; @@ -135,7 +136,7 @@ function configure (app, wares, entries) { if (app.enabled('api')) { // Create and store new sgv entries - api.post('/', wares.verifyAuthorization, function (req, res, next) { + api.post('/entries/', wares.verifyAuthorization, function (req, res, next) { req.persist_entries = true; next( ); return; @@ -143,7 +144,7 @@ function configure (app, wares, entries) { } // Fetch one entry by id - api.get('/find/:id', function(req, res) { + api.get('/entries/:id', function(req, res) { entries.getEntry(function(err, entry) { if (err) res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); diff --git a/lib/api/index.js b/lib/api/index.js index 29c69115a0f..7fce2bbe2a9 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -8,6 +8,9 @@ function create (env, entries, settings) { var wares = require('../middleware/')(env); // set up express app with our options + app.set('name', env.name); + app.set('version', env.version); + app.set('units', env.DISPLAY_UNITS); // Only allow access to the API if API_SECRET is set on the server. app.disable('api'); if (env.api_secret) { @@ -15,7 +18,7 @@ function create (env, entries, settings) { app.enable('api'); } - app.set('title', 'Nightscout API v1'); + app.set('title', [app.get('name'), 'API', app.get('version')].join(' ')); // Start setting up routes if (app.enabled('api')) { @@ -24,11 +27,11 @@ function create (env, entries, settings) { } // Entries and settings - app.use('/entries', require('./entries/')(app, wares, entries)); - app.use('/settings', require('./settings/')(app, wares, settings)); + app.use('/', require('./entries/')(app, wares, entries)); + app.use('/', require('./settings/')(app, wares, settings)); // Status - app.use('/status', require('./status')(app, wares)); + app.use('/', require('./status')(app, wares)); return app; } diff --git a/lib/api/settings/index.js b/lib/api/settings/index.js index 815fecec0f2..c997024270a 100644 --- a/lib/api/settings/index.js +++ b/lib/api/settings/index.js @@ -19,44 +19,47 @@ function configure (app, wares, settings) { /**********\ * Settings \**********/ + // Handler for grabbing alias/profile + api.param('alias', function (req, res, next, alias) { + settings.alias(alias, function (err, profile) { + req.alias = profile; + next(err); + }); + }); + + // List settings available + api.get('/settings/', function(req, res) { + settings.list(function (err, profiles) { + return res.json(profiles); + }); + }); + // Fetch settings - api.get('/', function(req, res) { - settings.getSettings(function(err, settings) { - if (err) - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); - else - return res.json(settings); - }); + api.get('/settings/:alias', function(req, res) { + res.json(req.alias); }); function config_authed (app, api, wares, settings) { // Delete settings - api.delete('/', wares.verifyAuthorization, function(req, res) { - settings.remove(function ( ) { + api.delete('/settings/:alias', wares.verifyAuthorization, function(req, res) { + settings.remove(req.alias.alias, function ( ) { res.json({ }); }); }); - // Replace settings - api.put('/', wares.verifyAuthorization, function(req, res) { - // Retrieve the JSON formatted record. - var json = req.body; + function save_settings (req, res) { + var b = req.body; + b.alias = req.params.alias + settings.update(b, function (err, profile) { + res.json(profile); + }); + } + + // Update settings + api.put('/settings/:alias', wares.verifyAuthorization, save_settings); + api.post('/settings/:alias', wares.verifyAuthorization, save_settings); - // Send the new settings to mongodb. - settings.updateSettings(json, function(err, config) { - if (err) - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); - else { - // Add a warning to the outgoing status when HTTPS is not being used. - var warning = ''; - if (req.secure === false) - warning = 'WARNING: HTTPS is required to secure your data!'; - - res.json(config); - } - }); - }); } if (app.enabled('api')) { diff --git a/lib/api/status.js b/lib/api/status.js index 4be93c5ca09..6e524f5cf1f 100644 --- a/lib/api/status.js +++ b/lib/api/status.js @@ -6,11 +6,15 @@ function configure (app, wares) { ; api.use(wares.extensions([ - 'json', 'svg', 'csv', 'txt', 'png', 'html' + 'json', 'svg', 'csv', 'txt', 'png', 'html', 'js' ])); // Status badge/text/json - api.get('/', function (req, res, next) { - var status = {status: 'ok'}; + api.get('/status', function (req, res, next) { + var info = { status: 'ok' + , apiEnabled: app.enabled('api') + , units: app.get('units') + , version: app.get('version') + , name: app.get('name')}; var badge = 'http://img.shields.io/badge/Nightscout-OK-green'; return res.format({ html: function ( ) { @@ -22,11 +26,17 @@ function configure (app, wares) { svg: function ( ) { res.redirect(302, badge + '.svg'); }, + js: function ( ) { + var head = "this.serverSettings ="; + var body = JSON.stringify(info); + var tail = ';'; + res.send([head, body, tail].join(' ')); + }, text: function ( ) { res.send("STATUS OK"); }, json: function ( ) { - res.json(status); + res.json(info); } }); }); diff --git a/lib/middleware/verify-token.js b/lib/middleware/verify-token.js index 0a390a0b5ea..f7f875fa83a 100644 --- a/lib/middleware/verify-token.js +++ b/lib/middleware/verify-token.js @@ -5,12 +5,12 @@ function configure (env) { function verifyAuthorization(req, res, next) { // Retrieve the secret values to be compared. var api_secret = env.api_secret; - var secret = req.params.secret ? req.params.secret : req.header('API_SECRET'); + var secret = req.params.secret ? req.params.secret : req.header('api-secret'); // Return an error message if the authorization fails. var unauthorized = (typeof api_secret === 'undefined' || secret != api_secret); if (unauthorized) { - res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'API_SECRET Request Header is incorect or missing.'); + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'api-secret Request Header is incorrect or missing.'); } else { next(); } diff --git a/lib/pebble.js b/lib/pebble.js index a763b23760c..2eaef9c7379 100644 --- a/lib/pebble.js +++ b/lib/pebble.js @@ -21,35 +21,54 @@ function directionToTrend (direction) { function pebble (req, res) { var FORTY_MINUTES = 2400000; - var cgmData = [ ]; + + function requestMetric() { + var units = req.query.units; + if (units == "mmol") { + return true; + } + return false; + } + var useMetricBg = requestMetric(); + + function scaleBg(bg) { + if (useMetricBg) { + return (Math.round((bg / 18) * 10) / 10).toFixed(1); + } else + return bg; + } + function get_latest (err, results) { var now = Date.now(); - results.forEach(function(element, index, array) { - var next = null; - if (index + 1 < results.length) { - next = results[index + 1]; + results.forEach(function(element, index, array) { + var next = null; + if (index + 1 < results.length) { + next = results[index + 1]; + } + if (element) { + var obj = {}; + obj.sgv = scaleBg(element.sgv); + obj.bgdelta = (next ? (scaleBg(element.sgv) - scaleBg(next.sgv) ) : 0); + if (useMetricBg) { + obj.bgdelta = obj.bgdelta.toFixed(1); } - if (element) { - var obj = {}; - obj.sgv = element.sgv; - obj.bgdelta = (next ? (element.sgv - next.sgv ) : 0); - if ('direction' in element) { - obj.trend = directionToTrend(element.direction); - obj.direction = element.direction; - } - // obj.y = element.sgv; - // obj.x = element.date; - obj.datetime = element.date; - // obj.date = element.date.toString( ); - cgmData.push(obj); + if ('direction' in element) { + obj.trend = directionToTrend(element.direction); + obj.direction = element.direction; } - }); - var result = { status: [ {now:now}], bgs: cgmData.slice(0, 1) }; - res.setHeader('content-type', 'application/json'); - res.write(JSON.stringify(result)); - res.end( ); - // collection.db.close(); + // obj.y = element.sgv; + // obj.x = element.date; + obj.datetime = element.date; + // obj.date = element.date.toString( ); + cgmData.push(obj); + } + }); + var result = { status: [ {now:now}], bgs: cgmData.slice(0, 1) }; + res.setHeader('content-type', 'application/json'); + res.write(JSON.stringify(result)); + res.end( ); + // collection.db.close(); } req.entries.list({count: 2}, get_latest); } @@ -63,4 +82,3 @@ function configure (entries) { configure.pebble = pebble; module.exports = configure; - diff --git a/lib/settings.js b/lib/settings.js index c094d6bf704..e241078082e 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -3,10 +3,50 @@ var utils = require('./utils'); var ObjectID = require('mongodb').ObjectID; +function defaults ( ) { + var DEFAULT_SETTINGS_JSON = { + "units": "mg/dl" + }; // possible future settings: "theme": "subdued", "websockets": false, alertLow: 80, alertHigh: 180 + return DEFAULT_SETTINGS_JSON; +} + function configure (collection, storage) { var DEFAULT_SETTINGS_JSON = { "units": "mg/dl" - }; // possible future settings: "theme": "subdued", "websockets": false + }; + + function pop (fn) { + return function (err, results) { + if (err) fn(err); + fn(err, results.pop( )); + } + } + + function alias (alias, fn) { + return api( ).find({ alias: alias }).toArray(pop(fn)); + } + + function create (obj, fn) { + var result = merge(DEFAULT_SETTINGS_JSON, obj); + result.alias = obj.alias; + result.created_at = (new Date( )).toISOString( ); + api( ).insert(result, function (err, doc) { + fn(null, doc); + }); + } + + function lint (obj) { + var result = merge(DEFAULT_SETTINGS_JSON, json); + if (result.alias) return result; + } + + function list (fn) { + return api( ).find({ }, { alias: 1, nick: 1 }).toArray(pop(fn)); + } + + function remove (alias, fn) { + return api( ).remove({ alias: alias }, fn); + } var with_collection = storage.with_collection(collection); function getSettings (fn) { @@ -40,6 +80,34 @@ function configure (collection, storage) { return update; } + function update (json, fn) { + var updated = (new Date( )).toISOString( ); + alias(json.alias, function last (err, older) { + if (err) { return fn(err); } + var result; + if (older && older._id) { + // result = merge(older, json); + result = json; + result.updated_at = updated; + var deltas = Object.keys(result); + if (deltas.length < 1) { + fn("Bad Keys"); + return; + } + api( ).update( + { '_id' : new ObjectID(older._id) }, + { $set: result }, + function (err, res) { + // Return to the calling function to display our success. + fn(err, res); + } + ); + } else { + create(json, fn); + } + }); + } + function updateSettings (json, fn) { getSettings(function last (err, older) { var result = merge(older, json); @@ -73,19 +141,25 @@ function configure (collection, storage) { }); } - function remove (fn) { + function clear (fn) { with_collection(function (err, collection) { collection.remove({ }, fn); }); } function api ( ) { - storage.pool.db.collection(collection); + return storage.pool.db.collection(collection); } api.getSettings = getSettings; api.updateSettings = updateSettings; api.remove = remove; + api.clear = clear; + api.list = list; + api.create = create; + api.lint = lint; + api.alias = alias; + api.update = update; return api; } module.exports = configure; diff --git a/lib/websocket.js b/lib/websocket.js index 49dd8824db6..9ac00c2d7e8 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -142,7 +142,7 @@ function loadData() { actual.sort(function(a, b) { return a.x - b.x; }); - + // sgv less than or equal to 10 means error code // or warm up period code, so ignore actual = actual.filter(function (a) { @@ -195,6 +195,8 @@ 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) { @@ -242,4 +244,3 @@ function loadData() { function log10(val) { return Math.log(val) / Math.LN10; } module.exports = websocket; - diff --git a/package.json b/package.json index ffdaf1e394c..8bead2fed96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Nightscout", - "version": "0.1.20140708", + "version": "0.3.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/server.js b/server.js index 8921d5f9c93..8d8e460fece 100644 --- a/server.js +++ b/server.js @@ -46,14 +46,16 @@ var appInfo = software.name + ' ' + software.version; app.set('title', appInfo); app.enable('trust proxy'); // Allows req.secure test on heroku https connections. -if (env.api_secret) { - console.log("API_SECRET", env.api_secret); -} +//if (env.api_secret) { +// console.log("API_SECRET", env.api_secret); +//} app.use('/api/v1', api); // pebble data app.get('/pebble', pebble(entries)); +//app.get('/package.json', software); + // define static server var staticFiles = express.static(env.static_files, {maxAge: THIRTY_DAYS * 1000}); diff --git a/static/clock.html b/static/clock.html new file mode 100644 index 00000000000..e28fc9f0ee2 --- /dev/null +++ b/static/clock.html @@ -0,0 +1,69 @@ + + + + + Nightscout BG NOW + + + + + +
+

+
+ + + + + diff --git a/static/css/drawer.css b/static/css/drawer.css new file mode 100644 index 00000000000..5791834f9c3 --- /dev/null +++ b/static/css/drawer.css @@ -0,0 +1,202 @@ +#drawer { + 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: 200px; + top: 0; + z-index: 1; +} +#drawer i { + opacity: 0.6; +} +#about { + margin-top: 1em; +} +#about div { + font-size: 0.75em; +} +#about .links { + margin-top: 0.5em; +} +#about a { + color: #fff; +} + +form { + margin: 0; + padding: 0 10px; +} + +.serverSettings { + display: none; +} + +fieldset { + margin: 0.5em 0 1em 0; +} +legend { + font-size: 1.1em; +} +dl { + margin: 0 0 1em 0; +} +dl:last-child { + margin-bottom: 0; +} +dd { + margin-left: 0; +} + +dd.numbers input { + text-align: right; + width: 50px; +} +dd.numbers label { + display: block; + float: left; + width: 50px; +} + +h1, legend, +#about .appName, +#about .version { + font-weight: bold; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); +} +#about .appName { + font-size: 1em; +} +#about .version { + font-size: 1.25em; + margin-left: 0.25em; +} + +#toolbar { + background: url(/images/logo2.png) no-repeat 3px 3px #333; + border-bottom: 1px solid #999; + top: 0; + margin: 0; + height: 44px; +} + +#toolbar h1 { + color: #ccc; + font-size: 16px; + margin-top: 0; + margin-left: 42px; + padding: 10px; +} +#buttonbar { + padding: 0; + float: right; + height: 44px; + width: 180px; + opacity: 0.75; + vertical-align: middle; +} +#buttonbar div { + border-radius: 5px; + float: left; + height: 44px; + width: 44px; +} +#buttonbar a, +#buttonbar i { + color: #ccc; + font-size: 16px; + height: 44px; + line-height: 44px; +} +#buttonbar a { + float: left; + text-decoration: none; + width: 44px; +} +#buttonbar i { + padding-left: 12px; +} + +.icon-battery-25, +.icon-battery-50, +.icon-battery-75, +.icon-battery-100, +.icon-angle-double-up { + font-size: 21px !important; + padding-left: 9px !important; +} + +#showToolbar, +#notification { + display: none; + font-size: 16px; + position: absolute; +} +#showToolbar { + opacity: 0.5; + text-align: center; + top: 0px; + width: 100%; + z-index: 98; +} +#showToolbar a { + border: solid #fff 1px; + border-top: 0; + color: #fff; + margin: 0 auto; + padding: 2px; +} +#notification, +#notification a { + overflow: hidden; +} +#notification { + opacity: 0.75; + top: 4px; + z-index: 99; +} +#notification.info a { + background: #00f; + color: #fff; +} +#notification.warn a { + background: #cc0; + color: #000; +} +#notification.success a { + background: #090; + color: #fff; +} +#notification.urgent a { + background: #c00; + color: #fff; +} +#notification a { + background: #c00; + border-radius: 5px; + color: #fff; + display: block; + height: 20px; + padding: 0.5em; + text-decoration: none; +} +#notification span { + margin-right: 0.25em; +} +#notification i { + float: right; + opacity: 0.5; + vertical-align: top; +} + +.experiments a { + color: #fff; + font-size: 1.5em; +} diff --git a/static/css/main.css b/static/css/main.css index db53966e55f..2b31e78b565 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,56 +1,95 @@ @import url("//fonts.googleapis.com/css?family=Ubuntu:300,400,500,700,300italic,400italic,500italic,700italic"); @import url("//fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,300,400,600,700,800"); +@import url("/glyphs/css/fontello.css"); -/* Large desktop */ -@media (min-width: 980px) { - body { - padding-top: 20px; - } - .linediv-l { - border-right: 1px white solid; - } - .linedic-r { - border-left: 1px white solid; - } -} -html { - height: 75%; +html, body { + height: 100%; + margin: 0; + padding: 0; } - body { - font-family: 'Open Sans', Helvetica, Arial, sans-serif; - fill: #fff; - background: #000; - color: #808080; + font-family: 'Open Sans', Helvetica, Arial, sans-serif; + fill: #fff; + background: #000; + color: #808080; } -.heading, .subheading { - font-family: 'Ubuntu', Helvetica, Arial, sans-serif; - text-align: center; + +.container { + bottom: 0; + display: block; + height: 100%; + left: 0; + margin: 0; + padding: 0; + top: 45px; + width: 100%; + z-index: 2; } -.axis path, -.axis line { - fill: none; - stroke: #ffffff; - shape-rendering: crispEdges; +.status { + font-family: 'Ubuntu', Helvetica, Arial, sans-serif; + height: 180px; + vertical-align: middle; } -#chartContainer { - overflow: hidden; - width: 100%; - height: 100%; +.bgStatus { + float: right; + font-size: 900%; + margin-right: 25px; + text-align: center; + text-decoration: line-through; +} +.bgStatus.current { + text-decoration: none; +} +.currentDirection { + font-weight: normal; + text-decoration: none !important; +} +.time { + font-size: 600%; + margin-left: 25px; + padding-top: 15px; +} +.timeOther { + font-size: 25%; + margin-left: 32px; +} +#lastEntry { + background: #999; + border-radius: 5px; + color: #000; + padding: 0.25em; +} +#lastEntry.warn { + background: #cc0; +} +#lastEntry.urgent { + background: #c00; + color: #000; } +#chartContainer { + background: #111; + bottom:0; + display: block; + height:auto; + left:0; + position:absolute; + right:0; + top: calc(180px + 45px); +} #chartContainer svg { - width: 100%; - height: 100%; + height: calc(100vh - (180px + 45px)); + width: 100%; } -.container { - overflow: hidden; - height: auto; - display: block; +.axis path, +.axis line { + fill: none; + stroke: #ffffff; + shape-rendering: crispEdges; } .open-right, .open-left { @@ -67,90 +106,177 @@ body { fill-opacity: 0; } -@media (max-width: 815px) { - body { - font-size: 80%; - } -} -@media (max-width: 768px) { - body { - font-size: 60%; - } +#noButton { + padding: 2px; +} - #chartContainer { - font-size: 150%; - } +#bgButton, +#silenceBtn { + z-index: 999; } -@media (max-width: 510px) { - body { - font-size: 50%; - } +#bgButton { + background: #ff2035; + border: 2px solid #DDD; + border-right: 2px solid #ccc; + border-bottom: 2px solid #ccc; + border-radius: 5px; + box-shadow: 2px 4px 6px #ddd; + color: #000000; + cursor: default; + font-size: 75%; + margin-top: 15px; + margin-right: 37px; + width: auto; + + user-select: none; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + -moz-box-shadow: 2px 2px 4px #ddd; + -webkit-box-shadow: 2px 2px 4px #ddd; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; } -@media (max-width: 480px) { - body { - font-size: 40%; - } - #chartContainer { - font-size: 150%; - } +.bgButton:active { + background: #850406; + border: 2px solid #999; + box-shadow: none; + + -moz-box-shadow: none; + -webkit-box-shadow: none; } -[class*='span']{float:left;min-height:1px;} +.button { + text-align: center; + background: #ababab; + margin: 10px auto; +} -.span1{width: 20%; float: left; padding-top: 3%; line-height: 50%;} +#silenceBtn, #silenceBtn * { + font-size: 70%; +} -.span2{margin-left: 35%; float: right;} +.alarms { + display: none; +} -#noButton { - padding: 2px; +#testAlarms { + font-size: 20%; } -#bgButton { - background: #ff2035; - color: #000000; - width: auto; - border: 2px solid #DDD; - border-right: 2px solid #ccc; - border-bottom: 2px solid #ccc; - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - box-shadow: 2px 4px 6px #ddd; - -moz-box-shadow: 2px 2px 4px #ddd; - -webkit-box-shadow: 2px 2px 4px #ddd; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - cursor: default; - margin-right: 37px; + +/* +@media (max-width: 1060px) { + body { + font-size: 70%; + } + + #toolbar, #buttonbar { + font-size: 100%; + } + + .status { + height: 130px; + } + + #chartContainer { + top: calc(130px + 45px); + } + #chartContainer svg { + height: calc(100vh - (130px + 45px)); + } } +*/ -.bgButton:active { - background: #850406; - border: 2px solid #999; - box-shadow: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; +/* Large desktop */ +@media (min-width: 980px) { + .content { + padding-top: 20px; + } + .linediv-l { + border-right: 1px white solid; + } + .linedic-r { + border-left: 1px white solid; + } } -.button { - text-align: center; - background: #ababab; - margin: 10px auto; +@media (max-width: 768px) { + #chartContainer { + font-size: 150%; + } + .bgStatus { + width: 50%; + } } -#silenceBtn, #silenceBtn * { - font-size: 70%; +@media (max-width: 700px) { + body { + font-size: 70%; + } + + #notification { + margin-top: 148px; + } + + .status { + text-align: center; + margin-bottom: 0; + height: 152px; + } + + /* Unremark to test alert/silence button + menu. */ + /* #noButton {display:none;} + #bgButton {display:block;} */ + + .bgStatus { + float: none; + margin: 0 auto; + text-align: center; + width: 100vw; + } + #bgButton { + font-size: 120%; + height: 142px; + margin-top: 1vw; + width: 98vw; + } + .dropdown-menu { + font-size: 60% !important; + margin-left: 2vw; + width: 96vw; + } + .time { + margin-bottom: 0.1em; + margin-top: -1em; + margin-left: 0; + width: 100%; + } + #currentTime { + display: inline; + font-size: 25%; + font-weight: bold; + } + .timeOther { + display: inline; + margin-left: auto; + } + + #chartContainer { + top: calc(152px + 45px); + } + #chartContainer svg { + height: calc(100vh - (152px + 45px)); + } } -#testAlarms { - font-size: 20%; - color: blue; - text-decoration: none; +@media (max-width: 480px) { + #chartContainer { + font-size: 150%; + } } diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100755 index 260dc4050bb..00000000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/glyphs/LICENSE.txt b/static/glyphs/LICENSE.txt new file mode 100644 index 00000000000..1970f449a11 --- /dev/null +++ b/static/glyphs/LICENSE.txt @@ -0,0 +1,57 @@ +Font license info + + +## MFG Labs + + Copyright (C) 2012 by Daniel Bruce + + Author: MFG Labs + License: SIL (http://scripts.sil.org/OFL) + Homepage: http://www.mfglabs.com/ + + +## Font Awesome + + Copyright (C) 2012 by Dave Gandy + + Author: Dave Gandy + License: SIL () + Homepage: http://fortawesome.github.com/Font-Awesome/ + + +## Typicons + + (c) Stephen Hutchings 2012 + + Author: Stephen Hutchings + License: SIL (http://scripts.sil.org/OFL) + Homepage: http://typicons.com/ + + +## Web Symbols + + Copyright (c) 2011 by Just Be Nice studio. All rights reserved. + + Author: Just Be Nice studio + License: SIL (http://scripts.sil.org/OFL) + Homepage: http://www.justbenicestudio.com/ + + +## Entypo + + Copyright (C) 2012 by Daniel Bruce + + Author: Daniel Bruce + License: SIL (http://scripts.sil.org/OFL) + Homepage: http://www.entypo.com + + +## Elusive + + Copyright (C) 2013 by Aristeides Stathopoulos + + Author: Aristeides Stathopoulos + License: SIL (http://scripts.sil.org/OFL) + Homepage: http://aristeides.com/ + + diff --git a/static/glyphs/README.txt b/static/glyphs/README.txt new file mode 100644 index 00000000000..43e23f2833b --- /dev/null +++ b/static/glyphs/README.txt @@ -0,0 +1,75 @@ +This webfont is generated by http://fontello.com open source project. + + +================================================================================ +Please, note, that you should obey original font licences, used to make this +webfont pack. Details available in LICENSE.txt file. + +- Usually, it's enough to publish content of LICENSE.txt file somewhere on your + site in "About" section. + +- If your project is open-source, usually, it will be ok to make LICENSE.txt + file publically available in your repository. + +- Fonts, used in Fontello, don't require to make clickable links on your site. + But any kind of additional authors crediting is welcome. +================================================================================ + + +Comments on archive content +--------------------------- + +- /font/* - fonts in different formats + +- /css/* - different kinds of css, for all situations. Should be ok with + twitter bootstrap. Also, you can skip style and assign icon classes + directly to text elements, if you don't mind about IE7. + +- demo.html - demo file, to show your webfont content + +- LICENSE.txt - license info about source fonts, used to build your one. + +- config.json - keeps your settings. You can import it back to fontello anytime, + to continue your work + + +Why so many CSS files ? +----------------------- + +Because we like to fit all your needs :) + +- basic file, .css - is usually enougth, in contains @font-face + and character codes definition + +- *-ie7.css - if you need IE7 support, but still don't wish to put char codes + directly into html + +- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face + rules, but still wish to benefit of css generation. That can be very + convenient for automated assets build systems. When you need to update font - + no needs to manually edit files, just override old version with archive + content. See fontello source codes for example. + +- *-embedded.css - basic css file, but with embedded WOFF font, to avoid + CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. + We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` + server headers. But if you ok with dirty hack - this file is for you. Note, + that data url moved to separate @font-face to avoid problems with + + + + + + + + +
+

+ fontello + font demo +

+ +
+
+
+
icon-help-circled0xe800
+
icon-angle-double-up0xe801
+
icon-angle-double-down0xe802
+
icon-menu0xe803
+
+
+
icon-battery-250xe804
+
icon-battery-500xe805
+
icon-cog0xe806
+
icon-battery-750xe807
+
+
+
icon-battery-1000xe808
+
icon-cancel-circled0xe809
+
icon-volume0xe80a
+
+
+ + + \ No newline at end of file diff --git a/static/glyphs/font/fontello.eot b/static/glyphs/font/fontello.eot new file mode 100644 index 00000000000..1d70fcab912 Binary files /dev/null and b/static/glyphs/font/fontello.eot differ diff --git a/static/glyphs/font/fontello.svg b/static/glyphs/font/fontello.svg new file mode 100644 index 00000000000..ed019eabf63 --- /dev/null +++ b/static/glyphs/font/fontello.svg @@ -0,0 +1,22 @@ + + + +Copyright (C) 2014 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/glyphs/font/fontello.ttf b/static/glyphs/font/fontello.ttf new file mode 100644 index 00000000000..3b68bf5eade Binary files /dev/null and b/static/glyphs/font/fontello.ttf differ diff --git a/static/glyphs/font/fontello.woff b/static/glyphs/font/fontello.woff new file mode 100644 index 00000000000..6a19ec216c1 Binary files /dev/null and b/static/glyphs/font/fontello.woff differ diff --git a/static/images/logo1.png b/static/images/logo1.png new file mode 100644 index 00000000000..52992336c8d Binary files /dev/null and b/static/images/logo1.png differ diff --git a/static/images/logo2.png b/static/images/logo2.png new file mode 100755 index 00000000000..350520441c5 Binary files /dev/null and b/static/images/logo2.png differ diff --git a/static/images/round1.png b/static/images/round1.png new file mode 100644 index 00000000000..689be0fc337 Binary files /dev/null and b/static/images/round1.png differ diff --git a/static/index.html b/static/index.html index 8e81aab0d9d..e36acd1a96e 100644 --- a/static/index.html +++ b/static/index.html @@ -1,27 +1,40 @@ - + NightScout - - + + + + + -
-
-
-
---
-
- --- -
-
-
-
- --- - -✓♪ +
+
+ + + + +
+

Nightscout

+
+
+ +
+
+ +
+ +
+
+
+
+ --- + -
- + +
+
---
+
+ --- +
+
-
-
+
+
+ +
+
+
+ Settings +
+
Units
+

+
+
+
+
Night Mode
+
+
+
+
Custom Title
+
+
+
+ + + +
+ Experiments +
+ Hamburger?
+ + +
+
+ +
+ About +
+
version
+ +
+
+
+
- - + +
- - - - - + + + + + + + + + diff --git a/static/js/client.js b/static/js/client.js index 5a2a6fb4be2..13c06dd4e02 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -1,14 +1,14 @@ -(function() { +(function () { "use strict"; var retrospectivePredictor = true, latestSGV, treatments, - padding = {top: 20, right: 10, bottom: 80, left: 10}, - opacity = {current: 1, DAY: 1, NIGHT: 0.8}, + padding = { top: 20, right: 10, bottom: 30, left: 10 }, + opacity = {current: 1, DAY: 1, NIGHT: 0.5}, now = Date.now(), data = [], - dateFn = function (d) { return new Date(d.date)}, + dateFn = function (d) { return new Date(d.date) }, xScale, xScale2, yScale, yScale2, xAxis, yAxis, xAxis2, yAxis2, prevChartWidth = 0, @@ -25,12 +25,21 @@ THIRTY_MINS_IN_MS = 1800000, FORTY_TWO_MINS_IN_MS = 2520000, FOCUS_DATA_RANGE_MS = 12600000, // 3.5 hours of actual data + FORMAT_TIME = '%I:%M%', //alternate format '%H:%M' audio = document.getElementById('audio'), alarmInProgress = false, currentAlarmType = null, alarmSound = 'alarm.mp3', - urgentAlarmSound = 'alarm2.mp3'; - + urgentAlarmSound = 'alarm2.mp3', + WIDTH_TIME_HIDDEN = 600, + MINUTES_SINCE_LAST_UPDATE_WARN = 10, + MINUTES_SINCE_LAST_UPDATE_URGENT = 20; + + // 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]; + } // create svg and g to contain the chart contents var charts = d3.select('#chartContainer').append('svg') @@ -58,6 +67,23 @@ context.append('g') .attr('class', 'y axis'); + + // Remove leading zeros from the time (eg. 08:40 = 8:40) & lowercase the am/pm + function formatTime(time) { + time = d3.time.format(FORMAT_TIME)(time); + time = time.replace(/^0/, '').toLowerCase(); + return time; + } + + // lixgbg: Convert mg/dL BG value to metric mmol + function scaleBg(bg) { + if (browserSettings.units == "mmol") { + return (Math.round((bg / 18) * 10) / 10).toFixed(1); + } else { + return bg; + } + } + // initial setup of chart when data is first made available function initializeCharts() { @@ -66,13 +92,13 @@ .domain(d3.extent(data, function (d) { return d.date; })); yScale = d3.scale.log() - .domain([30, 420]); + .domain([scaleBg(30), scaleBg(420)]); xScale2 = d3.time.scale() .domain(d3.extent(data, function (d) { return d.date; })); yScale2 = d3.scale.log() - .domain([36, 420]); + .domain([scaleBg(36), scaleBg(420)]); xAxis = d3.svg.axis() .scale(xScale) @@ -82,7 +108,7 @@ yAxis = d3.svg.axis() .scale(yScale) .tickFormat(d3.format('d')) - .tickValues([40, 60, 80, 120, 180, 300, 400]) + .tickValues(tickValues) .orient('left'); xAxis2 = d3.svg.axis() @@ -93,7 +119,7 @@ yAxis2 = d3.svg.axis() .scale(yScale2) .tickFormat(d3.format('d')) - .tickValues([40, 60, 80, 120, 180, 300, 400]) + .tickValues(tickValues) .orient('right'); // setup a brush @@ -136,7 +162,7 @@ // update the opacity of the context data points to brush extent context.selectAll('circle') .data(data) - .style('opacity', function(d) {return 1;} ); + .style('opacity', function (d) { return 1; }); } function brushEnded() { @@ -176,7 +202,16 @@ // get slice of data so that concatenation of predictions do not interfere with subsequent updates var focusData = data.slice(); + if (alarmInProgress) { + if ($(window).width() > WIDTH_TIME_HIDDEN) { + $(".time").show(); + } else { + $(".time").hide(); + } + } + var element = document.getElementById('bgButton').hidden == ''; + var nowDate = new Date(brushExtent[1] - THIRTY_MINS_IN_MS); // predict for retrospective data @@ -191,7 +226,7 @@ focusData = focusData.concat(prediction); var focusPoint = nowData[nowData.length - 1]; $('.container .currentBG') - .text((focusPoint.sgv)) + .text(focusPoint.sgv) .css('text-decoration','line-through'); $('.container .currentDirection') .html(focusPoint.direction) @@ -201,17 +236,17 @@ .css('text-decoration','none'); } $('#currentTime') - .text(d3.time.format('%I:%M%p')(new Date(brushExtent[1] - THIRTY_MINS_IN_MS))) + .text(formatTime(new Date(brushExtent[1] - THIRTY_MINS_IN_MS))) .css('text-decoration','line-through'); } else if (retrospectivePredictor) { // if the brush comes back into the current time range then it should reset to the current time and sg var dateTime = new Date(now); nowDate = dateTime; $('#currentTime') - .text(d3.time.format('%I:%M%p')(dateTime)) + .text(formatTime(dateTime)) .css('text-decoration','none'); $('.container .currentBG') - .text(latestSGV.y) + .text(scaleBg(latestSGV.y)) .css('text-decoration','none'); $('.container .currentDirection') .html(latestSGV.direction); @@ -220,7 +255,7 @@ xScale.domain(brush.extent()); // bind up the focus chart data to an array of circles - // selects all our data into data and uses date function to get current max date + // selects all our data into data and uses date function to get current max date var focusCircles = focus.selectAll('circle').data(focusData, dateFn); // if already existing then transition each circle to its new position @@ -228,14 +263,14 @@ .transition() .duration(UPDATE_TRANS_MS) .attr('cx', function (d) { return xScale(d.date); }) - .attr('cy', function (d) { return yScale(d.sgv); }) - .attr('fill', function (d) { return d.color; }); + .attr('cy', function (d) { return yScale(d.sgv); }) + .attr('fill', function (d) { return d.color; }); // if new circle then just display focusCircles.enter().append('circle') .attr('cx', function (d) { return xScale(d.date); }) - .attr('cy', function (d) { return yScale(d.sgv); }) - .attr('fill', function (d) { return d.color; }) + .attr('cy', function (d) { return yScale(d.sgv); }) + .attr('fill', function (d) { return d.color; }) .attr('r', 3); focusCircles.exit() @@ -248,14 +283,14 @@ var bubbleSize = prevChartWidth < 400 ? 4 : (prevChartWidth < 600 ? 3 : 2); focus.selectAll('circle') .data(treatments) - .each(function (d) { drawTreatment(d, bubbleSize, true)}); + .each(function (d) { drawTreatment(d, bubbleSize, true) }); // transition open-top line to correct location focus.select('.open-top') .attr('x1', xScale2(brush.extent()[0])) - .attr('y1', yScale(30)) + .attr('y1', yScale(scaleBg(30))) .attr('x2', xScale2(brush.extent()[1])) - .attr('y2', yScale(30)); + .attr('y2', yScale(scaleBg(30))); // transition open-left line to correct location focus.select('.open-left') @@ -275,9 +310,9 @@ .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale(nowDate)) - .attr('y1', yScale(36)) + .attr('y1', yScale(scaleBg(36))) .attr('x2', xScale(nowDate)) - .attr('y2', yScale(420)); + .attr('y2', yScale(scaleBg(420))); // update x axis focus.select('.x.axis') @@ -363,9 +398,9 @@ focus.append('line') .attr('class', 'now-line') .attr('x1', xScale(new Date(now))) - .attr('y1', yScale(36)) + .attr('y1', yScale(scaleBg(36))) .attr('x2', xScale(new Date(now))) - .attr('y2', yScale(420)) + .attr('y2', yScale(scaleBg(420))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -373,9 +408,9 @@ focus.append('line') .attr('class', 'high-line') .attr('x1', xScale(dataRange[0])) - .attr('y1', yScale(180)) + .attr('y1', yScale(scaleBg(180))) .attr('x2', xScale(dataRange[1])) - .attr('y2', yScale(180)) + .attr('y2', yScale(scaleBg(180))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -383,9 +418,9 @@ focus.append('line') .attr('class', 'low-line') .attr('x1', xScale(dataRange[0])) - .attr('y1', yScale(80)) + .attr('y1', yScale(scaleBg(80))) .attr('x2', xScale(dataRange[1])) - .attr('y2', yScale(80)) + .attr('y2', yScale(scaleBg(80))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -409,9 +444,9 @@ context.append('line') .attr('class', 'now-line') .attr('x1', xScale(new Date(now))) - .attr('y1', yScale2(36)) + .attr('y1', yScale2(scaleBg(36))) .attr('x2', xScale(new Date(now))) - .attr('y2', yScale2(420)) + .attr('y2', yScale2(scaleBg(420))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -419,9 +454,9 @@ context.append('line') .attr('class', 'high-line') .attr('x1', xScale(dataRange[0])) - .attr('y1', yScale2(180)) + .attr('y1', yScale2(scaleBg(180))) .attr('x2', xScale(dataRange[1])) - .attr('y2', yScale2(180)) + .attr('y2', yScale2(scaleBg(180))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -429,9 +464,9 @@ context.append('line') .attr('class', 'low-line') .attr('x1', xScale(dataRange[0])) - .attr('y1', yScale2(80)) + .attr('y1', yScale2(scaleBg(80))) .attr('x2', xScale(dataRange[1])) - .attr('y2', yScale2(80)) + .attr('y2', yScale2(scaleBg(80))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -476,27 +511,27 @@ .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale(currentBrushExtent[0])) - .attr('y1', yScale(180)) + .attr('y1', yScale(scaleBg(180))) .attr('x2', xScale(currentBrushExtent[1])) - .attr('y2', yScale(180)); + .attr('y2', yScale(scaleBg(180))); // transition low line to correct location focus.select('.low-line') .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale(currentBrushExtent[0])) - .attr('y1', yScale(80)) + .attr('y1', yScale(scaleBg(80))) .attr('x2', xScale(currentBrushExtent[1])) - .attr('y2', yScale(80)); + .attr('y2', yScale(scaleBg(80))); // transition open-top line to correct location focus.select('.open-top') .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale2(currentBrushExtent[0])) - .attr('y1', yScale(30)) + .attr('y1', yScale(scaleBg(30))) .attr('x2', xScale2(currentBrushExtent[1])) - .attr('y2', yScale(30)); + .attr('y2', yScale(scaleBg(30))); // transition open-left line to correct location focus.select('.open-left') @@ -521,18 +556,18 @@ .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale2(dataRange[0])) - .attr('y1', yScale2(180)) + .attr('y1', yScale2(scaleBg(180))) .attr('x2', xScale2(dataRange[1])) - .attr('y2', yScale2(180)); + .attr('y2', yScale2(scaleBg(180))); // transition low line to correct location context.select('.low-line') .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale2(dataRange[0])) - .attr('y1', yScale2(80)) + .attr('y1', yScale2(scaleBg(80))) .attr('x2', xScale2(dataRange[1])) - .attr('y2', yScale2(80)); + .attr('y2', yScale2(scaleBg(80))); } } @@ -543,9 +578,9 @@ .transition() .duration(UPDATE_TRANS_MS) .attr('x1', xScale2(new Date(now))) - .attr('y1', yScale2(36)) + .attr('y1', yScale2(scaleBg(36))) .attr('x2', xScale2(new Date(now))) - .attr('y2', yScale2(420)); + .attr('y2', yScale2(scaleBg(420))); // only if a user brush is not active, update brush and focus chart with recent data // else, just transition brush @@ -568,16 +603,16 @@ contextCircles.transition() .duration(UPDATE_TRANS_MS) .attr('cx', function (d) { return xScale2(d.date); }) - .attr('cy', function (d) { return yScale2(d.sgv); }) - .attr('fill', function (d) { return d.color; }) - .style('opacity', function (d) { return highlightBrushPoints(d) }); + .attr('cy', function (d) { return yScale2(d.sgv); }) + .attr('fill', function (d) { return d.color; }) + .style('opacity', function (d) { return highlightBrushPoints(d) }); // if new circle then just display contextCircles.enter().append('circle') - .attr('cx', function (d) { return xScale2(d.date); }) - .attr('cy', function (d) { return yScale2(d.sgv); }) - .attr('fill', function (d) { return d.color; }) - .style('opacity', function (d) { return highlightBrushPoints(d) }) + .attr('cx', function (d) { return xScale2(d.date); }) + .attr('cy', function (d) { return yScale2(d.sgv); }) + .attr('fill', function (d) { return d.color; }) + .style('opacity', function (d) { return highlightBrushPoints(d) }) .attr('r', 2); contextCircles.exit() @@ -599,11 +634,11 @@ var silenceDropdown = new Dropdown(".dropdown-menu"); - $('#bgButton').click(function(e) { + $('#bgButton').click(function (e) { silenceDropdown.open(e); }); - $("#silenceBtn").find("a").click(function() { + $("#silenceBtn").find("a").click(function () { stopAlarm(true, $(this).data("snooze-time")); }); @@ -618,13 +653,16 @@ socket.on('now', function (d) { now = d; var dateTime = new Date(now); - $('#currentTime').text(d3.time.format('%I:%M%p')(dateTime)); + // 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 - if (opacity.current != opacity.NIGHT && (dateTime.getHours() > 21 || dateTime.getHours() < 7 )) { - $('body').css({'opacity': opacity.NIGHT}); - } else { - $('body').css({'opacity': opacity.DAY}); + if (browserSettings.nightMode) { + if (opacity.current != opacity.NIGHT && (dateTime.getHours() > 21 || dateTime.getHours() < 7)) { + $('body').css({ 'opacity': opacity.NIGHT }); + } else { + $('body').css({ 'opacity': opacity.DAY }); + } } }); @@ -651,6 +689,9 @@ 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; } $('#lastEntry').text(timeAgo(secsSinceLast)).toggleClass('current', secsSinceLast < 10 * 60); @@ -658,9 +699,10 @@ $('.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: obj.y, direction: obj.direction, color: 'grey'} }); - data = data.concat(d[1].map(function (obj) { return { date: new Date(obj.x), sgv: obj.y, color: 'blue'} })); - data = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), sgv: obj.y, color: 'red'} })); + 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 = data.concat(d[2].map(function (obj) { return { date: new Date(obj.x), sgv: scaleBg(obj.y), color: 'red'} })); + treatments = d[3]; if (!isInitialData) { isInitialData = true; @@ -679,21 +721,21 @@ socket.on('connect', function () { console.log('Client connected to server.') }); - socket.on('alarm', function() { + socket.on('alarm', function () { console.log("Alarm raised!"); currentAlarmType = 'alarm'; generateAlarm(alarmSound); brushInProgress = false; updateChart(false); }); - socket.on('urgent_alarm', function() { + socket.on('urgent_alarm', function () { console.log("Urgent alarm raised!"); currentAlarmType = 'urgent_alarm'; generateAlarm(urgentAlarmSound); brushInProgress = false; updateChart(false); }); - socket.on('clear_alarm', function() { + socket.on('clear_alarm', function () { if (alarmInProgress) { console.log('clearing alarm'); stopAlarm(); @@ -704,7 +746,7 @@ $('#testAlarms').click(function(event) { d3.select('.audio.alarms audio').each(function (data, i) { var audio = this; - audio.play(); + playAlarm(audio); setTimeout(function() { audio.pause(); }, 4000); @@ -717,7 +759,7 @@ var selector = '.audio.alarms audio.' + file; d3.select(selector).each(function (d, i) { var audio = this; - audio.play(); + playAlarm(audio); $(this).addClass('playing'); }); var element = document.getElementById('bgButton'); @@ -725,6 +767,19 @@ var element1 = document.getElementById('noButton'); element1.hidden = 'true'; $('.container .currentBG').text(); + + if ($(window).width() <= WIDTH_TIME_HIDDEN) { + $(".time").hide(); + } + } + + function playAlarm(audio) { + // ?mute=true disables alarms to testers. + if (querystring.mute != "true") { + audio.play(); + } else { + showNotification("Alarm is muted per your request. (?mute=true)"); + } } function stopAlarm(isClient, silenceTime) { @@ -739,6 +794,8 @@ $(this).removeClass('playing'); }); + $(".time").show(); + // only emit ack if client invoke by button press if (isClient) { socket.emit('ack', currentAlarmType || 'alarm', silenceTime); @@ -753,7 +810,10 @@ DAY = 86400, WEEK = 604800; - if (offset <= MINUTE) parts = { lablel: 'now' }; + //offset = (MINUTE * MINUTES_SINCE_LAST_UPDATE_WARN) + 60 + //offset = (MINUTE * MINUTES_SINCE_LAST_UPDATE_URGENT) + 60 + + if (offset <= MINUTE) parts = { label: 'now' }; if (offset <= MINUTE * 2) parts = { label: '1 min ago' }; else if (offset < (MINUTE * 60)) parts = { value: Math.round(Math.abs(offset / MINUTE)), label: 'mins' }; else if (offset < (HOUR * 2)) parts = { label: '1 hr ago' }; @@ -763,6 +823,20 @@ else if (offset < (WEEK * 52)) parts = { value: Math.round(Math.abs(offset / WEEK)), label: 'week' }; else parts = { label: 'a long time ago' }; + if (offset > (MINUTE * MINUTES_SINCE_LAST_UPDATE_URGENT)) { + var lastEntry = $("#lastEntry"); + lastEntry.removeClass("warn"); + lastEntry.addClass("urgent"); + + $(".bgStatus").removeClass("current"); + } else if (offset > (MINUTE * MINUTES_SINCE_LAST_UPDATE_WARN)) { + var lastEntry = $("#lastEntry"); + lastEntry.removeClass("urgent"); + lastEntry.addClass("warn"); + } else { + $(".bgStatus").addClass("current"); + } + if (parts.value) return parts.value + ' ' + parts.label + ' ago'; else @@ -808,7 +882,7 @@ .data(arc_data) .enter() .append('g') - .attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(treatment.y) + ')'); + .attr('transform', 'translate(' + xScale(treatment.x) + ', ' + yScale(scaleBg(treatment.y)) + ')'); var arcs = treatmentDots.append('path') .attr('class', 'path') diff --git a/static/js/dropdown.js b/static/js/dropdown.js deleted file mode 100644 index 573e804c351..00000000000 --- a/static/js/dropdown.js +++ /dev/null @@ -1,23 +0,0 @@ -function Dropdown(el) { - this.ddmenuitem = 0; - - this.$el = $(el); - var that = this; - - $(document).click(function() { that.close(); }); -} - -Dropdown.prototype.close = function () { - if (this.ddmenuitem) { - this.ddmenuitem.css('visibility', 'hidden'); - this.ddmenuitem = 0; - } -}; - -Dropdown.prototype.open = function (e) { - this.close(); - this.ddmenuitem = $(this.$el).css('visibility', 'visible'); - e.stopPropagation(); -}; - - diff --git a/static/js/experiments.js b/static/js/experiments.js new file mode 100644 index 00000000000..d0e7b8aa100 --- /dev/null +++ b/static/js/experiments.js @@ -0,0 +1,28 @@ +$(function() { + if (querystring.experiments) { + $(".experiments").show(); + if (!querystring.drawer) { + $("#drawerToggle").click(); + } + } else { + $(".experiments").hide(); + } + + $(".glyphToggle").on("click", function(){ + var newGlyph = $(this).find("i").attr("class"); + $("#drawerToggle").find("i").prop("class", newGlyph); + event.preventDefault(); + }); + + $(".iconToggle").on("click", function(){ + var newIcon = $(this).find("img").attr("src"); + $("#favicon").prop("href", newIcon); + event.preventDefault(); + }); + + $(".toolbarIconToggle").on("click", function(){ + var newIcon = $(this).find("img").attr("src"); + $("#toolbar").css({'background-image':'url('+newIcon+')'}); + event.preventDefault(); + }); +}); diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js new file mode 100644 index 00000000000..8e6725dbe21 --- /dev/null +++ b/static/js/ui-utils.js @@ -0,0 +1,295 @@ +var drawerIsOpen = false; +var browserStorage = $.localStorage; +var defaultSettings = { + "units": "mg/dl", + "nightMode": false +} + +var app = {}; +$.ajax("/api/v1/status.json", { + success: function (xhr) { + app = { + "name": xhr.name, + "version": xhr.version, + "apiEnabled": xhr.apiEnabled + } + } +}).done(function() { + $(".appName").text(app.name); + $(".version").text(app.version); + if (app.apiEnabled) { + $(".serverSettings").show(); + } +}); + + +function getBrowserSettings(storage) { + var json = { + "units": storage.get("units"), + "nightMode": storage.get("nightMode"), + "customTitle": storage.get("customTitle") + }; + + // 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.nightMode = setDefault(json.nightMode, defaultSettings.nightMode); + $("#nightmode-browser").prop("checked", json.nightMode); + + if (json.customTitle) { + $("h1.customTitle").html(json.customTitle); + $("input#customTitle").prop("value", json.customTitle); + document.title = "Nightscout: " + json.customTitle; + } + + return json; +} +function getServerSettings() { + var json = { + "units": Object() + }; + + json.units = setDefault(json.units, defaultSettings.units); + //console.log("serverSettings.units: " + json.units); + if (json.units == "mmol") { + $("#mmol-server").prop("checked", true); + } else { + $("#mgdl-server").prop("checked", true); + } + + return json; +} +function setDefault(variable, defaultValue) { + if (typeof(variable) === "object") { + return defaultValue; + } + return variable; +} +function jsonIsNotEmpty(json) { + var jsonAsString = JSON.stringify(json); + jsonAsString.replace(/\s/g, ""); + return (jsonAsString != "{}") +} +function storeInBrowser(json, storage) { + if (json.units) storage.set("units", json.units); + if (json.nightMode == true) { + storage.set("nightMode", true) + } else { + storage.set("nightMode", false) + } + if (json.customTitle) storage.set("customTitle", json.customTitle); + event.preventDefault(); +} +function storeOnServer(json) { + if (jsonIsNotEmpty(json)) { + alert("TO DO: add storeOnServer() logic."); + // reference: http://code.tutsplus.com/tutorials/submit-a-form-without-page-refresh-using-jquery--net-59 + //var dataString = "name="+ name + "&email=" + email + "&phone=" + phone; + //alert (dataString);return false; + /* $.ajax({ + type: "POST", + url: "/api/v1/settings", + data: json + }); + */ + } +} + + +function getQueryParms() { + params = {}; + if (location.search) { + location.search.substr(1).split("&").forEach(function(item) { + params[item.split("=")[0]] = item.split("=")[1].replace(/[_\+]/g, " "); + }); + } + return params; +} + +function isTouch() { + try{ document.createEvent("TouchEvent"); return true; } + catch(e){ return false; } +} + + +function closeDrawer(callback) { + $("#container").animate({marginLeft: "0px"}, 300, callback); + $("#chartContainer").animate({marginLeft: "0px"}, 300); + $("#drawer").animate({right: "-200px"}, 300, function() { + $("#drawer").css("display", "none"); + }); + drawerIsOpen = false; +} +function openDrawer() { + drawerIsOpen = true; + $("#container").animate({marginLeft: "-200px"}, 300); + $("#chartContainer").animate({marginLeft: "-200px"}, 300); + $("#drawer").css("display", "block"); + $("#drawer").animate({right: "0"}, 300); +} + + +function closeNotification() { + var notify = $("#notification"); + notify.hide(); + notify.find("span").html(""); +} +function showNotification(note, type) { + var notify = $("#notification"); + notify.hide(); + + // Notification types: "info", "warn", "success", "urgent". + // - default: "urgent" + notify.removeClass("info warn urgent"); + notify.addClass(type ? type : "urgent"); + + notify.find("span").html(note); + notify.css("left", "calc(50% - " + ($("#notification").width() / 2) + "px)"); + notify.show(); +} + + +function closeToolbar() { + stretchStatusForToolbar("close"); + + $("#showToolbar").css({top: "44px"}); + $("#showToolbar").fadeIn(50, function() { + $("#showToolbar").animate({top: 0}, 200); + $("#toolbar").animate({marginTop: "-44px"}, 200); + }); +} +function openToolbar() { + $("#showToolbar").css({top: 0}); + $("#showToolbar").animate({top: "44px"}, 200).fadeOut(200); + $("#toolbar").animate({marginTop: "0px"}, 200); + + stretchStatusForToolbar("open"); +} +function stretchStatusForToolbar(toolbarState){ + // closed = up + if (toolbarState == "close") { + $(".status").css({ + "font-size": "+125%" + }); + } + + // open = down + if (toolbarState == "open") { + $(".status").css({ + "font-size": "-125%" + }); + } +} + + +var querystring = getQueryParms(); +// var serverSettings = getServerSettings(); +var browserSettings = getBrowserSettings(browserStorage); + +function Dropdown(el) { + this.ddmenuitem = 0; + + this.$el = $(el); + var that = this; + + $(document).click(function() { that.close(); }); +} +Dropdown.prototype.close = function () { + if (this.ddmenuitem) { + this.ddmenuitem.css('visibility', 'hidden'); + this.ddmenuitem = 0; + } +}; +Dropdown.prototype.open = function (e) { + this.close(); + this.ddmenuitem = $(this.$el).css('visibility', 'visible'); + e.stopPropagation(); +}; + + +$("#drawerToggle").click(function(event) { + if(drawerIsOpen) { + closeDrawer(); + drawerIsOpen = false; + } else { + openDrawer(); + drawerIsOpen = true; + } + event.preventDefault(); +}); + +$("#notification").click(function(event) { + closeNotification(); + event.preventDefault(); +}); + +$("#hideToolbar").click(function(event) { + if (drawerIsOpen) { + closeDrawer(function() { + closeToolbar(); + }); + } else { + closeToolbar(); + } + event.preventDefault(); +}); +$("#showToolbar").find("a").click(function(event) { + openToolbar(); + event.preventDefault(); +}); + +$("input#save").click(function() { + storeInBrowser({ + "units": $("input:radio[name=units-browser]:checked").val(), + "nightMode": $("#nightmode-browser").prop("checked"), + "customTitle": $("input#customTitle").prop("value") + }, browserStorage); + + storeOnServer({ + //"units": $("input:radio[name=units-server]:checked").val() + }); + + event.preventDefault(); + + // reload for changes to take effect + // -- strip '#' so form submission does not fail + var url = window.location.href; + url = url.replace(/#$/, ""); + window.location = url; +}); + + +$(function() { + // Tooltips can remain in the way on touch screens. + var notTouchScreen = (!isTouch()); + if (notTouchScreen) { + $(".tip").tipsy(); + } else { + // Drawer info tips should be displayed on touchscreens. + $("#drawer").find(".tip").tipsy(); + } + $.fn.tipsy.defaults = { + fade: true, + gravity: "n", + opacity: 0.75 + } + + if (querystring.notify) { + showNotification(querystring.notify, querystring.notifytype); + } + + if (querystring.drawer) { + openDrawer(); + } else { + // drawer=true cancels out toolbar=false + if (querystring.toolbar == "false") { + closeToolbar(); + } + } +}); diff --git a/tests/api.entries.test.js b/tests/api.entries.test.js index da123d85068..b99b0f4c4c6 100644 --- a/tests/api.entries.test.js +++ b/tests/api.entries.test.js @@ -14,7 +14,7 @@ describe('Entries REST api', function ( ) { this.app.enable('api'); var self = this; store(function ( ) { - self.app.use('/entries/', entries(self.app, self.wares, self.archive)); + self.app.use('/', entries(self.app, self.wares, self.archive)); self.archive.create(load('json'), done); }); }); @@ -28,14 +28,24 @@ describe('Entries REST api', function ( ) { }); it('/entries.json', function (done) { request(this.app) - .get('/entries/.json') + .get('/entries.json') .expect(200) .end(function (err, res) { // console.log('body', res.body); - res.body.length.should.equal(30); + res.body.length.should.equal(10); done( ); }); + }); + it('/entries.json', function (done) { + request(this.app) + .get('/entries.json?count=30') + .expect(200) + .end(function (err, res) { + // console.log('body', res.body); + res.body.length.should.equal(30); + done( ); + }); }); it('/entries/current.json', function (done) { diff --git a/tests/security.test.js b/tests/security.test.js index 573a087f41f..b94589ff7db 100644 --- a/tests/security.test.js +++ b/tests/security.test.js @@ -40,11 +40,35 @@ describe('API_SECRET', function ( ) { should.not.exist(env.api_secret); var ctx = setup_app(env, function ( ) { ctx.app.enabled('api').should.be.false; - ping_status(ctx.app, done); + ping_status(ctx.app, again); + function again ( ) { + ping_authorized_endpoint(ctx.app, 404, done); + } }); }); + it('should work fail set unauthorized', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + var ctx = setup_app(env, function ( ) { + // console.log(this.app.enabled('api')); + ctx.app.enabled('api').should.be.true; + // ping_status(ctx.app, done); + // ping_authorized_endpoint(ctx.app, 200, done); + ping_status(ctx.app, again); + function again ( ) { + ctx.app.api_secret = ''; + ping_authorized_endpoint(ctx.app, 401, done); + } + }); + + }); + + it('should work fine set', function (done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; @@ -54,7 +78,13 @@ describe('API_SECRET', function ( ) { var ctx = setup_app(env, function ( ) { // console.log(this.app.enabled('api')); ctx.app.enabled('api').should.be.true; - ping_status(ctx.app, done); + // ping_status(ctx.app, done); + // ping_authorized_endpoint(ctx.app, 200, done); + ping_status(ctx.app, again); + function again ( ) { + ctx.app.api_secret = env.api_secret; + ping_authorized_endpoint(ctx.app, 200, done); + } }); }); @@ -72,7 +102,7 @@ describe('API_SECRET', function ( ) { function ping_status (app, fn) { request(app) - .get('/status/.json') + .get('/status.json') .expect(200) .end(function (err, res) { // console.log(res.body); @@ -82,5 +112,19 @@ describe('API_SECRET', function ( ) { }) } + function ping_authorized_endpoint (app, fails, fn) { + request(app) + .get('/experiments/test') + .set('api-secret', app.api_secret || '') + .expect(fails) + .end(function (err, res) { + if (fails < 400) { + res.body.status.should.equal('ok'); + } + fn( ); + // console.log('err', err, 'res', res); + }) + } + });