mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Merge pull request #6836 from nightscout/dev
## Welcome to Release 14.2 Sweet Liquorice! This release focuses on improving the overall security of Nightscout. 14.2 adds a new method for Nightscout to notify you of various security issues in your setup. After upgrading to the this release, if Nightscout wants to tell you something about the system security, you'll see a red megaphone appear in the Nightscout web client. To see the messages, you'll have to sign in using your API-SECRET or a token that's got administration privileges. Full details of the messages can be found in the Nightscout documentation: https://nightscout.github.io/nightscout/security/ Advance warning regarding future releases: we are likely to make compatibility breaking changes in upcoming releases that will change how the authentication flows with the Nightscout API works, along with changes to validation of data sent to Nightscout. If you're an app developer and are using the Nightscout APIs in your application, please join our Discord channel to learn about the changes are they're implemented. You can join the channel here: https://discord.gg/zg7CvCQ Nightscout translations are now made in Crowdin. This is very easy even for non-technical folks, so please join and contribute! https://crowdin.com/project/nightscout Note if you're running your instance with a very old MongoDB version, your installation might break. We've tested the release using MongoDB 4.2 and 4.4. ## New Features and Improvements * Administration messages support * Bolus bubble rendering in Nightscout UI is now more configurable, see the new Settings in the client settings panel * You can now configure Nightscout to disable battery alarms during night * Security improvement: treatments and CGM entries sent over the REST API V1 are now filtered for XSS injection code * A lot of work has been put into localization, huge thanks to all the contributors * Reports now remember the settings you've chosen across sessions * Alexa integration now supports Spanish * Fixed a bug with AAPS updating CGM values after Dexcom rounds the value * Added support for Portuguese and Slovenian * Support for Traditional Chinese has been removed until we find a contributor to help with translating more of the software. The next release will remove support for Japanese unless a larger portion of the text has been translated by time of release. ## For developers * APIV3 results are now wrapped differently from before * Webpack was upgraded to V5 * Client JS bundling was simplified to just one bundle, cutting down bundling time to ~50% of current * Removed cache invalidation token from bundling process and generating it on server boot * Security improvement: generate strong persistent random string on deploy to use for JWT signing instead of api_secret * Security improvement: moved api-secret and JWT signing to a separate centralized security component and deletes api_secret from environment, so it's not accessible elsewhere * Security improvement: Clients can now send the api_secret using SHA512 * Moved some server components away from project root to make it easier to see what code runs in server vs client * Fixes some issues reported by linter
This commit is contained in:
+28
-16
@@ -1,23 +1,35 @@
|
||||
module.exports = {
|
||||
"plugins": [ ],
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
'plugins': [
|
||||
'security'
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"mocha": true,
|
||||
"jquery": true
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:security/recommended'
|
||||
],
|
||||
'parser': 'babel-eslint',
|
||||
'env': {
|
||||
'browser': true,
|
||||
'commonjs': true,
|
||||
'es6': true,
|
||||
'node': true,
|
||||
'mocha': true,
|
||||
'jquery': true
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
'rules': {
|
||||
'security/detect-object-injection' : 0,
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
"varsIgnorePattern": "should|expect"
|
||||
'varsIgnorePattern': 'should|expect'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
'overrides': [
|
||||
{
|
||||
'files': ['lib/client/*.js'],
|
||||
'rules': {
|
||||
'security/detect-object-injection': 0
|
||||
}
|
||||
}
|
||||
],
|
||||
};
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/dev' && github.repository_owner == 'nightscout'
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
+1
-1
@@ -9,6 +9,7 @@ bundle/bundle.out.js
|
||||
*.iml
|
||||
my.env
|
||||
my.*.env
|
||||
*.pem
|
||||
|
||||
static/bower_components/
|
||||
.*.sw?
|
||||
@@ -28,5 +29,4 @@ npm-debug.log
|
||||
/cgm-remote-monitor.njsproj
|
||||
/cgm-remote-monitor.sln
|
||||
/obj/Debug
|
||||
/bin
|
||||
/*.bat
|
||||
|
||||
+1
-1
@@ -15,4 +15,4 @@ RUN npm install && \
|
||||
|
||||
EXPOSE 1337
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
CMD ["node", "lib/server/server.js"]
|
||||
|
||||
@@ -104,6 +104,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
- [`bage` (Battery Age)](#bage-battery-age)
|
||||
- [`treatmentnotify` (Treatment Notifications)](#treatmentnotify-treatment-notifications)
|
||||
- [`basal` (Basal Profile)](#basal-basal-profile)
|
||||
- [`bolus` (Bolus Rendering)](#bolus-bolus-rendering)
|
||||
- [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge)
|
||||
- [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge)
|
||||
- [`pump` (Pump Monitoring)](#pump-pump-monitoring)
|
||||
@@ -294,6 +295,8 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
|
||||
### Predefined values for your browser settings (optional)
|
||||
|
||||
* `TIME_FORMAT` (`12`)- possible values `12` or `24`
|
||||
* `DAY_START` (`7.0`) - time for start of day (0.0 - 24.0) for features using day time / night time options
|
||||
* `DAY_END` (`21.0`) - time for end of day (0.0 - 24.0) for features using day time / night time options
|
||||
* `NIGHT_MODE` (`off`) - possible values `on` or `off`
|
||||
* `SHOW_RAWBG` (`never`) - possible values `always`, `never` or `noise`
|
||||
* `CUSTOM_TITLE` (`Nightscout`) - Title for the main view
|
||||
@@ -311,7 +314,6 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
|
||||
* The `linear` option has equidistant tick marks; the range used is dynamic so that space at the top of chart isn't wasted.
|
||||
* The `log-dynamic` is similar to the default `log` options, but uses the same dynamic range and the `linear` scale.
|
||||
* `EDIT_MODE` (`on`) - possible values `on` or `off`. Enables the icon allowing for editing of treatments in the main view.
|
||||
* `BOLUS_RENDER_OVER` (1) - U value over which the bolus values are rendered on the chart if the 'x U and Over' option is selected. This value can be an integer or a float, e.g. 0.3, 1.5, 2, etc...
|
||||
|
||||
### Predefined values for your server settings (optional)
|
||||
* `INSECURE_USE_HTTP` (`false`) - Redirect unsafe http traffic to https. Possible values `false`, or `true`. Your site redirects to `https` by default. If you don't want that from Nightscout, but want to implement that with a Nginx or Apache proxy, set `INSECURE_USE_HTTP` to `true`. Note: This will allow (unsafe) http traffic to your Nightscout instance and is not recommended.
|
||||
@@ -468,12 +470,20 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
|
||||
* `BAGE_URGENT` (`360`) - If time since last `Pump Battery Change` matches `BAGE_URGENT` hours, user will be issued a persistent warning of overdue change (default of 360 hours is 15 days).
|
||||
|
||||
##### `treatmentnotify` (Treatment Notifications)
|
||||
Generates notifications when a treatment has been entered and snoozes alarms minutes after a treatment. Default snooze is 10 minutes, and can be set using the `TREATMENTNOTIFY_SNOOZE_MINS` [extended setting](#extended-settings).
|
||||
Generates notifications when a treatment has been entered and snoozes alarms minutes after a treatment.
|
||||
* `TREATMENTNOTIFY_SNOOZE_MINS` (`10`) - Number of minutes to snooze notifications after a treatment is entered
|
||||
* `TREATMENTNOTIFY_INCLUDE_BOLUSES_OVER` (`0`) - U value over which the bolus will trigger a notification and snooze alarms
|
||||
|
||||
##### `basal` (Basal Profile)
|
||||
Adds the Basal pill visualization to display the basal rate for the current time. Also enables the `bwp` plugin to calculate correction temp basal suggestions. Uses the `basal` field from the [treatment profile](#treatment-profile). Also uses the extended setting:
|
||||
* `BASAL_RENDER` (`none`) - Possible values are `none`, `default`, or `icicle` (inverted)
|
||||
|
||||
##### `bolus` (Bolus Rendering)
|
||||
Settings to configure Bolus rendering
|
||||
* `BOLUS_RENDER_OVER` (`0`) - U value over which the bolus labels use the format defined in `BOLUS_RENDER_FORMAT`. This value can be an integer or a float, e.g. 0.3, 1.5, 2, etc.
|
||||
* `BOLUS_RENDER_FORMAT` (`default`) - Possible values are `hidden`, `default` (with leading zero and U), `concise` (with U, without leading zero), and `minimal` (without leading zero and U).
|
||||
* `BOLUS_RENDER_FORMAT_SMALL` (`default`) - Possible values are `hidden`, `default` (with leading zero and U), `concise` (with U, without leading zero), and `minimal` (without leading zero and U).
|
||||
|
||||
##### `bridge` (Share2Nightscout bridge)
|
||||
Glucose reading directly from the Dexcom Share service, uses these extended settings:
|
||||
* `BRIDGE_USER_NAME` - Your username for the Share service.
|
||||
@@ -511,6 +521,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or
|
||||
* `PUMP_URGENT_BATT_P` (`20`) - The % of the pump battery remaining, an urgent alarm will be triggered when dropping below this threshold.
|
||||
* `PUMP_WARN_BATT_V` (`1.35`) - The voltage (if percent isn't available) of the pump battery, a warning will be triggered when dropping below this threshold.
|
||||
* `PUMP_URGENT_BATT_V` (`1.30`) - The voltage (if percent isn't available) of the pump battery, an urgent alarm will be triggered when dropping below this threshold.
|
||||
* `PUMP_WARN_BATT_QUIET_NIGHT` (`false`) - Do not generate battery alarms at night.
|
||||
|
||||
##### `openaps` (OpenAPS)
|
||||
Integrated OpenAPS loop monitoring, uses these extended settings:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
|
||||
require('crypto').randomBytes(1024, function(err, buffer) {
|
||||
var token = buffer.toString('hex');
|
||||
console.log(token);
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const axios = require('axios');
|
||||
const moment = require('moment');
|
||||
const crypto = require('crypto');
|
||||
const shasum = crypto.createHash('sha1');
|
||||
|
||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
console.error('This is an utility to send continuous CGM entry data to a test Nightscout server')
|
||||
console.error('USAGE: node testdatarunner.js <SERVER BASE URL> <API_SECRET>');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const URL = process.argv[2];
|
||||
const SECRET = process.argv[3];
|
||||
shasum.update(SECRET);
|
||||
const SECRET_SHA1 = shasum.digest('hex');
|
||||
|
||||
const HEADERS = {'api-secret': SECRET_SHA1};
|
||||
|
||||
const ENTRIES_URL = URL + '/api/v1/entries';
|
||||
|
||||
var done = (function wait () { if (!done) setTimeout(wait, 1000); })();
|
||||
|
||||
console.log('NS data filler active');
|
||||
|
||||
const entry = {
|
||||
device: 'Dev simulator',
|
||||
date: 1609061083612,
|
||||
dateString: '2020-12-27T09:24:43.612Z',
|
||||
sgv: 100,
|
||||
delta: 0,
|
||||
direction: 'Flat',
|
||||
type: 'sgv'
|
||||
};
|
||||
|
||||
function addEntry () {
|
||||
console.log('Sending add new entry');
|
||||
sendEntry(Date.now());
|
||||
setTimeout(addEntry, FIVE_MINUTES);
|
||||
}
|
||||
|
||||
function oscillator(time, frequency = 1, amplitude = 1, phase = 0, offset = 0){
|
||||
return Math.sin(time * frequency * Math.PI * 2 + phase * Math.PI * 2) * amplitude + offset;
|
||||
}
|
||||
|
||||
async function sendFail() {
|
||||
try {
|
||||
console.log('Sending fail');
|
||||
const response = await axios.post(ENTRIES_URL, entry, {headers: {'api-secret': 'incorrect' }});
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
async function sendEntry (date) {
|
||||
const m = moment(date);
|
||||
entry.date = date;
|
||||
entry.dateString = m.toISOString();
|
||||
entry.sgv = 100 + Math.round(oscillator(date / 1000, 1/(60*60), 30));
|
||||
|
||||
console.log('Adding entry', entry);
|
||||
const response = await axios.post(ENTRIES_URL, entry, {headers: HEADERS});
|
||||
|
||||
if (date > Date.now() - 5000) sendFail();
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('GETTING', ENTRIES_URL);
|
||||
const response = await axios.get(ENTRIES_URL, {headers: HEADERS} );
|
||||
const latestEntry = response.data ? response.data[0] : null;
|
||||
|
||||
if (!latestEntry) {
|
||||
// Fill in history
|
||||
console.log('I would fill in history');
|
||||
const totalToSave = 24;
|
||||
const now = Date.now();
|
||||
const start = now - ( totalToSave * FIVE_MINUTES);
|
||||
let current = start;
|
||||
while (current <= now) {
|
||||
await sendEntry(current);
|
||||
current += FIVE_MINUTES;
|
||||
}
|
||||
|
||||
setTimeout(addEntry, 1000*60*5);
|
||||
|
||||
} else {
|
||||
let latestDate = latestEntry.date;
|
||||
const now = Date.now();
|
||||
if ((now - latestDate) > FIVE_MINUTES) {
|
||||
console.log('We got data but it is older than 5 minutes, makign a partial fill');
|
||||
|
||||
let current = latestDate + FIVE_MINUTES;
|
||||
|
||||
while (current < now) {
|
||||
await sendEntry(current);
|
||||
current += FIVE_MINUTES;
|
||||
}
|
||||
|
||||
latestDate = current;
|
||||
|
||||
} else {
|
||||
console.log('Looks like we got history, not filling');
|
||||
}
|
||||
setTimeout(addEntry, Date.now() - latestDate);
|
||||
}
|
||||
|
||||
sendFail();
|
||||
sendFail();
|
||||
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -5,6 +5,8 @@ console.info('Nightscout report bundle start');
|
||||
window.Nightscout.report_plugins_preinit = require('../lib/report_plugins/');
|
||||
window.Nightscout.predictions = require('../lib/report/predictions');
|
||||
window.Nightscout.reportclient = require('../lib/report/reportclient');
|
||||
window.Nightscout.profileclient = require('../lib/profile/profileeditor');
|
||||
window.Nightscout.foodclient = require('../lib/food/food');
|
||||
|
||||
console.info('Nightscout report bundle ready');
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ window.Nightscout = {
|
||||
admin_plugins: require('../lib/admin_plugins/')()
|
||||
};
|
||||
|
||||
window.Nightscout.report_plugins_preinit = require('../lib/report_plugins/');
|
||||
window.Nightscout.predictions = require('../lib/report/predictions');
|
||||
window.Nightscout.reportclient = require('../lib/report/reportclient');
|
||||
window.Nightscout.profileclient = require('../lib/profile/profileeditor');
|
||||
window.Nightscout.foodclient = require('../lib/food/food');
|
||||
|
||||
console.info('Nightscout bundle ready');
|
||||
|
||||
// Needed for Hot Module Replacement
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
{
|
||||
"interactionModel": {
|
||||
"languageModel": {
|
||||
"invocationName": "mi monitor",
|
||||
"intents": [
|
||||
{
|
||||
"name": "NSStatus",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"Como lo estoy haciendo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LastLoop",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"Cuando fue mi ultimo bucle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MetricNow",
|
||||
"slots": [
|
||||
{
|
||||
"name": "metric",
|
||||
"type": "LIST_OF_METRICS",
|
||||
"samples": [
|
||||
"que es {pwd} {metric}",
|
||||
"cual es mi {metric}",
|
||||
"como es {pwd} {metric}",
|
||||
"como es {metric}",
|
||||
"cuanta {metric} tiene {pwd}",
|
||||
"cuanta {metric} tengo",
|
||||
"cuanta {metric}",
|
||||
"{pwd} {metric}",
|
||||
"{metric}",
|
||||
"mi {metric}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pwd",
|
||||
"type": "AMAZON.FirstName"
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
"cuanto {metric} le queda a {pwd}",
|
||||
"cual es mi {metric}",
|
||||
"cuanta {metric} queda",
|
||||
"Cuanta {metric}",
|
||||
"como es {metric}",
|
||||
"como es mi {metric}",
|
||||
"como es {pwd} {metric}",
|
||||
"como esta mi {metric}",
|
||||
"que es {metric}",
|
||||
"cuanta {metric} tengo",
|
||||
"cuanta {metric} tiene {pwd}",
|
||||
"que es {pwd} {metric}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.NavigateHomeIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.StopIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.CancelIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.HelpIntent",
|
||||
"samples": []
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "LIST_OF_METRICS",
|
||||
"values": [
|
||||
{
|
||||
"name": {
|
||||
"value": "delta",
|
||||
"synonyms": [
|
||||
"delta de glucosa en sangre",
|
||||
"delta de azucar en sangre",
|
||||
"delta azucar",
|
||||
"delta glucosa"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "uploader battery",
|
||||
"synonyms": [
|
||||
"bateria restante del cargador",
|
||||
"carga de la batera"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "pump reservoir",
|
||||
"synonyms": [
|
||||
"insulina restante",
|
||||
"queda insulina",
|
||||
"insulina que queda",
|
||||
"insulina en mi bomba",
|
||||
"insulina"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "pump battery",
|
||||
"synonyms": [
|
||||
"bateria de la bomba restante",
|
||||
"bomba de energia de la bateria"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "bg",
|
||||
"synonyms": [
|
||||
"numero",
|
||||
"glucosa",
|
||||
"azucar en sangre",
|
||||
"glucosa en sangre"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "iob",
|
||||
"synonyms": [
|
||||
"insulina que tengo",
|
||||
"insulina a bordo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "basal",
|
||||
"synonyms": [
|
||||
"basal que tengo",
|
||||
"basal",
|
||||
"basal actual"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "cob",
|
||||
"synonyms": [
|
||||
"carbohidratos",
|
||||
"carbohidratos a bordo",
|
||||
"carbo hidratos",
|
||||
"carbohidratos que tengo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "forecast",
|
||||
"synonyms": [
|
||||
"prevision ar2",
|
||||
"prevision del bucle"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "raw bg",
|
||||
"synonyms": [
|
||||
"numero bruto",
|
||||
"azucar en sangre en bruto",
|
||||
"glucosa en sangre en bruto"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "cgm noise",
|
||||
"synonyms": [
|
||||
"ruido cgm",
|
||||
"ruido del cgm"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "cgm tx age",
|
||||
"synonyms": [
|
||||
"edad del transmisor",
|
||||
"transmisor edad",
|
||||
"edad del transmisor cgm"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "cgm tx status",
|
||||
"synonyms": [
|
||||
"estado del transmisor",
|
||||
"estado transmisor",
|
||||
"estado del transmisor cgm"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "cgm battery",
|
||||
"synonyms": [
|
||||
"nivel de bateria cgm",
|
||||
"niveles de bateria cgm",
|
||||
"bateria del cgm",
|
||||
"bateria del transmisor cgm",
|
||||
"nivel de bateria del transmisor cgm",
|
||||
"nivel bateria transmisor cgm",
|
||||
"nivel bateria del transmisor cgm",
|
||||
"bateria transmisor",
|
||||
"nivel bateria transmisor",
|
||||
"niveles de bateria del transmisor",
|
||||
"baterias del transmisor"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "cgm session age",
|
||||
"synonyms": [
|
||||
"edad de la sesion"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "cgm status",
|
||||
"synonyms": [
|
||||
"estado cgm",
|
||||
"estado del cgm"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "cgm mode",
|
||||
"synonyms": [
|
||||
"modo cgm",
|
||||
"modo del cgm"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": {
|
||||
"value": "db size",
|
||||
"synonyms": [
|
||||
"ocupacion de la base de datos",
|
||||
"ocupacion de datos",
|
||||
"ocupacion fichero"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"dialog": {
|
||||
"intents": [
|
||||
{
|
||||
"name": "MetricNow",
|
||||
"confirmationRequired": false,
|
||||
"prompts": {},
|
||||
"slots": [
|
||||
{
|
||||
"name": "metric",
|
||||
"type": "LIST_OF_METRICS",
|
||||
"confirmationRequired": false,
|
||||
"elicitationRequired": true,
|
||||
"prompts": {
|
||||
"elicitation": "Elicit.Slot.1421281086569.34001419564"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pwd",
|
||||
"type": "AMAZON.FirstName",
|
||||
"confirmationRequired": false,
|
||||
"elicitationRequired": false,
|
||||
"prompts": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"delegationStrategy": "ALWAYS"
|
||||
},
|
||||
"prompts": [
|
||||
{
|
||||
"id": "Elicit.Slot.1421281086569.34001419564",
|
||||
"variations": [
|
||||
{
|
||||
"type": "PlainText",
|
||||
"value": "¿Que metrica estas buscando?"
|
||||
},
|
||||
{
|
||||
"type": "PlainText",
|
||||
"value": "¿Que valor buscas?"
|
||||
},
|
||||
{
|
||||
"type": "PlainText",
|
||||
"value": "¿Que metrica quieres saber?"
|
||||
},
|
||||
{
|
||||
"type": "PlainText",
|
||||
"value": "¿Que valor quieres saber?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ function createOrSaveRole (role, client, callback) {
|
||||
reload(client, callback);
|
||||
}).fail(function fail (err) {
|
||||
console.error('Unable to ' + method + ' Role', err.responseText);
|
||||
window.alert(client.translate('Unable to %1 Role', { params: [method] }));
|
||||
window.alert(client.translate('Unable to save Role'));
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ function createOrSaveSubject (subject, client, callback) {
|
||||
reload(client, callback);
|
||||
}).fail(function fail (err) {
|
||||
console.error('Unable to ' + method + ' Subject', err.responseText);
|
||||
window.alert(client.translate('Unable to ' + method + ' Subject'));
|
||||
window.alert(client.translate('Unable to save Subject'));
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
function init (ctx) {
|
||||
|
||||
const adminnotifies = {};
|
||||
|
||||
adminnotifies.addNotify = function addnotify (notify) {
|
||||
if (!notify) return;
|
||||
|
||||
notify.title = notify.title || 'No title';
|
||||
notify.message = notify.message || 'No message';
|
||||
|
||||
const existingMessage = _.find(adminnotifies.notifies, function findExisting (obj) {
|
||||
return obj.message == notify.message;
|
||||
});
|
||||
|
||||
if (existingMessage) {
|
||||
existingMessage.count += 1;
|
||||
existingMessage.lastRecorded = Date.now();
|
||||
} else {
|
||||
notify.count = 1;
|
||||
notify.lastRecorded = Date.now();
|
||||
adminnotifies.notifies.push(notify);
|
||||
}
|
||||
}
|
||||
|
||||
adminnotifies.getNotifies = function getNotifies () {
|
||||
return adminnotifies.notifies;
|
||||
}
|
||||
|
||||
ctx.bus.on('admin-notify', adminnotifies.addNotify);
|
||||
|
||||
adminnotifies.clean = function cleanNotifies () {
|
||||
adminnotifies.notifies = _.filter(adminnotifies.notifies, function findExisting (obj) {
|
||||
return obj.persistent || ((Date.now() - obj.lastRecorded) < 1000 * 60 * 60 * 12);
|
||||
});
|
||||
}
|
||||
|
||||
adminnotifies.cleanAll = function cleanAll() {
|
||||
adminnotifies.notifies = [];
|
||||
}
|
||||
|
||||
adminnotifies.cleanAll();
|
||||
|
||||
ctx.bus.on('tick', adminnotifies.clean);
|
||||
|
||||
return adminnotifies;
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
@@ -22,6 +22,7 @@ function configure(app, wares, ctx) {
|
||||
// json body types get handled as parsed json
|
||||
api.use(wares.bodyParser.json({
|
||||
limit: 1048576
|
||||
, extended: true
|
||||
}));
|
||||
// also support url-encoded content-type
|
||||
api.use(wares.bodyParser.urlencoded({
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const consts = require('../constants');
|
||||
|
||||
function configure (ctx) {
|
||||
const express = require('express')
|
||||
, api = express.Router();
|
||||
|
||||
api.get('/adminnotifies', function(req, res) {
|
||||
ctx.authorization.resolveWithRequest(req, function resolved (err, result) {
|
||||
|
||||
const isAdmin = ctx.authorization.checkMultiple('*:*:admin', result.shiros); //full admin permissions
|
||||
const response = {
|
||||
notifies: []
|
||||
, notifyCount: 0
|
||||
};
|
||||
|
||||
if (ctx.adminnotifies) {
|
||||
const notifies = _.filter(ctx.adminnotifies.getNotifies(), function isOld (obj) {
|
||||
return (obj.persistent || (Date.now() - obj.lastRecorded) < 1000 * 60 * 60 * 8);
|
||||
});
|
||||
|
||||
if (isAdmin) { response.notifies = notifies }
|
||||
response.notifyCount = notifies.length;
|
||||
}
|
||||
|
||||
res.sendJSONStatus(res, consts.HTTP_OK, response);
|
||||
});
|
||||
});
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
module.exports = configure;
|
||||
@@ -13,7 +13,10 @@ function configure (app, wares, ctx, env) {
|
||||
// text body types get handled as raw buffer stream
|
||||
api.use(wares.bodyParser.raw());
|
||||
// json body types get handled as parsed json
|
||||
api.use(wares.bodyParser.json());
|
||||
api.use(wares.bodyParser.json({
|
||||
limit: 1048576
|
||||
, extended: true
|
||||
}));
|
||||
|
||||
ctx.virtAsstBase.setupVirtAsstHandlers(ctx.alexa);
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ function configure (app, wares, ctx, env) {
|
||||
// text body types get handled as raw buffer stream
|
||||
api.use(wares.bodyParser.raw());
|
||||
// json body types get handled as parsed json
|
||||
api.use(wares.bodyParser.json());
|
||||
api.use(wares.bodyParser.json({
|
||||
limit: 1048576
|
||||
, extended: true
|
||||
}));
|
||||
// also support url-encoded content-type
|
||||
api.use(wares.bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
@@ -67,6 +70,9 @@ function configure (app, wares, ctx, env) {
|
||||
|
||||
function doPost (req, res) {
|
||||
var obj = req.body;
|
||||
|
||||
ctx.purifier.purifyObject(obj);
|
||||
|
||||
ctx.devicestatus.create(obj, function(err, created) {
|
||||
if (err) {
|
||||
res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err);
|
||||
|
||||
@@ -43,7 +43,10 @@ function configure (app, wares, ctx, env) {
|
||||
// text body types get handled as raw buffer stream
|
||||
api.use(wares.bodyParser.raw());
|
||||
// json body types get handled as parsed json
|
||||
api.use(wares.bodyParser.json());
|
||||
api.use(wares.bodyParser.json({
|
||||
limit: 1048576
|
||||
, extended: true
|
||||
}));
|
||||
// shortcut to use extension to specify output content-type
|
||||
api.use(wares.extensions([
|
||||
'json', 'svg', 'csv', 'txt', 'png', 'html', 'tsv'
|
||||
@@ -274,6 +277,11 @@ function configure (app, wares, ctx, env) {
|
||||
incoming = incoming.concat(req.body);
|
||||
}
|
||||
|
||||
for (let i = 0; i < incoming.length; i++) {
|
||||
const e = incoming[i];
|
||||
ctx.purifier.purifyObject(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @function persist
|
||||
* @returns {WritableStream} a writable persistent storage stream
|
||||
|
||||
@@ -11,7 +11,10 @@ function configure (app, wares, ctx) {
|
||||
// text body types get handled as raw buffer stream
|
||||
api.use(wares.bodyParser.raw( ));
|
||||
// json body types get handled as parsed json
|
||||
api.use(wares.bodyParser.json( ));
|
||||
api.use(wares.bodyParser.json({
|
||||
limit: 1048576
|
||||
, extended: true
|
||||
}));
|
||||
// also support url-encoded content-type
|
||||
api.use(wares.bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
|
||||
+7
-4
@@ -13,13 +13,13 @@ function create (env, ctx) {
|
||||
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.
|
||||
// Only allow access to the API if API KEY is set on the server.
|
||||
app.disable('api');
|
||||
if (env.api_secret) {
|
||||
console.log('API_SECRET present, enabling API');
|
||||
if (env.enclave.isApiKeySet()) {
|
||||
console.log('API KEY present, enabling API');
|
||||
app.enable('api');
|
||||
} else {
|
||||
console.log('API_SECRET not found, API disabled');
|
||||
console.log('API KEY has not been set, API disabled');
|
||||
}
|
||||
|
||||
if (env.settings.enable) {
|
||||
@@ -58,6 +58,9 @@ function create (env, ctx) {
|
||||
app.all('/activity*', require('./activity/')(app, wares, ctx));
|
||||
|
||||
app.use('/', wares.sendJSONStatus, require('./verifyauth')(ctx));
|
||||
|
||||
app.use('/', wares.sendJSONStatus, require('./adminnotifiesapi')(ctx));
|
||||
|
||||
app.all('/food*', require('./food/')(app, wares, ctx));
|
||||
|
||||
// Status first
|
||||
|
||||
@@ -11,7 +11,10 @@ function configure (app, wares, ctx) {
|
||||
// text body types get handled as raw buffer stream
|
||||
api.use(wares.bodyParser.raw( ));
|
||||
// json body types get handled as parsed json
|
||||
api.use(wares.bodyParser.json( ));
|
||||
api.use(wares.bodyParser.json({
|
||||
limit: 1048576
|
||||
, extended: true
|
||||
}));
|
||||
// also support url-encoded content-type
|
||||
api.use(wares.bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
@@ -61,6 +64,7 @@ function configure (app, wares, ctx) {
|
||||
// create new record
|
||||
api.post('/profile/', ctx.authorization.isPermitted('api:profile:create'), function(req, res) {
|
||||
var data = req.body;
|
||||
ctx.purifier.purifyObject(data);
|
||||
ctx.profile.create(data, function (err, created) {
|
||||
if (err) {
|
||||
res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err);
|
||||
|
||||
@@ -15,6 +15,7 @@ function configure (app, wares, ctx, env) {
|
||||
api.use(wares.compression());
|
||||
api.use(wares.bodyParser({
|
||||
limit: 1048576 * 50
|
||||
, extended: true
|
||||
}));
|
||||
// text body types get handled as raw buffer stream
|
||||
api.use(wares.bodyParser.raw({
|
||||
@@ -23,6 +24,7 @@ function configure (app, wares, ctx, env) {
|
||||
// json body types get handled as parsed json
|
||||
api.use(wares.bodyParser.json({
|
||||
limit: 1048576
|
||||
, extended: true
|
||||
}));
|
||||
// also support url-encoded content-type
|
||||
api.use(wares.bodyParser.urlencoded({
|
||||
@@ -119,6 +121,8 @@ function configure (app, wares, ctx, env) {
|
||||
t.created_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
ctx.purifier.purifyObject(t);
|
||||
|
||||
/*
|
||||
if (!t.created_at) {
|
||||
console.log('Trying to create treatment without created_at field', t);
|
||||
|
||||
+3
-6
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"API3_VERSION": "3.0.0-alpha",
|
||||
"API3_VERSION": "3.0.3-alpha",
|
||||
"API3_SECURITY_ENABLE": true,
|
||||
"API3_TIME_SKEW_TOLERANCE": 5,
|
||||
"API3_DEDUP_FALLBACK_ENABLED": true,
|
||||
"API3_CREATED_AT_FALLBACK_ENABLED": true,
|
||||
"API3_MAX_LIMIT": 1000,
|
||||
@@ -34,13 +33,11 @@
|
||||
"HTTP_400_SORT_SORT_DESC": "Parameters sort and sort_desc cannot be combined",
|
||||
"HTTP_400_UNSUPPORTED_FILTER_OPERATOR": "Unsupported filter operator {0}",
|
||||
"HTTP_400_IMMUTABLE_FIELD": "Field {0} cannot be modified by the client",
|
||||
"HTTP_401_BAD_DATE": "Bad Date header",
|
||||
"HTTP_401_BAD_TOKEN": "Bad access token or JWT",
|
||||
"HTTP_401_DATE_OUT_OF_TOLERANCE": "Date header out of tolerance",
|
||||
"HTTP_401_MISSING_DATE": "Missing Date header",
|
||||
"HTTP_401_MISSING_OR_BAD_TOKEN": "Missing or bad access token or JWT",
|
||||
"HTTP_403_MISSING_PERMISSION": "Missing permission {0}",
|
||||
"HTTP_403_NOT_USING_HTTPS": "Not using SSL/TLS",
|
||||
"HTTP_404_BAD_OPERATION": "Bad operation or collection",
|
||||
"HTTP_406_UNSUPPORTED_FORMAT": "Unsupported output format requested",
|
||||
"HTTP_422_READONLY_MODIFICATION": "Trying to modify read-only document",
|
||||
"HTTP_500_INTERNAL_ERROR": "Internal Server Error",
|
||||
@@ -52,4 +49,4 @@
|
||||
"MIN_TIMESTAMP": 946684800000,
|
||||
"MIN_UTC_OFFSET": -1440,
|
||||
"MAX_UTC_OFFSET": 1440
|
||||
}
|
||||
}
|
||||
|
||||
+28
-25
@@ -18,31 +18,34 @@ The server replies with `406 Not Acceptable` HTTP status in case of not supporte
|
||||
|
||||
Default content type is JSON, output can look like this:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"type":"sgv",
|
||||
"sgv":"171",
|
||||
"dateString":"2014-07-19T02:44:15.000-07:00",
|
||||
"date":1405763055000,
|
||||
"device":"dexcom",
|
||||
"direction":"Flat",
|
||||
"identifier":"5c5a2404e0196f4d3d9a718a",
|
||||
"srvModified":1405763055000,
|
||||
"srvCreated":1405763055000
|
||||
},
|
||||
{
|
||||
"type":"sgv",
|
||||
"sgv":"176",
|
||||
"dateString":"2014-07-19T03:09:15.000-07:00",
|
||||
"date":1405764555000,
|
||||
"device":"dexcom",
|
||||
"direction":"Flat",
|
||||
"identifier":"5c5a2404e0196f4d3d9a7187",
|
||||
"srvModified":1405764555000,
|
||||
"srvCreated":1405764555000
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"result": [
|
||||
{
|
||||
"type": "sgv",
|
||||
"sgv": "171",
|
||||
"dateString": "2014-07-19T02:44:15.000-07:00",
|
||||
"date": 1405763055000,
|
||||
"device": "dexcom",
|
||||
"direction": "Flat",
|
||||
"identifier": "5c5a2404e0196f4d3d9a718a",
|
||||
"srvModified": 1405763055000,
|
||||
"srvCreated": 1405763055000
|
||||
},
|
||||
{
|
||||
"type": "sgv",
|
||||
"sgv": "176",
|
||||
"dateString": "2014-07-19T03:09:15.000-07:00",
|
||||
"date": 1405764555000,
|
||||
"device": "dexcom",
|
||||
"direction": "Flat",
|
||||
"identifier": "5c5a2404e0196f4d3d9a7187",
|
||||
"srvModified": 1405764555000,
|
||||
"srvCreated": 1405764555000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### XML
|
||||
|
||||
@@ -27,22 +27,6 @@ There are two ways to authorize API calls:
|
||||
- then, to each secure API operation attach a JWT token in the HTTP header, eg. `Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NUb2tlbiI6InRlc3RyZWFkYWItNzZlYWZmMjQxOGJmYjdlMCIsImlhdCI6MTU2NTAzOTczMSwiZXhwIjoxNTY1MDQzMzMxfQ.Y-OFtFJ-gZNJcnZfm9r4S7085Z7YKVPiaQxuMMnraVk` (until the JWT expires)
|
||||
|
||||
|
||||
|
||||
---
|
||||
### Client timestamps
|
||||
As previously mentioned, a potential attacker cannot decrypt the captured messages, but he can send them back to the client/server at any later time. APIv3 is partially preventing this by the temporal validity of each secured API call.
|
||||
|
||||
|
||||
The client must include his current timestamp to each call so that the server can compare it against its clock. If the timestamp difference is not within the limit, the request is considered invalid. The tolerance limit is set in minutes in the `API3_TIME_SKEW_TOLERANCE` environment variable.
|
||||
|
||||
There are two ways to include the client timestamp to the call:
|
||||
- use `now` query parameter with UNIX epoch millisecond timestamp, eg. `now=1565041446908`
|
||||
- add HTTP `Date` header to the request, eg. `Date: Sun, 12 May 2019 07:49:58 GMT`
|
||||
|
||||
|
||||
The client can check each server response in the same way, because each response contains a server timestamp in the HTTP *Date* header as well.
|
||||
|
||||
|
||||
---
|
||||
APIv3 security is enabled by default, but it can be completely disabled for development and debugging purposes by setting the web environment variable `API3_SECURITY_ENABLE=false`.
|
||||
This setting is hazardous and it is strongly discouraged to be used for production purposes!
|
||||
|
||||
+143
-115
@@ -22,15 +22,18 @@ request('https://nsapiv3.herokuapp.com/api/v3/version',
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```javascript
|
||||
{
|
||||
"version":"0.12.2",
|
||||
"apiVersion":"3.0.0-alpha",
|
||||
"srvDate":1564386001772,
|
||||
"storage":{
|
||||
"storage":"mongodb",
|
||||
"version":"3.6.12"
|
||||
}
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"result": {
|
||||
"version": "14.1.0",
|
||||
"apiVersion": "3.0.2-alpha",
|
||||
"srvDate": 1609402081548,
|
||||
"storage": {
|
||||
"storage": "mongodb",
|
||||
"version": "4.2.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -44,28 +47,31 @@ It is public (there is no need to add authorization parameters/headers).
|
||||
Sample GET `/status` client code (to get my actual permissions):
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
|
||||
request(`https://nsapiv3.herokuapp.com/api/v3/status?${auth}`,
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```javascript
|
||||
{
|
||||
"version":"0.12.2",
|
||||
"apiVersion":"3.0.0-alpha",
|
||||
"srvDate":1564391740738,
|
||||
"storage":{
|
||||
"storage":"mongodb",
|
||||
"version":"3.6.12"
|
||||
},
|
||||
"apiPermissions":{
|
||||
"devicestatus":"crud",
|
||||
"entries":"crud",
|
||||
"food":"crud",
|
||||
"profile":"crud",
|
||||
"settings":"crud",
|
||||
"treatments":"crud"
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"result": {
|
||||
"version": "14.1.0",
|
||||
"apiVersion": "3.0.2-alpha",
|
||||
"srvDate": 1609427571833,
|
||||
"storage": {
|
||||
"storage": "mongodb",
|
||||
"version": "4.2.11"
|
||||
},
|
||||
"apiPermissions": {
|
||||
"devicestatus": "crud",
|
||||
"entries": "crud",
|
||||
"food": "crud",
|
||||
"profile": "crud",
|
||||
"settings": "crud",
|
||||
"treatments": "crud"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -80,30 +86,33 @@ Sample result:
|
||||
Sample GET `/entries` client code (to retrieve last 3 BG values):
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
|
||||
request(`https://nsapiv3.herokuapp.com/api/v3/entries?${auth}&sort$desc=date&limit=3&fields=dateString,sgv,direction`,
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```
|
||||
[
|
||||
{
|
||||
"dateString":"2019-07-30T02:24:50.434+0200",
|
||||
"sgv":115,
|
||||
"direction":"FortyFiveDown"
|
||||
},
|
||||
{
|
||||
"dateString":"2019-07-30T02:19:50.374+0200",
|
||||
"sgv":121,
|
||||
"direction":"FortyFiveDown"
|
||||
},
|
||||
{
|
||||
"dateString":"2019-07-30T02:14:50.450+0200",
|
||||
"sgv":129,
|
||||
"direction":"FortyFiveDown"
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"result": [
|
||||
{
|
||||
"dateString": "2019-07-30T02:24:50.434+0200",
|
||||
"sgv": 115,
|
||||
"direction": "FortyFiveDown"
|
||||
},
|
||||
{
|
||||
"dateString": "2019-07-30T02:19:50.374+0200",
|
||||
"sgv": 121,
|
||||
"direction": "FortyFiveDown"
|
||||
},
|
||||
{
|
||||
"dateString": "2019-07-30T02:14:50.450+0200",
|
||||
"sgv": 129,
|
||||
"direction": "FortyFiveDown"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -115,7 +124,7 @@ Sample result:
|
||||
Sample POST `/treatments` client code:
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
const doc = {
|
||||
date: 1564591511232, // (new Date()).getTime(),
|
||||
app: 'AndroidAPS',
|
||||
@@ -129,11 +138,15 @@ request({
|
||||
json: true,
|
||||
url: `https://nsapiv3.herokuapp.com/api/v3/treatments?${auth}`
|
||||
},
|
||||
(error, response, body) => console.log(response.headers.location));
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```
|
||||
/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895
|
||||
```json
|
||||
{
|
||||
"status": 201,
|
||||
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
|
||||
"lastModified": 1564591511711
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -145,26 +158,29 @@ Sample result:
|
||||
Sample GET `/treatments/{identifier}` client code:
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
|
||||
|
||||
request(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`,
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```
|
||||
{
|
||||
"date":1564591511232,
|
||||
"app":"AndroidAPS",
|
||||
"device":"Samsung XCover 4-861536030196001",
|
||||
"eventType":"Correction Bolus",
|
||||
"insulin":0.3,
|
||||
"identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895",
|
||||
"utcOffset":0,
|
||||
"created_at":"2019-07-31T16:45:11.232Z",
|
||||
"srvModified":1564591627732,
|
||||
"srvCreated":1564591511711,
|
||||
"subject":"test-admin"
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"result": {
|
||||
"date": 1564591511232,
|
||||
"app": "AndroidAPS",
|
||||
"device": "Samsung XCover 4-861536030196001",
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.3,
|
||||
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
|
||||
"utcOffset": 0,
|
||||
"created_at": "2019-07-31T16:45:11.232Z",
|
||||
"srvModified": 1564591627732,
|
||||
"srvCreated": 1564591511711,
|
||||
"subject": "test-admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -177,20 +193,23 @@ Sample result:
|
||||
Sample GET `/lastModified` client code (to get latest modification dates):
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
|
||||
request(`https://nsapiv3.herokuapp.com/api/v3/lastModified?${auth}`,
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```javascript
|
||||
```json
|
||||
{
|
||||
"srvDate":1564591783202,
|
||||
"collections":{
|
||||
"devicestatus":1564591490074,
|
||||
"entries":1564591486801,
|
||||
"profile":1548524042744,
|
||||
"treatments":1564591627732
|
||||
"status": 200,
|
||||
"result": {
|
||||
"srvDate": 1564591783202,
|
||||
"collections": {
|
||||
"devicestatus": 1564591490074,
|
||||
"entries": 1564591486801,
|
||||
"profile": 1548524042744,
|
||||
"treatments": 1564591627732
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -204,7 +223,7 @@ Sample result:
|
||||
Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4):
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
|
||||
const doc = {
|
||||
date: 1564591511232,
|
||||
@@ -220,11 +239,13 @@ request({
|
||||
json: true,
|
||||
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
|
||||
},
|
||||
(error, response, body) => console.log(response.statusCode));
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```
|
||||
204
|
||||
```json
|
||||
{
|
||||
"status": 200
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -236,7 +257,7 @@ Sample result:
|
||||
Sample PATCH `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5):
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
|
||||
const doc = {
|
||||
insulin: 0.5
|
||||
@@ -248,11 +269,13 @@ request({
|
||||
json: true,
|
||||
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
|
||||
},
|
||||
(error, response, body) => console.log(response.statusCode));
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```
|
||||
204
|
||||
```json
|
||||
{
|
||||
"status": 200
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -264,18 +287,20 @@ Sample result:
|
||||
Sample DELETE `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5):
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
const identifier = '95e1a6e3-1146-5d6a-a3f1-41567cae0895';
|
||||
|
||||
request({
|
||||
method: 'delete',
|
||||
url: `https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}?${auth}`
|
||||
},
|
||||
(error, response, body) => console.log(response.statusCode));
|
||||
(error, response, body) => console.log(body));
|
||||
```
|
||||
Sample result:
|
||||
```
|
||||
204
|
||||
```json
|
||||
{
|
||||
"status": 200
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -287,43 +312,46 @@ Sample result:
|
||||
Sample HISTORY `/treatments/history/{lastModified}` client code:
|
||||
```javascript
|
||||
const request = require('request');
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5&now=${new Date().getTime()}`;
|
||||
const auth = `token=testadmin-ad3b1f9d7b3f59d5`;
|
||||
const lastModified = 1564521267421;
|
||||
|
||||
request(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}?${auth}`,
|
||||
(error, response, body) => console.log(response.body));
|
||||
```
|
||||
Sample result:
|
||||
```
|
||||
[
|
||||
{
|
||||
"date":1564521267421,
|
||||
"app":"AndroidAPS",
|
||||
"device":"Samsung XCover 4-861536030196001",
|
||||
"eventType":"Correction Bolus",
|
||||
"insulin":0.5,
|
||||
"utcOffset":0,
|
||||
"created_at":"2019-07-30T21:14:27.421Z",
|
||||
"identifier":"95e1a6e3-1146-5d6a-a3f1-41567cae0895",
|
||||
"srvModified":1564592440416,
|
||||
"srvCreated":1564592334853,
|
||||
"subject":"test-admin",
|
||||
"modifiedBy":"test-admin",
|
||||
"isValid":false
|
||||
},
|
||||
{
|
||||
"date":1564592545299,
|
||||
"app":"AndroidAPS",
|
||||
"device":"Samsung XCover 4-861536030196001",
|
||||
"eventType":"Snack Bolus",
|
||||
"carbs":10,
|
||||
"identifier":"267c43c2-f629-5191-a542-4f410c69e486",
|
||||
"utcOffset":0,
|
||||
"created_at":"2019-07-31T17:02:25.299Z",
|
||||
"srvModified":1564592545781,
|
||||
"srvCreated":1564592545781,
|
||||
"subject":"test-admin"
|
||||
}
|
||||
]
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"result": [
|
||||
{
|
||||
"date": 1564521267421,
|
||||
"app": "AndroidAPS",
|
||||
"device": "Samsung XCover 4-861536030196001",
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.5,
|
||||
"utcOffset": 0,
|
||||
"created_at": "2019-07-30T21:14:27.421Z",
|
||||
"identifier": "95e1a6e3-1146-5d6a-a3f1-41567cae0895",
|
||||
"srvModified": 1564592440416,
|
||||
"srvCreated": 1564592334853,
|
||||
"subject": "test-admin",
|
||||
"modifiedBy": "test-admin",
|
||||
"isValid": false
|
||||
},
|
||||
{
|
||||
"date": 1564592545299,
|
||||
"app": "AndroidAPS",
|
||||
"device": "Samsung XCover 4-861536030196001",
|
||||
"eventType": "Snack Bolus",
|
||||
"carbs": 10,
|
||||
"identifier": "267c43c2-f629-5191-a542-4f410c69e486",
|
||||
"utcOffset": 0,
|
||||
"created_at": "2019-07-31T17:02:25.299Z",
|
||||
"srvModified": 1564592545781,
|
||||
"srvCreated": 1564592545781,
|
||||
"subject": "test-admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Notice the `"isValid":false` field marking the deletion of the document.
|
||||
|
||||
@@ -4,6 +4,7 @@ const apiConst = require('../../const.json')
|
||||
, security = require('../../security')
|
||||
, validate = require('./validate.js')
|
||||
, path = require('path')
|
||||
, opTools = require('../../shared/operationTools')
|
||||
;
|
||||
|
||||
/**
|
||||
@@ -35,7 +36,13 @@ async function insert (opCtx, doc) {
|
||||
|
||||
res.setHeader('Last-Modified', now.toUTCString());
|
||||
res.setHeader('Location', path.posix.join(req.baseUrl, req.path, identifier));
|
||||
res.status(apiConst.HTTP.CREATED).send({ });
|
||||
|
||||
|
||||
const fields = {
|
||||
identifier: identifier,
|
||||
lastModified: now.getTime()
|
||||
};
|
||||
opTools.sendJSON({ res, status: apiConst.HTTP.CREATED, fields: fields });
|
||||
|
||||
ctx.bus.emit('storage-socket-create', { colName: col.colName, doc });
|
||||
col.autoPrune();
|
||||
@@ -43,4 +50,4 @@ async function insert (opCtx, doc) {
|
||||
}
|
||||
|
||||
|
||||
module.exports = insert;
|
||||
module.exports = insert;
|
||||
|
||||
@@ -36,7 +36,7 @@ async function validateDelete (opCtx) {
|
||||
throw new Error('empty result');
|
||||
|
||||
if (result.length === 0) {
|
||||
return res.status(apiConst.HTTP.NOT_FOUND).end();
|
||||
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
|
||||
}
|
||||
else {
|
||||
const storageDoc = result[0];
|
||||
@@ -62,13 +62,13 @@ async function deletePermanently (opCtx) {
|
||||
throw new Error('empty result');
|
||||
|
||||
if (!result.deleted) {
|
||||
return res.status(apiConst.HTTP.NOT_FOUND).end();
|
||||
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
|
||||
}
|
||||
|
||||
col.autoPrune();
|
||||
ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier });
|
||||
ctx.bus.emit('data-received');
|
||||
return res.status(apiConst.HTTP.NO_CONTENT).end();
|
||||
return opTools.sendJSON({ res, status: apiConst.HTTP.OK });
|
||||
}
|
||||
|
||||
|
||||
@@ -89,13 +89,13 @@ async function markAsDeleted (opCtx) {
|
||||
throw new Error('empty result');
|
||||
|
||||
if (!result.updated) {
|
||||
return res.status(apiConst.HTTP.NOT_FOUND).end();
|
||||
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
|
||||
}
|
||||
|
||||
ctx.bus.emit('storage-socket-delete', { colName: col.colName, identifier });
|
||||
col.autoPrune();
|
||||
ctx.bus.emit('data-received');
|
||||
return res.status(apiConst.HTTP.NO_CONTENT).end();
|
||||
return opTools.sendJSON({ res, status: apiConst.HTTP.OK });
|
||||
}
|
||||
|
||||
|
||||
@@ -119,4 +119,4 @@ function deleteOperation (ctx, env, app, col) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = deleteOperation;
|
||||
module.exports = deleteOperation;
|
||||
|
||||
@@ -39,7 +39,8 @@ async function history (opCtx, fieldsProjector) {
|
||||
throw new Error('empty result');
|
||||
|
||||
if (result.length === 0) {
|
||||
return res.status(apiConst.HTTP.NO_CONTENT).end();
|
||||
res.status(apiConst.HTTP.OK);
|
||||
return renderer.render(res, result);
|
||||
}
|
||||
|
||||
_.each(result, col.resolveDates);
|
||||
|
||||
@@ -36,7 +36,7 @@ async function patch (opCtx) {
|
||||
|
||||
const storageDoc = result[0];
|
||||
if (storageDoc.isValid === false) {
|
||||
return res.status(apiConst.HTTP.GONE).end();
|
||||
return opTools.sendJSONStatus(res, apiConst.HTTP.GONE);
|
||||
}
|
||||
|
||||
const modifiedDate = col.resolveDates(storageDoc)
|
||||
@@ -44,13 +44,13 @@ async function patch (opCtx) {
|
||||
|
||||
if (ifUnmodifiedSince
|
||||
&& dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) {
|
||||
return res.status(apiConst.HTTP.PRECONDITION_FAILED).end();
|
||||
return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
await applyPatch(opCtx, identifier, doc, storageDoc);
|
||||
}
|
||||
else {
|
||||
return res.status(apiConst.HTTP.NOT_FOUND).end();
|
||||
return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ async function applyPatch (opCtx, identifier, doc, storageDoc) {
|
||||
throw new Error('matchedCount empty');
|
||||
|
||||
res.setHeader('Last-Modified', now.toUTCString());
|
||||
res.status(apiConst.HTTP.NO_CONTENT).send({ });
|
||||
opTools.sendJSONStatus(res, apiConst.HTTP.OK);
|
||||
|
||||
const fieldsProjector = new FieldsProjector('_all');
|
||||
const patchedDocs = await col.storage.findOne(identifier, fieldsProjector);
|
||||
@@ -115,4 +115,4 @@ function patchOperation (ctx, env, app, col) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = patchOperation;
|
||||
module.exports = patchOperation;
|
||||
|
||||
@@ -26,12 +26,12 @@ async function read (opCtx) {
|
||||
throw new Error('empty result');
|
||||
|
||||
if (result.length === 0) {
|
||||
return res.status(apiConst.HTTP.NOT_FOUND).end();
|
||||
return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND });
|
||||
}
|
||||
|
||||
const doc = result[0];
|
||||
if (doc.isValid === false) {
|
||||
return res.status(apiConst.HTTP.GONE).end();
|
||||
return opTools.sendJSON({ res, status: apiConst.HTTP.GONE });
|
||||
}
|
||||
|
||||
|
||||
@@ -74,4 +74,4 @@ function readOperation (ctx, env, app, col) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = readOperation;
|
||||
module.exports = readOperation;
|
||||
|
||||
@@ -48,7 +48,7 @@ async function updateConditional (opCtx, doc, storageDoc) {
|
||||
const { col, req, res } = opCtx;
|
||||
|
||||
if (storageDoc.isValid === false) {
|
||||
return res.status(apiConst.HTTP.GONE).end();
|
||||
return opTools.sendJSONStatus(res, apiConst.HTTP.GONE);
|
||||
}
|
||||
|
||||
const modifiedDate = col.resolveDates(storageDoc)
|
||||
@@ -56,7 +56,7 @@ async function updateConditional (opCtx, doc, storageDoc) {
|
||||
|
||||
if (ifUnmodifiedSince
|
||||
&& dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) {
|
||||
return res.status(apiConst.HTTP.PRECONDITION_FAILED).end();
|
||||
return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
await replace(opCtx, doc, storageDoc);
|
||||
@@ -83,4 +83,4 @@ function updateOperation (ctx, env, app, col) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = updateOperation;
|
||||
module.exports = updateOperation;
|
||||
|
||||
@@ -4,6 +4,7 @@ const apiConst = require('../../const.json')
|
||||
, security = require('../../security')
|
||||
, validate = require('./validate.js')
|
||||
, path = require('path')
|
||||
, opTools = require('../../shared/operationTools')
|
||||
;
|
||||
|
||||
/**
|
||||
@@ -37,12 +38,20 @@ async function replace (opCtx, doc, storageDoc, options) {
|
||||
throw new Error('empty matchedCount');
|
||||
|
||||
res.setHeader('Last-Modified', now.toUTCString());
|
||||
const fields = {
|
||||
lastModified: now.getTime()
|
||||
}
|
||||
|
||||
if (storageDoc.identifier !== doc.identifier || isDeduplication) {
|
||||
res.setHeader('Location', path.posix.join(req.baseUrl, req.path, doc.identifier));
|
||||
fields.identifier = doc.identifier;
|
||||
fields.isDeduplication = true;
|
||||
if (storageDoc.identifier !== doc.identifier) {
|
||||
fields.deduplicatedIdentifier = storageDoc.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(apiConst.HTTP.NO_CONTENT).send({ });
|
||||
opTools.sendJSON({ res, status: apiConst.HTTP.OK, fields });
|
||||
|
||||
ctx.bus.emit('storage-socket-update', { colName: col.colName, doc });
|
||||
col.autoPrune();
|
||||
@@ -50,4 +59,4 @@ async function replace (opCtx, doc, storageDoc, options) {
|
||||
}
|
||||
|
||||
|
||||
module.exports = replace;
|
||||
module.exports = replace;
|
||||
|
||||
+5
-1
@@ -7,6 +7,7 @@ const express = require('express')
|
||||
, apiConst = require('./const.json')
|
||||
, security = require('./security')
|
||||
, genericSetup = require('./generic/setup')
|
||||
, opTools = require('./shared/operationTools')
|
||||
;
|
||||
|
||||
function configure (env, ctx) {
|
||||
@@ -65,7 +66,6 @@ function configure (env, ctx) {
|
||||
app.set('enabledCollections', ['devicestatus', 'entries', 'food', 'profile', 'settings', 'treatments']);
|
||||
|
||||
self.setENVTruthy('API3_SECURITY_ENABLE', apiConst.API3_SECURITY_ENABLE);
|
||||
self.setENVTruthy('API3_TIME_SKEW_TOLERANCE', apiConst.API3_TIME_SKEW_TOLERANCE);
|
||||
self.setENVTruthy('API3_DEDUP_FALLBACK_ENABLED', apiConst.API3_DEDUP_FALLBACK_ENABLED);
|
||||
self.setENVTruthy('API3_CREATED_AT_FALLBACK_ENABLED', apiConst.API3_CREATED_AT_FALLBACK_ENABLED);
|
||||
self.setENVTruthy('API3_MAX_LIMIT', apiConst.API3_MAX_LIMIT);
|
||||
@@ -104,6 +104,10 @@ function configure (env, ctx) {
|
||||
res.redirect(307, '../../../api3-docs');
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND, apiConst.MSG.HTTP_404_BAD_OPERATION);
|
||||
})
|
||||
|
||||
ctx.storageSocket = new StorageSocket(app, env, ctx);
|
||||
|
||||
return app;
|
||||
|
||||
+2
-45
@@ -1,10 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const moment = require('moment')
|
||||
, apiConst = require('./const.json')
|
||||
const apiConst = require('./const.json')
|
||||
, _ = require('lodash')
|
||||
, shiroTrie = require('shiro-trie')
|
||||
, dateTools = require('./shared/dateTools')
|
||||
, opTools = require('./shared/operationTools')
|
||||
;
|
||||
|
||||
@@ -13,37 +11,6 @@ function getRemoteIP (req) {
|
||||
return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Date header in HTTP request (or 'now' query parameter) is present and valid (with error response sending)
|
||||
*/
|
||||
function checkDateHeader (opCtx) {
|
||||
|
||||
const { app, req, res } = opCtx;
|
||||
|
||||
let dateString = req.header('Date');
|
||||
if (!dateString) {
|
||||
dateString = req.query.now;
|
||||
}
|
||||
|
||||
if (!dateString) {
|
||||
return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_DATE);
|
||||
}
|
||||
|
||||
let dateMoment = dateTools.parseToMoment(dateString);
|
||||
if (!dateMoment) {
|
||||
return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_DATE);
|
||||
}
|
||||
|
||||
let nowMoment = moment(new Date());
|
||||
let diffMinutes = moment.duration(nowMoment.diff(dateMoment)).asMinutes();
|
||||
|
||||
if (Math.abs(diffMinutes) > app.get('API3_TIME_SKEW_TOLERANCE')) {
|
||||
return opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_DATE_OUT_OF_TOLERANCE);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function authenticate (opCtx) {
|
||||
return new Promise(function promise (resolve, reject) {
|
||||
@@ -56,16 +23,6 @@ function authenticate (opCtx) {
|
||||
return resolve({ shiros: [ adminShiro ] });
|
||||
}
|
||||
|
||||
if (req.protocol !== 'https') {
|
||||
return reject(
|
||||
opTools.sendJSONStatus(res, apiConst.HTTP.FORBIDDEN, apiConst.MSG.HTTP_403_NOT_USING_HTTPS));
|
||||
}
|
||||
|
||||
const checkDateResult = checkDateHeader(opCtx);
|
||||
if (checkDateResult !== true) {
|
||||
return checkDateResult;
|
||||
}
|
||||
|
||||
let token = ctx.authorization.extractToken(req);
|
||||
if (!token) {
|
||||
return reject(
|
||||
@@ -123,4 +80,4 @@ module.exports = {
|
||||
authenticate,
|
||||
checkPermission,
|
||||
demandPermission
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,18 +6,41 @@ const apiConst = require('../const.json')
|
||||
, uuidNamespace = [...Buffer.from("NightscoutRocks!", "ascii")] // official namespace for NS :-)
|
||||
;
|
||||
|
||||
|
||||
function sendJSON ({ res, result, status, fields }) {
|
||||
|
||||
const json = {
|
||||
status: status || apiConst.HTTP.OK,
|
||||
result: result
|
||||
};
|
||||
|
||||
if (result) {
|
||||
json.result = result
|
||||
}
|
||||
|
||||
if (fields) {
|
||||
Object.assign(json, fields);
|
||||
}
|
||||
|
||||
res.status(json.status).json(json);
|
||||
}
|
||||
|
||||
|
||||
function sendJSONStatus (res, status, title, description, warning) {
|
||||
|
||||
const json = {
|
||||
status: status,
|
||||
message: title,
|
||||
description: description
|
||||
status: status
|
||||
};
|
||||
|
||||
if (title) { json.message = title }
|
||||
|
||||
if (description) { json.description = description }
|
||||
|
||||
// Add optional warning message.
|
||||
if (warning) { json.warning = warning; }
|
||||
|
||||
res.status(status).json(json);
|
||||
res.status(status)
|
||||
.json(json);
|
||||
|
||||
return title;
|
||||
}
|
||||
@@ -104,8 +127,9 @@ function resolveIdentifier (doc) {
|
||||
|
||||
|
||||
module.exports = {
|
||||
sendJSON,
|
||||
sendJSONStatus,
|
||||
validateCommon,
|
||||
calculateIdentifier,
|
||||
resolveIdentifier
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ function extension2accept (req, res, next) {
|
||||
*/
|
||||
function render (res, data) {
|
||||
res.format({
|
||||
'json': () => res.send(data),
|
||||
'json': () => renderJson(res, data),
|
||||
'csv': () => renderCsv(res, data),
|
||||
'xml': () => renderXml(res, data),
|
||||
'default': () =>
|
||||
@@ -62,6 +62,19 @@ function render (res, data) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format data to output as JSON
|
||||
* @param {Object} res
|
||||
* @param {any} data
|
||||
*/
|
||||
function renderJson (res, data) {
|
||||
res.send({
|
||||
status: apiConst.HTTP.OK,
|
||||
result: data
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format data to output as .csv
|
||||
* @param {Object} res
|
||||
@@ -96,4 +109,4 @@ function renderXml (res, data) {
|
||||
module.exports = {
|
||||
extension2accept,
|
||||
render
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ function configure (app, ctx, env) {
|
||||
return { colName: col.colName, lastModified: result };
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function collectionsAsync (auth) {
|
||||
|
||||
const cols = app.get('collections')
|
||||
@@ -77,7 +77,7 @@ function configure (app, ctx, env) {
|
||||
|
||||
info.collections = await collectionsAsync(auth);
|
||||
|
||||
res.json(info);
|
||||
opTools.sendJSON({ res, result: info });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ function configure (app, ctx, env) {
|
||||
}
|
||||
}
|
||||
|
||||
res.json(info);
|
||||
opTools.sendJSON({ res, result: info });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ function configure (app) {
|
||||
try {
|
||||
const versionInfo = await storageTools.getVersionInfo(app);
|
||||
|
||||
res.json(versionInfo);
|
||||
opTools.sendJSON({ res, result: versionInfo });
|
||||
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
|
||||
+854
-443
File diff suppressed because it is too large
Load Diff
+236
-116
@@ -2,7 +2,7 @@ openapi: 3.0.0
|
||||
servers:
|
||||
- url: '/api/v3'
|
||||
info:
|
||||
version: "3.0.1"
|
||||
version: 3.0.3
|
||||
title: Nightscout API
|
||||
contact:
|
||||
name: NS development discussion channel
|
||||
@@ -22,11 +22,6 @@ info:
|
||||
but this should never be set to false in production.
|
||||
|
||||
|
||||
- Number of minutes of acceptable time skew between client's and server's clock (optional, default = 5)
|
||||
<pre>API3_TIME_SKEW_TOLERANCE=5</pre>
|
||||
This security parameter is used for preventing anti-replay attacks, specifically when checking the time from `Date` header.
|
||||
|
||||
|
||||
- Maximum limit count of documents retrieved from single query
|
||||
<pre>API3_MAX_LIMIT=1000</pre>
|
||||
|
||||
@@ -80,8 +75,6 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/paramCollection'
|
||||
|
||||
- $ref: '#/components/parameters/dateHeader'
|
||||
- $ref: '#/components/parameters/nowParam'
|
||||
- $ref: '#/components/parameters/tokenParam'
|
||||
|
||||
######################################################################################
|
||||
@@ -102,7 +95,7 @@ paths:
|
||||
3) paging - using `limit` and `skip` parameters
|
||||
|
||||
|
||||
When there is no document matching the filtering criteria, HTTP status 204 is returned with empty response content. Otherwise HTTP 200 code is returned with JSON array of matching documents as a response content.
|
||||
If successful, HTTP 200 code is returned with JSON array of matching documents as a response content (it may be empty).
|
||||
|
||||
|
||||
This operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`).
|
||||
@@ -120,13 +113,12 @@ paths:
|
||||
- $ref: '#/components/parameters/fieldsParam'
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/search200'
|
||||
204:
|
||||
$ref: '#/components/responses/search204'
|
||||
400:
|
||||
$ref: '#/components/responses/400BadRequest'
|
||||
401:
|
||||
@@ -145,10 +137,11 @@ paths:
|
||||
- generic
|
||||
summary: 'CREATE: Inserts a new document into the collection'
|
||||
description:
|
||||
Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified and with an empty response content. `identifier` can be parsed from the `Location` response header.
|
||||
Using this operation you can insert new documents into collection. Normally the operation ends with 201 HTTP status code, `Last-Modified` and `Location` headers specified.
|
||||
`identifier` is included in response body or it can be parsed from the `Location` response header.
|
||||
|
||||
|
||||
When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 204 HTTP status code along with `Last-Modified` and correct `Location` headers.
|
||||
When the document to post is marked as a duplicate (using rules described at `API3_DEDUP_FALLBACK_ENABLED` switch), the update operation takes place instead of inserting. In this case the original document in the collection is found and it gets updated by the actual operation POST body. Finally the operation ends with 200 HTTP status code along with `Last-Modified` and correct `Location` headers. The response body then includes `isDeduplication`=`true` and `deduplicatedIdentifier` fields.
|
||||
|
||||
|
||||
This operation provides autopruning of the collection (if autopruning is enabled).
|
||||
@@ -165,13 +158,14 @@ paths:
|
||||
$ref: '#/components/schemas/DocumentToPost'
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/200Deduplication'
|
||||
201:
|
||||
$ref: '#/components/responses/201CreatedLocation'
|
||||
204:
|
||||
$ref: '#/components/responses/204NoContentLocation'
|
||||
400:
|
||||
$ref: '#/components/responses/400BadRequest'
|
||||
401:
|
||||
@@ -202,8 +196,6 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/paramIdentifier'
|
||||
|
||||
- $ref: '#/components/parameters/dateHeader'
|
||||
- $ref: '#/components/parameters/nowParam'
|
||||
- $ref: '#/components/parameters/tokenParam'
|
||||
|
||||
######################################################################################
|
||||
@@ -215,7 +207,7 @@ paths:
|
||||
Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code.
|
||||
|
||||
|
||||
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
|
||||
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
|
||||
|
||||
|
||||
When `If-Modified-Since` header is used and its value is greater than the timestamp of the document in the collection, 304 HTTP status code with empty response content is returned. It means that the document has not been modified on server since the last retrieval to client side.
|
||||
@@ -229,7 +221,8 @@ paths:
|
||||
- $ref: '#/components/parameters/fieldsParam'
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
200:
|
||||
@@ -254,10 +247,10 @@ paths:
|
||||
- generic
|
||||
summary: 'UPDATE: Updates a document in the collection'
|
||||
description:
|
||||
Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 204 HTTP status code will be returned with empty response body.
|
||||
Normally the document with the matching `identifier` will be replaced in the collection by the whole JSON request body and 200 HTTP status code will be returned.
|
||||
|
||||
|
||||
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
|
||||
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
|
||||
|
||||
|
||||
When no document with `identifier` has been found in the collection, then an insert operation takes place instead of updating. Finally 201 HTTP status code is returned with only `Last-Modified` header (`identifier` is already known from the path parameter).
|
||||
@@ -283,13 +276,14 @@ paths:
|
||||
$ref: '#/components/schemas/DocumentToPost'
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/200Ok'
|
||||
201:
|
||||
$ref: '#/components/responses/201Created'
|
||||
204:
|
||||
$ref: '#/components/responses/204NoContentLocation'
|
||||
400:
|
||||
$ref: '#/components/responses/400BadRequest'
|
||||
401:
|
||||
@@ -312,10 +306,10 @@ paths:
|
||||
- generic
|
||||
summary: 'PATCH: Partially updates document in the collection'
|
||||
description:
|
||||
Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 204 HTTP status code will be returned with empty response body.
|
||||
Normally the document with the matching `identifier` will be retrieved from the collection and it will be patched by all specified fields from the JSON request body. Finally 200 HTTP status code will be returned.
|
||||
|
||||
|
||||
If the document has been found in the collection but it had already been deleted, 410 HTTP status code with empty response content is to be returned.
|
||||
If the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.
|
||||
|
||||
|
||||
When no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code.
|
||||
@@ -347,11 +341,12 @@ paths:
|
||||
$ref: '#/components/schemas/DocumentToPost'
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
204:
|
||||
$ref: '#/components/responses/204NoContentLocation'
|
||||
200:
|
||||
$ref: '#/components/responses/200Ok'
|
||||
400:
|
||||
$ref: '#/components/responses/400BadRequest'
|
||||
401:
|
||||
@@ -360,10 +355,10 @@ paths:
|
||||
$ref: '#/components/responses/403Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/404NotFound'
|
||||
412:
|
||||
$ref: '#/components/responses/412PreconditionFailed'
|
||||
410:
|
||||
$ref: '#/components/responses/410Gone'
|
||||
412:
|
||||
$ref: '#/components/responses/412PreconditionFailed'
|
||||
422:
|
||||
$ref: '#/components/responses/422UnprocessableEntity'
|
||||
|
||||
@@ -387,11 +382,12 @@ paths:
|
||||
- $ref: '#/components/parameters/permanentParam'
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
204:
|
||||
description: Successful operation - empty response
|
||||
200:
|
||||
$ref: '#/components/responses/200Ok'
|
||||
401:
|
||||
$ref: '#/components/responses/401Unauthorized'
|
||||
403:
|
||||
@@ -412,8 +408,6 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/paramCollection'
|
||||
|
||||
- $ref: '#/components/parameters/dateHeader'
|
||||
- $ref: '#/components/parameters/nowParam'
|
||||
- $ref: '#/components/parameters/tokenParam'
|
||||
|
||||
get:
|
||||
@@ -430,9 +424,6 @@ paths:
|
||||
Deleted documents will appear with `isValid` = `false` field.
|
||||
|
||||
|
||||
When there is no change detected since the timestamp the operation ends with 204 HTTP status code and empty response content.
|
||||
|
||||
|
||||
HISTORY operation has a fallback mechanism in place for documents, which were not created by API v3. For such documents `srvModified` is virtually assigned from the `date` field (for `entries` collection) or from the `created_at` field (for other collections).
|
||||
|
||||
|
||||
@@ -448,13 +439,12 @@ paths:
|
||||
- $ref: '#/components/parameters/fieldsParam'
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/history200'
|
||||
204:
|
||||
$ref: '#/components/responses/history204'
|
||||
400:
|
||||
$ref: '#/components/responses/400BadRequest'
|
||||
401:
|
||||
@@ -485,8 +475,6 @@ paths:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
- $ref: '#/components/parameters/dateHeader'
|
||||
- $ref: '#/components/parameters/nowParam'
|
||||
- $ref: '#/components/parameters/tokenParam'
|
||||
|
||||
get:
|
||||
@@ -509,13 +497,12 @@ paths:
|
||||
- $ref: '#/components/parameters/fieldsParam'
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/history200'
|
||||
204:
|
||||
$ref: '#/components/responses/history204'
|
||||
400:
|
||||
$ref: '#/components/responses/400BadRequest'
|
||||
401:
|
||||
@@ -556,7 +543,8 @@ paths:
|
||||
This operation requires authorization in contrast with VERSION operation.
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
200:
|
||||
@@ -573,8 +561,6 @@ paths:
|
||||
######################################################################################
|
||||
/lastModified:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/dateHeader'
|
||||
- $ref: '#/components/parameters/nowParam'
|
||||
- $ref: '#/components/parameters/tokenParam'
|
||||
|
||||
get:
|
||||
@@ -591,7 +577,8 @@ paths:
|
||||
This operation requires `read` permission for the API and the collections (e.g. `api:treatments:read`). For each collection the permission is checked separately, you will get timestamps only for those collections that you have access to.
|
||||
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
- accessToken: []
|
||||
- jwtoken: []
|
||||
|
||||
responses:
|
||||
200:
|
||||
@@ -606,41 +593,6 @@ components:
|
||||
|
||||
parameters:
|
||||
|
||||
dateHeader:
|
||||
in: header
|
||||
name: Date
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
description:
|
||||
Timestamp (defined by client's clock) when the HTTP request was constructed on client.
|
||||
This mandatory header serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code.
|
||||
This can be set alternatively in `now` query parameter.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
<pre>Date: Wed, 17 Oct 2018 05:13:00 GMT</pre>
|
||||
|
||||
|
||||
nowParam:
|
||||
in: query
|
||||
name: now
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
required: false
|
||||
description:
|
||||
Timestamp (defined by client's clock) when the HTTP request was constructed on client.
|
||||
This mandatory parameter serves as an anti-replay precaution. After a period of time (specified by `API3_TIME_SKEW_TOLERANCE`) the message won't be valid any more and it will be denied with HTTP 401 Unauthorized code.
|
||||
This can be set alternatively in `Date` header.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
<pre>now=1525383610088</pre>
|
||||
|
||||
|
||||
tokenParam:
|
||||
in: query
|
||||
@@ -850,11 +802,54 @@ components:
|
||||
######################################################################################
|
||||
responses:
|
||||
|
||||
200Ok:
|
||||
description: The request was successfully processed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 200
|
||||
|
||||
200Deduplication:
|
||||
description: Successfully updated a duplicate document in the collection
|
||||
headers:
|
||||
'Last-Modified':
|
||||
$ref: '#/components/schemas/headerLastModified'
|
||||
'Location':
|
||||
$ref: '#/components/schemas/headerLocation'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 200
|
||||
identifier:
|
||||
$ref: '#/components/schemas/identifierField'
|
||||
isDeduplication:
|
||||
$ref: '#/components/schemas/isDeduplicationField'
|
||||
deduplicatedIdentifier:
|
||||
$ref: '#/components/schemas/deduplicatedIdentifierField'
|
||||
|
||||
|
||||
201Created:
|
||||
description: Successfully created a new document in collection
|
||||
headers:
|
||||
'Last-Modified':
|
||||
$ref: '#/components/schemas/headerLastModified'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 201
|
||||
identifier:
|
||||
$ref: '#/components/schemas/identifierField'
|
||||
lastModified:
|
||||
$ref: '#/components/schemas/lastModifiedField'
|
||||
|
||||
201CreatedLocation:
|
||||
description: Successfully created a new document in collection
|
||||
@@ -863,20 +858,17 @@ components:
|
||||
$ref: '#/components/schemas/headerLastModified'
|
||||
'Location':
|
||||
$ref: '#/components/schemas/headerLocation'
|
||||
|
||||
204NoContent:
|
||||
description: Successfully finished operation
|
||||
headers:
|
||||
'Last-Modified':
|
||||
$ref: '#/components/schemas/headerLastModified'
|
||||
|
||||
204NoContentLocation:
|
||||
description: Successfully finished operation
|
||||
headers:
|
||||
'Last-Modified':
|
||||
$ref: '#/components/schemas/headerLastModified'
|
||||
'Location':
|
||||
$ref: '#/components/schemas/headerLocation'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 201
|
||||
identifier:
|
||||
$ref: '#/components/schemas/identifierField'
|
||||
lastModified:
|
||||
$ref: '#/components/schemas/lastModifiedField'
|
||||
|
||||
304NotModified:
|
||||
description: The document has not been modified on the server since timestamp specified in If-Modified-Since header
|
||||
@@ -886,34 +878,95 @@ components:
|
||||
|
||||
400BadRequest:
|
||||
description: The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 400
|
||||
|
||||
401Unauthorized:
|
||||
description: The request was not successfully authenticated using access token or JWT, or the request has missing `Date` header or it contains an expired timestamp, so that the request cannot continue due to the security policy.
|
||||
description: The request was not successfully authenticated using access token or JWT, so that the request cannot continue due to the security policy.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 401
|
||||
|
||||
403Forbidden:
|
||||
description: Insecure HTTP scheme used or the request has been successfully authenticated, but the security subject is not authorized for the operation.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 403
|
||||
|
||||
404NotFound:
|
||||
description: The collection or document specified was not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 404
|
||||
|
||||
406NotAcceptable:
|
||||
description: The requested content type (in `Accept` header) is not supported.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 406
|
||||
|
||||
412PreconditionFailed:
|
||||
description: The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 412
|
||||
|
||||
410Gone:
|
||||
description: The requested document has already been deleted.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 410
|
||||
|
||||
422UnprocessableEntity:
|
||||
description: The client request is well formed but a server validation error occured. Eg. when trying to modify or delete a read-only document (having `isReadOnly=true`).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 422
|
||||
|
||||
search200:
|
||||
description: Successful operation returning array of documents matching the filtering criteria
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DocumentArray'
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 200
|
||||
result:
|
||||
$ref: '#/components/schemas/DocumentArray'
|
||||
text/csv:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DocumentArray'
|
||||
@@ -921,15 +974,17 @@ components:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DocumentArray'
|
||||
|
||||
search204:
|
||||
description: Successful operation - no documents matching the filtering criteria
|
||||
|
||||
read200:
|
||||
description: The document has been succesfully found and its JSON form returned in the response content.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Document'
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 200
|
||||
result:
|
||||
$ref: '#/components/schemas/Document'
|
||||
text/csv:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Document'
|
||||
@@ -946,7 +1001,12 @@ components:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DocumentArray'
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 200
|
||||
result:
|
||||
$ref: '#/components/schemas/DocumentArray'
|
||||
text/csv:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DocumentArray'
|
||||
@@ -959,15 +1019,18 @@ components:
|
||||
'ETag':
|
||||
$ref: '#/components/schemas/headerEtagLastModifiedMaximum'
|
||||
|
||||
history204:
|
||||
description: No changes detected
|
||||
|
||||
lastModified200:
|
||||
description: Successful operation returning the timestamps
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LastModifiedResult'
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 200
|
||||
result:
|
||||
$ref: '#/components/schemas/LastModifiedResult'
|
||||
|
||||
|
||||
######################################################################################
|
||||
schemas:
|
||||
@@ -1025,6 +1088,44 @@ components:
|
||||
example: '53409478-105f-11e9-ab14-d663bd873d93'
|
||||
|
||||
|
||||
identifierField:
|
||||
description:
|
||||
Identifier of created or modified document
|
||||
type: string
|
||||
example: '53409478-105f-11e9-ab14-d663bd873d93'
|
||||
|
||||
|
||||
lastModifiedField:
|
||||
type: integer
|
||||
format: int64
|
||||
description:
|
||||
Timestamp of the last document modification on the server, formatted as
|
||||
|
||||
Unix epoch in milliseconds (1525383610088)
|
||||
example: 1525383610088
|
||||
|
||||
statusField:
|
||||
type: integer
|
||||
description:
|
||||
HTTP response status code. The status appears also in response body's field for those clients
|
||||
that are unable to process standard HTTP status code.
|
||||
example: 200
|
||||
|
||||
|
||||
isDeduplicationField:
|
||||
type: boolean
|
||||
description:
|
||||
Flag whether the operation found a duplicate document (to update)
|
||||
example: true
|
||||
|
||||
|
||||
deduplicatedIdentifierField:
|
||||
type: string
|
||||
description:
|
||||
The original document that has been marked as a duplicate document and which has been updated
|
||||
example: 'abc09478-105f-11e9-ab14-d663bd873d93'
|
||||
|
||||
|
||||
DocumentBase:
|
||||
description: Shared base for all documents
|
||||
properties:
|
||||
@@ -1527,6 +1628,17 @@ components:
|
||||
|
||||
Version:
|
||||
description: Information about versions
|
||||
type: object
|
||||
properties:
|
||||
|
||||
status:
|
||||
$ref: '#/components/schemas/statusField'
|
||||
|
||||
result:
|
||||
$ref: '#/components/schemas/VersionResult'
|
||||
|
||||
|
||||
VersionResult:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -1562,8 +1674,17 @@ components:
|
||||
|
||||
Status:
|
||||
description: Information about versions and API permissions
|
||||
properties:
|
||||
status:
|
||||
$ref: '#/components/schemas/statusField'
|
||||
|
||||
result:
|
||||
$ref: '#/components/schemas/StatusResult'
|
||||
|
||||
|
||||
StatusResult:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Version'
|
||||
- $ref: '#/components/schemas/VersionResult'
|
||||
- type: object
|
||||
properties:
|
||||
|
||||
@@ -1586,7 +1707,6 @@ components:
|
||||
type: string
|
||||
example: 'crud'
|
||||
|
||||
|
||||
LastModifiedResult:
|
||||
description: Result of LAST MODIFIED operation
|
||||
properties:
|
||||
@@ -1644,4 +1764,4 @@ components:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Use this if you know the temporary json webtoken.
|
||||
bearerFormat: JWT
|
||||
bearerFormat: JWT
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
function init () {
|
||||
const _ = require('lodash');
|
||||
|
||||
function init (env) {
|
||||
|
||||
const ipDelayList = {};
|
||||
|
||||
const DELAY_ON_FAIL = 5000;
|
||||
const DELAY_ON_FAIL = _.get(env, 'settings.authFailDelay') || 5000;
|
||||
const FAIL_AGE = 60000;
|
||||
|
||||
const sleep = require('util').promisify(setTimeout);
|
||||
|
||||
ipDelayList.addFailedRequest = function addFailedRequest (ip) {
|
||||
const ipString = String(ip);
|
||||
let entry = ipDelayList[ipString];
|
||||
|
||||
+21
-18
@@ -4,20 +4,20 @@ const _ = require('lodash');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const shiroTrie = require('shiro-trie');
|
||||
|
||||
const ipdelaylist = require('./delaylist')();
|
||||
const consts = require('./../constants');
|
||||
|
||||
const sleep = require('util').promisify(setTimeout);
|
||||
|
||||
const addFailedRequest = ipdelaylist.addFailedRequest;
|
||||
const shouldDelayRequest = ipdelaylist.shouldDelayRequest;
|
||||
const requestSucceeded = ipdelaylist.requestSucceeded;
|
||||
|
||||
function getRemoteIP (req) {
|
||||
return req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
}
|
||||
|
||||
function init (env, ctx) {
|
||||
|
||||
const ipdelaylist = require('./delaylist')(env, ctx);
|
||||
const addFailedRequest = ipdelaylist.addFailedRequest;
|
||||
const shouldDelayRequest = ipdelaylist.shouldDelayRequest;
|
||||
const requestSucceeded = ipdelaylist.requestSucceeded;
|
||||
|
||||
var authorization = {};
|
||||
var storage = authorization.storage = require('./storage')(env, ctx);
|
||||
var defaultRoles = (env.settings.authDefaultRoles || '').split(/[, :]/);
|
||||
@@ -96,7 +96,7 @@ function init (env, ctx) {
|
||||
}
|
||||
|
||||
function authorizeAdminSecret (secret) {
|
||||
return (env.api_secret && env.api_secret.length > 12) ? (secret === env.api_secret) : false;
|
||||
return env.enclave.isApiKey(secret);
|
||||
}
|
||||
|
||||
authorization.seenPermissions = [];
|
||||
@@ -185,7 +185,7 @@ function init (env, ctx) {
|
||||
|
||||
// Tokens have to be well formed JWTs
|
||||
try {
|
||||
const verified = await jwt.verify(data.token, env.api_secret);
|
||||
const verified = env.enclave.verifyJWT(data.token);
|
||||
token = verified.accessToken;
|
||||
} catch (err) {}
|
||||
|
||||
@@ -206,7 +206,13 @@ function init (env, ctx) {
|
||||
|
||||
console.error('Resolving secret/token to permissions failed');
|
||||
addFailedRequest(data.ip);
|
||||
if (callback) { callback('All validation failed', {}); }
|
||||
|
||||
ctx.bus.emit('admin-notify', {
|
||||
title: ctx.language.translate('Failed authentication')
|
||||
, message: ctx.language.translate('A device at IP address %1 attempted authenticating with Nightscout with wrong credentials. Check if you have an uploader setup with wrong API_SECRET or token?', data.ip)
|
||||
});
|
||||
|
||||
if (callback) { callback('All validation failed', {}); }
|
||||
return {};
|
||||
|
||||
};
|
||||
@@ -231,7 +237,7 @@ function init (env, ctx) {
|
||||
|
||||
/**
|
||||
* Check if the client has a permission execute an action,
|
||||
* based on an API_SECRET or JWT in the request.
|
||||
* based on an API KEY or JWT in the request.
|
||||
*
|
||||
* Used to authorize API calls
|
||||
*
|
||||
@@ -275,8 +281,9 @@ function init (env, ctx) {
|
||||
*/
|
||||
authorization.authorize = function authorize (accessToken) {
|
||||
|
||||
let userToken = accessToken
|
||||
const decodedToken = jwt.decode(accessToken);
|
||||
|
||||
let userToken = accessToken;
|
||||
const decodedToken = env.enclave.verifyJWT(accessToken);
|
||||
|
||||
if (decodedToken && decodedToken.accessToken) {
|
||||
userToken = decodedToken.accessToken;
|
||||
@@ -286,18 +293,14 @@ function init (env, ctx) {
|
||||
var authorized = null;
|
||||
|
||||
if (subject) {
|
||||
var token = jwt.sign({ accessToken: subject.accessToken }, env.api_secret, { expiresIn: '8h' });
|
||||
|
||||
//decode so we can tell the client the issued and expired times
|
||||
var decoded = jwt.decode(token);
|
||||
const token = env.enclave.signJWT({ accessToken: subject.accessToken });
|
||||
const decoded = env.enclave.verifyJWT(token);
|
||||
|
||||
var roles = _.uniq(subject.roles.concat(defaultRoles));
|
||||
|
||||
authorized = {
|
||||
token
|
||||
, sub: subject.name
|
||||
// not sending roles to client to prevent us from treating them as magic
|
||||
// instead group permissions by role so the we can create correct shiros on the client
|
||||
, permissionGroups: _.map(roles, storage.roleToPermissions)
|
||||
, iat: decoded.iat
|
||||
, exp: decoded.exp
|
||||
|
||||
@@ -151,12 +151,9 @@ function init (env, ctx) {
|
||||
}
|
||||
|
||||
storage.subjects = _.map(results, function eachSubject (subject) {
|
||||
if (env.api_secret) {
|
||||
var shasum = crypto.createHash('sha1');
|
||||
shasum.update(env.api_secret);
|
||||
shasum.update(subject._id.toString());
|
||||
if (env.enclave.isApiKeySet()) {
|
||||
subject.digest = env.enclave.getSubjectHash(subject._id.toString());
|
||||
var abbrev = subject.name.toLowerCase().replace(/[\W]/g, '').substring(0, 10);
|
||||
subject.digest = shasum.digest('hex');
|
||||
subject.accessToken = abbrev + '-' + subject.digest.substring(0, 16);
|
||||
subject.accessTokenDigest = storage.getSHA1(subject.accessToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
|
||||
function init (client, $) {
|
||||
|
||||
var notifies = {};
|
||||
|
||||
client.notifies = notifies;
|
||||
|
||||
notifies.notifies = [];
|
||||
notifies.drawer = $('#adminNotifiesDrawer');
|
||||
notifies.button = $('#adminnotifies');
|
||||
|
||||
notifies.updateAdminNotifies = function updateAdminNotifies() {
|
||||
|
||||
var src = '/api/v1/adminnotifies?t=' + new Date().getTime();
|
||||
|
||||
$.ajax({
|
||||
method: 'GET'
|
||||
, url: src
|
||||
, headers: client.headers()
|
||||
}).done(function success (results) {
|
||||
if (results.message) {
|
||||
var m = results.message;
|
||||
client.notifies.notifies = m.notifies;
|
||||
client.notifies.notifyCount = m.notifyCount;
|
||||
if (m.notifyCount > 0) {
|
||||
notifies.button.show();
|
||||
}
|
||||
}
|
||||
window.setTimeout(notifies.updateAdminNotifies, 1000*60);
|
||||
}).fail(function fail () {
|
||||
console.error('Failed to load notifies');
|
||||
window.setTimeout(notifies.updateAdminNotifies, 1000*60);
|
||||
});
|
||||
}
|
||||
|
||||
notifies.updateAdminNotifies();
|
||||
|
||||
function wrapmessage(title, message, count, ago, persistent) {
|
||||
let html = '<hr><p><b>' + title + '</b></p><p class="adminNotifyMessage">' + message + '</p>';
|
||||
|
||||
let additional = '';
|
||||
|
||||
if (count > 1) additional += client.translate('Event repeated %1 times.', count) + ' ';
|
||||
let units = client.translate('minutes');
|
||||
if (ago > 60) {
|
||||
ago = ago / 60;
|
||||
ago = Math.round((ago + Number.EPSILON) * 10) / 10;
|
||||
units = client.translate('hours');
|
||||
}
|
||||
if (ago == 0) { ago = client.translate('less than 1'); }
|
||||
if (!persistent && ago) additional += client.translate('Last recorded %1 %2 ago.', ago, units);
|
||||
|
||||
if (additional) html += '<p class="adminNotifyMessageAdditionalInfo">' + additional + '</p>'
|
||||
return html;
|
||||
}
|
||||
|
||||
notifies.prepare = function prepare() {
|
||||
|
||||
var translate = client.translate;
|
||||
|
||||
var html = '<div id="adminNotifyContent">';
|
||||
var messages = client.notifies.notifies;
|
||||
var messageCount = client.notifies.notifyCount;
|
||||
|
||||
if (messages && messages.length > 0) {
|
||||
html += '<p><b>' + translate('You have administration messages') + '</b></p>';
|
||||
for(var i = 0 ; i < messages.length; i++) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
var m = messages[i];
|
||||
const ago = Math.round((Date.now() - m.lastRecorded) / 60000);
|
||||
html += wrapmessage(translate(m.title), translate(m.message), m.count, ago, m.persistent);
|
||||
}
|
||||
} else {
|
||||
if (messageCount > 0) {
|
||||
html = wrapmessage(translate('Admin messages in queue'), translate('Please sign in using the API_SECRET to see your administration messages'));
|
||||
} else {
|
||||
html = wrapmessage(translate('Queue empty'), translate('There are no admin messages in queue'));
|
||||
}
|
||||
}
|
||||
html += '<hr></div>';
|
||||
notifies.drawer.html(html);
|
||||
}
|
||||
|
||||
function maybePrevent (event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
notifies.toggleDrawer = function toggleDrawer (event) {
|
||||
client.browserUtils.toggleDrawer('#adminNotifiesDrawer', notifies.prepare);
|
||||
maybePrevent(event);
|
||||
};
|
||||
|
||||
notifies.button.click(notifies.toggleDrawer);
|
||||
notifies.button.css('color','red');
|
||||
|
||||
return notifies;
|
||||
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
+20
-10
@@ -252,6 +252,7 @@ function init (client, $) {
|
||||
var html = '<table style="float:right;margin-right:20px;font-size:12px">';
|
||||
var carbs = 0;
|
||||
for (var fi = 0; fi < record.foods.length; fi++) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
var f = record.foods[fi];
|
||||
carbs += f.carbs * f.portions;
|
||||
html += '<tr>';
|
||||
@@ -426,6 +427,7 @@ function init (client, $) {
|
||||
if (record.foods.length) {
|
||||
var gisum = 0;
|
||||
for (var fi = 0; fi < record.foods.length; fi++) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
var f = record.foods[fi];
|
||||
record.carbs += f.carbs * f.portions;
|
||||
gisum += f.carbs * f.portions * f.gi;
|
||||
@@ -576,7 +578,7 @@ function init (client, $) {
|
||||
foods = [];
|
||||
$('#bc_addfoodarea').css('display', '');
|
||||
} else {
|
||||
var qp = quickpicks[qpiselected];
|
||||
var qp = quickpicks[parseInt(qpiselected)];
|
||||
foods = _.cloneDeep(qp.foods);
|
||||
$('#bc_addfoodarea').css('display', 'none');
|
||||
}
|
||||
@@ -589,18 +591,20 @@ function init (client, $) {
|
||||
var qpiselected = $('#bc_quickpick').val();
|
||||
|
||||
if (qpiselected >= 0) {
|
||||
var qp = quickpicks[qpiselected];
|
||||
var qp = quickpicks[parseInt(qpiselected)];
|
||||
if (qp.hideafteruse) {
|
||||
qp.hidden = true;
|
||||
|
||||
var apisecrethash = localStorage.getItem('apisecrethash');
|
||||
var dataJson = JSON.stringify(qp, null, ' ');
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('PUT', '/api/v1/food/', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
xhr.setRequestHeader('api-secret', apisecrethash);
|
||||
xhr.send(dataJson);
|
||||
$.ajax({
|
||||
method: 'PUT'
|
||||
, url: '/api/v1/food/'
|
||||
, headers: client.headers()
|
||||
, data: qp
|
||||
}).done(function treatmentSaved (response) {
|
||||
console.info('quick pick saved', response);
|
||||
}).fail(function treatmentSaveFail (response) {
|
||||
console.info('quick pick failed to save', response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,6 +654,7 @@ function init (client, $) {
|
||||
});
|
||||
$('#bc_quickpick').empty().append('<option value="-1">' + translate('(none)') + '</option>');
|
||||
for (var i = 0; i < records.length; i++) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
var r = records[i];
|
||||
$('#bc_quickpick').append('<option value="' + i + '">' + r.name + ' (' + r.carbs + ' g)</option>');
|
||||
}
|
||||
@@ -694,6 +699,7 @@ function init (client, $) {
|
||||
}
|
||||
$('#bc_data').empty();
|
||||
for (var i = 0; i < foodlist.length; i++) {
|
||||
/* eslint-disable security/detect-object-injection */ // verified false positive
|
||||
if (filter.category !== '' && foodlist[i].category !== filter.category) { continue; }
|
||||
if (filter.subcategory !== '' && foodlist[i].subcategory !== filter.subcategory) { continue; }
|
||||
if (filter.name !== '' && foodlist[i].name.toLowerCase().indexOf(filter.name.toLowerCase()) < 0) { continue; }
|
||||
@@ -703,6 +709,7 @@ function init (client, $) {
|
||||
o += foodlist[i].unit + ' | ';
|
||||
o += 'Carbs: ' + foodlist[i].carbs + ' g';
|
||||
$('#bc_data').append('<option value="' + i + '">' + o + '</option>');
|
||||
/* eslint-enable security/detect-object-injection */ // verified false positive
|
||||
}
|
||||
$('#bc_addportions').val('1');
|
||||
|
||||
@@ -726,8 +733,11 @@ function init (client, $) {
|
||||
var index = $('#bc_data').val();
|
||||
var portions = parseFloat($('#bc_addportions').val().replace(',', '.'));
|
||||
if (index !== null && !isNaN(portions) && portions > 0) {
|
||||
index = parseInt(index);
|
||||
/* eslint-disable security/detect-object-injection */ // verified false positive
|
||||
foodlist[index].portions = portions;
|
||||
foods.push(_.cloneDeep(foodlist[index]));
|
||||
/* eslint-enable security/detect-object-injection */ // verified false positive
|
||||
$(this).dialog('close');
|
||||
boluscalc.calculateInsulin();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,29 @@ function init (client, serverSettings, $) {
|
||||
var storage = Storages.localStorage;
|
||||
var settings = require('../settings')();
|
||||
|
||||
function updateBolusRender () {
|
||||
var bolusSettings = client.settings.extendedSettings.bolus || {};
|
||||
|
||||
var allRenderOverOptions = [5, 1, 0.5, 0.1];
|
||||
if (_.isNumber(bolusSettings.renderOver) && bolusSettings.renderOver > 0 && bolusSettings.renderOver < Number.MAX_SAFE_INTEGER) {
|
||||
allRenderOverOptions.push(_.toNumber(bolusSettings.renderOver));
|
||||
}
|
||||
var sortedRenderOverOptions = _.chain(allRenderOverOptions).uniq().sort().reverse().value();
|
||||
|
||||
_.forEach(sortedRenderOverOptions, function (optionValue) {
|
||||
$('#bolusRenderOver').append(
|
||||
$('<option></option>')
|
||||
.attr('value', optionValue)
|
||||
.text(client.translate('%1 U and Over', { params: [optionValue] }))
|
||||
);
|
||||
});
|
||||
|
||||
$('#bolusRenderOver').val(String(bolusSettings.renderOver || 0.5));
|
||||
$('#bolusRenderFormat').val(bolusSettings.renderFormat ? bolusSettings.renderFormat : 'default');
|
||||
$('#bolusRenderFormatSmall').val(bolusSettings.renderFormatSmall ? bolusSettings.renderFormatSmall : 'default');
|
||||
|
||||
}
|
||||
|
||||
function loadForm () {
|
||||
var utils = client.utils;
|
||||
var language = client.language;
|
||||
@@ -70,7 +93,7 @@ function init (client, serverSettings, $) {
|
||||
|
||||
$('#basalrender').val(settings.extendedSettings.basal ? settings.extendedSettings.basal.render : 'none');
|
||||
|
||||
$('#bolusrender').val(settings.extendedSettings.bolus ? settings.extendedSettings.bolus.render : 'all');
|
||||
updateBolusRender();
|
||||
|
||||
if (settings.timeFormat === 24) {
|
||||
$('#24-browser').prop('checked', true);
|
||||
@@ -147,6 +170,7 @@ function init (client, serverSettings, $) {
|
||||
});
|
||||
|
||||
//if there is a token, append it to each of the links in the hamburger menu
|
||||
/* eslint-disable security/detect-possible-timing-attacks */ // verified false positive
|
||||
if (token != '') {
|
||||
token = '?token=' + token;
|
||||
$('#reportlink').attr('href', 'report' + token);
|
||||
@@ -162,7 +186,7 @@ function init (client, serverSettings, $) {
|
||||
storage.remove(name);
|
||||
});
|
||||
storage.remove('basalrender');
|
||||
storage.remove('bolusrender');
|
||||
storage.remove('bolus');
|
||||
event.preventDefault();
|
||||
client.browserUtils.reload();
|
||||
});
|
||||
@@ -192,6 +216,7 @@ function init (client, serverSettings, $) {
|
||||
|
||||
function storeInBrowser (data) {
|
||||
Object.keys(data).forEach(k => {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
storage.set(k, data[k]);
|
||||
});
|
||||
}
|
||||
@@ -215,7 +240,11 @@ function init (client, serverSettings, $) {
|
||||
, language: $('#language').val()
|
||||
, scaleY: $('#scaleY').val()
|
||||
, basalrender: $('#basalrender').val()
|
||||
, bolusrender: $('#bolusrender').val()
|
||||
, bolus: {
|
||||
renderOver: $('#bolusRenderOver').val()
|
||||
, renderFormat: $('#bolusRenderFormat').val()
|
||||
, renderFormatSmall: $('#bolusRenderFormatSmall').val()
|
||||
}
|
||||
, showPlugins: checkedPluginNames()
|
||||
, storageVersion: STORAGE_VERSION
|
||||
});
|
||||
@@ -252,6 +281,7 @@ function init (client, serverSettings, $) {
|
||||
try {
|
||||
settings.eachSetting(function setEach (name) {
|
||||
var stored = storage.get(name);
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
return stored !== undefined && stored !== null ? stored : serverSettings.settings[name];
|
||||
});
|
||||
|
||||
@@ -276,11 +306,17 @@ function init (client, serverSettings, $) {
|
||||
settings.extendedSettings.basal.render = basalStored !== null ? basalStored : settings.extendedSettings.basal.render;
|
||||
|
||||
if (!settings.extendedSettings.bolus) {
|
||||
settings.extendedSettings.bolus = {};
|
||||
settings.extendedSettings.bolus = {
|
||||
renderOver: 0
|
||||
, renderFormat: 'default'
|
||||
, renderFormatSmall: 'default'
|
||||
};
|
||||
}
|
||||
|
||||
var bolusStored = storage.get('bolusrender');
|
||||
settings.extendedSettings.bolus.render = bolusStored !== null ? bolusStored : settings.extendedSettings.bolus.render;
|
||||
var bolusStored = storage.get('bolus');
|
||||
settings.extendedSettings.bolus.renderOver = bolusStored !== null ? _.toNumber(bolusStored.renderOver) : settings.extendedSettings.bolus.renderOver;
|
||||
settings.extendedSettings.bolus.renderFormat = bolusStored !== null ? bolusStored.renderFormat : settings.extendedSettings.bolus.renderFormat;
|
||||
settings.extendedSettings.bolus.renderFormatSmall = bolusStored !== null ? bolusStored.renderFormatSmall : settings.extendedSettings.bolus.renderFormatSmall;
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -18,16 +18,6 @@ function init ($) {
|
||||
, opacity: 0.75
|
||||
};
|
||||
|
||||
var querystring = queryParms();
|
||||
|
||||
if (querystring.notify) {
|
||||
showNotification(querystring.notify, querystring.notifytype);
|
||||
}
|
||||
|
||||
if (querystring.drawer) {
|
||||
openDrawer('#drawer');
|
||||
}
|
||||
|
||||
$('#drawerToggle').click(function(event) {
|
||||
toggleDrawer('#drawer');
|
||||
event.preventDefault();
|
||||
|
||||
@@ -52,7 +52,7 @@ function init (client, $) {
|
||||
submitHooks = {};
|
||||
|
||||
_.forEach(careportal.allEventTypes, function each (event) {
|
||||
inputMatrix[event.val] = _.pick(event, ['otp','remoteCarbs', 'remoteAbsorption', 'remoteBolus', 'bg', 'insulin', 'carbs', 'protein', 'fat', 'prebolus', 'duration', 'percent', 'absolute', 'profile', 'split', 'reasons', 'targets']);
|
||||
inputMatrix[event.val] = _.pick(event, ['otp','remoteCarbs', 'remoteAbsorption', 'remoteBolus', 'bg', 'insulin', 'carbs', 'protein', 'fat', 'prebolus', 'duration', 'percent', 'absolute', 'profile', 'split', 'sensor', 'reasons', 'targets']);
|
||||
submitHooks[event.val] = event.submitHook;
|
||||
});
|
||||
}
|
||||
@@ -76,6 +76,13 @@ function init (client, $) {
|
||||
}
|
||||
}
|
||||
|
||||
// validate the eventType input - should never hit this but bail if we do
|
||||
if (!Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) {
|
||||
maybePrevent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
/* eslint-disable security/detect-object-injection */ // verified false positive by check above
|
||||
var reasons = inputMatrix[eventType]['reasons'];
|
||||
$('#reasonLabel').css('display', displayType(reasons && reasons.length > 0));
|
||||
$('#targets').css('display', displayType(inputMatrix[eventType]['targets']));
|
||||
@@ -92,6 +99,8 @@ function init (client, $) {
|
||||
$('#proteinGivenLabel').css('display', displayType(inputMatrix[eventType]['protein']));
|
||||
$('#fatGivenLabel').css('display', displayType(inputMatrix[eventType]['fat']));
|
||||
|
||||
$('#sensorInfo').css('display', displayType(inputMatrix[eventType]['sensor']));
|
||||
|
||||
$('#durationLabel').css('display', displayType(inputMatrix[eventType]['duration']));
|
||||
$('#percentLabel').css('display', displayType(inputMatrix[eventType]['percent'] && $('#absolute').val() === ''));
|
||||
$('#absoluteLabel').css('display', displayType(inputMatrix[eventType]['absolute'] && $('#percent').val() === ''));
|
||||
@@ -115,19 +124,28 @@ function init (client, $) {
|
||||
resetIfHidden(inputMatrix[eventType]['carbs'], '#carbsGiven');
|
||||
resetIfHidden(inputMatrix[eventType]['protein'], '#proteinGiven');
|
||||
resetIfHidden(inputMatrix[eventType]['fat'], '#fatGiven');
|
||||
resetIfHidden(inputMatrix[eventType]['sensor'], '#sensorCode');
|
||||
resetIfHidden(inputMatrix[eventType]['sensor'], '#transmitterId');
|
||||
resetIfHidden(inputMatrix[eventType]['duration'], '#duration');
|
||||
resetIfHidden(inputMatrix[eventType]['absolute'], '#absolute');
|
||||
resetIfHidden(inputMatrix[eventType]['percent'], '#percent');
|
||||
resetIfHidden(inputMatrix[eventType]['prebolus'], '#preBolus');
|
||||
resetIfHidden(inputMatrix[eventType]['split'], '#insulinSplitNow');
|
||||
resetIfHidden(inputMatrix[eventType]['split'], '#insulinSplitExt');
|
||||
/* eslint-enable security/detect-object-injection */ // verified false positive
|
||||
|
||||
maybePrevent(event);
|
||||
};
|
||||
|
||||
careportal.reasonable = function reasonable () {
|
||||
var eventType = $('#eventType').val();
|
||||
var reasons = inputMatrix[eventType]['reasons'];
|
||||
var reasons = [];
|
||||
|
||||
// validate the eventType input before getting the reasons list
|
||||
if (Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
reasons = inputMatrix[eventType]['reasons'];
|
||||
}
|
||||
var selected = $('#reason').val();
|
||||
|
||||
var reason = _.find(reasons, function matches (r) {
|
||||
@@ -213,6 +231,8 @@ function init (client, $) {
|
||||
$('#carbsGiven').val('');
|
||||
$('#proteinGiven').val('');
|
||||
$('#fatGiven').val('');
|
||||
$('#sensorCode').val('');
|
||||
$('#transmitterId').val('');
|
||||
$('#insulinGiven').val('');
|
||||
$('#duration').val('');
|
||||
$('#percent').val('');
|
||||
@@ -244,6 +264,8 @@ function init (client, $) {
|
||||
, carbs: $('#carbsGiven').val()
|
||||
, protein: $('#proteinGiven').val()
|
||||
, fat: $('#fatGiven').val()
|
||||
, sensorCode: $('#sensorCode').val()
|
||||
, transmitterId: $('#transmitterId').val()
|
||||
, insulin: $('#insulinGiven').val()
|
||||
, duration: times.msecs(parse_duration($('#duration').val())).mins < 1 ? $('#duration').val() : times.msecs(parse_duration($('#duration').val())).mins
|
||||
, percent: $('#percent').val()
|
||||
@@ -259,7 +281,13 @@ function init (client, $) {
|
||||
delete data.preBolus;
|
||||
}
|
||||
|
||||
var reasons = inputMatrix[eventType]['reasons'];
|
||||
var reasons = [];
|
||||
|
||||
// validate the eventType input before getting the reasons list
|
||||
if (Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
reasons = inputMatrix[eventType]['reasons'];
|
||||
}
|
||||
var reason = _.find(reasons, function matches (r) {
|
||||
return r.name === selectedReason;
|
||||
});
|
||||
@@ -307,9 +335,11 @@ function init (client, $) {
|
||||
|
||||
let d = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
/* eslint-disable security/detect-object-injection */ // verified false positive
|
||||
if (data[key] !== "" && data[key] !== null) {
|
||||
d[key] = data[key]
|
||||
}
|
||||
d[key] = data[key]
|
||||
}
|
||||
/* eslint-enable security/detect-object-injection */ // verified false positive
|
||||
});
|
||||
|
||||
return d;
|
||||
@@ -415,6 +445,8 @@ function init (client, $) {
|
||||
pushIf(data.carbs, translate('Carbs Given') + ': ' + data.carbs);
|
||||
pushIf(data.protein, translate('Protein Given') + ': ' + data.protein);
|
||||
pushIf(data.fat, translate('Fat Given') + ': ' + data.fat);
|
||||
pushIf(data.sensorCode, translate('Sensor Code') + ': ' + data.sensorCode);
|
||||
pushIf(data.transmitterId, translate('Transmitter ID') + ': ' + data.transmitterId);
|
||||
pushIf(data.insulin, translate('Insulin Given') + ': ' + data.insulin);
|
||||
pushIf(data.eventType === 'Combo Bolus', translate('Combo Bolus') + ': ' + data.splitNow + '% : ' + data.splitExt + '%');
|
||||
pushIf(data.duration, translate('Duration') + ': ' + data.duration + ' ' + translate('mins'));
|
||||
|
||||
@@ -703,6 +703,7 @@ function init (client, d3, $) {
|
||||
var pointTypes = client.settings.showForecast.split(' ');
|
||||
|
||||
var points = pointTypes.reduce( function (points, type) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
return points.concat(client.sbx.pluginBase.forecastPoints[type] || []);
|
||||
}, [] );
|
||||
|
||||
|
||||
@@ -90,11 +90,15 @@ client.render = function render () {
|
||||
|
||||
for (let param in faceParams) {
|
||||
if (param === '0') {
|
||||
bgColor = (faceParams[param].substr(0, 1) === 'c'); // do we want colorful background?
|
||||
alwaysShowTime = (faceParams[param].substr(1, 1) === 'y'); // always show "stale time" text?
|
||||
staleMinutes = (faceParams[param].substr(2, 2) - 0 >= 0) ? faceParams[param].substr(2, 2) : 13; // threshold value (0=never)
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
let faceParam = faceParams[param];
|
||||
bgColor = (faceParam.substr(0, 1) === 'c'); // do we want colorful background?
|
||||
alwaysShowTime = (faceParam.substr(1, 1) === 'y'); // always show "stale time" text?
|
||||
staleMinutes = (faceParam.substr(2, 2) - 0 >= 0) ? faceParam.substr(2, 2) : 13; // threshold value (0=never)
|
||||
} else if (!clockCreated) {
|
||||
let div = '<div class="' + faceParams[param].substr(0, 2) + '"' + ((faceParams[param].substr(2, 2) - 0 > 0) ? ' style="' + ((faceParams[param].substr(0, 2) === 'ar') ? 'height' : 'font-size') + ':' + faceParams[param].substr(2, 2) + 'vmin"' : '') + '></div>';
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
let faceParam = faceParams[param];
|
||||
let div = '<div class="' + faceParam.substr(0, 2) + '"' + ((faceParam.substr(2, 2) - 0 > 0) ? ' style="' + ((faceParam.substr(0, 2) === 'ar') ? 'height' : 'font-size') + ':' + faceParam.substr(2, 2) + 'vmin"' : '') + '></div>';
|
||||
$inner.append(div);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+6
-1
@@ -234,9 +234,14 @@ d3locales.locale = function locale (language) {
|
||||
,bg: 'bg_BG'
|
||||
};
|
||||
var loc = 'en_US';
|
||||
if (mapper[language]) {
|
||||
|
||||
// validate the eventType input before getting the reasons list
|
||||
if (Object.prototype.hasOwnProperty.call(mapper, language)) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
loc = mapper[language];
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
return d3locales[loc];
|
||||
};
|
||||
|
||||
|
||||
@@ -42,20 +42,20 @@ hashauth.init = function init (client, $) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.message === 'OK') {
|
||||
if (response.message === 'OK' || message.message === 'OK') {
|
||||
hashauth.authenticated = true;
|
||||
console.log('Authentication passed.');
|
||||
next(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Authentication failed.', response);
|
||||
console.log('Authentication failed!', response);
|
||||
hashauth.removeAuthentication();
|
||||
next(false);
|
||||
return;
|
||||
|
||||
}).fail(function verifyfail (err) {
|
||||
console.log('Authentication failed.', err);
|
||||
console.log('Authentication failure', err);
|
||||
hashauth.removeAuthentication();
|
||||
next(false);
|
||||
});
|
||||
@@ -113,7 +113,7 @@ hashauth.init = function init (client, $) {
|
||||
, buttons: [
|
||||
{
|
||||
id: 'requestauthenticationdialog-btn'
|
||||
, text: translate('Update')
|
||||
, text: translate('Authenticate')
|
||||
, click: function() {
|
||||
var dialog = this;
|
||||
hashauth.processSecret($('#apisecret').val(), $('#storeapisecret').is(':checked'), function done (close) {
|
||||
|
||||
+14
-7
@@ -237,6 +237,8 @@ client.load = function load (serverSettings, callback) {
|
||||
//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();
|
||||
@@ -266,6 +268,8 @@ client.load = function load (serverSettings, callback) {
|
||||
$('#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) {
|
||||
@@ -441,11 +445,6 @@ client.load = function load (serverSettings, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateBolusRenderOver () {
|
||||
var bolusRenderOver = (client.settings.bolusRenderOver || 1) + ' U and Over';
|
||||
$('#bolusRenderOver').text(bolusRenderOver);
|
||||
}
|
||||
|
||||
function alarmingNow () {
|
||||
return container.hasClass('alarming');
|
||||
}
|
||||
@@ -872,9 +871,15 @@ client.load = function load (serverSettings, callback) {
|
||||
|
||||
function getClientAlarm (level, group) {
|
||||
var key = level + '-' + group;
|
||||
var alarm = clientAlarms[key];
|
||||
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;
|
||||
@@ -964,6 +969,7 @@ client.load = function load (serverSettings, callback) {
|
||||
|
||||
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) {
|
||||
@@ -1003,6 +1009,7 @@ client.load = function load (serverSettings, callback) {
|
||||
|
||||
$('.bgButton').click(function(e) {
|
||||
if (alarmingNow()) {
|
||||
/* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive
|
||||
silenceDropdown.open(e);
|
||||
}
|
||||
});
|
||||
@@ -1017,6 +1024,7 @@ client.load = function load (serverSettings, callback) {
|
||||
Storages.localStorage.set('focusHours', hours);
|
||||
refreshChart();
|
||||
} else {
|
||||
/* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive
|
||||
viewDropdown.open(e);
|
||||
}
|
||||
});
|
||||
@@ -1301,7 +1309,6 @@ client.load = function load (serverSettings, callback) {
|
||||
|
||||
prepareEntries();
|
||||
updateTitle();
|
||||
updateBolusRenderOver();
|
||||
|
||||
// Don't invoke D3 in headless mode
|
||||
|
||||
|
||||
@@ -7,20 +7,33 @@ var TWO_DAYS = 172800000;
|
||||
function mergeDataUpdate (isDelta, cachedDataArray, receivedDataArray, maxAge) {
|
||||
|
||||
function nsArrayDiff (oldArray, newArray) {
|
||||
var seen = [];
|
||||
var knownMills = [];
|
||||
|
||||
var l = oldArray.length;
|
||||
|
||||
for (var i = 0; i < l; i++) {
|
||||
/* eslint-disable security/detect-object-injection */ // verified false positive
|
||||
if (oldArray[i] !== null) {
|
||||
seen.push(oldArray[i].mills);
|
||||
knownMills.push(oldArray[i].mills);
|
||||
}
|
||||
/* eslint-enable security/detect-object-injection */ // verified false positive
|
||||
}
|
||||
|
||||
var result = [];
|
||||
var result = {
|
||||
updates: [],
|
||||
new: []
|
||||
};
|
||||
|
||||
l = newArray.length;
|
||||
for (var j = 0; j < l; j++) {
|
||||
if (!seen.includes(newArray[j].mills)) {
|
||||
result.push(newArray[j]); //console.log('delta data found');
|
||||
/* eslint-disable security/detect-object-injection */ // verified false positive
|
||||
var item = newArray[j];
|
||||
var millsSeen = knownMills.includes(item.mills);
|
||||
|
||||
if (!millsSeen) {
|
||||
result.new.push(item);
|
||||
} else {
|
||||
result.updates.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -40,16 +53,33 @@ function mergeDataUpdate (isDelta, cachedDataArray, receivedDataArray, maxAge) {
|
||||
var mAge = (isNaN(maxAge) || maxAge == null) ? TWO_DAYS : maxAge;
|
||||
var twoDaysAgo = new Date().getTime() - mAge;
|
||||
|
||||
for (var i = 0; i < cachedDataArray.length; i++) {
|
||||
var i;
|
||||
|
||||
for (i = cachedDataArray.length -1; i >= 0; i--) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
var element = cachedDataArray[i];
|
||||
if (element !== null && element !== undefined && element.mills <= twoDaysAgo) {
|
||||
cachedDataArray.splice(i, 0);
|
||||
cachedDataArray.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is delta, calculate the difference, merge and sort
|
||||
var diff = nsArrayDiff(cachedDataArray, receivedDataArray);
|
||||
return cachedDataArray.concat(diff).sort(function(a, b) {
|
||||
|
||||
// if there's updated elements, replace those in place
|
||||
if (diff.updates.length > 0) {
|
||||
for (i = 0; i < diff.updates.length; i++) {
|
||||
var e = diff.updates[i];
|
||||
for (var j = 0; j < cachedDataArray.length; j++) {
|
||||
if (e.mills == cachedDataArray[j].mills) {
|
||||
cachedDataArray.splice(j,1,e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// merge new items in
|
||||
return cachedDataArray.concat(diff.new).sort(function(a, b) {
|
||||
return a.mills - b.mills;
|
||||
});
|
||||
}
|
||||
@@ -70,12 +100,14 @@ function mergeTreatmentUpdate (isDelta, cachedDataArray, receivedDataArray) {
|
||||
var l = receivedDataArray.length;
|
||||
var m = cachedDataArray.length;
|
||||
for (var i = 0; i < l; i++) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
var no = receivedDataArray[i];
|
||||
if (!no.action) {
|
||||
cachedDataArray.push(no);
|
||||
continue;
|
||||
}
|
||||
for (var j = 0; j < m; j++) {
|
||||
/* eslint-disable security/detect-object-injection */ // verified false positive
|
||||
if (no._id === cachedDataArray[j]._id) {
|
||||
if (no.action === 'remove') {
|
||||
cachedDataArray.splice(j, 1);
|
||||
|
||||
+21
-11
@@ -255,6 +255,8 @@ function init (client, d3) {
|
||||
(durationText ? '<strong>' + translate('Duration') + ':</strong> ' + durationText + '<br/>' : '') +
|
||||
(d.insulinNeedsScaleFactor ? '<strong>' + translate('Insulin Scale Factor') + ':</strong> ' + d.insulinNeedsScaleFactor * 100 + '%<br/>' : '') +
|
||||
(correctionRangeText ? '<strong>' + translate('Correction Range') + ':</strong> ' + correctionRangeText + '<br/>' : '') +
|
||||
(d.transmitterId ? '<strong>' + translate('Transmitter ID') + ':</strong> ' + d.transmitterId + '<br/>' : '') +
|
||||
(d.sensorCode ? '<strong>' + translate('Sensor Code') + ':</strong> ' + d.sensorCode + '<br/>' : '') +
|
||||
(d.notes ? '<strong>' + translate('Notes') + ':</strong> ' + d.notes : '');
|
||||
}
|
||||
|
||||
@@ -527,7 +529,7 @@ function init (client, d3) {
|
||||
};
|
||||
}
|
||||
|
||||
function prepareArc (treatment, radius, renderBasal) {
|
||||
function prepareArc (treatment, radius, bolusSettings) {
|
||||
var arc_data = [
|
||||
// white carb half-circle on top
|
||||
{ 'element': '', 'color': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': radius.R1 }
|
||||
@@ -564,12 +566,14 @@ function init (client, d3) {
|
||||
|
||||
if (treatment.insulin > 0) {
|
||||
var dosage_units = '' + Math.round(treatment.insulin * 100) / 100;
|
||||
|
||||
var format = treatment.insulin < bolusSettings.renderOver ? bolusSettings.renderFormatSmall : bolusSettings.renderFormat;
|
||||
|
||||
if (renderBasal === 'all-remove-zero-u') {
|
||||
if (_.includes(['concise', 'minimal'], format)) {
|
||||
dosage_units = (dosage_units + "").replace(/^0/, "");
|
||||
}
|
||||
|
||||
var unit_of_measurement = (renderBasal === 'all-remove-zero-u' ? '' : ' U'); // One international unit of insulin (1 IU) is shown as '1 U'
|
||||
var unit_of_measurement = (format === 'minimal' ? '' : ' U'); // One international unit of insulin (1 IU) is shown as '1 U'
|
||||
|
||||
arc_data[3].element = dosage_units + unit_of_measurement;
|
||||
}
|
||||
@@ -614,6 +618,7 @@ function init (client, d3) {
|
||||
if (treatment.boluscalc.foods && treatment.boluscalc.foods.length) {
|
||||
html += '<table><tr><td><strong>' + translate('Food') + '</strong></td></tr>';
|
||||
for (var fi = 0; fi < treatment.boluscalc.foods.length; fi++) {
|
||||
/* eslint-disable-next-line security/detect-object-injection */ // verified false positive
|
||||
var f = treatment.boluscalc.foods[fi];
|
||||
html += '<tr>';
|
||||
html += '<td>' + f.name + '</td>';
|
||||
@@ -630,7 +635,7 @@ function init (client, d3) {
|
||||
var glucose = treatment.glucose;
|
||||
if (client.settings.units != client.ddata.profile.getUnits()) {
|
||||
glucose *= (client.settings.units === 'mmol' ? (1 / consts.MMOL_TO_MGDL) : consts.MMOL_TO_MGDL);
|
||||
var decimals = (client.settings.units === 'mmol' ? 10 : 1);
|
||||
const decimals = (client.settings.units === 'mmol' ? 10 : 1);
|
||||
|
||||
glucose = Math.round(glucose * decimals) / decimals;
|
||||
}
|
||||
@@ -641,7 +646,7 @@ function init (client, d3) {
|
||||
(treatment.protein ? '<strong>' + translate('Protein') + ':</strong> ' + treatment.protein + '<br/>' : '') +
|
||||
(treatment.fat ? '<strong>' + translate('Fat') + ':</strong> ' + treatment.fat + '<br/>' : '') +
|
||||
(treatment.absorptionTime > 0 ? '<strong>' + translate('Absorption Time') + ':</strong> ' + (Math.round(treatment.absorptionTime / 60.0 * 10) / 10) + 'h' + '<br/>' : '') +
|
||||
(treatment.insulin ? '<strong>' + translate('Insulin') + ':</strong> ' + treatment.insulin + '<br/>' : '') +
|
||||
(treatment.insulin ? '<strong>' + translate('Insulin') + ':</strong> ' + utils.toRoundedStr(treatment.insulin, 2) + '<br/>' : '') +
|
||||
(treatment.enteredinsulin ? '<strong>' + translate('Combo Bolus') + ':</strong> ' + treatment.enteredinsulin + 'U, ' + treatment.splitNow + '% : ' + treatment.splitExt + '%, ' + translate('Duration') + ': ' + treatment.duration + '<br/>' : '') +
|
||||
(treatment.glucose ? '<strong>' + translate('BG') + ':</strong> ' + glucose + (treatment.glucoseType ? ' (' + translate(treatment.glucoseType) + ')' : '') + '<br/>' : '') +
|
||||
(treatment.enteredBy ? '<strong>' + translate('Entered By') + ':</strong> ' + treatment.enteredBy + '<br/>' : '') +
|
||||
@@ -990,7 +995,8 @@ function init (client, d3) {
|
||||
renderer.drawTreatments = function drawTreatments (client) {
|
||||
|
||||
var treatmentCount = 0;
|
||||
var renderBasal = client.settings.extendedSettings.bolus.render;
|
||||
var bolusSettings = client.settings.extendedSettings.bolus || {};
|
||||
|
||||
chart().focus.selectAll('.draggable-treatment').remove();
|
||||
|
||||
_.forEach(client.ddata.treatments, function eachTreatment (d) {
|
||||
@@ -999,17 +1005,21 @@ function init (client, d3) {
|
||||
|
||||
// add treatment bubbles
|
||||
_.forEach(client.ddata.treatments, function eachTreatment (d) {
|
||||
var showLabels = ( !d.carbs && ( ( renderBasal == 'none') || ( renderBasal === 'over' && d.insulin < client.settings.bolusRenderOver) ) ) ? false : true;
|
||||
var showLabels = d.carbs || d.insulin;
|
||||
if (d.insulin && d.insulin < bolusSettings.renderOver && bolusSettings.renderFormatSmall == 'hidden') {
|
||||
showLabels = false;
|
||||
}
|
||||
renderer.drawTreatment(d, {
|
||||
scale: renderer.bubbleScale()
|
||||
, showLabels: showLabels
|
||||
, treatments: treatmentCount
|
||||
}, client.sbx.data.profile.getCarbRatio(new Date()),
|
||||
renderBasal);
|
||||
}
|
||||
, client.sbx.data.profile.getCarbRatio(new Date())
|
||||
, bolusSettings);
|
||||
});
|
||||
};
|
||||
|
||||
renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio, renderBasal) {
|
||||
renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio, bolusSettings) {
|
||||
if (!treatment.carbs && !treatment.protein && !treatment.fat && !treatment.insulin) {
|
||||
return;
|
||||
}
|
||||
@@ -1027,7 +1037,7 @@ function init (client, d3) {
|
||||
return;
|
||||
}
|
||||
|
||||
var arc = prepareArc(treatment, radius, renderBasal);
|
||||
var arc = prepareArc(treatment, radius, bolusSettings);
|
||||
var treatmentDots = appendTreatments(treatment, arc);
|
||||
appendLabels(treatmentDots, arc, opts);
|
||||
};
|
||||
|
||||
@@ -66,16 +66,23 @@ module.exports = function calcDelta (oldData, newData) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function genKey(o) {
|
||||
let r = o.mills;
|
||||
r += o.sgv ? 'sgv' + o.sgv : '';
|
||||
r += o.mgdl ? 'sgv' + o.mgdl : '';
|
||||
return r;
|
||||
}
|
||||
|
||||
function nsArrayDiff(oldArray, newArray) {
|
||||
var seen = {};
|
||||
var l = oldArray.length;
|
||||
for (var i = 0; i < l; i++) {
|
||||
seen[oldArray[i].mills] = true;
|
||||
seen[genKey(oldArray[i])] = true;
|
||||
}
|
||||
var result = [];
|
||||
l = newArray.length;
|
||||
for (var j = 0; j < l; j++) {
|
||||
if (!Object.prototype.hasOwnProperty.call(seen, newArray[j].mills)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(seen, genKey(newArray[j]))) {
|
||||
result.push(newArray[j]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
var init = function init () {
|
||||
|
||||
//for the tests window isn't the global object
|
||||
var $ = window.$;
|
||||
var _ = window._;
|
||||
var Nightscout = window.Nightscout;
|
||||
var client = Nightscout.client;
|
||||
|
||||
(function () {
|
||||
|
||||
client.init(function loaded () {
|
||||
var translate = client.translate;
|
||||
|
||||
@@ -146,7 +146,7 @@ client.init(function loaded () {
|
||||
$('#fe_filter_subcategory').empty().append(new Option(translate('(none)'),''));
|
||||
if (filter.category !== '') {
|
||||
for (s in categories[filter.category]) {
|
||||
if (categories[filter.category].hasOwnProperty(s)) {
|
||||
if (Object.prototype.hasOwnProperty.call(categories[filter.category],s)) {
|
||||
$('#fe_filter_subcategory').append(new Option(s,s));
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ client.init(function loaded () {
|
||||
$('#fe_subcategory_list').empty().append(new Option(translate('(none)'),''));
|
||||
if (foodrec.category !== '') {
|
||||
for (s in categories[foodrec.category]) {
|
||||
if (categories[foodrec.category].hasOwnProperty(s)) {
|
||||
if (Object.prototype.hasOwnProperty.call(categories[foodrec.category],s)) {
|
||||
$('#fe_subcategory_list').append(new Option(s,s));
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ client.init(function loaded () {
|
||||
$('#fe_filter_category').empty().append(new Option(translate('(none)'),''));
|
||||
$('#fe_category_list').empty().append(new Option(translate('(none)'),''));
|
||||
for (var s in categories) {
|
||||
if (categories.hasOwnProperty(s)) {
|
||||
if (Object.prototype.hasOwnProperty.call(categories,s)) {
|
||||
$('#fe_filter_category').append(new Option(s,s));
|
||||
$('#fe_category_list').append(new Option(s,s));
|
||||
}
|
||||
@@ -398,7 +398,7 @@ client.init(function loaded () {
|
||||
function savePortions(event) {
|
||||
var index = $(this).attr('index');
|
||||
var findex = $(this).attr('findex');
|
||||
var val = parseFloat($(this).val().replace(/\,/g,'.'));
|
||||
var val = parseFloat($(this).val().replace(/,/g,'.'));
|
||||
foodquickpick[index].foods[findex].portions=val;
|
||||
calculateCarbs(index);
|
||||
drawQuickpick();
|
||||
@@ -677,4 +677,6 @@ client.init(function loaded () {
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
};
|
||||
|
||||
module.exports = init;
|
||||
+62
-33
@@ -2,9 +2,9 @@
|
||||
|
||||
var _ = require('lodash');
|
||||
|
||||
function init(fs) {
|
||||
function init (fs) {
|
||||
|
||||
function language() {
|
||||
function language () {
|
||||
return language;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ function init(fs) {
|
||||
, { code: 'cs', file: 'cs_CZ', language: 'Čeština', speechCode: 'cs-CZ' }
|
||||
, { code: 'de', file: 'de_DE', language: 'Deutsch', speechCode: 'de-DE' }
|
||||
, { code: 'dk', file: 'da_DK', language: 'Dansk', speechCode: 'dk-DK' }
|
||||
, { code: 'el', file: 'el_GR', language: 'Ελληνικά', speechCode: 'el-GR'}
|
||||
, { code: 'el', file: 'el_GR', language: 'Ελληνικά', speechCode: 'el-GR' }
|
||||
, { code: 'en', file: 'en_US', language: 'English', speechCode: 'en-US' }
|
||||
, { code: 'es', file: 'es_ES', language: 'Español', speechCode: 'es-ES' }
|
||||
, { code: 'fi', file: 'fi_FI', language: 'Suomi', speechCode: 'fi-FI' }
|
||||
@@ -30,38 +30,40 @@ function init(fs) {
|
||||
, { code: 'nb', file: 'nb_NO', language: 'Norsk (Bokmål)', speechCode: 'no-NO' }
|
||||
, { code: 'nl', file: 'nl_NL', language: 'Nederlands', speechCode: 'nl-NL' }
|
||||
, { code: 'pl', file: 'pl_PL', language: 'Polski', speechCode: 'pl-PL' }
|
||||
, { code: 'pt', file: 'pt_BR', language: 'Português (Brasil)', speechCode: 'pt-BR' }
|
||||
, { code: 'pt', file: 'pt_PT', language: 'Português', speechCode: 'pt-PT' }
|
||||
, { code: 'br', file: 'pt_BR', language: 'Português (Brasil)', speechCode: 'pt-BR' }
|
||||
, { code: 'ro', file: 'ro_RO', language: 'Română', speechCode: 'ro-RO' }
|
||||
, { code: 'ru', file: 'ru_RU', language: 'Русский', speechCode: 'ru-RU' }
|
||||
, { code: 'sk', file: 'sl_SL', language: 'Slovenčina', speechCode: 'sk-SK' }
|
||||
, { code: 'sk', file: 'sk_SK', language: 'Slovenčina', speechCode: 'sk-SK' }
|
||||
, { code: 'sl', file: 'sl_SL', language: 'Slovenščina', speechCode: 'sl-SL' }
|
||||
, { code: 'sv', file: 'sv_SE', language: 'Svenska', speechCode: 'sv-SE' }
|
||||
, { code: 'tr', file: 'tr_TR', language: 'Türkçe', speechCode: 'tr-TR' }
|
||||
, { code: 'zh_cn', file: 'zh_CN', language: '中文(简体)', speechCode: 'cmn-Hans-CN' }
|
||||
, { code: 'zh_tw', file: 'zh_TW', language: '中文(繁體)', speechCode: 'cmn-Hant-TW' }
|
||||
// , { code: 'zh_tw', file: 'zh_TW', language: '中文(繁體)', speechCode: 'cmn-Hant-TW' }
|
||||
];
|
||||
|
||||
var translations = {};
|
||||
|
||||
|
||||
language.translations = translations;
|
||||
|
||||
language.offerTranslations = function offerTranslations(localization) {
|
||||
language.offerTranslations = function offerTranslations (localization) {
|
||||
translations = localization;
|
||||
language.translations = translations;
|
||||
}
|
||||
|
||||
// case sensitive
|
||||
language.translateCS = function translateCaseSensitive(text) {
|
||||
language.translateCS = function translateCaseSensitive (text) {
|
||||
if (translations[text]) {
|
||||
return translations[text];
|
||||
}
|
||||
// console.log('localization:', text, 'not found');
|
||||
// console.log('localization:', text, 'not found');
|
||||
return text;
|
||||
};
|
||||
|
||||
// case insensitive
|
||||
language.translateCI = function translateCaseInsensitive(text) {
|
||||
language.translateCI = function translateCaseInsensitive (text) {
|
||||
var utext = text.toUpperCase();
|
||||
_.forEach(translations, function (ts, key) {
|
||||
_.forEach(translations, function(ts, key) {
|
||||
var ukey = key.toUpperCase();
|
||||
if (ukey === utext) {
|
||||
text = ts;
|
||||
@@ -70,69 +72,96 @@ function init(fs) {
|
||||
return text;
|
||||
};
|
||||
|
||||
language.translate = function translate(text, options) {
|
||||
language.translate = function translate (text, options) {
|
||||
var translated;
|
||||
if (options && options.ci) {
|
||||
translated = language.translateCI(text);
|
||||
} else {
|
||||
translated = language.translateCS(text);
|
||||
}
|
||||
if (options && options.params) {
|
||||
for (var i = 0; i < options.params.length; i++) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
var r = new RegExp('\%' + (i+1), 'g');
|
||||
translated = translated.replace(r, options.params[i]);
|
||||
|
||||
var hasCI = false;
|
||||
var hasParams = false;
|
||||
|
||||
if (options) {
|
||||
hasCI = Object.prototype.hasOwnProperty.call(options,'ci');
|
||||
hasParams = Object.prototype.hasOwnProperty.call(options,'params');
|
||||
}
|
||||
|
||||
var keys = hasParams ? options.params : null;
|
||||
|
||||
if (options && !hasCI && !hasParams) {
|
||||
keys = [];
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
keys.push(arguments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (options && (hasCI || hasParams) && arguments.length > 2) {
|
||||
if (!keys) keys = [];
|
||||
for (i = 2; i < arguments.length; i++) {
|
||||
keys.push(arguments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (keys) {
|
||||
for (i = 0; i < keys.length; i++) {
|
||||
/* eslint-disable-next-line no-useless-escape, security/detect-non-literal-regexp */ // validated false positive
|
||||
var r = new RegExp('\%' + (i + 1), 'g');
|
||||
translated = translated.replace(r, keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return translated;
|
||||
};
|
||||
|
||||
language.DOMtranslate = function DOMtranslate($) {
|
||||
language.DOMtranslate = function DOMtranslate ($) {
|
||||
// do translation of static text on load
|
||||
$('.translate').each(function () {
|
||||
$('.translate').each(function() {
|
||||
$(this).text(language.translate($(this).text()));
|
||||
});
|
||||
$('.titletranslate, .tip').each(function () {
|
||||
$(this).attr('title',language.translate($(this).attr('title')));
|
||||
$(this).attr('original-title',language.translate($(this).attr('original-title')));
|
||||
$(this).attr('placeholder',language.translate($(this).attr('placeholder')));
|
||||
});
|
||||
});
|
||||
$('.titletranslate, .tip').each(function() {
|
||||
$(this).attr('title', language.translate($(this).attr('title')));
|
||||
$(this).attr('original-title', language.translate($(this).attr('original-title')));
|
||||
$(this).attr('placeholder', language.translate($(this).attr('placeholder')));
|
||||
});
|
||||
};
|
||||
|
||||
language.getFilename = function getFilename(code) {
|
||||
language.getFilename = function getFilename (code) {
|
||||
|
||||
if (code == 'en') {
|
||||
return 'en/en.json';
|
||||
}
|
||||
|
||||
let file;
|
||||
language.languages.forEach(function (l) {
|
||||
language.languages.forEach(function(l) {
|
||||
if (l.code == code) file = l.file;
|
||||
});
|
||||
return file + '.json';
|
||||
}
|
||||
|
||||
// this is a server only call and needs fs by reference as the class is also used in the client
|
||||
language.loadLocalization = function loadLocalization(fs, path) {
|
||||
language.loadLocalization = function loadLocalization (fs, path) {
|
||||
let filename = './translations/' + this.getFilename(this.lang);
|
||||
if (path) filename = path.resolve(__dirname, filename);
|
||||
/* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive; well defined set of values
|
||||
const l = fs.readFileSync(filename);
|
||||
this.offerTranslations(JSON.parse(l));
|
||||
}
|
||||
|
||||
language.set = function set(newlang) {
|
||||
language.set = function set (newlang) {
|
||||
language.lang = newlang;
|
||||
|
||||
language.languages.forEach(function (l) {
|
||||
language.languages.forEach(function(l) {
|
||||
if (l.code === language.lang && l.speechCode) language.speechCode = l.speechCode;
|
||||
});
|
||||
|
||||
return language();
|
||||
};
|
||||
|
||||
language.get = function get(lang) {
|
||||
language.get = function get (lang) {
|
||||
var r;
|
||||
language.languages.forEach(function (l) {
|
||||
language.languages.forEach(function(l) {
|
||||
if (l.code === lang) r = l;
|
||||
});
|
||||
return r;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
function init () {
|
||||
|
||||
var bolus = {
|
||||
name: 'bolus'
|
||||
, label: 'Bolus'
|
||||
, pluginType: 'fake'
|
||||
};
|
||||
|
||||
bolus.getPrefs = function getPrefs(sbx) {
|
||||
return {
|
||||
renderFormat: sbx.extendedSettings.renderFormat ? sbx.extendedSettings.renderFormat : 'default'
|
||||
, renderOver: sbx.extendedSettings.renderOver ? sbx.extendedSettings.renderOver : 0
|
||||
, notifyOver: sbx.extendedSettings.notifyOver ? sbx.extendedSettings.notifyOver : 0
|
||||
};
|
||||
};
|
||||
|
||||
return bolus;
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
+21
-21
@@ -15,87 +15,87 @@ function init() {
|
||||
return [
|
||||
{ val: '<none>'
|
||||
, name: '<none>'
|
||||
, bg: true, insulin: true, carbs: true, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: true, carbs: true, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'BG Check'
|
||||
, name: 'BG Check'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Snack Bolus'
|
||||
, name: 'Snack Bolus'
|
||||
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Meal Bolus'
|
||||
, name: 'Meal Bolus'
|
||||
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Correction Bolus'
|
||||
, name: 'Correction Bolus'
|
||||
, bg: true, insulin: true, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: true, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Carb Correction'
|
||||
, name: 'Carb Correction'
|
||||
, bg: true, insulin: false, carbs: true, protein: true, fat: true, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: true, protein: true, fat: true, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Combo Bolus'
|
||||
, name: 'Combo Bolus'
|
||||
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: true, percent: false, absolute: false, profile: false, split: true
|
||||
, bg: true, insulin: true, carbs: true, protein: true, fat: true, prebolus: true, duration: true, percent: false, absolute: false, profile: false, split: true, sensor: false
|
||||
}
|
||||
, { val: 'Announcement'
|
||||
, name: 'Announcement'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Note'
|
||||
, name: 'Note'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Question'
|
||||
, name: 'Question'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Exercise'
|
||||
, name: 'Exercise'
|
||||
, bg: false, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: false, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Site Change'
|
||||
, name: 'Pump Site Change'
|
||||
, bg: true, insulin: true, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: true, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Sensor Start'
|
||||
, name: 'CGM Sensor Start'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: true
|
||||
}
|
||||
, { val: 'Sensor Change'
|
||||
, name: 'CGM Sensor Insert'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: true
|
||||
}
|
||||
, { val: 'Sensor Stop'
|
||||
, name: 'CGM Sensor Stop'
|
||||
, bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Pump Battery Change'
|
||||
, name: 'Pump Battery Change'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Insulin Change'
|
||||
, name: 'Insulin Cartridge Change'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Temp Basal Start'
|
||||
, name: 'Temp Basal Start'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: true, absolute: true, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: true, absolute: true, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Temp Basal End'
|
||||
, name: 'Temp Basal End'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
, { val: 'Profile Switch'
|
||||
, name: 'Profile Switch'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: true, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: true, percent: false, absolute: false, profile: true, split: false, sensor: false
|
||||
}
|
||||
, { val: 'D.A.D. Alert'
|
||||
, name: 'D.A.D. Alert'
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false
|
||||
, bg: true, insulin: false, carbs: false, protein: false, fat: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false, split: false, sensor: false
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ function init (ctx) {
|
||||
, require('./insulinage')(ctx)
|
||||
, require('./batteryage')(ctx)
|
||||
, require('./basalprofile')(ctx)
|
||||
, require('./bolus')(ctx) // fake plugin to hold extended settings
|
||||
, require('./boluscalc')(ctx) // fake plugin to show/hide
|
||||
, require('./profile')(ctx) // fake plugin to hold extended settings
|
||||
, require('./speech')(ctx)
|
||||
@@ -128,7 +129,7 @@ function init (ctx) {
|
||||
};
|
||||
|
||||
//these plugins are either always on or have custom settings
|
||||
plugins.specialPlugins = 'ar2 bgnow delta direction timeago upbat rawbg errorcodes profile';
|
||||
plugins.specialPlugins = 'ar2 bgnow delta direction timeago upbat rawbg errorcodes profile bolus';
|
||||
|
||||
plugins.shownPlugins = function(sbx) {
|
||||
return _filter(enabledPlugins, function filterPlugins (plugin) {
|
||||
|
||||
+25
-7
@@ -34,6 +34,14 @@ function init (ctx) {
|
||||
var retroFields = cleanList(sbx.extendedSettings.retroFields);
|
||||
retroFields = isEmpty(retroFields) ? ['reservoir', 'battery'] : retroFields;
|
||||
|
||||
var profile = sbx.data.profile;
|
||||
var warnBattQuietNight = sbx.extendedSettings.warnBattQuietNight;
|
||||
|
||||
if (warnBattQuietNight && (!profile || !profile.hasData() || !profile.getTimezone())) {
|
||||
console.warn('PUMP_WARN_BATT_QUIET_NIGHT requires a treatment profile with time zone set to obtain user time zone');
|
||||
warnBattQuietNight = false;
|
||||
}
|
||||
|
||||
return {
|
||||
fields: fields
|
||||
, retroFields: retroFields
|
||||
@@ -47,6 +55,9 @@ function init (ctx) {
|
||||
, urgentBattP: sbx.extendedSettings.urgentBattP || 20
|
||||
, warnOnSuspend: sbx.extendedSettings.warnOnSuspend || false
|
||||
, enableAlerts: sbx.extendedSettings.enableAlerts || false
|
||||
, warnBattQuietNight: warnBattQuietNight || false
|
||||
, dayStart: sbx.settings.dayStart
|
||||
, dayEnd: sbx.settings.dayEnd
|
||||
};
|
||||
};
|
||||
|
||||
@@ -225,7 +236,11 @@ function init (ctx) {
|
||||
function updateReservoir (prefs, result) {
|
||||
if (result.reservoir) {
|
||||
result.reservoir.label = 'Reservoir';
|
||||
result.reservoir.display = result.reservoir.value.toPrecision(3) + 'U';
|
||||
if (result.reservoir_display_override) {
|
||||
result.reservoir.display = result.reservoir_display_override;
|
||||
} else {
|
||||
result.reservoir.display = result.reservoir.value.toPrecision(3) + 'U';
|
||||
}
|
||||
if (result.reservoir.value < prefs.urgentRes) {
|
||||
result.reservoir.level = levels.URGENT;
|
||||
result.reservoir.message = 'URGENT: Pump Reservoir Low';
|
||||
@@ -242,17 +257,17 @@ function init (ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateBattery (type, prefs, result) {
|
||||
function updateBattery (type, prefs, result, batteryWarn) {
|
||||
if (result.battery) {
|
||||
result.battery.label = 'Battery';
|
||||
result.battery.display = result.battery.value + type;
|
||||
var urgent = type === 'v' ? prefs.urgentBattV : prefs.urgentBattP;
|
||||
var warn = type === 'v' ? prefs.warnBattV : prefs.warnBattP;
|
||||
|
||||
if (result.battery.value < urgent) {
|
||||
if (result.battery.value < urgent && batteryWarn) {
|
||||
result.battery.level = levels.URGENT;
|
||||
result.battery.message = 'URGENT: Pump Battery Low';
|
||||
} else if (result.battery.value < warn) {
|
||||
} else if (result.battery.value < warn && batteryWarn) {
|
||||
result.battery.level = levels.WARN;
|
||||
result.battery.message = 'Warning, Pump Battery Low';
|
||||
} else {
|
||||
@@ -296,11 +311,14 @@ function init (ctx) {
|
||||
|
||||
function prepareData (prop, prefs, sbx) {
|
||||
var pump = (prop && prop.pump) || { };
|
||||
|
||||
var time = (sbx.data.profile && sbx.data.profile.getTimezone()) ? moment(sbx.time).tz(sbx.data.profile.getTimezone()) : moment(sbx.time);
|
||||
var now = time.hours() + time.minutes() / 60.0 + time.seconds() / 3600.0;
|
||||
var batteryWarn = !(prefs.warnBattQuietNight && (now < prefs.dayStart || now > prefs.dayEnd));
|
||||
var result = {
|
||||
level: levels.NONE
|
||||
, clock: pump.clock ? { value: moment(pump.clock) } : null
|
||||
, reservoir: pump.reservoir || pump.reservoir === 0 ? { value: pump.reservoir } : null
|
||||
, reservoir_display_override: pump.reservoir_display_override || null
|
||||
, manufacturer: pump.manufacturer
|
||||
, model: pump.model
|
||||
, extended: pump.extended || null
|
||||
@@ -312,10 +330,10 @@ function init (ctx) {
|
||||
|
||||
if (pump.battery && pump.battery.percent) {
|
||||
result.battery = { value: pump.battery.percent, unit: 'percent' };
|
||||
updateBattery('%', prefs, result);
|
||||
updateBattery('%', prefs, result, batteryWarn);
|
||||
} else if (pump.battery && pump.battery.voltage) {
|
||||
result.battery = { value: pump.battery.voltage, unit: 'volts'};
|
||||
updateBattery('v', prefs, result);
|
||||
updateBattery('v', prefs, result, batteryWarn);
|
||||
}
|
||||
|
||||
result.device = { label: translate('Device'), display: prop.device };
|
||||
|
||||
@@ -184,6 +184,12 @@ function init(ctx) {
|
||||
if (!_.isEmpty(latest[event].notes)) {
|
||||
info.push({label: translate('Notes'), value: latest[event].notes});
|
||||
}
|
||||
if (!_.isEmpty(latest[event].transmitterId)) {
|
||||
info.push({label: translate('Transmitter ID'), value: latest[event].transmitterId});
|
||||
}
|
||||
if (!_.isEmpty(latest[event].sensorCode)) {
|
||||
info.push({label: translate('Sensor Code'), value: latest[event].sensorCode});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,12 +20,17 @@ function init(ctx) {
|
||||
function filterTreatments (sbx) {
|
||||
var treatments = sbx.data.treatments;
|
||||
|
||||
var includeBolusesOver = sbx.extendedSettings.includeBolusesOver || 0;
|
||||
|
||||
treatments = _.filter(treatments, function notOpenAPS (treatment) {
|
||||
var ok = true;
|
||||
var enteredBy = treatment.enteredBy;
|
||||
if (enteredBy && (enteredBy.indexOf('openaps://') === 0 || enteredBy.indexOf('loop://') === 0)) {
|
||||
ok = _.indexOf(MANUAL_TREATMENTS, treatment.eventType) >= 0;
|
||||
}
|
||||
if (ok && _.isNumber(treatment.insulin) && _.includes(['Meal Bolus', 'Correction Bolus'], treatment.eventType)) {
|
||||
ok = treatment.insulin >= includeBolusesOver;
|
||||
}
|
||||
return ok;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
var init = function init () {
|
||||
//for the tests window isn't the global object
|
||||
var $ = window.$;
|
||||
var _ = window._;
|
||||
@@ -99,12 +100,12 @@
|
||||
_.each(mongoprofile.store, function eachStoredProfile (p) {
|
||||
// allign with default profile
|
||||
for (var key in defaultprofile) {
|
||||
if (defaultprofile.hasOwnProperty(key) && !p.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(defaultprofile,key) && !Object.prototype.hasOwnProperty.call(p,key)) {
|
||||
p[key] = defaultprofile[key];
|
||||
}
|
||||
}
|
||||
for (key in p) {
|
||||
if (p.hasOwnProperty(key) && !defaultprofile.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(p,key) && !Object.prototype.hasOwnProperty.call(defaultprofile,key)) {
|
||||
delete p[key];
|
||||
}
|
||||
}
|
||||
@@ -231,7 +232,7 @@
|
||||
$('#pe_profiles').empty();
|
||||
|
||||
for (var key in record.store) {
|
||||
if (record.store.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(record.store,key)) {
|
||||
$('#pe_profiles').append('<option value="' + key + '">' + key + '</option>');
|
||||
}
|
||||
}
|
||||
@@ -655,7 +656,7 @@
|
||||
var adjustedRecord = _.cloneDeep(record);
|
||||
|
||||
for (var key in adjustedRecord.store) {
|
||||
if (adjustedRecord.store.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(adjustedRecord.store,key)) {
|
||||
var profile = adjustedRecord.store[key];
|
||||
if (!profile.perGIvalues) {
|
||||
delete profile.perGIvalues;
|
||||
@@ -706,7 +707,7 @@
|
||||
function getFirstAvailableProfile(record) {
|
||||
var availableProfiles = [];
|
||||
for (var key in record.store) {
|
||||
if (record.store.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(record.store,key)) {
|
||||
if (key !== currentprofile) {
|
||||
availableProfiles.push(key);
|
||||
}
|
||||
@@ -721,4 +722,6 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
};
|
||||
|
||||
module.exports = init;
|
||||
@@ -7,10 +7,10 @@ var times = require('./times');
|
||||
|
||||
var cacheTTL = 5000;
|
||||
var prevBasalTreatment = null;
|
||||
var cache = new c.Cache();
|
||||
|
||||
function init (profileData) {
|
||||
|
||||
var cache = new c.Cache();
|
||||
var profile = {};
|
||||
|
||||
profile.clear = function clear() {
|
||||
@@ -19,6 +19,8 @@ function init (profileData) {
|
||||
prevBasalTreatment = null;
|
||||
}
|
||||
|
||||
profile.clear();
|
||||
|
||||
profile.loadData = function loadData (profileData) {
|
||||
if (profileData && profileData.length) {
|
||||
profile.data = profile.convertToProfileStore(profileData);
|
||||
@@ -36,7 +38,7 @@ function init (profileData) {
|
||||
var newObject = {};
|
||||
newObject.defaultProfile = 'Default';
|
||||
newObject.store = {};
|
||||
newObject.startDate = profile.startDate;
|
||||
newObject.startDate = profile.startDate ? profile.startDate : '1980-01-01';
|
||||
newObject._id = profile._id;
|
||||
newObject.convertedOnTheFly = true;
|
||||
delete profile.startDate;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// TODO:
|
||||
// - bypass nightmode in reports
|
||||
// - on save/delete treatment ctx.bus.emit('data-received'); is not enough. we must add something like 'data-updated'
|
||||
|
||||
var init = function init () {
|
||||
'use strict';
|
||||
@@ -249,7 +246,11 @@ var init = function init () {
|
||||
options.cob = true;
|
||||
options.openAps = true;
|
||||
}
|
||||
|
||||
options.bgcheck = $('#rp_optionsbgcheck').is(':checked');
|
||||
options.othertreatments = $('#rp_optionsothertreatments').is(':checked');
|
||||
|
||||
const reportStorage = require('./reportstorage');
|
||||
reportStorage.saveProps(options);
|
||||
var matchesneeded = 0;
|
||||
|
||||
// date range
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const storage = require('js-storage').localStorage;
|
||||
const COOKIE_KEY = 'reportProperties';
|
||||
const defaultValues = {
|
||||
insulin: true,
|
||||
carbs: true,
|
||||
basal: true,
|
||||
notes: false,
|
||||
food: true,
|
||||
raw: false,
|
||||
iob: false,
|
||||
cob: false,
|
||||
predicted: false,
|
||||
openAps: false,
|
||||
insulindistribution: true,
|
||||
predictedTruncate: true,
|
||||
bgcheck: true,
|
||||
othertreatments: false
|
||||
};
|
||||
let cachedProps;
|
||||
|
||||
const saveProps = function (props) {
|
||||
let propsToSave = {};
|
||||
for (const prop in props) {
|
||||
if (!Object.prototype.hasOwnProperty.call(defaultValues, prop))
|
||||
continue;
|
||||
propsToSave[prop] = props[prop];
|
||||
}
|
||||
storage.set(COOKIE_KEY, propsToSave);
|
||||
};
|
||||
|
||||
const getValue = function (p) {
|
||||
if (!cachedProps)
|
||||
cachedProps = storage.get(COOKIE_KEY) || defaultValues;
|
||||
return cachedProps[p];
|
||||
};
|
||||
|
||||
module.exports = {saveProps: saveProps, getValue: getValue};
|
||||
@@ -18,22 +18,25 @@ function init () {
|
||||
module.exports = init;
|
||||
|
||||
daytoday.html = function html (client) {
|
||||
const reportStorage = require('../report/reportstorage');
|
||||
var translate = client.translate;
|
||||
var ret =
|
||||
'<h2>' + translate('Day to day') + '</h2>' +
|
||||
'<b>' + translate('To see this report, press SHOW while in this view') + '</b><br>' +
|
||||
translate('Display') + ': ' +
|
||||
'<label><input type="checkbox" id="rp_optionsinsulin" checked><span style="color:blue;opacity:0.5">' + translate('Insulin') + '</span></label>' +
|
||||
'<label><input type="checkbox" id="rp_optionscarbs" checked><span style="color:red;opacity:0.5">' + translate('Carbs') + '</span></label>' +
|
||||
'<label><input type="checkbox" id="rp_optionsbasal" checked><span style="color:#0099ff;opacity:0.5">' + translate('Basal rate') + '</span></label>' +
|
||||
'<label><input type="checkbox" id="rp_optionsnotes">' + translate('Notes') + '</label>' +
|
||||
'<label><input type="checkbox" id="rp_optionsfood" checked>' + translate('Food') + '</label>' +
|
||||
'<label><input type="checkbox" id="rp_optionsraw"><span style="color:gray;opacity:1">' + translate('Raw') + '</span></label>' +
|
||||
'<label><input type="checkbox" id="rp_optionsiob"><span style="color:blue;opacity:0.5">' + translate('IOB') + '</span></label>' +
|
||||
'<label><input type="checkbox" id="rp_optionscob"><span style="color:red;opacity:0.5">' + translate('COB') + '</span></label>' +
|
||||
'<label><input type="checkbox" id="rp_optionspredicted"><span style="color:sienna;opacity:0.5">' + translate('Predictions') + '</span></label>' +
|
||||
'<label><input type="checkbox" id="rp_optionsopenaps"><span style="color:sienna;opacity:0.5">' + translate('OpenAPS') + '</span></label>' +
|
||||
'<label><input type="checkbox" id="rp_optionsdistribution" checked><span style="color:blue;opacity:0.5">' + translate('Insulin distribution') + '</span></label>' +
|
||||
`<label><input type="checkbox" id="rp_optionsinsulin" ${reportStorage.getValue('insulin') ? "checked" : ""}><span style="color:blue;opacity:0.5"> ${translate('Insulin')} </span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionscarbs" ${reportStorage.getValue('carbs') ? "checked" : ""}><span style="color:red;opacity:0.5">${translate('Carbs')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsbasal" ${reportStorage.getValue('basal') ? "checked" : ""}><span style="color:#0099ff;opacity:0.5">${translate('Basal rate')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsnotes" ${reportStorage.getValue('notes') ? "checked" : ""}>${translate('Notes')}</label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsfood" ${reportStorage.getValue('food') ? "checked" : ""}>${translate('Food')}</label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsraw" ${reportStorage.getValue('raw') ? "checked" : ""}><span style="color:gray;opacity:1">${translate('Raw')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsiob" ${reportStorage.getValue('iob') ? "checked" : ""}><span style="color:blue;opacity:0.5">${translate('IOB')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionscob" ${reportStorage.getValue('cob') ? "checked" : ""}><span style="color:red;opacity:0.5">${translate('COB')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionspredicted" ${reportStorage.getValue('predicted') ? "checked" : ""}><span style="color:sienna;opacity:0.5">${translate('Predictions')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsopenaps" ${reportStorage.getValue('openAps') ? "checked" : ""}><span style="color:sienna;opacity:0.5">${translate('OpenAPS')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsdistribution" ${reportStorage.getValue('insulindistribution') ? "checked" : ""}><span style="color:blue;opacity:0.5">${translate('Insulin distribution')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsbgcheck" ${reportStorage.getValue('bgcheck') ? "checked" : ""}><span style="color:#ff0000;opacity:0.5">${translate('BG Check')}</span></label>` +
|
||||
`<label><input type="checkbox" id="rp_optionsothertreatments" ${reportStorage.getValue('othertreatments') ? "checked" : ""}>${translate('View all treatments')}</span></label>` +
|
||||
' ' + translate('Size') +
|
||||
' <select id="rp_size">' +
|
||||
' <option x="800" y="250">800x250px</option>' +
|
||||
@@ -48,9 +51,9 @@ daytoday.html = function html (client) {
|
||||
translate('Linear') + '</label>' +
|
||||
'<label><input type="radio" name="rp_scale" id="rp_log">' +
|
||||
translate('Logarithmic') + '</label>' +
|
||||
'<div id="rp_predictedSettings" style="display:none">' +
|
||||
`<div id="rp_predictedSettings" ${reportStorage.getValue('predicted') ? '' : 'style="display:none"'}>` +
|
||||
translate('Truncate predictions: ') +
|
||||
'<input type="checkbox" id="rp_optionsPredictedTruncate" checked>' +
|
||||
`<input type="checkbox" id="rp_optionsPredictedTruncate" ${reportStorage.getValue('predictedTruncate') ? "checked" : ""}>` +
|
||||
'<br>' +
|
||||
translate('Predictions offset') + ': ' +
|
||||
'<b><label id="rp_predictedOffset"></label> minutes</b>' +
|
||||
@@ -762,7 +765,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
|
||||
}
|
||||
|
||||
if (treatment.insulin && options.insulin) {
|
||||
var dataLabel = client.utils.toFixedMin(treatment.insulin,2)+ 'U';
|
||||
var dataLabel = client.utils.toRoundedStr(treatment.insulin, 2)+ 'U';
|
||||
context.append('rect')
|
||||
.attr('y', yInsulinScale(treatment.insulin))
|
||||
.attr('height', chartHeight - yInsulinScale(treatment.insulin))
|
||||
@@ -872,7 +875,16 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
|
||||
.attr('y', yScale2(client.utils.scaleMgdl(414)) + padding.top)
|
||||
.attr('x', xScale2(treatment.mills + times.mins(treatment.duration).msecs / 2) + padding.left)
|
||||
.text(treatment.reason);
|
||||
} else if (!treatment.duration) {
|
||||
} else if (treatment.eventType === 'BG Check' && !treatment.duration && options.bgcheck) {
|
||||
context.append('circle')
|
||||
.attr('cx', xScale2(treatment.mills) + padding.left)
|
||||
.attr('cy', yScale2(scaledTreatmentBG(treatment, data.sgv)) + padding.top)
|
||||
.attr('fill', 'red')
|
||||
.style('opacity', 1)
|
||||
.attr('stroke-width', 1)
|
||||
.attr('stroke', 'darkred')
|
||||
.attr('r', 4);
|
||||
} else if (!treatment.duration && options.othertreatments) {
|
||||
// other treatments without duration
|
||||
context.append('circle')
|
||||
.attr('cx', xScale2(treatment.mills) + padding.left)
|
||||
@@ -889,7 +901,7 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio
|
||||
.attr('y', yScale2(scaledTreatmentBG(treatment, data.sgv)) + padding.top - 10)
|
||||
.attr('x', xScale2(treatment.mills) + padding.left + 10)
|
||||
.text(translate(client.careportal.resolveEventName(treatment.eventType)));
|
||||
} else if (treatment.duration) {
|
||||
} else if (treatment.duration && options.othertreatments) {
|
||||
// other treatments with duration
|
||||
context.append('rect')
|
||||
.attr('x', xScale2(treatment.mills) + padding.left)
|
||||
|
||||
@@ -42,24 +42,22 @@ loopalyzer.html = function html (client) {
|
||||
ret += '<br/>';
|
||||
ret += '<span id="rp_loopalyzertimeshiftinput">'; /* So we can show only if viewing multiple days style="display:none;" */
|
||||
ret += '<input type="checkbox" id="rp_loopalyzertimeshift">';
|
||||
ret += translate('Timeshift on meals larger than') + ' ';
|
||||
ret += '<input type="number" style="width: 3.5em" value="10" id="rp_loopalyzermincarbs">' + ' ' + translate('g carbs');
|
||||
ret += ' ' + translate('consumed between');
|
||||
ret += ' <select id="rp_loopalyzert1">';
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const H = (i < 10 ? '0' : '') + i;
|
||||
ret += ' <option t1="' + H + ':00"' + (i == 6 ? ' selected' : '') + '>' + H + ':00</option>';
|
||||
ret += ' <option t1="' + H + ':30">' + H + ':30</option>';
|
||||
|
||||
let numberInput = '<input type="number" style="width: 3.5em" value="10" id="rp_loopalyzermincarbs">';
|
||||
|
||||
function genTimePicker(id) {
|
||||
let timerPicker = ' <select id="' + id + '">';
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const H = (i < 10 ? '0' : '') + i;
|
||||
timerPicker += ' <option t1="' + H + ':00"' + (i == 6 ? ' selected' : '') + '>' + H + ':00</option>';
|
||||
timerPicker += ' <option t1="' + H + ':30">' + H + ':30</option>';
|
||||
}
|
||||
timerPicker += '</select>';
|
||||
return timerPicker;
|
||||
}
|
||||
ret += '</select>';
|
||||
ret += ' ' + translate('and');
|
||||
ret += ' <select id="rp_loopalyzert2">';
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const H = (i < 10 ? '0' : '') + i;
|
||||
ret += ' <option t2="' + H + ':00"' + (i == 9 ? ' selected' : '') + '>' + H + ':00</option>';
|
||||
ret += ' <option t2="' + H + ':30">' + H + ':30</option>';
|
||||
}
|
||||
ret += '</select>';
|
||||
|
||||
ret += translate('Timeshift on meals larger than %1 g carbs consumed between %2 and %3', numberInput, genTimePicker('rp_loopalyzert1'), genTimePicker('rp_loopalyzert2'));
|
||||
|
||||
ret += '</span>'; /* timeShift */
|
||||
ret += '<br/><br/>';
|
||||
ret += '<input type="button" onclick="loopalyzerMoreBackward();" value="<<< ' + translate('Previous') + '">';
|
||||
|
||||
+47
-58
@@ -4,16 +4,30 @@ const _get = require('lodash/get');
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
const bodyParser = require('body-parser');
|
||||
const randomToken = require('random-token');
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ejs = require('ejs');
|
||||
|
||||
function resolvePath(filePath) {
|
||||
|
||||
if (fs.existsSync(filePath)) return filePath;
|
||||
let p = path.join(__dirname, filePath);
|
||||
if (fs.existsSync(p)) return p;
|
||||
p = path.join(process.cwd(), filePath);
|
||||
if (fs.existsSync(p)) return p;
|
||||
|
||||
return require.resolve(filePath);
|
||||
}
|
||||
|
||||
function create (env, ctx) {
|
||||
var app = express();
|
||||
var appInfo = env.name + ' ' + env.version;
|
||||
app.set('title', appInfo);
|
||||
app.enable('trust proxy'); // Allows req.secure test on heroku https connections.
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
var insecureUseHttp = env.insecureUseHttp;
|
||||
var secureHstsHeader = env.secureHstsHeader;
|
||||
if (!insecureUseHttp) {
|
||||
@@ -66,7 +80,7 @@ function create (env, ctx) {
|
||||
, reportOnly: secureCspReportOnly
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
console.info('Enabled SECURE_HSTS_HEADER (HTTP Strict Transport Security)');
|
||||
const helmet = require('helmet');
|
||||
@@ -101,42 +115,27 @@ function create (env, ctx) {
|
||||
}
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
// this allows you to render .html files as templates in addition to .ejs
|
||||
app.engine('html', require('ejs').renderFile);
|
||||
app.set("views", path.join(__dirname, "views/"));
|
||||
app.set("views", resolvePath('/views'));
|
||||
|
||||
let cacheBuster = 'developmentMode';
|
||||
let lastModified = new Date();
|
||||
let busterPath = '/tmp/cacheBusterToken';
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
busterPath = process.cwd() + busterPath;
|
||||
} else {
|
||||
busterPath = __dirname + busterPath;
|
||||
}
|
||||
|
||||
if (fs.existsSync(busterPath)) {
|
||||
cacheBuster = fs.readFileSync(busterPath).toString().trim();
|
||||
var stats = fs.statSync(busterPath);
|
||||
lastModified = stats.mtime;
|
||||
}
|
||||
let cacheBuster = process.env.NODE_ENV == 'development' ? 'developmentMode': randomToken(16);
|
||||
app.locals.cachebuster = cacheBuster;
|
||||
|
||||
let lastModified = new Date();
|
||||
|
||||
app.get("/robots.txt", (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.send(['User-agent: *','Disallow: /'].join('\n'));
|
||||
});
|
||||
|
||||
const swcontent = fs.readFileSync(resolvePath('/views/service-worker.js'), { encoding: 'utf-8' });
|
||||
|
||||
app.get("/sw.js", (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/javascript');
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
res.setHeader('Last-Modified', lastModified.toUTCString());
|
||||
}
|
||||
res.send(ejs.render(fs.readFileSync(
|
||||
require.resolve(`${__dirname}/views/service-worker.js`),
|
||||
{ encoding: 'utf-8' }),
|
||||
{ locals: app.locals}
|
||||
));
|
||||
res.send(ejs.render(swcontent, { locals: app.locals} ));
|
||||
});
|
||||
|
||||
// Allow static resources to be cached for week
|
||||
@@ -147,19 +146,19 @@ function create (env, ctx) {
|
||||
console.log('Development environment detected, setting static file cache age to 1 second');
|
||||
}
|
||||
|
||||
var staticFiles = express.static(env.static_files, {
|
||||
var staticFiles = express.static(resolvePath(env.static_files), {
|
||||
maxAge
|
||||
});
|
||||
|
||||
// serve the static content
|
||||
app.use(staticFiles);
|
||||
|
||||
app.use('/translations', express.static('translations', {
|
||||
app.use('/translations', express.static(resolvePath('/translations'), {
|
||||
maxAge
|
||||
}));
|
||||
|
||||
if (ctx.bootErrors && ctx.bootErrors.length > 0) {
|
||||
const bootErrorView = require('./lib/server/booterror')(env, ctx);
|
||||
const bootErrorView = require('./booterror')(env, ctx);
|
||||
bootErrorView.setLocals(app.locals);
|
||||
app.get('*', bootErrorView);
|
||||
return app;
|
||||
@@ -185,11 +184,11 @@ function create (env, ctx) {
|
||||
///////////////////////////////////////////////////
|
||||
// api and json object variables
|
||||
///////////////////////////////////////////////////
|
||||
const apiRoot = require('./lib/api/root')(env, ctx);
|
||||
var api = require('./lib/api/')(env, ctx);
|
||||
var api3 = require('./lib/api3/')(env, ctx);
|
||||
var ddata = require('./lib/data/endpoints')(env, ctx);
|
||||
var notificationsV2 = require('./lib/api/notifications-v2')(app, ctx);
|
||||
const apiRoot = require('../api/root')(env, ctx);
|
||||
var api = require('../api/')(env, ctx);
|
||||
var api3 = require('../api3/')(env, ctx);
|
||||
var ddata = require('../data/endpoints')(env, ctx);
|
||||
var notificationsV2 = require('../api/notifications-v2')(app, ctx);
|
||||
|
||||
app.use(compression({
|
||||
filter: function shouldCompress (req, res) {
|
||||
@@ -242,22 +241,16 @@ function create (env, ctx) {
|
||||
});
|
||||
});
|
||||
|
||||
const clockviews = require('./lib/server/clocks.js')(env, ctx);
|
||||
const clockviews = require('./clocks.js')(env, ctx);
|
||||
clockviews.setLocals(app.locals);
|
||||
|
||||
app.use("/clock", clockviews);
|
||||
|
||||
app.use('/api', bodyParser({
|
||||
limit: 1048576 * 50
|
||||
}), apiRoot);
|
||||
app.use('/api', apiRoot);
|
||||
|
||||
app.use('/api/v1', bodyParser({
|
||||
limit: 1048576 * 50
|
||||
}), api);
|
||||
app.use('/api/v1', api);
|
||||
|
||||
app.use('/api/v2', bodyParser({
|
||||
limit: 1048576 * 50
|
||||
}), api);
|
||||
app.use('/api/v2', api);
|
||||
|
||||
app.use('/api/v2/properties', ctx.properties);
|
||||
app.use('/api/v2/authorization', ctx.authorization.endpoints);
|
||||
@@ -269,14 +262,19 @@ function create (env, ctx) {
|
||||
// pebble data
|
||||
app.get('/pebble', ctx.pebble);
|
||||
|
||||
const swaggerjson = fs.readFileSync(resolvePath(__dirname + '/swagger.json'), { encoding: 'utf-8' });
|
||||
const swaggeryaml = fs.readFileSync(resolvePath(__dirname + '/swagger.yaml'), { encoding: 'utf-8' });
|
||||
|
||||
// expose swagger.json
|
||||
app.get('/swagger.json', function(req, res) {
|
||||
res.sendFile(__dirname + '/swagger.json');
|
||||
res.setHeader("Content-Type", 'application/json');
|
||||
res.send(swaggerjson);
|
||||
});
|
||||
|
||||
// expose swagger.yaml
|
||||
app.get('/swagger.yaml', function(req, res) {
|
||||
res.sendFile(__dirname + '/swagger.yaml');
|
||||
res.setHeader("Content-Type", 'text/vnd.yaml');
|
||||
res.send(swaggeryaml);
|
||||
});
|
||||
|
||||
// API docs
|
||||
@@ -284,7 +282,7 @@ function create (env, ctx) {
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const swaggerUseSchema = schema => (...args) => swaggerUi.setup(schema)(...args);
|
||||
const swaggerDocument = require('./swagger.json');
|
||||
const swaggerDocumentApiV3 = require('./lib/api3/swagger.json');
|
||||
const swaggerDocumentApiV3 = require('../api3/swagger.json');
|
||||
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUseSchema(swaggerDocument));
|
||||
app.use('/api3-docs', swaggerUi.serve, swaggerUseSchema(swaggerDocumentApiV3));
|
||||
@@ -297,7 +295,6 @@ function create (env, ctx) {
|
||||
// if production, rely on postinstall script to run packaging for us
|
||||
|
||||
app.locals.bundle = '/bundle';
|
||||
|
||||
app.locals.mode = 'production';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@@ -308,7 +305,7 @@ function create (env, ctx) {
|
||||
app.locals.bundle = '/devbundle';
|
||||
|
||||
const webpack = require('webpack');
|
||||
var webpack_conf = require('./webpack.config');
|
||||
var webpack_conf = require('../../webpack.config');
|
||||
const middleware = require('webpack-dev-middleware');
|
||||
const compiler = webpack(webpack_conf);
|
||||
|
||||
@@ -316,7 +313,6 @@ function create (env, ctx) {
|
||||
middleware(compiler, {
|
||||
// webpack-dev-middleware options
|
||||
publicPath: webpack_conf.output.publicPath
|
||||
, lazy: false
|
||||
})
|
||||
);
|
||||
|
||||
@@ -326,16 +322,9 @@ function create (env, ctx) {
|
||||
}
|
||||
|
||||
// Production bundling
|
||||
var tmpFiles;
|
||||
if (fs.existsSync(process.cwd() + '/tmp/cacheBusterToken')) {
|
||||
tmpFiles = express.static('tmp', {
|
||||
maxAge: maxAge
|
||||
});
|
||||
} else {
|
||||
tmpFiles = express.static(__dirname + '/tmp', {
|
||||
maxAge: maxAge
|
||||
});
|
||||
}
|
||||
const tmpFiles = express.static(resolvePath('/tmp/public'), {
|
||||
maxAge: maxAge
|
||||
});
|
||||
|
||||
// serve the static content
|
||||
app.use('/bundle', tmpFiles);
|
||||
@@ -356,7 +345,7 @@ function create (env, ctx) {
|
||||
, coffee_match: /coffeescript/
|
||||
, json_match: /json/
|
||||
, cssmin: myCssmin
|
||||
, cache: __dirname + '/tmp'
|
||||
, cache: resolvePath('/tmp/public')
|
||||
, onerror: undefined
|
||||
, }));
|
||||
|
||||
+22
-17
@@ -1,8 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
|
||||
var UPDATE_THROTTLE = 5000;
|
||||
const _ = require('lodash');
|
||||
const UPDATE_THROTTLE = 5000;
|
||||
|
||||
function boot (env, language) {
|
||||
|
||||
@@ -11,23 +10,20 @@ function boot (env, language) {
|
||||
console.log('Executing startBoot');
|
||||
|
||||
ctx.runtimeState = 'booting';
|
||||
ctx.bus = require('../bus')(env.settings, ctx);
|
||||
ctx.adminnotifies = require('../adminnotifies')(ctx);
|
||||
if (env.notifies) {
|
||||
for (var i = 0; i < env.notifies.length; i++) {
|
||||
ctx.adminnotifies.addNotify(env.notifies[i]);
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Check Node version.
|
||||
// Latest Node 8 LTS and Latest Node 10 LTS are recommended and supported.
|
||||
// Latest Node version on Azure is tolerated, but not recommended
|
||||
// Latest Node (non LTS) version works, but is not recommended
|
||||
// Latest Node 10 to 14 LTS are recommended and supported.
|
||||
// Older Node versions or Node versions with known security issues will not work.
|
||||
// More explicit:
|
||||
// < 8 does not work, not supported
|
||||
// >= 8.15.1 works, supported and recommended
|
||||
// == 9.x does not work, not supported
|
||||
// == 10.15.2 works, not fully supported and not recommended (Azure version)
|
||||
// >= 10.16.0 works, supported and recommended
|
||||
// == 11.x does not work, not supported
|
||||
// >= 12.6.0 does work, not recommended, will not be supported. We only support Node LTS releases
|
||||
///////////////////////////////////////////////////
|
||||
function checkNodeVersion (ctx, next) {
|
||||
|
||||
@@ -56,7 +52,7 @@ function boot (env, language) {
|
||||
console.log('Executing checkEnv');
|
||||
|
||||
ctx.language = language;
|
||||
if (env.err) {
|
||||
if (env.err.length > 0) {
|
||||
ctx.bootErrors = ctx.bootErrors || [ ];
|
||||
ctx.bootErrors.push({'desc': 'ENV Error', err: env.err});
|
||||
}
|
||||
@@ -116,11 +112,20 @@ function boot (env, language) {
|
||||
err: 'MONGODB_URI setting is missing, cannot connect to database'});
|
||||
}
|
||||
|
||||
if (!env.api_secret) {
|
||||
if (!env.enclave.isApiKeySet()) {
|
||||
ctx.bootErrors.push({'desc': 'Mandatory setting missing',
|
||||
err: 'API_SECRET setting is missing, cannot enable REST API'});
|
||||
}
|
||||
|
||||
if (env.settings.authDefaultRoles == 'readable') {
|
||||
const message = {
|
||||
title: "Nightscout readable by world"
|
||||
,message: "Your Nightscout installation is readable by anyone who knows the web page URL. Please consider closing access to the site by following the instructions in the <a href=\"http://nightscout.github.io/nightscout/admin_tools/#turn-off-unauthorized-access\" target=\"_new\">Nightscout documentation</a>."
|
||||
,persistent: true
|
||||
};
|
||||
ctx.adminnotifies.addNotify(message);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -215,11 +220,11 @@ function boot (env, language) {
|
||||
ctx.food = require('./food')(env, ctx);
|
||||
ctx.pebble = require('./pebble')(env, ctx);
|
||||
ctx.properties = require('../api/properties')(env, ctx);
|
||||
ctx.bus = require('../bus')(env.settings, ctx);
|
||||
ctx.ddata = require('../data/ddata')();
|
||||
ctx.cache = require('./cache')(env,ctx);
|
||||
ctx.dataloader = require('../data/dataloader')(env, ctx);
|
||||
ctx.notifications = require('../notifications')(env, ctx);
|
||||
ctx.purifier = require('./purifier')(env,ctx);
|
||||
|
||||
if (env.settings.isEnabled('alexa') || env.settings.isEnabled('googlehome')) {
|
||||
ctx.virtAsstBase = require('../plugins/virtAsstBase')(env, ctx);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
'use strict;'
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const fs = require('fs');
|
||||
|
||||
// this is a class for holding potentially sensitive data in the app
|
||||
// the class also implement functions to use the data, so the data is not shared outside the class
|
||||
|
||||
const init = function init () {
|
||||
|
||||
const enclave = {};
|
||||
const secrets = {};
|
||||
const apiKey = Symbol('api-secret');
|
||||
const apiKeySHA1 = Symbol('api-secretSHA1');
|
||||
const apiKeySHA512 = Symbol('api-secretSHA512');
|
||||
const jwtKey = Symbol('jwtkey');
|
||||
let apiKeySet = false;
|
||||
|
||||
function readKey (filename) {
|
||||
let filePath = path.resolve(__dirname + '/../../tmp/' + filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
return fs.readFileSync(filePath).toString().trim();
|
||||
}
|
||||
console.error('Key file ', filePath, 'not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
secrets[jwtKey] = readKey('randomString');
|
||||
|
||||
function genHash(data, algorihtm) {
|
||||
const hash = crypto.createHash(algorihtm);
|
||||
data = hash.update(data, 'utf-8');
|
||||
return data.digest('hex');
|
||||
}
|
||||
|
||||
enclave.setApiKey = function setApiKey (keyValue) {
|
||||
if (keyValue.length < 12) return;
|
||||
apiKeySet = true;
|
||||
secrets[apiKey] = keyValue;
|
||||
secrets[apiKeySHA1] = genHash(keyValue,'sha1');
|
||||
secrets[apiKeySHA512] = genHash(keyValue,'sha512');
|
||||
}
|
||||
|
||||
enclave.isApiKeySet = function isApiKeySet () {
|
||||
return isApiKeySet;
|
||||
}
|
||||
|
||||
enclave.isApiKey = function isApiKey (keyValue) {
|
||||
return keyValue == secrets[apiKeySHA1] || keyValue == secrets[apiKeySHA512];
|
||||
}
|
||||
|
||||
enclave.setJWTKey = function setJWTKey (keyValue) {
|
||||
secrets[jwtKey] = keyValue;
|
||||
}
|
||||
|
||||
enclave.signJWT = function signJWT(token, lifetime) {
|
||||
const lt = lifetime ? lifetime : '8h';
|
||||
return jwt.sign(token, secrets[jwtKey], { expiresIn: lt });
|
||||
}
|
||||
|
||||
enclave.verifyJWT = function verifyJWT(tokenString) {
|
||||
try {
|
||||
return jwt.verify(tokenString, secrets[jwtKey]);
|
||||
} catch(err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
enclave.getSubjectHash = function getSubjectHash(id) {
|
||||
var shasum = crypto.createHash('sha1');
|
||||
shasum.update(secrets[apiKeySHA1]);
|
||||
shasum.update(id);
|
||||
return shasum.digest('hex');
|
||||
}
|
||||
|
||||
return enclave;
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
+65
-44
@@ -1,62 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
var _each = require('lodash/each');
|
||||
var _trim = require('lodash/trim');
|
||||
var _forIn = require('lodash/forIn');
|
||||
var _startsWith = require('lodash/startsWith');
|
||||
var _camelCase = require('lodash/camelCase');
|
||||
const _each = require('lodash/each');
|
||||
const _trim = require('lodash/trim');
|
||||
const _forIn = require('lodash/forIn');
|
||||
const _startsWith = require('lodash/startsWith');
|
||||
const _camelCase = require('lodash/camelCase');
|
||||
const enclave = require('./enclave');
|
||||
|
||||
var fs = require('fs');
|
||||
var crypto = require('crypto');
|
||||
var consts = require('./lib/constants');
|
||||
const mongoParser = require('mongo-url-parser');
|
||||
|
||||
var env = {
|
||||
settings: require('./lib/settings')()
|
||||
const stringEntropy = require('fast-password-entropy')
|
||||
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const consts = require('../constants');
|
||||
|
||||
const env = {
|
||||
settings: require('../settings')()
|
||||
};
|
||||
|
||||
var shadowEnv;
|
||||
|
||||
// Module to constrain all config and environment parsing to one spot.
|
||||
// See README.md for info about all the supported ENV VARs
|
||||
function config ( ) {
|
||||
function config () {
|
||||
|
||||
// Assume users will typo whitespaces into keys and values
|
||||
|
||||
shadowEnv = {};
|
||||
|
||||
Object.keys(process.env).forEach((key, index) => {
|
||||
shadowEnv[_trim(key)] = _trim(process.env[key]);
|
||||
shadowEnv[_trim(key)] = _trim(process.env[key]);
|
||||
});
|
||||
|
||||
env.PORT = readENV('PORT', 1337);
|
||||
env.HOSTNAME = readENV('HOSTNAME', null);
|
||||
env.IMPORT_CONFIG = readENV('IMPORT_CONFIG', null);
|
||||
env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/');
|
||||
env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', '/static');
|
||||
env.debug = {
|
||||
minify: readENVTruthy('DEBUG_MINIFY', true)
|
||||
};
|
||||
|
||||
if (env.err) {
|
||||
delete env.err;
|
||||
}
|
||||
env.err = [];
|
||||
env.notifies = [];
|
||||
env.enclave = enclave();
|
||||
|
||||
setSSL();
|
||||
setStorage();
|
||||
setAPISecret();
|
||||
setVersion();
|
||||
setStorage();
|
||||
updateSettings();
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
function setSSL() {
|
||||
function setSSL () {
|
||||
env.SSL_KEY = readENV('SSL_KEY');
|
||||
env.SSL_CERT = readENV('SSL_CERT');
|
||||
env.SSL_CA = readENV('SSL_CA');
|
||||
env.ssl = false;
|
||||
if (env.SSL_KEY && env.SSL_CERT) {
|
||||
env.ssl = {
|
||||
key: fs.readFileSync(env.SSL_KEY), cert: fs.readFileSync(env.SSL_CERT)
|
||||
key: fs.readFileSync(env.SSL_KEY)
|
||||
, cert: fs.readFileSync(env.SSL_CERT)
|
||||
};
|
||||
if (env.SSL_CA) {
|
||||
env.ca = fs.readFileSync(env.SSL_CA);
|
||||
@@ -66,13 +72,13 @@ function setSSL() {
|
||||
env.insecureUseHttp = readENVTruthy("INSECURE_USE_HTTP", false);
|
||||
env.secureHstsHeader = readENVTruthy("SECURE_HSTS_HEADER", true);
|
||||
env.secureHstsHeaderIncludeSubdomains = readENVTruthy("SECURE_HSTS_HEADER_INCLUDESUBDOMAINS", false);
|
||||
env.secureHstsHeaderPreload= readENVTruthy("SECURE_HSTS_HEADER_PRELOAD", false);
|
||||
env.secureHstsHeaderPreload = readENVTruthy("SECURE_HSTS_HEADER_PRELOAD", false);
|
||||
env.secureCsp = readENVTruthy("SECURE_CSP", false);
|
||||
env.secureCspReportOnly = readENVTruthy("SECURE_CSP_REPORT_ONLY", false);
|
||||
}
|
||||
|
||||
// A little ugly, but we don't want to read the secret into a var
|
||||
function setAPISecret() {
|
||||
function setAPISecret () {
|
||||
var useSecret = (readENV('API_SECRET') && readENV('API_SECRET').length > 0);
|
||||
//TODO: should we clear API_SECRET from process env?
|
||||
env.api_secret = null;
|
||||
@@ -81,22 +87,39 @@ function setAPISecret() {
|
||||
if (readENV('API_SECRET').length < consts.MIN_PASSPHRASE_LENGTH) {
|
||||
var msg = ['API_SECRET should be at least', consts.MIN_PASSPHRASE_LENGTH, 'characters'].join(' ');
|
||||
console.error(msg);
|
||||
env.err = {desc: msg};
|
||||
env.err.push({ desc: msg });
|
||||
} else {
|
||||
var shasum = crypto.createHash('sha1');
|
||||
shasum.update(readENV('API_SECRET'));
|
||||
env.api_secret = shasum.digest('hex');
|
||||
|
||||
const apiSecret = readENV('API_SECRET');
|
||||
delete process.env.API_SECRET;
|
||||
|
||||
env.enclave.setApiKey(apiSecret);
|
||||
var testresult = stringEntropy(apiSecret);
|
||||
|
||||
console.log('API_SECRET has', testresult, 'bits of entropy');
|
||||
|
||||
if (testresult < 60) {
|
||||
env.notifies.push({ persistent: true, title: 'Security issue', message: 'Weak API_SECRET detected. Please use a mix of small and CAPITAL letters, numbers and non-alphanumeric characters such as !#%&/ to reduce the risk of unauthorized access. The minimum length of the API_SECRET is 12 characters.' });
|
||||
}
|
||||
|
||||
if (env.storageURI) {
|
||||
const parsedURL = mongoParser(env.storageURI);
|
||||
if (parsedURL.auth && parsedURL.auth.password == apiSecret) {
|
||||
env.notifies.push({ persistent: true, title: 'Security issue', message: 'MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.' });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVersion() {
|
||||
var software = require('./package.json');
|
||||
function setVersion () {
|
||||
var software = require('../../package.json');
|
||||
env.version = software.version;
|
||||
env.name = software.name;
|
||||
}
|
||||
|
||||
function setStorage() {
|
||||
function setStorage () {
|
||||
env.storageURI = readENV('STORAGE_URI') || readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI') || readENV('MONGODB_URI');
|
||||
env.entries_collection = readENV('ENTRIES_COLLECTION') || readENV('MONGO_COLLECTION', 'entries');
|
||||
env.authentication_collections_prefix = readENV('MONGO_AUTHENTICATION_COLLECTIONS_PREFIX', 'auth_');
|
||||
@@ -110,14 +133,14 @@ function setStorage() {
|
||||
// TODO: clean up a bit
|
||||
// Some people prefer to use a json configuration file instead.
|
||||
// This allows a provided json config to override environment variables
|
||||
var DB = require('./database_configuration.json'),
|
||||
DB_URL = DB.url ? DB.url : env.storageURI,
|
||||
DB_COLLECTION = DB.collection ? DB.collection : env.entries_collection;
|
||||
var DB = require('../../database_configuration.json')
|
||||
, DB_URL = DB.url ? DB.url : env.storageURI
|
||||
, DB_COLLECTION = DB.collection ? DB.collection : env.entries_collection;
|
||||
env.storageURI = DB_URL;
|
||||
env.entries_collection = DB_COLLECTION;
|
||||
}
|
||||
|
||||
function updateSettings() {
|
||||
function updateSettings () {
|
||||
|
||||
var envNameOverrides = {
|
||||
UNITS: 'DISPLAY_UNITS'
|
||||
@@ -141,12 +164,12 @@ function updateSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
function readENV(varName, defaultValue) {
|
||||
function readENV (varName, defaultValue) {
|
||||
//for some reason Azure uses this prefix, maybe there is a good reason
|
||||
var value = shadowEnv['CUSTOMCONNSTR_' + varName]
|
||||
|| shadowEnv['CUSTOMCONNSTR_' + varName.toLowerCase()]
|
||||
|| shadowEnv[varName]
|
||||
|| shadowEnv[varName.toLowerCase()];
|
||||
var value = shadowEnv['CUSTOMCONNSTR_' + varName] ||
|
||||
shadowEnv['CUSTOMCONNSTR_' + varName.toLowerCase()] ||
|
||||
shadowEnv[varName] ||
|
||||
shadowEnv[varName.toLowerCase()];
|
||||
|
||||
if (varName == 'DISPLAY_UNITS') {
|
||||
if (value && value.toLowerCase().includes('mmol')) {
|
||||
@@ -159,11 +182,9 @@ function readENV(varName, defaultValue) {
|
||||
return value != null ? value : defaultValue;
|
||||
}
|
||||
|
||||
function readENVTruthy(varName, defaultValue) {
|
||||
function readENVTruthy (varName, defaultValue) {
|
||||
var value = readENV(varName, defaultValue);
|
||||
if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; }
|
||||
else if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; }
|
||||
else { value=defaultValue }
|
||||
if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } else if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } else { value = defaultValue }
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -173,13 +194,13 @@ function findExtendedSettings (envs) {
|
||||
extended.devicestatus = {};
|
||||
extended.devicestatus.advanced = true;
|
||||
extended.devicestatus.days = 1;
|
||||
if(shadowEnv['DEVICESTATUS_DAYS'] && shadowEnv['DEVICESTATUS_DAYS'] == '2') extended.devicestatus.days = 1;
|
||||
if (shadowEnv['DEVICESTATUS_DAYS'] && shadowEnv['DEVICESTATUS_DAYS'] == '2') extended.devicestatus.days = 1;
|
||||
|
||||
function normalizeEnv (key) {
|
||||
return key.toUpperCase().replace('CUSTOMCONNSTR_', '');
|
||||
}
|
||||
|
||||
_each(env.settings.enable, function eachEnable(enable) {
|
||||
_each(env.settings.enable, function eachEnable (enable) {
|
||||
if (_trim(enable)) {
|
||||
_forIn(envs, function eachEnvPair (value, key) {
|
||||
var env = normalizeEnv(key);
|
||||
@@ -199,6 +220,6 @@ function findExtendedSettings (envs) {
|
||||
}
|
||||
});
|
||||
return extended;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
const createDOMPurify = require('dompurify');
|
||||
const { JSDOM } = require('jsdom');
|
||||
const window = new JSDOM('').window;
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
function init (env, ctx) {
|
||||
|
||||
const purifier = {};
|
||||
|
||||
function iterate (obj) {
|
||||
for (var property in obj) {
|
||||
if (obj.hasOwnProperty(property)) {
|
||||
if (typeof obj[property] == 'object')
|
||||
iterate(obj[property]);
|
||||
else
|
||||
if (isNaN(obj[property])) {
|
||||
const clean = DOMPurify.sanitize(obj[property]);
|
||||
if (obj[property] !== clean) {
|
||||
obj[property] = clean;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
purifier.purifyObject = function purifyObject (obj) {
|
||||
return iterate(obj);
|
||||
}
|
||||
|
||||
return purifier;
|
||||
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const env = require('./env')( );
|
||||
const language = require('./lib/language')();
|
||||
const language = require('../language')();
|
||||
const translate = language.set(env.settings.language).translate;
|
||||
language.loadLocalization(fs);
|
||||
|
||||
@@ -47,7 +47,7 @@ function create (app) {
|
||||
return transport.createServer(app);
|
||||
}
|
||||
|
||||
require('./lib/server/bootevent')(env, language).boot(function booted (ctx) {
|
||||
require('./bootevent')(env, language).boot(function booted (ctx) {
|
||||
|
||||
console.log('Boot event processing completed');
|
||||
|
||||
@@ -68,7 +68,7 @@ require('./lib/server/bootevent')(env, language).boot(function booted (ctx) {
|
||||
///////////////////////////////////////////////////
|
||||
// setup socket io for data and message transmission
|
||||
///////////////////////////////////////////////////
|
||||
var websocket = require('./lib/server/websocket')(env, ctx, server);
|
||||
var websocket = require('./websocket')(env, ctx, server);
|
||||
|
||||
ctx.bus.on('data-processed', function() {
|
||||
websocket.update();
|
||||
@@ -8,7 +8,7 @@
|
||||
"info": {
|
||||
"title": "Nightscout API",
|
||||
"description": "Own your DData with the Nightscout API",
|
||||
"version": "14.1.0",
|
||||
"version": "14.2.0",
|
||||
"license": {
|
||||
"name": "AGPL 3",
|
||||
"url": "https://www.gnu.org/licenses/agpl.txt"
|
||||
@@ -1192,6 +1192,14 @@
|
||||
"type": "string",
|
||||
"description": "The units for the glucose value, mg/dl or mmol."
|
||||
},
|
||||
"transmitterId": {
|
||||
"type": "string",
|
||||
"description": "The transmitter ID of the transmitter being started."
|
||||
},
|
||||
"sensorCode": {
|
||||
"type": "string",
|
||||
"description": "The code used to start a Dexcom G6 sensor."
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Description/notes of treatment."
|
||||
@@ -4,7 +4,7 @@ servers:
|
||||
info:
|
||||
title: Nightscout API
|
||||
description: Own your DData with the Nightscout API
|
||||
version: 14.1.0
|
||||
version: 14.2.0
|
||||
license:
|
||||
name: AGPL 3
|
||||
url: 'https://www.gnu.org/licenses/agpl.txt'
|
||||
@@ -891,6 +891,12 @@ components:
|
||||
units:
|
||||
type: string
|
||||
description: 'The units for the glucose value, mg/dl or mmol.'
|
||||
transmitterId:
|
||||
type: string
|
||||
description: 'The transmitter ID of the transmitter being started.'
|
||||
sensorCode:
|
||||
type: string
|
||||
description: 'The code used to start a Dexcom G6 sensor.'
|
||||
notes:
|
||||
type: string
|
||||
description: Description/notes of treatment.
|
||||
+44
-11
@@ -37,11 +37,13 @@ function init (env, ctx, server) {
|
||||
// This is little ugly copy but I was unable to pass testa after making module from status and share with /api/v1/status
|
||||
function status () {
|
||||
var versionNum = 0;
|
||||
var verParse = /(\d+)\.(\d+)\.(\d+)*/.exec(env.version);
|
||||
const vString = '' + env.version;
|
||||
const verParse = vString.split('.');
|
||||
if (verParse) {
|
||||
versionNum = 10000 * parseInt(verParse[1]) + 100 * parseInt(verParse[2]) + 1 * parseInt(verParse[3]);
|
||||
versionNum = 10000 * Number(verParse[0]) + 100 * Number(verParse[1]) + 1 * Number(verParse[2]);
|
||||
}
|
||||
var apiEnabled = env.api_secret ? true : false;
|
||||
|
||||
var apiEnabled = env.enclave.isApiKeySet();
|
||||
|
||||
var activeProfile = ctx.ddata.lastProfileFromSwitch;
|
||||
|
||||
@@ -84,6 +86,9 @@ function init (env, ctx, server) {
|
||||
}
|
||||
|
||||
function verifyAuthorization (message, ip, callback) {
|
||||
|
||||
if (!message) message = {};
|
||||
|
||||
ctx.authorization.resolve({ api_secret: message.secret, token: message.token, ip: ip }, function resolved (err, result) {
|
||||
|
||||
if (err) {
|
||||
@@ -252,9 +257,7 @@ function init (env, ctx, server) {
|
||||
}
|
||||
|
||||
var objId = new ObjectID(data._id);
|
||||
ctx.store.collection(collection).update(
|
||||
{ '_id': objId },
|
||||
{ $unset: data.data }
|
||||
ctx.store.collection(collection).update({ '_id': objId }, { $unset: data.data }
|
||||
, function(err, results) {
|
||||
|
||||
if (!err) {
|
||||
@@ -270,7 +273,7 @@ function init (env, ctx, server) {
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback({ result: 'success' });
|
||||
@@ -320,7 +323,13 @@ function init (env, ctx, server) {
|
||||
|
||||
// try to find exact match
|
||||
ctx.store.collection(collection).find(query).toArray(function findResult (err, array) {
|
||||
if (err || array.length > 0) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (array.length > 0) {
|
||||
console.log(LOG_DEDUP + 'Exact match');
|
||||
if (callback) {
|
||||
callback([array[0]]);
|
||||
@@ -363,7 +372,14 @@ function init (env, ctx, server) {
|
||||
// try to find similiar
|
||||
ctx.store.collection(collection).find(query_similiar).toArray(function findSimiliarResult (err, array) {
|
||||
// if found similiar just update date. next time it will match exactly
|
||||
if (err || array.length > 0) {
|
||||
|
||||
if (err) {
|
||||
console.error(err);
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (array.length > 0) {
|
||||
console.log(LOG_DEDUP + 'Found similiar', array[0]);
|
||||
array[0].created_at = data.data.created_at;
|
||||
var objId = new ObjectID(array[0]._id);
|
||||
@@ -408,14 +424,22 @@ function init (env, ctx, server) {
|
||||
|
||||
// try to find exact match
|
||||
ctx.store.collection(collection).find(queryDev).toArray(function findResult (err, array) {
|
||||
if (err || array.length > 0) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (array.length > 0) {
|
||||
console.log(LOG_DEDUP + 'Devicestatus exact match');
|
||||
if (callback) {
|
||||
callback([array[0]]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) {
|
||||
if (err != null && err.message) {
|
||||
console.log('devicestatus insertion error: ', err.message);
|
||||
@@ -439,7 +463,7 @@ function init (env, ctx, server) {
|
||||
console.log(data.collection + ' insertion error: ', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ctx.bus.emit('data-update', {
|
||||
type: data.collection
|
||||
, op: 'update'
|
||||
@@ -501,6 +525,15 @@ function init (env, ctx, server) {
|
||||
socket.on('authorize', function authorize (message, callback) {
|
||||
const remoteIP = socket.request.connection.remoteAddress;
|
||||
verifyAuthorization(message, remoteIP, function verified (err, authorization) {
|
||||
|
||||
if (err) {
|
||||
console.log('Websocket authorization failed:', err);
|
||||
socket.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('connected');
|
||||
|
||||
socketAuthorization = authorization;
|
||||
clientType = message.client;
|
||||
history = message.history || 48; //default history is 48 hours
|
||||
|
||||
+5
-2
@@ -8,6 +8,8 @@ function init () {
|
||||
var settings = {
|
||||
units: 'mg/dl'
|
||||
, timeFormat: 12
|
||||
, dayStart: 7.0
|
||||
, dayEnd: 21.0
|
||||
, nightMode: false
|
||||
, editMode: true
|
||||
, showRawbg: 'never'
|
||||
@@ -50,7 +52,6 @@ function init () {
|
||||
, deNormalizeDates: false
|
||||
, showClockDelta: false
|
||||
, showClockLastTime: false
|
||||
, bolusRenderOver: 1
|
||||
, frameUrl1: ''
|
||||
, frameUrl2: ''
|
||||
, frameUrl3: ''
|
||||
@@ -67,6 +68,7 @@ function init () {
|
||||
, frameName6: ''
|
||||
, frameName7: ''
|
||||
, frameName8: ''
|
||||
, authFailDelay: 5000
|
||||
};
|
||||
|
||||
var secureSettings = [
|
||||
@@ -102,6 +104,7 @@ function init () {
|
||||
, bgLow: mapNumber
|
||||
, bgTargetTop: mapNumber
|
||||
, bgTargetBottom: mapNumber
|
||||
, authFailDelay: mapNumber
|
||||
};
|
||||
|
||||
function filterObj(obj, secureKeys) {
|
||||
@@ -166,7 +169,7 @@ function init () {
|
||||
}
|
||||
|
||||
//TODO: getting sent in status.json, shouldn't be
|
||||
settings.DEFAULT_FEATURES = ['bgnow', 'delta', 'direction', 'timeago', 'devicestatus', 'upbat', 'errorcodes', 'profile', 'dbsize', 'runtimestate', 'basal', 'careportal'];
|
||||
settings.DEFAULT_FEATURES = ['bgnow', 'delta', 'direction', 'timeago', 'devicestatus', 'upbat', 'errorcodes', 'profile', 'bolus', 'dbsize', 'runtimestate', 'basal', 'careportal'];
|
||||
|
||||
var wasSet = [];
|
||||
|
||||
|
||||
@@ -24,10 +24,7 @@ function init(env, cb, forceNewConnection) {
|
||||
}
|
||||
|
||||
console.log('Setting up new connection to MongoDB');
|
||||
const timeout = 10 * 1000;
|
||||
const options = {
|
||||
connectTimeoutMS: timeout,
|
||||
socketTimeoutMS: timeout,
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
};
|
||||
@@ -38,7 +35,7 @@ function init(env, cb, forceNewConnection) {
|
||||
try {
|
||||
await mongo.client.connect();
|
||||
|
||||
console.log('Successfully established a connected to MongoDB');
|
||||
console.log('Successfully established connection to MongoDB');
|
||||
|
||||
const dbName = mongo.client.s.options.dbName;
|
||||
mongo.db = mongo.client.db(dbName);
|
||||
@@ -69,7 +66,7 @@ function init(env, cb, forceNewConnection) {
|
||||
setTimeout(connect_with_retry, timeout, i + 1);
|
||||
if (i == 1) cb(new Error('MongoDB connection failed! Double check the MONGODB_URI setting in Heroku.'), null);
|
||||
} else {
|
||||
cb(new Error('MONGODB_URI ' + env.storageURI + ' seems invalid: ' + err.message));
|
||||
cb(new Error('MONGODB_URI seems invalid: ' + err.message));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+7
-3
@@ -34,12 +34,16 @@ function init(ctx) {
|
||||
}
|
||||
};
|
||||
|
||||
utils.toFixedMin = function toFixedMin(value,digits) {
|
||||
/**
|
||||
* Round the number to maxDigits places, return a string
|
||||
* that truncates trailing zeros
|
||||
*/
|
||||
utils.toRoundedStr = function toRoundedStr (value, maxDigits) {
|
||||
if (!value) {
|
||||
return '0';
|
||||
}
|
||||
var mult = Math.pow(10,digits);
|
||||
var fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult;
|
||||
const mult = Math.pow(10, maxDigits);
|
||||
const fixed = Math.sign(value) * Math.round(Math.abs(value)*mult) / mult;
|
||||
if (isNaN(fixed)) return '0';
|
||||
return String(fixed);
|
||||
};
|
||||
|
||||
+2
-1
@@ -11,4 +11,5 @@ ALARM_TYPES="predict"
|
||||
LANGUAGE=en
|
||||
INSECURE_USE_HTTP=true
|
||||
PORT=1337
|
||||
NODE_ENV=development
|
||||
NODE_ENV=development
|
||||
AUTH_FAIL_DELAY=50
|
||||
Generated
+3294
-2306
File diff suppressed because it is too large
Load Diff
+44
-26
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nightscout",
|
||||
"version": "14.1.0",
|
||||
"version": "14.2.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": "AGPL-3.0",
|
||||
"author": "Nightscout Team",
|
||||
@@ -26,22 +26,30 @@
|
||||
"url": "https://github.com/nightscout/cgm-remote-monitor/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"test": "env-cmd -f ./my.test.env mocha --exit tests/*.test.js",
|
||||
"test-single": "env-cmd -f ./my.test.env mocha --exit tests/$TEST.test.js",
|
||||
"test-ci": "env-cmd -f ./ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --exit tests/*.test.js",
|
||||
"start": "node lib/server/server.js",
|
||||
"test": "env-cmd -f ./my.test.env mocha --require ./tests/hooks.js -exit ./tests/*.test.js",
|
||||
"test-single": "env-cmd -f ./my.test.env mocha --require ./tests/hooks.js --exit ./tests/$TEST.test.js",
|
||||
"test-ci": "env-cmd -f ./ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --require ./tests/hooks.js --exit ./tests/*.test.js",
|
||||
"env": "env",
|
||||
"postinstall": "webpack --mode production --config webpack.config.js && npm run-script update-buster",
|
||||
"bundle": "webpack --mode production --config webpack.config.js && npm run-script update-buster",
|
||||
"bundle-dev": "webpack --mode development --config webpack.config.js && npm run-script update-buster",
|
||||
"postinstall": "webpack --mode production --config webpack.config.js && npm run-script generate-keys",
|
||||
"bundle": "webpack --mode production --config webpack.config.js && npm run-script generate-keys",
|
||||
"bundle-dev": "webpack --mode development --config webpack.config.js && npm run-script generate-keys",
|
||||
"bundle-analyzer": "webpack --mode development --config webpack.config.js --profile --json > stats.json && webpack-bundle-analyzer stats.json",
|
||||
"update-buster": "node bin/generateCacheBuster.js >tmp/cacheBusterToken",
|
||||
"generate-keys": "node bin/generateRandomString.js >tmp/randomString",
|
||||
"coverage": "cat ./coverage/lcov.info | env-cmd -f ./ci.test.env codacy-coverage",
|
||||
"dev": "env-cmd -f ./my.env nodemon server.js 0.0.0.0",
|
||||
"prod": "env-cmd -f ./my.prod.env node server.js 0.0.0.0",
|
||||
"dev": "env-cmd -f ./my.env nodemon --inspect lib/server/server.js 0.0.0.0",
|
||||
"dev-test": "env-cmd -f ./my.devtest.env nodemon --inspect lib/server/server.js 0.0.0.0",
|
||||
"prod": "env-cmd -f ./my.prod.env node lib/server/server.js 0.0.0.0",
|
||||
"lint": "eslint lib"
|
||||
},
|
||||
"main": "server.js",
|
||||
"main": "lib/server/server.js",
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"tests/*",
|
||||
"node_modules/*",
|
||||
"bin/*"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"blanket": {
|
||||
"pattern": [
|
||||
@@ -62,7 +70,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"acorn": "^8.0.5",
|
||||
"acorn-jsx": "^5.3.1",
|
||||
"apn": "^2.2.0",
|
||||
"async": "^0.9.2",
|
||||
"babel-loader": "^8.1.0",
|
||||
@@ -70,26 +80,30 @@
|
||||
"body-parser": "^1.19.0",
|
||||
"bootevent": "0.0.1",
|
||||
"braces": "^3.0.2",
|
||||
"buffer": "^6.0.3",
|
||||
"compression": "^1.7.4",
|
||||
"css-loader": "^1.0.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^5.0.1",
|
||||
"cssmin": "^0.4.3",
|
||||
"csv-stringify": "^5.5.1",
|
||||
"d3": "^5.16.0",
|
||||
"dompurify": "^2.2.6",
|
||||
"easyxml": "^2.0.1",
|
||||
"ejs": "^2.7.4",
|
||||
"errorhandler": "^1.5.1",
|
||||
"event-stream": "3.3.4",
|
||||
"expose-loader": "^0.7.5",
|
||||
"expose-loader": "^2.0.0",
|
||||
"express": "^4.17.1",
|
||||
"express-minify": "^1.0.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"fast-password-entropy": "^1.1.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"flot": "^0.8.3",
|
||||
"helmet": "^4.0.0",
|
||||
"jquery": "^3.5.1",
|
||||
"jquery-ui-bundle": "^1.12.1-migrate",
|
||||
"jquery.tooltips": "^1.0.0",
|
||||
"js-storage": "^1.1.0",
|
||||
"jsdom": "~11.11.0",
|
||||
"jsdom": "^11.11.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.20",
|
||||
"memory-cache": "^0.2.0",
|
||||
@@ -99,11 +113,13 @@
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"moment-timezone": "^0.5.31",
|
||||
"moment-timezone-data-webpack-plugin": "^1.3.0",
|
||||
"mongo-url-parser": "^1.0.1",
|
||||
"mongodb": "^3.6.0",
|
||||
"mongomock": "^0.1.2",
|
||||
"node-cache": "^4.2.1",
|
||||
"parse-duration": "^0.1.3",
|
||||
"pem": "^1.14.4",
|
||||
"process": "^0.11.10",
|
||||
"pushover-notifications": "^1.2.2",
|
||||
"random-token": "0.0.8",
|
||||
"request": "^2.88.2",
|
||||
@@ -112,33 +128,35 @@
|
||||
"shiro-trie": "^0.4.9",
|
||||
"simple-statistics": "^0.7.0",
|
||||
"socket.io": "~2.1.1",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"swagger-ui-dist": "^3.32.1",
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"terser": "^3.17.0",
|
||||
"traverse": "^0.6.6",
|
||||
"uuid": "^3.4.0",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"webpack": "^5.20.2",
|
||||
"webpack-cli": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/tough-cookie": "^4.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"benv": "^3.3.0",
|
||||
"codacy-coverage": "^3.4.0",
|
||||
"csv-parse": "^4.12.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-loader": "^2.2.1",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"eslint-webpack-plugin": "^2.4.3",
|
||||
"mocha": "^8.1.1",
|
||||
"nodemon": "^1.19.4",
|
||||
"nyc": "^14.1.1",
|
||||
"should": "^13.2.3",
|
||||
"supertest": "^3.4.2",
|
||||
"terser-webpack-plugin": "^1.4.5",
|
||||
"webpack-bundle-analyzer": "^3.8.0",
|
||||
"webpack-dev-middleware": "^3.7.2",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"webpack-dev-middleware": "^4.1.0",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"browserslist": "> 0.25%, not dead"
|
||||
"browserslist": "> 0.25%, not dead, ios_saf 10"
|
||||
}
|
||||
|
||||
+15
-2
@@ -49,7 +49,7 @@ input[type=number]:invalid {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#treatmentDrawer {
|
||||
#treatmentDrawer, #adminNotifiesDrawer {
|
||||
background-color: #666;
|
||||
border-left: 1px solid #999;
|
||||
box-shadow: inset 4px 4px 5px 0 rgba(50, 50, 50, 0.75);
|
||||
@@ -66,6 +66,19 @@ input[type=number]:invalid {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#adminNotifyContent {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.adminNotifyMessage {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.adminNotifyMessageAdditionalInfo {
|
||||
margin-left: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#treatmentDrawer input {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -214,7 +227,7 @@ h1, legend,
|
||||
padding: 0 15px 0 40px;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
background: url(/images/logo2.png) no-repeat 3px center #333;
|
||||
background: url("../images/logo2.png") no-repeat 3px center #333;
|
||||
border-bottom: 1px solid #999;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
+5
-4
@@ -9,15 +9,15 @@
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Icon font for additional plugin icons.
|
||||
Please read assets/fonts/README.md about update process
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'pluginicons';
|
||||
/* Plugin Icons font files content (from WOFF and SVG icon files, base64 encoded) */
|
||||
src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAWAAAsAAAAABTQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgDxIE8mNtYXAAAAFoAAAAVAAAAFQXVdKJZ2FzcAAAAbwAAAAIAAAACAAAABBnbHlmAAABxAAAAUgAAAFIFA4eR2hlYWQAAAMMAAAANgAAADYXVLrVaGhlYQAAA0QAAAAkAAAAJAdQA8ZobXR4AAADaAAAABQAAAAUCY4AAGxvY2EAAAN8AAAADAAAAAwAKAC4bWF4cAAAA4gAAAAgAAAAIAAJAFxuYW1lAAADqAAAAbYAAAG2DBt7mXBvc3QAAAVgAAAAIAAAACAAAwAAAAMCxwGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA6QEDwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADgAAAAKAAgAAgACAAEAIOkB//3//wAAAAAAIOkB//3//wAB/+MXAwADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAwAA/8ADjgPAABsAOgBZAAABIgcOAQcGFRQXHgEXFjMyNz4BNzY1NCcuAScmARUUFx4BFxYzMjc+ATc2PQEUBw4BBwYjIicuAScmNREVFBceARcWMzI3PgE3Nj0BFAcOAQcGIyInLgEnJjUBx15TU3skJCQke1NTXl5TU3wjJCQjfFNT/dskJHtTU15eU1N8IyQkI3xTU15eU1N7JCQkJHtTU15eU1N8IyQkI3xTU15eU1N7JCQDwBISPikpMC8pKj0SEhISPSopLzApKT4SEv6rqy8qKT4SEhISPikqL6svKik+EhISEj4pKi/+46owKSk+EhISEj4pKTCqLykqPhESEhE+KikvAAAAAAEAAAABAABgRbaTXw889QALBAAAAAAA2lO7LAAAAADaU7ssAAD/wAOOA8AAAAAIAAIAAAAAAAAAAQAAA8D/wAAABAAAAAAAA44AAQAAAAAAAAAAAAAAAAAAAAUEAAAAAAAAAAAAAAACAAAAA44AAAAAAAAACgAUAB4ApAABAAAABQBaAAMAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEACwAAAAEAAAAAAAIABwCEAAEAAAAAAAMACwBCAAEAAAAAAAQACwCZAAEAAAAAAAUACwAhAAEAAAAAAAYACwBjAAEAAAAAAAoAGgC6AAMAAQQJAAEAFgALAAMAAQQJAAIADgCLAAMAAQQJAAMAFgBNAAMAAQQJAAQAFgCkAAMAAQQJAAUAFgAsAAMAAQQJAAYAFgBuAAMAAQQJAAoANADUcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzVmVyc2lvbiAxLjAAVgBlAHIAcwBpAG8AbgAgADEALgAwcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzcGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzUmVndWxhcgBSAGUAZwB1AGwAYQBycGx1Z2luaWNvbnMAcABsAHUAZwBpAG4AaQBjAG8AbgBzRm9udCBnZW5lcmF0ZWQgYnkgSWNvTW9vbi4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('woff'),
|
||||
url(data:application/font-svg;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIiA+DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+DQo8bWV0YWRhdGE+R2VuZXJhdGVkIGJ5IEljb01vb248L21ldGFkYXRhPg0KPGRlZnM+DQo8Zm9udCBpZD0icGx1Z2luaWNvbnMiIGhvcml6LWFkdi14PSIxMDI0Ij4NCjxmb250LWZhY2UgdW5pdHMtcGVyLWVtPSIxMDI0IiBhc2NlbnQ9Ijk2MCIgZGVzY2VudD0iLTY0IiAvPg0KPG1pc3NpbmctZ2x5cGggaG9yaXotYWR2LXg9IjEwMjQiIC8+DQo8Z2x5cGggdW5pY29kZT0iJiN4MjA7IiBob3Jpei1hZHYteD0iNTEyIiBkPSIiIC8+DQo8Z2x5cGggdW5pY29kZT0iJiN4ZTkwMTsiIGdseXBoLW5hbWU9ImRhdGFiYXNlIiBob3Jpei1hZHYteD0iOTEwIiBkPSJNNDU1LjExMSA5NjBjLTI1MS40NDkgMC00NTUuMTExLTEwMS44MzEtNDU1LjExMS0yMjcuNTU2czIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTYgNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1Ni0yMDMuNjYyIDIyNy41NTYtNDU1LjExMSAyMjcuNTU2ek0wIDYxOC42Njd2LTE3MC42NjdjMC0xMjUuNzI0IDIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTZzNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1NnYxNzAuNjY3YzAtMTI1LjcyNC0yMDMuNjYyLTIyNy41NTYtNDU1LjExMS0yMjcuNTU2cy00NTUuMTExIDEwMS44MzEtNDU1LjExMSAyMjcuNTU2ek0wIDMzNC4yMjJ2LTE3MC42NjdjMC0xMjUuNzI0IDIwMy42NjItMjI3LjU1NiA0NTUuMTExLTIyNy41NTZzNDU1LjExMSAxMDEuODMxIDQ1NS4xMTEgMjI3LjU1NnYxNzAuNjY3YzAtMTI1LjcyNC0yMDMuNjYyLTIyNy41NTYtNDU1LjExMS0yMjcuNTU2cy00NTUuMTExIDEwMS44MzEtNDU1LjExMSAyMjcuNTU2eiIgLz4NCjwvZm9udD48L2RlZnM+PC9zdmc+) format('svg');
|
||||
src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAcEAAsAAAAABrgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgDxIFc2NtYXAAAAFoAAAAXAAAAFzpVumzZ2FzcAAAAcQAAAAIAAAACAAAABBnbHlmAAABzAAAAuwAAALs3l4nFmhlYWQAAAS4AAAANgAAADYbA9uPaGhlYQAABPAAAAAkAAAAJAfCA8dobXR4AAAFFAAAABgAAAAYDY4AAGxvY2EAAAUsAAAADgAAAA4BngC4bWF4cAAABTwAAAAgAAAAIAAMAJRuYW1lAAAFXAAAAYYAAAGGmUoJ+3Bvc3QAAAbkAAAAIAAAACAAAwAAAAMDLwGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA6RoDwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEAEAAAAAMAAgAAgAEAAEAIOkB6Rr//f//AAAAAAAg6QHpGv/9//8AAf/jFwMW6wADAAEAAAAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAADAAD/wAOOA8AAGwA6AFkAAAEiBw4BBwYVFBceARcWMzI3PgE3NjU0Jy4BJyYBFRQXHgEXFjMyNz4BNzY9ARQHDgEHBiMiJy4BJyY1ERUUFx4BFxYzMjc+ATc2PQEUBw4BBwYjIicuAScmNQHHXlNTeyQkJCR7U1NeXlNTfCMkJCN8U1P92yQke1NTXl5TU3wjJCQjfFNTXl5TU3skJCQke1NTXl5TU3wjJCQjfFNTXl5TU3skJAPAEhI+KSkwLykqPRISEhI9KikvMCkpPhIS/qurLyopPhISEhI+KSovqy8qKT4SEhISPikqL/7jqjApKT4SEhISPikpMKovKSo+ERISET4qKS8AAAAABQAAAAIEAAOAACoATgBjAG0AkQAAATQnLgEnJic4ATEjMAcOAQcGBw4BFRQWFxYXHgEXFjEzMDQxMjc+ATc2NQMiJicuAScuATU0Njc+ATc+ATMyFhceARceARUUBgcOAQcOAQE0NjcOASMqATEHFRcwMjMyFhcuARcnEx4BPwE+AScBIiYnLgEnLgE1NDY3PgE3PgEzMhYXHgEXHgEVFAYHDgEHDgEEAAoLIxgYG1MiI35XWGkGCAgGaVhXfiMiUxsYGCMLCp8HDgQJEggSEhISCBIJBA4HBw4ECRIIERMTEQgSCQQO/ZQFBiRCJjMRNzcRMyZCJAYFdIBSAxYMdgwJBwF2AwUCAwcDBwcHBwMHAwIFAwMFAQQHAwcHBwcDBwQBBQITS0JDYx0cARgYQSMiFiJRLi9RIhUjIkIYGAEdHWNCQkz+ygsECyAVLndCQncuFCEKBQsLBQohFC53QkJ3LhUgCwQLATYnSyMFBV9YXwUFI0uuGP6/DQsFMAQXDAFCBQEEDQgRLhoZLhIIDAQCBAQCBAwIEi4ZGi4RCA0EAQUAAQAAAAAAAPoSCcNfDzz1AAsEAAAAAADb8suJAAAAANvyy4kAAP/ABAADwAAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAAEAAABAAAAAAAAAAAAAAAAAAAABgQAAAAAAAAAAAAAAAIAAAADjgAABAAAAAAAAAAACgAUAB4ApAF2AAAAAQAAAAYAkgAFAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGljb21vb24AaQBjAG8AbQBvAG8AblZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGljb21vb24AaQBjAG8AbQBvAG8Abmljb21vb24AaQBjAG8AbQBvAG8AblJlZ3VsYXIAUgBlAGcAdQBsAGEAcmljb21vb24AaQBjAG8AbQBvAG8AbkZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('woff'),
|
||||
url(data:application/font-svg;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiID4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8bWV0YWRhdGE+R2VuZXJhdGVkIGJ5IEljb01vb248L21ldGFkYXRhPgo8ZGVmcz4KPGZvbnQgaWQ9Imljb21vb24iIGhvcml6LWFkdi14PSIxMDI0Ij4KPGZvbnQtZmFjZSB1bml0cy1wZXItZW09IjEwMjQiIGFzY2VudD0iOTYwIiBkZXNjZW50PSItNjQiIC8+CjxtaXNzaW5nLWdseXBoIGhvcml6LWFkdi14PSIxMDI0IiAvPgo8Z2x5cGggdW5pY29kZT0iJiN4MjA7IiBob3Jpei1hZHYteD0iNTEyIiBkPSIiIC8+CjxnbHlwaCB1bmljb2RlPSImI3hlOTAxOyIgZ2x5cGgtbmFtZT0iZGF0YWJhc2UiIGhvcml6LWFkdi14PSI5MTAiIGQ9Ik00NTUuMTExIDk2MGMtMjUxLjQ0OSAwLTQ1NS4xMTEtMTAxLjgzMS00NTUuMTExLTIyNy41NTZzMjAzLjY2Mi0yMjcuNTU2IDQ1NS4xMTEtMjI3LjU1NiA0NTUuMTExIDEwMS44MzEgNDU1LjExMSAyMjcuNTU2LTIwMy42NjIgMjI3LjU1Ni00NTUuMTExIDIyNy41NTZ6TTAgNjE4LjY2N3YtMTcwLjY2N2MwLTEyNS43MjQgMjAzLjY2Mi0yMjcuNTU2IDQ1NS4xMTEtMjI3LjU1NnM0NTUuMTExIDEwMS44MzEgNDU1LjExMSAyMjcuNTU2djE3MC42NjdjMC0xMjUuNzI0LTIwMy42NjItMjI3LjU1Ni00NTUuMTExLTIyNy41NTZzLTQ1NS4xMTEgMTAxLjgzMS00NTUuMTExIDIyNy41NTZ6TTAgMzM0LjIyMnYtMTcwLjY2N2MwLTEyNS43MjQgMjAzLjY2Mi0yMjcuNTU2IDQ1NS4xMTEtMjI3LjU1NnM0NTUuMTExIDEwMS44MzEgNDU1LjExMSAyMjcuNTU2djE3MC42NjdjMC0xMjUuNzI0LTIwMy42NjItMjI3LjU1Ni00NTUuMTExLTIyNy41NTZzLTQ1NS4xMTEgMTAxLjgzMS00NTUuMTExIDIyNy41NTZ6IiAvPgo8Z2x5cGggdW5pY29kZT0iJiN4ZTkxYTsiIGdseXBoLW5hbWU9Im5vdGlmaWVzIiBkPSJNMTAyNCA1MzAuNzQ0YzAgMjAwLjkyNi01OC43OTIgMzYzLjkzOC0xMzEuNDgyIDM2NS4yMjYgMC4yOTIgMC4wMDYgMC41NzggMC4wMzAgMC44NzIgMC4wMzBoLTgyLjk0MmMwIDAtMTk0LjgtMTQ2LjMzNi00NzUuMjMtMjAzLjc1NC04LjU2LTQ1LjI5Mi0xNC4wMzAtOTkuMjc0LTE0LjAzMC0xNjEuNTAyczUuNDY2LTExNi4yMDggMTQuMDMwLTE2MS41YzI4MC40MjgtNTcuNDE4IDQ3NS4yMy0yMDMuNzU2IDQ3NS4yMy0yMDMuNzU2aDgyLjk0MmMtMC4yOTIgMC0wLjU3OCAwLjAyNC0wLjg3MiAwLjAzMiA3Mi42OTYgMS4yODggMTMxLjQ4MiAxNjQuMjk4IDEzMS40ODIgMzY1LjIyNHpNODY0LjgyNCAyMjAuNzQ4Yy05LjM4MiAwLTE5LjUzMiA5Ljc0Mi0yNC43NDYgMTUuNTQ4LTEyLjYzIDE0LjA2NC0yNC43OTIgMzUuOTYtMzUuMTg4IDYzLjMyOC0yMy4yNTYgNjEuMjMyLTM2LjA2NiAxNDMuMzEtMzYuMDY2IDIzMS4xMjQgMCA4Ny44MSAxMi44MSAxNjkuODkgMzYuMDY2IDIzMS4xMjIgMTAuMzk0IDI3LjM2OCAyMi41NjIgNDkuMjY2IDM1LjE4OCA2My4zMjggNS4yMTQgNS44MTIgMTUuMzY0IDE1LjU1MiAyNC43NDYgMTUuNTUyIDkuMzggMCAxOS41MzYtOS43NDQgMjQuNzQ0LTE1LjU1MiAxMi42MzQtMTQuMDY0IDI0Ljc5Ni0zNS45NTggMzUuMTg4LTYzLjMyOCAyMy4yNTgtNjEuMjMgMzYuMDY4LTE0My4zMTIgMzYuMDY4LTIzMS4xMjIgMC04Ny44MDQtMTIuODEtMTY5Ljg4OC0zNi4wNjgtMjMxLjEyNC0xMC4zOS0yNy4zNjgtMjIuNTYyLTQ5LjI2NC0zNS4xODgtNjMuMzI4LTUuMjA4LTUuODA2LTE1LjM2LTE1LjU0OC0yNC43NDQtMTUuNTQ4ek0yNTEuODEyIDUzMC43NDRjMCA1MS45NSAzLjgxIDEwMi40MyAxMS4wNTIgMTQ5LjA5NC00Ny4zNzItNi41NTQtODguOTQyLTEwLjMyNC0xNDAuMzQtMTAuMzI0LTY3LjA1OCAwLTY3LjA1OCAwLTY3LjA1OCAwbC01NS40NjYtOTQuNjg2di04OC4xN2w1NS40Ni05NC42ODZjMCAwIDAgMCA2Ny4wNjAgMCA1MS4zOTggMCA5Mi45NjgtMy43NzQgMTQwLjM0LTEwLjMyNC03LjIzNiA0Ni42NjQtMTEuMDQ4IDk3LjE0Ni0xMS4wNDggMTQ5LjA5NnpNMzY4LjE1IDMxNy44MjhsLTEyNy45OTggMjQuNTEgODEuODQyLTMyMS41NDRjNC4yMzYtMTYuNjM0IDIwLjc0NC0yNS4wMzggMzYuNjg2LTE4LjY1NGwxMTguNTU2IDQ3LjQ1MmMxNS45NDQgNi4zNzYgMjIuMzI4IDIzLjk2NCAxNC4xOTYgMzkuMDg0bC0xMjMuMjgyIDIyOS4xNTJ6TTg2NC44MjQgNDExLjI3Yy0zLjYxOCAwLTcuNTI4IDMuNzU0LTkuNTM4IDUuOTkyLTQuODcgNS40Mi05LjU1NiAxMy44Ni0xMy41NjIgMjQuNDA4LTguOTYyIDIzLjYtMTMuOSA1NS4yMzQtMTMuOSA4OS4wNzhzNC45MzggNjUuNDc4IDEzLjkgODkuMDc4YzQuMDA2IDEwLjU0OCA4LjY5NiAxOC45ODggMTMuNTYyIDI0LjQwOCAyLjAxMCAyLjI0IDUuOTIgNS45OTQgOS41MzggNS45OTQgMy42MTYgMCA3LjUzLTMuNzU2IDkuNTM4LTUuOTk0IDQuODctNS40MiA5LjU1Ni0xMy44NTggMTMuNTYtMjQuNDA4IDguOTY0LTIzLjU5OCAxMy45MDItNTUuMjM0IDEzLjkwMi04OS4wNzggMC0zMy44NDItNC45MzgtNjUuNDc4LTEzLjkwMi04OS4wNzgtNC4wMDQtMTAuNTQ4LTguNjk2LTE4Ljk4OC0xMy41Ni0yNC40MDgtMi4wMDgtMi4yMzgtNS45Mi01Ljk5Mi05LjUzOC01Ljk5MnoiIC8+CjwvZm9udD48L2RlZnM+PC9zdmc+) format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -61,7 +61,7 @@
|
||||
[class^="plugicon-"]:before, [class*=" plugicon-"]:before {
|
||||
font-family: "pluginicons";
|
||||
}
|
||||
|
||||
|
||||
.icon-volume:before { content: '\e800'; }
|
||||
.icon-plus:before { content: '\e801'; }
|
||||
.icon-edit:before { content: '\e802'; }
|
||||
@@ -85,6 +85,7 @@
|
||||
|
||||
/* Plugin Icons id-s (copy from generated icon style.css) */
|
||||
.plugicon-database:before { content: "\e901"; }
|
||||
.plugicon-notifies:before { content: "\e91a"; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log('Application got ready event');
|
||||
window.Nightscout.foodclient();
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log('Application got ready event');
|
||||
window.Nightscout.profileclient();
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
/* Modernizr 2.8.3 (Custom Build) | MIT & BSD
|
||||
* Build: http://modernizr.com/download/#-touch-teststyles-prefixes
|
||||
*/
|
||||
;window.Modernizr=function(a,b,c){function v(a){i.cssText=a}function w(a,b){return v(l.join(a+";")+(b||""))}function x(a,b){return typeof a===b}function y(a,b){return!!~(""+a).indexOf(b)}function z(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:x(f,"function")?f.bind(d||b):f}return!1}var d="2.8.3",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l=" -webkit- -moz- -o- -ms- ".split(" "),m={},n={},o={},p=[],q=p.slice,r,s=function(a,c,d,e){var h,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:g+(d+1),l.appendChild(j);return h=["­",'<style id="s',g,'">',a,"</style>"].join(""),l.id=g,(m?l:n).innerHTML+=h,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=f.style.overflow,f.style.overflow="hidden",f.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),f.style.overflow=k),!!i},t={}.hasOwnProperty,u;!x(t,"undefined")&&!x(t.call,"undefined")?u=function(a,b){return t.call(a,b)}:u=function(a,b){return b in a&&x(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:s(["@media (",l.join("touch-enabled),("),g,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c};for(var A in m)u(m,A)&&(r=A.toLowerCase(),e[r]=m[A](),p.push((e[r]?"":"no-")+r));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)u(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},v(""),h=j=null,e._version=d,e._prefixes=l,e.testStyles=s,e}(this,this.document);
|
||||
@@ -1,671 +0,0 @@
|
||||
/**
|
||||
* CONTENTS
|
||||
*
|
||||
* #Introduction........Naming conventions used throughout the code.
|
||||
*
|
||||
* #SETTINGS
|
||||
* Variables............Globally-available variables and config.
|
||||
*
|
||||
* #TOOLS
|
||||
* Mixins...............Useful mixins.
|
||||
*
|
||||
* #GENERIC
|
||||
* Demo styles..........Styles for demo only (consider removing these).
|
||||
*
|
||||
* #BASE
|
||||
* Raw styles...........The very basic component wrapper.
|
||||
* Modifiers............The basic styles dependant on component placement.
|
||||
* Debuggers............The basic styles dependant on component placement.
|
||||
*
|
||||
* #BUTTONS
|
||||
* Base..................Wrapping and constraining every button.
|
||||
* Modifiers.............Styles that depends on state and settings.
|
||||
* Animations............Main animations of the component.
|
||||
* Debuggers.............Styles for development.
|
||||
*
|
||||
* #LABELS
|
||||
* Base..................Wrapping and constraining every label.
|
||||
* Modifiers.............Styles that depends on state and settings.
|
||||
* Debuggers.............Styles for development.
|
||||
*
|
||||
* #DEVELOPMENT
|
||||
* In development........These styles are in development and not yet finalised
|
||||
* Debuggers.............Helper styles and flags for development.
|
||||
*/
|
||||
/*------------------------------------*\
|
||||
#Introduction
|
||||
\*------------------------------------*/
|
||||
/**
|
||||
* The code AND the comments use naming conventions to refer to each part of
|
||||
* the UI put in place by this component. If you see that somewhere they are
|
||||
* not followed please consider a Pull Request. The naming conventions are:
|
||||
*
|
||||
* "Component" : the widget itself as a whole. This is the last time it will be
|
||||
* called anything different than "component". So, stay away from
|
||||
* "widget", "button" or anything else when referring to the
|
||||
* Component in general.
|
||||
*
|
||||
* "Main Button" : the button that is always in view. Hovering or clicking on it
|
||||
* will reveal the child buttons.
|
||||
*
|
||||
* "Child buttons" : if you've read the previous point you know what they are.
|
||||
* Did you read the previous point? :)
|
||||
*
|
||||
* "Label(s)" : the tooltip that fades in when hovering over a button.
|
||||
|
||||
/*------------------------------------*\
|
||||
#SETTINGS | Variables
|
||||
\*------------------------------------*/
|
||||
/**
|
||||
* These variables are the default styles that serve as fallback and can be
|
||||
* easily customised at compile time.
|
||||
* Consider overriding them in your own style sheets rather than editing them
|
||||
* here. Refer to the docs for more info.
|
||||
*/
|
||||
/* COLORS ----------------------------*/
|
||||
/* EFFECTS ---------------------------*/
|
||||
/* SPEEDS ----------------------------*/
|
||||
/* SIZES -----------------------------*/
|
||||
/* SPACING ---------------------------*/
|
||||
/* OTHER VARIABLES -------------------*/
|
||||
/*------------------------------------*\
|
||||
#BASE | Raw styles
|
||||
\*------------------------------------*/
|
||||
/**
|
||||
* The very core styling of the button.
|
||||
* These styles are shared by every instance of the button.
|
||||
* Styles placed here should NOT care about placement in the screen,
|
||||
* options chosen by the user or state of the button.
|
||||
*/
|
||||
.mfb-component--tl, .mfb-component--tr, .mfb-component--bl, .mfb-component--br {
|
||||
box-sizing: border-box;
|
||||
margin: 25px;
|
||||
position: fixed;
|
||||
white-space: nowrap;
|
||||
z-index: 30;
|
||||
padding-left: 0;
|
||||
list-style: none; }
|
||||
.mfb-component--tl *, .mfb-component--tr *, .mfb-component--bl *, .mfb-component--br *, .mfb-component--tl *:before, .mfb-component--tr *:before, .mfb-component--bl *:before, .mfb-component--br *:before, .mfb-component--tl *:after, .mfb-component--tr *:after, .mfb-component--bl *:after, .mfb-component--br *:after {
|
||||
box-sizing: inherit; }
|
||||
|
||||
/*------------------------------------*\
|
||||
#BASE | Modifiers
|
||||
\*------------------------------------*/
|
||||
/**
|
||||
* These styles depends on the placement of the button.
|
||||
* Styles can be:
|
||||
* 1. Top-left: modified by the " --tl " suffix.
|
||||
* 2. Top-right: modified by the " --tr " suffix.
|
||||
* 3. Bottom-left: modified by the " --bl " suffix.
|
||||
* 4. Bottom-right: modified by the " --br " suffix.
|
||||
*/
|
||||
.mfb-component--tl {
|
||||
left: 0;
|
||||
top: 0; }
|
||||
|
||||
.mfb-component--tr {
|
||||
right: 0;
|
||||
top: 0; }
|
||||
|
||||
.mfb-component--bl {
|
||||
left: 0;
|
||||
bottom: 0; }
|
||||
|
||||
.mfb-component--br {
|
||||
right: 0;
|
||||
bottom: 0; }
|
||||
|
||||
/*------------------------------------*\
|
||||
#BUTTONS | Base
|
||||
\*------------------------------------*/
|
||||
.mfb-component__button--main, .mfb-component__button--child {
|
||||
background-color: #E40A5D;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
-webkit-user-drag: none;
|
||||
color: #f1f1f1; }
|
||||
|
||||
/**
|
||||
* This is the unordered list for the list items that contain
|
||||
* the child buttons.
|
||||
*
|
||||
*/
|
||||
.mfb-component__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
.mfb-component__list > li {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1px;
|
||||
padding: 10px 0;
|
||||
margin: -10px 0; }
|
||||
|
||||
/**
|
||||
* These are the basic styles for all the icons inside the main button
|
||||
*/
|
||||
.mfb-component__icon, .mfb-component__main-icon--active,
|
||||
.mfb-component__main-icon--resting, .mfb-component__child-icon {
|
||||
position: absolute;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
line-height: 56px;
|
||||
width: 100%; }
|
||||
|
||||
.mfb-component__wrap {
|
||||
padding: 25px;
|
||||
margin: -25px; }
|
||||
|
||||
[data-mfb-toggle="hover"]:hover .mfb-component__icon, [data-mfb-toggle="hover"]:hover .mfb-component__main-icon--active,
|
||||
[data-mfb-toggle="hover"]:hover .mfb-component__main-icon--resting, [data-mfb-toggle="hover"]:hover .mfb-component__child-icon,
|
||||
[data-mfb-state="open"] .mfb-component__icon,
|
||||
[data-mfb-state="open"] .mfb-component__main-icon--active,
|
||||
[data-mfb-state="open"] .mfb-component__main-icon--resting,
|
||||
[data-mfb-state="open"] .mfb-component__child-icon {
|
||||
-webkit-transform: scale(1) rotate(0deg);
|
||||
transform: scale(1) rotate(0deg); }
|
||||
|
||||
/*------------------------------------*\
|
||||
#BUTTONS | Modifiers
|
||||
\*------------------------------------*/
|
||||
.mfb-component__button--main {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
z-index: 20; }
|
||||
|
||||
.mfb-component__button--child {
|
||||
height: 56px;
|
||||
width: 56px; }
|
||||
|
||||
.mfb-component__main-icon--active,
|
||||
.mfb-component__main-icon--resting {
|
||||
-webkit-transform: scale(1) rotate(360deg);
|
||||
transform: scale(1) rotate(360deg);
|
||||
-webkit-transition: -webkit-transform 150ms cubic-bezier(0.4, 0, 1, 1);
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 1, 1); }
|
||||
|
||||
.mfb-component__child-icon,
|
||||
.mfb-component__child-icon {
|
||||
line-height: 56px;
|
||||
font-size: 18px; }
|
||||
|
||||
.mfb-component__main-icon--active {
|
||||
opacity: 0; }
|
||||
|
||||
[data-mfb-toggle="hover"]:hover .mfb-component__main-icon,
|
||||
[data-mfb-state="open"] .mfb-component__main-icon {
|
||||
-webkit-transform: scale(1) rotate(0deg);
|
||||
transform: scale(1) rotate(0deg); }
|
||||
[data-mfb-toggle="hover"]:hover .mfb-component__main-icon--resting,
|
||||
[data-mfb-state="open"] .mfb-component__main-icon--resting {
|
||||
opacity: 0;
|
||||
position: absolute !important; }
|
||||
[data-mfb-toggle="hover"]:hover .mfb-component__main-icon--active,
|
||||
[data-mfb-state="open"] .mfb-component__main-icon--active {
|
||||
opacity: 1; }
|
||||
|
||||
/*------------------------------------*\
|
||||
#BUTTONS | Animations
|
||||
\*------------------------------------*/
|
||||
/**
|
||||
* SLIDE IN + FADE
|
||||
* When hovering the main button, the child buttons slide out from beneath
|
||||
* the main button while transitioning from transparent to opaque.
|
||||
*
|
||||
*/
|
||||
.mfb-component--tl.mfb-slidein .mfb-component__list li,
|
||||
.mfb-component--tr.mfb-slidein .mfb-component__list li {
|
||||
opacity: 0;
|
||||
transition: all 0.5s; }
|
||||
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li, .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li,
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li,
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li {
|
||||
opacity: 1; }
|
||||
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(70px);
|
||||
transform: translateY(70px); }
|
||||
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(140px);
|
||||
transform: translateY(140px); }
|
||||
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(210px);
|
||||
transform: translateY(210px); }
|
||||
.mfb-component--tl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--tl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(280px);
|
||||
transform: translateY(280px); }
|
||||
|
||||
.mfb-component--bl.mfb-slidein .mfb-component__list li,
|
||||
.mfb-component--br.mfb-slidein .mfb-component__list li {
|
||||
opacity: 0;
|
||||
transition: all 0.5s; }
|
||||
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li, .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li,
|
||||
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li,
|
||||
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li {
|
||||
opacity: 1; }
|
||||
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(-70px);
|
||||
transform: translateY(-70px); }
|
||||
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(-140px);
|
||||
transform: translateY(-140px); }
|
||||
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(-210px);
|
||||
transform: translateY(-210px); }
|
||||
.mfb-component--bl.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--bl.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-slidein[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-slidein[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(-280px);
|
||||
transform: translateY(-280px); }
|
||||
|
||||
/**
|
||||
* SLIDE IN SPRING
|
||||
* Same as slide-in but with a springy animation.
|
||||
*
|
||||
*/
|
||||
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li,
|
||||
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li {
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); }
|
||||
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li:nth-child(1) {
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li:nth-child(2) {
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li:nth-child(3) {
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--tl.mfb-slidein-spring .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-slidein-spring .mfb-component__list li:nth-child(4) {
|
||||
transition-delay: 0.2s; }
|
||||
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li, .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li,
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li,
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li {
|
||||
opacity: 1; }
|
||||
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
|
||||
transition-delay: 0.05s;
|
||||
-webkit-transform: translateY(70px);
|
||||
transform: translateY(70px); }
|
||||
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
|
||||
transition-delay: 0.1s;
|
||||
-webkit-transform: translateY(140px);
|
||||
transform: translateY(140px); }
|
||||
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
|
||||
transition-delay: 0.15s;
|
||||
-webkit-transform: translateY(210px);
|
||||
transform: translateY(210px); }
|
||||
.mfb-component--tl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--tl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
|
||||
transition-delay: 0.2s;
|
||||
-webkit-transform: translateY(280px);
|
||||
transform: translateY(280px); }
|
||||
|
||||
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li,
|
||||
.mfb-component--br.mfb-slidein-spring .mfb-component__list li {
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); }
|
||||
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-slidein-spring .mfb-component__list li:nth-child(1) {
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-slidein-spring .mfb-component__list li:nth-child(2) {
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-slidein-spring .mfb-component__list li:nth-child(3) {
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--bl.mfb-slidein-spring .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-slidein-spring .mfb-component__list li:nth-child(4) {
|
||||
transition-delay: 0.2s; }
|
||||
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li, .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li,
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li,
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li {
|
||||
opacity: 1; }
|
||||
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
|
||||
transition-delay: 0.05s;
|
||||
-webkit-transform: translateY(-70px);
|
||||
transform: translateY(-70px); }
|
||||
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
|
||||
transition-delay: 0.1s;
|
||||
-webkit-transform: translateY(-140px);
|
||||
transform: translateY(-140px); }
|
||||
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
|
||||
transition-delay: 0.15s;
|
||||
-webkit-transform: translateY(-210px);
|
||||
transform: translateY(-210px); }
|
||||
.mfb-component--bl.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--bl.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-slidein-spring[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
|
||||
transition-delay: 0.2s;
|
||||
-webkit-transform: translateY(-280px);
|
||||
transform: translateY(-280px); }
|
||||
|
||||
/**
|
||||
* ZOOM-IN
|
||||
* When hovering the main button, the child buttons grow
|
||||
* from zero to normal size.
|
||||
*
|
||||
*/
|
||||
.mfb-component--tl.mfb-zoomin .mfb-component__list li,
|
||||
.mfb-component--tr.mfb-zoomin .mfb-component__list li {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0); }
|
||||
.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(70px) scale(0);
|
||||
transform: translateY(70px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(140px) scale(0);
|
||||
transform: translateY(140px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(210px) scale(0);
|
||||
transform: translateY(210px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--tl.mfb-zoomin .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-zoomin .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(280px) scale(0);
|
||||
transform: translateY(280px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0s; }
|
||||
.mfb-component--tl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--tl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(70px) scale(1);
|
||||
transform: translateY(70px) scale(1);
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--tl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--tl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(140px) scale(1);
|
||||
transform: translateY(140px) scale(1);
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--tl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--tl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(210px) scale(1);
|
||||
transform: translateY(210px) scale(1);
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--tl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--tl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(280px) scale(1);
|
||||
transform: translateY(280px) scale(1);
|
||||
transition-delay: 0.2s; }
|
||||
|
||||
.mfb-component--bl.mfb-zoomin .mfb-component__list li,
|
||||
.mfb-component--br.mfb-zoomin .mfb-component__list li {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0); }
|
||||
.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(-70px) scale(0);
|
||||
transform: translateY(-70px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(-140px) scale(0);
|
||||
transform: translateY(-140px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(-210px) scale(0);
|
||||
transform: translateY(-210px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--bl.mfb-zoomin .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-zoomin .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(-280px) scale(0);
|
||||
transform: translateY(-280px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0s; }
|
||||
.mfb-component--bl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--bl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(-70px) scale(1);
|
||||
transform: translateY(-70px) scale(1);
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--bl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--bl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(-140px) scale(1);
|
||||
transform: translateY(-140px) scale(1);
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--bl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--bl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(-210px) scale(1);
|
||||
transform: translateY(-210px) scale(1);
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--bl.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--bl.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-zoomin[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-zoomin[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(-280px) scale(1);
|
||||
transform: translateY(-280px) scale(1);
|
||||
transition-delay: 0.2s; }
|
||||
|
||||
/**
|
||||
* FOUNTAIN
|
||||
* When hovering the main button the child buttons
|
||||
* jump into view from outside the viewport
|
||||
*/
|
||||
.mfb-component--tl.mfb-fountain .mfb-component__list li,
|
||||
.mfb-component--tr.mfb-fountain .mfb-component__list li {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0); }
|
||||
.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(-70px) scale(0);
|
||||
transform: translateY(-70px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(-140px) scale(0);
|
||||
transform: translateY(-140px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(-210px) scale(0);
|
||||
transform: translateY(-210px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--tl.mfb-fountain .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-fountain .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(-280px) scale(0);
|
||||
transform: translateY(-280px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0s; }
|
||||
.mfb-component--tl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--tl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--tr.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(70px) scale(1);
|
||||
transform: translateY(70px) scale(1);
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--tl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--tl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--tr.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(140px) scale(1);
|
||||
transform: translateY(140px) scale(1);
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--tl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--tl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--tr.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(210px) scale(1);
|
||||
transform: translateY(210px) scale(1);
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--tl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--tl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--tr.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(280px) scale(1);
|
||||
transform: translateY(280px) scale(1);
|
||||
transition-delay: 0.2s; }
|
||||
|
||||
.mfb-component--bl.mfb-fountain .mfb-component__list li,
|
||||
.mfb-component--br.mfb-fountain .mfb-component__list li {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0); }
|
||||
.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(70px) scale(0);
|
||||
transform: translateY(70px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(140px) scale(0);
|
||||
transform: translateY(140px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(210px) scale(0);
|
||||
transform: translateY(210px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--bl.mfb-fountain .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-fountain .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(280px) scale(0);
|
||||
transform: translateY(280px) scale(0);
|
||||
transition: all 0.5s;
|
||||
transition-delay: 0s; }
|
||||
.mfb-component--bl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1), .mfb-component--bl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(1),
|
||||
.mfb-component--br.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(1) {
|
||||
-webkit-transform: translateY(-70px) scale(1);
|
||||
transform: translateY(-70px) scale(1);
|
||||
transition-delay: 0.05s; }
|
||||
.mfb-component--bl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2), .mfb-component--bl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(2),
|
||||
.mfb-component--br.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(2) {
|
||||
-webkit-transform: translateY(-140px) scale(1);
|
||||
transform: translateY(-140px) scale(1);
|
||||
transition-delay: 0.1s; }
|
||||
.mfb-component--bl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3), .mfb-component--bl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(3),
|
||||
.mfb-component--br.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(3) {
|
||||
-webkit-transform: translateY(-210px) scale(1);
|
||||
transform: translateY(-210px) scale(1);
|
||||
transition-delay: 0.15s; }
|
||||
.mfb-component--bl.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4), .mfb-component--bl.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-fountain[data-mfb-toggle="hover"]:hover .mfb-component__list li:nth-child(4),
|
||||
.mfb-component--br.mfb-fountain[data-mfb-state="open"] .mfb-component__list li:nth-child(4) {
|
||||
-webkit-transform: translateY(-280px) scale(1);
|
||||
transform: translateY(-280px) scale(1);
|
||||
transition-delay: 0.2s; }
|
||||
|
||||
/*------------------------------------*\
|
||||
#LABELS | base
|
||||
\*------------------------------------*/
|
||||
/**
|
||||
* These are the labels associated to each button,
|
||||
* exposed only when hovering the related button.
|
||||
* They are called labels but are in fact data-attributes of
|
||||
* each button (an anchor tag).
|
||||
*/
|
||||
[data-mfb-label]:after {
|
||||
content: attr(data-mfb-label);
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
pointer-events: none;
|
||||
line-height: normal;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -11px;
|
||||
transition: all 0.5s; }
|
||||
|
||||
[data-mfb-toggle="hover"] [data-mfb-label]:hover:after,
|
||||
[data-mfb-state="open"] [data-mfb-label]:after {
|
||||
content: attr(data-mfb-label);
|
||||
opacity: 1;
|
||||
transition: all 0.3s; }
|
||||
|
||||
/*------------------------------------*\
|
||||
#LABELS | Modifiers
|
||||
\*------------------------------------*/
|
||||
.mfb-component--br [data-mfb-label]:after, .mfb-component--tr [data-mfb-label]:after {
|
||||
content: attr(data-mfb-label);
|
||||
right: 70px; }
|
||||
|
||||
.mfb-component--br .mfb-component__list [data-mfb-label]:after, .mfb-component--tr .mfb-component__list [data-mfb-label]:after {
|
||||
content: attr(data-mfb-label);
|
||||
right: 70px; }
|
||||
|
||||
.mfb-component--tl [data-mfb-label]:after, .mfb-component--bl [data-mfb-label]:after {
|
||||
content: attr(data-mfb-label);
|
||||
left: 70px; }
|
||||
|
||||
.mfb-component--tl .mfb-component__list [data-mfb-label]:after, .mfb-component--bl .mfb-component__list [data-mfb-label]:after {
|
||||
content: attr(data-mfb-label);
|
||||
left: 70px; }
|
||||
|
||||
/*------------------------------------*\
|
||||
#DEVELOPMENT | In development
|
||||
\*------------------------------------*/
|
||||
/**
|
||||
* This part is where unfinished code should stay.
|
||||
* When a feature is ready(sh) move these styles to their proper place.
|
||||
*/
|
||||
/*------------------------------------*\
|
||||
#DEVELOPMENT | Debuggers
|
||||
\*------------------------------------*/
|
||||
/**
|
||||
* These are mainly helpers for development. They do not have to end up
|
||||
* in production but it's handy to keep them when developing.
|
||||
*/
|
||||
/**
|
||||
* Apply this class to the html tag when developing the slide-in button
|
||||
*/
|
||||
|
||||
/*# sourceMappingURL=mfb.css.map */
|
||||
File diff suppressed because one or more lines are too long
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* Material floating button
|
||||
* By: Nobita
|
||||
* Repo and docs: https://github.com/nobitagit/material-floating-button
|
||||
*
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
// build script hook - don't remove
|
||||
;(function ( window, document, undefined ) {
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Some defaults
|
||||
*/
|
||||
var clickOpt = 'click',
|
||||
hoverOpt = 'hover',
|
||||
toggleMethod = 'data-mfb-toggle',
|
||||
menuState = 'data-mfb-state',
|
||||
isOpen = 'open',
|
||||
isClosed = 'closed',
|
||||
mainButtonClass = 'mfb-component__button--main';
|
||||
|
||||
/**
|
||||
* Internal references
|
||||
*/
|
||||
var elemsToClick,
|
||||
elemsToHover,
|
||||
mainButton,
|
||||
target,
|
||||
currentState;
|
||||
|
||||
/**
|
||||
* For every menu we need to get the main button and attach the appropriate evt.
|
||||
*/
|
||||
function attachEvt( elems, evt ){
|
||||
for( var i = 0, len = elems.length; i < len; i++ ){
|
||||
mainButton = elems[i].querySelector('.' + mainButtonClass);
|
||||
mainButton.addEventListener( evt , toggleButton, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the hover option, set a click toggle and a default,
|
||||
* initial state of 'closed' to menu that's been targeted.
|
||||
*/
|
||||
function replaceAttrs( elems ){
|
||||
for( var i = 0, len = elems.length; i < len; i++ ){
|
||||
elems[i].setAttribute( toggleMethod, clickOpt );
|
||||
elems[i].setAttribute( menuState, isClosed );
|
||||
}
|
||||
}
|
||||
|
||||
function getElemsByToggleMethod( selector ){
|
||||
return document.querySelectorAll('[' + toggleMethod + '="' + selector + '"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* The open/close action is performed by toggling an attribute
|
||||
* on the menu main element.
|
||||
*
|
||||
* First, check if the target is the menu itself. If it's a child
|
||||
* keep walking up the tree until we found the main element
|
||||
* where we can toggle the state.
|
||||
*/
|
||||
function toggleButton( evt ){
|
||||
|
||||
target = evt.target;
|
||||
while ( target && !target.getAttribute( toggleMethod ) ){
|
||||
target = target.parentNode;
|
||||
if(!target) { return; }
|
||||
}
|
||||
|
||||
currentState = target.getAttribute( menuState ) === isOpen ? isClosed : isOpen;
|
||||
|
||||
target.setAttribute(menuState, currentState);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* On touch enabled devices we assume that no hover state is possible.
|
||||
* So, we get the menu with hover action configured and we set it up
|
||||
* in order to make it usable with tap/click.
|
||||
**/
|
||||
if ( window.Modernizr && Modernizr.touch ){
|
||||
elemsToHover = getElemsByToggleMethod( hoverOpt );
|
||||
replaceAttrs( elemsToHover );
|
||||
}
|
||||
|
||||
elemsToClick = getElemsByToggleMethod( clickOpt );
|
||||
|
||||
attachEvt( elemsToClick, 'click' );
|
||||
|
||||
// build script hook - don't remove
|
||||
})( window, document );
|
||||
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
@@ -1 +0,0 @@
|
||||
!function(a,b){"use strict";function c(a,b){for(var c=0,d=a.length;d>c;c++)i=a[c].querySelector("."+r),i.addEventListener(b,f,!1)}function d(a){for(var b=0,c=a.length;c>b;b++)a[b].setAttribute(n,l),a[b].setAttribute(o,q)}function e(a){return b.querySelectorAll("["+n+'="'+a+'"]')}function f(a){for(j=a.target;j&&!j.getAttribute(n);)if(j=j.parentNode,!j)return;k=j.getAttribute(o)===p?q:p,j.setAttribute(o,k)}var g,h,i,j,k,l="click",m="hover",n="data-mfb-toggle",o="data-mfb-state",p="open",q="closed",r="mfb-component__button--main";a.Modernizr&&Modernizr.touch&&(h=e(m),d(h)),g=e(l),c(g,"click")}(window,document);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user