nightscout
Version:
Nightscout acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patients glucose data in realtime.
1,333 lines (1,095 loc) • 44.2 kB
JavaScript
;
var _ = require('lodash');
var $ = (global && global.$) || require('jquery');
var d3 = (global && global.d3) || require('d3');
var shiroTrie = require('shiro-trie');
var Storages = require('js-storage');
var language = require('../language')();
var sandbox = require('../sandbox')();
var profile = require('../profilefunctions')();
var units = require('../units')();
var levels = require('../levels');
var times = require('../times');
var receiveDData = require('./receiveddata');
var brushing = false;
var browserSettings;
var client = {};
var hashauth = require('./hashauth');
client.hashauth = hashauth.init(client, $);
$('#loadingMessageText').html('Connecting to server');
client.headers = function headers () {
if (client.authorized) {
return {
Authorization: 'Bearer ' + client.authorized.token
};
} else if (client.hashauth) {
return {
'api-secret': client.hashauth.hash()
};
} else {
return {};
}
};
client.crashed = function crashed () {
$('#centerMessagePanel').show();
$('#loadingMessageText').html('It appears the server has crashed. Please go to Heroku or Azure and reboot the server.');
}
client.init = function init (callback) {
client.browserUtils = require('./browser-utils')($);
var token = client.browserUtils.queryParms().token;
var secret = client.hashauth.apisecrethash || Storages.localStorage.get('apisecrethash');
var src = '/api/v1/status.json?t=' + new Date().getTime();
if (secret) {
src += '&secret=' + secret;
} else if (token) {
src += '&token=' + token;
}
$.ajax({
method: 'GET'
, url: src
, headers: client.headers()
}).done(function success (serverSettings) {
if (serverSettings.runtimeState !== 'loaded') {
console.log('Server is still loading data');
$('#loadingMessageText').html('Server is starting and still loading data, retrying load in 5 seconds');
window.setTimeout(window.Nightscout.client.init, 5000);
return;
}
client.settingsFailed = false;
client.loadLanguage(serverSettings, callback);
}).fail(function fail (jqXHR) {
// check if we couldn't reach the server at all, show offline message
if (!jqXHR.readyState) {
console.log('Application appears to be OFFLINE');
$('#loadingMessageText').html('Connecting to Nightscout server failed, retrying every 5 seconds');
window.setTimeout(window.Nightscout.client.init(), 5000);
return;
}
//no server setting available, use defaults, auth, etc
if (client.settingsFailed) {
console.log('Already tried to get settings after auth, but failed');
} else {
client.settingsFailed = true;
// detect browser language
var lang = Storages.localStorage.get('language') || (navigator.language || navigator.userLanguage).toLowerCase();
if (lang !== 'zh_cn' && lang !== 'zh-cn' && lang !== 'zh_tw' && lang !== 'zh-tw') {
lang = lang.substring(0, 2);
} else {
lang = lang.replace('-', '_');
}
if (language.languages.find(l => l.code === lang)) {
language.set(lang);
} else {
language.set('en');
}
client.translate = language.translate;
// auth failed, hide loader and request for key
$('#centerMessagePanel').hide();
client.hashauth.requestAuthentication(function afterRequest () {
window.setTimeout(client.init(callback), 5000);
});
}
});
};
client.loadLanguage = function loadLanguage (serverSettings, callback) {
$('#loadingMessageText').html('Loading language file');
browserSettings = require('./browser-settings');
client.settings = browserSettings(client, serverSettings, $);
console.log('language is', client.settings.language);
let filename = language.getFilename(client.settings.language);
$.ajax({
method: 'GET'
, url: '/translations/' + filename
}).done(function success (localization) {
language.offerTranslations(localization);
console.log('Application appears to be online');
$('#centerMessagePanel').hide();
client.load(serverSettings, callback);
}).fail(function fail () {
console.error('Loading localization failed, continuing with English');
console.log('Application appears to be online');
$('#centerMessagePanel').hide();
client.load(serverSettings, callback);
});
}
client.load = function load (serverSettings, callback) {
var FORMAT_TIME_12 = '%-I:%M %p'
, FORMAT_TIME_12_COMPACT = '%-I:%M'
, FORMAT_TIME_24 = '%H:%M%'
, FORMAT_TIME_12_SCALE = '%-I %p'
, FORMAT_TIME_24_SCALE = '%H';
var history = 48;
var chart
, socket
, isInitialData = false
, opacity = { current: 1, DAY: 1, NIGHT: 0.5 }
, clientAlarms = {}
, alarmInProgress = false
, alarmMessage
, currentNotify
, currentAnnouncement
, alarmSound = 'alarm.mp3'
, urgentAlarmSound = 'alarm2.mp3'
, previousNotifyTimestamp;
client.entryToDate = function entryToDate (entry) {
if (entry.date) return entry.date;
entry.date = new Date(entry.mills);
return entry.date;
};
client.now = Date.now();
client.dataLastUpdated = 0;
client.lastPluginUpdateTime = 0;
client.ddata = require('../data/ddata')();
client.defaultForecastTime = times.mins(30).msecs;
client.forecastTime = client.now + client.defaultForecastTime;
client.entries = [];
client.ticks = require('./ticks');
//containers
var container = $('.container')
, bgStatus = $('.bgStatus')
, currentBG = $('.bgStatus .currentBG')
, majorPills = $('.bgStatus .majorPills')
, minorPills = $('.bgStatus .minorPills')
, statusPills = $('.status .statusPills')
, primary = $('.primary')
, editButton = $('#editbutton');
client.tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
client.settings = browserSettings(client, serverSettings, $);
language.set(client.settings.language).DOMtranslate($);
client.translate = language.translate;
client.language = language;
client.plugins = require('../plugins/')({
settings: client.settings
, extendedSettings: client.settings.extendedSettings
, language: language
, levels: levels
}).registerClientDefaults();
browserSettings.loadPluginSettings(client);
client.utils = require('../utils')({
settings: client.settings
, language: language
});
client.rawbg = client.plugins('rawbg');
client.delta = client.plugins('delta');
client.timeago = client.plugins('timeago');
client.direction = client.plugins('direction');
client.errorcodes = client.plugins('errorcodes');
client.ctx = {
data: {}
, bus: require('../bus')(client.settings, client.ctx)
, settings: client.settings
, pluginBase: client.plugins.base(majorPills, minorPills, statusPills, bgStatus, client.tooltip, Storages.localStorage)
};
client.ctx.language = language;
levels.translate = language.translate;
client.ctx.levels = levels;
client.ctx.notifications = require('../notifications')(client.settings, client.ctx);
client.sbx = sandbox.clientInit(client.ctx, client.now);
client.renderer = require('./renderer')(client, d3, $);
//After plugins are initialized with browser settings;
browserSettings.loadAndWireForm();
client.adminnotifies = require('./adminnotifiesclient')(client, $);
if (serverSettings && serverSettings.authorized) {
client.authorized = serverSettings.authorized;
client.authorized.lat = Date.now();
client.authorized.shiros = _.map(client.authorized.permissionGroups, function toShiro (group) {
var shiro = shiroTrie.new();
_.forEach(group, function eachPermission (permission) {
shiro.add(permission);
});
return shiro;
});
client.authorized.check = function check (permission) {
var found = _.find(client.authorized.shiros, function checkEach (shiro) {
return shiro.check(permission);
});
return _.isObject(found);
};
}
client.afterAuth = function afterAuth (isAuthenticated) {
var treatmentCreateAllowed = client.authorized ? client.authorized.check('api:treatments:create') : isAuthenticated;
var treatmentUpdateAllowed = client.authorized ? client.authorized.check('api:treatments:update') : isAuthenticated;
$('#lockedToggle').click(client.hashauth.requestAuthentication).toggle(!treatmentCreateAllowed && client.settings.showPlugins.indexOf('careportal') > -1);
$('#treatmentDrawerToggle').toggle(treatmentCreateAllowed && client.settings.showPlugins.indexOf('careportal') > -1);
$('#boluscalcDrawerToggle').toggle(treatmentCreateAllowed && client.settings.showPlugins.indexOf('boluscalc') > -1);
if (isAuthenticated) client.notifies.updateAdminNotifies();
// Edit mode
editButton.toggle(client.settings.editMode && treatmentUpdateAllowed);
editButton.click(function editModeClick (event) {
client.editMode = !client.editMode;
if (client.editMode) {
client.renderer.drawTreatments(client);
editButton.find('i').addClass('selected');
} else {
chart.focus.selectAll('.draggable-treatment')
.style('cursor', 'default')
.on('mousedown.drag', null);
editButton.find('i').removeClass('selected');
}
if (event) { event.preventDefault(); }
});
};
client.hashauth.initAuthentication(client.afterAuth);
client.focusRangeMS = times.hours(client.settings.focusHours).msecs;
$('.focus-range li[data-hours=' + client.settings.focusHours + ']').addClass('selected');
client.brushed = brushed;
client.formatTime = formatTime;
client.dataUpdate = dataUpdate;
client.careportal = require('./careportal')(client, $);
client.boluscalc = require('./boluscalc')(client, $);
client.profilefunctions = profile;
client.editMode = false;
//TODO: use the bus for updates and notifications
//client.ctx.bus.on('tick', function timedReload (tick) {
// console.info('tick', tick.now);
//});
//broadcast 'tock' event each minute, start a new setTimeout each time it fires make it happen on the minute
//see updateClock
//start the bus after setting up listeners
//client.ctx.bus.uptime( );
client.dataExtent = function dataExtent () {
if (client.entries.length > 0) {
return [client.entryToDate(client.entries[0]), client.entryToDate(client.entries[client.entries.length - 1])];
} else {
return [new Date(client.now - times.hours(history).msecs), new Date(client.now)];
}
};
client.bottomOfPills = function bottomOfPills () {
//the offset's might not exist for some tests
var bottomOfPrimary = primary.offset() ? primary.offset().top + primary.height() : 0;
var bottomOfMinorPills = minorPills.offset() ? minorPills.offset().top + minorPills.height() : 0;
var bottomOfStatusPills = statusPills.offset() ? statusPills.offset().top + statusPills.height() : 0;
return Math.max(bottomOfPrimary, bottomOfMinorPills, bottomOfStatusPills);
};
function formatTime (time, compact) {
var timeFormat = getTimeFormat(false, compact);
time = d3.timeFormat(timeFormat)(time);
if (client.settings.timeFormat !== 24) {
time = time.toLowerCase();
}
return time;
}
function getTimeFormat (isForScale, compact) {
var timeFormat = FORMAT_TIME_12;
if (client.settings.timeFormat === 24) {
timeFormat = isForScale ? FORMAT_TIME_24_SCALE : FORMAT_TIME_24;
} else {
timeFormat = isForScale ? FORMAT_TIME_12_SCALE : (compact ? FORMAT_TIME_12_COMPACT : FORMAT_TIME_12);
}
return timeFormat;
}
//TODO: replace with utils.scaleMgdl and/or utils.roundBGForDisplay
function scaleBg (bg) {
if (client.settings.units === 'mmol') {
return units.mgdlToMMOL(bg);
} else {
return bg;
}
}
function generateTitle () {
function s (value, sep) { return value ? value + ' ' : sep || ''; }
var title = '';
var status = client.timeago.checkStatus(client.sbx);
if (status !== 'current') {
var ago = client.timeago.calcDisplay(client.sbx.lastSGVEntry(), client.sbx.time);
title = s(ago.value) + s(ago.label, ' - ') + title;
} else if (client.latestSGV) {
var currentMgdl = client.latestSGV.mgdl;
if (currentMgdl < 39) {
title = s(client.errorcodes.toDisplay(currentMgdl), ' - ') + title;
} else {
var delta = client.nowSBX.properties.delta;
if (delta) {
var deltaDisplay = delta.display;
title = s(scaleBg(currentMgdl)) + s(deltaDisplay) + s(client.direction.info(client.latestSGV).label) + title;
}
}
}
return title;
}
function resetCustomTitle () {
var customTitle = client.settings.customTitle || 'Nightscout';
$('.customTitle').text(customTitle);
}
function checkAnnouncement () {
var result = {
inProgress: currentAnnouncement ? Date.now() - currentAnnouncement.received < times.mins(5).msecs : false
};
if (result.inProgress) {
var message = currentAnnouncement.message.length > 1 ? currentAnnouncement.message : currentAnnouncement.title;
result.message = message;
$('.customTitle').text(message);
} else if (currentAnnouncement) {
currentAnnouncement = null;
console.info('cleared announcement');
}
return result;
}
function updateTitle () {
var windowTitle;
var announcementStatus = checkAnnouncement();
if (alarmMessage && alarmInProgress) {
$('.customTitle').text(alarmMessage);
if (!isTimeAgoAlarmType()) {
windowTitle = alarmMessage + ': ' + generateTitle();
}
} else if (announcementStatus.inProgress && announcementStatus.message) {
windowTitle = announcementStatus.message + ': ' + generateTitle();
} else {
resetCustomTitle();
}
container.toggleClass('announcing', announcementStatus.inProgress);
$(document).attr('title', windowTitle || generateTitle());
}
// clears the current user brush and resets to the current real time data
function updateBrushToNow (skipBrushing) {
// update brush and focus chart with recent data
var brushExtent = client.dataExtent();
brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS);
// console.log('updateBrushToNow(): Resetting brush: ', brushExtent);
if (chart.theBrush) {
chart.theBrush.call(chart.brush)
chart.theBrush.call(chart.brush.move, brushExtent.map(chart.xScale2));
}
if (!skipBrushing) {
brushed();
}
}
function alarmingNow () {
return container.hasClass('alarming');
}
function inRetroMode () {
return chart && chart.inRetroMode();
}
function brushed () {
// Brush not initialized
if (!chart.theBrush) {
return;
}
if (brushing) {
return;
}
brushing = true;
// default to most recent focus period
var brushExtent = client.dataExtent();
brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS);
var brushedRange = d3.brushSelection(chart.theBrush.node());
// console.log("brushed(): coordinates: ", brushedRange);
if (brushedRange) {
brushExtent = brushedRange.map(chart.xScale2.invert);
}
// console.log('brushed(): Brushed to: ', brushExtent);
if (!brushedRange || (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.focusRangeMS)) {
// ensure that brush updating is with the time range
if (brushExtent[0].getTime() + client.focusRangeMS > client.dataExtent()[1].getTime()) {
brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS);
} else {
brushExtent[1] = new Date(brushExtent[0].getTime() + client.focusRangeMS);
}
// console.log('brushed(): updating to: ', brushExtent);
chart.theBrush.call(chart.brush.move, brushExtent.map(chart.xScale2));
}
function adjustCurrentSGVClasses (value, isCurrent) {
var reallyCurrentAndNotAlarming = isCurrent && !inRetroMode() && !alarmingNow();
bgStatus.toggleClass('current', alarmingNow() || reallyCurrentAndNotAlarming);
if (!alarmingNow()) {
container.removeClass('urgent warning inrange');
if (reallyCurrentAndNotAlarming) {
container.addClass(sgvToColoredRange(value));
}
}
currentBG.toggleClass('icon-hourglass', value === 9);
currentBG.toggleClass('error-code', value < 39);
currentBG.toggleClass('bg-limit', value === 39 || value > 400);
}
function updateCurrentSGV (entry) {
var value = entry.mgdl
, isCurrent = 'current' === client.timeago.checkStatus(client.sbx);
if (value === 9) {
currentBG.text('');
} else if (value < 39) {
currentBG.html(client.errorcodes.toDisplay(value));
} else if (value < 40) {
currentBG.text('LOW');
} else if (value > 400) {
currentBG.text('HIGH');
} else {
currentBG.text(scaleBg(value));
}
adjustCurrentSGVClasses(value, isCurrent);
}
function mergeDeviceStatus (retro, ddata) {
if (!retro) {
return ddata;
}
var result = retro.map(x => Object.assign(x, ddata.find(y => y._id == x._id)));
var missingInRetro = ddata.filter(y => !retro.find(x => x._id == y._id));
result.push(...missingInRetro);
return result;
}
function updatePlugins (time) {
if (time > client.lastPluginUpdateTime && time > client.dataLastUpdated) {
if ((time - client.lastPluginUpdateTime) < 1000) {
return; // Don't update the plugins more than once a second
}
client.lastPluginUpdateTime = time;
}
//TODO: doing a clone was slow, but ok to let plugins muck with data?
//var ddata = client.ddata.clone();
client.ddata.inRetroMode = inRetroMode();
client.ddata.profile = profile;
// retro data only ever contains device statuses
// Cleate a clone of the data for the sandbox given to plugins
var mergedStatuses = client.ddata.devicestatus;
if (client.retro.data) {
mergedStatuses = mergeDeviceStatus(client.retro.data.devicestatus, client.ddata.devicestatus);
}
var clonedData = _.clone(client.ddata);
clonedData.devicestatus = mergedStatuses;
client.sbx = sandbox.clientInit(
client.ctx
, new Date(time).getTime() //make sure we send a timestamp
, clonedData
);
//all enabled plugins get a chance to set properties, even if they aren't shown
client.plugins.setProperties(client.sbx);
//only shown plugins get a chance to update visualisations
client.plugins.updateVisualisations(client.sbx);
var viewMenu = $('#viewMenu');
viewMenu.empty();
_.each(client.sbx.pluginBase.forecastInfos, function eachInfo (info) {
var forecastOption = $('<li/>');
var forecastLabel = $('<label/>');
var forecastCheckbox = $('<input type="checkbox" data-forecast-type="' + info.type + '"/>');
forecastCheckbox.prop('checked', client.settings.showForecast.indexOf(info.type) > -1);
forecastOption.append(forecastLabel);
forecastLabel.append(forecastCheckbox);
forecastLabel.append('<span>Show ' + info.label + '</span>');
forecastCheckbox.change(function onChange (event) {
var checkbox = $(event.target);
var type = checkbox.attr('data-forecast-type');
var checked = checkbox.prop('checked');
if (checked) {
client.settings.showForecast += ' ' + type;
} else {
client.settings.showForecast = _.chain(client.settings.showForecast.split(' '))
.filter(function(forecast) { return forecast !== type; })
.value()
.join(' ');
}
Storages.localStorage.set('showForecast', client.settings.showForecast);
refreshChart(true);
});
viewMenu.append(forecastOption);
});
//send data to boluscalc too
client.boluscalc.updateVisualisations(client.sbx);
}
function clearCurrentSGV () {
currentBG.text('---');
container.removeClass('alarming urgent warning inrange');
}
var nowDate = null;
var nowData = client.entries.filter(function(d) {
return d.type === 'sgv' && d.mills <= brushExtent[1].getTime();
});
var focusPoint = _.last(nowData);
function updateHeader () {
if (inRetroMode()) {
nowDate = brushExtent[1];
$('#currentTime')
.text(formatTime(nowDate, true))
.css('text-decoration', 'line-through');
} else {
nowDate = new Date(client.now);
updateClockDisplay();
}
if (focusPoint) {
if (brushExtent[1].getTime() - focusPoint.mills > times.mins(15).msecs) {
clearCurrentSGV();
} else {
updateCurrentSGV(focusPoint);
}
updatePlugins(nowDate.getTime());
} else {
clearCurrentSGV();
updatePlugins(nowDate);
}
}
updateHeader();
updateTimeAgo();
if (chart.prevChartHeight) {
chart.scroll(nowDate);
}
var top = (client.bottomOfPills() + 5);
$('#chartContainer').css({ top: top + 'px', height: $(window).height() - top - 10 });
container.removeClass('loading');
brushing = false;
}
function sgvToColor (sgv) {
var color = 'grey';
if (client.settings.theme !== 'default') {
if (sgv > client.settings.thresholds.bgHigh) {
color = 'red';
} else if (sgv > client.settings.thresholds.bgTargetTop) {
color = 'yellow';
} else if (sgv >= client.settings.thresholds.bgTargetBottom && sgv <= client.settings.thresholds.bgTargetTop && client.settings.theme === 'colors') {
color = '#4cff00';
} else if (sgv < client.settings.thresholds.bgLow) {
color = 'red';
} else if (sgv < client.settings.thresholds.bgTargetBottom) {
color = 'yellow';
}
}
return color;
}
function sgvToColoredRange (sgv) {
var range = '';
if (client.settings.theme !== 'default') {
if (sgv > client.settings.thresholds.bgHigh) {
range = 'urgent';
} else if (sgv > client.settings.thresholds.bgTargetTop) {
range = 'warning';
} else if (sgv >= client.settings.thresholds.bgTargetBottom && sgv <= client.settings.thresholds.bgTargetTop && client.settings.theme === 'colors') {
range = 'inrange';
} else if (sgv < client.settings.thresholds.bgLow) {
range = 'urgent';
} else if (sgv < client.settings.thresholds.bgTargetBottom) {
range = 'warning';
}
}
return range;
}
function formatAlarmMessage (notify) {
var announcementMessage = notify && notify.isAnnouncement && notify.message && notify.message.length > 1;
if (announcementMessage) {
return levels.toDisplay(notify.level) + ': ' + notify.message;
} else if (notify) {
return notify.title;
}
return null;
}
function setAlarmMessage (notify) {
alarmMessage = formatAlarmMessage(notify);
}
function generateAlarm (file, notify) {
alarmInProgress = true;
currentNotify = notify;
setAlarmMessage(notify);
var selector = '.audio.alarms audio.' + file;
if (!alarmingNow()) {
d3.select(selector).each(function() {
var audio = this;
playAlarm(audio);
$(this).addClass('playing');
});
console.log('Asking plugins to visualize alarms');
client.plugins.visualizeAlarm(client.sbx, notify, alarmMessage);
}
container.addClass('alarming').addClass(file === urgentAlarmSound ? 'urgent' : 'warning');
var silenceBtn = $('#silenceBtn');
silenceBtn.empty();
_.each(client.settings.snoozeMinsForAlarmEvent(notify), function eachOption (mins) {
var snoozeOption = $('<li><a data-snooze-time="' + times.mins(mins).msecs + '">' + client.translate('Silence for %1 minutes', { params: [mins] }) + '</a></li>');
snoozeOption.click(snoozeAlarm);
silenceBtn.append(snoozeOption);
});
updateTitle();
}
function snoozeAlarm (event) {
stopAlarm(true, $(event.target).data('snooze-time'));
event.preventDefault();
}
function playAlarm (audio) {
// ?mute=true disables alarms to testers.
if (client.browserUtils.queryParms().mute !== 'true') {
audio.play();
} else {
client.browserUtils.showNotification('Alarm was muted (?mute=true)');
}
}
function stopAlarm (isClient, silenceTime, notify) {
alarmInProgress = false;
alarmMessage = null;
container.removeClass('urgent warning');
d3.selectAll('audio.playing').each(function() {
var audio = this;
audio.pause();
$(this).removeClass('playing');
});
client.browserUtils.closeNotification();
container.removeClass('alarming');
updateTitle();
silenceTime = silenceTime || times.mins(5).msecs;
var alarm = null;
if (notify) {
if (notify.level) {
alarm = getClientAlarm(notify.level, notify.group);
} else if (notify.group) {
alarm = getClientAlarm(currentNotify.level, notify.group);
} else {
alarm = getClientAlarm(currentNotify.level, currentNotify.group);
}
} else if (currentNotify) {
alarm = getClientAlarm(currentNotify.level, currentNotify.group);
}
if (alarm) {
alarm.lastAckTime = Date.now();
alarm.silenceTime = silenceTime;
if (alarm.group === 'Time Ago') {
container.removeClass('alarming-timeago');
}
} else {
console.info('No alarm to ack for', notify || currentNotify);
}
// only emit ack if client invoke by button press
if (isClient && currentNotify) {
socket.emit('ack', currentNotify.level, currentNotify.group, silenceTime);
}
currentNotify = null;
brushed();
}
function refreshAuthIfNeeded () {
var clientToken = client.authorized ? client.authorized.token : null;
var token = client.browserUtils.queryParms().token || clientToken;
if (token && client.authorized) {
var renewTime = (client.authorized.exp * 1000) - times.mins(15).msecs - Math.abs((client.authorized.iat * 1000) - client.authorized.lat);
var refreshIn = Math.round((renewTime - client.now) / 1000);
if (client.now > renewTime) {
console.info('Refreshing authorization renewal');
$.ajax('/api/v2/authorization/request/' + token, {
success: function(authorized) {
if (authorized) {
console.info('Got new authorization', authorized);
authorized.lat = client.now;
client.authorized = authorized;
}
}
});
} else if (refreshIn < times.mins(5).secs) {
console.info('authorization refresh in ' + refreshIn + 's');
}
}
}
function updateClock () {
updateClockDisplay();
// Update at least every 15 seconds
var interval = Math.min(15 * 1000, (60 - (new Date()).getSeconds()) * 1000 + 5);
setTimeout(updateClock, interval);
updateTimeAgo();
if (chart) {
brushed();
}
// Dim the screen by reducing the opacity when at nighttime
if (client.settings.nightMode) {
var dateTime = new Date();
if (opacity.current !== opacity.NIGHT && (dateTime.getHours() > 21 || dateTime.getHours() < 7)) {
$('body').css({ 'opacity': opacity.NIGHT });
} else {
$('body').css({ 'opacity': opacity.DAY });
}
}
refreshAuthIfNeeded();
if (client.resetRetroIfNeeded) {
client.resetRetroIfNeeded();
}
}
function updateClockDisplay () {
if (inRetroMode()) {
return;
}
client.now = Date.now();
$('#currentTime').text(formatTime(new Date(client.now), true)).css('text-decoration', '');
}
function getClientAlarm (level, group) {
var key = level + '-' + group;
var alarm = null;
// validate the key before getting the alarm
if (Object.prototype.hasOwnProperty.call(clientAlarms, key)) {
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
alarm = clientAlarms[key];
}
if (!alarm) {
alarm = { level: level, group: group };
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
clientAlarms[key] = alarm;
}
return alarm;
}
function isTimeAgoAlarmType () {
return currentNotify && currentNotify.group === 'Time Ago';
}
function isStale (status) {
return client.settings.alarmTimeagoWarn && status === 'warn' ||
client.settings.alarmTimeagoUrgent && status === 'urgent';
}
function notAcked (alarm) {
return Date.now() >= (alarm.lastAckTime || 0) + (alarm.silenceTime || 0);
}
function checkTimeAgoAlarm (status) {
var level = status === 'urgent' ? levels.URGENT : levels.WARN;
var alarm = getClientAlarm(level, 'Time Ago');
if (isStale(status) && notAcked(alarm)) {
console.info('generating timeAgoAlarm', alarm);
container.addClass('alarming-timeago');
var display = client.timeago.calcDisplay(client.sbx.lastSGVEntry(), client.sbx.time);
var translate = client.translate;
var notify = {
title: translate('Last data received') + ' ' + display.value + ' ' + translate(display.label)
, level: status === 'urgent' ? 2 : 1
, group: 'Time Ago'
};
var sound = status === 'warn' ? alarmSound : urgentAlarmSound;
generateAlarm(sound, notify);
}
container.toggleClass('alarming-timeago', status !== 'current');
if (status === 'warn') {
container.addClass('warn');
} else if (status === 'urgent') {
container.addClass('urgent');
}
if (alarmingNow() && status === 'current' && isTimeAgoAlarmType()) {
stopAlarm(true, times.min().msecs);
}
}
function updateTimeAgo () {
var status = client.timeago.checkStatus(client.sbx);
if (status !== 'current') {
updateTitle();
}
checkTimeAgoAlarm(status);
}
function updateTimeAgoSoon () {
setTimeout(function updatingTimeAgoNow () {
updateTimeAgo();
}, times.secs(10).msecs);
}
function refreshChart (updateToNow) {
if (updateToNow) {
updateBrushToNow();
}
chart.update(false);
}
(function watchVisibility () {
// Set the name of the hidden property and the change event for visibility
var hidden, visibilityChange;
if (typeof document.hidden !== 'undefined') {
hidden = 'hidden';
visibilityChange = 'visibilitychange';
} else if (typeof document.mozHidden !== 'undefined') {
hidden = 'mozHidden';
visibilityChange = 'mozvisibilitychange';
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}
document.addEventListener(visibilityChange, function visibilityChanged () {
var prevHidden = client.documentHidden;
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
client.documentHidden = document[hidden];
if (prevHidden && !client.documentHidden) {
console.info('Document now visible, updating - ' + new Date());
refreshChart(true);
}
});
})();
window.onresize = refreshChart;
updateClock();
updateTimeAgoSoon();
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();
};
var silenceDropdown = new Dropdown('#silenceBtn');
var viewDropdown = new Dropdown('#viewMenu');
$('.bgButton').click(function(e) {
if (alarmingNow()) {
/* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive
silenceDropdown.open(e);
}
});
$('.focus-range li').click(function(e) {
var li = $(e.target);
if (li.attr('data-hours')) {
$('.focus-range li').removeClass('selected');
li.addClass('selected');
var hours = Number(li.data('hours'));
client.focusRangeMS = times.hours(hours).msecs;
Storages.localStorage.set('focusHours', hours);
refreshChart();
} else {
/* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive
viewDropdown.open(e);
}
});
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Client-side code to connect to server and handle incoming data
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* global io */
client.socket = socket = io.connect();
socket.on('dataUpdate', dataUpdate);
function resetRetro () {
client.retro = {
loadedMills: 0
, loadStartedMills: 0
};
}
client.resetRetroIfNeeded = function resetRetroIfNeeded () {
if (client.retro.loadedMills > 0 && Date.now() - client.retro.loadedMills > times.mins(5).msecs) {
resetRetro();
console.info('Cleared retro data to free memory');
}
};
resetRetro();
client.loadRetroIfNeeded = function loadRetroIfNeeded () {
var now = Date.now();
if (now - client.retro.loadStartedMills < times.secs(30).msecs) {
console.info('retro already loading, started', new Date(client.retro.loadStartedMills));
return;
}
if (now - client.retro.loadedMills > times.mins(3).msecs) {
client.retro.loadStartedMills = now;
console.info('retro not fresh load started', new Date(client.retro.loadStartedMills));
socket.emit('loadRetro', {
loadedMills: client.retro.loadedMills
});
}
};
socket.on('retroUpdate', function retroUpdate (retroData) {
console.info('got retroUpdate', retroData, new Date(client.now));
client.retro = {
loadedMills: Date.now()
, loadStartedMills: 0
, data: retroData
};
});
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Alarms and Text handling
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
client.authorizeSocket = function authorizeSocket () {
console.log('Authorizing socket');
var auth_data = {
client: 'web'
, secret: client.authorized && client.authorized.token ? null : client.hashauth.hash()
, token: client.authorized && client.authorized.token
, history: history
};
socket.emit(
'authorize'
, auth_data
, function authCallback (data) {
if (!data) {
console.log('Crashed!');
client.crashed();
}
if (!data.read || !hasRequiredPermission()) {
client.hashauth.requestAuthentication(function afterRequest () {
client.hashauth.updateSocketAuth();
if (callback) {
callback();
}
});
} else if (callback) {
callback();
}
}
);
}
socket.on('connect', function() {
console.log('Client connected to server.');
client.authorizeSocket();
});
function hasRequiredPermission () {
if (client.requiredPermission) {
if (client.hashauth && client.hashauth.isAuthenticated()) {
return true;
} else {
return client.authorized && client.authorized.check(client.requiredPermission);
}
} else {
return true;
}
}
//with predicted alarms, latestSGV may still be in target so to see if the alarm
// is for a HIGH we can only check if it's >= the bottom of the target
function isAlarmForHigh () {
return client.latestSGV && client.latestSGV.mgdl >= client.settings.thresholds.bgTargetBottom;
}
//with predicted alarms, latestSGV may still be in target so to see if the alarm
// is for a LOW we can only check if it's <= the top of the target
function isAlarmForLow () {
return client.latestSGV && client.latestSGV.mgdl <= client.settings.thresholds.bgTargetTop;
}
socket.on('notification', function(notify) {
console.log('notification from server:', notify);
if (notify.timestamp && previousNotifyTimestamp !== notify.timestamp) {
previousNotifyTimestamp = notify.timestamp;
client.plugins.visualizeAlarm(client.sbx, notify, notify.title + ' ' + notify.message);
} else {
console.log('No timestamp found for notify, not passing to plugins');
}
});
socket.on('announcement', function(notify) {
console.info('announcement received from server');
currentAnnouncement = notify;
currentAnnouncement.received = Date.now();
updateTitle();
});
socket.on('alarm', function(notify) {
console.info('alarm received from server');
var enabled = (isAlarmForHigh() && client.settings.alarmHigh) || (isAlarmForLow() && client.settings.alarmLow);
if (enabled) {
console.log('Alarm raised!');
generateAlarm(alarmSound, notify);
} else {
console.info('alarm was disabled locally', client.latestSGV.mgdl, client.settings);
}
chart.update(false);
});
socket.on('urgent_alarm', function(notify) {
console.info('urgent alarm received from server');
var enabled = (isAlarmForHigh() && client.settings.alarmUrgentHigh) || (isAlarmForLow() && client.settings.alarmUrgentLow);
if (enabled) {
console.log('Urgent alarm raised!');
generateAlarm(urgentAlarmSound, notify);
} else {
console.info('urgent alarm was disabled locally', client.latestSGV.mgdl, client.settings);
}
chart.update(false);
});
socket.on('clear_alarm', function(notify) {
if (alarmInProgress) {
console.log('clearing alarm');
stopAlarm(false, null, notify);
}
});
$('#testAlarms').click(function(event) {
// Speech synthesis also requires on iOS that user triggers a speech event for it to speak anything
if (client.plugins('speech').isEnabled) {
var msg = new SpeechSynthesisUtterance('Ok ok.');
msg.lang = 'en-US';
window.speechSynthesis.speak(msg);
}
d3.selectAll('.audio.alarms audio').each(function() {
var audio = this;
playAlarm(audio);
setTimeout(function() {
audio.pause();
}, 4000);
});
event.preventDefault();
});
if (serverSettings) {
$('.appName').text(serverSettings.name);
$('.version').text(serverSettings.version);
$('.head').text(serverSettings.head);
if (serverSettings.apiEnabled) {
$('.serverSettings').show();
}
}
client.updateAdminMenu = function updateAdminMenu() {
// hide food control if not enabled
$('.foodcontrol').toggle(client.settings.enable.indexOf('food') > -1);
// hide cob control if not enabled
$('.cobcontrol').toggle(client.settings.enable.indexOf('cob') > -1);
}
client.updateAdminMenu();
container.toggleClass('has-minor-pills', client.plugins.hasShownType('pill-minor', client.settings));
function prepareEntries () {
// Post processing after data is in
var temp1 = [];
var sbx = client.sbx.withExtendedSettings(client.rawbg);
if (client.ddata.cal && client.rawbg.isEnabled(sbx)) {
temp1 = client.ddata.sgvs.map(function(entry) {
var rawbgValue = client.rawbg.showRawBGs(entry.mgdl, entry.noise, client.ddata.cal, sbx) ? client.rawbg.calc(entry, client.ddata.cal, sbx) : 0;
if (rawbgValue > 0) {
return { mills: entry.mills - 2000, mgdl: rawbgValue, color: 'white', type: 'rawbg' };
} else {
return null;
}
}).filter(function(entry) {
return entry !== null;
});
}
var temp2 = client.ddata.sgvs.map(function(obj) {
return { mills: obj.mills, mgdl: obj.mgdl, direction: obj.direction, color: sgvToColor(obj.mgdl), type: 'sgv', noise: obj.noise, filtered: obj.filtered, unfiltered: obj.unfiltered };
});
client.entries = [];
client.entries = client.entries.concat(temp1, temp2);
client.entries = client.entries.concat(client.ddata.mbgs.map(function(obj) {
return { mills: obj.mills, mgdl: obj.mgdl, color: 'red', type: 'mbg', device: obj.device };
}));
var tooOld = client.now - times.hours(48).msecs;
client.entries = _.filter(client.entries, function notTooOld (entry) {
return entry.mills > tooOld;
});
client.entries.forEach(function(point) {
if (point.mgdl < 39) {
point.color = 'transparent';
}
});
client.entries.sort(function sorter (a, b) {
return a.mills - b.mills;
});
}
function dataUpdate (received, headless) {
console.info('got dataUpdate', new Date(client.now));
var lastUpdated = Date.now();
client.dataLastUpdated = lastUpdated;
receiveDData(received, client.ddata, client.settings);
// Resend new treatments to profile
client.profilefunctions.updateTreatments(client.ddata.profileTreatments, client.ddata.tempbasalTreatments, client.ddata.combobolusTreatments);
if (received.profiles) {
profile.loadData(received.profiles);
}
if (client.ddata.sgvs) {
// TODO change the next line so that it uses the prediction if the signal gets lost (max 1/2 hr)
client.ctx.data.lastUpdated = lastUpdated;
client.latestSGV = client.ddata.sgvs[client.ddata.sgvs.length - 1];
}
client.ddata.inRetroMode = false;
client.ddata.profile = profile;
client.nowSBX = sandbox.clientInit(
client.ctx
, lastUpdated
, client.ddata
);
//all enabled plugins get a chance to set properties, even if they aren't shown
client.plugins.setProperties(client.nowSBX);
prepareEntries();
updateTitle();
// Don't invoke D3 in headless mode
if (headless) return;
if (!isInitialData) {
isInitialData = true;
chart = client.chart = require('./chart')(client, d3, $);
chart.update(true);
brushed();
chart.update(false);
} else if (!inRetroMode()) {
brushed();
chart.update(false);
} else {
chart.updateContext();
}
}
};
module.exports = client;