diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000000..ff3059c3f09 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} \ No newline at end of file diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index c2587b247fc..00000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "./static/bower_components" -} diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000000..34e99e5f0fc --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,12 @@ +# Browsers we support +# See https://github.com/browserslist/browserslist for details + +> 0.25% +ios_saf 9.3 +ios_saf 10.3 +ios_saf 13.7 +ios_saf 14.8 +not dead +not and_uc 12.12 +not ie 11 + diff --git a/.deployment b/.deployment index 1e42f16cade..7d94259b916 100644 --- a/.deployment +++ b/.deployment @@ -1,2 +1,2 @@ [config] -command = bash deploy.sh \ No newline at end of file +command = bash bin/azure-deploy.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..e462a2e5ef4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +# Don't include the .git history in the Docker image: +.git + +# Items from .gitignore +bower_components/ +node_modules/ +bundle/bundle.out.js +.idea/ +*.iml +my.env +*.env +static/bower_components/ +.*.sw? +.DS_Store +.vagrant +/iisnode +coverage/ +npm-debug.log diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000000..7710aa21263 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,35 @@ +module.exports = { + 'plugins': [ + 'security' + ], + 'extends': [ + 'eslint:recommended', + 'plugin:security/recommended' + ], + 'parser': 'babel-eslint', + 'env': { + 'browser': true, + 'commonjs': true, + 'es6': true, + 'node': true, + 'mocha': true, + 'jquery': true + }, + 'rules': { + 'security/detect-object-injection' : 0, + 'no-unused-vars': [ + 'error', + { + 'varsIgnorePattern': 'should|expect' + } + ] + }, + 'overrides': [ + { + 'files': ['lib/client/*.js'], + 'rules': { + 'security/detect-object-injection': 0 + } + } + ], + }; \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/--bug-report.md b/.github/ISSUE_TEMPLATE/--bug-report.md new file mode 100644 index 00000000000..417f1eee1f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--bug-report.md @@ -0,0 +1,34 @@ +--- +name: "\U0001F41BBug report" +about: Create a report to help us improve things +label: bug + +--- + +**If you need support for Nightscout, PLEASE DO NOT FILE A TICKET HERE** +For support, please post a question to the "CGM in The Cloud" group in Facebook +(https://www.facebook.com/groups/cgminthecloud) or visit the WeAreNotWaiting Discord at https://discord.gg/zg7CvCQ + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Your setup information** +- What version of Nightscout (e.g. 0.10.3) +- What type of CGM, and how do you get your data there? (e.g. G4 and ShareBridge, or wired receiver, etc.) +- Is your issue specific to a browser (Firefox/Safari/Chrome?) or a device (Android phone, etc.)? + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/--feature-request--.md b/.github/ISSUE_TEMPLATE/--feature-request--.md new file mode 100644 index 00000000000..293efbc9c1e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--feature-request--.md @@ -0,0 +1,21 @@ +--- +name: "\U0001F4A1Feature request\U0001F4A1" +about: Suggest an idea for this project + +--- + +**If you need support for Nightscout, PLEASE DO NOT FILE A TICKET HERE** +For support, please post a question to the "CGM in The Cloud" group in Facebook +(https://www.facebook.com/groups/cgminthecloud) or visit the WeAreNotWaiting Discord at https://discord.gg/zg7CvCQ + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..659a213d692 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Troubleshooting Documentation + url: http://nightscout.github.io/troubleshoot/troublehoot/ + about: Having trouble with Nightscout? Please check our step by step troubleshooting instructions. + - name: Nightscout Community Support in Facebook + url: https://www.facebook.com/groups/cgminthecloud + about: If you're a Nightscout user and have trouble with your site, please post questions here. We don't have the resources to answer support questions posted here as tickets. + - name: Nightscout Community Support in Discord + url: https://discord.gg/zg7CvCQ + about: If you're a Nightscout user and have trouble with your site, please post questions here. We don't have the resources to answer support questions posted here as tickets. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000000..5303890ef87 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,69 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# ******** NOTE ******** + +name: "CodeQL" + +on: + push: + branches: [ dev ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ dev ] + schedule: + - cron: '43 23 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/dev' && github.repository_owner == 'nightscout' + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000000..b3d49dc6332 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,90 @@ +name: CI test and publish Docker image + +on: + push: + branches: + - master + - dev + pull_request: + branches: + - master + - dev + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x, 16.x] + mongodb-version: [4.4, 5.0, 6.0] + + steps: + - name: Git Checkout + uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Start MongoDB ${{ matrix.mongodb-version }} + uses: supercharge/mongodb-github-action@1.3.0 + with: + mongodb-version: ${{ matrix.mongodb-version }} + + - name: Install dependencies + run: npm install + - name: Run Tests + run: npm run-script test-ci + - name: Send Coverage + run: npm run-script coverage + + publish: + name: Publish to Docker Hub + needs: test + runs-on: ubuntu-latest + if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev') && github.repository_owner == 'nightscout' + env: + DOCKER_IMAGE: nightscout/cgm-remote-monitor + PLATFORMS: linux/amd64,linux/arm64 + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASS }} + - name: Clean git Checkout + if: success() + uses: actions/checkout@v3 + - name: Build, tag and push the dev Docker image + if: success() && github.ref == 'refs/heads/dev' + uses: docker/build-push-action@v2 + with: + context: . + push: true + no-cache: true + platforms: ${{ env.PLATFORMS }} + tags: | + ${{ env.DOCKER_IMAGE }}:dev_${{ github.sha }} + ${{ env.DOCKER_IMAGE }}:latest_dev + + - name: Get Nightscout release version + if: success() && github.ref == 'refs/heads/master' + id: package-version + uses: martinbeentjes/npm-get-version-action@master + - name: Build, tag and push the master Docker image + if: success() && github.ref == 'refs/heads/master' + uses: docker/build-push-action@v2 + with: + context: . + push: true + no-cache: true + platforms: ${{ env.PLATFORMS }} + tags: | + ${{ env.DOCKER_IMAGE }}:${{ steps.package-version.outputs.current-version }} + ${{ env.DOCKER_IMAGE }}:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4fcbc57e59d..7b8875e5a73 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,13 @@ node_modules/ bundle/bundle.out.js +.vscode/ .idea/ *.iml my.env +my.*.env +*.pem -*.env static/bower_components/ .*.sw? .DS_Store @@ -20,3 +22,15 @@ static/bower_components/ coverage/ npm-debug.log +*.heapsnapshot + +/tmp +/.vs +/cgm-remote-monitor.njsproj +/cgm-remote-monitor.sln +/obj/Debug +/*.bat + +# directories created by docker-compose.yml +mongo-data/ +letsencrypt/ diff --git a/.jsbeautifyrc b/.jsbeautifyrc new file mode 100644 index 00000000000..1c15d3872ce --- /dev/null +++ b/.jsbeautifyrc @@ -0,0 +1,16 @@ +{ + "indent_size": 2 + , "indent_char": " " + , "comma_first": true + , "keep-array-indentation": true + , "space_after_named_function": true + , "space_after_anon_function": true + , "end_with_newline": true + , "brace_style": "collapse,preserve-inline" + , "space_in_brace": true + , "space-in-paren": false + , "break-chained-methods": false + , "max-preserve-newlines": 2 + , "space-after-anon-function": false + , "indent-empty-lines": false +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000000..0ff38047bb8 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.16.0 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 562c8d9edbb..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: node_js -sudo: false -node_js: - - "0.10" - - "0.12" -services: mongodb -script: make travis -after_script: make report diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6169a7cbc4a..ddc20c1620e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,26 +3,28 @@ **Table of Contents** - [Contributing to cgm-remote-monitor](#contributing-to-cgm-remote-monitor) - - [Design](#design) + - [Design & new features](#design--new-features) - [Develop on `dev`](#develop-on-dev) - [Style Guide](#style-guide) - [Create a prototype](#create-a-prototype) - [Submit a pull request](#submit-a-pull-request) + - [Bug fixing](#bug-fixing) - [Comments and issues](#comments-and-issues) - [Co-ordination](#co-ordination) - [Other Dev Tips](#other-dev-tips) + - [List of Contributors](#list-of-contributors) + - [Core developers, contributing developers, coordinators and documentation writers](#core-developers-contributing-developers-coordinators-and-documentation-writers) + - [Plugin contributors](#plugin-contributors) + - [List of all contributors](#list-of-all-contributors) - # Contributing to cgm-remote-monitor [![Build Status][build-img]][build-url] [![Dependency Status][dependency-img]][dependency-url] [![Coverage Status][coverage-img]][coverage-url] -[![Gitter chat][gitter-img]][gitter-url] -[![Stories in Ready][ready-img]][waffle] -[![Stories in Progress][progress-img]][waffle] +[![Discord chat][discord-img]][discord-url] [build-img]: https://img.shields.io/travis/nightscout/cgm-remote-monitor.svg [build-url]: https://travis-ci.org/nightscout/cgm-remote-monitor @@ -30,30 +32,59 @@ [dependency-url]: https://david-dm.org/nightscout/cgm-remote-monitor [coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg [coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master -[gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg -[gitter-url]: https://gitter.im/nightscout/public -[ready-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=ready&title=Ready -[waffle]: https://waffle.io/nightscout/cgm-remote-monitor -[progress-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=in+progress&title=In+Progress +[discord-img]: https://img.shields.io/discord/629952586895851530?label=discord%20chat +[discord-url]: https://discord.gg/rTKhrqz +[example-env-template]: docs/example-template.env + +## Translations + +Please visit our [project in Crowdin](https://crowdin.com/project/nightscout) to translate Nigthscout. If you want to add a new language, please get in touch with the dev team in [Discord][discord-url]. -## Design +## Installation for development -Participate in the design process by creating an issue to discuss your -design. +Nightscout is a Node.js application. The basic installation of the software for local purposes is: + +1. Clone the software to your local machine using git +2. Install Node from https://nodejs.org/en/download/ +2. Use `npm` to install Nightscout dependencies by invoking `npm install` in the project directory. Note the + dependency installation has to be done using a non-root user - _do not use root_ for development and hosting + the software! +3. Get a Mongo database by either installing Mongo locally, or get a free cloud account from mLab or MongoDB Atlas. +4. Configure Nightscout by copying [`docs/example-template.env`][example-env-template] to `my.env` and run it - see the next chapter in the instructions ## Develop on `dev` -We develop on the `dev` branch. -You can get the dev branch checked out using `git checkout dev`. +We develop on the `dev` branch. All new pull requests should be targeted to `dev`. The `master` branch is only used for distributing the latest version of the tested sources. + +You can get the `dev` branch checked out using `git checkout dev`. + +Once checked out, install the dependencies using `npm install`, then copy the included [`docs/example-template.env`][example-env-template] to `my.env` file to `my.env` and edit the file to include your settings (like the Mongo URL). Leave the `NODE_ENV=development` line intact. Once set, run the site using `npm run dev`. This will start Nightscout in the development mode, with different code packaging rules and automatic restarting of the server using nodemon, when you save changed files on disk. The client also hot-reloads new code in, but it's recommended to reload the website after changes due to the way the plugin sandbox works. + +Note the template sets `INSECURE_USE_HTTP` to `true` to enable the site to work over HTTP in local development. + +If you want to additionaly test the site in production mode, create a file called `my.prod.env` that's a copy of the dev file but with `NODE_ENV=production` and start the site using `npm run prod`. + +## REST API + +Nightscout implements a REST API for data syncronization. The API is documented using Swagger. To access the documentation for the API, run Nightscout locally and load the documentation from /api-docs (or read the associated swagger.json and swagger.yaml files locally). + +Note all dates used to access the API and dates stored in the objects are expected to comply with the ISO-8601 format and be deserializable by the Javascript Date class. Of note here is the dates can contain a plus sign which has a special meaning in URL encoding, so when issuing requests that place dates to the URL, take special care to ensure the data is properly URL encoded. + +## Design & new features + +If you intend to add a new feature, please allow the community to participate in the design process by creating an issue to discuss your design. For new features, the issue should describe what use cases the new feature intends to solve, or which existing use cases are being improved. + +Note Nightscout has a plugin architecture for adding new features. We expect most code for new features live inside a Plugin, so the code retains a clear separation of concerns. If the Plugin API doesn't implement all features you need to implement your feature, please discuss with us on adding those features to the API. Note new features should under almost no circumstances require changes to the existing plugins. ## Style Guide -Some simple rules, that will make it easier to maintain our codebase: +Some simple rules that will make it easier to maintain our codebase: -* All indenting should use 2 space where possible (js, css, html, etc) -* A space before function parameters, such as: `function boom (name, callback) { }`, this makes searching for calls easier -* Name your callback functions, such as `boom('the name', function afterBoom ( result ) { }` +* All indenting should use 2 space where possible (js, css, html, etc). +* Include a space before function parameters, such as: `function boom (name, callback) { }`, this makes searching for function calls easier. +* Name your callback functions, such as `boom('the name', function afterBoom ( result ) { }`. * Don't include author names in the header of your files, if you need to give credit to someone else do it in the commit comment. +* Use single quotes. * Use the comma first style, for example: ```javascript @@ -64,60 +95,147 @@ Some simple rules, that will make it easier to maintain our codebase: }; ``` +If in doubt, format your code with `js-beautify --indent-size 2 --comma-first --keep-array-indentation` + ## Create a prototype -Fork cgm-remote-monitor and create a branch. -You can create a branch using `git checkout -b wip/add-my-widget`. -This creates a new branch called `wip/add-my-widget`. The `wip` -stands for work in progress and is a common prefix so that when know -what to expect when reviewing many branches. +Fork cgm-remote-monitor and create a branch. You can create a branch using `git checkout -b wip/add-my-widget`. This creates a new branch called `wip/add-my-widget`. The "`wip`" stands for work-in-progress and is a common prefix so that we know what to expect when reviewing many branches. ## Submit a pull request -When you are done working with your prototype, it can be tempting to -post on popular channels such as Facebook. We encourage contributors -to submit their code for review, debate, and release before announcing -features on social media. +When you are done working with your prototype, it can be tempting to post on popular channels such as Facebook. We encourage contributors to submit their code for review, debate, and release before announcing features on social media. + +This can be done by checking your code `git commit -avm 'my improvements are here'`, the branch you created back to your own fork. This will probably look something like `git push -u origin wip/add-my-widget`. + +Now that the commits are available on github, you can click on the compare buttons on your fork to create a pull request. Make sure to select [Nightscout's `dev` branch](https://github.com/nightscout/cgm-remote-monitor/tree/dev). + +We assume all new Pull Requests are at least smoke tested by the author and all code in the PR actually works. Please include a description of what the features do and rationalize why the changes are needed. -This can be done by checking your code `git commit -avm 'my -improvements are here'`, the branch you created back to your own +If you add any new NPM module dependencies, you have to rationalize why they are needed - we prefer pull requests that reduce dependencies, not add them. Before releasing a a new version, we check with `npm audit` if our dependencies don't have known security issues. + +When adding new features that add configuration options, please ensure the `README` document is amended with information on the new configuration. + +## Bug fixing + +If you've fixed a bug, please consider adding a unit test to the `/tests` folder that reproduces the original bug without the change. + +Try to identify the root cause of the issue and fix the issue. Pull requests that simply add null checks to hide issues are unlikely to be accepted. + +This can be done by committing your code `git commit -avm 'my +improvements are here'`, and pushing it to the branch you created on your own fork. This will probably look something like `git push -u origin wip/add-my-widget`. -Now that the commits are available on github, you can click on the -compare buttons on your fork to create a pull request. Make sure to -select [Nightscout's `dev` branch](https://github.com/nightscout/cgm-remote-monitor/tree/dev). +Please include instructions how to test the changes. ## Comments and issues -We encourage liberal use of the comments, including images where -appropriate. +We encourage liberal use of the comments, including images where appropriate. ## Co-ordination -There is a google groups nightscout-core developers list where lots of -people discuss Nightscout. Most cgm-remote-monitor hackers use -github's ticketing system, along with Facebook cgm-in-the-cloud, and -gitter system. +We primarily use GitHub's ticketing system for discussing PRs and bugs, and [Discord][discord-url] for general development chatter. -We use git-flow, with `master` as our production, stable branch, and -`dev` is used to queue up for upcoming releases. Everything else is -done on branches, hopefully with names that indicate what to expect. +We use git-flow, with `master` as our production, stable branch, and `dev` is used to queue up for upcoming releases. Everything else is done on branches, hopefully with names that indicate what to expect. -Once `dev` has been reviewed and people feel it's time to release, we -follow the git-flow release process, which creates a new tag and bumps -the version correctly. See sem-ver for versioning strategy. +Once `dev` has been reviewed and people feel it's time to release, we follow the git-flow release process, which creates a new tag and bumps the version correctly. See sem-ver for versioning strategy. -Every commit is tested by travis. We encourage adding tests to -validate your design. We encourage discussing your use cases to help -everyone get a better understanding of your design. +Every commit is tested by travis. We encourage adding tests to validate your design. We encourage discussing your use cases to help everyone get a better understanding of your design. ## Other Dev Tips -* Join the [Gitter chat][gitter-url] -* Get a local dev environment setup if you haven't already -* Try breaking up big features/improvements into small parts. It's much easier to accept small PR's -* Create tests for your new code, and for the old code too. We are aiming for a full test coverage. -* If your going to be working in old code that needs lots of reformatting consider doing the clean as a separate PR. -* If you can find others to help test your PR is will help get them merged in sooner. +* Join the [Discord chat][discord-url]. +* Get a local dev environment setup if you haven't already. +* Try breaking up big features/improvements into small parts. It's much easier to accept small PR's. +* Create tests for your new code as well as the old code. We are aiming for a full test coverage. +* If you're going to be working in old code that needs lots of reformatting, consider doing it as a separate PR. +* If you can find others to help test your PR, it will help get them merged in sooner. +## List of Contributors + +We welcome new contributors. We do not only need core contributors. Regular or one time contributors are welcomed as well. +Also if you can't code, it's possible to contribute by improving the documentation or by translating Nightscout in your own language + +### Core developers, contributing developers, coordinators and documentation writers + +[@andrew-warrington]: https://github.com/andrew-warrington +[@apanasef]: https://github.com/apanasef +[@bewest]: https://github.com/bewest +[@danamlewis]: https://github.com/danamlewis +[@diabetlum]: https://github.com/diabetlum +[@herzogmedia]: https://github.com/herzogmedia +[@jamieowendexcom ]: https://github.com/jamieowendexcom +[@janrpn]: https://github.com/janrpn +[@jasoncalabrese]: https://github.com/jasoncalabrese +[@jizhongwen]: https://github.com/jizhongwen +[@jpcunningh]: https://github.com/jpcunningh +[@jweismann]: https://github.com/jweismann +[@komarserjio]: https://github.com/komarserjio +[@LuminaryXion]: https://github.com/LuminaryXion +[@mcdafydd]: https://github.com/mcdafydd +[@mdomox]: https://github.com/mdomox +[@MilosKozak]: https://github.com/MilosKozak +[@oteroos]: https://github.com/oteroos +[@PieterGit]: https://github.com/PieterGit +[@rarneson]: https://github.com/rarneson +[@rickfriele]: https://github.com/rickfriele +[@scottleibrand]: https://github.com/scottleibrand +[@sulkaharo]: https://github.com/sulkaharo +[@tynbendad]: https://github.com/tynbendad +[@unsoluble]: https://github.com/unsoluble +[@viderehh]: https://github.com/viderehh +[@OpossumGit]: https://github.com/OpossumGit +[@Bartlomiejsz]: https://github.com/Bartlomiejsz + +| Contribution area | List of contributors | +| ------------------------------------- | ---------------------------------- | +| Core developers: | [@jasoncalabrese] [@MilosKozak] [@PieterGit] [@sulkaharo] | +| Former Core developers: (not active): | [@bewest] | +| Contributing developers: | [@jpcunningh] [@scottleibrand] [@komarserjio] [@jweismann] | +| Issue/Pull request coordination: | Please volunteer | +| Cleaning up git fork spam: | Please volunteer | +| Documentation writers: | [@andrew-warrington] [@unsoluble] [@tynbendad] [@danamlewis] [@rarneson] | + +### Plugin contributors + +| Contribution area | List of developers | List of testers +| ------------------------------------- | -------------------- | -------------------- | +| [`alexa` (Amazon Alexa)](README.md#alexa-amazon-alexa)| [@inventor96] | Please volunteer | +| [`ar2` (AR2 Forecasting)](README.md#ar2-ar2-forecasting)| Please volunteer | Please volunteer | +| [`basal` (Basal Profile)](README.md#basal-basal-profile)| Please volunteer | Please volunteer | +| [`boluscalc` (Bolus Wizard)](README.md#boluscalc-bolus-wizard)| Please volunteer | Please volunteer | +| [`bridge` (Share2Nightscout bridge)](README.md#bridge-share2nightscout-bridge)| Please volunteer | Please volunteer | +| [`bwp` (Bolus Wizard Preview)](README.md#bwp-bolus-wizard-preview)| Please volunteer | Please volunteer | +| [`cage` (Cannula Age)](README.md#cage-cannula-age)| [@jpcunningh] | Please volunteer | +| [`careportal` (Careportal)](README.md#careportal-careportal)| Please volunteer | Please volunteer | +| [`cob` (Carbs-on-Board)](README.md#cob-carbs-on-board)| Please volunteer | Please volunteer | +| [`cors` (CORS)](README.md#cors-cors)| Please volunteer | Please volunteer | +| [`delta` (BG Delta)](README.md#delta-bg-delta)| Please volunteer | Please volunteer | +| [`devicestatus` (Device Status)](README.md#devicestatus-device-status)| Please volunteer | Please volunteer | +| [`direction` (BG Direction)](README.md#direction-bg-direction)| Please volunteer | Please volunteer | +| [`errorcodes` (CGM Error Codes)](README.md#errorcodes-cgm-error-codes)| Please volunteer | Please volunteer | +| [`food` (Custom Foods)](README.md#food-custom-foods)| Please volunteer | Please volunteer | +| [`googlehome` (Google Home/DialogFlow)](README.md#googlehome-google-homedialogflow)| [@mdomox] [@rickfriele] [@inventor96] | [@mcdafydd] [@oteroos] [@jamieowendexcom] | +| [`iage` (Insulin Age)](README.md#iage-insulin-age)| Please volunteer | Please volunteer | +| [`iob` (Insulin-on-Board)](README.md#iob-insulin-on-board)| Please volunteer | Please volunteer | +| [`loop` (Loop)](README.md#loop-loop)| Please volunteer | Please volunteer | +| [`mmconnect` (MiniMed Connect bridge)](README.md#mmconnect-minimed-connect-bridge)| Please volunteer | Please volunteer | +| [`openaps` (OpenAPS)](README.md#openaps-openaps)| Please volunteer | Please volunteer | +| [`profile` (Treatment Profile)](README.md#profile-treatment-profile)| Please volunteer | Please volunteer | +| [`pump` (Pump Monitoring)](README.md#pump-pump-monitoring)| Please volunteer | Please volunteer | +| [`rawbg` (Raw BG)](README.md#rawbg-raw-bg)| [@jpcunningh] | Please volunteer | +| [`sage` (Sensor Age)](README.md#sage-sensor-age)| [@jpcunningh] | Please volunteer | +| [`simplealarms` (Simple BG Alarms)](README.md#simplealarms-simple-bg-alarms)| Please volunteer | Please volunteer | +| [`speech` (Speech)](README.md#speech-speech)| [@sulkaharo] | Please volunteer | +| [`timeago` (Time Ago)](README.md#timeago-time-ago)| Please volunteer | Please volunteer | +| [`treatmentnotify` (Treatment Notifications)](README.md#treatmentnotify-treatment-notifications)| Please volunteer | Please volunteer | +| [`upbat` (Uploader Battery)](README.md#upbat-uploader-battery)| [@jpcunningh] | Please volunteer | +| [`xdrip-js` (xDrip-js)](README.md#xdrip-js-xdrip-js)| [@jpcunningh] | Please volunteer | + +### List of all contributors +| Contribution area | List of contributors | +| ------------------------------------- | -------------------- | +| All active developers: | [@jasoncalabrese] [@jpcunningh] [@jweismann] [@komarserjio] [@mdomox] [@MilosKozak] [@PieterGit] [@rickfriele] [@sulkaharo] [@unsoluble] +| All active testers/documentors: | [@danamlewis] [@jamieowendexcom] [@mcdafydd] [@oteroos] [@rarneson] [@tynbendad] [@unsoluble] +| All active translators: | [@apanasef] [@jizhongwen] [@viderehh] [@herzogmedia] [@LuminaryXion] [@OpossumGit] + diff --git a/COPYRIGHT b/COPYRIGHT index b5038bbdfdc..300be637b69 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1 +1,5 @@ -Copyright (C) 2015 The Nightscout Foundation, http://www.nightscoutfoundation.org + +We track contributions on a per-patch basis using git. +Please see our published git log: +* https://github.com/nightscout/cgm-remote-monitor/commits/master + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..279d1f21eb3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:16.16.0-alpine + +LABEL maintainer="Nightscout Contributors" + +WORKDIR /opt/app +ADD . /opt/app + +# TODO: We should be able to do `RUN npm install --only=production`. +# For this to work, we need to copy only package.json and things needed for `npm`'s to succeed. +# TODO: Do we need to re-add `npm audit fix`? Or should that be part of a development process/stage? +RUN npm install --cache /tmp/empty-cache && \ + npm run postinstall && \ + npm run env && \ + rm -rf /tmp/* + # TODO: These should be added in the future to correctly cache express-minify content to disk + # Currently, doing this breaks the browser cache. + # mkdir /tmp/public && \ + # chown node:node /tmp/public + +USER node +EXPOSE 1337 + +CMD ["node", "lib/server/server.js"] diff --git a/Makefile b/Makefile index 2b60d26001b..46a3c462131 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Nightscout tests/builds/analysis TESTS=tests/*.js -MONGO_CONNECTION?=mongodb://localhost/test_db +MONGO_CONNECTION?=mongodb://localhost:27017/test_db CUSTOMCONNSTR_mongo_settings_collection?=test_settings CUSTOMCONNSTR_mongo_collection?=test_sgvs MONGO_SETTINGS=MONGO_CONNECTION=${MONGO_CONNECTION} \ @@ -15,37 +15,67 @@ MONGO_SETTINGS=MONGO_CONNECTION=${MONGO_CONNECTION} \ # coverage reporter's ability to instrument the tests correctly. # Hard coding it to the local with our pinned version is bigger for # initial installs, but ensures a consistent environment everywhere. -# On Travis, ./node_modules/.bin and other `nvm` and `npm` bundles are +# On GA, ./node_modules/.bin and other `nvm` and `npm` bundles are # inserted into the default `$PATH` enviroinment, making pointing to # the unwrapped mocha executable necessary. MOCHA=./node_modules/mocha/bin/_mocha # Pinned from dependency list. ISTANBUL=./node_modules/.bin/istanbul ANALYZED=./coverage/lcov.info -export CODACY_REPO_TOKEN=e29ae5cf671f4f918912d9864316207c +# Following token deprecated +# export CODACY_REPO_TOKEN=e29ae5cf671f4f918912d9864316207c + +DOCKER_IMAGE=nightscout/cgm-remote-monitor all: test coverage: NODE_ENV=test ${MONGO_SETTINGS} \ - ${ISTANBUL} cover ${MOCHA} -- -R tap ${TESTS} + ${ISTANBUL} cover ${MOCHA} -- --timeout 15000 -R tap ${TESTS} report: test -f ${ANALYZED} && \ (npm install coveralls && cat ${ANALYZED} | \ ./node_modules/.bin/coveralls) || echo "NO COVERAGE" test -f ${ANALYZED} && \ - (npm install codecov.io && cat ${ANALYZED} | \ - ./node_modules/codecov.io/bin/codecov.io.js) || echo "NO COVERAGE" - test -f ${ANALYZED} && \ (npm install codacy-coverage && cat ${ANALYZED} | \ YOURPACKAGE_COVERAGE=1 ./node_modules/codacy-coverage/bin/codacy-coverage.js) || echo "NO COVERAGE" +test_onebyone: + python -c 'import os,sys,fcntl; flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL); fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK);' + for var in tests/*.js; do ${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap $$var; done | tap-set-exit + test: - ${MONGO_SETTINGS} ${MOCHA} -R tap ${TESTS} + ${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap ${TESTS} -travis: - NODE_ENV=test ${MONGO_SETTINGS} \ - ${ISTANBUL} cover ${MOCHA} --report lcovonly -- -R tap ${TESTS} +ci_tests: + python -c 'import os,sys,fcntl; flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL); fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK);' +# NODE_ENV=test ${MONGO_SETTINGS} \ +# ${ISTANBUL} cover ${MOCHA} --report lcovonly -- --timeout 5000 -R tap ${TESTS} + for var in tests/*.js; do ${MONGO_SETTINGS} ${MOCHA} --timeout 30000 --exit --bail -R tap $$var; done + +docker_release: + # Get the version from the package.json file + $(eval DOCKER_TAG=$(shell cat package.json | jq '.version' | tr -d '"')) + $(eval BRANCH=$(lastword $(subst /, ,$(GITHUB_REF)))) + # + # + # Rebuild the image. We do this with no-cache so that we have all security upgrades, + # since that's more important than fewer layers in the Docker image. + docker build --no-cache=true -t $(DOCKER_IMAGE):$(DOCKER_TAG) . + # + # Push the master branch to Docker hub as 'latest' + if [ "$(BRANCH)" = "master" ]; then \ + docker tag $(DOCKER_IMAGE):$(DOCKER_TAG) $(DOCKER_IMAGE):latest && \ + docker push $(DOCKER_IMAGE):$(DOCKER_TAG) + docker push $(DOCKER_IMAGE):latest; \ + fi + # + # Push the dev branch to Docker Hub as 'latest_dev' + if [ "$(BRANCH)" = "dev" ]; then \ + docker tag $(DOCKER_IMAGE):$(DOCKER_TAG) $(DOCKER_IMAGE):latest_dev && \ + docker push $(DOCKER_IMAGE):$(DOCKER_TAG) + docker push $(DOCKER_IMAGE):latest_dev; \ + fi -.PHONY: all coverage report test travis +.PHONY: all coverage docker_release report test ci_tests diff --git a/Procfile b/Procfile index 32dd1c83adc..ac69b0a7601 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: ./node_modules/.bin/forever --minUptime 100 -c node server.js +web: node lib/server/server.js diff --git a/README.md b/README.md index 4278d62c49f..3123ec3f4af 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ Nightscout Web Monitor (a.k.a. cgm-remote-monitor) -====================================== +================================================== ![nightscout horizontal](https://cloud.githubusercontent.com/assets/751143/8425633/93c94dc0-1ebc-11e5-99e7-71a8f464caac.png) @@ -7,11 +7,7 @@ Nightscout Web Monitor (a.k.a. cgm-remote-monitor) [![Dependency Status][dependency-img]][dependency-url] [![Coverage Status][coverage-img]][coverage-url] [![Codacy Badge][codacy-img]][codacy-url] -[![Gitter chat][gitter-img]][gitter-url] -[![Stories in Ready][ready-img]][waffle] -[![Stories in Progress][progress-img]][waffle] - -[![Deploy to Heroku][heroku-img]][heroku-url] +[![Discord chat][discord-img]][discord-url] This acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patient's glucose data in @@ -22,48 +18,103 @@ and blood glucose values are predicted 0.5 hours ahead using an autoregressive second order model. Alarms are generated for high and low values, which can be cleared by any watcher of the data. -# [#WeAreNotWaiting](https://twitter.com/hashtag/wearenotwaiting?src=hash&vertical=default&f=images) and [this](https://vimeo.com/109767890) is why. +# Looking for documentation? + +## End user? + +Nightscout documentation is currently split to two locations. This page lists all the configuration options in +Nightscout and is useful for users who've already gone through the installation process. IF you're looking +for the documentation that looks like it's written for non-programmers, that's located at [nightscout.github.io](https://nightscout.github.io/). + +Older documentation is available at [nightscout.info](http://nightscout.info). + +## Developer? + +See [CONTRIBUTING.md](CONTRIBUTING.md) + +## [#WeAreNotWaiting](https://twitter.com/hashtag/wearenotwaiting?src=hash&vertical=default&f=images) and [this](https://vimeo.com/109767890) is why. -Community maintained fork of the -[original cgm-remote-monitor][original]. +[![Coverage Status](https://coveralls.io/repos/github/nightscout/cgm-remote-monitor/badge.svg?branch=master)](https://coveralls.io/github/nightscout/cgm-remote-monitor?branch=master) [build-img]: https://img.shields.io/travis/nightscout/cgm-remote-monitor.svg [build-url]: https://travis-ci.org/nightscout/cgm-remote-monitor [dependency-img]: https://img.shields.io/david/nightscout/cgm-remote-monitor.svg [dependency-url]: https://david-dm.org/nightscout/cgm-remote-monitor -[coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg -[coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master +[coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/dev.svg +[coverage-url]: https://coveralls.io/github/nightscout/cgm-remote-monitor?branch=master [codacy-img]: https://www.codacy.com/project/badge/f79327216860472dad9afda07de39d3b [codacy-url]: https://www.codacy.com/app/Nightscout/cgm-remote-monitor -[gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg -[gitter-url]: https://gitter.im/nightscout/public -[ready-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=ready&title=Ready -[waffle]: https://waffle.io/nightscout/cgm-remote-monitor -[progress-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=in+progress&title=In+Progress +[discord-img]: https://img.shields.io/discord/629952586895851530?label=discord%20chat +[discord-url]: https://discord.gg/rTKhrqz [heroku-img]: https://www.herokucdn.com/deploy/button.png -[heroku-url]: https://heroku.com/deploy +[heroku-url]: https://heroku.com/deploy?template=https://github.com/nightscout/cgm-remote-monitor +[update-img]: docs/update.png +[update-fork]: http://nightscout.github.io/pages/update-fork/ [original]: https://github.com/rnpenguin/cgm-remote-monitor + **Table of Contents** - [Install](#install) + - [Supported configurations:](#supported-configurations) + - [Recommended minimum browser versions for using Nightscout:](#recommended-minimum-browser-versions-for-using-nightscout) + - [Windows installation software requirements:](#windows-installation-software-requirements) + - [Installation notes for users with nginx or Apache reverse proxy for SSL/TLS offloading:](#installation-notes-for-users-with-nginx-or-apache-reverse-proxy-for-ssltls-offloading) + - [Installation notes for Microsoft Azure, Windows:](#installation-notes-for-microsoft-azure-windows) +- [Development](#development) - [Usage](#usage) - [Updating my version?](#updating-my-version) - - [What is my mongo string?](#what-is-my-mongo-string) - [Configure my uploader to match](#configure-my-uploader-to-match) - [Nightscout API](#nightscout-api) - [Example Queries](#example-queries) - [Environment](#environment) - [Required](#required) - - [Features/Labs](#featureslabs) + - [Features](#features) - [Alarms](#alarms) - [Core](#core) - [Predefined values for your browser settings (optional)](#predefined-values-for-your-browser-settings-optional) + - [Predefined values for your server settings (optional)](#predefined-values-for-your-server-settings-optional) + - [Views](#views) - [Plugins](#plugins) - [Default Plugins](#default-plugins) - - [Built-in/Example Plugins:](#built-inexample-plugins) + - [`delta` (BG Delta)](#delta-bg-delta) + - [`direction` (BG Direction)](#direction-bg-direction) + - [`upbat` (Uploader Battery)](#upbat-uploader-battery) + - [`timeago` (Time Ago)](#timeago-time-ago) + - [`devicestatus` (Device Status)](#devicestatus-device-status) + - [`errorcodes` (CGM Error Codes)](#errorcodes-cgm-error-codes) + - [`ar2` (AR2 Forecasting)](#ar2-ar2-forecasting) + - [`simplealarms` (Simple BG Alarms)](#simplealarms-simple-bg-alarms) + - [`profile` (Treatment Profile)](#profile-treatment-profile) + - [Advanced Plugins:](#advanced-plugins) + - [`careportal` (Careportal)](#careportal-careportal) + - [`boluscalc` (Bolus Wizard)](#boluscalc-bolus-wizard) + - [`food` (Custom Foods)](#food-custom-foods) + - [`rawbg` (Raw BG)](#rawbg-raw-bg) + - [`iob` (Insulin-on-Board)](#iob-insulin-on-board) + - [`cob` (Carbs-on-Board)](#cob-carbs-on-board) + - [`bwp` (Bolus Wizard Preview)](#bwp-bolus-wizard-preview) + - [`cage` (Cannula Age)](#cage-cannula-age) + - [`sage` (Sensor Age)](#sage-sensor-age) + - [`iage` (Insulin Age)](#iage-insulin-age) + - [`bage` (Battery Age)](#bage-battery-age) + - [`treatmentnotify` (Treatment Notifications)](#treatmentnotify-treatment-notifications) + - [`basal` (Basal Profile)](#basal-basal-profile) + - [`bolus` (Bolus Rendering)](#bolus-bolus-rendering) + - [`connect` (Nightscout Connect)](#connect-nightscout-connect) + - [`bridge` (Share2Nightscout bridge)](#bridge-share2nightscout-bridge), _deprecated_ + - [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge), _deprecated_ + - [`pump` (Pump Monitoring)](#pump-pump-monitoring) + - [`openaps` (OpenAPS)](#openaps-openaps) + - [`loop` (Loop)](#loop-loop) + - [`override` (Override Mode)](#override-override-mode) + - [`xdripjs` (xDrip-js)](#xdripjs-xdrip-js) + - [`alexa` (Amazon Alexa)](#alexa-amazon-alexa) + - [`googlehome` (Google Home/DialogFLow)](#googlehome-google-homedialogflow) + - [`speech` (Speech)](#speech-speech) + - [`cors` (CORS)](#cors-cors) - [Extended Settings](#extended-settings) - [Pushover](#pushover) - [IFTTT Maker](#ifttt-maker) @@ -71,69 +122,114 @@ Community maintained fork of the - [Setting environment variables](#setting-environment-variables) - [Vagrant install](#vagrant-install) - [More questions?](#more-questions) + - [Browser testing suite provided by](#browser-testing-suite-provided-by) - [License](#license) # Install -Requirements: +## Supported configurations: + +- [Nightscout Setup](https://nightscout.github.io/nightscout/new_user/) (recommended) + +While you can install Nightscout on a virtual server or a Raspberry Pi, we do not recommend this unless you have at least some +experience hosting Node applications and development using the toolchain in use with Nightscout. + +If you're a hosting provider and want to provide our users additional hosting options, +you're welcome to issue a documentation pull request with instructions on how to setup Nightscout on your system. + +## Recommended minimum browser versions for using Nightscout: -- [Node.js](http://nodejs.org/) +Our [browserslist](https://github.com/browserslist/browserslist) policy is documented in `.browserlistrc`. +We currently support approximately [91%](https://browsersl.ist/?q=%3E+0.25%25%2C+ios_saf+9.3%2C+ios_saf+10.3%2C+ios_saf+13.7%2C+ios_saf+14.8%2C+not+dead%2C+not+and_uc+12.12%2C+not+ie+11%0A) of all browsers globally used. These include: -Clone this repo then install dependencies into the root of the project: +- Android Chrome: 104 or later (`and_chr`) +- Google Chrome: 101 or later (`chrome`) +- Microsoft Edge: 103 or later (`edge`) +- Mozilla Firefox: 102 or later (`firefox`) +- Apple Safari on iOS: 15.5 or later (`ios_saf`) +- Opera Mini on Android: 63 or later (`op_mini`) +- Opera: 88 or later (`opera`) +- Apple Safari for macOS 10.15 Catalina or later: : 15.5 or later (`safari`) +- Samsung Internet on Android: 17.0 or later (`samsung`) +- Internet Explorer 11 : not supported + +Older versions or other browsers might work, but are untested and unsupported. We'll try to to keep Nightscout compatible with older iPads (e.g. Safari on iOS 10.3.4), but note that those devices are not supported by Apple anymore and have known security issues. Debugging these old devices gets harder due to Apple not supporting debugging the old devices on Macs that have been updated. Some features may not work with devices/browsers on the older end of these requirements. + + + +## Installation software requirements: + +- [Node.js](http://nodejs.org/) Latest Node v14 or v16 LTS. Node versions that do not have the latest security patches will not be supported. Use [Install instructions for Node](https://nodejs.org/en/download/package-manager/) or use `bin/setup.sh`) +- [MongoDB](https://www.mongodb.com/download-center?jmp=nav#community) 4.2 or 4.4. + +As a non-root user clone this repo then install dependencies into the root of the project: ```bash $ npm install ``` -#Usage +## Installation notes for users with nginx or Apache reverse proxy for SSL/TLS offloading: -The data being uploaded from the server to the client is from a -MongoDB server such as [mongolab][mongodb]. +- Your site redirects insecure connections to `https` by default. If you use a reverse proxy like nginx or Apache to handle the connection security for you, make sure it sets the `X-Forwarded-Proto` header. Otherwise nightscout will be unable to know if it was called through a secure connection and will try to redirect you to the https version. If you're unable to set this Header, you can change the `INSECURE_USE_HTTP` setting in nightscout to true in order to allow insecure connections without being redirected. +- In case you use a proxy. Do not use an external network interfaces for hosting Nightscout. Make sure the unsecure port is not available from a remote network connection +- HTTP Strict Transport Security (HSTS) headers are enabled by default, use settings `SECURE_HSTS_HEADER` and `SECURE_HSTS_HEADER_*` +- See [Predefined values for your server settings](#predefined-values-for-your-server-settings-optional) for more details -[mongodb]: https://mongolab.com -[autoconfigure]: http://nightscout.github.io/pages/configure/ -[mongostring]: http://nightscout.github.io/pages/mongostring/ -[update-fork]: http://nightscout.github.io/pages/update-fork/ +## Installation notes for Microsoft Azure, Windows: -## Updating my version? -The easiest way to update your version of cgm-remote-monitor to our latest -recommended version is to use the [update my fork tool][update-fork]. It even -gives out stars if you are up to date. +- If deploying the software to Microsoft Azure, you must set ** in the app settings for *WEBSITE_NODE_DEFAULT_VERSION* and *SCM_COMMAND_IDLE_TIMEOUT* **before** you deploy the latest Nightscout or the site deployment will likely fail. Other hosting environments do not require this setting. Additionally, if using the Azure free hosting tier, the installation might fail due to resource constraints imposed by Azure on the free hosting. Please set the following settings to the environment in Azure: +``` +WEBSITE_NODE_DEFAULT_VERSION=16.16.0 +SCM_COMMAND_IDLE_TIMEOUT=300 +``` +- See [install MongoDB, Node.js, and Nightscouton a single Windows system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). if you want to host your Nightscout outside of the cloud. Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10. +- If you deploy to Windows and want to develop or test you need to install [Cygwin](https://www.cygwin.com/) (use [setup-x86_64.exe](https://www.cygwin.com/setup-x86_64.exe) and make sure to install `build-essential` package. Test your configuration by executing `make` and check if all tests are ok. + +# Development + +Want to help with development, or just see how Nightscout works? Great! See [CONTRIBUTING.md](CONTRIBUTING.md) for development-related documentation. -## What is my mongo string? +# Usage -Try the [what is my mongo string tool][mongostring] to get a good idea of your -mongo string. You can copy and paste the text in the gray box into your -`MONGO_CONNECTION` environment variable. +The data being uploaded from the server to the client is from a MongoDB server such as [MongoDB Atlas][https://www.mongodb.com]. + +[autoconfigure]: https://nightscout.github.io/pages/configure/ +[mongostring]: https://nightscout.github.io/pages/mongostring/ + +## Updating my version? + +The easiest way to update your version of cgm-remote-monitor to the latest version is to use the [update tool][update-fork]. A step-by-step guide is available [here][http://www.nightscout.info/wiki/welcome/how-to-update-to-latest-cgm-remote-monitor-aka-cookie]. +To downgrade to an older version, follow [this guide][http://www.nightscout.info/wiki/welcome/how-to-deploy-an-older-version-of-nightscout]. ## Configure my uploader to match Use the [autoconfigure tool][autoconfigure] to sync an uploader to your config. - ## Nightscout API -The Nightscout API enables direct access to your DData without the need for direct Mongo access. +The Nightscout API enables direct access to your data without the need for Mongo access. You can find CGM data in `/api/v1/entries`, Care Portal Treatments in `/api/v1/treatments`, and Treatment Profiles in `/api/v1/profile`. The server status and settings are available from `/api/v1/status.json`. By default the `/entries` and `/treatments` APIs limit results to the the most recent 10 values from the last 2 days. You can get many more results, by using the `count`, `date`, `dateString`, and `created_at` parameters, depending on the type of data you're looking for. - + +Once you've installed Nightscout, you can access API documentation by loading `/api-docs/` URL in your instance. + #### Example Queries -(replace `http://localhost:1337` with your base url, YOUR-SITE) - +(replace `http://localhost:1337` with your own URL) + * 100's: `http://localhost:1337/api/v1/entries.json?find[sgv]=100` + * Count of 100's in a month: `http://localhost:1337/api/v1/count/entries/where?find[dateString][$gte]=2016-09&find[dateString][$lte]=2016-10&find[sgv]=100` * BGs between 2 days: `http://localhost:1337/api/v1/entries/sgv.json?find[dateString][$gte]=2015-08-28&find[dateString][$lte]=2015-08-30` * Juice Box corrections in a year: `http://localhost:1337/api/v1/treatments.json?count=1000&find[carbs]=15&find[eventType]=Carb+Correction&find[created_at][$gte]=2015` * Boluses over 2U: `http://localhost:1337/api/v1/treatments.json?find[insulin][$gte]=2` The API is Swagger enabled, so you can generate client code to make working with the API easy. -To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.html or review [swagger.yaml](swagger.yaml). - +To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or review [swagger.yaml](lib/server/swagger.yaml). ## Environment @@ -141,27 +237,39 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm ### Required - * `MONGO_CONNECTION` - Your mongo uri, for example: `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout` - * `DISPLAY_UNITS` (`mg/dl`) - Choices: `mg/dl` and `mmol`. Setting to `mmol` puts the entire server into `mmol` mode by default, no further settings needed. - * `BASE_URL` - Used for building links to your sites api, ie pushover callbacks, usually the URL of your Nightscout site you may want https instead of http + * `MONGODB_URI` - The connection string for your Mongo database. Something like `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout`. + * `API_SECRET` - A secret passphrase that must be at least 12 characters long. + * `MONGODB_COLLECTION` (`entries`) - The Mongo collection where CGM entries are stored. + * `DISPLAY_UNITS` (`mg/dl`) - Options are `mg/dl` or `mmol/L` (or just `mmol`). Setting to `mmol/L` puts the entire server into `mmol/L` mode by default, no further settings needed. -### Features/Labs +### Features * `ENABLE` - Used to enable optional features, expects a space delimited list, such as: `careportal rawbg iob`, see [plugins](#plugins) below * `DISABLE` - Used to disable default features, expects a space delimited list, such as: `direction upbat`, see [plugins](#plugins) below - * `API_SECRET` - A secret passphrase that must be at least 12 characters long, required to enable `POST` and `PUT`; also required for the Care Portal - * `TREATMENTS_AUTH` (`off`) - possible values `on` or `off`. When on device must be authenticated by entering `API_SECRET` to create treatments + * `BASE_URL` - Used for building links to your site's API, i.e. Pushover callbacks, usually the URL of your Nightscout site. + * `AUTH_DEFAULT_ROLES` (`readable`) - possible values `readable`, `denied`, or any valid role + name. When `readable`, anyone can view Nightscout without a token. + Setting it to `denied` will require a token from every visit, using `status-only` will enable api-secret based login. + * `IMPORT_CONFIG` - Used to import settings and extended settings from a url such as a gist. Structure of file should be something like: `{"settings": {"theme": "colors"}, "extendedSettings": {"upbat": {"enableAlerts": true}}}` + * `TREATMENTS_AUTH` (`on`) - possible values `on` or `off`. Deprecated, if set to `off` the `careportal` role will be added to `AUTH_DEFAULT_ROLES` + +#### Data Rights +These are useful to help protect your rights to portability and +autonomy for your data: + * `OBSCURED` - list, identical to `ENABLE`, a list of plugins to + obscure. + * `OBSCURE_DEVICE_PROVENANCE` - Required, a string visible to the [companies deciding to filter based on your data](https://help.sugarmate.io/en/articles/4673402-adding-a-nightscout-data-source). For example, `my-data-rights`. ### Alarms - These alarm setting effect all delivery methods (browser, pushover, maker, etc), some settings can be overridden per client (web browser) - + These alarm setting affect all delivery methods (browser, Pushover, IFTTT, etc.). Values and settings entered here will be the defaults for new browser views, but will be overridden if different choices are made in the settings UI. + * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's - * `BG_HIGH` (`260`) - must be set using mg/dl units; the high BG outside the target range that is considered urgent - * `BG_TARGET_TOP` (`180`) - must be set using mg/dl units; the top of the target range, also used to draw the line on the chart - * `BG_TARGET_BOTTOM` (`80`) - must be set using mg/dl units; the bottom of the target range, also used to draw the line on the chart - * `BG_LOW` (`55`) - must be set using mg/dl units; the low BG outside the target range that is considered urgent + * `BG_HIGH` (`260`) - the high BG outside the target range that is considered urgent (interprets units based on DISPLAY_UNITS setting) + * `BG_TARGET_TOP` (`180`) - the top of the target range, also used to draw the line on the chart (interprets units based on DISPLAY_UNITS setting) + * `BG_TARGET_BOTTOM` (`80`) - the bottom of the target range, also used to draw the line on the chart (interprets units based on DISPLAY_UNITS setting) + * `BG_LOW` (`55`) - the low BG outside the target range that is considered urgent (interprets units based on DISPLAY_UNITS setting) * `ALARM_URGENT_HIGH` (`on`) - possible values `on` or `off` * `ALARM_URGENT_HIGH_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent high alarms, space separated for options in browser, first used for pushover * `ALARM_HIGH` (`on`) - possible values `on` or `off` @@ -173,37 +281,83 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `ALARM_URGENT_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover * `ALARM_WARN_MINS` (`30 60 90 120`) - Number of minutes to snooze warning alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover - ### Core - * `MONGO_COLLECTION` (`entries`) - The collection used to store SGV, MBG, and CAL records from your CGM device * `MONGO_TREATMENTS_COLLECTION` (`treatments`) -The collection used to store treatments entered in the Care Portal, see the `ENABLE` env var above * `MONGO_DEVICESTATUS_COLLECTION`(`devicestatus`) - The collection used to store device status information such as uploader battery * `MONGO_PROFILE_COLLECTION`(`profile`) - The collection used to store your profiles * `MONGO_FOOD_COLLECTION`(`food`) - The collection used to store your food database + * `MONGO_ACTIVITY_COLLECTION`(`activity`) - The collection used to store activity data * `PORT` (`1337`) - The port that the node.js application will listen on. - * `SSL_KEY` - Path to your ssl key file, so that ssl(https) can be enabled directly in node.js - * `SSL_CERT` - Path to your ssl cert file, so that ssl(https) can be enabled directly in node.js - * `SSL_CA` - Path to your ssl ca file, so that ssl(https) can be enabled directly in node.js + * `HOSTNAME` - The hostname that the node.js application will listen on, null by default for any hostname for IPv6 you may need to use `::`. + * `SSL_KEY` - Path to your ssl key file, so that ssl(https) can be enabled directly in node.js. If using Let's Encrypt, make this variable the path to your privkey.pem file (private key). + * `SSL_CERT` - Path to your ssl cert file, so that ssl(https) can be enabled directly in node.js. If using Let's Encrypt, make this variable the path to fullchain.pem file (cert + ca). + * `SSL_CA` - Path to your ssl ca file, so that ssl(https) can be enabled directly in node.js. If using Let's Encrypt, make this variable the path to chain.pem file (chain). * `HEARTBEAT` (`60`) - Number of seconds to wait in between database checks - + * `DEBUG_MINIFY` (`true`) - Debug option, setting to `false` will disable bundle minification to help tracking down error and speed up development + * `DE_NORMALIZE_DATES`(`true`) - The Nightscout REST API normalizes all entered dates to UTC zone. Some Nightscout clients have broken date deserialization logic and expect to received back dates in zoned formats. Setting this variable to `true` causes the REST API to serialize dates sent to Nightscout in zoned format back to zoned format when served to clients over REST. ### 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`) - Usually name of T1 - * `THEME` (`default`) - possible values `default` or `colors` + * `CUSTOM_TITLE` (`Nightscout`) - Title for the main view + * `THEME` (`colors`) - possible values `default`, `colors`, or `colorblindfriendly` * `ALARM_TIMEAGO_WARN` (`on`) - possible values `on` or `off` * `ALARM_TIMEAGO_WARN_MINS` (`15`) - minutes since the last reading to trigger a warning * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm * `SHOW_PLUGINS` - enabled plugins that should have their visualizations shown, defaults to all enabled + * `SHOW_FORECAST` (`ar2`) - plugin forecasts that should be shown by default, supports space delimited values such as `"ar2 openaps"` * `LANGUAGE` (`en`) - language of Nightscout. If not available english is used + * Currently supported language codes are: bg (Български), cs (Čeština), de (Deutsch), dk (Dansk), el (Ελληνικά), en (English), es (Español), fi (Suomi), fr (Français), he (עברית), hr (Hrvatski), hu (magyar), it (Italiano), ko (한국어), nb (Norsk (Bokmål)), nl (Nederlands), pl (Polski), pt (Português (Brasil)), ro (Română), ru (Русский), sk (Slovenčina), sv (Svenska), tr (Turkish), zh_cn (中文(简体)), zh_tw (中文(繁體)) * `SCALE_Y` (`log`) - The type of scaling used for the Y axis of the charts system wide. * The default `log` (logarithmic) option will let you see more detail towards the lower range, while still showing the full CGM range. - * 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. + * 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. + +### 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. + * `SECURE_HSTS_HEADER` (`true`) - Add HTTP Strict Transport Security (HSTS) header. Possible values `false`, or `true`. + * `SECURE_HSTS_HEADER_INCLUDESUBDOMAINS` (`false`) - includeSubdomains options for HSTS. Possible values `false`, or `true`. + * `SECURE_HSTS_HEADER_PRELOAD` (`false`) - ask for preload in browsers for HSTS. Possible values `false`, or `true`. + * `SECURE_CSP` (`false`) - Add Content Security Policy headers. Possible values `false`, or `true`. + * `SECURE_CSP_REPORT_ONLY` (`false`) - If set to `true` allows to experiment with policies by monitoring (but not enforcing) their effects. Possible values `false`, or `true`. + +### Views + + Nightscout allows to create custom, simplified views using a predefined set of elements. This option is available under `[+]` link in the main menu. + + List of available items: + * `SGV` - Sensor Glucose Value + * `SGV age` - time since the last SGV read + * `SGV delta` - change of SGV in the last 5 minutes + * `Trend arrow` - icon of the SG trend + * `Time` - current time + * `Line break` - invisible item that will move following items to the next line (by default all are showing on the same level) + + All visible items have `Size` property which allows to customize the view even more. Also, all items may appear multiple times on the view. + + Apart from adding items, it is possible to customize other aspects of the views, like selecting `Color` or `Black` background. The first one will indicate current BG threshold (green = in range; blue = below range; yellow = above range; red = urgent below/above). + `Show SGV age` option will make `SGV age` item appear `Always` or only if the predefined threshold is reached: `Only after threshold`. Breaching `SGV age threshold` will also make `Color` background turn grey and strike through `SGV`. + `Clock view configurator` will generate an URL (available under `Open my clock view!` link) that could be bookmarked. + + There are a few default views available from the main menu: + * `Clock` - Shows current BG, trend arrow, and time of day. Grey text on a black background. + * `Color` - Shows current BG and trend arrow. White text on a color background. + * `Simple` - Shows current BG. Grey text on a black background. + + If you launch one of these views in a fullscreen view in iOS, you can use a left-to-right swipe gesture to exit the view. + +### Split View + + Some users will need easy access to multiple Nightscout views at the same time. We have a special view for this case, accessed on /split path on your Nightscout URL. The view supports any number of sites between 1 to 8 way split, where the content for the screen can be loaded from multiple Nightscout instances. Note you still need to host separate instances for each Nightscout being monitored including the one that hosts the split view page - these variables only add the ability to load multiple views into one browser page. To set the URLs from which the content is loaded, set: + * `FRAME_URL_1` - URL where content is loaded, for the first view (increment the number up to 8 to get more views) + * `FRAME_NAME_1` - Name for the first split view portion of the screen (increment the number to name more views) ### Plugins @@ -212,69 +366,345 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm The built-in/example plugins that are available by default are listed below. The plugins may still need to be enabled by adding to the `ENABLE` environment variable. #### Default Plugins + + These can be disabled by adding them to the `DISABLE` variable, for example `DISABLE="direction upbat"` + +##### `delta` (BG Delta) + Calculates and displays the change between the last 2 BG values. + +##### `direction` (BG Direction) + Displays the trend direction. + +##### `upbat` (Uploader Battery) + Displays the most recent battery status from the uploader phone. . Use these [extended setting](#extended-settings) to adjust behavior: + * `UPBAT_ENABLE_ALERTS` (`false`) - Set to `true` to enable uploader battery alarms via Pushover and IFTTT. + * `UPBAT_WARN` (`30`) - Minimum battery percent to trigger warning. + * `UPBAT_URGENT` (`20`) - Minimum battery percent to trigger urgent alarm. + +##### `timeago` (Time Ago) + Displays the time since last CGM entry. Use these [extended setting](#extended-settings) to adjust behavior: + * `TIMEAGO_ENABLE_ALERTS` (`false`) - Set to `true` to enable stale data alarms via Pushover and IFTTT. + * `ALARM_TIMEAGO_WARN` (`on`) - possible values `on` or `off` + * `ALARM_TIMEAGO_WARN_MINS` (`15`) - minutes since the last reading to trigger a warning + * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` + * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm + +##### `devicestatus` (Device Status) + Used by `upbat` and other plugins to display device status info. Supports the `DEVICESTATUS_ADVANCED="true"` [extended setting](#extended-settings) to send all device statuses to the client for retrospective use and to support other plugins. + +##### `errorcodes` (CGM Error Codes) + Generates alarms for CGM codes `9` (hourglass) and `10` (???). + * Use [extended settings](#extended-settings) to adjust what errorcodes trigger notifications and alarms: + * `ERRORCODES_INFO` (`1 2 3 4 5 6 7 8`) - By default the needs calibration (blood drop) and other codes below 9 generate an info level notification, set to a space separate list of number or `off` to disable + * `ERRORCODES_WARN` (`off`) - By default there are no warning configured, set to a space separate list of numbers or `off` to disable + * `ERRORCODES_URGENT` (`9 10`) - By default the hourglass and ??? generate an urgent alarm, set to a space separate list of numbers or `off` to disable + +##### `ar2` (AR2 Forecasting) + Generates alarms based on forecasted values. See [Forecasting using AR2 algorithm](https://github.com/nightscout/nightscout.github.io/wiki/Forecasting) + * Enabled by default if no thresholds are set **OR** `ALARM_TYPES` includes `predict`. + * Use [extended settings](#extended-settings) to adjust AR2 behavior: + * `AR2_CONE_FACTOR` (`2`) - to adjust size of cone, use `0` for a single line. + +##### `simplealarms` (Simple BG Alarms) + Uses `BG_HIGH`, `BG_TARGET_TOP`, `BG_TARGET_BOTTOM`, `BG_LOW` thresholds to generate alarms. + * Enabled by default if 1 of these thresholds is set **OR** `ALARM_TYPES` includes `simple`. + +##### `profile` (Treatment Profile) + Add link to Profile Editor and allow to enter treatment profile settings. Also uses the extended setting: + * `PROFILE_HISTORY` (`off`) - possible values `on` or `off`. Enable/disable NS ability to keep history of your profiles (still experimental) + * `PROFILE_MULTIPLE` (`off`) - possible values `on` or `off`. Enable/disable NS ability to handle and switch between multiple treatment profiles + +#### Advanced Plugins: + +##### `careportal` (Careportal) + An optional form to enter treatments. + +##### `boluscalc` (Bolus Wizard) + +##### `food` (Custom Foods) + An option plugin to enable adding foods from database in Bolus Wizard and enable . + +##### `rawbg` (Raw BG) + Calculates BG using sensor and calibration records from and displays an alternate BG values and noise levels. Defaults that can be adjusted with [extended setting](#extended-settings) + * `DISPLAY` (`unsmoothed`) - Allows the user to control which algorithm is used to calculate the displayed raw BG values using the most recent calibration record. + * `unfiltered` - Raw BG is calculated by applying the calibration to the glucose record's unfiltered value. + * `filtered` - Raw BG is calculated by applying the calibration to the glucose record's filtered value. The glucose record's filtered values are generally produced by the CGM by a running average of the unfiltered values to produce a smoothed value when the sensor noise is high. + * `unsmoothed` - Raw BG is calculated by first finding the ratio of the calculated filtered value (the same value calculated by the `filtered` setting) to the reported glucose value. The displayed raw BG value is calculated by dividing the calculated unfiltered value (the same value calculated by the `unfiltered` setting) by the ratio. The effect is to exagerate changes in trend direction so the trend changes are more noticeable to the user. This is the legacy raw BG calculation algorithm. + +##### `iob` (Insulin-on-Board) + Adds the IOB pill visualization in the client and calculates values that used by other plugins. Uses treatments with insulin doses and the `dia` and `sens` fields from the [treatment profile](#treatment-profile). + +##### `cob` (Carbs-on-Board) + Adds the COB pill visualization in the client and calculates values that used by other plugins. Uses treatments with carb doses and the `carbs_hr`, `carbratio`, and `sens` fields from the [treatment profile](#treatment-profile). + +##### `bwp` (Bolus Wizard Preview) + This plugin in intended for the purpose of automatically snoozing alarms when the CGM indicates high blood sugar but there is also insulin on board (IOB) and secondly, alerting to user that it might be beneficial to measure the blood sugar using a glucometer and dosing insulin as calculated by the pump or instructed by trained medicare professionals. ***The values provided by the plugin are provided as a reference based on CGM data and insulin sensitivity you have configured, and are not intended to be used as a reference for bolus calculation.*** The plugin calculates the bolus amount when above your target, generates alarms when you should consider checking and bolusing, and snoozes alarms when there is enough IOB to cover a high BG. Uses the results of the `iob` plugin and `sens`, `target_high`, and `target_low` fields from the [treatment profile](#treatment-profile). Defaults that can be adjusted with [extended setting](#extended-settings) + * `BWP_WARN` (`0.50`) - If `BWP` is > `BWP_WARN` a warning alarm will be triggered. + * `BWP_URGENT` (`1.00`) - If `BWP` is > `BWP_URGENT` an urgent alarm will be triggered. + * `BWP_SNOOZE_MINS` (`10`) - minutes to snooze when there is enough IOB to cover a high BG. + * `BWP_SNOOZE` - (`0.10`) If BG is higher then the `target_high` and `BWP` < `BWP_SNOOZE` alarms will be snoozed for `BWP_SNOOZE_MINS`. + +##### `cage` (Cannula Age) + Calculates the number of hours since the last `Site Change` treatment that was recorded. + * `CAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming cannula change. + * `CAGE_INFO` (`44`) - If time since last `Site Change` matches `CAGE_INFO`, user will be warned of upcoming cannula change + * `CAGE_WARN` (`48`) - If time since last `Site Change` matches `CAGE_WARN`, user will be alarmed to to change the cannula + * `CAGE_URGENT` (`72`) - If time since last `Site Change` matches `CAGE_URGENT`, user will be issued a persistent warning of overdue change. + * `CAGE_DISPLAY` (`hours`) - Possible values are 'hours' or 'days'. If 'days' is selected and age of canula is greater than 24h number is displayed in days and hours + +##### `sage` (Sensor Age) + Calculates the number of days and hours since the last `Sensor Start` and `Sensor Change` treatment that was recorded. + * `SAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming sensor change. + * `SAGE_INFO` (`144`) - If time since last sensor event matches `SAGE_INFO`, user will be warned of upcoming sensor change + * `SAGE_WARN` (`164`) - If time since last sensor event matches `SAGE_WARN`, user will be alarmed to to change/restart the sensor + * `SAGE_URGENT` (`166`) - If time since last sensor event matches `SAGE_URGENT`, user will be issued a persistent warning of overdue change. + +##### `iage` (Insulin Age) + Calculates the number of days and hours since the last `Insulin Change` treatment that was recorded. + * `IAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming insulin reservoir change. + * `IAGE_INFO` (`44`) - If time since last `Insulin Change` matches `IAGE_INFO`, user will be warned of upcoming insulin reservoir change + * `IAGE_WARN` (`48`) - If time since last `Insulin Change` matches `IAGE_WARN`, user will be alarmed to to change the insulin reservoir + * `IAGE_URGENT` (`72`) - If time since last `Insulin Change` matches `IAGE_URGENT`, user will be issued a persistent warning of overdue change. + +##### `bage` (Battery Age) + Calculates the number of days and hours since the last `Pump Battery Change` treatment that was recorded. + * `BAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming pump battery change. + * `BAGE_DISPLAY` (`days`) - Set to `hours` to display time since last `Pump Battery Change` in hours only. + * `BAGE_INFO` (`312`) - If time since last `Pump Battery Change` matches `BAGE_INFO` hours, user will be warned of upcoming pump battery change (default of 312 hours is 13 days). + * `BAGE_WARN` (`336`) - If time since last `Pump Battery Change` matches `BAGE_WARN` hours, user will be alarmed to to change the pump battery (default of 336 hours is 14 days). + * `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. + * `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). - These can be disabled by setting the `DISABLE` env var, for example `DISABLE="direction upbat"` - - * `delta` (BG Delta) - Calculates and displays the change between the last 2 BG values. - * `direction` (BG Direction) - Displays the trend direction. - * `upbat` (Uploader Battery) - Displays the most recent battery status from the uploader phone. - * `errorcodes` (CGM Error Codes) - Generates alarms for CGM codes `9` (hourglass) and `10` (???). - * Use [extended settings](#extended-settings) to adjust what errorcodes trigger notifications and alarms: - * `ERRORCODES_INFO` (`1 2 3 4 5 6 7 8`) - By default the needs calibration (blood drop) and other codes below 9 generate an info level notification, set to a space separate list of number or `off` to disable - * `ERRORCODES_WARN` (`off`) - By default there are no warning configured, set to a space separate list of numbers or `off` to disable - * `ERRORCODES_URGENT` (`9 10`) - By default the hourglass and ??? generate an urgent alarm, set to a space separate list of numbers or `off` to disable - * `ar2` ([Forcasting using AR2 algorithm](https://github.com/nightscout/nightscout.github.io/wiki/Forecasting)) - Generates alarms based on forecasted values. - * Enabled by default if no thresholds are set **OR** `ALARM_TYPES` includes `predict`. - * Use [extended settings](#extended-settings) to adjust AR2 behavior: - * `AR2_USE_RAW` (`false`) - to forecast using `rawbg` values when standard values don't trigger an alarm. - * `AR2_CONE_FACTOR` (`2`) - to adjust size of cone, use `0` for a single line. - * `simplealarms` (Simple BG Alarms) - Uses `BG_HIGH`, `BG_TARGET_TOP`, `BG_TARGET_BOTTOM`, `BG_LOW` thresholds to generate alarms. - * Enabled by default if 1 of these thresholds is set **OR** `ALARM_TYPES` includes `simple`. - -#### Built-in/Example Plugins: - - * `rawbg` (Raw BG) - Calculates BG using sensor and calibration records from and displays an alternate BG values and noise levels. - * `iob` (Insulin-on-Board) - Adds the IOB pill visualization in the client and calculates values that used by other plugins. Uses treatments with insulin doses and the `dia` and `sens` fields from the [treatment profile](#treatment-profile). - * `cob` (Carbs-on-Board) - Adds the COB pill visualization in the client and calculates values that used by other plugins. Uses treatments with carb doses and the `carbs_hr`, `carbratio`, and `sens` fields from the [treatment profile](#treatment-profile). - * `bwp` (Bolus Wizard Preview) - This plugin in intended for the purpose of automatically snoozing alarms when the CGM indicates high blood sugar but there is also insulin on board (IOB) and secondly, alerting to user that it might be beneficial to measure the blood sugar using a glucometer and dosing insulin as calculated by the pump or instructed by trained medicare professionals. ***The values provided by the plugin are provided as a reference based on CGM data and insulin sensitivity you have configured, and are not intended to be used as a reference for bolus calculation.*** The plugin calculates the bolus amount when above your target, generates alarms when you should consider checking and bolusing, and snoozes alarms when there is enough IOB to cover a high BG. Uses the results of the `iob` plugin and `sens`, `target_high`, and `target_low` fields from the [treatment profile](#treatment-profile). Defaults that can be adjusted with [extended setting](#extended-settings) - * `BWP_WARN` (`0.50`) - If `BWP` is > `BWP_WARN` a warning alarm will be triggered. - * `BWP_URGENT` (`1.00`) - If `BWP` is > `BWP_URGENT` an urgent alarm will be triggered. - * `BWP_SNOOZE_MINS` (`10`) - minutes to snooze when there is enough IOB to cover a high BG. - * `BWP_SNOOZE` - (`0.10`) If BG is higher then the `target_high` and `BWP` < `BWP_SNOOZE` alarms will be snoozed for `BWP_SNOOZE_MINS`. - * `cage` (Cannula Age) - Calculates the number of hours since the last `Site Change` treatment that was recorded. - * `CAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming cannula change. - * `CAGE_INFO` (`44`) - If time since last `Site Change` matches `CAGE_INFO`, user will be warned of upcoming cannula change - * `CAGE_WARN` (`48`) - If time since last `Site Change` matches `CAGE_WARN`, user will be alarmed to to change the cannula - * `CAGE_URGENT` (`72`) - If time since last `Site Change` matches `CAGE_URGENT`, user will be issued a persistent warning of overdue change. - * `CAGE_DISPLAY` (`hours`) - Possible values are 'hours' or 'days'. If 'days' is selected and age of canula is greater than 24h number is displayed like `1.2d` - * `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). - * `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) - * `bridge` (Share2Nightscout bridge) - Glucose reading directly from the Share service, uses these extended settings: - * `BRIDGE_USER_NAME` - Your user name for the Share service. - * `BRIDGE_PASSWORD` - Your password for the Share service. - * `BRIDGE_INTERVAL` (`150000` *2.5 minutes*) - The time to wait between each update. - * `BRIDGE_MAX_COUNT` (`1`) - The maximum number of records to fetch per update. - * `BRIDGE_FIRST_FETCH_COUNT` (`3`) - Changes max count during the very first update only. - * `BRIDGE_MAX_FAILURES` (`3`) - How many failures before giving up. - * `BRIDGE_MINUTES` (`1400`) - The time window to search for new data per update (default is one day in minutes). - * `mmconnect` (MiniMed Connect bridge) - Transfer real-time MiniMed Connect data from the Medtronic CareLink server into Nightscout ([read more](https://github.com/mddub/minimed-connect-to-nightscout)) - * `MMCONNECT_USER_NAME` - Your user name for CareLink Connect. - * `MMCONNECT_PASSWORD` - Your password for CareLink Connect. - * `MMCONNECT_INTERVAL` (`60000` *1 minute*) - Number of milliseconds to wait between requests to the CareLink server. - * `MMCONNECT_MAX_RETRY_DURATION` (`32`) - Maximum number of total seconds to spend retrying failed requests before giving up. - * `MMCONNECT_SGV_LIMIT` (`24`) - Maximum number of recent sensor glucose values to send to Nightscout on each request. - * `MMCONNECT_VERBOSE` - Set this to "true" to log CareLink request information to the console. - * `MMCONNECT_STORE_RAW_DATA` - Set this to "true" to store raw data returned from CareLink as `type: "carelink_raw"` database entries (useful for development). - +##### `connect` (Nightscout Connect) + +Connect common diabetes cloud resources to Nightscout. +Include the keyword `connect` in the `ENABLE` list. +Nightscout connection uses extended settings using the environment variable prefix `CONNECT_`. + * `CONNECT_SOURCE` - The name for the source of one of the supported inputs. one of `nightscout`, `dexcomshare`, etc... +###### Nightscout + +> Work in progress + +To sync from another Nightscout site, include `CONNECT_SOURCE_ENDPOINT` and +`CONNECT_SOURCE_API_SECRET`. +* `CONNECT_SOURCE=nightscout` +* `CONNECT_SOURCE_ENDPOINT=` +* `CONNECT_SOURCE_API_SECRET=` + +The `CONNECT_SOURCE_ENDPOINT` must be a fully qualified URL and may contain a +`?token=` query string to specify an accessToken. +The `CONNECT_SOURCE_API_SECRET`, if provided, will be used to create a token +called `nightscout-connect-reader`. This information or the token provided in +the query will be used to read information from Nightscout and is optional if +the site is readable by default. + +Select this driver by setting `CONNECT_SOURCE` equal to `nightscout`. + + + +###### Dexcom Share +To synchronize from Dexcom Share use the following variables. +* `CONNECT_SOURCE=dexcomshare` +* `CONNECT_SHARE_ACCOUNT_NAME=` +* `CONNECT_SHARE_PASSWORD=` + +Optional, `CONNECT_SHARE_REGION` and `CONNECT_SHARE_SERVER` do the same thing, only specify one. +* `CONNECT_SHARE_REGION=` `ous` or `us`. `us` is the default if nothing is + provided. Selecting `us` sets `CONNECT_SHARE_SERVER` to `share2.dexcom.com`. + Selecting `ous` here sets `CONNECT_SHARE_SERVER` to `shareous1.dexcom.com`. +* `CONNECT_SHARE_SERVER=` set the server domain to use. + + +###### Glooko + +> Note: Experimental. + +To synchronize from Glooko use the following variables. +* `CONNECT_SOURCE=glooko` +* `CONNECT_GLOOKO_EMAIL=` +* `CONNECT_GLOOKO_PASSWORD=` + +By default, `CONNECT_GLOOKO_SERVER` is set to `api.glooko.com` because the +default value for `CONNECT_GLOOKO_ENV` is `default`. +* `CONNECT_GLOOKO_ENV` is the word `default` by defalt. Other values are + `development`, `production`, for `api.glooko.work`, and + `externalapi.glooko.com`, respectively. +* `CONNECT_GLOOKO_SERVER` the hostname server to use - `api.glooko.com` by `default`. + +If both, `CONNECT_GLOOKO_SERVER` and `CONNECT_GLOOKO_ENV` are set, only +`CONNECT_GLOOKO_SERVER` will be used. + +###### Libre Link Up +To synchronize from Libre Link Up use the following variables. +* `CONNECT_SOURCE=linkup` +* `CONNECT_LINK_UP_USERNAME=` +* `CONNECT_LINK_UP_PASSWORD=` + +By default, `CONNECT_LINK_UP_SERVER` is set to `api-eu.libreview.io` because the +default value for `CONNECT_LINK_UP_REGION` is `EU`. +Other available values for `CONNECT_LINK_UP_REGION`: + * `US`, `EU`, `DE`, `FR`, `JP`, `AP`, `AU`, `AE` + +For folks connected to many patients, you can provide the patient ID by setting +the `CONNECT_LINK_UP_PATIENT_ID` variable. + +###### Minimed Carelink + +To synchronize from Medtronic Minimed Carelink, set the following +environment variables. +* `CONNECT_SOURCE=minimedcarelink` +* `CONNECT_CARELINK_USERNAME` +* `CONNECT_CARELINK_PASSWORD` +* `CONNECT_CARELINK_REGION` Either `eu` to set `CONNECT_CARELINK_SERVER` to + `carelink.minimed.eu` or `us` to use `carelink.minimed.com`. + +For folks using the new Many to Many feature, please provide the username of the +patient to follow using `CONNECT_CARELINK_PATIENT_USERNAME` variable. + + +##### `bridge` (Share2Nightscout bridge) + +> **Deprecated** Please consider using the `connect` plugin instead. + +Fetch glucose reading directly from the Dexcom Share service, uses these extended settings: + * `BRIDGE_USER_NAME` - Your username for the Share service. + * `BRIDGE_PASSWORD` - Your password for the Share service. + * `BRIDGE_INTERVAL` (`150000` *2.5 minutes*) - The time (in milliseconds) to wait between each update. + * `BRIDGE_MAX_COUNT` (`1`) - The number of records to attempt to fetch per update. + * `BRIDGE_FIRST_FETCH_COUNT` (`3`) - Changes max count during the very first update only. + * `BRIDGE_MAX_FAILURES` (`3`) - How many failures before giving up. + * `BRIDGE_MINUTES` (`1400`) - The time window to search for new data per update (the default value is one day in minutes). + * `BRIDGE_SERVER` (``) - The default blank value is used to fetch data from Dexcom servers in the US. Set to (`EU`) to fetch from European servers instead. + +##### `mmconnect` (MiniMed Connect bridge) + +> **Deprecated** Please consider using the `connect` plugin instead. + + Transfer real-time MiniMed Connect data from the Medtronic CareLink server into Nightscout ([read more](https://github.com/mddub/minimed-connect-to-nightscout)) + * `MMCONNECT_USER_NAME` - Your user name for CareLink Connect. + * `MMCONNECT_PASSWORD` - Your password for CareLink Connect. + * `MMCONNECT_INTERVAL` (`60000` *1 minute*) - Number of milliseconds to wait between requests to the CareLink server. + * `MMCONNECT_MAX_RETRY_DURATION` (`32`) - Maximum number of total seconds to spend retrying failed requests before giving up. + * `MMCONNECT_SGV_LIMIT` (`24`) - Maximum number of recent sensor glucose values to send to Nightscout on each request. + * `MMCONNECT_VERBOSE` - Set this to "true" to log CareLink request information to the console. + * `MMCONNECT_STORE_RAW_DATA` - Set this to "true" to store raw data returned from CareLink as `type: "carelink_raw"` database entries (useful for development). + * `MMCONNECT_SERVER` - Set this to `EU` if you're using the European Medtronic services + +##### `pump` (Pump Monitoring) + Generic Pump Monitoring for OpenAPS, MiniMed Connect, RileyLink, t:slim, with more on the way + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + * `PUMP_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications for Pump battery and reservoir. + * `PUMP_WARN_ON_SUSPEND` (`false`) - Set to `true` to get an alarm when the pump is suspended. + * `PUMP_FIELDS` (`reservoir battery`) - The fields to display by default. Any of the following fields: `reservoir`, `battery`, `clock`, `status`, and `device` + * `PUMP_RETRO_FIELDS` (`reservoir battery clock`) - The fields to display in retro mode. Any of the above fields. + * `PUMP_WARN_CLOCK` (`30`) - The number of minutes ago that needs to be exceed before an alert is triggered. + * `PUMP_URGENT_CLOCK` (`60`) - The number of minutes ago that needs to be exceed before an urgent alarm is triggered. + * `PUMP_WARN_RES` (`10`) - The number of units remaining, a warning will be triggered when dropping below this threshold. + * `PUMP_URGENT_RES` (`5`) - The number of units remaining, an urgent alarm will be triggered when dropping below this threshold. + * `PUMP_WARN_BATT_P` (`30`) - The % of the pump battery remaining, a warning will be triggered when dropping below this threshold. + * `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: + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + * `OPENAPS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when OpenAPS isn't looping. If OpenAPS is going to offline for a period of time, you can add an `OpenAPS Offline` event for the expected duration from Careportal to avoid getting alerts. + * `OPENAPS_WARN` (`30`) - The number of minutes since the last loop that needs to be exceed before an alert is triggered + * `OPENAPS_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceed before an urgent alarm is triggered + * `OPENAPS_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display by default. Any of the following fields: `status-symbol`, `status-label`, `iob`, `meal-assist`, `freq`, and `rssi` + * `OPENAPS_RETRO_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display in retro mode. Any of the above fields. + * `OPENAPS_PRED_IOB_COLOR` (`#1e88e5`) - The color to use for IOB prediction lines. Colors can be in `#RRGGBB` format, but [other CSS color units](https://www.w3.org/TR/css-color-3/#colorunits) may be used as well. + * `OPENAPS_PRED_COB_COLOR` (`#FB8C00`) - The color to use for COB prediction lines. Same format as above. + * `OPENAPS_PRED_ACOB_COLOR` (`#FB8C00`) - The color to use for ACOB prediction lines. Same format as above. + * `OPENAPS_PRED_ZT_COLOR` (`#00d2d2`) - The color to use for ZT prediction lines. Same format as above. + * `OPENAPS_PRED_UAM_COLOR` (`#c9bd60`) - The color to use for UAM prediction lines. Same format as above. + * `OPENAPS_COLOR_PREDICTION_LINES` (`true`) - Enables / disables the colored lines vs the classic purple color. + Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker). - + +##### `loop` (Loop) + iOS Loop app monitoring, uses these extended settings: + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + * `LOOP_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when Loop isn't looping. + * `LOOP_WARN` (`30`) - The number of minutes since the last loop that needs to be exceeded before an alert is triggered + * `LOOP_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceeded before an urgent alarm is triggered + * Add `loop` to `SHOW_FORECAST` to show forecasted BG. + +For remote overrides, the following extended settings must be configured: + * `LOOP_APNS_KEY` - Apple Push Notifications service (APNs) Key, created in the Apple Developer website. + * `LOOP_APNS_KEY_ID` - The Key ID for the above key. + * `LOOP_DEVELOPER_TEAM_ID` - Your Apple developer team ID. + * `LOOP_PUSH_SERVER_ENVIRONMENT` - (optional) Set this to `production` if you are using a provisioning profile that specifies production aps-environment, such as when distributing builds via TestFlight. + +##### `override` (Override Mode) + Additional monitoring for DIY automated insulin delivery systems to display real-time overrides such as Eating Soon or Exercise Mode: + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + +##### `xdripjs` (xDrip-js) + Integrated xDrip-js monitoring, uses these extended settings: + * Requires `DEVICESTATUS_ADVANCED="true"` to be set + * `XDRIPJS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when CGM state is not OK or battery voltages fall below threshold. + * `XDRIPJS_STATE_NOTIFY_INTRVL` (`0.5`) - Set to number of hours between CGM state notifications + * `XDRIPJS_WARN_BAT_V` (`300`) - The voltage of either transmitter battery, a warning will be triggered when dropping below this threshold. + +##### `alexa` (Amazon Alexa) + Integration with Amazon Alexa, [detailed setup instructions](docs/plugins/alexa-plugin.md) + +##### `googlehome` (Google Home/DialogFLow) + Integration with Google Home (via DialogFlow), [detailed setup instructions](docs/plugins/googlehome-plugin.md) + +##### `speech` (Speech) + Speech synthesis plugin. When enabled, speaks out the blood glucose values, IOB and alarms. Note you have to set the LANGUAGE setting on the server to get all translated alarms. + +##### `cors` (CORS) + Enabled [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) so other websites can make request to your Nightscout site, uses these extended settings: + * `CORS_ALLOW_ORIGIN` (`*`) - The list of sites that are allow to make requests + +##### `dbsize` (Database Size) + Show size of Nightscout Database, as a percentage of declared available space or in MiB. + + Many deployments of Nightscout use free tier of MongoDB Atlas on Heroku, which is limited in size. After some time, as volume of stored data grows, it may happen that this limit is reached and system is unable to store new data. This plugin provides pill that indicates size of Database and shows (when configured) alarms regarding reaching space limit. + + **IMPORTANT:** This plugin can only check how much space database already takes, _but cannot infer_ max size available on server for it. To have correct alarms and realistic percentage, `DBSIZE_MAX` need to be properly set - according to your mongoDB hosting configuration. + + **NOTE:** This plugin rely on db.stats() for reporting _logical_ size of database, which may be different than _physical_ size of database on server. It may work for free tier of MongoDB on Atlas, since it calculate quota according to logical size too, but may fail for other hostings or self-hosted database with quota based on physical size. + + **NOTE:** MongoDB Atlas quota is for **all** databases in cluster, while each instance will get only size of **its own database only**. It is ok when you only have **one** database in cluster (most common scenario) but will not work for multiple parallel databases. In such case, spliting known quota equally beetween databases and setting `DBSIZE_MAX` to that fraction may help, but wont be precise. + + All sizes are expressed as integers, in _Mebibytes_ `1 MiB == 1024 KiB == 1024*1024 B` + + * `DBSIZE_MAX` (`496`) - Maximal allowed size of database on your mongoDB server, in MiB. You need to adjust that value to match your database hosting limits - default value is for standard Heroku mongoDB free tier. + * `DBSIZE_WARN_PERCENTAGE` (`60`) - Threshold to show first warning about database size. When database reach this percentage of `DBSIZE_MAX` size - pill will show size in yellow. + * `DBSIZE_URGENT_PERCENTAGE` (`75`) - Threshold to show urgent warning about database size. When database reach this percentage of `DBSIZE_MAX` size, it is urgent to do backup and clean up old data. At this percentage info pill turns red. + * `DBSIZE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications about database size. + * `DBSIZE_IN_MIB` (`false`) - Set to `true` to display size of database in MiB-s instead of default percentage. + + This plugin should be enabled by default, if needed can be diasabled by adding `dbsize` to the list of disabled plugins, for example: `DISABLE="dbsize"`. #### Extended Settings Some plugins support additional configuration using extra environment variables. These are prefixed with the name of the plugin and a `_`. For example setting `MYPLUGIN_EXAMPLE_VALUE=1234` would make `extendedSettings.exampleValue` available to the `MYPLUGIN` plugin. Plugins only have access to their own extended settings, all the extended settings of client plugins will be sent to the browser. + * `DEVICESTATUS_ADVANCED` (`true`) - Defaults to true. Users who only have a single device uploading data to Nightscout can set this to false to reduce the data use of the site. + * `DEVICESTATUS_DAYS` (`1`) - Defaults to 1, can optionally be set to 2. Users can use this to show 48 hours of device status data for in retro mode, rather than the default 24 hours. Setting this value to 2 will roughly double the bandwidth usage of nightscout, so users with a data cap may not want to update this setting. + #### Pushover In addition to the normal web based alarms, there is also support for [Pushover](https://pushover.net/) based alarms and notifications. @@ -285,7 +715,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm You’ll need to [Create a Pushover Application](https://pushover.net/apps/build). You only need to set the Application name, you can ignore all the other settings, but setting an Icon is a nice touch. Maybe you'd like to use [this one](https://raw.githubusercontent.com/nightscout/cgm-remote-monitor/master/static/images/large.png)? Pushover is configured using the following Environment Variables: - + * `ENABLE` - `pushover` should be added to the list of plugin, for example: `ENABLE="pushover"`. * `PUSHOVER_API_TOKEN` - Used to enable pushover notifications, this token is specific to the application you create from in [Pushover](https://pushover.net/), ***[additional pushover information](#pushover)*** below. * `PUSHOVER_USER_KEY` - Your Pushover user key, can be found in the top left of the [Pushover](https://pushover.net/) site, this can also be a pushover delivery group key to send to a group rather than just a single user. This also supports a space delimited list of keys. To disable `INFO` level pushes set this to `off`. @@ -297,25 +727,24 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm If you never want to get info level notifications (treatments) use `PUSHOVER_USER_KEY="off"` If you never want to get an alarm via pushover use `PUSHOVER_ALARM_KEY="off"` If you never want to get an announcement via pushover use `PUSHOVER_ANNOUNCEMENT_KEY="off"` - + If only `PUSHOVER_USER_KEY` is set it will be used for all info notifications, alarms, and announcements For testing/development try [localtunnel](http://localtunnel.me/). #### IFTTT Maker - In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Maker](https://ifttt.com/maker). - - With Maker you are able to integrate with all the other [IFTTT Channels](https://ifttt.com/channels). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more. - + In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Webhooks](https://ifttt.com/maker_webhooks). + + With Maker you are able to integrate with all the other [IFTTT Services](https://ifttt.com/services). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more. + 1. Setup IFTTT account: [login](https://ifttt.com/login) or [create an account](https://ifttt.com/join) - 2. Find your secret key on the [maker page](https://ifttt.com/maker) - 3. Configure Nightscout by setting these environment variables: - * `ENABLE` - `maker` should be added to the list of plugin, for example: `ENABLE="maker"`. - * `MAKER_KEY` - Set this to your secret key that you located in step 2, for example: `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` This also support a space delimited list of keys. - * `MAKER_ANNOUNCEMENT_KEY` - An optional Maker key, will be used for system wide user generated announcements. If not defined this will fallback to `MAKER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. - 4. [Create a recipe](https://ifttt.com/myrecipes/personal/new) or see [more detailed instructions](lib/plugins/maker-setup.md#create-a-recipe) - - Plugins can create custom events, but all events sent to maker will be prefixed with `ns-`. The core events are: + 2. Follow the [Detailed IFTTT setup Instructions](docs/plugins/maker-setup.md) + 3. Configure Nightscout by setting these webpage environment variables: + * `ENABLE` - `maker` should be added to the list of plugins, for example: `ENABLE="maker"`. + * `MAKER_KEY` - Set this to your secret key (see [[Detailed Instructions](docs/plugins/maker-setup.md) ) `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` This also supports a space delimited list of keys. + * `MAKER_ANNOUNCEMENT_KEY` - An optional Maker key, will be used for system wide user generated announcements. If not defined this will fallback to `MAKER_KEY`. A possible use for this is sending important messages and alarms to another device that you don't want to send all notification too. This also support a space delimited list of keys. + + Plugins can create custom events, but all events sent to IFTTT webhooks will be prefixed with `ns-`. The core events are: * `ns-event` - This event is sent to the maker service for all alarms and notifications. This is good catch all event for general logging. * `ns-allclear` - This event is sent to the maker service when an alarm has been ack'd or when the server starts up without triggering any alarms. For example, you could use this event to turn a light to green. * `ns-info` - Plugins that generate notifications at the info level will cause this event to also be triggered. It will be sent in addition to `ns-event`. @@ -326,11 +755,11 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm ### Treatment Profile Some of the [plugins](#plugins) make use of a treatment profile that can be edited using the Profile Editor, see the link in the Settings drawer on your site. - + Treatment Profile Fields: * `timezone` (Time Zone) - time zone local to the patient. *Should be set.* - * `units` (Profile Units) - blood glucose units used in the profile, either "mgdl" or "mmol" + * `units` (Profile Units) - blood glucose units used in the profile, either "mg/dl" or "mmol" * `dia` (Insulin duration) - value should be the duration of insulin action to use in calculating how much insulin is left active. Defaults to 3 hours. * `carbs_hr` (Carbs per Hour) - The number of carbs that are processed per hour, for more information see [#DIYPS](http://diyps.org/2014/05/29/determining-your-carbohydrate-absorption-rate-diyps-lessons-learned/). * `carbratio` (Carb Ratio) - grams per unit of insulin. @@ -338,8 +767,8 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm * `basal` The basal rate set on the pump. * `target_high` - Upper target for correction boluses. * `target_low` - Lower target for correction boluses. - - Some example profiles are [here](example-profiles.md). + + Some example profiles are [here](docs/plugins/example-profiles.md). ## Setting environment variables Easy to emulate on the commandline: @@ -351,7 +780,7 @@ Easy to emulate on the commandline: From now on you can run using ```bash - $ env $(cat my.env) PORT=1337 node server.js + $ (eval $(cat my.env | sed 's/^/export /') && PORT=1337 node server.js) ``` Your hosting provider probably has a way to set these through their GUI. @@ -359,13 +788,13 @@ Your hosting provider probably has a way to set these through their GUI. ### Vagrant install Optionally, use [Vagrant](https://www.vagrantup.com/) with the -included `Vagrantfile` and `setup.sh` to install OS and node packages to +included `Vagrantfile` and `bin/setup.sh` to install OS and node packages to a virtual machine. ```bash host$ vagrant up host$ vagrant ssh -vm$ setup.sh +vm$ ./bin/setup.sh ``` The setup script will install OS packages then run `npm install`. @@ -381,13 +810,21 @@ Feel free to [post an issue][issues], but read the [wiki][wiki] first. [issues]: https://github.com/nightscout/cgm-remote-monitor/issues [wiki]: https://github.com/nightscout/cgm-remote-monitor/wiki +### Browser testing suite provided by +[![BrowserStack][browserstack-img]][browserstack-url] + +[browserstack-img]: /static/images/browserstack-logo.png +[browserstack-url]: https://www.browserstack.com/ + License --------------- [agpl-3]: http://www.gnu.org/licenses/agpl-3.0.txt cgm-remote-monitor - web app to broadcast cgm readings - Copyright (C) 2015 The Nightscout Foundation, http://www.nightscoutfoundation.org. + Copyright (C) 2017 Nightscout contributors. See the COPYRIGHT file + at the root directory of this distribution and at + https://github.com/nightscout/cgm-remote-monitor/blob/master/COPYRIGHT This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published diff --git a/app.js b/app.js deleted file mode 100644 index fa807b81a5d..00000000000 --- a/app.js +++ /dev/null @@ -1,55 +0,0 @@ - -var express = require('express'); -var compression = require('compression'); -function create (env, ctx) { - /////////////////////////////////////////////////// - // api and json object variables - /////////////////////////////////////////////////// - var api = require('./lib/api/')(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(compression({filter: function shouldCompress(req, res) { - //TODO: return false here if we find a condition where we don't want to compress - // fallback to standard filter function - return compression.filter(req, res); - }})); - - //if (env.api_secret) { - // console.log("API_SECRET", env.api_secret); - //} - app.use('/api/v1', api); - - - // pebble data - app.get('/pebble', ctx.pebble); - - // expose swagger.yaml - app.get('/swagger.yaml', function (req, res) { - res.sendFile(__dirname + '/swagger.yaml'); - }); - - //app.get('/package.json', software); - - // define static server - //TODO: JC - changed cache to 1 hour from 30d ays to bypass cache hell until we have a real solution - var staticFiles = express.static(env.static_files, {maxAge: 60 * 60 * 1000}); - - // serve the static content - app.use(staticFiles); - - var bundle = require('./bundle')(); - app.use(bundle); - - // Handle errors with express's errorhandler, to display more readable error messages. - var errorhandler = require('errorhandler'); - //if (process.env.NODE_ENV === 'development') { - app.use(errorhandler()); - //} - return app; -} -module.exports = create; - diff --git a/app.json b/app.json index c5a6417eed5..7bf58f03247 100644 --- a/app.json +++ b/app.json @@ -2,169 +2,163 @@ "name": "CGM Remote Monitor", "repository": "https://github.com/nightscout/cgm-remote-monitor", "env": { - "MONGO_COLLECTION": { - "description": "REQUIRED: The mongo collection used for CGM data. Default value is 'entries'. Most users should use the default.", - "value": "entries", - "required": true - }, - "API_SECRET": { - "description": "REQUIRED: A secret passphrase that must be at least 12 characters long, required to enable POST and PUT; also required for the Care Portal", + "MONGODB_URI": { + "description": "The MongoDB Connection String to connect to your MongoDB cluster. If you don't have this from MongoDB Atlas, please re-read installation instructions at http://nightscout.github.io/nightscout/new_user/ before continuing", "value": "", "required": true }, - "DISPLAY_UNITS": { - "description": "Choices: mg/dl and mmol. Setting to mmol puts the entire server into mmol mode by default, no further settings needed.", - "value": "", + "ALARM_HIGH": { + "description": "Default setting for new browser views, for the High alarm (triggered when BG crosses BG_TARGET_TOP). ('on' or 'off')", + "value": "on", "required": false }, - "ENABLE": { - "description": "Used to enable optional features, expects a space delimited list, such as: careportal rawbg iob, see https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md for more info", - "value": "", + "ALARM_LOW": { + "description": "Default setting for new browser views, for the Low alarm (triggered when BG crosses BG_TARGET_BOTTOM). ('on' or 'off')", + "value": "on", "required": false }, - "DISABLE": { - "description": "Used to disable default features, expects a space delimited list, such as: direction upbat, see https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md for more info", - "value": "", + "ALARM_TIMEAGO_URGENT": { + "description": "Default setting for new browser views, for an urgent alarm when CGM data hasn't been received in the number of minutes set in ALARM_TIMEAGO_URGENT_MINS. ('on' or 'off')", + "value": "on", + "required": false + }, + "ALARM_TIMEAGO_URGENT_MINS": { + "description": "Default setting for new browser views, for the number of minutes since the last CGM reading to trigger an ALARM_TIMEAGO_URGENT alarm.", + "value": "30", + "required": false + }, + "ALARM_TIMEAGO_WARN": { + "description": "Default setting for new browser views, for a warning alarm when CGM data hasn't been received in the number of minutes set in ALARM_TIMEAGO_WARN_MINS. ('on' or 'off')", + "value": "on", + "required": false + }, + "ALARM_TIMEAGO_WARN_MINS": { + "description": "Default setting for new browser views, for the number of minutes since the last CGM reading to trigger an ALARM_TIMEAGO_WARN alarm.", + "value": "15", "required": false }, "ALARM_TYPES": { - "description": "Alarm behavior currently 2 alarm types are supported simple and predict, and can be used independently or combined. The simple alarm type only compares the current BG to BG_ thresholds above, the predict alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. predict DOES NOT currently use any of the BG_* ENV's", - "value": "", + "description": "'simple' and/or 'predict'. Simple alarms trigger when BG crosses the various thresholds set below. Predict alarms use a formula that forecasts where the BG is going based on its trend. You will *not* get warnings when crossing the BG thresholds set below when using the predict type.", + "value": "simple", "required": false }, - "BG_HIGH": { - "description": "Urgent high BG alarm. Default null value implies 260. Must be set in mg/dL. Only used with simple alarms.", - "value": "", + "ALARM_URGENT_HIGH": { + "description": "Default setting for new browser views, for the Urgent High alarm (triggered when BG crosses BG_HIGH). ('on' or 'off')", + "value": "on", + "required": false + }, + "ALARM_URGENT_LOW": { + "description": "Default setting for new browser views, for the Urgent Low alarm (triggered when BG crosses BG_LOW). ('on' or 'off')", + "value": "on", "required": false }, - "BG_TARGET_TOP": { - "description": "Non-urgent high BG alarm, the top of your target range. Default null value implies 180. Must be set in mg/dL. Only used with simple alarms.", + "API_SECRET": { + "description": "A passphrase that must be at least 12 characters long. Avoid 'special' characters, which can cause problems in some cases.", "value": "", + "required": true + }, + "BG_HIGH": { + "description": "Urgent High BG threshold, triggers the ALARM_URGENT_HIGH alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", + "value": "260", + "required": false + }, + "BG_LOW": { + "description": "Urgent Low BG threshold, triggers the ALARM_URGENT_LOW alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", + "value": "55", "required": false }, "BG_TARGET_BOTTOM": { - "description": "Non urgent low BG alarm, the bottom of your target range. Default null value implies 80. Must be set in mg/dL. Only used with simple alarms.", - "value": "", + "description": "Low BG threshold, triggers the ALARM_LOW alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", + "value": "80", "required": false }, - "BG_LOW": { - "description": "Urgent Low BG alarm. Default null value implies 55. Must be set in mg/dL. Only used with simple alarms.", - "value": "", + "BG_TARGET_TOP": { + "description": "High BG threshold, triggers the ALARM_HIGH alarm. Set in mg/dL or mmol/L, as set in DISPLAY_UNITS variable.", + "value": "180", "required": false }, - "PUSHOVER_API_TOKEN": { - "description": "Pushover API token, required for Pushover notifications. Leave blank if not using Pushover.", - "value": "", + "BOLUS_RENDER_OVER": { + "description": "U value over which the bolus values are rendered on the chart if the 'x U and Over' option is selected.", + "value": "1", "required": false }, - "PUSHOVER_USER_KEY": { - "description": "Pushover user key, required for Pushover notifications. Leave blank if not using Pushover.", + "BRIDGE_PASSWORD": { + "description": "Your Dexcom account password, to receive CGM data from the Dexcom Share service. Also make sure to include 'bridge' in your ENABLE line.", "value": "", "required": false }, - "PUSHOVER_ANNOUNCEMENT_KEY": { - "description": "An optional Pushover user/group key, will be used for system wide user generated announcements. If not defined this will fallback to `PUSHOVER_USER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. Leave blank if not using Pushover", - "value": "", + "BRIDGE_SERVER": { + "description": "If you are bridging from the Dexcom Share service, and are anywhere *outside* the US, change this to EU. ('US' or 'EU')", + "value": "US", "required": false }, - "CUSTOM_TITLE": { - "description": "Customize the name of the website, usually the name of T1.", + "BRIDGE_USER_NAME": { + "description": "Your Dexcom account username, to receive CGM data from the Dexcom Share service. Also make sure to include 'bridge' in your ENABLE line.", "value": "", "required": false }, - "THEME": { - "description": "Possible values default or colors", + "CUSTOM_TITLE": { + "description": "The display name for the Nightscout site. Appears in the upper left of the main view. Often set to the name of the CGM wearer.", "value": "", "required": false }, - "SHOW_RAWBG": { - "description": "Possible values always, never or noise", - "value": "", + "DISPLAY_UNITS": { + "description": "Preferred BG units for the site: 'mg/dl' or 'mmol/L' (or just 'mmol').", + "value": "mg/dl", + "required": true + }, + "ENABLE": { + "description": "Plugins to enable for your site. Must be a space-delimited, lower-case list. Include the word 'bridge' here if you are receiving data from the Dexcom Share service. Include 'mmconnect' if you are bridging from the MiniMed CareLink service.", + "value": "careportal basal dbsize", "required": false }, - "BRIDGE_USER_NAME": { - "description": "Share bridge - Your user name for the Share service. ENSURE bridge is in ENABLE if you want to use the share bridge", + "MMCONNECT_USER_NAME": { + "description": "Your CareLink account username, to receive CGM data from the CareLink service. Also make sure to include 'mmconnect' in your ENABLE line.", "value": "", "required": false }, - "BRIDGE_PASSWORD": { - "description": "Share bridge - Your password for the Share service. ENSURE bridge is in ENABLE if you want to use the share bridge", + "MMCONNECT_PASSWORD": { + "description": "Your CareLink account password, to receive CGM data from the CareLink service. Also make sure to include 'mmconnect' in your ENABLE line.", "value": "", "required": false }, - "TIME_FORMAT": { - "description": "Browser default time mode valid settings are 12 or 24", - "value": "12", - "required": false + "MMCONNECT_SERVER": { + "description": "If you are bridging from the CareLink service, and are anywhere *outside* the US, change this to EU. ('US' or 'EU')", + "value": "US", + "required": false }, "NIGHT_MODE": { - "description": "Browser defaults to night mode valid settings are on or off", + "description": "Default setting for new browser views, for whether Night Mode should be enabled. ('on' or 'off')", "value": "off", "required": false }, + "SHOW_PLUGINS": { + "description": "Default setting for whether or not these plugins are checked (active) by default, not merely enabled. Include plugins here as in the ENABLE line; space-separated and lower-case.", + "value": "careportal dbsize", + "required": false + }, "SHOW_RAWBG": { - "description": "Browser default raw display mode vaild settings are always never or noise", + "description": "Default setting for new browser views, for the display of raw CGM data (if available). ('always', 'never', or 'noise')", "value": "never", "required": false }, "THEME": { - "description": "Browser default theme setting vaild settings are default or colors", - "value": "default", + "description": "Default setting for new browser views, for the color theme of the CGM graph. ('default', 'colors', or 'colorblindfriendly')", + "value": "colors", "required": false }, - "ALARM_URGENT_HIGH": { - "description": "Browser default urgent high alarm enabled vaild settings are on or off", - "value": "on", + "TIME_FORMAT": { + "description": "Default setting for new browser views, for the time mode. ('12' or '24')", + "value": "12", "required": false }, - "ALARM_HIGH": { - "description": "Browser default high alarm enabled vaild settings are on or off", - "value": "on", - "required": false - }, - "ALARM_LOW": { - "description": "Browser default low alarm enabled vaild settings are on or off", - "value": "on", - "required": false - }, - "ALARM_URGENT_LOW": { - "description": "Browser default urgent low alarm enabled vaild settings are on or off", - "value": "on", - "required": false - }, - "ALARM_TIMEAGO_WARN": { - "description": "Browser default warn after time of last data exceeds ALARM_TIMEAGO_WARN_MINS alarm enabled vaild settings are on or off", - "value": "on", - "required": false - }, - "ALARM_TIMEAGO_WARN_MINS": { - "description": "Browser default minutes since the last reading to trigger a warning", - "value": "15", - "required": false - }, - "ALARM_TIMEAGO_URGENT": { - "description": "Browser default urgent warning after time of last data exceeds ALARM_TIMEAGO_URGENT_MINS alarm enabled vaild settings are on or off", - "value": "on", - "required": false - }, - "ALARM_TIMEAGO_URGENT_MINS": { - "description": "Browser default minutes since last reading to trigger an urgent alarm", - "value": "30", - "required": false - }, - "MAKER_KEY": { - "description": "Maker Key - Set this to your secret key Note for additional info see https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#ifttt-maker , maker should be added to enable if you want to use maker, Leave blank if not using maker", - "value": "", - "required": false - }, - "MAKER_ANNOUNCEMENT_KEY": { - "description": "Maker Announcement Key - Set this to your secret key for announcements Note for additional info see https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#ifttt-maker , maker should be added to enable if you want to use maker Leave blank if not using maker", - "value": "", - "required": false + "USE_NPM_INSTALL": { + "description": "You need to have this set for deployment to work in Heroku", + "value": "true", + "required": true } }, "addons": [ - "mongolab:sandbox", "papertrail" ] } diff --git a/assets/fonts/Nightscout Plugin Icons.json b/assets/fonts/Nightscout Plugin Icons.json new file mode 100644 index 00000000000..f1ff8306de0 --- /dev/null +++ b/assets/fonts/Nightscout Plugin Icons.json @@ -0,0 +1,11517 @@ +{ + "metadata": { + "name": "Nightscout Plugin Icons", + "lastOpened": 0, + "created": 1607277376360 + }, + "iconSets": [ + { + "selection": [ + { + "ligatures": "home, house", + "name": "home", + "id": 0, + "order": 0 + }, + { + "ligatures": "home2, house2", + "name": "home2", + "id": 1, + "order": 0 + }, + { + "ligatures": "home3, house3", + "name": "home3", + "id": 2, + "order": 0 + }, + { + "ligatures": "office, buildings", + "name": "office", + "id": 3, + "order": 0 + }, + { + "ligatures": "newspaper, news", + "name": "newspaper", + "id": 4, + "order": 0 + }, + { + "ligatures": "pencil, write", + "name": "pencil", + "id": 5, + "order": 0 + }, + { + "ligatures": "pencil2, write2", + "name": "pencil2", + "id": 6, + "order": 0 + }, + { + "ligatures": "quill, feather", + "name": "quill", + "id": 7, + "order": 0 + }, + { + "ligatures": "pen, write3", + "name": "pen", + "id": 8, + "order": 0 + }, + { + "ligatures": "blog, pen2", + "name": "blog", + "id": 9, + "order": 0 + }, + { + "ligatures": "eyedropper, color", + "name": "eyedropper", + "id": 10, + "order": 0 + }, + { + "ligatures": "droplet, color2", + "name": "droplet", + "id": 11, + "order": 0 + }, + { + "ligatures": "paint-format, format", + "name": "paint-format", + "id": 12, + "order": 0 + }, + { + "ligatures": "image, picture", + "name": "image", + "id": 13, + "order": 0 + }, + { + "ligatures": "images, pictures", + "name": "images", + "id": 14, + "order": 0 + }, + { + "ligatures": "camera, photo", + "name": "camera", + "id": 15, + "order": 0 + }, + { + "ligatures": "headphones, headset", + "name": "headphones", + "id": 16, + "order": 0 + }, + { + "ligatures": "music, song", + "name": "music", + "id": 17, + "order": 0 + }, + { + "ligatures": "play, video", + "name": "play", + "id": 18, + "order": 0 + }, + { + "ligatures": "film, video2", + "name": "film", + "id": 19, + "order": 0 + }, + { + "ligatures": "video-camera, video3", + "name": "video-camera", + "id": 20, + "order": 0 + }, + { + "ligatures": "dice, game", + "name": "dice", + "id": 21, + "order": 0 + }, + { + "ligatures": "pacman, game2", + "name": "pacman", + "id": 22, + "order": 0 + }, + { + "ligatures": "spades, cards", + "name": "spades", + "id": 23, + "order": 0 + }, + { + "ligatures": "clubs, cards2", + "name": "clubs", + "id": 24, + "order": 0 + }, + { + "ligatures": "diamonds, cards3", + "name": "diamonds", + "id": 25, + "order": 0 + }, + { + "ligatures": "bullhorn, megaphone", + "name": "bullhorn", + "id": 26, + "order": 3, + "prevSize": 32, + "code": 59674, + "tempChar": "" + }, + { + "ligatures": "connection, wifi", + "name": "connection", + "id": 27, + "order": 0 + }, + { + "ligatures": "podcast, broadcast", + "name": "podcast", + "id": 28, + "order": 0 + }, + { + "ligatures": "feed, wave", + "name": "feed", + "id": 29, + "order": 0 + }, + { + "ligatures": "mic, microphone", + "name": "mic", + "id": 30, + "order": 0 + }, + { + "ligatures": "book, read", + "name": "book", + "id": 31, + "order": 0 + }, + { + "ligatures": "books, library", + "name": "books", + "id": 32, + "order": 0 + }, + { + "ligatures": "library2, bank", + "name": "library", + "id": 33, + "order": 0 + }, + { + "ligatures": "file-text, file", + "name": "file-text", + "id": 34, + "order": 0 + }, + { + "ligatures": "profile, file2", + "name": "profile", + "id": 35, + "order": 0 + }, + { + "ligatures": "file-empty, file3", + "name": "file-empty", + "id": 36, + "order": 0 + }, + { + "ligatures": "files-empty, files", + "name": "files-empty", + "id": 37, + "order": 0 + }, + { + "ligatures": "file-text2, file4", + "name": "file-text2", + "id": 38, + "order": 0 + }, + { + "ligatures": "file-picture, file5", + "name": "file-picture", + "id": 39, + "order": 0 + }, + { + "ligatures": "file-music, file6", + "name": "file-music", + "id": 40, + "order": 0 + }, + { + "ligatures": "file-play, file7", + "name": "file-play", + "id": 41, + "order": 0 + }, + { + "ligatures": "file-video, file8", + "name": "file-video", + "id": 42, + "order": 0 + }, + { + "ligatures": "file-zip, file9", + "name": "file-zip", + "id": 43, + "order": 0 + }, + { + "ligatures": "copy, duplicate", + "name": "copy", + "id": 44, + "order": 0 + }, + { + "ligatures": "paste, clipboard-file", + "name": "paste", + "id": 45, + "order": 0 + }, + { + "ligatures": "stack, layers", + "name": "stack", + "id": 46, + "order": 0 + }, + { + "ligatures": "folder, directory", + "name": "folder", + "id": 47, + "order": 0 + }, + { + "ligatures": "folder-open, directory2", + "name": "folder-open", + "id": 48, + "order": 0 + }, + { + "ligatures": "folder-plus, directory3", + "name": "folder-plus", + "id": 49, + "order": 0 + }, + { + "ligatures": "folder-minus, directory4", + "name": "folder-minus", + "id": 50, + "order": 0 + }, + { + "ligatures": "folder-download, directory5", + "name": "folder-download", + "id": 51, + "order": 0 + }, + { + "ligatures": "folder-upload, directory6", + "name": "folder-upload", + "id": 52, + "order": 0 + }, + { + "ligatures": "price-tag", + "name": "price-tag", + "id": 53, + "order": 0 + }, + { + "ligatures": "price-tags", + "name": "price-tags", + "id": 54, + "order": 0 + }, + { + "ligatures": "barcode", + "name": "barcode", + "id": 55, + "order": 0 + }, + { + "ligatures": "qrcode", + "name": "qrcode", + "id": 56, + "order": 0 + }, + { + "ligatures": "ticket, theater", + "name": "ticket", + "id": 57, + "order": 0 + }, + { + "ligatures": "cart, purchase", + "name": "cart", + "id": 58, + "order": 0 + }, + { + "ligatures": "coin-dollar, money", + "name": "coin-dollar", + "id": 59, + "order": 0 + }, + { + "ligatures": "coin-euro, money2", + "name": "coin-euro", + "id": 60, + "order": 0 + }, + { + "ligatures": "coin-pound, money3", + "name": "coin-pound", + "id": 61, + "order": 0 + }, + { + "ligatures": "coin-yen, money4", + "name": "coin-yen", + "id": 62, + "order": 0 + }, + { + "ligatures": "credit-card, money5", + "name": "credit-card", + "id": 63, + "order": 0 + }, + { + "ligatures": "calculator, compute", + "name": "calculator", + "id": 64, + "order": 0 + }, + { + "ligatures": "lifebuoy, support", + "name": "lifebuoy", + "id": 65, + "order": 0 + }, + { + "ligatures": "phone, telephone", + "name": "phone", + "id": 66, + "order": 0 + }, + { + "ligatures": "phone-hang-up, telephone2", + "name": "phone-hang-up", + "id": 67, + "order": 0 + }, + { + "ligatures": "address-book, contact", + "name": "address-book", + "id": 68, + "order": 0 + }, + { + "ligatures": "envelop, mail", + "name": "envelop", + "id": 69, + "order": 0 + }, + { + "ligatures": "pushpin, pin", + "name": "pushpin", + "id": 70, + "order": 0 + }, + { + "ligatures": "location, map-marker", + "name": "location", + "id": 71, + "order": 0 + }, + { + "ligatures": "location2, map-marker2", + "name": "location2", + "id": 72, + "order": 0 + }, + { + "ligatures": "compass, direction", + "name": "compass", + "id": 73, + "order": 0 + }, + { + "ligatures": "compass2, direction2", + "name": "compass2", + "id": 74, + "order": 0 + }, + { + "ligatures": "map, guide", + "name": "map", + "id": 75, + "order": 0 + }, + { + "ligatures": "map2, guide2", + "name": "map2", + "id": 76, + "order": 0 + }, + { + "ligatures": "history, time", + "name": "history", + "id": 77, + "order": 0 + }, + { + "ligatures": "clock, time2", + "name": "clock", + "id": 78, + "order": 0 + }, + { + "ligatures": "clock2, time3", + "name": "clock2", + "id": 79, + "order": 0 + }, + { + "ligatures": "alarm, time4", + "name": "alarm", + "id": 80, + "order": 0 + }, + { + "ligatures": "bell, alarm2", + "name": "bell", + "id": 81, + "order": 0 + }, + { + "ligatures": "stopwatch, time5", + "name": "stopwatch", + "id": 82, + "order": 0 + }, + { + "ligatures": "calendar, date", + "name": "calendar", + "id": 83, + "order": 0 + }, + { + "ligatures": "printer, print", + "name": "printer", + "id": 84, + "order": 0 + }, + { + "ligatures": "keyboard, typing", + "name": "keyboard", + "id": 85, + "order": 0 + }, + { + "ligatures": "display, screen", + "name": "display", + "id": 86, + "order": 0 + }, + { + "ligatures": "laptop, computer", + "name": "laptop", + "id": 87, + "order": 0 + }, + { + "ligatures": "mobile, cell-phone", + "name": "mobile", + "id": 88, + "order": 0 + }, + { + "ligatures": "mobile2, cell-phone2", + "name": "mobile2", + "id": 89, + "order": 0 + }, + { + "ligatures": "tablet, mobile3", + "name": "tablet", + "id": 90, + "order": 0 + }, + { + "ligatures": "tv, television", + "name": "tv", + "id": 91, + "order": 0 + }, + { + "ligatures": "drawer, box", + "name": "drawer", + "id": 92, + "order": 0 + }, + { + "ligatures": "drawer2, box2", + "name": "drawer2", + "id": 93, + "order": 0 + }, + { + "ligatures": "box-add, box3", + "name": "box-add", + "id": 94, + "order": 0 + }, + { + "ligatures": "box-remove, box4", + "name": "box-remove", + "id": 95, + "order": 0 + }, + { + "ligatures": "download, save", + "name": "download", + "id": 96, + "order": 0 + }, + { + "ligatures": "upload, load", + "name": "upload", + "id": 97, + "order": 0 + }, + { + "ligatures": "floppy-disk, save2", + "name": "floppy-disk", + "id": 98, + "order": 0 + }, + { + "ligatures": "drive, save3", + "name": "drive", + "id": 99, + "order": 0 + }, + { + "ligatures": "database, db", + "name": "database", + "id": 100, + "order": 0 + }, + { + "ligatures": "undo, ccw", + "name": "undo", + "id": 101, + "order": 0 + }, + { + "ligatures": "redo, cw", + "name": "redo", + "id": 102, + "order": 0 + }, + { + "ligatures": "undo2, left", + "name": "undo2", + "id": 103, + "order": 0 + }, + { + "ligatures": "redo2, right", + "name": "redo2", + "id": 104, + "order": 0 + }, + { + "ligatures": "forward, right2", + "name": "forward", + "id": 105, + "order": 0 + }, + { + "ligatures": "reply, left2", + "name": "reply", + "id": 106, + "order": 0 + }, + { + "ligatures": "bubble, comment", + "name": "bubble", + "id": 107, + "order": 0 + }, + { + "ligatures": "bubbles, comments", + "name": "bubbles", + "id": 108, + "order": 0 + }, + { + "ligatures": "bubbles2, comments2", + "name": "bubbles2", + "id": 109, + "order": 0 + }, + { + "ligatures": "bubble2, comment2", + "name": "bubble2", + "id": 110, + "order": 0 + }, + { + "ligatures": "bubbles3, comments3", + "name": "bubbles3", + "id": 111, + "order": 0 + }, + { + "ligatures": "bubbles4, comments4", + "name": "bubbles4", + "id": 112, + "order": 0 + }, + { + "ligatures": "user, profile2", + "name": "user", + "id": 113, + "order": 0 + }, + { + "ligatures": "users, group", + "name": "users", + "id": 114, + "order": 0 + }, + { + "ligatures": "user-plus, user2", + "name": "user-plus", + "id": 115, + "order": 0 + }, + { + "ligatures": "user-minus, user3", + "name": "user-minus", + "id": 116, + "order": 0 + }, + { + "ligatures": "user-check, user4", + "name": "user-check", + "id": 117, + "order": 0 + }, + { + "ligatures": "user-tie, user5", + "name": "user-tie", + "id": 118, + "order": 0 + }, + { + "ligatures": "quotes-left, ldquo", + "name": "quotes-left", + "id": 119, + "order": 0 + }, + { + "ligatures": "quotes-right, rdquo", + "name": "quotes-right", + "id": 120, + "order": 0 + }, + { + "ligatures": "hour-glass, loading", + "name": "hour-glass", + "id": 121, + "order": 0 + }, + { + "ligatures": "spinner, loading2", + "name": "spinner", + "id": 122, + "order": 0 + }, + { + "ligatures": "spinner2, loading3", + "name": "spinner2", + "id": 123, + "order": 0 + }, + { + "ligatures": "spinner3, loading4", + "name": "spinner3", + "id": 124, + "order": 0 + }, + { + "ligatures": "spinner4, loading5", + "name": "spinner4", + "id": 125, + "order": 0 + }, + { + "ligatures": "spinner5, loading6", + "name": "spinner5", + "id": 126, + "order": 0 + }, + { + "ligatures": "spinner6, loading7", + "name": "spinner6", + "id": 127, + "order": 0 + }, + { + "ligatures": "spinner7, loading8", + "name": "spinner7", + "id": 128, + "order": 0 + }, + { + "ligatures": "spinner8, loading9", + "name": "spinner8", + "id": 129, + "order": 0 + }, + { + "ligatures": "spinner9, loading10", + "name": "spinner9", + "id": 130, + "order": 0 + }, + { + "ligatures": "spinner10, loading11", + "name": "spinner10", + "id": 131, + "order": 0 + }, + { + "ligatures": "spinner11, loading12", + "name": "spinner11", + "id": 132, + "order": 0 + }, + { + "ligatures": "binoculars, lookup", + "name": "binoculars", + "id": 133, + "order": 0 + }, + { + "ligatures": "search, magnifier", + "name": "search", + "id": 134, + "order": 0 + }, + { + "ligatures": "zoom-in, magnifier2", + "name": "zoom-in", + "id": 135, + "order": 0 + }, + { + "ligatures": "zoom-out, magnifier3", + "name": "zoom-out", + "id": 136, + "order": 0 + }, + { + "ligatures": "enlarge, expand", + "name": "enlarge", + "id": 137, + "order": 0 + }, + { + "ligatures": "shrink, collapse", + "name": "shrink", + "id": 138, + "order": 0 + }, + { + "ligatures": "enlarge2, expand2", + "name": "enlarge2", + "id": 139, + "order": 0 + }, + { + "ligatures": "shrink2, collapse2", + "name": "shrink2", + "id": 140, + "order": 0 + }, + { + "ligatures": "key, password", + "name": "key", + "id": 141, + "order": 0 + }, + { + "ligatures": "key2, password2", + "name": "key2", + "id": 142, + "order": 0 + }, + { + "ligatures": "lock, secure", + "name": "lock", + "id": 143, + "order": 0 + }, + { + "ligatures": "unlocked, lock-open", + "name": "unlocked", + "id": 144, + "order": 0 + }, + { + "ligatures": "wrench, tool", + "name": "wrench", + "id": 145, + "order": 0 + }, + { + "ligatures": "equalizer, sliders", + "name": "equalizer", + "id": 146, + "order": 0 + }, + { + "ligatures": "equalizer2, sliders2", + "name": "equalizer2", + "id": 147, + "order": 0 + }, + { + "ligatures": "cog, gear", + "name": "cog", + "id": 148, + "order": 0 + }, + { + "ligatures": "cogs, gears", + "name": "cogs", + "id": 149, + "order": 0 + }, + { + "ligatures": "hammer, tool2", + "name": "hammer", + "id": 150, + "order": 0 + }, + { + "ligatures": "magic-wand, wizard", + "name": "magic-wand", + "id": 151, + "order": 0 + }, + { + "ligatures": "aid-kit, health", + "name": "aid-kit", + "id": 152, + "order": 0 + }, + { + "ligatures": "bug, virus", + "name": "bug", + "id": 153, + "order": 0 + }, + { + "ligatures": "pie-chart, stats", + "name": "pie-chart", + "id": 154, + "order": 0 + }, + { + "ligatures": "stats-dots, stats2", + "name": "stats-dots", + "id": 155, + "order": 0 + }, + { + "ligatures": "stats-bars, stats3", + "name": "stats-bars", + "id": 156, + "order": 0 + }, + { + "ligatures": "stats-bars2, stats4", + "name": "stats-bars2", + "id": 157, + "order": 0 + }, + { + "ligatures": "trophy, cup", + "name": "trophy", + "id": 158, + "order": 0 + }, + { + "ligatures": "gift, present", + "name": "gift", + "id": 159, + "order": 0 + }, + { + "ligatures": "glass, drink", + "name": "glass", + "id": 160, + "order": 0 + }, + { + "ligatures": "glass2, drink2", + "name": "glass2", + "id": 161, + "order": 0 + }, + { + "ligatures": "mug, drink3", + "name": "mug", + "id": 162, + "order": 0 + }, + { + "ligatures": "spoon-knife, food", + "name": "spoon-knife", + "id": 163, + "order": 0 + }, + { + "ligatures": "leaf, nature", + "name": "leaf", + "id": 164, + "order": 0 + }, + { + "ligatures": "rocket, jet", + "name": "rocket", + "id": 165, + "order": 0 + }, + { + "ligatures": "meter, gauge", + "name": "meter", + "id": 166, + "order": 0 + }, + { + "ligatures": "meter2, gauge2", + "name": "meter2", + "id": 167, + "order": 0 + }, + { + "ligatures": "hammer2, gavel", + "name": "hammer2", + "id": 168, + "order": 0 + }, + { + "ligatures": "fire, flame", + "name": "fire", + "id": 169, + "order": 0 + }, + { + "ligatures": "lab, beta", + "name": "lab", + "id": 170, + "order": 0 + }, + { + "ligatures": "magnet, attract", + "name": "magnet", + "id": 171, + "order": 0 + }, + { + "ligatures": "bin, trashcan", + "name": "bin", + "id": 172, + "order": 0 + }, + { + "ligatures": "bin2, trashcan2", + "name": "bin2", + "id": 173, + "order": 0 + }, + { + "ligatures": "briefcase, portfolio", + "name": "briefcase", + "id": 174, + "order": 0 + }, + { + "ligatures": "airplane, travel", + "name": "airplane", + "id": 175, + "order": 0 + }, + { + "ligatures": "truck, transit", + "name": "truck", + "id": 176, + "order": 0 + }, + { + "ligatures": "road, asphalt", + "name": "road", + "id": 177, + "order": 0 + }, + { + "ligatures": "accessibility", + "name": "accessibility", + "id": 178, + "order": 0 + }, + { + "ligatures": "target, goal", + "name": "target", + "id": 179, + "order": 0 + }, + { + "ligatures": "shield, security", + "name": "shield", + "id": 180, + "order": 0 + }, + { + "ligatures": "power, lightning", + "name": "power", + "id": 181, + "order": 0 + }, + { + "ligatures": "switch", + "name": "switch", + "id": 182, + "order": 0 + }, + { + "ligatures": "power-cord, plugin", + "name": "power-cord", + "id": 183, + "order": 0 + }, + { + "ligatures": "clipboard, board", + "name": "clipboard", + "id": 184, + "order": 0 + }, + { + "ligatures": "list-numbered, options", + "name": "list-numbered", + "id": 185, + "order": 0 + }, + { + "ligatures": "list, todo", + "name": "list", + "id": 186, + "order": 0 + }, + { + "ligatures": "list2, todo2", + "name": "list2", + "id": 187, + "order": 0 + }, + { + "ligatures": "tree, branches", + "name": "tree", + "id": 188, + "order": 0 + }, + { + "ligatures": "menu, list3", + "name": "menu", + "id": 189, + "order": 0 + }, + { + "ligatures": "menu2, options2", + "name": "menu2", + "id": 190, + "order": 0 + }, + { + "ligatures": "menu3, options3", + "name": "menu3", + "id": 191, + "order": 0 + }, + { + "ligatures": "menu4, options4", + "name": "menu4", + "id": 192, + "order": 0 + }, + { + "ligatures": "cloud, weather", + "name": "cloud", + "id": 193, + "order": 0 + }, + { + "ligatures": "cloud-download, cloud2", + "name": "cloud-download", + "id": 194, + "order": 0 + }, + { + "ligatures": "cloud-upload, cloud3", + "name": "cloud-upload", + "id": 195, + "order": 0 + }, + { + "ligatures": "cloud-check, cloud4", + "name": "cloud-check", + "id": 196, + "order": 0 + }, + { + "ligatures": "download2, save4", + "name": "download2", + "id": 197, + "order": 0 + }, + { + "ligatures": "upload2, load2", + "name": "upload2", + "id": 198, + "order": 0 + }, + { + "ligatures": "download3, save5", + "name": "download3", + "id": 199, + "order": 0 + }, + { + "ligatures": "upload3, load3", + "name": "upload3", + "id": 200, + "order": 0 + }, + { + "ligatures": "sphere, globe", + "name": "sphere", + "id": 201, + "order": 0 + }, + { + "ligatures": "earth, globe2", + "name": "earth", + "id": 202, + "order": 0 + }, + { + "ligatures": "link, chain", + "name": "link", + "id": 203, + "order": 0 + }, + { + "ligatures": "flag, report", + "name": "flag", + "id": 204, + "order": 0 + }, + { + "ligatures": "attachment, paperclip", + "name": "attachment", + "id": 205, + "order": 0 + }, + { + "ligatures": "eye, views", + "name": "eye", + "id": 206, + "order": 0 + }, + { + "ligatures": "eye-plus, views2", + "name": "eye-plus", + "id": 207, + "order": 0 + }, + { + "ligatures": "eye-minus, views3", + "name": "eye-minus", + "id": 208, + "order": 0 + }, + { + "ligatures": "eye-blocked, views4", + "name": "eye-blocked", + "id": 209, + "order": 0 + }, + { + "ligatures": "bookmark, ribbon", + "name": "bookmark", + "id": 210, + "order": 0 + }, + { + "ligatures": "bookmarks, ribbons", + "name": "bookmarks", + "id": 211, + "order": 0 + }, + { + "ligatures": "sun, weather2", + "name": "sun", + "id": 212, + "order": 0 + }, + { + "ligatures": "contrast", + "name": "contrast", + "id": 213, + "order": 0 + }, + { + "ligatures": "brightness-contrast", + "name": "brightness-contrast", + "id": 214, + "order": 0 + }, + { + "ligatures": "star-empty, rate", + "name": "star-empty", + "id": 215, + "order": 0 + }, + { + "ligatures": "star-half, rate2", + "name": "star-half", + "id": 216, + "order": 0 + }, + { + "ligatures": "star-full, rate3", + "name": "star-full", + "id": 217, + "order": 0 + }, + { + "ligatures": "heart, like", + "name": "heart", + "id": 218, + "order": 0 + }, + { + "ligatures": "heart-broken, heart2", + "name": "heart-broken", + "id": 219, + "order": 0 + }, + { + "ligatures": "man, male", + "name": "man", + "id": 220, + "order": 0 + }, + { + "ligatures": "woman, female", + "name": "woman", + "id": 221, + "order": 0 + }, + { + "ligatures": "man-woman, toilet", + "name": "man-woman", + "id": 222, + "order": 0 + }, + { + "ligatures": "happy, emoticon", + "name": "happy", + "id": 223, + "order": 0 + }, + { + "ligatures": "happy2, emoticon2", + "name": "happy2", + "id": 224, + "order": 0 + }, + { + "ligatures": "smile, emoticon3", + "name": "smile", + "id": 225, + "order": 0 + }, + { + "ligatures": "smile2, emoticon4", + "name": "smile2", + "id": 226, + "order": 0 + }, + { + "ligatures": "tongue, emoticon5", + "name": "tongue", + "id": 227, + "order": 0 + }, + { + "ligatures": "tongue2, emoticon6", + "name": "tongue2", + "id": 228, + "order": 0 + }, + { + "ligatures": "sad, emoticon7", + "name": "sad", + "id": 229, + "order": 0 + }, + { + "ligatures": "sad2, emoticon8", + "name": "sad2", + "id": 230, + "order": 0 + }, + { + "ligatures": "wink, emoticon9", + "name": "wink", + "id": 231, + "order": 0 + }, + { + "ligatures": "wink2, emoticon10", + "name": "wink2", + "id": 232, + "order": 0 + }, + { + "ligatures": "grin, emoticon11", + "name": "grin", + "id": 233, + "order": 0 + }, + { + "ligatures": "grin2, emoticon12", + "name": "grin2", + "id": 234, + "order": 0 + }, + { + "ligatures": "cool, emoticon13", + "name": "cool", + "id": 235, + "order": 0 + }, + { + "ligatures": "cool2, emoticon14", + "name": "cool2", + "id": 236, + "order": 0 + }, + { + "ligatures": "angry, emoticon15", + "name": "angry", + "id": 237, + "order": 0 + }, + { + "ligatures": "angry2, emoticon16", + "name": "angry2", + "id": 238, + "order": 0 + }, + { + "ligatures": "evil, emoticon17", + "name": "evil", + "id": 239, + "order": 0 + }, + { + "ligatures": "evil2, emoticon18", + "name": "evil2", + "id": 240, + "order": 0 + }, + { + "ligatures": "shocked, emoticon19", + "name": "shocked", + "id": 241, + "order": 0 + }, + { + "ligatures": "shocked2, emoticon20", + "name": "shocked2", + "id": 242, + "order": 0 + }, + { + "ligatures": "baffled, emoticon21", + "name": "baffled", + "id": 243, + "order": 0 + }, + { + "ligatures": "baffled2, emoticon22", + "name": "baffled2", + "id": 244, + "order": 0 + }, + { + "ligatures": "confused, emoticon23", + "name": "confused", + "id": 245, + "order": 0 + }, + { + "ligatures": "confused2, emoticon24", + "name": "confused2", + "id": 246, + "order": 0 + }, + { + "ligatures": "neutral, emoticon25", + "name": "neutral", + "id": 247, + "order": 0 + }, + { + "ligatures": "neutral2, emoticon26", + "name": "neutral2", + "id": 248, + "order": 0 + }, + { + "ligatures": "hipster, emoticon27", + "name": "hipster", + "id": 249, + "order": 0 + }, + { + "ligatures": "hipster2, emoticon28", + "name": "hipster2", + "id": 250, + "order": 0 + }, + { + "ligatures": "wondering, emoticon29", + "name": "wondering", + "id": 251, + "order": 0 + }, + { + "ligatures": "wondering2, emoticon30", + "name": "wondering2", + "id": 252, + "order": 0 + }, + { + "ligatures": "sleepy, emoticon31", + "name": "sleepy", + "id": 253, + "order": 0 + }, + { + "ligatures": "sleepy2, emoticon32", + "name": "sleepy2", + "id": 254, + "order": 0 + }, + { + "ligatures": "frustrated, emoticon33", + "name": "frustrated", + "id": 255, + "order": 0 + }, + { + "ligatures": "frustrated2, emoticon34", + "name": "frustrated2", + "id": 256, + "order": 0 + }, + { + "ligatures": "crying, emoticon35", + "name": "crying", + "id": 257, + "order": 0 + }, + { + "ligatures": "crying2, emoticon36", + "name": "crying2", + "id": 258, + "order": 0 + }, + { + "ligatures": "point-up, finger", + "name": "point-up", + "id": 259, + "order": 0 + }, + { + "ligatures": "point-right, finger2", + "name": "point-right", + "id": 260, + "order": 0 + }, + { + "ligatures": "point-down, finger3", + "name": "point-down", + "id": 261, + "order": 0 + }, + { + "ligatures": "point-left, finger4", + "name": "point-left", + "id": 262, + "order": 0 + }, + { + "ligatures": "warning, sign", + "name": "warning", + "id": 263, + "order": 0 + }, + { + "ligatures": "notification, warning2", + "name": "notification", + "id": 264, + "order": 0 + }, + { + "ligatures": "question, help", + "name": "question", + "id": 265, + "order": 0 + }, + { + "ligatures": "plus, add", + "name": "plus", + "id": 266, + "order": 0 + }, + { + "ligatures": "minus, subtract", + "name": "minus", + "id": 267, + "order": 0 + }, + { + "ligatures": "info, information", + "name": "info", + "id": 268, + "order": 0 + }, + { + "ligatures": "cancel-circle, close", + "name": "cancel-circle", + "id": 269, + "order": 0 + }, + { + "ligatures": "blocked, forbidden", + "name": "blocked", + "id": 270, + "order": 0 + }, + { + "ligatures": "cross, cancel", + "name": "cross", + "id": 271, + "order": 0 + }, + { + "ligatures": "checkmark, tick", + "name": "checkmark", + "id": 272, + "order": 0 + }, + { + "ligatures": "checkmark2, tick2", + "name": "checkmark2", + "id": 273, + "order": 0 + }, + { + "ligatures": "spell-check, spelling", + "name": "spell-check", + "id": 274, + "order": 0 + }, + { + "ligatures": "enter, signin", + "name": "enter", + "id": 275, + "order": 0 + }, + { + "ligatures": "exit, signout", + "name": "exit", + "id": 276, + "order": 0 + }, + { + "ligatures": "play2, player", + "name": "play2", + "id": 277, + "order": 0 + }, + { + "ligatures": "pause, player2", + "name": "pause", + "id": 278, + "order": 0 + }, + { + "ligatures": "stop, player3", + "name": "stop", + "id": 279, + "order": 0 + }, + { + "ligatures": "previous, player4", + "name": "previous", + "id": 280, + "order": 0 + }, + { + "ligatures": "next, player5", + "name": "next", + "id": 281, + "order": 0 + }, + { + "ligatures": "backward, player6", + "name": "backward", + "id": 282, + "order": 0 + }, + { + "ligatures": "forward2, player7", + "name": "forward2", + "id": 283, + "order": 0 + }, + { + "ligatures": "play3, player8", + "name": "play3", + "id": 284, + "order": 0 + }, + { + "ligatures": "pause2, player9", + "name": "pause2", + "id": 285, + "order": 0 + }, + { + "ligatures": "stop2, player10", + "name": "stop2", + "id": 286, + "order": 0 + }, + { + "ligatures": "backward2, player11", + "name": "backward2", + "id": 287, + "order": 0 + }, + { + "ligatures": "forward3, player12", + "name": "forward3", + "id": 288, + "order": 0 + }, + { + "ligatures": "first, player13", + "name": "first", + "id": 289, + "order": 0 + }, + { + "ligatures": "last, player14", + "name": "last", + "id": 290, + "order": 0 + }, + { + "ligatures": "previous2, player15", + "name": "previous2", + "id": 291, + "order": 0 + }, + { + "ligatures": "next2, player16", + "name": "next2", + "id": 292, + "order": 0 + }, + { + "ligatures": "eject, player17", + "name": "eject", + "id": 293, + "order": 0 + }, + { + "ligatures": "volume-high, volume", + "name": "volume-high", + "id": 294, + "order": 0 + }, + { + "ligatures": "volume-medium, volume2", + "name": "volume-medium", + "id": 295, + "order": 0 + }, + { + "ligatures": "volume-low, volume3", + "name": "volume-low", + "id": 296, + "order": 0 + }, + { + "ligatures": "volume-mute, volume4", + "name": "volume-mute", + "id": 297, + "order": 0 + }, + { + "ligatures": "volume-mute2, volume5", + "name": "volume-mute2", + "id": 298, + "order": 0 + }, + { + "ligatures": "volume-increase, volume6", + "name": "volume-increase", + "id": 299, + "order": 0 + }, + { + "ligatures": "volume-decrease, volume7", + "name": "volume-decrease", + "id": 300, + "order": 0 + }, + { + "ligatures": "loop, repeat", + "name": "loop", + "id": 301, + "order": 0 + }, + { + "ligatures": "loop2, repeat2", + "name": "loop2", + "id": 302, + "order": 0 + }, + { + "ligatures": "infinite", + "name": "infinite", + "id": 303, + "order": 0 + }, + { + "ligatures": "shuffle, random", + "name": "shuffle", + "id": 304, + "order": 0 + }, + { + "ligatures": "arrow-up-left, up-left", + "name": "arrow-up-left", + "id": 305, + "order": 0 + }, + { + "ligatures": "arrow-up, up", + "name": "arrow-up", + "id": 306, + "order": 0 + }, + { + "ligatures": "arrow-up-right, up-right", + "name": "arrow-up-right", + "id": 307, + "order": 0 + }, + { + "ligatures": "arrow-right, right3", + "name": "arrow-right", + "id": 308, + "order": 0 + }, + { + "ligatures": "arrow-down-right, down-right", + "name": "arrow-down-right", + "id": 309, + "order": 0 + }, + { + "ligatures": "arrow-down, down", + "name": "arrow-down", + "id": 310, + "order": 0 + }, + { + "ligatures": "arrow-down-left, down-left", + "name": "arrow-down-left", + "id": 311, + "order": 0 + }, + { + "ligatures": "arrow-left, left3", + "name": "arrow-left", + "id": 312, + "order": 0 + }, + { + "ligatures": "arrow-up-left2, up-left2", + "name": "arrow-up-left2", + "id": 313, + "order": 0 + }, + { + "ligatures": "arrow-up2, up2", + "name": "arrow-up2", + "id": 314, + "order": 0 + }, + { + "ligatures": "arrow-up-right2, up-right2", + "name": "arrow-up-right2", + "id": 315, + "order": 0 + }, + { + "ligatures": "arrow-right2, right4", + "name": "arrow-right2", + "id": 316, + "order": 0 + }, + { + "ligatures": "arrow-down-right2, down-right2", + "name": "arrow-down-right2", + "id": 317, + "order": 0 + }, + { + "ligatures": "arrow-down2, down2", + "name": "arrow-down2", + "id": 318, + "order": 0 + }, + { + "ligatures": "arrow-down-left2, down-left2", + "name": "arrow-down-left2", + "id": 319, + "order": 0 + }, + { + "ligatures": "arrow-left2, left4", + "name": "arrow-left2", + "id": 320, + "order": 0 + }, + { + "ligatures": "circle-up, up3", + "name": "circle-up", + "id": 321, + "order": 0 + }, + { + "ligatures": "circle-right, right5", + "name": "circle-right", + "id": 322, + "order": 0 + }, + { + "ligatures": "circle-down, down3", + "name": "circle-down", + "id": 323, + "order": 0 + }, + { + "ligatures": "circle-left, left5", + "name": "circle-left", + "id": 324, + "order": 0 + }, + { + "ligatures": "tab, arrows", + "name": "tab", + "id": 325, + "order": 0 + }, + { + "ligatures": "move-up, sort", + "name": "move-up", + "id": 326, + "order": 0 + }, + { + "ligatures": "move-down, sort2", + "name": "move-down", + "id": 327, + "order": 0 + }, + { + "ligatures": "sort-alpha-asc, arrange", + "name": "sort-alpha-asc", + "id": 328, + "order": 0 + }, + { + "ligatures": "sort-alpha-desc, arrange2", + "name": "sort-alpha-desc", + "id": 329, + "order": 0 + }, + { + "ligatures": "sort-numeric-asc, arrange3", + "name": "sort-numeric-asc", + "id": 330, + "order": 0 + }, + { + "ligatures": "sort-numberic-desc, arrange4", + "name": "sort-numberic-desc", + "id": 331, + "order": 0 + }, + { + "ligatures": "sort-amount-asc, arrange5", + "name": "sort-amount-asc", + "id": 332, + "order": 0 + }, + { + "ligatures": "sort-amount-desc, arrange6", + "name": "sort-amount-desc", + "id": 333, + "order": 0 + }, + { + "ligatures": "command, cmd", + "name": "command", + "id": 334, + "order": 0 + }, + { + "ligatures": "shift", + "name": "shift", + "id": 335, + "order": 0 + }, + { + "ligatures": "ctrl, control", + "name": "ctrl", + "id": 336, + "order": 0 + }, + { + "ligatures": "opt, option", + "name": "opt", + "id": 337, + "order": 0 + }, + { + "ligatures": "checkbox-checked, checkbox", + "name": "checkbox-checked", + "id": 338, + "order": 0 + }, + { + "ligatures": "checkbox-unchecked, checkbox2", + "name": "checkbox-unchecked", + "id": 339, + "order": 0 + }, + { + "ligatures": "radio-checked, radio-button", + "name": "radio-checked", + "id": 340, + "order": 0 + }, + { + "ligatures": "radio-checked2, radio-button2", + "name": "radio-checked2", + "id": 341, + "order": 0 + }, + { + "ligatures": "radio-unchecked, radio-button3", + "name": "radio-unchecked", + "id": 342, + "order": 0 + }, + { + "ligatures": "crop, resize", + "name": "crop", + "id": 343, + "order": 0 + }, + { + "ligatures": "make-group", + "name": "make-group", + "id": 344, + "order": 0 + }, + { + "ligatures": "ungroup", + "name": "ungroup", + "id": 345, + "order": 0 + }, + { + "ligatures": "scissors, cut", + "name": "scissors", + "id": 346, + "order": 0 + }, + { + "ligatures": "filter, funnel", + "name": "filter", + "id": 347, + "order": 0 + }, + { + "ligatures": "font, typeface", + "name": "font", + "id": 348, + "order": 0 + }, + { + "ligatures": "ligature, typography", + "name": "ligature", + "id": 349, + "order": 0 + }, + { + "ligatures": "ligature2, typography2", + "name": "ligature2", + "id": 350, + "order": 0 + }, + { + "ligatures": "text-height, wysiwyg", + "name": "text-height", + "id": 351, + "order": 0 + }, + { + "ligatures": "text-width, wysiwyg2", + "name": "text-width", + "id": 352, + "order": 0 + }, + { + "ligatures": "font-size, wysiwyg3", + "name": "font-size", + "id": 353, + "order": 0 + }, + { + "ligatures": "bold, wysiwyg4", + "name": "bold", + "id": 354, + "order": 0 + }, + { + "ligatures": "underline, wysiwyg5", + "name": "underline", + "id": 355, + "order": 0 + }, + { + "ligatures": "italic, wysiwyg6", + "name": "italic", + "id": 356, + "order": 0 + }, + { + "ligatures": "strikethrough, wysiwyg7", + "name": "strikethrough", + "id": 357, + "order": 0 + }, + { + "ligatures": "omega, wysiwyg8", + "name": "omega", + "id": 358, + "order": 0 + }, + { + "ligatures": "sigma, wysiwyg9", + "name": "sigma", + "id": 359, + "order": 0 + }, + { + "ligatures": "page-break, wysiwyg10", + "name": "page-break", + "id": 360, + "order": 0 + }, + { + "ligatures": "superscript, wysiwyg11", + "name": "superscript", + "id": 361, + "order": 0 + }, + { + "ligatures": "subscript, wysiwyg12", + "name": "subscript", + "id": 362, + "order": 0 + }, + { + "ligatures": "superscript2, wysiwyg13", + "name": "superscript2", + "id": 363, + "order": 0 + }, + { + "ligatures": "subscript2, wysiwyg14", + "name": "subscript2", + "id": 364, + "order": 0 + }, + { + "ligatures": "text-color, wysiwyg15", + "name": "text-color", + "id": 365, + "order": 0 + }, + { + "ligatures": "pagebreak, wysiwyg16", + "name": "pagebreak", + "id": 366, + "order": 0 + }, + { + "ligatures": "clear-formatting, wysiwyg17", + "name": "clear-formatting", + "id": 367, + "order": 0 + }, + { + "ligatures": "table, wysiwyg18", + "name": "table", + "id": 368, + "order": 0 + }, + { + "ligatures": "table2, wysiwyg19", + "name": "table2", + "id": 369, + "order": 0 + }, + { + "ligatures": "insert-template, wysiwyg20", + "name": "insert-template", + "id": 370, + "order": 0 + }, + { + "ligatures": "pilcrow, wysiwyg21", + "name": "pilcrow", + "id": 371, + "order": 0 + }, + { + "ligatures": "ltr, wysiwyg22", + "name": "ltr", + "id": 372, + "order": 0 + }, + { + "ligatures": "rtl, wysiwyg23", + "name": "rtl", + "id": 373, + "order": 0 + }, + { + "ligatures": "section, wysiwyg24", + "name": "section", + "id": 374, + "order": 0 + }, + { + "ligatures": "paragraph-left, wysiwyg25", + "name": "paragraph-left", + "id": 375, + "order": 0 + }, + { + "ligatures": "paragraph-center, wysiwyg26", + "name": "paragraph-center", + "id": 376, + "order": 0 + }, + { + "ligatures": "paragraph-right, wysiwyg27", + "name": "paragraph-right", + "id": 377, + "order": 0 + }, + { + "ligatures": "paragraph-justify, wysiwyg28", + "name": "paragraph-justify", + "id": 378, + "order": 0 + }, + { + "ligatures": "indent-increase, wysiwyg29", + "name": "indent-increase", + "id": 379, + "order": 0 + }, + { + "ligatures": "indent-decrease, wysiwyg30", + "name": "indent-decrease", + "id": 380, + "order": 0 + }, + { + "ligatures": "share, out", + "name": "share", + "id": 381, + "order": 0 + }, + { + "ligatures": "new-tab, out2", + "name": "new-tab", + "id": 382, + "order": 0 + }, + { + "ligatures": "embed, code", + "name": "embed", + "id": 383, + "order": 0 + }, + { + "ligatures": "embed2, code2", + "name": "embed2", + "id": 384, + "order": 0 + }, + { + "ligatures": "terminal, console", + "name": "terminal", + "id": 385, + "order": 0 + }, + { + "ligatures": "share2, social", + "name": "share2", + "id": 386, + "order": 0 + }, + { + "ligatures": "mail2, contact2", + "name": "mail", + "id": 387, + "order": 0 + }, + { + "ligatures": "mail3, contact3", + "name": "mail2", + "id": 388, + "order": 0 + }, + { + "ligatures": "mail4, contact4", + "name": "mail3", + "id": 389, + "order": 0 + }, + { + "ligatures": "mail5, contact5", + "name": "mail4", + "id": 390, + "order": 0 + }, + { + "name": "amazon", + "ligatures": "amazon, brand", + "id": 391, + "order": 0 + }, + { + "name": "google", + "ligatures": "google, brand2", + "id": 392, + "order": 0 + }, + { + "name": "google2", + "ligatures": "google2, brand3", + "id": 393, + "order": 0 + }, + { + "name": "google3", + "ligatures": "google3, brand4", + "id": 394, + "order": 0 + }, + { + "ligatures": "google-plus, brand5", + "name": "google-plus", + "id": 395, + "order": 0 + }, + { + "ligatures": "google-plus2, brand6", + "name": "google-plus2", + "id": 396, + "order": 0 + }, + { + "ligatures": "google-plus3, brand7", + "name": "google-plus3", + "id": 397, + "order": 0 + }, + { + "name": "hangouts", + "ligatures": "hangouts, brand8", + "id": 398, + "order": 0 + }, + { + "ligatures": "google-drive, brand9", + "name": "google-drive", + "id": 399, + "order": 0 + }, + { + "ligatures": "facebook, brand10", + "name": "facebook", + "id": 400, + "order": 0 + }, + { + "ligatures": "facebook2, brand11", + "name": "facebook2", + "id": 401, + "order": 0 + }, + { + "ligatures": "instagram, brand12", + "name": "instagram", + "id": 402, + "order": 0 + }, + { + "name": "whatsapp", + "ligatures": "whatsapp, brand13", + "id": 403, + "order": 0 + }, + { + "name": "spotify", + "ligatures": "spotify, brand14", + "id": 404, + "order": 0 + }, + { + "name": "telegram", + "ligatures": "telegram, brand15", + "id": 405, + "order": 0 + }, + { + "ligatures": "twitter, brand16", + "name": "twitter", + "id": 406, + "order": 0 + }, + { + "name": "vine", + "ligatures": "vine, brand17", + "id": 407, + "order": 0 + }, + { + "name": "vk", + "ligatures": "vk, brand18", + "id": 408, + "order": 0 + }, + { + "name": "renren", + "ligatures": "renren, brand19", + "id": 409, + "order": 0 + }, + { + "name": "sina-weibo", + "ligatures": "sina-weibo, brand20", + "id": 410, + "order": 0 + }, + { + "ligatures": "feed2, rss", + "name": "rss", + "id": 411, + "order": 0 + }, + { + "ligatures": "feed3, rss2", + "name": "rss2", + "id": 412, + "order": 0 + }, + { + "ligatures": "youtube, brand21", + "name": "youtube", + "id": 413, + "order": 0 + }, + { + "ligatures": "youtube2, brand22", + "name": "youtube2", + "id": 414, + "order": 0 + }, + { + "ligatures": "twitch, brand23", + "name": "twitch", + "id": 415, + "order": 0 + }, + { + "ligatures": "vimeo, brand24", + "name": "vimeo", + "id": 416, + "order": 0 + }, + { + "ligatures": "vimeo2, brand25", + "name": "vimeo2", + "id": 417, + "order": 0 + }, + { + "ligatures": "lanyrd, brand26", + "name": "lanyrd", + "id": 418, + "order": 0 + }, + { + "ligatures": "flickr, brand27", + "name": "flickr", + "id": 419, + "order": 0 + }, + { + "ligatures": "flickr2, brand28", + "name": "flickr2", + "id": 420, + "order": 0 + }, + { + "ligatures": "flickr3, brand29", + "name": "flickr3", + "id": 421, + "order": 0 + }, + { + "ligatures": "flickr4, brand30", + "name": "flickr4", + "id": 422, + "order": 0 + }, + { + "ligatures": "dribbble, brand31", + "name": "dribbble", + "id": 423, + "order": 0 + }, + { + "name": "behance", + "ligatures": "behance, brand32", + "id": 424, + "order": 0 + }, + { + "name": "behance2", + "ligatures": "behance2, brand33", + "id": 425, + "order": 0 + }, + { + "ligatures": "deviantart, brand34", + "name": "deviantart", + "id": 426, + "order": 0 + }, + { + "name": "500px", + "ligatures": "500px, brand35", + "id": 427, + "order": 0 + }, + { + "ligatures": "steam, brand36", + "name": "steam", + "id": 428, + "order": 0 + }, + { + "ligatures": "steam2, brand37", + "name": "steam2", + "id": 429, + "order": 0 + }, + { + "ligatures": "dropbox, brand38", + "name": "dropbox", + "id": 430, + "order": 0 + }, + { + "ligatures": "onedrive, brand39", + "name": "onedrive", + "id": 431, + "order": 0 + }, + { + "ligatures": "github, brand40", + "name": "github", + "id": 432, + "order": 0 + }, + { + "name": "npm", + "ligatures": "npm, brand41", + "id": 433, + "order": 0 + }, + { + "name": "basecamp", + "ligatures": "basecamp, brand42", + "id": 434, + "order": 0 + }, + { + "name": "trello", + "ligatures": "trello, brand43", + "id": 435, + "order": 0 + }, + { + "ligatures": "wordpress, brand44", + "name": "wordpress", + "id": 436, + "order": 0 + }, + { + "ligatures": "joomla, brand45", + "name": "joomla", + "id": 437, + "order": 0 + }, + { + "ligatures": "ello, brand46", + "name": "ello", + "id": 438, + "order": 0 + }, + { + "ligatures": "blogger, brand47", + "name": "blogger", + "id": 439, + "order": 0 + }, + { + "ligatures": "blogger2, brand48", + "name": "blogger2", + "id": 440, + "order": 0 + }, + { + "ligatures": "tumblr, brand49", + "name": "tumblr", + "id": 441, + "order": 0 + }, + { + "ligatures": "tumblr2, brand50", + "name": "tumblr2", + "id": 442, + "order": 0 + }, + { + "ligatures": "yahoo, brand51", + "name": "yahoo", + "id": 443, + "order": 0 + }, + { + "name": "yahoo2", + "ligatures": "yahoo2", + "id": 444, + "order": 0 + }, + { + "ligatures": "tux, brand52", + "name": "tux", + "id": 445, + "order": 0 + }, + { + "ligatures": "apple, brand53", + "name": "appleinc", + "id": 446, + "order": 0 + }, + { + "ligatures": "finder, brand54", + "name": "finder", + "id": 447, + "order": 0 + }, + { + "ligatures": "android, brand55", + "name": "android", + "id": 448, + "order": 0 + }, + { + "ligatures": "windows, brand56", + "name": "windows", + "id": 449, + "order": 0 + }, + { + "ligatures": "windows8, brand57", + "name": "windows8", + "id": 450, + "order": 0 + }, + { + "ligatures": "soundcloud, brand58", + "name": "soundcloud", + "id": 451, + "order": 0 + }, + { + "ligatures": "soundcloud2, brand59", + "name": "soundcloud2", + "id": 452, + "order": 0 + }, + { + "ligatures": "skype, brand60", + "name": "skype", + "id": 453, + "order": 0 + }, + { + "ligatures": "reddit, brand61", + "name": "reddit", + "id": 454, + "order": 0 + }, + { + "name": "hackernews", + "ligatures": "hackernews, brand62", + "id": 455, + "order": 0 + }, + { + "name": "wikipedia", + "ligatures": "wikipedia, brand63", + "id": 456, + "order": 0 + }, + { + "ligatures": "linkedin, brand64", + "name": "linkedin", + "id": 457, + "order": 0 + }, + { + "ligatures": "linkedin2, brand65", + "name": "linkedin2", + "id": 458, + "order": 0 + }, + { + "ligatures": "lastfm, brand66", + "name": "lastfm", + "id": 459, + "order": 0 + }, + { + "ligatures": "lastfm2, brand67", + "name": "lastfm2", + "id": 460, + "order": 0 + }, + { + "ligatures": "delicious, brand68", + "name": "delicious", + "id": 461, + "order": 0 + }, + { + "name": "stumbleupon", + "ligatures": "stumbleupon, brand69", + "id": 462, + "order": 0 + }, + { + "ligatures": "stumbleupon2, brand70", + "name": "stumbleupon2", + "id": 463, + "order": 0 + }, + { + "ligatures": "stackoverflow, brand71", + "name": "stackoverflow", + "id": 464, + "order": 0 + }, + { + "name": "pinterest", + "ligatures": "pinterest, brand72", + "id": 465, + "order": 0 + }, + { + "ligatures": "pinterest2, brand73", + "name": "pinterest2", + "id": 466, + "order": 0 + }, + { + "ligatures": "xing, brand74", + "name": "xing", + "id": 467, + "order": 0 + }, + { + "ligatures": "xing2, brand75", + "name": "xing2", + "codes": [ + 61231 + ], + "id": 468, + "order": 0 + }, + { + "ligatures": "flattr, brand76", + "name": "flattr", + "id": 469, + "order": 0 + }, + { + "ligatures": "foursquare, brand77", + "name": "foursquare", + "id": 470, + "order": 0 + }, + { + "ligatures": "yelp, brand78", + "name": "yelp", + "id": 471, + "order": 0 + }, + { + "ligatures": "paypal, brand79", + "name": "paypal", + "codes": [ + 61234 + ], + "id": 472, + "order": 0 + }, + { + "ligatures": "chrome, browser", + "name": "chrome", + "id": 473, + "order": 0 + }, + { + "ligatures": "firefox, browser2", + "name": "firefox", + "id": 474, + "order": 0 + }, + { + "ligatures": "IE, browser3", + "name": "IE", + "id": 475, + "order": 0 + }, + { + "name": "edge", + "ligatures": "edge, browser4", + "id": 476, + "order": 0 + }, + { + "ligatures": "safari, browser5", + "name": "safari", + "id": 477, + "order": 0 + }, + { + "ligatures": "opera, browser6", + "name": "opera", + "id": 478, + "order": 0 + }, + { + "ligatures": "file-pdf, file10", + "name": "file-pdf", + "id": 479, + "order": 0 + }, + { + "ligatures": "file-openoffice, file11", + "name": "file-openoffice", + "id": 480, + "order": 0 + }, + { + "ligatures": "file-word, file12", + "name": "file-word", + "id": 481, + "order": 0 + }, + { + "ligatures": "file-excel, file13", + "name": "file-excel", + "id": 482, + "order": 0 + }, + { + "ligatures": "libreoffice, file14", + "name": "libreoffice", + "id": 483, + "order": 0 + }, + { + "ligatures": "html-five, w3c", + "name": "html-five", + "id": 484, + "order": 0 + }, + { + "ligatures": "html-five2, w3c2", + "name": "html-five2", + "id": 485, + "order": 0 + }, + { + "ligatures": "css3, w3c3", + "name": "css3", + "id": 486, + "order": 0 + }, + { + "ligatures": "git, brand80", + "name": "git", + "id": 487, + "order": 0 + }, + { + "ligatures": "codepen, brand81", + "name": "codepen", + "id": 488, + "order": 0 + }, + { + "ligatures": "svg", + "name": "svg", + "id": 489, + "order": 0 + }, + { + "ligatures": "IcoMoon, icomoon", + "name": "IcoMoon", + "id": 490, + "order": 0 + } + ], + "id": 3, + "metadata": { + "name": "IcoMoon - Free", + "licenseURL": "https://icomoon.io/#icons-icomoon", + "license": "GPL or CC BY 4.0", + "designerURL": "http://keyamoon.com", + "designer": "Keyamoon", + "url": "https://icomoon.io/#icons-icomoon" + }, + "height": 1024, + "prevSize": 32, + "icons": [ + { + "id": 0, + "paths": [ + "M1024 590.444l-512-397.426-512 397.428v-162.038l512-397.426 512 397.428zM896 576v384h-256v-256h-256v256h-256v-384l384-288z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "home", + "house" + ], + "defaultCode": 59648, + "grid": 16 + }, + { + "id": 1, + "paths": [ + "M512 32l-512 512 96 96 96-96v416h256v-192h128v192h256v-416l96 96 96-96-512-512zM512 448c-35.346 0-64-28.654-64-64s28.654-64 64-64c35.346 0 64 28.654 64 64s-28.654 64-64 64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "home", + "house" + ], + "defaultCode": 59649, + "grid": 16 + }, + { + "id": 2, + "paths": [ + "M1024 608l-192-192v-288h-128v160l-192-192-512 512v32h128v320h320v-192h128v192h320v-320h128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "home", + "house" + ], + "defaultCode": 59650, + "grid": 16 + }, + { + "id": 3, + "paths": [ + "M0 1024h512v-1024h-512v1024zM320 128h128v128h-128v-128zM320 384h128v128h-128v-128zM320 640h128v128h-128v-128zM64 128h128v128h-128v-128zM64 384h128v128h-128v-128zM64 640h128v128h-128v-128zM576 320h448v64h-448zM576 1024h128v-256h192v256h128v-576h-448z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "office", + "buildings", + "work" + ], + "defaultCode": 59651, + "grid": 16 + }, + { + "id": 4, + "paths": [ + "M896 256v-128h-896v704c0 35.346 28.654 64 64 64h864c53.022 0 96-42.978 96-96v-544h-128zM832 832h-768v-640h768v640zM128 320h640v64h-640zM512 448h256v64h-256zM512 576h256v64h-256zM512 704h192v64h-192zM128 448h320v320h-320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "newspaper", + "news", + "paper" + ], + "defaultCode": 59652, + "grid": 16 + }, + { + "id": 5, + "paths": [ + "M864 0c88.364 0 160 71.634 160 160 0 36.020-11.91 69.258-32 96l-64 64-224-224 64-64c26.742-20.090 59.978-32 96-32zM64 736l-64 288 288-64 592-592-224-224-592 592zM715.578 363.578l-448 448-55.156-55.156 448-448 55.156 55.156z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pencil", + "write", + "edit" + ], + "defaultCode": 59653, + "grid": 16 + }, + { + "id": 6, + "paths": [ + "M384 640l128-64 448-448-64-64-448 448-64 128zM289.3 867.098c-31.632-66.728-65.666-100.762-132.396-132.394l99.096-272.792 128-77.912 384-384h-192l-384 384-192 640 640-192 384-384v-192l-384 384-77.912 128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pencil", + "write", + "edit" + ], + "defaultCode": 59654, + "grid": 16 + }, + { + "id": 7, + "paths": [ + "M0 1024c128-384 463-1024 1024-1024-263 211-384 704-576 704s-192 0-192 0l-192 320h-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "quill", + "feather", + "write", + "edit" + ], + "defaultCode": 59655, + "grid": 16 + }, + { + "id": 8, + "paths": [ + "M1018.17 291.89l-286.058-286.058c-9.334-9.334-21.644-7.234-27.356 4.666l-38.354 79.904 267.198 267.198 79.904-38.354c11.9-5.712 14-18.022 4.666-27.356z", + "M615.384 135.384l-263.384 21.95c-17.5 2.166-32.080 5.898-37.090 28.752-0.006 0.024-0.012 0.042-0.018 0.066-71.422 343.070-314.892 677.848-314.892 677.848l57.374 57.374 271.986-271.99c-5.996-12.53-9.36-26.564-9.36-41.384 0-53.020 42.98-96 96-96s96 42.98 96 96-42.98 96-96 96c-14.82 0-28.852-3.364-41.384-9.36l-271.988 271.986 57.372 57.374c0 0 334.778-243.47 677.848-314.892 0.024-0.006 0.042-0.012 0.066-0.018 22.854-5.010 26.586-19.59 28.752-37.090l21.95-263.384-273.232-273.232z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pen", + "write", + "edit" + ], + "defaultCode": 59656, + "grid": 16 + }, + { + "id": 9, + "paths": [ + "M384 0v96c73.482 0 144.712 14.37 211.716 42.71 64.768 27.394 122.958 66.632 172.948 116.624s89.228 108.18 116.624 172.948c28.342 67.004 42.712 138.238 42.712 211.718h96c0-353.46-286.54-640-640-640z", + "M384 192v96c94.022 0 182.418 36.614 248.9 103.098 66.486 66.484 103.1 154.878 103.1 248.902h96c0-247.422-200.576-448-448-448z", + "M480 384l-64 64-224 64-192 416 25.374 25.374 232.804-232.804c-1.412-5.286-2.178-10.84-2.178-16.57 0-35.346 28.654-64 64-64s64 28.654 64 64-28.654 64-64 64c-5.732 0-11.282-0.764-16.568-2.178l-232.804 232.804 25.372 25.374 416-192 64-224 64-64-160-160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "blog", + "pen", + "feed", + "publish", + "broadcast", + "write" + ], + "defaultCode": 59657, + "grid": 16 + }, + { + "id": 10, + "paths": [ + "M986.51 37.49c-49.988-49.986-131.032-49.986-181.020 0l-172.118 172.118-121.372-121.372-135.764 135.764 106.426 106.426-472.118 472.118c-8.048 8.048-11.468 18.958-10.3 29.456h-0.244v160c0 17.674 14.328 32 32 32h160c0 0 2.664 0 4 0 9.212 0 18.426-3.516 25.456-10.544l472.118-472.118 106.426 106.426 135.764-135.764-121.372-121.372 172.118-172.118c49.986-49.988 49.986-131.032 0-181.020zM173.090 960h-109.090v-109.090l469.574-469.572 109.088 109.088-469.572 469.574z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "eyedropper", + "color", + "color-picker", + "sample" + ], + "defaultCode": 59658, + "grid": 16 + }, + { + "id": 11, + "paths": [ + "M864.626 473.162c-65.754-183.44-205.11-348.15-352.626-473.162-147.516 125.012-286.87 289.722-352.626 473.162-40.664 113.436-44.682 236.562 12.584 345.4 65.846 125.14 198.632 205.438 340.042 205.438s274.196-80.298 340.040-205.44c57.27-108.838 53.25-231.962 12.586-345.398zM738.764 758.956c-43.802 83.252-132.812 137.044-226.764 137.044-55.12 0-108.524-18.536-152.112-50.652 13.242 1.724 26.632 2.652 40.112 2.652 117.426 0 228.668-67.214 283.402-171.242 44.878-85.292 40.978-173.848 23.882-244.338 14.558 28.15 26.906 56.198 36.848 83.932 22.606 63.062 40.024 156.34-5.368 242.604z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "droplet", + "color", + "water" + ], + "defaultCode": 59659, + "grid": 16 + }, + { + "id": 12, + "paths": [ + "M1024 576v-384h-192v-64c0-35.2-28.8-64-64-64h-704c-35.2 0-64 28.8-64 64v192c0 35.2 28.8 64 64 64h704c35.2 0 64-28.8 64-64v-64h128v256h-576v128h-32c-17.674 0-32 14.326-32 32v320c0 17.674 14.326 32 32 32h128c17.674 0 32-14.326 32-32v-320c0-17.674-14.326-32-32-32h-32v-64h576zM768 192h-704v-64h704v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "paint-format", + "format", + "color" + ], + "defaultCode": 59660, + "grid": 16 + }, + { + "id": 13, + "paths": [ + "M959.884 128c0.040 0.034 0.082 0.076 0.116 0.116v767.77c-0.034 0.040-0.076 0.082-0.116 0.116h-895.77c-0.040-0.034-0.082-0.076-0.114-0.116v-767.772c0.034-0.040 0.076-0.082 0.114-0.114h895.77zM960 64h-896c-35.2 0-64 28.8-64 64v768c0 35.2 28.8 64 64 64h896c35.2 0 64-28.8 64-64v-768c0-35.2-28.8-64-64-64v0z", + "M832 288c0 53.020-42.98 96-96 96s-96-42.98-96-96 42.98-96 96-96 96 42.98 96 96z", + "M896 832h-768v-128l224-384 256 320h64l224-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "image", + "picture", + "photo", + "graphic" + ], + "defaultCode": 59661, + "grid": 16 + }, + { + "id": 14, + "paths": [ + "M1088 128h-64v-64c0-35.2-28.8-64-64-64h-896c-35.2 0-64 28.8-64 64v768c0 35.2 28.8 64 64 64h64v64c0 35.2 28.8 64 64 64h896c35.2 0 64-28.8 64-64v-768c0-35.2-28.8-64-64-64zM128 192v640h-63.886c-0.040-0.034-0.082-0.076-0.114-0.116v-767.77c0.034-0.040 0.076-0.082 0.114-0.114h895.77c0.040 0.034 0.082 0.076 0.116 0.116v63.884h-768c-35.2 0-64 28.8-64 64v0zM1088 959.884c-0.034 0.040-0.076 0.082-0.116 0.116h-895.77c-0.040-0.034-0.082-0.076-0.114-0.116v-767.77c0.034-0.040 0.076-0.082 0.114-0.114h895.77c0.040 0.034 0.082 0.076 0.116 0.116v767.768z", + "M960 352c0 53.020-42.98 96-96 96s-96-42.98-96-96 42.98-96 96-96 96 42.98 96 96z", + "M1024 896h-768v-128l224-384 256 320h64l224-192z" + ], + "width": 1152, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "images", + "pictures", + "photos", + "graphics" + ], + "defaultCode": 59662, + "grid": 16 + }, + { + "id": 15, + "paths": [ + "M304 608c0 114.876 93.124 208 208 208s208-93.124 208-208-93.124-208-208-208-208 93.124-208 208zM960 256h-224c-16-64-32-128-96-128h-256c-64 0-80 64-96 128h-224c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h896c35.2 0 64-28.8 64-64v-576c0-35.2-28.8-64-64-64zM512 892c-156.85 0-284-127.148-284-284 0-156.85 127.15-284 284-284 156.852 0 284 127.15 284 284 0 156.852-127.146 284-284 284zM960 448h-128v-64h128v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "camera", + "photo", + "picture", + "image" + ], + "defaultCode": 59663, + "grid": 16 + }, + { + "id": 16, + "paths": [ + "M288 576h-64v448h64c17.6 0 32-14.4 32-32v-384c0-17.6-14.4-32-32-32z", + "M736 576c-17.602 0-32 14.4-32 32v384c0 17.6 14.398 32 32 32h64v-448h-64z", + "M1024 512c0-282.77-229.23-512-512-512s-512 229.23-512 512c0 61.412 10.83 120.29 30.656 174.848-19.478 33.206-30.656 71.87-30.656 113.152 0 112.846 83.448 206.188 192 221.716v-443.418c-31.914 4.566-61.664 15.842-87.754 32.378-5.392-26.718-8.246-54.364-8.246-82.676 0-229.75 186.25-416 416-416s416 186.25 416 416c0 28.314-2.83 55.968-8.22 82.696-26.1-16.546-55.854-27.848-87.78-32.418v443.44c108.548-15.532 192-108.874 192-221.714 0-41.274-11.178-79.934-30.648-113.138 19.828-54.566 30.648-113.452 30.648-174.866z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "headphones", + "headset", + "music", + "audio" + ], + "defaultCode": 59664, + "grid": 16 + }, + { + "id": 17, + "paths": [ + "M960 0h64v736c0 88.366-100.29 160-224 160s-224-71.634-224-160c0-88.368 100.29-160 224-160 62.684 0 119.342 18.4 160 48.040v-368.040l-512 113.778v494.222c0 88.366-100.288 160-224 160s-224-71.634-224-160c0-88.368 100.288-160 224-160 62.684 0 119.342 18.4 160 48.040v-624.040l576-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "music", + "song", + "audio", + "sound", + "note" + ], + "defaultCode": 59665, + "grid": 16 + }, + { + "id": 18, + "paths": [ + "M981.188 160.108c-143.632-20.65-302.332-32.108-469.186-32.108-166.86 0-325.556 11.458-469.194 32.108-27.53 107.726-42.808 226.75-42.808 351.892 0 125.14 15.278 244.166 42.808 351.89 143.638 20.652 302.336 32.11 469.194 32.11 166.854 0 325.552-11.458 469.186-32.11 27.532-107.724 42.812-226.75 42.812-351.89 0-125.142-15.28-244.166-42.812-351.892zM384.002 704v-384l320 192-320 192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "play", + "video", + "movie" + ], + "defaultCode": 59666, + "grid": 16 + }, + { + "id": 19, + "paths": [ + "M0 128v768h1024v-768h-1024zM192 832h-128v-128h128v128zM192 576h-128v-128h128v128zM192 320h-128v-128h128v128zM768 832h-512v-640h512v640zM960 832h-128v-128h128v128zM960 576h-128v-128h128v128zM960 320h-128v-128h128v128zM384 320v384l256-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "film", + "video", + "movie", + "tape", + "play" + ], + "defaultCode": 59667, + "grid": 16 + }, + { + "id": 20, + "paths": [ + "M384 288c0-88.366 71.634-160 160-160s160 71.634 160 160c0 88.366-71.634 160-160 160s-160-71.634-160-160zM0 288c0-88.366 71.634-160 160-160s160 71.634 160 160c0 88.366-71.634 160-160 160s-160-71.634-160-160zM768 608v-96c0-35.2-28.8-64-64-64h-640c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64v-96l256 160v-448l-256 160zM640 768h-512v-192h512v192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "video-camera", + "video", + "media", + "film", + "movie" + ], + "defaultCode": 59668, + "grid": 16 + }, + { + "id": 21, + "paths": [ + "M864 192h-512c-88 0-160 72-160 160v512c0 88 72 160 160 160h512c88 0 160-72 160-160v-512c0-88-72-160-160-160zM416 896c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96zM416 512c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96zM608 704c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96zM800 896c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96zM800 512c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96zM828.76 128c-14.93-72.804-79.71-128-156.76-128h-512c-88 0-160 72-160 160v512c0 77.046 55.196 141.83 128 156.76v-636.76c0-35.2 28.8-64 64-64h636.76z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "dice", + "game", + "chance", + "luck", + "random", + "gample" + ], + "defaultCode": 59669, + "grid": 16 + }, + { + "id": 22, + "paths": [ + "M964.73 178.804c-93.902-109.45-233.21-178.804-388.73-178.804-282.77 0-512 229.23-512 512s229.23 512 512 512c155.52 0 294.828-69.356 388.728-178.804l-324.728-333.196 324.73-333.196zM704 120.602c39.432 0 71.398 31.964 71.398 71.398 0 39.432-31.966 71.398-71.398 71.398s-71.398-31.966-71.398-71.398c0-39.432 31.966-71.398 71.398-71.398z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pacman", + "game", + "arcade" + ], + "defaultCode": 59670, + "grid": 16 + }, + { + "id": 23, + "paths": [ + "M817.57 348.15c-193.566-143.858-260.266-259.018-305.566-348.148v0c-0.004 0-0.004-0.002-0.004-0.002v0.002c-45.296 89.13-112 204.292-305.566 348.148-330.036 245.286-19.376 587.668 253.758 399.224-17.796 116.93-78.53 202.172-140.208 238.882v37.744h384.032v-37.74c-61.682-36.708-122.41-121.954-140.212-238.884 273.136 188.446 583.8-153.94 253.766-399.226z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spades", + "cards", + "poker" + ], + "defaultCode": 59671, + "grid": 16 + }, + { + "id": 24, + "paths": [ + "M786.832 392.772c-59.032 0-112.086 24.596-149.852 64.694-15.996 16.984-43.762 37.112-73.8 54.81 14.11-53.868 58.676-121.7 89.628-151.456 39.64-38.17 63.984-91.83 63.984-151.5 0.006-114.894-91.476-208.096-204.788-209.32-113.32 1.222-204.796 94.426-204.796 209.318 0 59.672 24.344 113.33 63.986 151.5 30.954 29.756 75.52 97.588 89.628 151.456-30.042-17.7-57.806-37.826-73.8-54.81-37.768-40.098-90.82-64.694-149.85-64.694-114.386 0-207.080 93.664-207.080 209.328 0 115.638 92.692 209.338 207.080 209.338 59.042 0 112.082-25.356 149.85-65.452 16.804-17.872 46.444-40.138 78.292-58.632-3.002 147.692-73.532 256.168-145.318 298.906v37.742h384.014v-37.74c-71.792-42.736-142.32-151.216-145.32-298.906 31.852 18.494 61.488 40.768 78.292 58.632 37.766 40.094 90.808 65.452 149.852 65.452 114.386 0 207.078-93.7 207.078-209.338-0.002-115.664-92.692-209.328-207.080-209.328z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "clubs", + "cards", + "poker" + ], + "defaultCode": 59672, + "grid": 16 + }, + { + "id": 25, + "paths": [ + "M512 0l-320 512 320 512 320-512z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "diamonds", + "cards", + "poker" + ], + "defaultCode": 59673, + "grid": 16 + }, + { + "id": 26, + "paths": [ + "M1024 429.256c0-200.926-58.792-363.938-131.482-365.226 0.292-0.006 0.578-0.030 0.872-0.030h-82.942c0 0-194.8 146.336-475.23 203.754-8.56 45.292-14.030 99.274-14.030 161.502s5.466 116.208 14.030 161.5c280.428 57.418 475.23 203.756 475.23 203.756h82.942c-0.292 0-0.578-0.024-0.872-0.032 72.696-1.288 131.482-164.298 131.482-365.224zM864.824 739.252c-9.382 0-19.532-9.742-24.746-15.548-12.63-14.064-24.792-35.96-35.188-63.328-23.256-61.232-36.066-143.31-36.066-231.124 0-87.81 12.81-169.89 36.066-231.122 10.394-27.368 22.562-49.266 35.188-63.328 5.214-5.812 15.364-15.552 24.746-15.552 9.38 0 19.536 9.744 24.744 15.552 12.634 14.064 24.796 35.958 35.188 63.328 23.258 61.23 36.068 143.312 36.068 231.122 0 87.804-12.81 169.888-36.068 231.124-10.39 27.368-22.562 49.264-35.188 63.328-5.208 5.806-15.36 15.548-24.744 15.548zM251.812 429.256c0-51.95 3.81-102.43 11.052-149.094-47.372 6.554-88.942 10.324-140.34 10.324-67.058 0-67.058 0-67.058 0l-55.466 94.686v88.17l55.46 94.686c0 0 0 0 67.060 0 51.398 0 92.968 3.774 140.34 10.324-7.236-46.664-11.048-97.146-11.048-149.096zM368.15 642.172l-127.998-24.51 81.842 321.544c4.236 16.634 20.744 25.038 36.686 18.654l118.556-47.452c15.944-6.376 22.328-23.964 14.196-39.084l-123.282-229.152zM864.824 548.73c-3.618 0-7.528-3.754-9.538-5.992-4.87-5.42-9.556-13.86-13.562-24.408-8.962-23.6-13.9-55.234-13.9-89.078s4.938-65.478 13.9-89.078c4.006-10.548 8.696-18.988 13.562-24.408 2.010-2.24 5.92-5.994 9.538-5.994 3.616 0 7.53 3.756 9.538 5.994 4.87 5.42 9.556 13.858 13.56 24.408 8.964 23.598 13.902 55.234 13.902 89.078 0 33.842-4.938 65.478-13.902 89.078-4.004 10.548-8.696 18.988-13.56 24.408-2.008 2.238-5.92 5.992-9.538 5.992z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bullhorn", + "megaphone", + "announcement", + "advertisement", + "news" + ], + "defaultCode": 59674, + "grid": 16 + }, + { + "id": 27, + "paths": [ + "M640 576c105.87 0 201.87 43.066 271.402 112.598l-90.468 90.468c-46.354-46.356-110.356-75.066-180.934-75.066s-134.578 28.71-180.934 75.066l-90.468-90.468c69.532-69.532 165.532-112.598 271.402-112.598zM187.452 507.452c120.88-120.88 281.598-187.452 452.548-187.452s331.668 66.572 452.55 187.452l-90.51 90.508c-96.706-96.704-225.28-149.96-362.040-149.96-136.762 0-265.334 53.256-362.038 149.962l-90.51-90.51zM988.784 134.438c106.702 45.132 202.516 109.728 284.782 191.996v0l-90.508 90.508c-145.056-145.056-337.92-224.942-543.058-224.942-205.14 0-398 79.886-543.058 224.942l-90.51-90.51c82.268-82.266 178.082-146.862 284.784-191.994 110.504-46.738 227.852-70.438 348.784-70.438s238.278 23.7 348.784 70.438zM576 896c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64z" + ], + "width": 1280, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "connection", + "wifi", + "wave" + ], + "defaultCode": 59675, + "grid": 16 + }, + { + "id": 28, + "paths": [ + "M1024 512c0-282.77-229.23-512-512-512s-512 229.23-512 512c0 220.054 138.836 407.664 333.686 480.068l-13.686 31.932h384l-13.686-31.932c194.85-72.404 333.686-260.014 333.686-480.068zM486.79 634.826c-22.808-9.788-38.79-32.436-38.79-58.826 0-35.346 28.654-64 64-64s64 28.654 64 64c0 26.39-15.978 49.044-38.786 58.834l-25.214-58.834-25.21 58.826zM538.268 637.292c58.092-12.118 101.732-63.602 101.732-125.292 0-70.694-57.306-128-128-128-70.692 0-128 57.306-128 128 0 61.692 43.662 113.122 101.76 125.228l-74.624 174.122c-91.23-39.15-155.136-129.784-155.136-235.35 0-141.384 114.616-268 256-268s256 126.616 256 268c0 105.566-63.906 196.2-155.136 235.35l-74.596-174.058zM688.448 987.708l-73.924-172.486c126.446-42.738 217.476-162.346 217.476-303.222 0-176.73-143.268-320-320-320-176.73 0-320 143.27-320 320 0 140.876 91.030 260.484 217.476 303.222l-73.924 172.486c-159.594-68.488-271.386-227.034-271.386-411.708 0-247.332 200.502-459.834 447.834-459.834s447.834 212.502 447.834 459.834c0 184.674-111.792 343.22-271.386 411.708z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "podcast", + "broadcast", + "live", + "radio", + "feed" + ], + "defaultCode": 59676, + "grid": 16 + }, + { + "id": 29, + "paths": [ + "M384 512c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM664.348 230.526c99.852 54.158 167.652 159.898 167.652 281.474s-67.8 227.316-167.652 281.474c44.066-70.126 71.652-170.27 71.652-281.474s-27.586-211.348-71.652-281.474zM288 512c0 111.204 27.584 211.348 71.652 281.474-99.852-54.16-167.652-159.898-167.652-281.474s67.8-227.314 167.652-281.474c-44.068 70.126-71.652 170.27-71.652 281.474zM96 512c0 171.9 54.404 326.184 140.652 431.722-142.302-90.948-236.652-250.314-236.652-431.722s94.35-340.774 236.652-431.722c-86.248 105.538-140.652 259.822-140.652 431.722zM787.352 80.28c142.298 90.946 236.648 250.312 236.648 431.72s-94.35 340.774-236.648 431.72c86.244-105.536 140.648-259.82 140.648-431.72s-54.404-326.184-140.648-431.72z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "feed", + "wave", + "radio", + "live", + "broadcast" + ], + "defaultCode": 59677, + "grid": 16 + }, + { + "id": 30, + "paths": [ + "M480 704c88.366 0 160-71.634 160-160v-384c0-88.366-71.634-160-160-160s-160 71.634-160 160v384c0 88.366 71.636 160 160 160zM704 448v96c0 123.71-100.29 224-224 224-123.712 0-224-100.29-224-224v-96h-64v96c0 148.238 112.004 270.3 256 286.22v129.78h-128v64h320v-64h-128v-129.78c143.994-15.92 256-137.982 256-286.22v-96h-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mic", + "microphone", + "voice", + "audio" + ], + "defaultCode": 59678, + "grid": 16 + }, + { + "id": 31, + "paths": [ + "M896 128v832h-672c-53.026 0-96-42.98-96-96s42.974-96 96-96h608v-768h-640c-70.398 0-128 57.6-128 128v768c0 70.4 57.602 128 128 128h768v-896h-64z", + "M224.056 832v0c-0.018 0.002-0.038 0-0.056 0-17.672 0-32 14.326-32 32s14.328 32 32 32c0.018 0 0.038-0.002 0.056-0.002v0.002h607.89v-64h-607.89z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "book", + "read", + "reading" + ], + "defaultCode": 59679, + "grid": 16 + }, + { + "id": 32, + "paths": [ + "M224 128h-192c-17.6 0-32 14.4-32 32v704c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32v-704c0-17.6-14.4-32-32-32zM192 320h-128v-64h128v64z", + "M544 128h-192c-17.6 0-32 14.4-32 32v704c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32v-704c0-17.6-14.4-32-32-32zM512 320h-128v-64h128v64z", + "M765.088 177.48l-171.464 86.394c-15.716 7.918-22.096 27.258-14.178 42.976l287.978 571.548c7.918 15.718 27.258 22.098 42.976 14.178l171.464-86.392c15.716-7.92 22.096-27.26 14.178-42.974l-287.978-571.55c-7.92-15.718-27.26-22.1-42.976-14.18z" + ], + "width": 1152, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "books", + "library", + "archive" + ], + "defaultCode": 59680, + "grid": 16 + }, + { + "id": 33, + "paths": [ + "M1024 960v-64h-64v-384h64v-64h-192v64h64v384h-192v-384h64v-64h-192v64h64v384h-192v-384h64v-64h-192v64h64v384h-192v-384h64v-64h-192v64h64v384h-64v64h-64v64h1088v-64h-64z", + "M512 0h64l512 320v64h-1088v-64l512-320z" + ], + "width": 1088, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "library", + "bank", + "building" + ], + "defaultCode": 59681, + "grid": 16 + }, + { + "id": 34, + "paths": [ + "M864 0h-768c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h768c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM832 896h-704v-768h704v768zM256 448h448v64h-448zM256 576h448v64h-448zM256 704h448v64h-448zM256 320h448v64h-448z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-text", + "file", + "document", + "list", + "paper" + ], + "defaultCode": 59682, + "grid": 16 + }, + { + "id": 35, + "paths": [ + "M864 0h-768c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h768c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM832 896h-704v-768h704v768zM256 576h448v64h-448zM256 704h448v64h-448zM320 288c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96zM480 384h-128c-52.8 0-96 28.8-96 64v64h320v-64c0-35.2-43.2-64-96-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "profile", + "file", + "document", + "page", + "user", + "paper" + ], + "defaultCode": 59683, + "grid": 16 + }, + { + "id": 36, + "paths": [ + "M917.806 229.076c-22.212-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.888 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.984 17.78 50.678 41.878 81.374 72.572zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.326 32 32 32h224v624z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-empty", + "file", + "document", + "paper", + "page", + "new", + "empty", + "blank" + ], + "defaultCode": 59684, + "grid": 16 + }, + { + "id": 37, + "paths": [ + "M917.806 357.076c-22.21-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-368c-44.114 0-80 35.888-80 80v736c0 44.112 35.886 80 80 80h608c44.112 0 80-35.888 80-80v-496c0-14.332-4.372-39.35-42.194-90.924zM785.374 302.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.982 17.78 50.678 41.878 81.374 72.572v0zM896 944c0 8.672-7.328 16-16 16h-608c-8.672 0-16-7.328-16-16v-736c0-8.672 7.328-16 16-16 0 0 367.956-0.002 368 0v224c0 17.672 14.324 32 32 32h224v496z", + "M602.924 42.196c-51.574-37.822-76.592-42.196-90.924-42.196h-368c-44.112 0-80 35.888-80 80v736c0 38.632 27.528 70.958 64 78.39v-814.39c0-8.672 7.328-16 16-16h486.876c-9.646-7.92-19.028-15.26-27.952-21.804z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "files-empty", + "files", + "documents", + "papers", + "pages" + ], + "defaultCode": 59685, + "grid": 16 + }, + { + "id": 38, + "paths": [ + "M917.806 229.076c-22.212-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.888 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.984 17.78 50.678 41.878 81.374 72.572zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.326 32 32 32h224v624z", + "M736 832h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z", + "M736 704h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z", + "M736 576h-448c-17.672 0-32-14.326-32-32s14.328-32 32-32h448c17.674 0 32 14.326 32 32s-14.326 32-32 32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-text", + "file", + "document", + "list", + "paper", + "page" + ], + "defaultCode": 59686, + "grid": 16 + }, + { + "id": 39, + "paths": [ + "M832 896h-640v-128l192-320 263 320 185-128v256z", + "M832 480c0 53.020-42.98 96-96 96-53.022 0-96-42.98-96-96s42.978-96 96-96c53.020 0 96 42.98 96 96z", + "M917.806 229.076c-22.212-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.888 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.984 17.78 50.678 41.878 81.374 72.572zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.326 32 32 32h224v624z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-picture", + "file", + "document", + "file-image" + ], + "defaultCode": 59687, + "grid": 16 + }, + { + "id": 40, + "paths": [ + "M917.806 229.076c-22.21-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.886 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924v0zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.982 17.78 50.678 41.878 81.374 72.572v0zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.324 32 32 32h224v624z", + "M756.288 391.252c-7.414-6.080-17.164-8.514-26.562-6.632l-320 64c-14.958 2.994-25.726 16.126-25.726 31.38v236.876c-18.832-8.174-40.678-12.876-64-12.876-70.692 0-128 42.98-128 96s57.308 96 128 96 128-42.98 128-96v-229.766l256-51.202v133.842c-18.832-8.174-40.678-12.876-64-12.876-70.692 0-128 42.98-128 96s57.308 96 128 96 128-42.98 128-96v-319.998c0-9.586-4.298-18.668-11.712-24.748z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-music", + "file", + "document", + "file-song", + "file-audio" + ], + "defaultCode": 59688, + "grid": 16 + }, + { + "id": 41, + "paths": [ + "M384 384l320 224-320 224v-448z", + "M917.806 229.076c-22.212-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.888 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.984 17.78 50.678 41.878 81.374 72.572zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.326 32 32 32h224v624z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-play", + "file", + "document", + "file-media", + "file-video" + ], + "defaultCode": 59689, + "grid": 16 + }, + { + "id": 42, + "paths": [ + "M917.806 229.076c-22.208-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.594-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.882 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924v0 0zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.98 17.78 50.678 41.878 81.374 72.572v0 0zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.32 32 32 32h224v624z", + "M256 512h320v320h-320v-320z", + "M576 640l192-128v320l-192-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-video", + "file", + "document", + "file-camera" + ], + "defaultCode": 59690, + "grid": 16 + }, + { + "id": 43, + "paths": [ + "M917.806 229.076c-22.208-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.884 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924v0 0zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.98 17.78 50.678 41.878 81.374 72.572v0 0zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.322 32 32 32h224v624z", + "M256 64h128v64h-128v-64z", + "M384 128h128v64h-128v-64z", + "M256 192h128v64h-128v-64z", + "M384 256h128v64h-128v-64z", + "M256 320h128v64h-128v-64z", + "M384 384h128v64h-128v-64z", + "M256 448h128v64h-128v-64z", + "M384 512h128v64h-128v-64z", + "M256 848c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-80v-64h-128v272zM448 768v64h-128v-64h128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-zip", + "file", + "document", + "file-compressed", + "file-type", + "file-format" + ], + "defaultCode": 59691, + "grid": 16 + }, + { + "id": 44, + "paths": [ + "M640 256v-256h-448l-192 192v576h384v256h640v-768h-384zM192 90.51v101.49h-101.49l101.49-101.49zM64 704v-448h192v-192h320v192l-192 192v256h-320zM576 346.51v101.49h-101.49l101.49-101.49zM960 960h-512v-448h192v-192h320v640z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "copy", + "duplicate", + "files", + "pages", + "papers", + "documents" + ], + "defaultCode": 59692, + "grid": 16 + }, + { + "id": 45, + "paths": [ + "M704 128h-128v-64c0-35.2-28.8-64-64-64h-128c-35.204 0-64 28.8-64 64v64h-128v128h512v-128zM512 128h-128v-63.886c0.034-0.038 0.072-0.078 0.114-0.114h127.768c0.042 0.036 0.082 0.076 0.118 0.114v63.886zM832 320v-160c0-17.6-14.4-32-32-32h-64v64h32v128h-192l-192 192v256h-256v-576h32v-64h-64c-17.602 0-32 14.4-32 32v640c0 17.6 14.398 32 32 32h288v192h640v-704h-192zM576 410.51v101.49h-101.49l101.49-101.49zM960 960h-512v-384h192v-192h320v576z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "paste", + "clipboard-file" + ], + "defaultCode": 59693, + "grid": 16 + }, + { + "id": 46, + "paths": [ + "M1024 320l-512-256-512 256 512 256 512-256zM512 148.97l342.058 171.030-342.058 171.030-342.058-171.030 342.058-171.030zM921.444 460.722l102.556 51.278-512 256-512-256 102.556-51.278 409.444 204.722zM921.444 652.722l102.556 51.278-512 256-512-256 102.556-51.278 409.444 204.722z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stack", + "layers" + ], + "defaultCode": 59694, + "grid": 16 + }, + { + "id": 47, + "paths": [ + "M448 128l128 128h448v704h-1024v-832z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder", + "directory", + "category", + "browse" + ], + "defaultCode": 59695, + "grid": 16 + }, + { + "id": 48, + "paths": [ + "M832 960l192-512h-832l-192 512zM128 384l-128 576v-832h288l128 128h416v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder-open", + "directory", + "category", + "browse" + ], + "defaultCode": 59696, + "grid": 16 + }, + { + "id": 49, + "paths": [ + "M576 256l-128-128h-448v832h1024v-704h-448zM704 704h-128v128h-128v-128h-128v-128h128v-128h128v128h128v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder-plus", + "directory", + "folder-add" + ], + "defaultCode": 59697, + "grid": 16 + }, + { + "id": 50, + "paths": [ + "M576 256l-128-128h-448v832h1024v-704h-448zM704 704h-384v-128h384v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder-minus", + "directory", + "folder-remove" + ], + "defaultCode": 59698, + "grid": 16 + }, + { + "id": 51, + "paths": [ + "M576 256l-128-128h-448v832h1024v-704h-448zM512 864l-224-224h160v-256h128v256h160l-224 224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder-download", + "directory", + "folder-save" + ], + "defaultCode": 59699, + "grid": 16 + }, + { + "id": 52, + "paths": [ + "M576 256l-128-128h-448v832h1024v-704h-448zM512 480l224 224h-160v256h-128v-256h-160l224-224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "folder-upload", + "directory", + "folder-load" + ], + "defaultCode": 59700, + "grid": 16 + }, + { + "id": 53, + "paths": [ + "M976 0h-384c-26.4 0-63.274 15.274-81.942 33.942l-476.116 476.116c-18.668 18.668-18.668 49.214 0 67.882l412.118 412.118c18.668 18.668 49.214 18.668 67.882 0l476.118-476.118c18.666-18.666 33.94-55.54 33.94-81.94v-384c0-26.4-21.6-48-48-48zM736 384c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "price-tag" + ], + "defaultCode": 59701, + "grid": 16 + }, + { + "id": 54, + "paths": [ + "M1232 0h-384c-26.4 0-63.274 15.274-81.942 33.942l-476.116 476.116c-18.668 18.668-18.668 49.214 0 67.882l412.118 412.118c18.668 18.668 49.214 18.668 67.882 0l476.118-476.118c18.666-18.666 33.94-55.54 33.94-81.94v-384c0-26.4-21.6-48-48-48zM992 384c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96z", + "M128 544l544-544h-80c-26.4 0-63.274 15.274-81.942 33.942l-476.116 476.116c-18.668 18.668-18.668 49.214 0 67.882l412.118 412.118c18.668 18.668 49.214 18.668 67.882 0l30.058-30.058-416-416z" + ], + "width": 1280, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "price-tags" + ], + "defaultCode": 59702, + "grid": 16 + }, + { + "id": 55, + "paths": [ + "M0 128h128v640h-128zM192 128h64v640h-64zM320 128h64v640h-64zM512 128h64v640h-64zM768 128h64v640h-64zM960 128h64v640h-64zM640 128h32v640h-32zM448 128h32v640h-32zM864 128h32v640h-32zM0 832h64v64h-64zM192 832h64v64h-64zM320 832h64v64h-64zM640 832h64v64h-64zM960 832h64v64h-64zM768 832h128v64h-128zM448 832h128v64h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "barcode" + ], + "defaultCode": 59703, + "grid": 16 + }, + { + "id": 56, + "paths": [ + "M320 64h-256v256h256v-256zM384 0v0 384h-384v-384h384zM128 128h128v128h-128zM960 64h-256v256h256v-256zM1024 0v0 384h-384v-384h384zM768 128h128v128h-128zM320 704h-256v256h256v-256zM384 640v0 384h-384v-384h384zM128 768h128v128h-128zM448 0h64v64h-64zM512 64h64v64h-64zM448 128h64v64h-64zM512 192h64v64h-64zM448 256h64v64h-64zM512 320h64v64h-64zM448 384h64v64h-64zM448 512h64v64h-64zM512 576h64v64h-64zM448 640h64v64h-64zM512 704h64v64h-64zM448 768h64v64h-64zM512 832h64v64h-64zM448 896h64v64h-64zM512 960h64v64h-64zM960 512h64v64h-64zM64 512h64v64h-64zM128 448h64v64h-64zM0 448h64v64h-64zM256 448h64v64h-64zM320 512h64v64h-64zM384 448h64v64h-64zM576 512h64v64h-64zM640 448h64v64h-64zM704 512h64v64h-64zM768 448h64v64h-64zM832 512h64v64h-64zM896 448h64v64h-64zM960 640h64v64h-64zM576 640h64v64h-64zM640 576h64v64h-64zM704 640h64v64h-64zM832 640h64v64h-64zM896 576h64v64h-64zM960 768h64v64h-64zM576 768h64v64h-64zM640 704h64v64h-64zM768 704h64v64h-64zM832 768h64v64h-64zM896 704h64v64h-64zM960 896h64v64h-64zM640 832h64v64h-64zM704 896h64v64h-64zM768 832h64v64h-64zM832 896h64v64h-64zM640 960h64v64h-64zM768 960h64v64h-64zM896 960h64v64h-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "qrcode" + ], + "defaultCode": 59704, + "grid": 16 + }, + { + "id": 57, + "paths": [ + "M575.996 320l127.998 127.998-255.994 255.994-127.998-127.998zM1001.526 297.504l-73.516-73.516-32.008 32.008c-16.378 16.38-39.010 26.51-64 26.51-49.988 0-90.514-40.522-90.514-90.51 0-25.002 10.14-47.638 26.534-64.018l31.988-31.986-73.518-73.516c-29.968-29.968-79.008-29.968-108.976 0l-595.040 595.038c-29.966 29.968-29.966 79.010 0 108.976l73.52 73.518 31.962-31.964c16.382-16.406 39.030-26.552 64.044-26.552 49.988 0 90.51 40.524 90.51 90.51 0 25.006-10.14 47.64-26.534 64.022l-31.984 31.986 73.516 73.518c29.966 29.966 79.008 29.966 108.976 0l595.040-595.040c29.964-29.976 29.964-79.016 0-108.984zM448.002 831.996l-256-256 384-384 256 256-384 384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "ticket", + "theater", + "cinema" + ], + "defaultCode": 59705, + "grid": 16 + }, + { + "id": 58, + "paths": [ + "M384 928c0 53.019-42.981 96-96 96s-96-42.981-96-96c0-53.019 42.981-96 96-96s96 42.981 96 96z", + "M1024 928c0 53.019-42.981 96-96 96s-96-42.981-96-96c0-53.019 42.981-96 96-96s96 42.981 96 96z", + "M1024 512v-384h-768c0-35.346-28.654-64-64-64h-192v64h128l48.074 412.054c-29.294 23.458-48.074 59.5-48.074 99.946 0 70.696 57.308 128 128 128h768v-64h-768c-35.346 0-64-28.654-64-64 0-0.218 0.014-0.436 0.016-0.656l831.984-127.344z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cart", + "purchase", + "ecommerce", + "shopping" + ], + "defaultCode": 59706, + "grid": 16 + }, + { + "id": 59, + "paths": [ + "M480 64c-265.096 0-480 214.904-480 480 0 265.098 214.904 480 480 480 265.098 0 480-214.902 480-480 0-265.096-214.902-480-480-480zM480 928c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.078 0 384 171.922 384 384s-171.922 384-384 384zM512 512v-128h128v-64h-128v-64h-64v64h-128v256h128v128h-128v64h128v64h64v-64h128.002l-0.002-256h-128zM448 512h-64v-128h64v128zM576.002 704h-64.002v-128h64.002v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "coin-dollar", + "money", + "cash", + "currency-dollar" + ], + "defaultCode": 59707, + "grid": 16 + }, + { + "id": 60, + "paths": [ + "M480 64c-265.096 0-480 214.904-480 480s214.904 480 480 480c265.098 0 480-214.902 480-480s-214.902-480-480-480zM480 928c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.076 0 384 171.922 384 384s-171.924 384-384 384z", + "M670.824 644.34c-15.27-8.884-34.862-3.708-43.75 11.57-17.256 29.662-49.088 48.090-83.074 48.090h-128c-41.716 0-77.286-26.754-90.496-64h154.496c17.672 0 32-14.326 32-32s-14.328-32-32-32h-160v-64h160c17.672 0 32-14.328 32-32s-14.328-32-32-32h-154.496c13.21-37.246 48.78-64 90.496-64h128c33.986 0 65.818 18.426 83.074 48.090 8.888 15.276 28.478 20.456 43.752 11.568 15.276-8.888 20.456-28.476 11.568-43.752-28.672-49.288-81.702-79.906-138.394-79.906h-128c-77.268 0-141.914 55.056-156.78 128h-35.22c-17.672 0-32 14.328-32 32s14.328 32 32 32h32v64h-32c-17.672 0-32 14.326-32 32s14.328 32 32 32h35.22c14.866 72.944 79.512 128 156.78 128h128c56.692 0 109.72-30.62 138.394-79.91 8.888-15.276 3.708-34.864-11.57-43.75z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "coin-euro", + "money", + "cash", + "currency-euro" + ], + "defaultCode": 59708, + "grid": 16 + }, + { + "id": 61, + "paths": [ + "M480 64c-265.096 0-480 214.904-480 480s214.904 480 480 480c265.098 0 480-214.902 480-480s-214.902-480-480-480zM480 928c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.074 0 384 171.922 384 384s-171.926 384-384 384z", + "M608 704h-224v-128h96c17.672 0 32-14.326 32-32s-14.328-32-32-32h-96v-32c0-52.934 43.066-96 96-96 34.17 0 66.042 18.404 83.18 48.030 8.85 15.298 28.426 20.526 43.722 11.676 15.296-8.848 20.526-28.424 11.676-43.722-28.538-49.336-81.638-79.984-138.578-79.984-88.224 0-160 71.776-160 160v32h-32c-17.672 0-32 14.326-32 32s14.328 32 32 32h32v192h288c17.674 0 32-14.326 32-32s-14.326-32-32-32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "coin-pound", + "money", + "cash", + "currency-pound" + ], + "defaultCode": 59709, + "grid": 16 + }, + { + "id": 62, + "paths": [ + "M480 64c-265.096 0-480 214.904-480 480s214.904 480 480 480c265.098 0 480-214.902 480-480s-214.902-480-480-480zM480 928c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.076 0 384 171.922 384 384s-171.924 384-384 384z", + "M608 576c17.674 0 32-14.326 32-32s-14.326-32-32-32h-68.208l94.832-142.25c9.804-14.704 5.83-34.572-8.876-44.376-14.704-9.802-34.572-5.83-44.376 8.876l-101.372 152.062-101.374-152.062c-9.804-14.706-29.672-18.68-44.376-8.876-14.706 9.804-18.678 29.672-8.876 44.376l94.834 142.25h-68.208c-17.672 0-32 14.326-32 32s14.328 32 32 32h96v64h-96c-17.672 0-32 14.326-32 32s14.328 32 32 32h96v96c0 17.674 14.328 32 32 32s32-14.326 32-32v-96h96c17.674 0 32-14.326 32-32s-14.326-32-32-32h-96v-64h96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "coin-yen", + "money", + "cash", + "currency-yen" + ], + "defaultCode": 59710, + "grid": 16 + }, + { + "id": 63, + "paths": [ + "M928 128h-832c-52.8 0-96 43.2-96 96v576c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-576c0-52.8-43.2-96-96-96zM96 192h832c17.346 0 32 14.654 32 32v96h-896v-96c0-17.346 14.654-32 32-32zM928 832h-832c-17.346 0-32-14.654-32-32v-288h896v288c0 17.346-14.654 32-32 32zM128 640h64v128h-64zM256 640h64v128h-64zM384 640h64v128h-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "credit-card", + "money", + "payment", + "ecommerce" + ], + "defaultCode": 59711, + "grid": 16 + }, + { + "id": 64, + "paths": [ + "M384 64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.796 64 64 64h320c35.2 0 64-28.8 64-64v-320c0-35.2-28.8-64-64-64zM384 320h-320v-64h320v64zM896 64h-320c-35.204 0-64 28.8-64 64v832c0 35.2 28.796 64 64 64h320c35.2 0 64-28.8 64-64v-832c0-35.2-28.8-64-64-64zM896 640h-320v-64h320v64zM896 448h-320v-64h320v64zM384 576h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.796 64 64 64h320c35.2 0 64-28.8 64-64v-320c0-35.2-28.8-64-64-64zM384 832h-128v128h-64v-128h-128v-64h128v-128h64v128h128v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "calculator", + "compute", + "math", + "arithmetic", + "sum" + ], + "defaultCode": 59712, + "grid": 16 + }, + { + "id": 65, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM320 512c0-106.040 85.96-192 192-192s192 85.96 192 192-85.96 192-192 192-192-85.96-192-192zM925.98 683.476v0l-177.42-73.49c12.518-30.184 19.44-63.276 19.44-97.986s-6.922-67.802-19.44-97.986l177.42-73.49c21.908 52.822 34.020 110.73 34.020 171.476s-12.114 118.654-34.020 171.476v0zM683.478 98.020v0 0l-73.49 177.42c-30.184-12.518-63.276-19.44-97.988-19.44s-67.802 6.922-97.986 19.44l-73.49-177.422c52.822-21.904 110.732-34.018 171.476-34.018 60.746 0 118.654 12.114 171.478 34.020zM98.020 340.524l177.422 73.49c-12.518 30.184-19.442 63.276-19.442 97.986s6.922 67.802 19.44 97.986l-177.42 73.49c-21.906-52.822-34.020-110.73-34.020-171.476s12.114-118.654 34.020-171.476zM340.524 925.98l73.49-177.42c30.184 12.518 63.276 19.44 97.986 19.44s67.802-6.922 97.986-19.44l73.49 177.42c-52.822 21.904-110.73 34.020-171.476 34.020-60.744 0-118.654-12.114-171.476-34.020z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "lifebuoy", + "support", + "help" + ], + "defaultCode": 59713, + "grid": 16 + }, + { + "id": 66, + "paths": [ + "M704 640c-64 64-64 128-128 128s-128-64-192-128-128-128-128-192 64-64 128-128-128-256-192-256-192 192-192 192c0 128 131.5 387.5 256 512s384 256 512 256c0 0 192-128 192-192s-192-256-256-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone", + "telephone", + "contact", + "support", + "call" + ], + "defaultCode": 59714, + "grid": 16 + }, + { + "id": 67, + "paths": [ + "M1017.378 575.994c8.004 55.482 13.216 131.392-11.664 160.446-41.142 48.044-301.712 48.044-301.712-48.042 0-48.398 42.856-80.134 1.712-128.178-40.472-47.262-113.026-48.030-193.714-48.042-80.686 0.012-153.242 0.78-193.714 48.042-41.142 48.046 1.714 79.78 1.714 128.178 0 96.086-260.57 96.086-301.714 48.044-24.878-29.054-19.668-104.964-11.662-160.446 6.16-37.038 21.724-76.996 71.548-127.994 0-0.002 0.002-0.002 0.002-0.004 74.738-69.742 187.846-126.738 429.826-127.968v-0.030c1.344 0 2.664 0.010 4 0.014 1.338-0.004 2.656-0.014 4-0.014v0.028c241.98 1.23 355.088 58.226 429.826 127.968 0.002 0.002 0.002 0.004 0.002 0.004 49.824 50.996 65.39 90.954 71.55 127.994z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "phone-hang-up", + "telephone", + "contact", + "support", + "call" + ], + "defaultCode": 59715, + "grid": 16 + }, + { + "id": 68, + "paths": [ + "M192 0v1024h768v-1024h-768zM576 256.33c70.51 0 127.67 57.16 127.67 127.67s-57.16 127.67-127.67 127.67-127.67-57.16-127.67-127.67 57.16-127.67 127.67-127.67v0zM768 768h-384v-64c0-70.696 57.306-128 128-128v0h128c70.696 0 128 57.304 128 128v64z", + "M64 64h96v192h-96v-192z", + "M64 320h96v192h-96v-192z", + "M64 576h96v192h-96v-192z", + "M64 832h96v192h-96v-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "address-book", + "contact", + "book", + "contacts" + ], + "defaultCode": 59716, + "grid": 16 + }, + { + "id": 69, + "paths": [ + "M928 128h-832c-52.8 0-96 43.2-96 96v640c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-640c0-52.8-43.2-96-96-96zM398.74 550.372l-270.74 210.892v-501.642l270.74 290.75zM176.38 256h671.24l-335.62 252-335.62-252zM409.288 561.698l102.712 110.302 102.71-110.302 210.554 270.302h-626.528l210.552-270.302zM625.26 550.372l270.74-290.75v501.642l-270.74-210.892z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "envelop", + "mail", + "email", + "contact", + "letter" + ], + "defaultCode": 59717, + "grid": 16 + }, + { + "id": 70, + "paths": [ + "M544 0l-96 96 96 96-224 256h-224l176 176-272 360.616v39.384h39.384l360.616-272 176 176v-224l256-224 96 96 96-96-480-480zM448 544l-64-64 224-224 64 64-224 224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pushpin", + "pin" + ], + "defaultCode": 59718, + "grid": 16 + }, + { + "id": 71, + "paths": [ + "M512 0c-176.732 0-320 143.268-320 320 0 320 320 704 320 704s320-384 320-704c0-176.732-143.27-320-320-320zM512 512c-106.040 0-192-85.96-192-192s85.96-192 192-192 192 85.96 192 192-85.96 192-192 192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "location", + "map-marker", + "pin" + ], + "defaultCode": 59719, + "grid": 16 + }, + { + "id": 72, + "paths": [ + "M512 0c-176.732 0-320 143.268-320 320 0 320 320 704 320 704s320-384 320-704c0-176.732-143.27-320-320-320zM512 516c-108.248 0-196-87.752-196-196s87.752-196 196-196 196 87.752 196 196-87.752 196-196 196zM388 320c0-68.483 55.517-124 124-124s124 55.517 124 124c0 68.483-55.517 124-124 124s-124-55.517-124-124z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "location", + "map-marker", + "pin" + ], + "defaultCode": 59720, + "grid": 16 + }, + { + "id": 73, + "paths": [ + "M544.010 1024.004c-2.296 0-4.622-0.25-6.94-0.764-14.648-3.25-25.070-16.238-25.070-31.24v-480h-480c-15.002 0-27.992-10.422-31.24-25.070-3.25-14.646 4.114-29.584 17.708-35.928l960-448c12.196-5.688 26.644-3.144 36.16 6.372 9.516 9.514 12.060 23.966 6.372 36.16l-448 960c-5.342 11.44-16.772 18.47-28.99 18.47zM176.242 448h367.758c17.674 0 32 14.328 32 32v367.758l349.79-749.546-749.548 349.788z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "compass", + "direction", + "location" + ], + "defaultCode": 59721, + "grid": 16 + }, + { + "id": 74, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM96 512c0-229.75 186.25-416 416-416 109.574 0 209.232 42.386 283.534 111.628l-411.534 176.372-176.372 411.534c-69.242-74.302-111.628-173.96-111.628-283.534zM585.166 585.166l-256.082 109.75 109.75-256.082 146.332 146.332zM512 928c-109.574 0-209.234-42.386-283.532-111.628l411.532-176.372 176.372-411.532c69.242 74.298 111.628 173.958 111.628 283.532 0 229.75-186.25 416-416 416z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "compass", + "direction", + "location" + ], + "defaultCode": 59722, + "grid": 16 + }, + { + "id": 75, + "paths": [ + "M0 192l320-128v768l-320 128z", + "M384 32l320 192v736l-320-160z", + "M768 224l256-192v768l-256 192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "map", + "guide" + ], + "defaultCode": 59723, + "grid": 16 + }, + { + "id": 76, + "paths": [ + "M672 192l-320-128-352 128v768l352-128 320 128 352-128v-768l-352 128zM384 145.73l256 102.4v630.138l-256-102.398v-630.14zM64 236.828l256-93.090v631.8l-256 93.088v-631.798zM960 787.172l-256 93.092v-631.8l256-93.090v631.798z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "map", + "guide" + ], + "defaultCode": 59724, + "grid": 16 + }, + { + "id": 77, + "paths": [ + "M640 64c247.424 0 448 200.576 448 448s-200.576 448-448 448v-96c94.024 0 182.418-36.614 248.902-103.098s103.098-154.878 103.098-248.902c0-94.022-36.614-182.418-103.098-248.902s-154.878-103.098-248.902-103.098c-94.022 0-182.418 36.614-248.902 103.098-51.14 51.138-84.582 115.246-97.306 184.902h186.208l-224 256-224-256h164.57c31.060-217.102 217.738-384 443.43-384zM832 448v128h-256v-320h128v192z" + ], + "width": 1088, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "history", + "time", + "archive", + "past" + ], + "defaultCode": 59725, + "grid": 16 + }, + { + "id": 78, + "paths": [ + "M658.744 749.256l-210.744-210.746v-282.51h128v229.49l173.256 173.254zM512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 896c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.078 0 384 171.922 384 384s-171.922 384-384 384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "clock", + "time", + "schedule" + ], + "defaultCode": 59726, + "grid": 16 + }, + { + "id": 79, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM658.744 749.256l-210.744-210.746v-282.51h128v229.49l173.256 173.254-90.512 90.512z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "clock", + "time", + "schedule" + ], + "defaultCode": 59727, + "grid": 16 + }, + { + "id": 80, + "paths": [ + "M512 128c-247.424 0-448 200.576-448 448s200.576 448 448 448 448-200.576 448-448-200.576-448-448-448zM512 936c-198.824 0-360-161.178-360-360 0-198.824 161.176-360 360-360 198.822 0 360 161.176 360 360 0 198.822-161.178 360-360 360zM934.784 287.174c16.042-28.052 25.216-60.542 25.216-95.174 0-106.040-85.96-192-192-192-61.818 0-116.802 29.222-151.92 74.596 131.884 27.236 245.206 105.198 318.704 212.578v0zM407.92 74.596c-35.116-45.374-90.102-74.596-151.92-74.596-106.040 0-192 85.96-192 192 0 34.632 9.174 67.122 25.216 95.174 73.5-107.38 186.822-185.342 318.704-212.578z", + "M512 576v-256h-64v320h256v-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "alarm", + "time", + "clock" + ], + "defaultCode": 59728, + "grid": 16 + }, + { + "id": 81, + "paths": [ + "M1025.5 800c0-288-256-224-256-448 0-18.56-1.788-34.42-5.048-47.928-16.83-113.018-92.156-203.72-189.772-231.36 0.866-3.948 1.32-8.032 1.32-12.21 0-33.278-28.8-60.502-64-60.502s-64 27.224-64 60.5c0 4.18 0.456 8.264 1.32 12.21-109.47 30.998-190.914 141.298-193.254 273.442-0.040 1.92-0.066 3.864-0.066 5.846 0 224.002-256 160.002-256 448.002 0 76.226 170.59 139.996 398.97 156.080 21.524 40.404 64.056 67.92 113.030 67.92s91.508-27.516 113.030-67.92c228.38-16.084 398.97-79.854 398.97-156.080 0-0.228-0.026-0.456-0.028-0.682l1.528 0.682zM826.246 854.096c-54.23 14.47-118.158 24.876-186.768 30.648-5.704-65.418-60.582-116.744-127.478-116.744s-121.774 51.326-127.478 116.744c-68.608-5.772-132.538-16.178-186.768-30.648-74.63-19.914-110.31-42.19-123.368-54.096 13.058-11.906 48.738-34.182 123.368-54.096 86.772-23.152 198.372-35.904 314.246-35.904s227.474 12.752 314.246 35.904c74.63 19.914 110.31 42.19 123.368 54.096-13.058 11.906-48.738 34.182-123.368 54.096z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bell", + "alarm", + "notification" + ], + "defaultCode": 59729, + "grid": 16 + }, + { + "id": 82, + "paths": [ + "M512.002 193.212v-65.212h128v-64c0-35.346-28.654-64-64.002-64h-191.998c-35.346 0-64 28.654-64 64v64h128v65.212c-214.798 16.338-384 195.802-384 414.788 0 229.75 186.25 416 416 416s416-186.25 416-416c0-218.984-169.202-398.448-384-414.788zM706.276 834.274c-60.442 60.44-140.798 93.726-226.274 93.726s-165.834-33.286-226.274-93.726c-60.44-60.44-93.726-140.8-93.726-226.274s33.286-165.834 93.726-226.274c58.040-58.038 134.448-91.018 216.114-93.548l-21.678 314.020c-1.86 26.29 12.464 37.802 31.836 37.802s33.698-11.512 31.836-37.802l-21.676-314.022c81.666 2.532 158.076 35.512 216.116 93.55 60.44 60.44 93.726 140.8 93.726 226.274s-33.286 165.834-93.726 226.274z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stopwatch", + "time", + "speed", + "meter", + "chronometer" + ], + "defaultCode": 59730, + "grid": 16 + }, + { + "id": 83, + "paths": [ + "M320 384h128v128h-128zM512 384h128v128h-128zM704 384h128v128h-128zM128 768h128v128h-128zM320 768h128v128h-128zM512 768h128v128h-128zM320 576h128v128h-128zM512 576h128v128h-128zM704 576h128v128h-128zM128 576h128v128h-128zM832 0v64h-128v-64h-448v64h-128v-64h-128v1024h960v-1024h-128zM896 960h-832v-704h832v704z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "calendar", + "date", + "schedule", + "time", + "day" + ], + "defaultCode": 59731, + "grid": 16 + }, + { + "id": 84, + "paths": [ + "M256 64h512v128h-512v-128z", + "M960 256h-896c-35.2 0-64 28.8-64 64v320c0 35.2 28.794 64 64 64h192v256h512v-256h192c35.2 0 64-28.8 64-64v-320c0-35.2-28.8-64-64-64zM128 448c-35.346 0-64-28.654-64-64s28.654-64 64-64 64 28.654 64 64-28.652 64-64 64zM704 896h-384v-320h384v320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "printer", + "print" + ], + "defaultCode": 59732, + "grid": 16 + }, + { + "id": 85, + "paths": [ + "M1088 128h-1024c-35.2 0-64 28.8-64 64v640c0 35.2 28.8 64 64 64h1024c35.2 0 64-28.8 64-64v-640c0-35.2-28.8-64-64-64zM640 256h128v128h-128v-128zM832 448v128h-128v-128h128zM448 256h128v128h-128v-128zM640 448v128h-128v-128h128zM256 256h128v128h-128v-128zM448 448v128h-128v-128h128zM128 256h64v128h-64v-128zM128 448h128v128h-128v-128zM192 768h-64v-128h64v128zM768 768h-512v-128h512v128zM1024 768h-192v-128h192v128zM1024 576h-128v-128h128v128zM1024 384h-192v-128h192v128z" + ], + "width": 1152, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "keyboard", + "typing", + "type" + ], + "defaultCode": 59733, + "grid": 16 + }, + { + "id": 86, + "paths": [ + "M0 64v640h1024v-640h-1024zM960 640h-896v-512h896v512zM672 768h-320l-32 128-64 64h512l-64-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "display", + "screen", + "monitor", + "computer", + "desktop", + "pc" + ], + "defaultCode": 59734, + "grid": 16 + }, + { + "id": 87, + "paths": [ + "M896 704v-512c0-35.2-28.8-64-64-64h-640c-35.2 0-64 28.8-64 64v512h-128v192h1024v-192h-128zM640 832h-256v-64h256v64zM832 704h-640v-511.886c0.034-0.040 0.076-0.082 0.114-0.114h639.77c0.040 0.034 0.082 0.076 0.116 0.116v511.884z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "laptop", + "computer", + "pc" + ], + "defaultCode": 59735, + "grid": 16 + }, + { + "id": 88, + "paths": [ + "M736 0h-448c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h448c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM384 48h256v32h-256v-32zM512 960c-35.346 0-64-28.654-64-64s28.654-64 64-64 64 28.654 64 64-28.654 64-64 64zM768 768h-512v-640h512v640z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mobile", + "cell-phone", + "handheld" + ], + "defaultCode": 59736, + "grid": 16 + }, + { + "id": 89, + "paths": [ + "M768 0h-576c-35.2 0-64 28.798-64 64v896c0 35.2 28.798 64 64 64h576c35.2 0 64-28.8 64-64v-896c0-35.202-28.8-64-64-64zM480 977.782c-27.492 0-49.782-22.29-49.782-49.782s22.29-49.782 49.782-49.782 49.782 22.29 49.782 49.782-22.29 49.782-49.782 49.782zM768 832h-576v-704h576v704z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mobile", + "cell-phone", + "handheld", + "tablet", + "phablet" + ], + "defaultCode": 59737, + "grid": 16 + }, + { + "id": 90, + "paths": [ + "M800 0h-640c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h640c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM480 992c-17.672 0-32-14.326-32-32s14.328-32 32-32 32 14.326 32 32-14.328 32-32 32zM768 896h-576v-768h576v768z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tablet", + "mobile" + ], + "defaultCode": 59738, + "grid": 16 + }, + { + "id": 91, + "paths": [ + "M981.188 288.108c-88.808-12.768-183.382-22.016-282.076-27.22l164.888-164.888-64-64-224.558 224.556c-21.006-0.368-42.156-0.556-63.442-0.556v0l-256-256-64 64 194.196 194.196c-120.922 4.242-236.338 14.524-343.386 29.912-27.532 107.726-42.81 226.752-42.81 351.892s15.278 244.166 42.804 351.89c143.642 20.652 302.34 32.11 469.196 32.11s325.55-11.458 469.188-32.11c27.534-107.724 42.812-226.75 42.812-351.89s-15.278-244.166-42.812-351.892zM863.892 874.594c-107.73 13.766-226.75 21.406-351.892 21.406s-244.166-7.64-351.892-21.406c-20.648-71.816-32.108-151.166-32.108-234.594 0-83.43 11.458-162.78 32.108-234.596 107.726-13.766 226.75-21.404 351.892-21.404 125.136 0 244.162 7.638 351.886 21.404 20.656 71.816 32.114 151.166 32.114 234.596 0 83.428-11.458 162.778-32.108 234.594z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tv", + "television", + "show" + ], + "defaultCode": 59739, + "grid": 16 + }, + { + "id": 92, + "paths": [ + "M1016.988 652.010l-256-320c-6.074-7.592-15.266-12.010-24.988-12.010h-448c-9.72 0-18.916 4.418-24.988 12.010l-256 320c-4.538 5.674-7.012 12.724-7.012 19.99v288c0 35.346 28.654 64 64 64h896c35.348 0 64-28.654 64-64v-288c0-7.266-2.472-14.316-7.012-19.99zM960 704h-224l-128 128h-192l-128-128h-224v-20.776l239.38-299.224h417.24l239.38 299.224v20.776z", + "M736 512h-448c-17.672 0-32-14.328-32-32s14.328-32 32-32h448c17.674 0 32 14.328 32 32s-14.326 32-32 32z", + "M800 640h-576c-17.672 0-32-14.326-32-32s14.328-32 32-32h576c17.674 0 32 14.326 32 32s-14.326 32-32 32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "drawer", + "box", + "inbox", + "archive", + "category" + ], + "defaultCode": 59740, + "grid": 16 + }, + { + "id": 93, + "paths": [ + "M1016.988 652.010l-256-320c-6.074-7.592-15.266-12.010-24.988-12.010h-448c-9.72 0-18.916 4.418-24.988 12.010l-256 320c-4.538 5.674-7.012 12.724-7.012 19.99v288c0 35.346 28.654 64 64 64h896c35.348 0 64-28.654 64-64v-288c0-7.266-2.472-14.316-7.012-19.99zM960 704h-224l-128 128h-192l-128-128h-224v-20.776l239.38-299.224h417.24l239.38 299.224v20.776z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "drawer", + "box", + "inbox", + "archive", + "category" + ], + "defaultCode": 59741, + "grid": 16 + }, + { + "id": 94, + "paths": [ + "M832 64h-640l-192 192v672c0 17.674 14.326 32 32 32h960c17.672 0 32-14.326 32-32v-672l-192-192zM512 832l-320-256h192v-192h256v192h192l-320 256zM154.51 192l64-64h586.978l64 64h-714.978z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "box-add", + "box", + "download", + "storage", + "inbox", + "archive" + ], + "defaultCode": 59742, + "grid": 16 + }, + { + "id": 95, + "paths": [ + "M832 64h-640l-192 192v672c0 17.674 14.326 32 32 32h960c17.672 0 32-14.326 32-32v-672l-192-192zM640 640v192h-256v-192h-192l320-256 320 256h-192zM154.51 192l64-64h586.976l64 64h-714.976z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "box-remove", + "box", + "upload", + "storage", + "outbox", + "archive" + ], + "defaultCode": 59743, + "grid": 16 + }, + { + "id": 96, + "paths": [ + "M512 576l256-256h-192v-256h-128v256h-192zM744.726 471.272l-71.74 71.742 260.080 96.986-421.066 157.018-421.066-157.018 260.080-96.986-71.742-71.742-279.272 104.728v256l512 192 512-192v-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "download", + "save", + "store", + "arrow" + ], + "defaultCode": 59744, + "grid": 16 + }, + { + "id": 97, + "paths": [ + "M448 576h128v-256h192l-256-256-256 256h192zM640 432v98.712l293.066 109.288-421.066 157.018-421.066-157.018 293.066-109.288v-98.712l-384 144v256l512 192 512-192v-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "upload", + "load", + "arrow" + ], + "defaultCode": 59745, + "grid": 16 + }, + { + "id": 98, + "paths": [ + "M896 0h-896v1024h1024v-896l-128-128zM512 128h128v256h-128v-256zM896 896h-768v-768h64v320h576v-320h74.978l53.022 53.018v714.982z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "floppy-disk", + "save" + ], + "defaultCode": 59746, + "grid": 16 + }, + { + "id": 99, + "paths": [ + "M192 896h640c106.038 0 192-85.96 192-192h-1024c0 106.040 85.962 192 192 192zM832 768h64v64h-64v-64zM960 128h-896l-64 512h1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "drive", + "save", + "hdd", + "hard-disk" + ], + "defaultCode": 59747, + "grid": 16 + }, + { + "id": 100, + "paths": [ + "M512 0c-282.77 0-512 71.634-512 160v128c0 88.366 229.23 160 512 160s512-71.634 512-160v-128c0-88.366-229.23-160-512-160z", + "M512 544c-282.77 0-512-71.634-512-160v192c0 88.366 229.23 160 512 160s512-71.634 512-160v-192c0 88.366-229.23 160-512 160z", + "M512 832c-282.77 0-512-71.634-512-160v192c0 88.366 229.23 160 512 160s512-71.634 512-160v-192c0 88.366-229.23 160-512 160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "database", + "db", + "server", + "host", + "storage", + "save", + "datecenter" + ], + "defaultCode": 59748, + "grid": 16 + }, + { + "id": 101, + "paths": [ + "M512 64c-141.384 0-269.376 57.32-362.032 149.978l-149.968-149.978v384h384l-143.532-143.522c69.496-69.492 165.492-112.478 271.532-112.478 212.068 0 384 171.924 384 384 0 114.696-50.292 217.636-130.018 288l84.666 96c106.302-93.816 173.352-231.076 173.352-384 0-282.77-229.23-512-512-512z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "undo", + "ccw", + "arrow" + ], + "defaultCode": 59749, + "grid": 16 + }, + { + "id": 102, + "paths": [ + "M0 576c0 152.924 67.048 290.184 173.35 384l84.666-96c-79.726-70.364-130.016-173.304-130.016-288 0-212.076 171.93-384 384-384 106.042 0 202.038 42.986 271.53 112.478l-143.53 143.522h384v-384l-149.97 149.978c-92.654-92.658-220.644-149.978-362.030-149.978-282.77 0-512 229.23-512 512z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "redo", + "cw", + "arrow" + ], + "defaultCode": 59750, + "grid": 16 + }, + { + "id": 103, + "paths": [ + "M761.862 1024c113.726-206.032 132.888-520.306-313.862-509.824v253.824l-384-384 384-384v248.372c534.962-13.942 594.57 472.214 313.862 775.628z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "undo", + "left", + "arrow-left" + ], + "defaultCode": 59751, + "grid": 16 + }, + { + "id": 104, + "paths": [ + "M576 248.372v-248.372l384 384-384 384v-253.824c-446.75-10.482-427.588 303.792-313.86 509.824-280.712-303.414-221.1-789.57 313.86-775.628z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "redo", + "right", + "arrow-right" + ], + "defaultCode": 59752, + "grid": 16 + }, + { + "id": 105, + "paths": [ + "M262.14 0c-113.728 206.032-132.89 520.304 313.86 509.824v-253.824l384 384-384 384v-248.372c-534.96 13.942-594.572-472.214-313.86-775.628z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "forward", + "right", + "arrow-right" + ], + "defaultCode": 59753, + "grid": 16 + }, + { + "id": 106, + "paths": [ + "M448 775.628v248.372l-384-384 384-384v253.824c446.75 10.48 427.588-303.792 313.862-509.824 280.71 303.414 221.1 789.57-313.862 775.628z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "reply", + "left", + "arrow-left" + ], + "defaultCode": 59754, + "grid": 16 + }, + { + "id": 107, + "paths": [ + "M512 64c282.77 0 512 186.25 512 416 0 229.752-229.23 416-512 416-27.156 0-53.81-1.734-79.824-5.044-109.978 109.978-241.25 129.7-368.176 132.596v-26.916c68.536-33.578 128-94.74 128-164.636 0-9.754-0.758-19.33-2.164-28.696-115.796-76.264-189.836-192.754-189.836-323.304 0-229.75 229.23-416 512-416z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bubble", + "comment", + "chat", + "talk" + ], + "defaultCode": 59755, + "grid": 16 + }, + { + "id": 108, + "paths": [ + "M1088 901.166c0 45.5 26.028 84.908 64 104.184v15.938c-10.626 1.454-21.472 2.224-32.5 2.224-68.008 0-129.348-28.528-172.722-74.264-26.222 6.982-54.002 10.752-82.778 10.752-159.058 0-288-114.616-288-256s128.942-256 288-256c159.058 0 288 114.616 288 256 0 55.348-19.764 106.592-53.356 148.466-6.824 14.824-10.644 31.312-10.644 48.7zM512 0c278.458 0 504.992 180.614 511.836 405.52-49.182-21.92-103.586-33.52-159.836-33.52-95.56 0-185.816 33.446-254.138 94.178-70.846 62.972-109.862 147.434-109.862 237.822 0 44.672 9.544 87.888 27.736 127.788-5.228 0.126-10.468 0.212-15.736 0.212-27.156 0-53.81-1.734-79.824-5.044-109.978 109.978-241.25 129.7-368.176 132.596v-26.916c68.536-33.578 128-94.74 128-164.636 0-9.754-0.758-19.33-2.164-28.696-115.796-76.264-189.836-192.754-189.836-323.304 0-229.75 229.23-416 512-416z" + ], + "width": 1152, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bubbles", + "comments", + "chat", + "talk" + ], + "defaultCode": 59756, + "grid": 16 + }, + { + "id": 109, + "paths": [ + "M480 0v0c265.096 0 480 173.914 480 388.448s-214.904 388.448-480 388.448c-25.458 0-50.446-1.62-74.834-4.71-103.106 102.694-222.172 121.108-341.166 123.814v-25.134c64.252-31.354 116-88.466 116-153.734 0-9.106-0.712-18.048-2.030-26.794-108.558-71.214-177.97-179.988-177.97-301.89 0-214.534 214.904-388.448 480-388.448zM996 870.686c0 55.942 36.314 104.898 92 131.772v21.542c-103.126-2.318-197.786-18.102-287.142-106.126-21.14 2.65-42.794 4.040-64.858 4.040-95.47 0-183.408-25.758-253.614-69.040 144.674-0.506 281.26-46.854 384.834-130.672 52.208-42.252 93.394-91.826 122.414-147.348 30.766-58.866 46.366-121.582 46.366-186.406 0-10.448-0.45-20.836-1.258-31.168 72.57 59.934 117.258 141.622 117.258 231.676 0 104.488-60.158 197.722-154.24 258.764-1.142 7.496-1.76 15.16-1.76 22.966z" + ], + "width": 1152, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bubbles", + "comments", + "chat", + "talk" + ], + "defaultCode": 59757, + "grid": 16 + }, + { + "id": 110, + "paths": [ + "M512 192c-54.932 0-107.988 8.662-157.694 25.742-46.712 16.054-88.306 38.744-123.628 67.444-66.214 53.798-102.678 122.984-102.678 194.814 0 40.298 11.188 79.378 33.252 116.152 22.752 37.92 56.982 72.586 98.988 100.252 30.356 19.992 50.78 51.948 56.176 87.894 1.8 11.984 2.928 24.088 3.37 36.124 7.47-6.194 14.75-12.846 21.88-19.976 24.154-24.152 56.78-37.49 90.502-37.49 5.368 0 10.762 0.336 16.156 1.024 20.974 2.666 42.398 4.020 63.676 4.020 54.934 0 107.988-8.66 157.694-25.742 46.712-16.054 88.306-38.744 123.628-67.444 66.214-53.796 102.678-122.984 102.678-194.814s-36.464-141.016-102.678-194.814c-35.322-28.698-76.916-51.39-123.628-67.444-49.706-17.080-102.76-25.742-157.694-25.742zM512 64v0c282.77 0 512 186.25 512 416 0 229.752-229.23 416-512 416-27.156 0-53.81-1.734-79.824-5.044-109.978 109.978-241.25 129.7-368.176 132.596v-26.916c68.536-33.578 128-94.74 128-164.636 0-9.754-0.758-19.33-2.164-28.696-115.796-76.264-189.836-192.754-189.836-323.304 0-229.75 229.23-416 512-416z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bubble", + "comment", + "chat", + "talk" + ], + "defaultCode": 59758, + "grid": 16 + }, + { + "id": 111, + "paths": [ + "M1088 901.166c0 45.5 26.028 84.908 64 104.184v15.938c-10.626 1.454-21.472 2.224-32.5 2.224-68.008 0-129.348-28.528-172.722-74.264-26.222 6.982-54.002 10.752-82.778 10.752-159.058 0-288-114.616-288-256s128.942-256 288-256c159.058 0 288 114.616 288 256 0 55.348-19.764 106.592-53.356 148.466-6.824 14.824-10.644 31.312-10.644 48.7zM230.678 221.186c-66.214 53.798-102.678 122.984-102.678 194.814 0 40.298 11.188 79.378 33.252 116.15 22.752 37.92 56.982 72.586 98.988 100.252 30.356 19.992 50.78 51.948 56.176 87.894 1.8 11.984 2.928 24.088 3.37 36.124 7.47-6.194 14.75-12.846 21.88-19.976 24.154-24.152 56.78-37.49 90.502-37.49 5.368 0 10.762 0.336 16.156 1.024 20.948 2.662 42.344 4.016 63.594 4.020v128c-27.128-0.002-53.754-1.738-79.742-5.042-109.978 109.978-241.25 129.7-368.176 132.596v-26.916c68.536-33.578 128-94.74 128-164.636 0-9.754-0.758-19.33-2.164-28.696-115.796-76.264-189.836-192.754-189.836-323.304 0-229.75 229.23-416 512-416 278.458 0 504.992 180.614 511.836 405.52-41.096-18.316-85.84-29.422-132.262-32.578-11.53-56.068-45.402-108.816-98.252-151.756-35.322-28.698-76.916-51.39-123.628-67.444-49.706-17.080-102.76-25.742-157.694-25.742-54.932 0-107.988 8.662-157.694 25.742-46.712 16.054-88.306 38.744-123.628 67.444z" + ], + "width": 1152, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bubbles", + "comments", + "chat", + "talk" + ], + "defaultCode": 59759, + "grid": 16 + }, + { + "id": 112, + "paths": [ + "M480 128c-50.666 0-99.582 7.95-145.386 23.628-42.924 14.694-81.114 35.436-113.502 61.646-60.044 48.59-93.112 110.802-93.112 175.174 0 35.99 10.066 70.948 29.92 103.898 20.686 34.34 51.898 65.794 90.26 90.958 30.44 19.968 50.936 51.952 56.362 87.95 0.902 5.99 1.63 12.006 2.18 18.032 2.722-2.52 5.424-5.114 8.114-7.794 24.138-24.040 56.688-37.312 90.322-37.312 5.348 0 10.718 0.336 16.094 1.018 19.36 2.452 39.124 3.696 58.748 3.696 50.666 0 99.58-7.948 145.384-23.628 42.926-14.692 81.116-35.434 113.504-61.644 60.046-48.59 93.112-110.802 93.112-175.174s-33.066-126.582-93.112-175.174c-32.388-26.212-70.578-46.952-113.504-61.646-45.804-15.678-94.718-23.628-145.384-23.628zM480 0v0c265.096 0 480 173.914 480 388.448s-214.904 388.448-480 388.448c-25.458 0-50.446-1.62-74.834-4.71-103.106 102.694-222.172 121.108-341.166 123.814v-25.134c64.252-31.354 116-88.466 116-153.734 0-9.106-0.712-18.048-2.030-26.794-108.558-71.214-177.97-179.988-177.97-301.89 0-214.534 214.904-388.448 480-388.448zM996 870.686c0 55.942 36.314 104.898 92 131.772v21.542c-103.126-2.318-197.786-18.102-287.142-106.126-21.14 2.65-42.794 4.040-64.858 4.040-95.47 0-183.408-25.758-253.614-69.040 144.674-0.506 281.26-46.854 384.834-130.672 52.208-42.252 93.394-91.826 122.414-147.348 30.766-58.866 46.366-121.582 46.366-186.406 0-10.448-0.45-20.836-1.258-31.168 72.57 59.934 117.258 141.622 117.258 231.676 0 104.488-60.158 197.722-154.24 258.764-1.142 7.496-1.76 15.16-1.76 22.966z" + ], + "width": 1152, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bubbles", + "comments", + "chat", + "talk" + ], + "defaultCode": 59760, + "grid": 16 + }, + { + "id": 113, + "paths": [ + "M576 706.612v-52.78c70.498-39.728 128-138.772 128-237.832 0-159.058 0-288-192-288s-192 128.942-192 288c0 99.060 57.502 198.104 128 237.832v52.78c-217.102 17.748-384 124.42-384 253.388h896c0-128.968-166.898-235.64-384-253.388z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user", + "profile", + "avatar", + "person", + "member" + ], + "defaultCode": 59761, + "grid": 16 + }, + { + "id": 114, + "paths": [ + "M768 770.612v-52.78c70.498-39.728 128-138.772 128-237.832 0-159.058 0-288-192-288s-192 128.942-192 288c0 99.060 57.502 198.104 128 237.832v52.78c-217.102 17.748-384 124.42-384 253.388h896c0-128.968-166.898-235.64-384-253.388z", + "M327.196 795.328c55.31-36.15 124.080-63.636 199.788-80.414-15.054-17.784-28.708-37.622-40.492-59.020-30.414-55.234-46.492-116.058-46.492-175.894 0-86.042 0-167.31 30.6-233.762 29.706-64.504 83.128-104.496 159.222-119.488-16.914-76.48-61.94-126.75-181.822-126.75-192 0-192 128.942-192 288 0 99.060 57.502 198.104 128 237.832v52.78c-217.102 17.748-384 124.42-384 253.388h279.006c14.518-12.91 30.596-25.172 48.19-36.672z" + ], + "width": 1152, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "users", + "group", + "team", + "members", + "community", + "collaborate" + ], + "defaultCode": 59762, + "grid": 16 + }, + { + "id": 115, + "paths": [ + "M384 736c0-151.234 95.874-280.486 230.032-330.2 16.28-36.538 25.968-77.164 25.968-117.8 0-159.058 0-288-192-288s-192 128.942-192 288c0 99.060 57.502 198.104 128 237.832v52.78c-217.102 17.748-384 124.42-384 253.388h397.306c-8.664-30.53-13.306-62.732-13.306-96z", + "M736 448c-159.058 0-288 128.942-288 288s128.942 288 288 288c159.056 0 288-128.942 288-288s-128.942-288-288-288zM896 768h-128v128h-64v-128h-128v-64h128v-128h64v128h128v64z" + ], + "width": 1024, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user-plus", + "user", + "user-add", + "profile", + "avatar", + "person", + "member" + ], + "defaultCode": 59763, + "grid": 16 + }, + { + "id": 116, + "paths": [ + "M384 736c0-151.234 95.874-280.486 230.032-330.2 16.28-36.538 25.968-77.164 25.968-117.8 0-159.058 0-288-192-288s-192 128.942-192 288c0 99.060 57.502 198.104 128 237.832v52.78c-217.102 17.748-384 124.42-384 253.388h397.306c-8.664-30.53-13.306-62.732-13.306-96z", + "M736 448c-159.058 0-288 128.942-288 288s128.942 288 288 288c159.056 0 288-128.942 288-288s-128.942-288-288-288zM896 768h-320v-64h320v64z" + ], + "width": 1024, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user-minus", + "user", + "user-remove", + "profile", + "avatar", + "person", + "member" + ], + "defaultCode": 59764, + "grid": 16 + }, + { + "id": 117, + "paths": [ + "M960 608l-288 288-96-96-64 64 160 160 352-352z", + "M448 768h320v-115.128c-67.22-39.2-156.308-66.11-256-74.26v-52.78c70.498-39.728 128-138.772 128-237.832 0-159.058 0-288-192-288s-192 128.942-192 288c0 99.060 57.502 198.104 128 237.832v52.78c-217.102 17.748-384 124.42-384 253.388h448v-64z" + ], + "width": 1024, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user-check", + "user", + "user-tick", + "profile", + "avatar", + "person", + "member" + ], + "defaultCode": 59765, + "grid": 16 + }, + { + "id": 118, + "paths": [ + "M320 192c0-106.039 85.961-192 192-192s192 85.961 192 192c0 106.039-85.961 192-192 192s-192-85.961-192-192zM768.078 448h-35.424l-199.104 404.244 74.45-372.244-96-96-96 96 74.45 372.244-199.102-404.244h-35.424c-127.924 0-127.924 85.986-127.924 192v320h768v-320c0-106.014 0-192-127.922-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "user-tie", + "user", + "user-employee", + "profile", + "avatar", + "person", + "member", + "job", + "official" + ], + "defaultCode": 59766, + "grid": 16 + }, + { + "id": 119, + "paths": [ + "M225 448c123.712 0 224 100.29 224 224 0 123.712-100.288 224-224 224s-224-100.288-224-224l-1-32c0-247.424 200.576-448 448-448v128c-85.474 0-165.834 33.286-226.274 93.726-11.634 11.636-22.252 24.016-31.83 37.020 11.438-1.8 23.16-2.746 35.104-2.746zM801 448c123.71 0 224 100.29 224 224 0 123.712-100.29 224-224 224s-224-100.288-224-224l-1-32c0-247.424 200.576-448 448-448v128c-85.474 0-165.834 33.286-226.274 93.726-11.636 11.636-22.254 24.016-31.832 37.020 11.44-1.8 23.16-2.746 35.106-2.746z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "quotes-left", + "ldquo" + ], + "defaultCode": 59767, + "grid": 16 + }, + { + "id": 120, + "paths": [ + "M800 640c-123.712 0-224-100.29-224-224 0-123.712 100.288-224 224-224s224 100.288 224 224l1 32c0 247.424-200.576 448-448 448v-128c85.474 0 165.834-33.286 226.274-93.726 11.634-11.636 22.252-24.016 31.83-37.020-11.438 1.8-23.16 2.746-35.104 2.746zM224 640c-123.71 0-224-100.29-224-224 0-123.712 100.29-224 224-224s224 100.288 224 224l1 32c0 247.424-200.576 448-448 448v-128c85.474 0 165.834-33.286 226.274-93.726 11.636-11.636 22.254-24.016 31.832-37.020-11.44 1.8-23.16 2.746-35.106 2.746z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "quotes-right", + "rdquo" + ], + "defaultCode": 59768, + "grid": 16 + }, + { + "id": 121, + "paths": [ + "M728.992 512c137.754-87.334 231.008-255.208 231.008-448 0-21.676-1.192-43.034-3.478-64h-889.042c-2.29 20.968-3.48 42.326-3.48 64 0 192.792 93.254 360.666 231.006 448-137.752 87.334-231.006 255.208-231.006 448 0 21.676 1.19 43.034 3.478 64h889.042c2.288-20.966 3.478-42.324 3.478-64 0.002-192.792-93.252-360.666-231.006-448zM160 960c0-186.912 80.162-345.414 224-397.708v-100.586c-143.838-52.29-224-210.792-224-397.706v0h704c0 186.914-80.162 345.416-224 397.706v100.586c143.838 52.294 224 210.796 224 397.708h-704zM619.626 669.594c-71.654-40.644-75.608-93.368-75.626-125.366v-64.228c0-31.994 3.804-84.914 75.744-125.664 38.504-22.364 71.808-56.348 97.048-98.336h-409.582c25.266 42.032 58.612 76.042 97.166 98.406 71.654 40.644 75.606 93.366 75.626 125.366v64.228c0 31.992-3.804 84.914-75.744 125.664-72.622 42.18-126.738 125.684-143.090 226.336h501.67c-16.364-100.708-70.53-184.248-143.212-226.406z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hour-glass", + "loading", + "busy", + "wait" + ], + "defaultCode": 59769, + "grid": 16 + }, + { + "id": 122, + "paths": [ + "M384 128c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM655.53 240.47c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM832 512c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM719.53 783.53c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM448.002 896c0 0 0 0 0 0 0-35.346 28.654-64 64-64s64 28.654 64 64c0 0 0 0 0 0 0 35.346-28.654 64-64 64s-64-28.654-64-64zM176.472 783.53c0 0 0 0 0 0 0-35.346 28.654-64 64-64s64 28.654 64 64c0 0 0 0 0 0 0 35.346-28.654 64-64 64s-64-28.654-64-64zM144.472 240.47c0 0 0 0 0 0 0-53.019 42.981-96 96-96s96 42.981 96 96c0 0 0 0 0 0 0 53.019-42.981 96-96 96s-96-42.981-96-96zM56 512c0-39.765 32.235-72 72-72s72 32.235 72 72c0 39.765-32.235 72-72 72s-72-32.235-72-72z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59770, + "grid": 16 + }, + { + "id": 123, + "paths": [ + "M1024 512c-1.278-66.862-15.784-133.516-42.576-194.462-26.704-61-65.462-116.258-113.042-161.92-47.552-45.696-103.944-81.82-164.984-105.652-61.004-23.924-126.596-35.352-191.398-33.966-64.81 1.282-129.332 15.374-188.334 41.356-59.048 25.896-112.542 63.47-156.734 109.576-44.224 46.082-79.16 100.708-102.186 159.798-23.114 59.062-34.128 122.52-32.746 185.27 1.286 62.76 14.964 125.148 40.134 182.206 25.088 57.1 61.476 108.828 106.11 151.548 44.61 42.754 97.472 76.504 154.614 98.72 57.118 22.304 118.446 32.902 179.142 31.526 60.708-1.29 120.962-14.554 176.076-38.914 55.15-24.282 105.116-59.48 146.366-102.644 41.282-43.14 73.844-94.236 95.254-149.43 13.034-33.458 21.88-68.4 26.542-103.798 1.246 0.072 2.498 0.12 3.762 0.12 35.346 0 64-28.652 64-64 0-1.796-0.094-3.572-0.238-5.332h0.238zM922.306 681.948c-23.472 53.202-57.484 101.4-99.178 141.18-41.67 39.81-91 71.186-144.244 91.79-53.228 20.678-110.29 30.452-166.884 29.082-56.604-1.298-112.596-13.736-163.82-36.474-51.25-22.666-97.684-55.49-135.994-95.712-38.338-40.198-68.528-87.764-88.322-139.058-19.87-51.284-29.228-106.214-27.864-160.756 1.302-54.552 13.328-108.412 35.254-157.69 21.858-49.3 53.498-93.97 92.246-130.81 38.73-36.868 84.53-65.87 133.874-84.856 49.338-19.060 102.136-28.006 154.626-26.644 52.5 1.306 104.228 12.918 151.562 34.034 47.352 21.050 90.256 51.502 125.624 88.782 35.396 37.258 63.21 81.294 81.39 128.688 18.248 47.392 26.782 98.058 25.424 148.496h0.238c-0.144 1.76-0.238 3.536-0.238 5.332 0 33.012 24.992 60.174 57.086 63.624-6.224 34.822-16.53 68.818-30.78 100.992z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59771, + "grid": 16 + }, + { + "id": 124, + "paths": [ + "M512 303.096c-32.964 0-59.686-26.724-59.686-59.686v-179.060c0-32.964 26.722-59.686 59.686-59.686 32.962 0 59.688 26.722 59.688 59.686v179.060c0 32.964-26.726 59.686-59.688 59.686z", + "M512 996.956c-20.602 0-37.304-16.702-37.304-37.304v-179.060c0-20.602 16.702-37.304 37.304-37.304 20.604 0 37.304 16.704 37.304 37.304v179.060c0 20.602-16.7 37.304-37.304 37.304z", + "M377.756 335.36c-19.34 0-38.146-10.034-48.512-27.988l-89.53-155.070c-15.452-26.764-6.282-60.986 20.482-76.438 26.762-15.45 60.986-6.284 76.438 20.482l89.53 155.072c15.452 26.764 6.282 60.986-20.482 76.438-8.81 5.084-18.432 7.504-27.926 7.504z", + "M735.856 933.256c-11.602 0-22.886-6.022-29.108-16.792l-89.53-155.070c-9.27-16.056-3.77-36.592 12.29-45.864 16.056-9.264 36.59-3.77 45.864 12.292l89.532 155.068c9.27 16.058 3.768 36.592-12.292 45.864-5.286 3.048-11.060 4.502-16.756 4.502z", + "M279.344 429.94c-8.86 0-17.838-2.256-26.064-7.006l-155.072-89.53c-24.978-14.422-33.538-46.362-19.116-71.342 14.42-24.978 46.364-33.538 71.342-19.116l155.070 89.53c24.98 14.422 33.538 46.362 19.116 71.34-9.668 16.756-27.226 26.124-45.276 26.124z", + "M899.648 765.674c-5.064 0-10.196-1.29-14.894-4.004l-155.068-89.53c-14.274-8.24-19.164-26.494-10.924-40.768 8.242-14.276 26.496-19.166 40.766-10.924l155.070 89.532c14.274 8.24 19.164 26.492 10.924 40.766-5.53 9.574-15.562 14.928-25.874 14.928z", + "M243.41 560.496h-179.060c-26.784 0-48.496-21.712-48.496-48.496s21.712-48.496 48.496-48.496h179.060c26.784 0 48.496 21.712 48.496 48.496s-21.712 48.496-48.496 48.496z", + "M959.65 541.844c-0.002 0 0 0 0 0h-179.060c-16.482-0.002-29.844-13.364-29.844-29.844s13.364-29.844 29.844-29.844c0.002 0 0 0 0 0h179.060c16.482 0 29.844 13.362 29.844 29.844 0 16.48-13.364 29.844-29.844 29.844z", + "M124.366 780.598c-15.472 0-30.518-8.028-38.81-22.39-12.362-21.41-5.026-48.79 16.384-61.148l155.072-89.532c21.41-12.368 48.79-5.028 61.15 16.384 12.362 21.412 5.026 48.79-16.384 61.15l-155.072 89.53c-7.050 4.070-14.748 6.006-22.34 6.006z", + "M744.632 407.552c-10.314 0-20.346-5.352-25.874-14.926-8.24-14.274-3.35-32.526 10.924-40.768l155.070-89.528c14.272-8.236 32.526-3.352 40.768 10.922 8.24 14.274 3.35 32.526-10.924 40.768l-155.070 89.528c-4.7 2.714-9.83 4.004-14.894 4.004z", + "M288.136 940.716c-6.962 0-14.016-1.774-20.48-5.504-19.626-11.332-26.35-36.428-15.020-56.054l89.53-155.070c11.33-19.628 36.426-26.352 56.054-15.022 19.626 11.332 26.35 36.43 15.020 56.054l-89.53 155.072c-7.598 13.166-21.392 20.524-35.574 20.524z", + "M646.266 309.242c-5.062 0-10.196-1.29-14.894-4.002-14.274-8.242-19.164-26.494-10.924-40.766l89.534-155.070c8.24-14.274 26.492-19.166 40.766-10.922 14.274 8.242 19.164 26.494 10.924 40.766l-89.532 155.070c-5.53 9.57-15.56 14.924-25.874 14.924z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59772, + "grid": 16 + }, + { + "id": 125, + "paths": [ + "M192 512c0-12.18 0.704-24.196 2.030-36.022l-184.98-60.104c-5.916 31.14-9.050 63.264-9.050 96.126 0 147.23 62.166 279.922 161.654 373.324l114.284-157.296c-52.124-56.926-83.938-132.758-83.938-216.028zM832 512c0 83.268-31.812 159.102-83.938 216.028l114.284 157.296c99.488-93.402 161.654-226.094 161.654-373.324 0-32.862-3.132-64.986-9.048-96.126l-184.98 60.104c1.324 11.828 2.028 23.842 2.028 36.022zM576 198.408c91.934 18.662 169.544 76.742 214.45 155.826l184.978-60.102c-73.196-155.42-222.24-268.060-399.428-290.156v194.432zM233.55 354.232c44.906-79.084 122.516-137.164 214.45-155.826v-194.43c-177.188 22.096-326.23 134.736-399.426 290.154l184.976 60.102zM644.556 803.328c-40.39 18.408-85.272 28.672-132.556 28.672s-92.166-10.264-132.554-28.67l-114.292 157.31c73.206 40.366 157.336 63.36 246.846 63.36s173.64-22.994 246.848-63.36l-114.292-157.312z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59773, + "grid": 16 + }, + { + "id": 126, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 256c141.384 0 256 114.616 256 256s-114.616 256-256 256-256-114.616-256-256 114.616-256 256-256zM817.47 817.47c-81.594 81.594-190.080 126.53-305.47 126.53-115.392 0-223.876-44.936-305.47-126.53s-126.53-190.078-126.53-305.47c0-115.39 44.936-223.876 126.53-305.47l67.882 67.882c0 0 0 0 0 0-131.006 131.006-131.006 344.17 0 475.176 63.462 63.462 147.838 98.412 237.588 98.412 89.748 0 174.124-34.95 237.588-98.412 131.006-131.006 131.006-344.168 0-475.176l67.882-67.882c81.594 81.594 126.53 190.080 126.53 305.47 0 115.392-44.936 223.876-126.53 305.47z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59774, + "grid": 16 + }, + { + "id": 127, + "paths": [ + "M384 128c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM790.994 512c0 0 0 0 0 0 0-57.993 47.013-105.006 105.006-105.006s105.006 47.013 105.006 105.006c0 0 0 0 0 0 0 57.993-47.013 105.006-105.006 105.006s-105.006-47.013-105.006-105.006zM688.424 783.53c0-52.526 42.58-95.106 95.106-95.106s95.106 42.58 95.106 95.106c0 52.526-42.58 95.106-95.106 95.106s-95.106-42.58-95.106-95.106zM425.862 896c0-47.573 38.565-86.138 86.138-86.138s86.138 38.565 86.138 86.138c0 47.573-38.565 86.138-86.138 86.138s-86.138-38.565-86.138-86.138zM162.454 783.53c0-43.088 34.93-78.018 78.018-78.018s78.018 34.93 78.018 78.018c0 43.088-34.93 78.018-78.018 78.018s-78.018-34.93-78.018-78.018zM57.338 512c0-39.026 31.636-70.662 70.662-70.662s70.662 31.636 70.662 70.662c0 39.026-31.636 70.662-70.662 70.662s-70.662-31.636-70.662-70.662zM176.472 240.472c0 0 0 0 0 0 0-35.346 28.654-64 64-64s64 28.654 64 64c0 0 0 0 0 0 0 35.346-28.654 64-64 64s-64-28.654-64-64zM899.464 240.472c0 64.024-51.906 115.934-115.936 115.934-64.024 0-115.936-51.91-115.936-115.934 0-64.032 51.912-115.934 115.936-115.934 64.030 0 115.936 51.902 115.936 115.934z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59775, + "grid": 16 + }, + { + "id": 128, + "paths": [ + "M416 928c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96zM0 512c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96zM832 512c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96zM121.844 217.844c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96zM710.156 806.156c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96zM121.844 806.156c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96zM710.156 217.844c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59776, + "grid": 16 + }, + { + "id": 129, + "paths": [ + "M512 1024c-136.76 0-265.334-53.258-362.040-149.96-96.702-96.706-149.96-225.28-149.96-362.040 0-96.838 27.182-191.134 78.606-272.692 50-79.296 120.664-143.372 204.356-185.3l43 85.832c-68.038 34.084-125.492 86.186-166.15 150.67-41.746 66.208-63.812 142.798-63.812 221.49 0 229.382 186.618 416 416 416s416-186.618 416-416c0-78.692-22.066-155.282-63.81-221.49-40.66-64.484-98.114-116.584-166.15-150.67l43-85.832c83.692 41.928 154.358 106.004 204.356 185.3 51.422 81.558 78.604 175.854 78.604 272.692 0 136.76-53.258 265.334-149.96 362.040-96.706 96.702-225.28 149.96-362.040 149.96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59777, + "grid": 16 + }, + { + "id": 130, + "paths": [ + "M512 0c-278.748 0-505.458 222.762-511.848 499.974 5.92-241.864 189.832-435.974 415.848-435.974 229.75 0 416 200.576 416 448 0 53.020 42.98 96 96 96s96-42.98 96-96c0-282.77-229.23-512-512-512zM512 1024c278.748 0 505.458-222.762 511.848-499.974-5.92 241.864-189.832 435.974-415.848 435.974-229.75 0-416-200.576-416-448 0-53.020-42.98-96-96-96s-96 42.98-96 96c0 282.77 229.23 512 512 512z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59778, + "grid": 16 + }, + { + "id": 131, + "paths": [ + "M0.042 513.618l-0.022 0.004c0 0 0.012 0.090 0.028 0.222 0.11 3.878 0.55 7.676 1.322 11.352 0.204 1.746 0.428 3.66 0.674 5.774 0.222 1.886 0.46 3.914 0.718 6.078 0.374 2.566 0.77 5.292 1.19 8.176 0.856 5.746 1.8 12.124 2.908 18.958 1.348 6.446 2.804 13.414 4.364 20.864 0.71 3.718 1.776 7.504 2.786 11.406 1.024 3.89 2.078 7.894 3.16 12.004 0.566 2.042 1.040 4.132 1.708 6.208 0.656 2.074 1.32 4.176 1.988 6.3 1.348 4.234 2.726 8.566 4.136 12.988 0.352 1.106 0.708 2.21 1.064 3.324 0.408 1.102 0.814 2.208 1.226 3.316 0.826 2.218 1.658 4.458 2.502 6.714 1.696 4.496 3.422 9.078 5.18 13.742 1.968 4.566 3.97 9.214 6.004 13.934 1.018 2.348 2.044 4.714 3.078 7.098 1.048 2.376 2.27 4.704 3.408 7.074 2.322 4.714 4.678 9.496 7.062 14.332 2.47 4.786 5.208 9.512 7.846 14.328 1.336 2.398 2.68 4.808 4.028 7.23 1.368 2.41 2.902 4.75 4.356 7.14 2.95 4.738 5.93 9.524 8.934 14.348 12.64 18.894 26.676 37.566 42.21 55.278 15.712 17.578 32.726 34.25 50.692 49.602 18.18 15.136 37.264 28.902 56.726 41.114 19.604 12.036 39.644 22.312 59.376 31.144 5.004 2.040 9.964 4.062 14.878 6.066 2.462 0.972 4.868 2.032 7.336 2.918 2.47 0.868 4.93 1.734 7.376 2.594 4.898 1.684 9.678 3.468 14.484 4.992 4.832 1.43 9.604 2.844 14.312 4.242 2.356 0.672 4.66 1.426 7.004 2.012 2.346 0.574 4.676 1.14 6.986 1.704 4.606 1.118 9.142 2.214 13.604 3.296 4.5 0.868 8.926 1.722 13.27 2.558 2.166 0.41 4.31 0.82 6.434 1.222 1.062 0.2 2.118 0.398 3.166 0.598 1.060 0.148 2.118 0.292 3.166 0.442 4.192 0.582 8.292 1.152 12.3 1.71 1.998 0.274 3.972 0.546 5.922 0.816 1.946 0.286 3.904 0.378 5.814 0.57 3.822 0.336 7.544 0.664 11.164 0.98 3.616 0.304 7.104 0.688 10.526 0.738 0.23 0.008 0.452 0.016 0.682 0.026 0.614 34.812 29.008 62.846 63.968 62.846 0.542 0 1.080-0.028 1.62-0.042v0.022c0 0 0.090-0.012 0.224-0.028 3.878-0.11 7.674-0.55 11.35-1.322 1.748-0.204 3.662-0.426 5.776-0.672 1.884-0.222 3.912-0.462 6.076-0.718 2.566-0.376 5.292-0.772 8.176-1.192 5.746-0.856 12.124-1.8 18.958-2.908 6.446-1.348 13.414-2.804 20.864-4.362 3.718-0.712 7.504-1.778 11.406-2.786 3.892-1.026 7.894-2.080 12.004-3.162 2.044-0.566 4.132-1.040 6.208-1.708 2.074-0.656 4.174-1.318 6.3-1.988 4.232-1.348 8.564-2.726 12.988-4.134 1.104-0.354 2.21-0.708 3.324-1.066 1.1-0.406 2.206-0.814 3.316-1.226 2.216-0.824 4.456-1.658 6.714-2.5 4.496-1.698 9.078-3.424 13.74-5.182 4.568-1.968 9.216-3.97 13.936-6.004 2.348-1.018 4.714-2.044 7.098-3.078 2.376-1.048 4.702-2.27 7.074-3.408 4.714-2.322 9.494-4.678 14.33-7.062 4.786-2.47 9.512-5.208 14.328-7.846 2.398-1.336 4.808-2.678 7.23-4.028 2.41-1.366 4.75-2.9 7.14-4.354 4.738-2.952 9.524-5.93 14.35-8.936 18.89-12.64 37.564-26.674 55.278-42.21 17.574-15.712 34.248-32.726 49.602-50.69 15.136-18.182 28.902-37.264 41.112-56.728 12.036-19.602 22.314-39.644 31.142-59.376 2.042-5.002 4.062-9.964 6.068-14.878 0.974-2.462 2.032-4.868 2.918-7.334 0.87-2.472 1.732-4.932 2.592-7.376 1.686-4.898 3.468-9.678 4.994-14.484 1.432-4.832 2.846-9.604 4.24-14.31 0.674-2.358 1.43-4.66 2.016-7.004 0.57-2.348 1.138-4.676 1.702-6.988 1.118-4.606 2.216-9.14 3.296-13.602 0.868-4.502 1.72-8.928 2.558-13.272 0.41-2.164 0.818-4.308 1.222-6.434 0.2-1.060 0.398-2.116 0.596-3.164 0.148-1.062 0.296-2.118 0.444-3.168 0.582-4.19 1.152-8.292 1.708-12.3 0.278-1.996 0.55-3.97 0.82-5.922 0.284-1.946 0.376-3.902 0.568-5.812 0.336-3.822 0.664-7.546 0.98-11.164 0.304-3.616 0.686-7.106 0.738-10.528 0.020-0.534 0.040-1.044 0.058-1.574 35.224-0.146 63.732-28.738 63.732-63.992 0-0.542-0.028-1.080-0.042-1.62h0.022c0 0-0.012-0.090-0.028-0.224-0.11-3.878-0.55-7.674-1.322-11.35-0.204-1.748-0.428-3.662-0.674-5.776-0.222-1.886-0.46-3.914-0.718-6.076-0.374-2.566-0.77-5.294-1.19-8.176-0.856-5.746-1.8-12.124-2.908-18.958-1.348-6.444-2.804-13.414-4.364-20.862-0.71-3.72-1.776-7.506-2.786-11.408-1.024-3.892-2.078-7.894-3.16-12.002-0.566-2.044-1.040-4.134-1.708-6.208-0.656-2.076-1.32-4.174-1.988-6.3-1.348-4.234-2.726-8.566-4.136-12.99-0.352-1.102-0.708-2.21-1.064-3.324-0.408-1.1-0.814-2.206-1.226-3.316-0.826-2.216-1.658-4.454-2.502-6.714-1.696-4.498-3.422-9.080-5.18-13.74-1.968-4.57-3.97-9.216-6.004-13.936-1.020-2.348-2.044-4.714-3.078-7.098-1.048-2.376-2.27-4.702-3.408-7.076-2.322-4.714-4.678-9.494-7.062-14.33-2.47-4.786-5.208-9.512-7.846-14.328-1.336-2.398-2.68-4.808-4.028-7.23-1.368-2.41-2.902-4.75-4.356-7.14-2.95-4.74-5.93-9.524-8.934-14.35-12.64-18.892-26.676-37.564-42.21-55.278-15.712-17.576-32.726-34.25-50.692-49.602-18.18-15.136-37.264-28.902-56.726-41.112-19.604-12.036-39.644-22.314-59.376-31.142-5.004-2.040-9.964-4.062-14.878-6.068-2.462-0.974-4.868-2.032-7.336-2.918-2.47-0.87-4.93-1.734-7.376-2.592-4.898-1.684-9.678-3.468-14.484-4.994-4.832-1.432-9.604-2.846-14.312-4.242-2.356-0.672-4.66-1.428-7.004-2.014-2.346-0.572-4.676-1.138-6.986-1.702-4.606-1.118-9.142-2.216-13.604-3.298-4.5-0.868-8.926-1.72-13.27-2.558-2.166-0.412-4.31-0.82-6.434-1.222-1.062-0.2-2.118-0.398-3.166-0.596-1.060-0.148-2.118-0.296-3.166-0.442-4.192-0.584-8.292-1.154-12.3-1.71-1.998-0.276-3.972-0.55-5.922-0.82-1.946-0.284-3.904-0.376-5.814-0.57-3.822-0.336-7.544-0.664-11.164-0.98-3.616-0.304-7.104-0.686-10.526-0.738-0.852-0.032-1.674-0.062-2.512-0.092-0.65-34.78-29.028-62.778-63.966-62.778-0.542 0-1.080 0.028-1.62 0.042l-0.002-0.022c0 0-0.090 0.012-0.222 0.028-3.878 0.11-7.676 0.55-11.352 1.322-1.748 0.204-3.662 0.426-5.776 0.672-1.884 0.222-3.912 0.462-6.076 0.718-2.566 0.376-5.292 0.772-8.176 1.192-5.746 0.856-12.124 1.8-18.958 2.908-6.446 1.348-13.414 2.804-20.864 4.362-3.718 0.712-7.504 1.778-11.406 2.786-3.892 1.026-7.894 2.080-12.004 3.162-2.044 0.566-4.132 1.040-6.208 1.708-2.074 0.656-4.174 1.318-6.3 1.988-4.232 1.348-8.564 2.726-12.988 4.134-1.104 0.354-2.21 0.708-3.324 1.066-1.1 0.406-2.206 0.814-3.316 1.226-2.216 0.824-4.456 1.658-6.714 2.5-4.496 1.698-9.078 3.424-13.74 5.182-4.568 1.968-9.216 3.97-13.936 6.004-2.348 1.018-4.714 2.044-7.098 3.078-2.376 1.048-4.702 2.27-7.074 3.408-4.714 2.322-9.494 4.678-14.33 7.062-4.786 2.47-9.512 5.208-14.328 7.846-2.398 1.336-4.808 2.678-7.23 4.028-2.41 1.366-4.75 2.9-7.14 4.354-4.738 2.952-9.524 5.93-14.35 8.936-18.89 12.64-37.564 26.674-55.278 42.21-17.574 15.712-34.248 32.726-49.602 50.69-15.136 18.182-28.902 37.264-41.112 56.728-12.036 19.602-22.314 39.644-31.142 59.376-2.042 5.002-4.062 9.964-6.068 14.878-0.974 2.462-2.032 4.868-2.918 7.334-0.87 2.472-1.732 4.932-2.592 7.376-1.686 4.898-3.468 9.678-4.994 14.484-1.432 4.832-2.846 9.604-4.24 14.31-0.674 2.358-1.43 4.66-2.016 7.004-0.57 2.348-1.138 4.676-1.702 6.988-1.118 4.606-2.216 9.14-3.296 13.602-0.868 4.502-1.72 8.928-2.558 13.272-0.41 2.164-0.818 4.308-1.222 6.434-0.2 1.060-0.398 2.116-0.596 3.164-0.148 1.062-0.296 2.118-0.444 3.168-0.582 4.19-1.152 8.292-1.708 12.3-0.278 1.996-0.55 3.97-0.82 5.922-0.284 1.946-0.376 3.902-0.568 5.812-0.336 3.822-0.664 7.546-0.98 11.164-0.304 3.616-0.686 7.106-0.738 10.528-0.020 0.548-0.040 1.076-0.058 1.62-34.376 1.112-61.902 29.304-61.902 63.946 0 0.542 0.028 1.078 0.042 1.618zM73.518 448.706c0.042-0.196 0.086-0.384 0.128-0.58 0.644-3.248 1.632-6.542 2.556-9.942 0.934-3.388 1.894-6.876 2.88-10.454 0.516-1.78 0.934-3.602 1.546-5.406 0.596-1.802 1.202-3.628 1.81-5.476 1.218-3.682 2.464-7.45 3.736-11.294 0.316-0.958 0.634-1.924 0.956-2.892 0.37-0.954 0.74-1.914 1.114-2.876 0.746-1.924 1.5-3.868 2.26-5.83 1.52-3.904 3.070-7.882 4.646-11.93 1.768-3.96 3.566-7.99 5.392-12.080 0.908-2.038 1.824-4.090 2.746-6.156 0.932-2.060 2.036-4.072 3.052-6.126 2.070-4.084 4.17-8.222 6.294-12.412 2.202-4.142 4.654-8.224 6.998-12.392 1.184-2.074 2.374-4.16 3.57-6.256 1.21-2.086 2.586-4.102 3.876-6.166 2.616-4.098 5.256-8.232 7.918-12.402 11.234-16.298 23.632-32.398 37.33-47.638 13.874-15.104 28.842-29.404 44.598-42.548 15.974-12.928 32.686-24.65 49.676-35.022 17.13-10.194 34.6-18.838 51.734-26.258 4.35-1.7 8.662-3.382 12.934-5.050 2.136-0.812 4.216-1.71 6.36-2.444 2.146-0.714 4.28-1.428 6.404-2.136 4.25-1.386 8.382-2.888 12.548-4.142 4.184-1.174 8.314-2.332 12.392-3.474 2.038-0.55 4.026-1.19 6.054-1.662 2.030-0.458 4.044-0.914 6.044-1.368 3.978-0.91 7.896-1.806 11.748-2.688 3.888-0.686 7.71-1.36 11.462-2.022 1.868-0.33 3.716-0.658 5.546-0.98 0.914-0.162 1.824-0.324 2.728-0.484 0.916-0.112 1.828-0.222 2.734-0.332 3.612-0.448 7.148-0.882 10.604-1.31 1.72-0.216 3.422-0.432 5.102-0.644 1.674-0.226 3.364-0.266 5.010-0.408 3.292-0.238 6.498-0.472 9.616-0.7 3.11-0.218 6.11-0.524 9.058-0.508 5.848-0.132 11.32-0.256 16.38-0.372 4.664 0.168 8.948 0.324 12.818 0.462 1.914 0.054 3.726 0.108 5.432 0.156 2.122 0.134 4.108 0.26 5.958 0.378 2.13 0.138 4.060 0.266 5.82 0.38 3.256 0.51 6.592 0.782 9.99 0.782 0.466 0 0.93-0.026 1.396-0.036 0.132 0.008 0.224 0.014 0.224 0.014v-0.020c31.14-0.778 56.75-23.784 61.556-53.754 0.542 0.12 1.064 0.236 1.612 0.356 3.246 0.644 6.542 1.632 9.942 2.556 3.386 0.934 6.876 1.894 10.454 2.88 1.778 0.516 3.602 0.934 5.404 1.546 1.802 0.596 3.63 1.202 5.478 1.812 3.68 1.218 7.448 2.464 11.292 3.736 0.96 0.316 1.924 0.634 2.892 0.956 0.956 0.37 1.914 0.74 2.876 1.112 1.926 0.746 3.868 1.5 5.83 2.26 3.904 1.52 7.884 3.070 11.932 4.646 3.96 1.768 7.988 3.566 12.080 5.392 2.038 0.908 4.088 1.824 6.156 2.746 2.060 0.932 4.072 2.036 6.126 3.054 4.082 2.070 8.222 4.17 12.41 6.294 4.144 2.202 8.226 4.654 12.394 6.998 2.074 1.184 4.16 2.374 6.256 3.572 2.086 1.21 4.102 2.586 6.166 3.876 4.098 2.616 8.23 5.256 12.402 7.918 16.296 11.234 32.398 23.632 47.636 37.33 15.104 13.874 29.406 28.842 42.55 44.598 12.928 15.974 24.648 32.686 35.020 49.676 10.196 17.13 18.84 34.6 26.26 51.736 1.698 4.348 3.382 8.662 5.050 12.932 0.812 2.136 1.71 4.216 2.444 6.36 0.714 2.146 1.428 4.28 2.136 6.404 1.386 4.25 2.888 8.384 4.142 12.548 1.174 4.184 2.33 8.316 3.474 12.392 0.55 2.038 1.19 4.026 1.66 6.054 0.46 2.030 0.916 4.046 1.368 6.046 0.91 3.978 1.808 7.896 2.688 11.748 0.688 3.888 1.362 7.71 2.024 11.462 0.33 1.868 0.656 3.716 0.98 5.548 0.162 0.914 0.324 1.824 0.484 2.728 0.11 0.916 0.222 1.828 0.332 2.734 0.446 3.612 0.882 7.148 1.31 10.604 0.216 1.72 0.432 3.42 0.642 5.1 0.226 1.674 0.268 3.364 0.41 5.010 0.238 3.292 0.472 6.498 0.7 9.616 0.218 3.11 0.524 6.11 0.508 9.058 0.132 5.848 0.256 11.32 0.372 16.38-0.168 4.664-0.324 8.948-0.462 12.818-0.054 1.914-0.108 3.726-0.156 5.432-0.134 2.122-0.26 4.108-0.378 5.958-0.138 2.13-0.266 4.060-0.38 5.82-0.498 3.256-0.768 6.592-0.768 9.99 0 0.468 0.026 0.93 0.036 1.396-0.008 0.132-0.016 0.224-0.016 0.224h0.022c0.768 30.766 23.236 56.128 52.682 61.37-0.066 0.296-0.13 0.584-0.198 0.884-0.644 3.248-1.632 6.542-2.556 9.942-0.934 3.388-1.894 6.876-2.88 10.454-0.516 1.78-0.934 3.602-1.546 5.406-0.596 1.802-1.202 3.628-1.81 5.476-1.218 3.682-2.464 7.45-3.736 11.294-0.316 0.958-0.634 1.924-0.956 2.892-0.37 0.954-0.74 1.914-1.114 2.876-0.746 1.924-1.5 3.868-2.26 5.83-1.52 3.904-3.070 7.882-4.646 11.93-1.768 3.96-3.566 7.99-5.392 12.080-0.908 2.038-1.824 4.090-2.746 6.156-0.932 2.060-2.036 4.072-3.052 6.126-2.070 4.084-4.17 8.222-6.294 12.412-2.202 4.142-4.654 8.224-6.998 12.392-1.184 2.074-2.374 4.16-3.57 6.256-1.21 2.086-2.586 4.102-3.876 6.166-2.616 4.098-5.256 8.232-7.918 12.402-11.234 16.298-23.632 32.398-37.33 47.638-13.874 15.104-28.842 29.404-44.598 42.548-15.974 12.928-32.686 24.65-49.676 35.022-17.13 10.194-34.6 18.838-51.734 26.258-4.35 1.7-8.662 3.382-12.934 5.050-2.136 0.812-4.216 1.71-6.36 2.444-2.146 0.714-4.28 1.428-6.404 2.136-4.25 1.386-8.382 2.888-12.548 4.142-4.184 1.174-8.314 2.332-12.392 3.474-2.038 0.55-4.026 1.19-6.054 1.662-2.030 0.458-4.044 0.914-6.044 1.368-3.978 0.91-7.896 1.806-11.748 2.688-3.888 0.686-7.71 1.36-11.462 2.022-1.868 0.33-3.716 0.658-5.546 0.98-0.914 0.162-1.824 0.324-2.728 0.484-0.916 0.112-1.828 0.222-2.734 0.332-3.612 0.448-7.148 0.882-10.604 1.31-1.72 0.216-3.422 0.432-5.102 0.644-1.674 0.226-3.364 0.266-5.010 0.408-3.292 0.238-6.498 0.472-9.616 0.7-3.11 0.218-6.11 0.524-9.058 0.508-5.848 0.132-11.32 0.256-16.38 0.372-4.664-0.168-8.948-0.324-12.818-0.462-1.914-0.054-3.726-0.108-5.432-0.156-2.122-0.134-4.108-0.26-5.958-0.378-2.13-0.138-4.060-0.266-5.82-0.38-3.256-0.51-6.592-0.782-9.99-0.782-0.466 0-0.93 0.026-1.396 0.036-0.132-0.008-0.224-0.014-0.224-0.014v0.020c-31.004 0.774-56.524 23.586-61.488 53.364-3.2-0.64-6.446-1.61-9.792-2.522-3.386-0.934-6.876-1.894-10.454-2.878-1.778-0.516-3.602-0.938-5.404-1.546-1.802-0.598-3.63-1.204-5.478-1.812-3.68-1.218-7.448-2.464-11.292-3.738-0.96-0.316-1.924-0.632-2.892-0.954-0.956-0.372-1.914-0.742-2.876-1.114-1.926-0.746-3.868-1.5-5.83-2.258-3.904-1.524-7.884-3.070-11.932-4.648-3.96-1.77-7.988-3.566-12.080-5.39-2.038-0.91-4.088-1.824-6.156-2.746-2.060-0.934-4.072-2.036-6.126-3.054-4.082-2.070-8.222-4.172-12.41-6.296-4.144-2.2-8.226-4.652-12.394-6.996-2.074-1.184-4.16-2.376-6.256-3.57-2.086-1.21-4.102-2.586-6.166-3.878-4.098-2.614-8.23-5.254-12.402-7.918-16.296-11.23-32.398-23.632-47.636-37.328-15.104-13.876-29.406-28.84-42.55-44.598-12.928-15.972-24.648-32.684-35.020-49.676-10.196-17.128-18.84-34.602-26.26-51.734-1.698-4.352-3.382-8.664-5.050-12.934-0.812-2.136-1.71-4.218-2.444-6.36-0.714-2.148-1.428-4.282-2.136-6.406-1.386-4.25-2.888-8.382-4.142-12.546-1.174-4.184-2.33-8.316-3.474-12.394-0.55-2.036-1.19-4.024-1.66-6.054-0.46-2.028-0.916-4.042-1.368-6.042-0.91-3.98-1.808-7.898-2.688-11.75-0.688-3.886-1.362-7.71-2.024-11.46-0.33-1.868-0.656-3.718-0.98-5.546-0.162-0.914-0.324-1.824-0.484-2.73-0.11-0.914-0.222-1.828-0.332-2.734-0.446-3.61-0.882-7.148-1.31-10.602-0.216-1.722-0.432-3.422-0.642-5.102-0.226-1.676-0.268-3.364-0.41-5.012-0.238-3.29-0.472-6.496-0.7-9.614-0.218-3.11-0.524-6.11-0.508-9.058-0.132-5.848-0.256-11.32-0.372-16.382 0.168-4.664 0.324-8.946 0.462-12.816 0.054-1.914 0.108-3.726 0.156-5.434 0.134-2.122 0.26-4.106 0.378-5.958 0.138-2.128 0.266-4.058 0.38-5.82 0.496-3.26 0.766-6.596 0.766-9.994 0-0.466-0.026-0.93-0.036-1.396 0.008-0.132 0.016-0.224 0.016-0.224h-0.022c-0.78-31.38-24.134-57.154-54.44-61.674z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "busy", + "wait" + ], + "defaultCode": 59779, + "grid": 16 + }, + { + "id": 132, + "paths": [ + "M1024 384h-384l143.53-143.53c-72.53-72.526-168.96-112.47-271.53-112.47s-199 39.944-271.53 112.47c-72.526 72.53-112.47 168.96-112.47 271.53s39.944 199 112.47 271.53c72.53 72.526 168.96 112.47 271.53 112.47s199-39.944 271.528-112.472c6.056-6.054 11.86-12.292 17.456-18.668l96.32 84.282c-93.846 107.166-231.664 174.858-385.304 174.858-282.77 0-512-229.23-512-512s229.23-512 512-512c141.386 0 269.368 57.326 362.016 149.984l149.984-149.984v384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spinner", + "loading", + "loading-wheel", + "refresh", + "repeat", + "busy", + "wait", + "arrow" + ], + "defaultCode": 59780, + "grid": 16 + }, + { + "id": 133, + "paths": [ + "M64 0h384v64h-384zM576 0h384v64h-384zM952 320h-56v-256h-256v256h-256v-256h-256v256h-56c-39.6 0-72 32.4-72 72v560c0 39.6 32.4 72 72 72h304c39.6 0 72-32.4 72-72v-376h128v376c0 39.6 32.4 72 72 72h304c39.6 0 72-32.4 72-72v-560c0-39.6-32.4-72-72-72zM348 960h-248c-19.8 0-36-14.4-36-32s16.2-32 36-32h248c19.8 0 36 14.4 36 32s-16.2 32-36 32zM544 512h-64c-17.6 0-32-14.4-32-32s14.4-32 32-32h64c17.6 0 32 14.4 32 32s-14.4 32-32 32zM924 960h-248c-19.8 0-36-14.4-36-32s16.2-32 36-32h248c19.8 0 36 14.4 36 32s-16.2 32-36 32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "binoculars", + "lookup", + "search", + "find" + ], + "defaultCode": 59781, + "grid": 16 + }, + { + "id": 134, + "paths": [ + "M992.262 871.396l-242.552-206.294c-25.074-22.566-51.89-32.926-73.552-31.926 57.256-67.068 91.842-154.078 91.842-249.176 0-212.078-171.922-384-384-384-212.076 0-384 171.922-384 384s171.922 384 384 384c95.098 0 182.108-34.586 249.176-91.844-1 21.662 9.36 48.478 31.926 73.552l206.294 242.552c35.322 39.246 93.022 42.554 128.22 7.356s31.892-92.898-7.354-128.22zM384 640c-141.384 0-256-114.616-256-256s114.616-256 256-256 256 114.616 256 256-114.614 256-256 256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "search", + "magnifier", + "magnifying-glass", + "inspect", + "find" + ], + "defaultCode": 59782, + "grid": 16 + }, + { + "id": 135, + "paths": [ + "M992.262 871.396l-242.552-206.294c-25.074-22.566-51.89-32.926-73.552-31.926 57.256-67.068 91.842-154.078 91.842-249.176 0-212.078-171.922-384-384-384-212.076 0-384 171.922-384 384s171.922 384 384 384c95.098 0 182.108-34.586 249.176-91.844-1 21.662 9.36 48.478 31.926 73.552l206.294 242.552c35.322 39.246 93.022 42.554 128.22 7.356s31.892-92.898-7.354-128.22zM384 640c-141.384 0-256-114.616-256-256s114.616-256 256-256 256 114.616 256 256-114.614 256-256 256zM448 192h-128v128h-128v128h128v128h128v-128h128v-128h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "zoom-in", + "magnifier", + "magnifier-plus", + "enlarge" + ], + "defaultCode": 59783, + "grid": 16 + }, + { + "id": 136, + "paths": [ + "M992.262 871.396l-242.552-206.294c-25.074-22.566-51.89-32.926-73.552-31.926 57.256-67.068 91.842-154.078 91.842-249.176 0-212.078-171.922-384-384-384-212.076 0-384 171.922-384 384s171.922 384 384 384c95.098 0 182.108-34.586 249.176-91.844-1 21.662 9.36 48.478 31.926 73.552l206.294 242.552c35.322 39.246 93.022 42.554 128.22 7.356s31.892-92.898-7.354-128.22zM384 640c-141.384 0-256-114.616-256-256s114.616-256 256-256 256 114.616 256 256-114.614 256-256 256zM192 320h384v128h-384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "zoom-out", + "magnifier", + "magnifier-minus", + "reduce" + ], + "defaultCode": 59784, + "grid": 16 + }, + { + "id": 137, + "paths": [ + "M1024 0h-416l160 160-192 192 96 96 192-192 160 160z", + "M1024 1024v-416l-160 160-192-192-96 96 192 192-160 160z", + "M0 1024h416l-160-160 192-192-96-96-192 192-160-160z", + "M0 0v416l160-160 192 192 96-96-192-192 160-160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "enlarge", + "expand", + "maximize", + "fullscreen" + ], + "defaultCode": 59785, + "grid": 16 + }, + { + "id": 138, + "paths": [ + "M576 448h416l-160-160 192-192-96-96-192 192-160-160z", + "M576 576v416l160-160 192 192 96-96-192-192 160-160z", + "M448 575.996h-416l160 160-192 192 96 96 192-192 160 160z", + "M448 448v-416l-160 160-192-192-96 96 192 192-160 160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shrink", + "collapse", + "minimize", + "contract" + ], + "defaultCode": 59786, + "grid": 16 + }, + { + "id": 139, + "paths": [ + "M1024 0v416l-160-160-192 192-96-96 192-192-160-160zM448 672l-192 192 160 160h-416v-416l160 160 192-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "enlarge", + "expand", + "maximize", + "fullscreen" + ], + "defaultCode": 59787, + "grid": 16 + }, + { + "id": 140, + "paths": [ + "M448 576v416l-160-160-192 192-96-96 192-192-160-160zM1024 96l-192 192 160 160h-416v-416l160 160 192-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shrink", + "collapse", + "minimize", + "contract" + ], + "defaultCode": 59788, + "grid": 16 + }, + { + "id": 141, + "paths": [ + "M704 0c-176.73 0-320 143.268-320 320 0 20.026 1.858 39.616 5.376 58.624l-389.376 389.376v192c0 35.346 28.654 64 64 64h64v-64h128v-128h128v-128h128l83.042-83.042c34.010 12.316 70.696 19.042 108.958 19.042 176.73 0 320-143.268 320-320s-143.27-320-320-320zM799.874 320.126c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "key", + "password", + "login", + "signin" + ], + "defaultCode": 59789, + "grid": 16 + }, + { + "id": 142, + "paths": [ + "M1002.132 314.242l-101.106-101.104c-24.792-24.794-65.37-65.368-90.162-90.164l-101.106-101.104c-24.792-24.794-68.954-29.166-98.13-9.716l-276.438 184.292c-29.176 19.452-40.218 61.028-24.536 92.39l70.486 140.974c2.154 4.306 4.646 8.896 7.39 13.66l-356.53 356.53-32 224h192v-64h128v-128h128v-128h128v-71.186c6.396 3.812 12.534 7.216 18.192 10.044l140.97 70.488c31.366 15.682 72.94 4.638 92.39-24.538l184.294-276.44c19.454-29.172 15.078-73.33-9.714-98.126zM150.628 854.626l-45.254-45.254 311.572-311.57 45.254 45.254-311.572 311.57zM917.020 423.764l-45.256 45.256c-12.446 12.444-32.808 12.444-45.254 0l-271.53-271.53c-12.446-12.444-12.446-32.81 0-45.254l45.256-45.256c12.446-12.444 32.808-12.444 45.254 0l271.53 271.53c12.446 12.444 12.446 32.81 0 45.254z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "key", + "password", + "login", + "signin" + ], + "defaultCode": 59790, + "grid": 16 + }, + { + "id": 143, + "paths": [ + "M592 448h-16v-192c0-105.87-86.13-192-192-192h-128c-105.87 0-192 86.13-192 192v192h-16c-26.4 0-48 21.6-48 48v480c0 26.4 21.6 48 48 48h544c26.4 0 48-21.6 48-48v-480c0-26.4-21.6-48-48-48zM192 256c0-35.29 28.71-64 64-64h128c35.29 0 64 28.71 64 64v192h-256v-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "lock", + "secure", + "private", + "encrypted" + ], + "defaultCode": 59791, + "grid": 16 + }, + { + "id": 144, + "paths": [ + "M768 64c105.87 0 192 86.13 192 192v192h-128v-192c0-35.29-28.71-64-64-64h-128c-35.29 0-64 28.71-64 64v192h16c26.4 0 48 21.6 48 48v480c0 26.4-21.6 48-48 48h-544c-26.4 0-48-21.6-48-48v-480c0-26.4 21.6-48 48-48h400v-192c0-105.87 86.13-192 192-192h128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "unlocked", + "lock-open" + ], + "defaultCode": 59792, + "grid": 16 + }, + { + "id": 145, + "paths": [ + "M1002.934 817.876l-460.552-394.76c21.448-40.298 33.618-86.282 33.618-135.116 0-159.058-128.942-288-288-288-29.094 0-57.172 4.332-83.646 12.354l166.39 166.39c24.89 24.89 24.89 65.62 0 90.51l-101.49 101.49c-24.89 24.89-65.62 24.89-90.51 0l-166.39-166.39c-8.022 26.474-12.354 54.552-12.354 83.646 0 159.058 128.942 288 288 288 48.834 0 94.818-12.17 135.116-33.62l394.76 460.552c22.908 26.724 62.016 28.226 86.904 3.338l101.492-101.492c24.888-24.888 23.386-63.994-3.338-86.902z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wrench", + "tool", + "fix", + "settings", + "control", + "options", + "preferences" + ], + "defaultCode": 59793, + "grid": 16 + }, + { + "id": 146, + "paths": [ + "M448 128v-16c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-192v128h192v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h576v-128h-576zM256 256v-128h128v128h-128zM832 432c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-576v128h576v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h192v-128h-192v-16zM640 576v-128h128v128h-128zM448 752c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v16h-192v128h192v16c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-16h576v-128h-576v-16zM256 896v-128h128v128h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "equalizer", + "sliders", + "settings", + "preferences", + "dashboard", + "control" + ], + "defaultCode": 59794, + "grid": 16 + }, + { + "id": 147, + "paths": [ + "M896 448h16c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-16v-192h-128v192h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h16v576h128v-576zM768 256h128v128h-128v-128zM592 832c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-16v-576h-128v576h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h16v192h128v-192h16zM448 640h128v128h-128v-128zM272 448c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-16v-192h-128v192h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h16v576h128v-576h16zM128 256h128v128h-128v-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "equalizer", + "sliders", + "settings", + "preferences", + "dashboard", + "control" + ], + "defaultCode": 59795, + "grid": 16 + }, + { + "id": 148, + "paths": [ + "M933.79 610.25c-53.726-93.054-21.416-212.304 72.152-266.488l-100.626-174.292c-28.75 16.854-62.176 26.518-97.846 26.518-107.536 0-194.708-87.746-194.708-195.99h-201.258c0.266 33.41-8.074 67.282-25.958 98.252-53.724 93.056-173.156 124.702-266.862 70.758l-100.624 174.292c28.97 16.472 54.050 40.588 71.886 71.478 53.638 92.908 21.512 211.92-71.708 266.224l100.626 174.292c28.65-16.696 61.916-26.254 97.4-26.254 107.196 0 194.144 87.192 194.7 194.958h201.254c-0.086-33.074 8.272-66.57 25.966-97.218 53.636-92.906 172.776-124.594 266.414-71.012l100.626-174.29c-28.78-16.466-53.692-40.498-71.434-71.228zM512 719.332c-114.508 0-207.336-92.824-207.336-207.334 0-114.508 92.826-207.334 207.336-207.334 114.508 0 207.332 92.826 207.332 207.334-0.002 114.51-92.824 207.334-207.332 207.334z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cog", + "gear", + "preferences", + "settings", + "generate", + "control", + "options" + ], + "defaultCode": 59796, + "grid": 16 + }, + { + "id": 149, + "paths": [ + "M363.722 722.052l41.298-57.816-45.254-45.256-57.818 41.296c-10.722-5.994-22.204-10.774-34.266-14.192l-11.682-70.084h-64l-11.68 70.086c-12.062 3.418-23.544 8.198-34.266 14.192l-57.818-41.298-45.256 45.256 41.298 57.816c-5.994 10.72-10.774 22.206-14.192 34.266l-70.086 11.682v64l70.086 11.682c3.418 12.060 8.198 23.544 14.192 34.266l-41.298 57.816 45.254 45.256 57.818-41.296c10.722 5.994 22.204 10.774 34.266 14.192l11.682 70.084h64l11.68-70.086c12.062-3.418 23.544-8.198 34.266-14.192l57.818 41.296 45.254-45.256-41.298-57.816c5.994-10.72 10.774-22.206 14.192-34.266l70.088-11.68v-64l-70.086-11.682c-3.418-12.060-8.198-23.544-14.192-34.266zM224 864c-35.348 0-64-28.654-64-64s28.652-64 64-64 64 28.654 64 64-28.652 64-64 64zM1024 384v-64l-67.382-12.25c-1.242-8.046-2.832-15.978-4.724-23.79l57.558-37.1-24.492-59.128-66.944 14.468c-4.214-6.91-8.726-13.62-13.492-20.13l39.006-56.342-45.256-45.254-56.342 39.006c-6.512-4.766-13.22-9.276-20.13-13.494l14.468-66.944-59.128-24.494-37.1 57.558c-7.812-1.892-15.744-3.482-23.79-4.724l-12.252-67.382h-64l-12.252 67.382c-8.046 1.242-15.976 2.832-23.79 4.724l-37.098-57.558-59.128 24.492 14.468 66.944c-6.91 4.216-13.62 8.728-20.13 13.494l-56.342-39.006-45.254 45.254 39.006 56.342c-4.766 6.51-9.278 13.22-13.494 20.13l-66.944-14.468-24.492 59.128 57.558 37.1c-1.892 7.812-3.482 15.742-4.724 23.79l-67.384 12.252v64l67.382 12.25c1.242 8.046 2.832 15.978 4.724 23.79l-57.558 37.1 24.492 59.128 66.944-14.468c4.216 6.91 8.728 13.618 13.494 20.13l-39.006 56.342 45.254 45.256 56.342-39.006c6.51 4.766 13.22 9.276 20.13 13.492l-14.468 66.944 59.128 24.492 37.102-57.558c7.81 1.892 15.742 3.482 23.788 4.724l12.252 67.384h64l12.252-67.382c8.044-1.242 15.976-2.832 23.79-4.724l37.1 57.558 59.128-24.492-14.468-66.944c6.91-4.216 13.62-8.726 20.13-13.492l56.342 39.006 45.256-45.256-39.006-56.342c4.766-6.512 9.276-13.22 13.492-20.13l66.944 14.468 24.492-59.13-57.558-37.1c1.892-7.812 3.482-15.742 4.724-23.79l67.382-12.25zM672 491.2c-76.878 0-139.2-62.322-139.2-139.2s62.32-139.2 139.2-139.2 139.2 62.322 139.2 139.2c0 76.878-62.32 139.2-139.2 139.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cogs", + "gears", + "preferences", + "settings", + "generate", + "control", + "options" + ], + "defaultCode": 59797, + "grid": 16 + }, + { + "id": 150, + "paths": [ + "M1009.996 828.976l-301.544-301.544c-18.668-18.668-49.214-18.668-67.882 0l-22.626 22.626-184-184 302.056-302.058h-320l-142.058 142.058-14.060-14.058h-67.882v67.882l14.058 14.058-206.058 206.060 160 160 206.058-206.058 184 184-22.626 22.626c-18.668 18.668-18.668 49.214 0 67.882l301.544 301.544c18.668 18.668 49.214 18.668 67.882 0l113.136-113.136c18.67-18.666 18.67-49.214 0.002-67.882z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hammer", + "tool", + "fix", + "make", + "generate", + "work", + "build" + ], + "defaultCode": 59798, + "grid": 16 + }, + { + "id": 151, + "paths": [ + "M256 192l-128-128h-64v64l128 128zM320 0h64v128h-64zM576 320h128v64h-128zM640 128v-64h-64l-128 128 64 64zM0 320h128v64h-128zM320 576h64v128h-64zM64 576v64h64l128-128-64-64zM1010 882l-636.118-636.118c-18.668-18.668-49.214-18.668-67.882 0l-60.118 60.118c-18.668 18.668-18.668 49.214 0 67.882l636.118 636.118c18.668 18.668 49.214 18.668 67.882 0l60.118-60.118c18.668-18.668 18.668-49.214 0-67.882zM480 544l-192-192 64-64 192 192-64 64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "magic-wand", + "wizard" + ], + "defaultCode": 59799, + "grid": 16 + }, + { + "id": 152, + "paths": [ + "M896 256h-192v-128c0-35.2-28.8-64-64-64h-256c-35.2 0-64 28.8-64 64v128h-192c-70.4 0-128 57.6-128 128v512c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-512c0-70.4-57.6-128-128-128zM384 128h256v128h-256v-128zM768 704h-192v192h-128v-192h-192v-128h192v-192h128v192h192v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "aid-kit", + "health", + "medicine", + "medical" + ], + "defaultCode": 59800, + "grid": 16 + }, + { + "id": 153, + "paths": [ + "M1024 576v-64h-193.29c-5.862-72.686-31.786-139.026-71.67-192.25h161.944l70.060-280.24-62.090-15.522-57.94 231.76h-174.68c-0.892-0.694-1.796-1.374-2.698-2.056 6.71-19.502 10.362-40.422 10.362-62.194 0.002-105.76-85.958-191.498-191.998-191.498s-192 85.738-192 191.5c0 21.772 3.65 42.692 10.362 62.194-0.9 0.684-1.804 1.362-2.698 2.056h-174.68l-57.94-231.76-62.090 15.522 70.060 280.24h161.944c-39.884 53.222-65.806 119.562-71.668 192.248h-193.29v64h193.37c3.802 45.664 15.508 88.812 33.638 127.75h-123.992l-70.060 280.238 62.090 15.524 57.94-231.762h112.354c58.692 78.032 147.396 127.75 246.66 127.75s187.966-49.718 246.662-127.75h112.354l57.94 231.762 62.090-15.524-70.060-280.238h-123.992c18.13-38.938 29.836-82.086 33.636-127.75h193.37z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bug", + "virus", + "error" + ], + "defaultCode": 59801, + "grid": 16 + }, + { + "id": 154, + "paths": [ + "M448 576v-448c-247.424 0-448 200.576-448 448s200.576 448 448 448 448-200.576 448-448c0-72.034-17.028-140.084-47.236-200.382l-400.764 200.382zM912.764 247.618c-73.552-146.816-225.374-247.618-400.764-247.618v448l400.764-200.382z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pie-chart", + "stats", + "statistics", + "graph" + ], + "defaultCode": 59802, + "grid": 16 + }, + { + "id": 155, + "paths": [ + "M128 896h896v128h-1024v-1024h128zM288 832c-53.020 0-96-42.98-96-96s42.98-96 96-96c2.828 0 5.622 0.148 8.388 0.386l103.192-171.986c-9.84-15.070-15.58-33.062-15.58-52.402 0-53.020 42.98-96 96-96s96 42.98 96 96c0 19.342-5.74 37.332-15.58 52.402l103.192 171.986c2.766-0.238 5.56-0.386 8.388-0.386 2.136 0 4.248 0.094 6.35 0.23l170.356-298.122c-10.536-15.408-16.706-34.036-16.706-54.11 0-53.020 42.98-96 96-96s96 42.98 96 96c0 53.020-42.98 96-96 96-2.14 0-4.248-0.094-6.35-0.232l-170.356 298.124c10.536 15.406 16.706 34.036 16.706 54.11 0 53.020-42.98 96-96 96s-96-42.98-96-96c0-19.34 5.74-37.332 15.578-52.402l-103.19-171.984c-2.766 0.238-5.56 0.386-8.388 0.386s-5.622-0.146-8.388-0.386l-103.192 171.986c9.84 15.068 15.58 33.060 15.58 52.4 0 53.020-42.98 96-96 96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stats-dots", + "stats", + "plot", + "statistics", + "chart" + ], + "defaultCode": 59803, + "grid": 16 + }, + { + "id": 156, + "paths": [ + "M0 832h1024v128h-1024zM128 576h128v192h-128zM320 320h128v448h-128zM512 512h128v256h-128zM704 128h128v640h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stats-bars", + "stats", + "statistics", + "chart" + ], + "defaultCode": 59804, + "grid": 16 + }, + { + "id": 157, + "paths": [ + "M288 384h-192c-17.6 0-32 14.4-32 32v576c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32v-576c0-17.6-14.4-32-32-32zM288 960h-192v-256h192v256zM608 256h-192c-17.6 0-32 14.4-32 32v704c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32v-704c0-17.6-14.4-32-32-32zM608 960h-192v-320h192v320zM928 128h-192c-17.6 0-32 14.4-32 32v832c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32v-832c0-17.6-14.4-32-32-32zM928 960h-192v-384h192v384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stats-bars", + "stats", + "statistics", + "chart" + ], + "defaultCode": 59805, + "grid": 16 + }, + { + "id": 158, + "paths": [ + "M832 192v-128h-640v128h-192v128c0 106.038 85.958 192 192 192 20.076 0 39.43-3.086 57.62-8.802 46.174 66.008 116.608 113.796 198.38 130.396v198.406h-64c-70.694 0-128 57.306-128 128h512c0-70.694-57.306-128-128-128h-64v-198.406c81.772-16.6 152.206-64.386 198.38-130.396 18.19 5.716 37.544 8.802 57.62 8.802 106.042 0 192-85.962 192-192v-128h-192zM192 436c-63.962 0-116-52.038-116-116v-64h116v64c0 40.186 7.43 78.632 20.954 114.068-6.802 1.246-13.798 1.932-20.954 1.932zM948 320c0 63.962-52.038 116-116 116-7.156 0-14.152-0.686-20.954-1.932 13.524-35.436 20.954-73.882 20.954-114.068v-64h116v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "trophy", + "cup", + "prize", + "award", + "winner", + "tournament" + ], + "defaultCode": 59806, + "grid": 16 + }, + { + "id": 159, + "paths": [ + "M771.516 320c18.126-12.88 35.512-27.216 51.444-43.148 33.402-33.402 55.746-74.5 62.912-115.722 7.858-45.186-3.672-87.14-31.63-115.1-22.3-22.298-52.51-34.086-87.364-34.086-49.632 0-101.922 23.824-143.46 65.362-66.476 66.476-105.226 158.238-126.076 223.722-15.44-65.802-46.206-154.644-106.018-214.458-32.094-32.092-73.114-48.57-111.846-48.57-31.654 0-61.78 11.004-84.26 33.486-49.986 49.988-43.232 137.786 15.086 196.104 20.792 20.792 45.098 38.062 70.72 52.412h-217.024v256h64v448h768v-448.002h64v-256h-188.484zM674.326 128.218c27.724-27.724 62.322-44.274 92.55-44.274 10.7 0 25.708 2.254 36.45 12.998 26.030 26.028 11.412 86.308-31.28 128.998-43.946 43.946-103.060 74.168-154.432 94.060h-50.672c18.568-57.548 52.058-136.456 107.384-191.782zM233.934 160.89c-0.702-9.12-0.050-26.248 12.196-38.494 10.244-10.244 23.788-12.396 33.348-12.396v0c21.258 0 43.468 10.016 60.932 27.48 33.872 33.872 61.766 87.772 80.668 155.876 0.51 1.84 1.008 3.67 1.496 5.486-1.816-0.486-3.646-0.984-5.486-1.496-68.104-18.904-122.002-46.798-155.874-80.67-15.828-15.826-25.77-36.16-27.28-55.786zM448 960h-256v-416h256v416zM448 512h-320v-128h320v128zM832 960h-256v-416h256v416zM896 512h-320v-128h320v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "gift", + "present", + "box" + ], + "defaultCode": 59807, + "grid": 16 + }, + { + "id": 160, + "paths": [ + "M777.784 16.856c-5.576-10.38-16.406-16.856-28.19-16.856h-475.188c-11.784 0-22.614 6.476-28.19 16.856-35.468 66.020-54.216 143.184-54.216 223.144 0 105.412 32.372 204.828 91.154 279.938 45.428 58.046 102.48 96.54 164.846 112.172v327.89h-96c-17.672 0-32 14.326-32 32s14.328 32 32 32h320c17.674 0 32-14.326 32-32s-14.326-32-32-32h-96v-327.89c62.368-15.632 119.418-54.124 164.846-112.172 58.782-75.11 91.154-174.526 91.154-279.938 0-79.96-18.748-157.122-54.216-223.144zM294.1 64h435.8c24.974 52.902 38.1 113.338 38.1 176 0 5.364-0.108 10.696-0.296 16h-511.406c-0.19-5.304-0.296-10.636-0.296-16-0.002-62.664 13.126-123.098 38.098-176z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "glass", + "drink", + "beverage", + "wine" + ], + "defaultCode": 59808, + "grid": 16 + }, + { + "id": 161, + "paths": [ + "M889.162 179.77c7.568-9.632 8.972-22.742 3.62-33.758-5.356-11.018-16.532-18.012-28.782-18.012h-704c-12.25 0-23.426 6.994-28.78 18.012-5.356 11.018-3.95 24.126 3.618 33.758l313.162 398.57v381.66h-96c-17.672 0-32 14.326-32 32s14.328 32 32 32h320c17.674 0 32-14.326 32-32s-14.326-32-32-32h-96v-381.66l313.162-398.57zM798.162 192l-100.572 128h-371.18l-100.57-128h572.322z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "glass", + "drink", + "beverage", + "wine" + ], + "defaultCode": 59809, + "grid": 16 + }, + { + "id": 162, + "paths": [ + "M960 320h-192v-96c0-88.366-171.922-160-384-160s-384 71.634-384 160v640c0 88.366 171.922 160 384 160s384-71.634 384-160v-96h192c35.346 0 64-28.654 64-64v-320c0-35.346-28.654-64-64-64zM176.056 258.398c-36.994-12.19-59.408-25.246-71.41-34.398 12.004-9.152 34.416-22.208 71.41-34.398 57.942-19.090 131.79-29.602 207.944-29.602s150.004 10.512 207.944 29.602c36.994 12.188 59.408 25.246 71.41 34.398-12.002 9.152-34.416 22.208-71.41 34.398-57.94 19.090-131.79 29.602-207.944 29.602s-150.002-10.512-207.944-29.602zM896 640h-128v-192h128v192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mug", + "drink", + "glass", + "beverage" + ], + "defaultCode": 59810, + "grid": 16 + }, + { + "id": 163, + "paths": [ + "M224 0c-106.040 0-192 100.288-192 224 0 105.924 63.022 194.666 147.706 217.998l-31.788 518.124c-2.154 35.132 24.882 63.878 60.082 63.878h32c35.2 0 62.236-28.746 60.082-63.878l-31.788-518.124c84.684-23.332 147.706-112.074 147.706-217.998 0-123.712-85.96-224-192-224zM869.334 0l-53.334 320h-40l-26.666-320h-26.668l-26.666 320h-40l-53.334-320h-26.666v416c0 17.672 14.326 32 32 32h83.338l-31.42 512.122c-2.154 35.132 24.882 63.878 60.082 63.878h32c35.2 0 62.236-28.746 60.082-63.878l-31.42-512.122h83.338c17.674 0 32-14.328 32-32v-416h-26.666z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spoon-knife", + "food", + "restaurant" + ], + "defaultCode": 59811, + "grid": 16 + }, + { + "id": 164, + "paths": [ + "M1011.328 134.496c-110.752-83.928-281.184-134.034-455.91-134.034-216.12 0-392.226 75.456-483.16 207.020-42.708 61.79-66.33 134.958-70.208 217.474-3.454 73.474 8.884 154.726 36.684 242.146 94.874-284.384 359.82-507.102 665.266-507.102 0 0-285.826 75.232-465.524 308.192-0.112 0.138-2.494 3.090-6.614 8.698-36.080 48.278-67.538 103.162-91.078 165.328-39.87 94.83-76.784 224.948-76.784 381.782h128c0 0-19.43-122.222 14.36-262.79 55.89 7.556 105.858 11.306 150.852 11.306 117.678 0 201.37-25.46 263.388-80.124 55.568-48.978 86.198-114.786 118.624-184.456 49.524-106.408 105.654-227.010 268.654-320.152 9.33-5.332 15.362-14.992 16.056-25.716s-4.040-21.080-12.606-27.572z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "leaf", + "nature", + "plant", + "tea", + "green", + "vegan", + "vegetarian" + ], + "defaultCode": 59812, + "grid": 16 + }, + { + "id": 165, + "paths": [ + "M704 64l-320 320h-192l-192 256c0 0 203.416-56.652 322.066-30.084l-322.066 414.084 421.902-328.144c58.838 134.654-37.902 328.144-37.902 328.144l256-192v-192l320-320 64-320-320 64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "rocket", + "jet", + "speed", + "spaceship", + "fast" + ], + "defaultCode": 59813, + "grid": 16 + }, + { + "id": 166, + "paths": [ + "M512 64c282.77 0 512 229.23 512 512 0 192.792-106.576 360.666-264.008 448h-495.984c-157.432-87.334-264.008-255.208-264.008-448 0-282.77 229.23-512 512-512zM801.914 865.914c77.438-77.44 120.086-180.398 120.086-289.914h-90v-64h85.038c-7.014-44.998-21.39-88.146-42.564-128h-106.474v-64h64.284c-9.438-11.762-19.552-23.096-30.37-33.914-46.222-46.22-101.54-80.038-161.914-99.798v69.712h-64v-85.040c-20.982-3.268-42.36-4.96-64-4.96s-43.018 1.69-64 4.96v85.040h-64v-69.712c-60.372 19.76-115.692 53.576-161.914 99.798-10.818 10.818-20.932 22.152-30.37 33.914h64.284v64h-106.476c-21.174 39.854-35.552 83.002-42.564 128h85.040v64h-90c0 109.516 42.648 212.474 120.086 289.914 10.71 10.71 21.924 20.728 33.56 30.086h192.354l36.572-512h54.856l36.572 512h192.354c11.636-9.358 22.852-19.378 33.56-30.086z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "meter", + "gauge", + "dashboard", + "speedometer", + "performance" + ], + "defaultCode": 59814, + "grid": 16 + }, + { + "id": 167, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM302.836 834.152c11.106-30.632 17.164-63.688 17.164-98.152 0-124.35-78.81-230.292-189.208-270.606 10.21-84.924 48.254-163.498 109.678-224.924 72.53-72.526 168.96-112.47 271.53-112.47s199 39.944 271.53 112.47c61.428 61.426 99.468 140 109.682 224.924-110.402 40.314-189.212 146.256-189.212 270.606 0 34.468 6.060 67.52 17.166 98.15-61.706 40.242-133.77 61.85-209.166 61.85-75.394 0-147.458-21.608-209.164-61.848zM551.754 640.996c13.878 3.494 24.246 16.080 24.246 31.004v64c0 17.6-14.4 32-32 32h-64c-17.6 0-32-14.4-32-32v-64c0-14.924 10.368-27.51 24.246-31.004l23.754-448.996h32l23.754 448.996z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "meter", + "gauge", + "dashboard", + "speedometer", + "performance" + ], + "defaultCode": 59815, + "grid": 16 + }, + { + "id": 168, + "paths": [ + "M1010.174 915.75l-548.634-499.458 25.534-25.598c20.894-20.954 32.188-48.030 33.918-75.61 1.002-0.45 2.002-0.912 2.958-1.442l102.99-64.402c13.934-16.392 12.916-42.268-2.284-57.502l-179.12-179.608c-15.19-15.234-40.998-16.262-57.344-2.284l-64.236 103.268c-0.526 0.966-0.99 1.966-1.44 2.974-27.502 1.736-54.5 13.056-75.398 34.006l-97.428 97.702c-20.898 20.956-32.184 48.026-33.918 75.604-1.004 0.45-2.004 0.916-2.964 1.446l-102.986 64.406c-13.942 16.39-12.916 42.264 2.276 57.496l179.12 179.604c15.194 15.238 40.996 16.262 57.35 2.286l64.228-103.27c0.528-0.958 0.988-1.96 1.442-2.966 27.502-1.738 54.504-13.050 75.398-34.004l28.292-28.372 498.122 550.114c14.436 15.944 36.7 18.518 49.474 5.712l50.356-50.488c12.764-12.808 10.196-35.132-5.706-49.614z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hammer", + "gavel", + "rules", + "justice", + "legal" + ], + "defaultCode": 59816, + "grid": 16 + }, + { + "id": 169, + "paths": [ + "M321.008 1024c-68.246-142.008-31.902-223.378 20.55-300.044 57.44-83.956 72.244-167.066 72.244-167.066s45.154 58.7 27.092 150.508c79.772-88.8 94.824-230.28 82.782-284.464 180.314 126.012 257.376 398.856 153.522 601.066 552.372-312.532 137.398-780.172 65.154-832.85 24.082 52.676 28.648 141.85-20 185.126-82.352-312.276-285.972-376.276-285.972-376.276 24.082 161.044-87.296 337.144-194.696 468.73-3.774-64.216-7.782-108.528-41.55-169.98-7.58 116.656-96.732 211.748-120.874 328.628-32.702 158.286 24.496 274.18 241.748 396.622z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "fire", + "flame", + "hot", + "popular" + ], + "defaultCode": 59817, + "grid": 16 + }, + { + "id": 170, + "paths": [ + "M956.29 804.482l-316.29-527.024v-213.458h32c17.6 0 32-14.4 32-32s-14.4-32-32-32h-320c-17.6 0-32 14.4-32 32s14.4 32 32 32h32v213.458l-316.288 527.024c-72.442 120.734-16.512 219.518 124.288 219.518h640c140.8 0 196.73-98.784 124.29-219.518zM241.038 640l206.962-344.938v-231.062h128v231.062l206.964 344.938h-541.926z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "lab", + "beta", + "beaker", + "test", + "experiment" + ], + "defaultCode": 59818, + "grid": 16 + }, + { + "id": 171, + "paths": [ + "M896 0h-256l64 576c0 106.040-85.96 192-192 192s-192-85.96-192-192l64-576h-256l-64 576c0 247.424 200.576 448 448 448s448-200.576 448-448l-64-576zM777.874 841.874c-71.018 71.014-165.44 110.126-265.874 110.126s-194.856-39.112-265.872-110.126c-70.116-70.118-109.13-163.048-110.11-262.054l36.092-324.82h111.114l-35.224 317.010v3.99c0 70.518 27.46 136.814 77.324 186.676 49.862 49.864 116.158 77.324 186.676 77.324s136.814-27.46 186.676-77.324c49.864-49.862 77.324-116.158 77.324-186.676v-3.988l-0.44-3.962-34.782-313.050h111.114l36.090 324.818c-0.98 99.006-39.994 191.938-110.108 262.056z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "magnet", + "attract" + ], + "defaultCode": 59819, + "grid": 16 + }, + { + "id": 172, + "paths": [ + "M128 320v640c0 35.2 28.8 64 64 64h576c35.2 0 64-28.8 64-64v-640h-704zM320 896h-64v-448h64v448zM448 896h-64v-448h64v448zM576 896h-64v-448h64v448zM704 896h-64v-448h64v448z", + "M848 128h-208v-80c0-26.4-21.6-48-48-48h-224c-26.4 0-48 21.6-48 48v80h-208c-26.4 0-48 21.6-48 48v80h832v-80c0-26.4-21.6-48-48-48zM576 128h-192v-63.198h192v63.198z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bin", + "trashcan", + "remove", + "delete", + "recycle", + "dispose" + ], + "defaultCode": 59820, + "grid": 16 + }, + { + "id": 173, + "paths": [ + "M192 1024h640l64-704h-768zM640 128v-128h-256v128h-320v192l64-64h768l64 64v-192h-320zM576 128h-128v-64h128v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bin", + "trashcan", + "remove", + "delete", + "recycle", + "dispose" + ], + "defaultCode": 59821, + "grid": 16 + }, + { + "id": 174, + "paths": [ + "M960 256h-256v-64c0-35.2-28.8-64-64-64h-256c-35.204 0-64 28.8-64 64v64h-256c-35.2 0-64 28.8-64 64v576c0 35.202 28.796 64 64 64h896c35.2 0 64-28.798 64-64v-576c0-35.2-28.8-64-64-64zM384 192.116c0.034-0.040 0.074-0.082 0.114-0.116h255.772c0.042 0.034 0.082 0.076 0.118 0.116v63.884h-256.004v-63.884zM960 512h-128v96c0 17.602-14.4 32-32 32h-64c-17.604 0-32-14.398-32-32v-96h-384v96c0 17.602-14.4 32-32 32h-64c-17.602 0-32-14.398-32-32v-96h-128v-64h896v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "briefcase", + "portfolio", + "suitcase", + "work", + "job", + "employee" + ], + "defaultCode": 59822, + "grid": 16 + }, + { + "id": 175, + "paths": [ + "M768 639.968l-182.82-182.822 438.82-329.15-128.010-127.996-548.52 219.442-172.7-172.706c-49.78-49.778-119.302-61.706-154.502-26.508-35.198 35.198-23.268 104.726 26.51 154.5l172.686 172.684-219.464 548.582 127.99 128.006 329.19-438.868 182.826 182.828v255.98h127.994l63.992-191.988 191.988-63.996v-127.992l-255.98 0.004z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "airplane", + "travel", + "flight", + "plane", + "transport", + "fly", + "vacation" + ], + "defaultCode": 59823, + "grid": 16 + }, + { + "id": 176, + "paths": [ + "M1024 576l-128-256h-192v-128c0-35.2-28.8-64-64-64h-576c-35.2 0-64 28.8-64 64v512l64 64h81.166c-10.898 18.832-17.166 40.678-17.166 64 0 70.692 57.308 128 128 128s128-57.308 128-128c0-23.322-6.268-45.168-17.166-64h354.334c-10.898 18.832-17.168 40.678-17.168 64 0 70.692 57.308 128 128 128s128-57.308 128-128c0-23.322-6.27-45.168-17.168-64h81.168v-192zM704 576v-192h132.668l96 192h-228.668z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "truck", + "transit", + "transport", + "delivery", + "vehicle" + ], + "defaultCode": 59824, + "grid": 16 + }, + { + "id": 177, + "paths": [ + "M704 1024h320l-256-1024h-192l32 256h-192l32-256h-192l-256 1024h320l32-256h320l32 256zM368 640l32-256h224l32 256h-288z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "road", + "asphalt", + "travel" + ], + "defaultCode": 59825, + "grid": 16 + }, + { + "id": 178, + "paths": [ + "M416 96c0-53.018 42.98-96 96-96s96 42.982 96 96c0 53.020-42.98 96-96 96s-96-42.98-96-96z", + "M640 320l329.596-142.172-23.77-59.424-401.826 137.596h-64l-401.826-137.596-23.77 59.424 329.596 142.172v256l-131.27 424.57 59.84 22.7 185.716-415.27h27.428l185.716 415.27 59.84-22.7-131.27-424.57z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "accessibility" + ], + "defaultCode": 59826, + "grid": 16 + }, + { + "id": 179, + "paths": [ + "M1024 448h-100.924c-27.64-178.24-168.836-319.436-347.076-347.076v-100.924h-128v100.924c-178.24 27.64-319.436 168.836-347.076 347.076h-100.924v128h100.924c27.64 178.24 168.836 319.436 347.076 347.076v100.924h128v-100.924c178.24-27.64 319.436-168.836 347.076-347.076h100.924v-128zM792.822 448h-99.762c-19.284-54.55-62.51-97.778-117.060-117.060v-99.762c107.514 24.49 192.332 109.31 216.822 216.822zM512 576c-35.346 0-64-28.654-64-64s28.654-64 64-64c35.346 0 64 28.654 64 64s-28.654 64-64 64zM448 231.178v99.762c-54.55 19.282-97.778 62.51-117.060 117.060h-99.762c24.49-107.512 109.31-192.332 216.822-216.822zM231.178 576h99.762c19.282 54.55 62.51 97.778 117.060 117.060v99.762c-107.512-24.49-192.332-109.308-216.822-216.822zM576 792.822v-99.762c54.55-19.284 97.778-62.51 117.060-117.060h99.762c-24.49 107.514-109.308 192.332-216.822 216.822z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "target", + "goal", + "location", + "spot" + ], + "defaultCode": 59827, + "grid": 16 + }, + { + "id": 180, + "paths": [ + "M960 0l-448 128-448-128c0 0-4.5 51.698 0 128l448 140.090 448-140.090c4.498-76.302 0-128 0-128zM72.19 195.106c23.986 250.696 113.49 672.234 439.81 828.894 326.32-156.66 415.824-578.198 439.81-828.894l-439.81 165.358-439.81-165.358z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shield", + "security", + "defense", + "protection", + "anti virus" + ], + "defaultCode": 59828, + "grid": 16 + }, + { + "id": 181, + "paths": [ + "M384 0l-384 512h384l-256 512 896-640h-512l384-384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "power", + "lightning", + "bolt", + "electricity" + ], + "defaultCode": 59829, + "grid": 16 + }, + { + "id": 182, + "paths": [ + "M640 146.588v135.958c36.206 15.804 69.5 38.408 98.274 67.18 60.442 60.44 93.726 140.8 93.726 226.274s-33.286 165.834-93.726 226.274c-60.44 60.44-140.798 93.726-226.274 93.726s-165.834-33.286-226.274-93.726c-60.44-60.44-93.726-140.8-93.726-226.274s33.286-165.834 93.726-226.274c28.774-28.774 62.068-51.378 98.274-67.182v-135.956c-185.048 55.080-320 226.472-320 429.412 0 247.424 200.578 448 448 448 247.424 0 448-200.576 448-448 0-202.94-134.95-374.332-320-429.412zM448 0h128v512h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "switch" + ], + "defaultCode": 59830, + "grid": 16 + }, + { + "id": 183, + "paths": [ + "M1024 282.5l-90.506-90.5-178.746 178.752-101.5-101.502 178.75-178.75-90.5-90.5-178.75 178.75-114.748-114.75-86.626 86.624 512.002 512 86.624-86.622-114.752-114.752 178.752-178.75z", + "M794.040 673.79l-443.824-443.824c-95.818 114.904-204.52 292.454-129.396 445.216l-132.248 132.248c-31.112 31.114-31.112 82.024 0 113.136l14.858 14.858c31.114 31.114 82.026 31.114 113.138 0l132.246-132.244c152.764 75.132 330.318-33.566 445.226-129.39z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "power-cord", + "plugin", + "extension" + ], + "defaultCode": 59831, + "grid": 16 + }, + { + "id": 184, + "paths": [ + "M928 128h-288c0-70.692-57.306-128-128-128-70.692 0-128 57.308-128 128h-288c-17.672 0-32 14.328-32 32v832c0 17.674 14.328 32 32 32h832c17.674 0 32-14.326 32-32v-832c0-17.672-14.326-32-32-32zM512 64c35.346 0 64 28.654 64 64s-28.654 64-64 64c-35.346 0-64-28.654-64-64s28.654-64 64-64zM896 960h-768v-768h128v96c0 17.672 14.328 32 32 32h448c17.674 0 32-14.328 32-32v-96h128v768z", + "M448 858.51l-205.254-237.254 58.508-58.51 146.746 114.744 274.742-242.744 58.514 58.508z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "clipboard", + "board", + "signup", + "register", + "agreement" + ], + "defaultCode": 59832, + "grid": 16 + }, + { + "id": 185, + "paths": [ + "M384 832h640v128h-640zM384 448h640v128h-640zM384 64h640v128h-640zM192 0v256h-64v-192h-64v-64zM128 526v50h128v64h-192v-146l128-60v-50h-128v-64h192v146zM256 704v320h-192v-64h128v-64h-128v-64h128v-64h-128v-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "list-numbered", + "options" + ], + "defaultCode": 59833, + "grid": 16 + }, + { + "id": 186, + "paths": [ + "M0 0h256v256h-256zM384 64h640v128h-640zM0 384h256v256h-256zM384 448h640v128h-640zM0 768h256v256h-256zM384 832h640v128h-640z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "list", + "todo", + "bullet", + "menu", + "options" + ], + "defaultCode": 59834, + "grid": 16 + }, + { + "id": 187, + "paths": [ + "M384 64h640v128h-640v-128zM384 448h640v128h-640v-128zM384 832h640v128h-640v-128zM0 128c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM0 512c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM0 896c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "list", + "todo", + "bullet", + "menu", + "options" + ], + "defaultCode": 59835, + "grid": 16 + }, + { + "id": 188, + "paths": [ + "M976 768h-16v-208c0-61.756-50.242-112-112-112h-272v-128h16c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-160c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h16v128h-272c-61.756 0-112 50.244-112 112v208h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-16v-192h256v192h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-16v-192h256v192h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h160c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48zM192 960h-128v-128h128v128zM576 960h-128v-128h128v128zM448 256v-128h128v128h-128zM960 960h-128v-128h128v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tree", + "branches", + "inheritance" + ], + "defaultCode": 59836, + "grid": 16 + }, + { + "id": 189, + "paths": [ + "M64 192h896v192h-896zM64 448h896v192h-896zM64 704h896v192h-896z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "menu", + "list", + "options", + "lines", + "hamburger" + ], + "defaultCode": 59837, + "grid": 16 + }, + { + "id": 190, + "paths": [ + "M0 192h896v192h-896v-192zM0 448h896v192h-896v-192zM0 704h896v192h-896v-192z", + "M992 576l192 192 192-192z", + "M1376 512l-192-192-192 192z" + ], + "width": 1408, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "menu", + "options", + "hamburger" + ], + "defaultCode": 59838, + "grid": 16 + }, + { + "id": 191, + "paths": [ + "M0 192h896v192h-896v-192zM0 448h896v192h-896v-192zM0 704h896v192h-896v-192z", + "M992 448l192 192 192-192z" + ], + "width": 1408, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "menu", + "options", + "hamburger" + ], + "defaultCode": 59839, + "grid": 16 + }, + { + "id": 192, + "paths": [ + "M0 192h896v192h-896v-192zM0 448h896v192h-896v-192zM0 704h896v192h-896v-192z", + "M992 640l192-192 192 192z" + ], + "width": 1408, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "menu", + "options", + "hamburger" + ], + "defaultCode": 59840, + "grid": 16 + }, + { + "id": 193, + "paths": [ + "M1024 657.542c0-82.090-56.678-150.9-132.996-169.48-3.242-128.7-108.458-232.062-237.862-232.062-75.792 0-143.266 35.494-186.854 90.732-24.442-31.598-62.69-51.96-105.708-51.96-73.81 0-133.642 59.874-133.642 133.722 0 6.436 0.48 12.76 1.364 18.954-11.222-2.024-22.766-3.138-34.57-3.138-106.998-0.002-193.732 86.786-193.732 193.842 0 107.062 86.734 193.848 193.73 193.848l656.262-0.012c96.138-0.184 174.008-78.212 174.008-174.446z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud", + "weather" + ], + "defaultCode": 59841, + "grid": 16 + }, + { + "id": 194, + "paths": [ + "M891.004 360.060c-3.242-128.698-108.458-232.060-237.862-232.060-75.792 0-143.266 35.494-186.854 90.732-24.442-31.598-62.69-51.96-105.708-51.96-73.81 0-133.642 59.876-133.642 133.722 0 6.436 0.48 12.76 1.364 18.954-11.222-2.024-22.766-3.138-34.57-3.138-106.998-0.002-193.732 86.786-193.732 193.842 0 107.062 86.734 193.848 193.73 193.848h91.76l226.51 234.51 226.51-234.51 111.482-0.012c96.138-0.184 174.008-78.21 174.008-174.446 0-82.090-56.678-150.9-132.996-169.482zM512 832l-192-192h128v-192h128v192h128l-192 192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud-download", + "cloud", + "save", + "download" + ], + "defaultCode": 59842, + "grid": 16 + }, + { + "id": 195, + "paths": [ + "M892.268 386.49c2.444-11.11 3.732-22.648 3.732-34.49 0-88.366-71.634-160-160-160-14.222 0-28.014 1.868-41.132 5.352-24.798-77.352-97.29-133.352-182.868-133.352-87.348 0-161.054 58.336-184.326 138.17-22.742-6.622-46.792-10.17-71.674-10.17-141.384 0-256 114.616-256 256 0 141.388 114.616 256 256 256h128v192h256v-192h224c88.366 0 160-71.632 160-160 0-78.72-56.854-144.162-131.732-157.51zM576 640v192h-128v-192h-160l224-224 224 224h-160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud-upload", + "cloud", + "load", + "upload" + ], + "defaultCode": 59843, + "grid": 16 + }, + { + "id": 196, + "paths": [ + "M892.268 514.49c2.442-11.108 3.732-22.646 3.732-34.49 0-88.366-71.634-160-160-160-14.224 0-28.014 1.868-41.134 5.352-24.796-77.352-97.288-133.352-182.866-133.352-87.348 0-161.054 58.336-184.326 138.17-22.742-6.62-46.792-10.17-71.674-10.17-141.384 0-256 114.616-256 256 0 141.382 114.616 256 256 256h608c88.366 0 160-71.632 160-160 0-78.718-56.854-144.16-131.732-157.51zM416 768l-160-160 64-64 96 96 224-224 64 64-288 288z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cloud-check", + "cloud", + "synced" + ], + "defaultCode": 59844, + "grid": 16 + }, + { + "id": 197, + "paths": [ + "M896 512h-160l-224 224-224-224h-160l-128 256v64h1024v-64l-128-256zM0 896h1024v64h-1024v-64zM576 320v-256h-128v256h-224l288 288 288-288h-224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "download", + "save", + "store" + ], + "defaultCode": 59845, + "grid": 16 + }, + { + "id": 198, + "paths": [ + "M0 896h1024v64h-1024zM1024 768v64h-1024v-64l128-256h256v128h256v-128h256zM224 320l288-288 288 288h-224v256h-128v-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "upload", + "load", + "open" + ], + "defaultCode": 59846, + "grid": 16 + }, + { + "id": 199, + "paths": [ + "M736 448l-256 256-256-256h160v-384h192v384zM480 704h-480v256h960v-256h-480zM896 832h-128v-64h128v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "download", + "save", + "store" + ], + "defaultCode": 59847, + "grid": 16 + }, + { + "id": 200, + "paths": [ + "M480 704h-480v256h960v-256h-480zM896 832h-128v-64h128v64zM224 320l256-256 256 256h-160v320h-192v-320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "upload", + "load", + "open" + ], + "defaultCode": 59848, + "grid": 16 + }, + { + "id": 201, + "paths": [ + "M480 64c-265.096 0-480 214.904-480 480 0 265.098 214.904 480 480 480 265.098 0 480-214.902 480-480 0-265.096-214.902-480-480-480zM751.59 704c8.58-40.454 13.996-83.392 15.758-128h127.446c-3.336 44.196-13.624 87.114-30.68 128h-112.524zM208.41 384c-8.58 40.454-13.996 83.392-15.758 128h-127.444c3.336-44.194 13.622-87.114 30.678-128h112.524zM686.036 384c9.614 40.962 15.398 83.854 17.28 128h-191.316v-128h174.036zM512 320v-187.338c14.59 4.246 29.044 11.37 43.228 21.37 26.582 18.74 52.012 47.608 73.54 83.486 14.882 24.802 27.752 52.416 38.496 82.484h-155.264zM331.232 237.516c21.528-35.878 46.956-64.748 73.54-83.486 14.182-10 28.638-17.124 43.228-21.37v187.34h-155.264c10.746-30.066 23.616-57.68 38.496-82.484zM448 384v128h-191.314c1.88-44.146 7.666-87.038 17.278-128h174.036zM95.888 704c-17.056-40.886-27.342-83.804-30.678-128h127.444c1.762 44.608 7.178 87.546 15.758 128h-112.524zM256.686 576h191.314v128h-174.036c-9.612-40.96-15.398-83.854-17.278-128zM448 768v187.34c-14.588-4.246-29.044-11.372-43.228-21.37-26.584-18.74-52.014-47.61-73.54-83.486-14.882-24.804-27.75-52.418-38.498-82.484h155.266zM628.768 850.484c-21.528 35.876-46.958 64.746-73.54 83.486-14.184 9.998-28.638 17.124-43.228 21.37v-187.34h155.266c-10.746 30.066-23.616 57.68-38.498 82.484zM512 704v-128h191.314c-1.88 44.146-7.666 87.040-17.28 128h-174.034zM767.348 512c-1.762-44.608-7.178-87.546-15.758-128h112.524c17.056 40.886 27.344 83.806 30.68 128h-127.446zM830.658 320h-95.9c-18.638-58.762-44.376-110.294-75.316-151.428 42.536 20.34 81.058 47.616 114.714 81.272 21.48 21.478 40.362 44.938 56.502 70.156zM185.844 249.844c33.658-33.658 72.18-60.932 114.714-81.272-30.942 41.134-56.676 92.666-75.316 151.428h-95.898c16.138-25.218 35.022-48.678 56.5-70.156zM129.344 768h95.898c18.64 58.762 44.376 110.294 75.318 151.43-42.536-20.34-81.058-47.616-114.714-81.274-21.48-21.478-40.364-44.938-56.502-70.156zM774.156 838.156c-33.656 33.658-72.18 60.934-114.714 81.274 30.942-41.134 56.678-92.668 75.316-151.43h95.9c-16.14 25.218-35.022 48.678-56.502 70.156z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sphere", + "globe", + "internet" + ], + "defaultCode": 59849, + "grid": 16 + }, + { + "id": 202, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 960.002c-62.958 0-122.872-13.012-177.23-36.452l233.148-262.29c5.206-5.858 8.082-13.422 8.082-21.26v-96c0-17.674-14.326-32-32-32-112.99 0-232.204-117.462-233.374-118.626-6-6.002-14.14-9.374-22.626-9.374h-128c-17.672 0-32 14.328-32 32v192c0 12.122 6.848 23.202 17.69 28.622l110.31 55.156v187.886c-116.052-80.956-192-215.432-192-367.664 0-68.714 15.49-133.806 43.138-192h116.862c8.488 0 16.626-3.372 22.628-9.372l128-128c6-6.002 9.372-14.14 9.372-22.628v-77.412c40.562-12.074 83.518-18.588 128-18.588 70.406 0 137.004 16.26 196.282 45.2-4.144 3.502-8.176 7.164-12.046 11.036-36.266 36.264-56.236 84.478-56.236 135.764s19.97 99.5 56.236 135.764c36.434 36.432 85.218 56.264 135.634 56.26 3.166 0 6.342-0.080 9.518-0.236 13.814 51.802 38.752 186.656-8.404 372.334-0.444 1.744-0.696 3.488-0.842 5.224-81.324 83.080-194.7 134.656-320.142 134.656z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "earth", + "globe", + "language", + "web", + "internet", + "sphere", + "planet" + ], + "defaultCode": 59850, + "grid": 16 + }, + { + "id": 203, + "paths": [ + "M440.236 635.766c-13.31 0-26.616-5.076-36.77-15.23-95.134-95.136-95.134-249.934 0-345.070l192-192c46.088-46.086 107.36-71.466 172.534-71.466s126.448 25.38 172.536 71.464c95.132 95.136 95.132 249.934 0 345.070l-87.766 87.766c-20.308 20.308-53.23 20.308-73.54 0-20.306-20.306-20.306-53.232 0-73.54l87.766-87.766c54.584-54.586 54.584-143.404 0-197.99-26.442-26.442-61.6-41.004-98.996-41.004s-72.552 14.562-98.996 41.006l-192 191.998c-54.586 54.586-54.586 143.406 0 197.992 20.308 20.306 20.306 53.232 0 73.54-10.15 10.152-23.462 15.23-36.768 15.23z", + "M256 1012c-65.176 0-126.45-25.38-172.534-71.464-95.134-95.136-95.134-249.934 0-345.070l87.764-87.764c20.308-20.306 53.234-20.306 73.54 0 20.308 20.306 20.308 53.232 0 73.54l-87.764 87.764c-54.586 54.586-54.586 143.406 0 197.992 26.44 26.44 61.598 41.002 98.994 41.002s72.552-14.562 98.998-41.006l192-191.998c54.584-54.586 54.584-143.406 0-197.992-20.308-20.308-20.306-53.232 0-73.54 20.306-20.306 53.232-20.306 73.54 0.002 95.132 95.134 95.132 249.932 0.002 345.068l-192.002 192c-46.090 46.088-107.364 71.466-172.538 71.466z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "link", + "chain", + "url", + "uri", + "anchor" + ], + "defaultCode": 59851, + "grid": 16 + }, + { + "id": 204, + "paths": [ + "M0 0h128v1024h-128v-1024z", + "M832 643.002c82.624 0 154.57-19.984 192-49.5v-512c-37.43 29.518-109.376 49.502-192 49.502s-154.57-19.984-192-49.502v512c37.43 29.516 109.376 49.5 192 49.5z", + "M608 32.528c-46.906-19.94-115.52-32.528-192-32.528-96.396 0-180.334 19.984-224 49.502v512c43.666-29.518 127.604-49.502 224-49.502 76.48 0 145.094 12.588 192 32.528v-512z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "flag", + "report", + "mark" + ], + "defaultCode": 59852, + "grid": 16 + }, + { + "id": 205, + "paths": [ + "M665.832 327.048l-64.952-64.922-324.81 324.742c-53.814 53.792-53.814 141.048 0 194.844 53.804 53.792 141.060 53.792 194.874 0l389.772-389.708c89.714-89.662 89.714-235.062 0-324.726-89.666-89.704-235.112-89.704-324.782 0l-409.23 409.178c-0.29 0.304-0.612 0.576-0.876 0.846-125.102 125.096-125.102 327.856 0 452.906 125.054 125.056 327.868 125.056 452.988 0 0.274-0.274 0.516-0.568 0.82-0.876l0.032 0.034 279.332-279.292-64.986-64.92-279.33 279.262c-0.296 0.268-0.564 0.57-0.846 0.844-89.074 89.058-233.98 89.058-323.076 0-89.062-89.042-89.062-233.922 0-322.978 0.304-0.304 0.604-0.582 0.888-0.846l-0.046-0.060 409.28-409.166c53.712-53.738 141.144-53.738 194.886 0 53.712 53.734 53.712 141.148 0 194.84l-389.772 389.7c-17.936 17.922-47.054 17.922-64.972 0-17.894-17.886-17.894-47.032 0-64.92l324.806-324.782z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "attachment", + "paperclip" + ], + "defaultCode": 59853, + "grid": 16 + }, + { + "id": 206, + "paths": [ + "M512 192c-223.318 0-416.882 130.042-512 320 95.118 189.958 288.682 320 512 320 223.312 0 416.876-130.042 512-320-95.116-189.958-288.688-320-512-320zM764.45 361.704c60.162 38.374 111.142 89.774 149.434 150.296-38.292 60.522-89.274 111.922-149.436 150.296-75.594 48.218-162.89 73.704-252.448 73.704-89.56 0-176.858-25.486-252.452-73.704-60.158-38.372-111.138-89.772-149.432-150.296 38.292-60.524 89.274-111.924 149.434-150.296 3.918-2.5 7.876-4.922 11.86-7.3-9.96 27.328-15.41 56.822-15.41 87.596 0 141.382 114.616 256 256 256 141.382 0 256-114.618 256-256 0-30.774-5.452-60.268-15.408-87.598 3.978 2.378 7.938 4.802 11.858 7.302v0zM512 416c0 53.020-42.98 96-96 96s-96-42.98-96-96 42.98-96 96-96 96 42.982 96 96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "eye", + "views", + "vision", + "visit" + ], + "defaultCode": 59854, + "grid": 16 + }, + { + "id": 207, + "paths": [ + "M1024 128h-128v-128h-128v128h-128v128h128v128h128v-128h128z", + "M863.862 446.028c18.436 20.478 35.192 42.53 50.022 65.972-38.292 60.522-89.274 111.922-149.436 150.296-75.594 48.218-162.89 73.704-252.448 73.704-89.56 0-176.86-25.486-252.454-73.704-60.156-38.372-111.136-89.772-149.43-150.296 38.292-60.524 89.274-111.924 149.434-150.296 3.918-2.5 7.876-4.922 11.862-7.3-9.962 27.328-15.412 56.822-15.412 87.596 0 141.382 114.616 256 256 256 141.38 0 256-114.618 256-256 0-0.692-0.018-1.38-0.024-2.072-109.284-28.138-190.298-126.63-191.932-244.31-21.026-2.38-42.394-3.618-64.044-3.618-223.318 0-416.882 130.042-512 320 95.118 189.958 288.682 320 512 320 223.31 0 416.876-130.042 512-320-17.64-35.23-38.676-68.394-62.65-99.054-29.28 17.178-62.272 28.71-97.488 33.082zM416 320c53.020 0 96 42.982 96 96 0 53.020-42.98 96-96 96s-96-42.98-96-96 42.98-96 96-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "eye-plus", + "views", + "vision", + "visit" + ], + "defaultCode": 59855, + "grid": 16 + }, + { + "id": 208, + "paths": [ + "M640 128h384v128h-384v-128z", + "M870.32 320h-294.32v-124.388c-21.014-2.376-42.364-3.612-64-3.612-223.318 0-416.882 130.042-512 320 95.118 189.958 288.682 320 512 320 223.31 0 416.876-130.042 512-320-37.396-74.686-90.020-140.1-153.68-192zM416 320c53.020 0 96 42.982 96 96 0 53.020-42.98 96-96 96s-96-42.98-96-96 42.98-96 96-96zM764.448 662.296c-75.594 48.218-162.89 73.704-252.448 73.704-89.56 0-176.86-25.486-252.454-73.704-60.156-38.372-111.136-89.772-149.43-150.296 38.292-60.524 89.274-111.924 149.434-150.296 3.918-2.5 7.876-4.922 11.862-7.3-9.962 27.328-15.412 56.822-15.412 87.596 0 141.382 114.616 256 256 256 141.38 0 256-114.618 256-256 0-30.774-5.454-60.268-15.408-87.598 3.976 2.378 7.938 4.802 11.858 7.302 60.162 38.374 111.142 89.774 149.434 150.296-38.292 60.522-89.274 111.922-149.436 150.296z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "eye-minus", + "views", + "vision", + "visit" + ], + "defaultCode": 59856, + "grid": 16 + }, + { + "id": 209, + "paths": [ + "M945.942 14.058c-18.746-18.744-49.136-18.744-67.882 0l-202.164 202.164c-51.938-15.754-106.948-24.222-163.896-24.222-223.318 0-416.882 130.042-512 320 41.122 82.124 100.648 153.040 173.022 207.096l-158.962 158.962c-18.746 18.746-18.746 49.136 0 67.882 9.372 9.374 21.656 14.060 33.94 14.060s24.568-4.686 33.942-14.058l864-864c18.744-18.746 18.744-49.138 0-67.884zM416 320c42.24 0 78.082 27.294 90.92 65.196l-121.724 121.724c-37.902-12.838-65.196-48.68-65.196-90.92 0-53.020 42.98-96 96-96zM110.116 512c38.292-60.524 89.274-111.924 149.434-150.296 3.918-2.5 7.876-4.922 11.862-7.3-9.962 27.328-15.412 56.822-15.412 87.596 0 54.89 17.286 105.738 46.7 147.418l-60.924 60.924c-52.446-36.842-97.202-83.882-131.66-138.342z", + "M768 442c0-27.166-4.256-53.334-12.102-77.898l-321.808 321.808c24.568 7.842 50.742 12.090 77.91 12.090 141.382 0 256-114.618 256-256z", + "M830.026 289.974l-69.362 69.362c1.264 0.786 2.53 1.568 3.786 2.368 60.162 38.374 111.142 89.774 149.434 150.296-38.292 60.522-89.274 111.922-149.436 150.296-75.594 48.218-162.89 73.704-252.448 73.704-38.664 0-76.902-4.76-113.962-14.040l-76.894 76.894c59.718 21.462 123.95 33.146 190.856 33.146 223.31 0 416.876-130.042 512-320-45.022-89.916-112.118-166.396-193.974-222.026z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "eye-blocked", + "views", + "vision", + "visit", + "banned", + "blocked", + "forbidden", + "private" + ], + "defaultCode": 59857, + "grid": 16 + }, + { + "id": 210, + "paths": [ + "M192 0v1024l320-320 320 320v-1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bookmark", + "ribbon" + ], + "defaultCode": 59858, + "grid": 16 + }, + { + "id": 211, + "paths": [ + "M256 128v896l320-320 320 320v-896zM768 0h-640v896l64-64v-768h576z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bookmarks", + "ribbons" + ], + "defaultCode": 59859, + "grid": 16 + }, + { + "id": 212, + "paths": [ + "M512 832c35.346 0 64 28.654 64 64v64c0 35.346-28.654 64-64 64s-64-28.654-64-64v-64c0-35.346 28.654-64 64-64zM512 192c-35.346 0-64-28.654-64-64v-64c0-35.346 28.654-64 64-64s64 28.654 64 64v64c0 35.346-28.654 64-64 64zM960 448c35.346 0 64 28.654 64 64s-28.654 64-64 64h-64c-35.348 0-64-28.654-64-64s28.652-64 64-64h64zM192 512c0 35.346-28.654 64-64 64h-64c-35.346 0-64-28.654-64-64s28.654-64 64-64h64c35.346 0 64 28.654 64 64zM828.784 738.274l45.256 45.258c24.992 24.99 24.992 65.516 0 90.508-24.994 24.992-65.518 24.992-90.51 0l-45.256-45.256c-24.992-24.99-24.992-65.516 0-90.51 24.994-24.992 65.518-24.992 90.51 0zM195.216 285.726l-45.256-45.256c-24.994-24.994-24.994-65.516 0-90.51s65.516-24.994 90.51 0l45.256 45.256c24.994 24.994 24.994 65.516 0 90.51s-65.516 24.994-90.51 0zM828.784 285.726c-24.992 24.992-65.516 24.992-90.51 0-24.992-24.994-24.992-65.516 0-90.51l45.256-45.254c24.992-24.994 65.516-24.994 90.51 0 24.992 24.994 24.992 65.516 0 90.51l-45.256 45.254zM195.216 738.274c24.992-24.992 65.518-24.992 90.508 0 24.994 24.994 24.994 65.52 0 90.51l-45.254 45.256c-24.994 24.992-65.516 24.992-90.51 0s-24.994-65.518 0-90.508l45.256-45.258z", + "M512 256c-141.384 0-256 114.616-256 256 0 141.382 114.616 256 256 256 141.382 0 256-114.618 256-256 0-141.384-114.616-256-256-256zM512 672c-88.366 0-160-71.634-160-160s71.634-160 160-160 160 71.634 160 160-71.634 160-160 160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sun", + "weather" + ], + "defaultCode": 59860, + "grid": 16 + }, + { + "id": 213, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM128 512c0-212.078 171.922-384 384-384v768c-212.078 0-384-171.922-384-384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "contrast" + ], + "defaultCode": 59861, + "grid": 16 + }, + { + "id": 214, + "paths": [ + "M512 256c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.616-256-256-256zM512 672v-320c88.224 0 160 71.776 160 160s-71.776 160-160 160zM512 832c35.346 0 64 28.654 64 64v64c0 35.346-28.654 64-64 64s-64-28.654-64-64v-64c0-35.346 28.654-64 64-64zM512 192c-35.346 0-64-28.654-64-64v-64c0-35.346 28.654-64 64-64s64 28.654 64 64v64c0 35.346-28.654 64-64 64zM960 448c35.346 0 64 28.654 64 64s-28.654 64-64 64h-64c-35.346 0-64-28.654-64-64s28.654-64 64-64h64zM192 512c0 35.346-28.654 64-64 64h-64c-35.346 0-64-28.654-64-64s28.654-64 64-64h64c35.346 0 64 28.654 64 64zM828.784 738.274l45.256 45.256c24.992 24.992 24.992 65.516 0 90.51-24.994 24.992-65.518 24.992-90.51 0l-45.256-45.256c-24.992-24.992-24.992-65.516 0-90.51 24.994-24.992 65.518-24.992 90.51 0zM195.216 285.726l-45.256-45.256c-24.994-24.994-24.994-65.516 0-90.51s65.516-24.994 90.51 0l45.256 45.256c24.994 24.994 24.994 65.516 0 90.51s-65.516 24.994-90.51 0zM828.784 285.726c-24.992 24.992-65.516 24.992-90.51 0-24.992-24.994-24.992-65.516 0-90.51l45.256-45.254c24.992-24.994 65.516-24.994 90.51 0 24.992 24.994 24.992 65.516 0 90.51l-45.256 45.254zM195.216 738.274c24.992-24.992 65.516-24.992 90.508 0 24.994 24.994 24.994 65.518 0 90.51l-45.254 45.256c-24.994 24.992-65.516 24.992-90.51 0-24.994-24.994-24.994-65.518 0-90.51l45.256-45.256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "brightness-contrast" + ], + "defaultCode": 59862, + "grid": 16 + }, + { + "id": 215, + "paths": [ + "M1024 397.050l-353.78-51.408-158.22-320.582-158.216 320.582-353.784 51.408 256 249.538-60.432 352.352 316.432-166.358 316.432 166.358-60.434-352.352 256.002-249.538zM512 753.498l-223.462 117.48 42.676-248.83-180.786-176.222 249.84-36.304 111.732-226.396 111.736 226.396 249.836 36.304-180.788 176.222 42.678 248.83-223.462-117.48z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "star-empty", + "rate", + "star", + "favorite", + "bookmark" + ], + "defaultCode": 59863, + "grid": 16 + }, + { + "id": 216, + "paths": [ + "M1024 397.050l-353.78-51.408-158.22-320.582-158.216 320.582-353.784 51.408 256 249.538-60.432 352.352 316.432-166.358 316.432 166.358-60.434-352.352 256.002-249.538zM512 753.498l-0.942 0.496 0.942-570.768 111.736 226.396 249.836 36.304-180.788 176.222 42.678 248.83-223.462-117.48z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "star-half", + "rate", + "star" + ], + "defaultCode": 59864, + "grid": 16 + }, + { + "id": 217, + "paths": [ + "M1024 397.050l-353.78-51.408-158.22-320.582-158.216 320.582-353.784 51.408 256 249.538-60.432 352.352 316.432-166.358 316.432 166.358-60.434-352.352 256.002-249.538z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "star-full", + "rate", + "star", + "favorite", + "bookmark" + ], + "defaultCode": 59865, + "grid": 16 + }, + { + "id": 218, + "paths": [ + "M755.188 64c-107.63 0-200.258 87.554-243.164 179-42.938-91.444-135.578-179-243.216-179-148.382 0-268.808 120.44-268.808 268.832 0 301.846 304.5 380.994 512.022 679.418 196.154-296.576 511.978-387.206 511.978-679.418 0-148.392-120.43-268.832-268.812-268.832z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "heart", + "like", + "love", + "favorite" + ], + "defaultCode": 59866, + "grid": 16 + }, + { + "id": 219, + "paths": [ + "M755.188 64c148.382 0 268.812 120.44 268.812 268.832 0 292.21-315.824 382.842-511.978 679.418-207.522-298.424-512.022-377.572-512.022-679.418 0-148.392 120.426-268.832 268.808-268.832 60.354 0 115.99 27.53 160.796 67.834l-77.604 124.166 224 128-128 320 352-384-224-128 61.896-92.846c35.42-21.768 75.21-35.154 117.292-35.154z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "heart-broken", + "heart", + "like", + "love" + ], + "defaultCode": 59867, + "grid": 16 + }, + { + "id": 220, + "paths": [ + "M576 96c0 53.019-42.981 96-96 96s-96-42.981-96-96c0-53.019 42.981-96 96-96s96 42.981 96 96z", + "M576 256h-192c-35.346 0-64 28.654-64 64v320h64v384h80v-384h32v384h80v-384h64v-320c0-35.346-28.652-64-64-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "man", + "male", + "gender", + "sex" + ], + "defaultCode": 59868, + "grid": 16 + }, + { + "id": 221, + "paths": [ + "M576 96c0 53.019-42.981 96-96 96s-96-42.981-96-96c0-53.019 42.981-96 96-96s96 42.981 96 96z", + "M719 512l49-35.5-133.286-206.116c-5.92-8.98-15.958-14.384-26.714-14.384h-256c-10.756 0-20.792 5.404-26.714 14.384l-133.286 206.116 49 35.5 110.644-143.596 38.458 89.74-134.102 245.856h122.666l21.334 320h64v-320h32v320h64l21.334-320h122.666l-134.104-245.858 38.458-89.74 110.646 143.598z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "woman", + "female", + "gender", + "sex" + ], + "defaultCode": 59869, + "grid": 16 + }, + { + "id": 222, + "paths": [ + "M256 96c0 53.019-42.981 96-96 96s-96-42.981-96-96c0-53.019 42.981-96 96-96s96 42.981 96 96z", + "M832 96c0 53.019-42.981 96-96 96s-96-42.981-96-96c0-53.019 42.981-96 96-96s96 42.981 96 96z", + "M256 256h-192c-35.346 0-64 28.654-64 64v320h64v384h80v-384h32v384h80v-384h64v-320c0-35.346-28.652-64-64-64z", + "M975 512l49-35.5-133.286-206.116c-5.92-8.98-15.958-14.384-26.714-14.384h-256c-10.756 0-20.792 5.404-26.714 14.384l-133.286 206.116 49 35.5 110.644-143.596 38.458 89.74-134.102 245.856h122.666l21.334 320h64v-320h32v320h64l21.334-320h122.666l-134.104-245.858 38.458-89.74 110.646 143.598z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "man-woman", + "toilet", + "bathroom", + "sex", + "gender" + ], + "defaultCode": 59870, + "grid": 16 + }, + { + "id": 223, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM512 598.76c115.95 0 226.23-30.806 320-84.92-14.574 178.438-153.128 318.16-320 318.16-166.868 0-305.422-139.872-320-318.304 93.77 54.112 204.050 85.064 320 85.064zM256 352c0-53.019 28.654-96 64-96s64 42.981 64 96c0 53.019-28.654 96-64 96s-64-42.981-64-96zM640 352c0-53.019 28.654-96 64-96s64 42.981 64 96c0 53.019-28.654 96-64 96s-64-42.981-64-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "happy", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59871, + "grid": 16 + }, + { + "id": 224, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM704 256c35.348 0 64 42.98 64 96s-28.652 96-64 96-64-42.98-64-96 28.652-96 64-96zM320 256c35.346 0 64 42.98 64 96s-28.654 96-64 96-64-42.98-64-96 28.654-96 64-96zM512 896c-166.868 0-305.422-139.872-320-318.304 93.77 54.114 204.050 85.064 320 85.064s226.23-30.806 320-84.92c-14.574 178.438-153.128 318.16-320 318.16z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "happy", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59872, + "grid": 16 + }, + { + "id": 225, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM256 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM640 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM704.098 627.26l82.328 49.396c-55.962 93.070-157.916 155.344-274.426 155.344s-218.464-62.274-274.426-155.344l82.328-49.396c39.174 65.148 110.542 108.74 192.098 108.74s152.924-43.592 192.098-108.74z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "smile", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59873, + "grid": 16 + }, + { + "id": 226, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM704 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64 28.654-64 64-64zM320 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64 28.654-64 64-64zM512 832c-116.51 0-218.464-62.274-274.426-155.344l82.328-49.396c39.174 65.148 110.542 108.74 192.098 108.74s152.924-43.592 192.098-108.74l82.328 49.396c-55.962 93.070-157.916 155.344-274.426 155.344z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "smile", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59874, + "grid": 16 + }, + { + "id": 227, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM256 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM640 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM768 576v64h-64v96c0 53.020-42.98 96-96 96s-96-42.98-96-96v-96h-256v-64h512z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tongue", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59875, + "grid": 16 + }, + { + "id": 228, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM320 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64 28.654-64 64-64zM768 640h-64v96c0 53.020-42.98 96-96 96s-96-42.98-96-96v-96h-256v-64h512v64zM704 384c-35.346 0-64-28.654-64-64s28.654-64 64-64 64 28.654 64 64-28.654 64-64 64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tongue", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59876, + "grid": 16 + }, + { + "id": 229, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM256 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM640 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM319.902 780.74l-82.328-49.396c55.962-93.070 157.916-155.344 274.426-155.344 116.508 0 218.462 62.274 274.426 155.344l-82.328 49.396c-39.174-65.148-110.542-108.74-192.098-108.74-81.558 0-152.924 43.592-192.098 108.74z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sad", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59877, + "grid": 16 + }, + { + "id": 230, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM704 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64 28.654-64 64-64zM320 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64 28.654-64 64-64zM704.098 780.74c-39.174-65.148-110.544-108.74-192.098-108.74-81.556 0-152.924 43.592-192.098 108.74l-82.328-49.396c55.96-93.070 157.916-155.344 274.426-155.344 116.508 0 218.464 62.274 274.426 155.344l-82.328 49.396z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sad", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59878, + "grid": 16 + }, + { + "id": 231, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM542.74 711.028c140.248-27.706 249.11-91.542 288.454-176.594-21.654 167.956-161.518 297.566-330.85 297.566-119.242 0-223.858-64.282-282.892-160.948 70.41 55.058 194.534 65.808 325.288 39.976zM640 352c0-53.019 28.654-96 64-96s64 42.981 64 96c0 53.019-28.654 96-64 96s-64-42.981-64-96zM352 371.5c-41.796 0-77.334 15.656-90.516 37.5-3.54-5.866-5.484-32.174-5.484-38.75 0-31.066 42.98-56.25 96-56.25s96 25.184 96 56.25c0 6.576-1.944 32.884-5.484 38.75-13.182-21.844-48.72-37.5-90.516-37.5z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wink", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59879, + "grid": 16 + }, + { + "id": 232, + "paths": [ + "M512 0c-282.77 0-512 229.228-512 512 0 282.77 229.228 512 512 512 282.77 0 512-229.23 512-512 0-282.772-229.23-512-512-512zM704 256c35.346 0 64 42.98 64 96s-28.654 96-64 96-64-42.98-64-96 28.654-96 64-96zM352 312.062c59.646 0 102 22.332 102 57.282 0 7.398 3.812 42.994-0.17 49.594-14.828-24.576-54.81-42.188-101.83-42.188s-87.002 17.612-101.83 42.188c-3.982-6.6-0.17-42.196-0.17-49.594 0-34.95 42.354-57.282 102-57.282zM500.344 832c-119.242 0-223.858-64.28-282.892-160.952 70.41 55.060 194.534 65.81 325.288 39.978 140.248-27.706 249.11-91.542 288.454-176.594-21.654 167.96-161.518 297.568-330.85 297.568z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wink", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59880, + "grid": 16 + }, + { + "id": 233, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM192 512v64c0 140.8 115.2 256 256 256h128c140.8 0 256-115.2 256-256v-64h-640zM384 756.988c-26.538-9.458-50.924-24.822-71.544-45.446-36.406-36.402-56.456-84.54-56.456-135.542h128v180.988zM576 768h-128v-192h128v192zM711.544 711.542c-20.624 20.624-45.010 35.988-71.544 45.446v-180.988h128c0 51.002-20.048 99.14-56.456 135.542zM225.352 384c0.002 0 0 0 0 0 9.768 0 18.108-7.056 19.724-16.69 6.158-36.684 37.668-63.31 74.924-63.31s68.766 26.626 74.924 63.31c1.616 9.632 9.956 16.69 19.722 16.69 9.768 0 18.108-7.056 19.724-16.688 1.082-6.436 1.628-12.934 1.628-19.312 0-63.962-52.038-116-116-116s-116 52.038-116 116c0 6.378 0.548 12.876 1.628 19.312 1.62 9.632 9.96 16.688 19.726 16.688zM609.352 384c0.002 0 0 0 0 0 9.77 0 18.112-7.056 19.724-16.69 6.158-36.684 37.668-63.31 74.924-63.31s68.766 26.626 74.924 63.31c1.616 9.632 9.958 16.69 19.722 16.69s18.108-7.056 19.722-16.688c1.082-6.436 1.628-12.934 1.628-19.312 0-63.962-52.038-116-116-116s-116 52.038-116 116c0 6.378 0.544 12.876 1.626 19.312 1.624 9.632 9.964 16.688 19.73 16.688z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "grin", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59881, + "grid": 16 + }, + { + "id": 234, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.226 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM704 236c63.962 0 116 52.038 116 116 0 6.378-0.546 12.876-1.628 19.312-1.618 9.632-9.958 16.688-19.724 16.688s-18.108-7.056-19.722-16.69c-6.16-36.684-37.67-53.31-74.926-53.31s-68.766 16.626-74.924 53.31c-1.616 9.632-9.956 16.69-19.722 16.69-0.002 0 0 0-0.002 0-9.766 0-18.106-7.056-19.722-16.688-1.084-6.436-1.63-12.934-1.63-19.312 0-63.962 52.038-116 116-116zM320 236c63.962 0 116 52.038 116 116 0 6.378-0.548 12.876-1.628 19.312-1.618 9.632-9.956 16.688-19.724 16.688s-18.106-7.056-19.722-16.69c-6.16-36.684-37.67-53.31-74.926-53.31s-68.766 16.626-74.924 53.31c-1.616 9.632-9.956 16.69-19.722 16.69 0 0 0 0 0 0-9.766 0-18.106-7.056-19.724-16.688-1.082-6.436-1.63-12.934-1.63-19.312 0-63.962 52.038-116 116-116zM192 576h192v247.846c-110.094-28.606-192-129.124-192-247.846zM448 832v-256h128v256h-128zM640 823.846v-247.846h192c0 118.722-81.904 219.24-192 247.846z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "grin", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59882, + "grid": 16 + }, + { + "id": 235, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM800 256c17.6 0 32 14.4 32 32v96c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64h-128c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64v-96c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v32h128v-32c0-17.6 14.4-32 32-32h192zM512 768c93.208 0 174.772-49.818 219.546-124.278l54.88 32.934c-55.966 93.070-157.916 155.344-274.426 155.344-48.458 0-94.384-10.796-135.54-30.082l33.162-55.278c31.354 13.714 65.964 21.36 102.378 21.36z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cool", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59883, + "grid": 16 + }, + { + "id": 236, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.226 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM512 832c-48.458 0-94.384-10.796-135.542-30.082l33.162-55.276c31.356 13.712 65.966 21.358 102.38 21.358 93.208 0 174.772-49.818 219.542-124.278l54.882 32.934c-55.964 93.070-157.914 155.344-274.424 155.344zM832 384c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64h-128c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64v-96c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v32h128v-32c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cool", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59884, + "grid": 16 + }, + { + "id": 237, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM704.098 780.74c-39.174-65.148-110.544-108.74-192.098-108.74-81.556 0-152.924 43.592-192.098 108.74l-82.328-49.396c55.96-93.070 157.916-155.344 274.426-155.344 116.508 0 218.464 62.274 274.426 155.344l-82.328 49.396zM767.042 280.24c4.284 17.144-6.14 34.518-23.282 38.804-17.626 4.45-38.522 12.12-56.936 21.35 10.648 11.43 17.174 26.752 17.174 43.606 0 35.346-28.654 64-64 64s-64-28.654-64-64c0-1.17 0.036-2.33 0.098-3.484 2.032-47.454 45.212-78.946 81.592-97.138 34.742-17.37 69.102-26.060 70.548-26.422 17.146-4.288 34.518 6.138 38.806 23.284zM256.958 280.24c4.288-17.146 21.66-27.572 38.806-23.284 1.446 0.362 35.806 9.052 70.548 26.422 36.38 18.192 79.56 49.684 81.592 97.138 0.062 1.154 0.098 2.314 0.098 3.484 0 35.346-28.654 64-64 64s-64-28.654-64-64c0-16.854 6.526-32.176 17.174-43.606-18.414-9.23-39.31-16.9-56.936-21.35-17.142-4.286-27.566-21.66-23.282-38.804z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "angry", + "emoticon", + "smiley", + "face", + "rage" + ], + "defaultCode": 59885, + "grid": 16 + }, + { + "id": 238, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM576.094 380.516c2.032-47.454 45.21-78.948 81.592-97.138 34.742-17.372 69.104-26.060 70.548-26.422 17.146-4.288 34.52 6.138 38.806 23.284s-6.138 34.518-23.284 38.806c-17.624 4.45-38.522 12.12-56.936 21.35 10.648 11.43 17.174 26.752 17.174 43.606 0 35.346-28.654 64-64 64s-64-28.654-64-64c0.002-1.17 0.038-2.332 0.1-3.486zM256.958 280.24c4.288-17.146 21.66-27.572 38.806-23.284 1.446 0.362 35.806 9.052 70.548 26.422 36.38 18.192 79.56 49.684 81.592 97.138 0.062 1.154 0.098 2.314 0.098 3.484 0 35.346-28.654 64-64 64s-64-28.654-64-64c0-16.854 6.526-32.176 17.174-43.606-18.414-9.23-39.31-16.9-56.936-21.35-17.142-4.286-27.566-21.66-23.282-38.804zM704.098 780.74c-39.174-65.148-110.544-108.74-192.098-108.74-81.556 0-152.924 43.592-192.098 108.74l-82.328-49.396c55.96-93.070 157.916-155.344 274.426-155.344 116.508 0 218.464 62.274 274.426 155.344l-82.328 49.396z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "angry", + "emoticon", + "smiley", + "face", + "rage" + ], + "defaultCode": 59886, + "grid": 16 + }, + { + "id": 239, + "paths": [ + "M639.996 448c-35.346 0-64-28.654-63.998-64.002 0-1.17 0.036-2.33 0.098-3.484 2.032-47.454 45.212-78.946 81.592-97.138 34.742-17.37 69.102-26.060 70.548-26.422 17.146-4.288 34.518 6.138 38.806 23.284 4.284 17.146-6.14 34.518-23.284 38.806-17.626 4.45-38.522 12.12-56.936 21.35 10.648 11.43 17.174 26.752 17.174 43.606 0 35.346-28.654 64-64 64zM280.242 319.044c-17.144-4.286-27.568-21.66-23.282-38.804 4.286-17.146 21.66-27.572 38.806-23.284 1.444 0.362 35.806 9.050 70.548 26.422 36.382 18.19 79.56 49.684 81.592 97.138 0.062 1.154 0.098 2.316 0.098 3.484 0 35.346-28.654 64-64 64s-64-28.654-64-64c0-16.854 6.526-32.176 17.174-43.606-18.414-9.23-39.312-16.9-56.936-21.35zM512 736c81.554 0 152.924-43.592 192.098-108.74l82.328 49.396c-55.962 93.070-157.916 155.344-274.426 155.344s-218.464-62.274-274.426-155.344l82.328-49.396c39.174 65.148 110.542 108.74 192.098 108.74zM1024 64c0-45.516-9.524-88.8-26.652-128-33.576 76.836-96.448 137.932-174.494 169.178-86.194-65.96-193.936-105.178-310.854-105.178s-224.66 39.218-310.854 105.178c-78.048-31.246-140.918-92.342-174.494-169.178-17.128 39.2-26.652 82.484-26.652 128 0 73.574 24.85 141.328 66.588 195.378-42.37 74.542-66.588 160.75-66.588 252.622 0 282.77 229.23 512 512 512s512-229.23 512-512c0-91.872-24.218-178.080-66.588-252.622 41.738-54.050 66.588-121.804 66.588-195.378zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "evil", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59887, + "grid": 16 + }, + { + "id": 240, + "paths": [ + "M1024 64c0-45.516-9.524-88.8-26.652-128-33.576 76.836-96.448 137.932-174.494 169.178-86.194-65.96-193.936-105.178-310.854-105.178s-224.66 39.218-310.854 105.178c-78.048-31.246-140.918-92.342-174.494-169.178-17.128 39.2-26.652 82.484-26.652 128 0 73.574 24.85 141.328 66.588 195.378-42.37 74.542-66.588 160.75-66.588 252.622 0 282.77 229.23 512 512 512s512-229.23 512-512c0-91.872-24.218-178.080-66.588-252.622 41.738-54.050 66.588-121.804 66.588-195.378zM576.094 380.516c2.032-47.454 45.21-78.948 81.592-97.138 34.742-17.372 69.104-26.060 70.548-26.422 17.146-4.288 34.52 6.138 38.806 23.284s-6.138 34.518-23.284 38.806c-17.624 4.45-38.522 12.12-56.936 21.35 10.648 11.43 17.174 26.752 17.174 43.606 0 35.346-28.654 64-64 64s-64-28.654-64-64c0.002-1.17 0.038-2.332 0.1-3.486zM256.958 280.24c4.288-17.146 21.66-27.572 38.806-23.284 1.446 0.362 35.806 9.052 70.548 26.422 36.38 18.192 79.56 49.684 81.592 97.138 0.062 1.154 0.098 2.314 0.098 3.484 0 35.346-28.654 64-64 64s-64-28.654-64-64c0-16.854 6.526-32.176 17.174-43.606-18.414-9.23-39.31-16.9-56.936-21.35-17.142-4.286-27.566-21.66-23.282-38.804zM512 832c-116.51 0-218.464-62.274-274.426-155.344l82.328-49.396c39.174 65.148 110.542 108.74 192.098 108.74 81.554 0 152.924-43.592 192.098-108.74l82.328 49.396c-55.962 93.070-157.916 155.344-274.426 155.344z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "evil", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59888, + "grid": 16 + }, + { + "id": 241, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM384 704c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM640 352c0-53.019 28.654-96 64-96s64 42.981 64 96c0 53.019-28.654 96-64 96s-64-42.981-64-96zM256 352c0-53.019 28.654-96 64-96s64 42.981 64 96c0 53.019-28.654 96-64 96s-64-42.981-64-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shocked", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59889, + "grid": 16 + }, + { + "id": 242, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM320 448c-35.346 0-64-42.98-64-96s28.654-96 64-96 64 42.98 64 96-28.654 96-64 96zM512 832c-70.692 0-128-57.308-128-128s57.308-128 128-128c70.692 0 128 57.308 128 128s-57.308 128-128 128zM704 448c-35.346 0-64-42.98-64-96s28.654-96 64-96 64 42.98 64 96-28.654 96-64 96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shocked", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59890, + "grid": 16 + }, + { + "id": 243, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416z", + "M384 416c0 17.673-14.327 32-32 32s-32-14.327-32-32c0-17.673 14.327-32 32-32s32 14.327 32 32z", + "M352 320c53.020 0 96 42.98 96 96s-42.98 96-96 96-96-42.98-96-96 42.98-96 96-96zM352 256c-88.224 0-160 71.776-160 160s71.776 160 160 160 160-71.776 160-160-71.776-160-160-160v0z", + "M704 416c0 17.673-14.327 32-32 32s-32-14.327-32-32c0-17.673 14.327-32 32-32s32 14.327 32 32z", + "M672 320c53.020 0 96 42.98 96 96s-42.98 96-96 96-96-42.98-96-96 42.98-96 96-96zM672 256c-88.224 0-160 71.776-160 160s71.776 160 160 160 160-71.776 160-160-71.776-160-160-160v0z", + "M384 704h256v64h-256v-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "baffled", + "emoticon", + "smiley", + "shocked", + "face" + ], + "defaultCode": 59891, + "grid": 16 + }, + { + "id": 244, + "paths": [ + "M384 416c0 17.674-14.326 32-32 32s-32-14.326-32-32 14.326-32 32-32 32 14.326 32 32z", + "M704 416c0 17.674-14.326 32-32 32s-32-14.326-32-32 14.326-32 32-32 32 14.326 32 32z", + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM256 416c0-53.020 42.98-96 96-96s96 42.98 96 96-42.98 96-96 96-96-42.98-96-96zM640 768h-256v-64h256v64zM672 512c-53.020 0-96-42.98-96-96s42.98-96 96-96 96 42.98 96 96-42.98 96-96 96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "baffled", + "emoticon", + "smiley", + "shocked", + "face" + ], + "defaultCode": 59892, + "grid": 16 + }, + { + "id": 245, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM256 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM640 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM726.106 640h64.864c9.246 72.506-32.452 144.53-103.958 170.56-82.904 30.176-174.9-12.716-205.080-95.616-18.108-49.744-73.306-75.482-123.048-57.372-45.562 16.588-70.956 64.298-60.988 110.424h-64.86c-9.242-72.508 32.45-144.528 103.956-170.56 82.904-30.178 174.902 12.716 205.082 95.614 18.104 49.748 73.306 75.482 123.044 57.372 45.562-16.584 70.956-64.298 60.988-110.422z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "confused", + "emoticon", + "smiley", + "face", + "bewildered" + ], + "defaultCode": 59893, + "grid": 16 + }, + { + "id": 246, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.226 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM704 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64c0-35.346 28.654-64 64-64zM320 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64c0-35.346 28.654-64 64-64zM687.010 810.56c-82.902 30.18-174.9-12.712-205.080-95.614-18.108-49.742-73.306-75.478-123.048-57.372-45.562 16.588-70.958 64.296-60.988 110.424h-64.86c-9.244-72.508 32.45-144.532 103.956-170.56 82.904-30.18 174.902 12.712 205.082 95.614 18.108 49.742 73.306 75.476 123.046 57.37 45.562-16.584 70.958-64.294 60.988-110.422h64.864c9.24 72.506-32.454 144.532-103.96 170.56z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "confused", + "emoticon", + "smiley", + "face", + "bewildered" + ], + "defaultCode": 59894, + "grid": 16 + }, + { + "id": 247, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM256 320c0 35.346 28.654 64 64 64s64-28.654 64-64-28.654-64-64-64-64 28.654-64 64zM640 320c0 35.346 28.654 64 64 64s64-28.654 64-64-28.654-64-64-64-64 28.654-64 64zM384 704h256v64h-256v-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "neutral", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59895, + "grid": 16 + }, + { + "id": 248, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.226 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM640 768h-256v-64h256v64zM704 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64c0-35.346 28.654-64 64-64zM320 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64c0-35.346 28.654-64 64-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "neutral", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59896, + "grid": 16 + }, + { + "id": 249, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM256 320c0-35.346 28.654-64 64-64s64 28.654 64 64-28.654 64-64 64-64-28.654-64-64zM640 320c0-35.346 28.654-64 64-64s64 28.654 64 64-28.654 64-64 64-64-28.654-64-64z", + "M675.882 540.118c-37.49-37.49-98.276-37.49-135.766 0s-37.49 98.276 0 135.766c1.204 1.204 2.434 2.368 3.684 3.492 86.528 78.512 288.2-1.842 288.2-103.376-62 40-110.45 9.786-156.118-35.882z", + "M348.118 540.118c37.49-37.49 98.276-37.49 135.766 0s37.49 98.276 0 135.766c-1.204 1.204-2.434 2.368-3.684 3.492-86.528 78.512-288.2-1.842-288.2-103.376 62 40 110.45 9.786 156.118-35.882z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hipster", + "emoticon", + "smiley", + "mustache", + "face" + ], + "defaultCode": 59897, + "grid": 16 + }, + { + "id": 250, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM704 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64 28.654-64 64-64zM320 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64 28.654-64 64-64zM543.8 679.376c-1.25-1.124-2.48-2.29-3.684-3.492-18.74-18.74-28.112-43.3-28.118-67.864-0.004 24.562-9.376 49.124-28.118 67.864-1.204 1.204-2.434 2.368-3.684 3.492-86.524 78.512-288.196-1.842-288.196-103.376 62 40 110.45 9.786 156.118-35.882 37.49-37.49 98.276-37.49 135.766 0 18.74 18.74 28.112 43.3 28.118 67.864 0.004-24.562 9.376-49.124 28.118-67.864 37.49-37.49 98.276-37.49 135.766 0 45.664 45.668 94.114 75.882 156.114 35.882 0 101.534-201.672 181.888-288.2 103.376z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hipster", + "emoticon", + "smiley", + "mustache", + "face" + ], + "defaultCode": 59898, + "grid": 16 + }, + { + "id": 251, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM745.74 601.62l22.488 76.776-437.008 128.002-22.488-76.776zM256 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM640 320c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wondering", + "emoticon", + "smiley", + "face", + "question" + ], + "defaultCode": 59899, + "grid": 16 + }, + { + "id": 252, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM704 256c35.346 0 64 28.654 64 64s-28.654 64-64 64-64-28.654-64-64 28.654-64 64-64zM256 320c0-35.346 28.654-64 64-64s64 28.654 64 64-28.654 64-64 64-64-28.654-64-64zM331.244 806.386l-22.488-76.774 437-128 22.488 76.774-437 128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wondering", + "emoticon", + "smiley", + "face", + "question" + ], + "defaultCode": 59900, + "grid": 16 + }, + { + "id": 253, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416z", + "M640 672c0 88.366-57.308 160-128.002 160s-128.002-71.634-128.002-160c0-88.366 57.308-160 128.002-160s128.002 71.634 128.002 160z", + "M416 340c-8.19 0-16.378-3.124-22.626-9.374-19.334-19.332-63.412-19.332-82.746 0-12.496 12.498-32.758 12.498-45.254 0-12.498-12.496-12.498-32.758 0-45.254 44.528-44.53 128.726-44.53 173.254 0 12.498 12.496 12.498 32.758 0 45.254-6.248 6.25-14.438 9.374-22.628 9.374z", + "M736 340c-8.19 0-16.378-3.124-22.626-9.374-19.332-19.332-63.414-19.332-82.746 0-12.496 12.498-32.758 12.498-45.254 0-12.498-12.496-12.498-32.758 0-45.254 44.528-44.53 128.726-44.53 173.254 0 12.498 12.496 12.498 32.758 0 45.254-6.248 6.25-14.438 9.374-22.628 9.374z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sleepy", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59901, + "grid": 16 + }, + { + "id": 254, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM310.628 330.626c-12.496 12.498-32.758 12.498-45.254 0-12.498-12.496-12.498-32.758 0-45.254 44.528-44.53 128.726-44.53 173.254 0 12.498 12.496 12.498 32.758 0 45.254-6.248 6.25-14.438 9.374-22.628 9.374s-16.378-3.124-22.626-9.374c-19.334-19.332-63.412-19.332-82.746 0zM511.998 832c-70.694 0-128.002-71.634-128.002-160s57.308-160 128.002-160 128.002 71.634 128.002 160-57.308 160-128.002 160zM758.628 330.626c-6.248 6.25-14.438 9.374-22.628 9.374s-16.378-3.124-22.626-9.374c-19.332-19.332-63.414-19.332-82.746 0-12.496 12.498-32.758 12.498-45.254 0-12.498-12.496-12.498-32.758 0-45.254 44.528-44.53 128.726-44.53 173.254 0 12.498 12.498 12.498 32.758 0 45.254z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sleepy", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59902, + "grid": 16 + }, + { + "id": 255, + "paths": [ + "M366.312 283.378c-34.742-17.37-69.102-26.060-70.548-26.422-17.146-4.288-34.518 6.138-38.806 23.284-4.284 17.144 6.14 34.518 23.282 38.804 17.626 4.45 38.522 12.12 56.936 21.35-10.648 11.43-17.174 26.752-17.174 43.606 0 35.346 28.654 64 64 64s64-28.654 64-64c0-1.17-0.036-2.33-0.098-3.484-2.032-47.454-45.212-78.946-81.592-97.138z", + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM236.498 823.664c10.706 5.324 22.756 8.336 35.502 8.336h480c12.746 0 24.796-3.012 35.502-8.338-73.378 64.914-169.828 104.338-275.502 104.338-105.672 0-202.124-39.424-275.502-104.336zM256 752v-96c0-8.674 7.328-16 16-16h112v128h-112c-8.672 0-16-7.326-16-16zM448 768v-128h128v128h-128zM640 768v-128h112c8.674 0 16 7.326 16 16v96c0 8.674-7.326 16-16 16h-112zM823.662 787.502c5.326-10.706 8.338-22.756 8.338-35.502v-96c0-44.112-35.888-80-80-80h-480c-44.112 0-80 35.888-80 80v96c0 12.746 3.012 24.796 8.336 35.502-64.912-73.378-104.336-169.828-104.336-275.502 0-229.75 186.25-416 416-416s416 186.25 416 416c0 105.674-39.424 202.124-104.338 275.502z", + "M728.236 256.956c-1.448 0.362-35.806 9.052-70.548 26.422-36.378 18.192-79.558 49.684-81.592 97.138-0.060 1.154-0.098 2.314-0.098 3.484 0 35.346 28.654 64 64 64s64-28.654 64-64c0-16.854-6.526-32.176-17.174-43.606 18.414-9.23 39.31-16.9 56.936-21.35 17.142-4.286 27.566-21.66 23.284-38.804-4.29-17.146-21.662-27.572-38.808-23.284z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "frustrated", + "emoticon", + "smiley", + "face", + "angry" + ], + "defaultCode": 59903, + "grid": 16 + }, + { + "id": 256, + "paths": [ + "M256 656v96c0 8.674 7.328 16 16 16h112v-128h-112c-8.672 0-16 7.326-16 16z", + "M448 640h128v128h-128v-128z", + "M752 640h-112v128h112c8.674 0 16-7.326 16-16v-96c0-8.674-7.326-16-16-16z", + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM576.096 380.516c2.034-47.454 45.212-78.946 81.592-97.138 34.742-17.37 69.102-26.060 70.548-26.422 17.146-4.288 34.518 6.138 38.806 23.284 4.284 17.144-6.14 34.518-23.284 38.804-17.624 4.45-38.522 12.12-56.936 21.35 10.648 11.43 17.174 26.752 17.174 43.606 0 35.346-28.654 64-64 64s-64-28.654-64-64c0.002-1.17 0.040-2.33 0.1-3.484zM256.958 280.24c4.288-17.146 21.66-27.572 38.806-23.284 1.446 0.362 35.806 9.052 70.548 26.422 36.38 18.192 79.56 49.684 81.592 97.138 0.062 1.154 0.098 2.314 0.098 3.484 0 35.346-28.654 64-64 64s-64-28.654-64-64c0-16.854 6.526-32.176 17.174-43.606-18.414-9.23-39.31-16.9-56.936-21.35-17.142-4.286-27.566-21.66-23.282-38.804zM832 752c0 44.112-35.888 80-80 80h-480c-44.112 0-80-35.888-80-80v-96c0-44.112 35.888-80 80-80h480c44.112 0 80 35.888 80 80v96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "frustrated", + "emoticon", + "smiley", + "face", + "angry" + ], + "defaultCode": 59904, + "grid": 16 + }, + { + "id": 257, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416z", + "M800 384h-128c-17.674 0-32-14.328-32-32s14.326-32 32-32h128c17.674 0 32 14.328 32 32s-14.326 32-32 32z", + "M352 384h-128c-17.672 0-32-14.328-32-32s14.328-32 32-32h128c17.672 0 32 14.328 32 32s-14.328 32-32 32z", + "M608 856c-8.19 0-16.378-3.124-22.626-9.374-4.582-4.582-29.42-14.626-73.374-14.626s-68.79 10.044-73.374 14.626c-12.496 12.496-32.758 12.496-45.254 0-12.498-12.496-12.498-32.758 0-45.254 30.122-30.12 92.994-33.372 118.628-33.372 25.632 0 88.506 3.252 118.626 33.374 12.498 12.496 12.498 32.758 0 45.254-6.248 6.248-14.436 9.372-22.626 9.372z", + "M736 576c-17.674 0-32-14.326-32-32v-64c0-17.672 14.326-32 32-32s32 14.328 32 32v64c0 17.674-14.326 32-32 32z", + "M736 768c-17.674 0-32-14.326-32-32v-64c0-17.674 14.326-32 32-32s32 14.326 32 32v64c0 17.674-14.326 32-32 32z", + "M288 576c-17.672 0-32-14.326-32-32v-64c0-17.672 14.328-32 32-32s32 14.328 32 32v64c0 17.674-14.328 32-32 32z", + "M288 768c-17.672 0-32-14.326-32-32v-64c0-17.674 14.328-32 32-32s32 14.326 32 32v64c0 17.674-14.328 32-32 32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "crying", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59905, + "grid": 16 + }, + { + "id": 258, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM320 736c0 17.674-14.328 32-32 32s-32-14.326-32-32v-64c0-17.674 14.328-32 32-32s32 14.326 32 32v64zM320 544c0 17.674-14.328 32-32 32s-32-14.326-32-32v-64c0-17.672 14.328-32 32-32s32 14.328 32 32v64zM352 384h-128c-17.672 0-32-14.328-32-32s14.328-32 32-32h128c17.672 0 32 14.328 32 32s-14.328 32-32 32zM630.626 846.626c-6.248 6.25-14.436 9.374-22.626 9.374s-16.378-3.124-22.626-9.374c-4.582-4.582-29.42-14.626-73.374-14.626s-68.79 10.044-73.374 14.626c-12.496 12.496-32.758 12.496-45.254 0-12.498-12.496-12.498-32.758 0-45.254 30.122-30.12 92.994-33.372 118.628-33.372 25.632 0 88.506 3.252 118.626 33.374 12.498 12.496 12.498 32.756 0 45.252zM768 736c0 17.674-14.326 32-32 32s-32-14.326-32-32v-64c0-17.674 14.326-32 32-32s32 14.326 32 32v64zM768 544c0 17.674-14.326 32-32 32s-32-14.326-32-32v-64c0-17.672 14.326-32 32-32s32 14.328 32 32v64zM800 384h-128c-17.674 0-32-14.328-32-32s14.326-32 32-32h128c17.674 0 32 14.328 32 32s-14.326 32-32 32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "crying", + "emoticon", + "smiley", + "face" + ], + "defaultCode": 59906, + "grid": 16 + }, + { + "id": 259, + "paths": [ + "M960 608v-160c0-52.934-43.066-96-96-96-17.104 0-33.176 4.494-47.098 12.368-17.076-26.664-46.958-44.368-80.902-44.368-24.564 0-47.004 9.274-64 24.504-16.996-15.23-39.436-24.504-64-24.504-11.214 0-21.986 1.934-32 5.484v-229.484c0-52.934-43.066-96-96-96s-96 43.066-96 96v394.676l-176.018-93.836c-14.536-8.4-31.126-12.84-47.982-12.84-52.934 0-96 43.066-96 96 0 26.368 10.472 50.954 29.49 69.226 0.248 0.238 0.496 0.47 0.75 0.7l239.17 218.074h-45.41c-17.672 0-32 14.326-32 32v192c0 17.674 14.328 32 32 32h640c17.674 0 32-14.326 32-32v-192c0-17.674-14.326-32-32-32h-44.222l72.844-145.69c2.222-4.442 3.378-9.342 3.378-14.31zM896 864c0 17.674-14.326 32-32 32s-32-14.326-32-32 14.326-32 32-32 32 14.326 32 32zM896 600.446l-83.776 167.554h-383.826l-290.818-265.166c-6.18-6.070-9.58-14.164-9.58-22.834 0-17.644 14.356-32 32-32 5.46 0 10.612 1.31 15.324 3.894 0.53 0.324 1.070 0.632 1.622 0.926l224 119.416c9.92 5.288 21.884 4.986 31.52-0.8 9.638-5.782 15.534-16.196 15.534-27.436v-448c0-17.644 14.356-32 32-32s32 14.356 32 32v320c0 17.672 14.326 32 32 32s32-14.328 32-32c0-17.644 14.356-32 32-32s32 14.356 32 32c0 17.672 14.326 32 32 32s32-14.328 32-32c0-17.644 14.356-32 32-32s32 14.356 32 32v32c0 17.672 14.326 32 32 32s32-14.328 32-32c0-17.644 14.356-32 32-32s32 14.356 32 32v152.446z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "point-up", + "finger", + "direction", + "hand" + ], + "defaultCode": 59907, + "grid": 16 + }, + { + "id": 260, + "paths": [ + "M416 960h160c52.934 0 96-43.066 96-96 0-17.104-4.494-33.176-12.368-47.098 26.664-17.076 44.368-46.958 44.368-80.902 0-24.564-9.276-47.004-24.504-64 15.228-16.996 24.504-39.436 24.504-64 0-11.214-1.934-21.986-5.484-32h229.484c52.934 0 96-43.066 96-96s-43.066-96-96-96h-394.676l93.836-176.018c8.4-14.536 12.84-31.126 12.84-47.982 0-52.934-43.066-96-96-96-26.368 0-50.954 10.472-69.226 29.49-0.238 0.248-0.47 0.496-0.7 0.75l-218.074 239.17v-45.41c0-17.672-14.326-32-32-32h-192c-17.674 0-32 14.328-32 32v640c0 17.674 14.326 32 32 32h192c17.674 0 32-14.326 32-32v-44.222l145.69 72.844c4.444 2.222 9.342 3.378 14.31 3.378zM160 896c-17.674 0-32-14.326-32-32s14.326-32 32-32 32 14.326 32 32-14.326 32-32 32zM423.556 896l-167.556-83.778v-383.824l265.168-290.818c6.066-6.18 14.162-9.58 22.832-9.58 17.644 0 32 14.356 32 32 0 5.46-1.308 10.612-3.894 15.324-0.324 0.53-0.632 1.070-0.926 1.622l-119.418 224c-5.288 9.92-4.986 21.884 0.8 31.52 5.784 9.638 16.198 15.534 27.438 15.534h448c17.644 0 32 14.356 32 32s-14.356 32-32 32h-320c-17.672 0-32 14.326-32 32s14.328 32 32 32c17.644 0 32 14.356 32 32s-14.356 32-32 32c-17.674 0-32 14.326-32 32s14.326 32 32 32c17.644 0 32 14.356 32 32s-14.356 32-32 32h-32c-17.674 0-32 14.326-32 32s14.326 32 32 32c17.644 0 32 14.356 32 32s-14.356 32-32 32h-152.444z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "point-right", + "finger", + "direction", + "hand" + ], + "defaultCode": 59908, + "grid": 16 + }, + { + "id": 261, + "paths": [ + "M960 416v160c0 52.934-43.066 96-96 96-17.104 0-33.176-4.494-47.098-12.368-17.076 26.662-46.96 44.368-80.902 44.368-24.564 0-47.004-9.276-64-24.504-16.996 15.228-39.436 24.504-64 24.504-11.214 0-21.986-1.934-32-5.484v229.484c0 52.934-43.066 96-96 96-52.936 0-96-43.066-96-96v-394.676l-176.018 93.836c-14.538 8.398-31.126 12.84-47.982 12.84-52.936 0-96-43.066-96-96 0-26.368 10.472-50.952 29.488-69.226 0.248-0.238 0.496-0.47 0.75-0.7l239.17-218.074h-45.408c-17.674 0-32-14.326-32-32v-192c0-17.674 14.326-32 32-32h640c17.674 0 32 14.326 32 32v192c0 17.674-14.326 32-32 32h-44.222l72.842 145.69c2.224 4.442 3.38 9.342 3.38 14.31zM896 160c0-17.674-14.326-32-32-32s-32 14.326-32 32 14.326 32 32 32 32-14.326 32-32zM896 423.554l-83.778-167.554h-383.824l-290.82 265.168c-6.18 6.066-9.578 14.162-9.578 22.832 0 17.644 14.356 32 32 32 5.458 0 10.612-1.308 15.324-3.894 0.53-0.324 1.070-0.632 1.622-0.926l224-119.416c9.92-5.288 21.884-4.986 31.52 0.8 9.638 5.782 15.534 16.196 15.534 27.436v448c0 17.644 14.356 32 32 32s32-14.356 32-32v-320c0-17.672 14.326-32 32-32s32 14.328 32 32c0 17.644 14.356 32 32 32s32-14.356 32-32c0-17.674 14.326-32 32-32s32 14.326 32 32c0 17.644 14.356 32 32 32s32-14.356 32-32v-32c0-17.674 14.326-32 32-32s32 14.326 32 32c0 17.644 14.356 32 32 32s32-14.356 32-32v-152.446z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "point-down", + "finger", + "direction", + "hand" + ], + "defaultCode": 59909, + "grid": 16 + }, + { + "id": 262, + "paths": [ + "M608 960h-160c-52.934 0-96-43.066-96-96 0-17.104 4.494-33.176 12.368-47.098-26.662-17.076-44.368-46.958-44.368-80.902 0-24.564 9.276-47.004 24.504-64-15.228-16.996-24.504-39.436-24.504-64 0-11.214 1.934-21.986 5.484-32h-229.484c-52.934 0-96-43.066-96-96 0-52.936 43.066-96 96-96h394.676l-93.836-176.018c-8.398-14.536-12.84-31.126-12.84-47.982 0-52.936 43.066-96 96-96 26.368 0 50.952 10.472 69.224 29.488 0.238 0.248 0.472 0.496 0.7 0.75l218.076 239.17v-45.408c0-17.674 14.326-32 32-32h192c17.674 0 32 14.326 32 32v640c0 17.674-14.326 32-32 32h-192c-17.674 0-32-14.326-32-32v-44.222l-145.69 72.844c-4.442 2.222-9.34 3.378-14.31 3.378zM864 896c17.674 0 32-14.326 32-32s-14.326-32-32-32-32 14.326-32 32 14.326 32 32 32zM600.446 896l167.554-83.778v-383.824l-265.168-290.82c-6.066-6.18-14.162-9.578-22.832-9.578-17.644 0-32 14.356-32 32 0 5.458 1.308 10.612 3.894 15.324 0.324 0.53 0.632 1.070 0.926 1.622l119.416 224c5.29 9.92 4.988 21.884-0.798 31.52-5.784 9.638-16.198 15.534-27.438 15.534h-448c-17.644 0-32 14.356-32 32s14.356 32 32 32h320c17.672 0 32 14.326 32 32s-14.328 32-32 32c-17.644 0-32 14.356-32 32s14.356 32 32 32c17.674 0 32 14.326 32 32s-14.326 32-32 32c-17.644 0-32 14.356-32 32s14.356 32 32 32h32c17.674 0 32 14.326 32 32s-14.326 32-32 32c-17.644 0-32 14.356-32 32s14.356 32 32 32h152.446z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "point-left", + "finger", + "direction", + "hand" + ], + "defaultCode": 59910, + "grid": 16 + }, + { + "id": 263, + "paths": [ + "M512 92.774l429.102 855.226h-858.206l429.104-855.226zM512 0c-22.070 0-44.14 14.882-60.884 44.648l-437.074 871.112c-33.486 59.532-5 108.24 63.304 108.24h869.308c68.3 0 96.792-48.708 63.3-108.24h0.002l-437.074-871.112c-16.742-29.766-38.812-44.648-60.882-44.648v0z", + "M576 832c0 35.346-28.654 64-64 64s-64-28.654-64-64c0-35.346 28.654-64 64-64s64 28.654 64 64z", + "M512 704c-35.346 0-64-28.654-64-64v-192c0-35.346 28.654-64 64-64s64 28.654 64 64v192c0 35.346-28.654 64-64 64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "warning", + "sign" + ], + "defaultCode": 59911, + "grid": 16 + }, + { + "id": 264, + "paths": [ + "M512 96c-111.118 0-215.584 43.272-294.156 121.844s-121.844 183.038-121.844 294.156c0 111.118 43.272 215.584 121.844 294.156s183.038 121.844 294.156 121.844c111.118 0 215.584-43.272 294.156-121.844s121.844-183.038 121.844-294.156c0-111.118-43.272-215.584-121.844-294.156s-183.038-121.844-294.156-121.844zM512 0v0c282.77 0 512 229.23 512 512s-229.23 512-512 512c-282.77 0-512-229.23-512-512s229.23-512 512-512zM448 704h128v128h-128zM448 192h128v384h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "notification", + "warning", + "notice", + "note", + "exclamation" + ], + "defaultCode": 59912, + "grid": 16 + }, + { + "id": 265, + "paths": [ + "M448 704h128v128h-128zM704 256c35.346 0 64 28.654 64 64v192l-192 128h-128v-64l192-128v-64h-320v-128h384zM512 96c-111.118 0-215.584 43.272-294.156 121.844s-121.844 183.038-121.844 294.156c0 111.118 43.272 215.584 121.844 294.156s183.038 121.844 294.156 121.844c111.118 0 215.584-43.272 294.156-121.844s121.844-183.038 121.844-294.156c0-111.118-43.272-215.584-121.844-294.156s-183.038-121.844-294.156-121.844zM512 0v0c282.77 0 512 229.23 512 512s-229.23 512-512 512c-282.77 0-512-229.23-512-512s229.23-512 512-512z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "question", + "help", + "support" + ], + "defaultCode": 59913, + "grid": 16 + }, + { + "id": 266, + "paths": [ + "M992 384h-352v-352c0-17.672-14.328-32-32-32h-192c-17.672 0-32 14.328-32 32v352h-352c-17.672 0-32 14.328-32 32v192c0 17.672 14.328 32 32 32h352v352c0 17.672 14.328 32 32 32h192c17.672 0 32-14.328 32-32v-352h352c17.672 0 32-14.328 32-32v-192c0-17.672-14.328-32-32-32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "plus", + "add", + "sum" + ], + "defaultCode": 59914, + "grid": 16 + }, + { + "id": 267, + "paths": [ + "M0 416v192c0 17.672 14.328 32 32 32h960c17.672 0 32-14.328 32-32v-192c0-17.672-14.328-32-32-32h-960c-17.672 0-32 14.328-32 32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "minus", + "subtract", + "minimize", + "line" + ], + "defaultCode": 59915, + "grid": 16 + }, + { + "id": 268, + "paths": [ + "M448 304c0-26.4 21.6-48 48-48h32c26.4 0 48 21.6 48 48v32c0 26.4-21.6 48-48 48h-32c-26.4 0-48-21.6-48-48v-32z", + "M640 768h-256v-64h64v-192h-64v-64h192v256h64z", + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "info", + "information" + ], + "defaultCode": 59916, + "grid": 16 + }, + { + "id": 269, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416z", + "M672 256l-160 160-160-160-96 96 160 160-160 160 96 96 160-160 160 160 96-96-160-160 160-160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cancel-circle", + "close", + "remove", + "delete" + ], + "defaultCode": 59917, + "grid": 16 + }, + { + "id": 270, + "paths": [ + "M874.040 149.96c-96.706-96.702-225.28-149.96-362.040-149.96s-265.334 53.258-362.040 149.96c-96.702 96.706-149.96 225.28-149.96 362.040s53.258 265.334 149.96 362.040c96.706 96.702 225.28 149.96 362.040 149.96s265.334-53.258 362.040-149.96c96.702-96.706 149.96-225.28 149.96-362.040s-53.258-265.334-149.96-362.040zM896 512c0 82.814-26.354 159.588-71.112 222.38l-535.266-535.268c62.792-44.758 139.564-71.112 222.378-71.112 211.738 0 384 172.262 384 384zM128 512c0-82.814 26.354-159.586 71.112-222.378l535.27 535.268c-62.794 44.756-139.568 71.11-222.382 71.11-211.738 0-384-172.262-384-384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "blocked", + "forbidden", + "denied", + "banned" + ], + "defaultCode": 59918, + "grid": 16 + }, + { + "id": 271, + "paths": [ + "M1014.662 822.66c-0.004-0.004-0.008-0.008-0.012-0.010l-310.644-310.65 310.644-310.65c0.004-0.004 0.008-0.006 0.012-0.010 3.344-3.346 5.762-7.254 7.312-11.416 4.246-11.376 1.824-24.682-7.324-33.83l-146.746-146.746c-9.148-9.146-22.45-11.566-33.828-7.32-4.16 1.55-8.070 3.968-11.418 7.31 0 0.004-0.004 0.006-0.008 0.010l-310.648 310.652-310.648-310.65c-0.004-0.004-0.006-0.006-0.010-0.010-3.346-3.342-7.254-5.76-11.414-7.31-11.38-4.248-24.682-1.826-33.83 7.32l-146.748 146.748c-9.148 9.148-11.568 22.452-7.322 33.828 1.552 4.16 3.97 8.072 7.312 11.416 0.004 0.002 0.006 0.006 0.010 0.010l310.65 310.648-310.65 310.652c-0.002 0.004-0.006 0.006-0.008 0.010-3.342 3.346-5.76 7.254-7.314 11.414-4.248 11.376-1.826 24.682 7.322 33.83l146.748 146.746c9.15 9.148 22.452 11.568 33.83 7.322 4.16-1.552 8.070-3.97 11.416-7.312 0.002-0.004 0.006-0.006 0.010-0.010l310.648-310.65 310.648 310.65c0.004 0.002 0.008 0.006 0.012 0.008 3.348 3.344 7.254 5.762 11.414 7.314 11.378 4.246 24.684 1.826 33.828-7.322l146.746-146.748c9.148-9.148 11.57-22.454 7.324-33.83-1.552-4.16-3.97-8.068-7.314-11.414z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "cross", + "cancel", + "close", + "quit", + "remove" + ], + "defaultCode": 59919, + "grid": 16 + }, + { + "id": 272, + "paths": [ + "M864 128l-480 480-224-224-160 160 384 384 640-640z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "checkmark", + "tick", + "correct", + "accept", + "ok" + ], + "defaultCode": 59920, + "grid": 16 + }, + { + "id": 273, + "paths": [ + "M397.434 917.696l-397.868-391.6 197.378-194.27 200.49 197.332 429.62-422.852 197.378 194.27-626.998 617.12zM107.912 526.096l289.524 284.962 518.656-510.482-89.036-87.632-429.62 422.852-200.49-197.334-89.034 87.634z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "checkmark", + "tick", + "correct", + "accept", + "ok" + ], + "defaultCode": 59921, + "grid": 16 + }, + { + "id": 274, + "paths": [ + "M128 256h128v192h64v-384c0-35.2-28.8-64-64-64h-128c-35.2 0-64 28.8-64 64v384h64v-192zM128 64h128v128h-128v-128zM960 64v-64h-192c-35.202 0-64 28.8-64 64v320c0 35.2 28.798 64 64 64h192v-64h-192v-320h192zM640 160v-96c0-35.2-28.8-64-64-64h-192v448h192c35.2 0 64-28.8 64-64v-96c0-35.2-8.8-64-44-64 35.2 0 44-28.8 44-64zM576 384h-128v-128h128v128zM576 192h-128v-128h128v128zM832 576l-416 448-224-288 82-70 142 148 352-302z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spell-check", + "spelling", + "correct" + ], + "defaultCode": 59922, + "grid": 16 + }, + { + "id": 275, + "paths": [ + "M384 512h-320v-128h320v-128l192 192-192 192zM1024 0v832l-384 192v-192h-384v-256h64v192h320v-576l256-128h-576v256h-64v-320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "enter", + "signin", + "login" + ], + "defaultCode": 59923, + "grid": 16 + }, + { + "id": 276, + "paths": [ + "M768 640v-128h-320v-128h320v-128l192 192zM704 576v256h-320v192l-384-192v-832h704v320h-64v-256h-512l256 128v576h256v-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "exit", + "signout", + "logout", + "quit", + "close" + ], + "defaultCode": 59924, + "grid": 16 + }, + { + "id": 277, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416zM384 288l384 224-384 224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "play", + "player" + ], + "defaultCode": 59925, + "grid": 16 + }, + { + "id": 278, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416zM320 320h128v384h-128zM576 320h128v384h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pause", + "player" + ], + "defaultCode": 59926, + "grid": 16 + }, + { + "id": 279, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416zM320 320h384v384h-384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stop", + "player" + ], + "defaultCode": 59927, + "grid": 16 + }, + { + "id": 280, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416z", + "M448 512l256-192v384z", + "M320 320h128v384h-128v-384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "previous", + "player" + ], + "defaultCode": 59928, + "grid": 16 + }, + { + "id": 281, + "paths": [ + "M512 0c282.77 0 512 229.23 512 512s-229.23 512-512 512-512-229.23-512-512 229.23-512 512-512zM512 928c229.75 0 416-186.25 416-416s-186.25-416-416-416-416 186.25-416 416 186.25 416 416 416z", + "M576 512l-256-192v384z", + "M704 320h-128v384h128v-384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "next", + "player" + ], + "defaultCode": 59929, + "grid": 16 + }, + { + "id": 282, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416zM704 672l-224-160 224-160zM448 672l-224-160 224-160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "backward", + "player" + ], + "defaultCode": 59930, + "grid": 16 + }, + { + "id": 283, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416zM320 352l224 160-224 160zM576 352l224 160-224 160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "forward", + "player" + ], + "defaultCode": 59931, + "grid": 16 + }, + { + "id": 284, + "paths": [ + "M192 128l640 384-640 384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "play", + "player" + ], + "defaultCode": 59932, + "grid": 16 + }, + { + "id": 285, + "paths": [ + "M128 128h320v768h-320zM576 128h320v768h-320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pause", + "player" + ], + "defaultCode": 59933, + "grid": 16 + }, + { + "id": 286, + "paths": [ + "M128 128h768v768h-768z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stop", + "player", + "square" + ], + "defaultCode": 59934, + "grid": 16 + }, + { + "id": 287, + "paths": [ + "M576 160v320l320-320v704l-320-320v320l-352-352z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "backward", + "player" + ], + "defaultCode": 59935, + "grid": 16 + }, + { + "id": 288, + "paths": [ + "M512 864v-320l-320 320v-704l320 320v-320l352 352z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "forward", + "player" + ], + "defaultCode": 59936, + "grid": 16 + }, + { + "id": 289, + "paths": [ + "M128 896v-768h128v352l320-320v320l320-320v704l-320-320v320l-320-320v352z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "first", + "player" + ], + "defaultCode": 59937, + "grid": 16 + }, + { + "id": 290, + "paths": [ + "M896 128v768h-128v-352l-320 320v-320l-320 320v-704l320 320v-320l320 320v-352z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "last", + "player" + ], + "defaultCode": 59938, + "grid": 16 + }, + { + "id": 291, + "paths": [ + "M256 896v-768h128v352l320-320v704l-320-320v352z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "previous", + "player" + ], + "defaultCode": 59939, + "grid": 16 + }, + { + "id": 292, + "paths": [ + "M768 128v768h-128v-352l-320 320v-704l320 320v-352z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "next", + "player" + ], + "defaultCode": 59940, + "grid": 16 + }, + { + "id": 293, + "paths": [ + "M0 768h1024v128h-1024zM512 128l512 512h-1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "eject", + "player" + ], + "defaultCode": 59941, + "grid": 16 + }, + { + "id": 294, + "paths": [ + "M890.040 922.040c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.746-18.744-49.136 0-67.882 87.638-87.642 135.904-204.16 135.904-328.1 0-123.938-48.266-240.458-135.904-328.098-18.744-18.746-18.744-49.138 0-67.882s49.138-18.744 67.882 0c105.77 105.772 164.022 246.4 164.022 395.98s-58.252 290.208-164.022 395.98c-9.372 9.372-21.656 14.058-33.94 14.058zM719.53 831.53c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.744-18.744-49.136 0-67.882 131.006-131.006 131.006-344.17 0-475.176-18.744-18.746-18.744-49.138 0-67.882 18.744-18.742 49.138-18.744 67.882 0 81.594 81.59 126.53 190.074 126.53 305.466 0 115.39-44.936 223.876-126.53 305.47-9.372 9.374-21.656 14.060-33.94 14.060v0zM549.020 741.020c-12.286 0-24.568-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z", + "M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z" + ], + "width": 1088, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-high", + "volume", + "audio", + "speaker", + "player" + ], + "defaultCode": 59942, + "grid": 16 + }, + { + "id": 295, + "paths": [ + "M719.53 831.53c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.744-18.744-49.136 0-67.882 131.006-131.006 131.006-344.17 0-475.176-18.744-18.746-18.744-49.138 0-67.882 18.744-18.742 49.138-18.744 67.882 0 81.594 81.59 126.53 190.074 126.53 305.466 0 115.39-44.936 223.876-126.53 305.47-9.372 9.374-21.656 14.060-33.94 14.060v0zM549.020 741.020c-12.286 0-24.566-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z", + "M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-medium", + "volume", + "audio", + "speaker", + "player" + ], + "defaultCode": 59943, + "grid": 16 + }, + { + "id": 296, + "paths": [ + "M549.020 741.020c-12.286 0-24.566-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z", + "M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-low", + "volume", + "audio", + "speaker", + "player" + ], + "defaultCode": 59944, + "grid": 16 + }, + { + "id": 297, + "paths": [ + "M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-mute", + "volume", + "audio", + "speaker", + "player" + ], + "defaultCode": 59945, + "grid": 16 + }, + { + "id": 298, + "paths": [ + "M960 619.148v84.852h-84.852l-107.148-107.148-107.148 107.148h-84.852v-84.852l107.148-107.148-107.148-107.148v-84.852h84.852l107.148 107.148 107.148-107.148h84.852v84.852l-107.148 107.148 107.148 107.148z", + "M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-mute", + "volume", + "audio", + "player" + ], + "defaultCode": 59946, + "grid": 16 + }, + { + "id": 299, + "paths": [ + "M1024 576h-192v192h-128v-192h-192v-128h192v-192h128v192h192v128z", + "M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-increase", + "volume", + "audio", + "speaker", + "player" + ], + "defaultCode": 59947, + "grid": 16 + }, + { + "id": 300, + "paths": [ + "M512 448h512v128h-512v-128z", + "M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "volume-decrease", + "volume", + "audio", + "speaker", + "player" + ], + "defaultCode": 59948, + "grid": 16 + }, + { + "id": 301, + "paths": [ + "M128 320h640v192l256-256-256-256v192h-768v384h128zM896 704h-640v-192l-256 256 256 256v-192h768v-384h-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "loop", + "repeat", + "player" + ], + "defaultCode": 59949, + "grid": 16 + }, + { + "id": 302, + "paths": [ + "M889.68 166.32c-93.608-102.216-228.154-166.32-377.68-166.32-282.77 0-512 229.23-512 512h96c0-229.75 186.25-416 416-416 123.020 0 233.542 53.418 309.696 138.306l-149.696 149.694h352v-352l-134.32 134.32z", + "M928 512c0 229.75-186.25 416-416 416-123.020 0-233.542-53.418-309.694-138.306l149.694-149.694h-352v352l134.32-134.32c93.608 102.216 228.154 166.32 377.68 166.32 282.77 0 512-229.23 512-512h-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "loop", + "repeat", + "player", + "reload", + "refresh", + "update", + "synchronize", + "arrows" + ], + "defaultCode": 59950, + "grid": 16 + }, + { + "id": 303, + "paths": [ + "M783.988 752.012c-64.104 0-124.372-24.96-169.7-70.288l-102.288-102.282-102.276 102.27c-45.332 45.336-105.6 70.3-169.706 70.3-64.118 0-124.39-24.964-169.722-70.3-45.332-45.334-70.296-105.604-70.296-169.712s24.964-124.38 70.296-169.714c45.334-45.332 105.608-70.296 169.714-70.296 64.108 0 124.38 24.964 169.712 70.296l102.278 102.276 102.276-102.276c45.332-45.332 105.604-70.298 169.712-70.298 64.112 0 124.384 24.966 169.71 70.298 45.338 45.334 70.302 105.606 70.302 169.714 0 64.112-24.964 124.382-70.3 169.71-45.326 45.336-105.598 70.302-169.712 70.302zM681.72 614.288c27.322 27.31 63.64 42.354 102.268 42.352 38.634 0 74.958-15.044 102.276-42.362 27.316-27.322 42.364-63.644 42.364-102.278s-15.046-74.956-42.364-102.274c-27.32-27.318-63.64-42.364-102.276-42.364-38.632 0-74.956 15.044-102.278 42.364l-102.268 102.274 102.278 102.288zM240.012 367.362c-38.634 0-74.956 15.044-102.274 42.364-27.32 27.318-42.364 63.64-42.364 102.274 0 38.632 15.044 74.954 42.364 102.276 27.32 27.316 63.642 42.364 102.274 42.364 38.634 0 74.956-15.044 102.272-42.362l102.276-102.278-102.276-102.274c-27.318-27.32-63.64-42.366-102.272-42.364v0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "infinite" + ], + "defaultCode": 59951, + "grid": 16 + }, + { + "id": 304, + "paths": [ + "M768 704h-101.49l-160-160 160-160h101.49v160l224-224-224-224v160h-128c-16.974 0-33.252 6.744-45.254 18.746l-178.746 178.744-178.746-178.746c-12-12-28.28-18.744-45.254-18.744h-192v128h165.49l160 160-160 160h-165.49v128h192c16.974 0 33.252-6.742 45.254-18.746l178.746-178.744 178.746 178.744c12.002 12.004 28.28 18.746 45.254 18.746h128v160l224-224-224-224v160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shuffle", + "random", + "player" + ], + "defaultCode": 59952, + "grid": 16 + }, + { + "id": 305, + "paths": [ + "M0 736l256-256 544 544 224-224-544-544 255.998-256h-735.998v736z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up-left", + "up-left", + "arrow-top-left" + ], + "defaultCode": 59953, + "grid": 16 + }, + { + "id": 306, + "paths": [ + "M512 32l-480 480h288v512h384v-512h288z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up", + "up", + "upload", + "top" + ], + "defaultCode": 59954, + "grid": 16 + }, + { + "id": 307, + "paths": [ + "M288 0l256 256-544 544 224 224 544-544 256 255.998v-735.998h-736z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up-right", + "up-right", + "arrow-top-right" + ], + "defaultCode": 59955, + "grid": 16 + }, + { + "id": 308, + "paths": [ + "M992 512l-480-480v288h-512v384h512v288z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-right", + "right", + "next" + ], + "defaultCode": 59956, + "grid": 16 + }, + { + "id": 309, + "paths": [ + "M1024 288l-256 256-544-544-224 224 544 544-255.998 256h735.998v-736z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down-right", + "down-right", + "arrow-bottom-right" + ], + "defaultCode": 59957, + "grid": 16 + }, + { + "id": 310, + "paths": [ + "M512 992l480-480h-288v-512h-384v512h-288z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down", + "down", + "download", + "bottom" + ], + "defaultCode": 59958, + "grid": 16 + }, + { + "id": 311, + "paths": [ + "M736 1024l-256-256 544-544-224-224-544 544-256-255.998v735.998h736z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down-left", + "down-left", + "arrow-bottom-left" + ], + "defaultCode": 59959, + "grid": 16 + }, + { + "id": 312, + "paths": [ + "M32 512l480 480v-288h512v-384h-512v-288z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-left", + "left", + "previous" + ], + "defaultCode": 59960, + "grid": 16 + }, + { + "id": 313, + "paths": [ + "M877.254 786.746l-530.744-530.746h229.49c35.346 0 64-28.654 64-64s-28.654-64-64-64h-384c-25.886 0-49.222 15.592-59.128 39.508-3.282 7.924-4.84 16.242-4.838 24.492h-0.034v384c0 35.346 28.654 64 64 64s64-28.654 64-64v-229.49l530.746 530.744c12.496 12.498 28.876 18.746 45.254 18.746s32.758-6.248 45.254-18.746c24.994-24.992 24.994-65.516 0-90.508z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up-left", + "up-left", + "arrow-top-left" + ], + "defaultCode": 59961, + "grid": 16 + }, + { + "id": 314, + "paths": [ + "M877.254 402.746l-320-320c-24.992-24.994-65.514-24.994-90.508 0l-320 320c-24.994 24.994-24.994 65.516 0 90.51 24.994 24.996 65.516 24.996 90.51 0l210.744-210.746v613.49c0 35.346 28.654 64 64 64s64-28.654 64-64v-613.49l210.746 210.746c12.496 12.496 28.876 18.744 45.254 18.744s32.758-6.248 45.254-18.746c24.994-24.994 24.994-65.514 0-90.508z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up", + "up", + "upload", + "top" + ], + "defaultCode": 59962, + "grid": 16 + }, + { + "id": 315, + "paths": [ + "M237.254 877.254l530.746-530.744v229.49c0 35.346 28.654 64 64 64s64-28.654 64-64v-384c0-25.884-15.594-49.222-39.508-59.126-7.924-3.284-16.242-4.84-24.492-4.838v-0.036h-384c-35.346 0-64 28.654-64 64 0 35.348 28.654 64 64 64h229.49l-530.744 530.746c-12.498 12.496-18.746 28.876-18.746 45.254s6.248 32.758 18.746 45.254c24.992 24.994 65.516 24.994 90.508 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-up-right", + "up-right", + "arrow-top-right" + ], + "defaultCode": 59963, + "grid": 16 + }, + { + "id": 316, + "paths": [ + "M621.254 877.254l320-320c24.994-24.992 24.994-65.516 0-90.51l-320-320c-24.994-24.992-65.516-24.992-90.51 0-24.994 24.994-24.994 65.516 0 90.51l210.746 210.746h-613.49c-35.346 0-64 28.654-64 64s28.654 64 64 64h613.49l-210.746 210.746c-12.496 12.496-18.744 28.876-18.744 45.254s6.248 32.758 18.744 45.254c24.994 24.994 65.516 24.994 90.51 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-right", + "right", + "next" + ], + "defaultCode": 59964, + "grid": 16 + }, + { + "id": 317, + "paths": [ + "M146.746 237.254l530.742 530.746h-229.488c-35.346 0-64 28.654-64 64s28.654 64 64 64h384c25.884 0 49.222-15.594 59.126-39.508 3.284-7.924 4.84-16.242 4.838-24.492h0.036v-384c0-35.346-28.654-64-64-64-35.348 0-64 28.654-64 64v229.49l-530.746-530.744c-12.496-12.498-28.874-18.746-45.254-18.746s-32.758 6.248-45.254 18.746c-24.994 24.992-24.994 65.516 0 90.508z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down-right", + "down-right", + "arrow-bottom-right" + ], + "defaultCode": 59965, + "grid": 16 + }, + { + "id": 318, + "paths": [ + "M877.254 621.254l-320 320c-24.992 24.994-65.514 24.994-90.508 0l-320-320c-24.994-24.994-24.994-65.516 0-90.51 24.994-24.996 65.516-24.996 90.51 0l210.744 210.746v-613.49c0-35.346 28.654-64 64-64s64 28.654 64 64v613.49l210.746-210.746c12.496-12.496 28.876-18.744 45.254-18.744s32.758 6.248 45.254 18.746c24.994 24.994 24.994 65.514 0 90.508z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down", + "down", + "download", + "bottom" + ], + "defaultCode": 59966, + "grid": 16 + }, + { + "id": 319, + "paths": [ + "M786.744 146.744l-530.744 530.744v-229.49c0-35.346-28.654-64-64-64s-64 28.654-64 64v384.002c0 25.886 15.592 49.222 39.508 59.128 7.924 3.282 16.242 4.84 24.492 4.836v0.036l384-0.002c35.344 0 64-28.654 64-63.998 0-35.348-28.656-64-64-64h-229.49l530.744-530.746c12.496-12.496 18.746-28.876 18.746-45.256 0-16.376-6.25-32.758-18.746-45.254-24.992-24.992-65.518-24.992-90.51 0v0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-down-left", + "down-left", + "arrow-bottom-left" + ], + "defaultCode": 59967, + "grid": 16 + }, + { + "id": 320, + "paths": [ + "M402.746 877.254l-320-320c-24.994-24.992-24.994-65.516 0-90.51l320-320c24.994-24.992 65.516-24.992 90.51 0 24.994 24.994 24.994 65.516 0 90.51l-210.746 210.746h613.49c35.346 0 64 28.654 64 64s-28.654 64-64 64h-613.49l210.746 210.746c12.496 12.496 18.744 28.876 18.744 45.254s-6.248 32.758-18.744 45.254c-24.994 24.994-65.516 24.994-90.51 0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "arrow-left", + "left", + "previous" + ], + "defaultCode": 59968, + "grid": 16 + }, + { + "id": 321, + "paths": [ + "M0 512c0 282.77 229.23 512 512 512s512-229.23 512-512-229.23-512-512-512-512 229.23-512 512zM928 512c0 229.75-186.25 416-416 416s-416-186.25-416-416 186.25-416 416-416 416 186.25 416 416z", + "M706.744 669.256l90.512-90.512-285.256-285.254-285.254 285.256 90.508 90.508 194.746-194.744z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "circle-up", + "up", + "circle-top", + "arrow" + ], + "defaultCode": 59969, + "grid": 16 + }, + { + "id": 322, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416z", + "M354.744 706.744l90.512 90.512 285.254-285.256-285.256-285.254-90.508 90.508 194.744 194.746z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "circle-right", + "right", + "circle-next", + "arrow" + ], + "defaultCode": 59970, + "grid": 16 + }, + { + "id": 323, + "paths": [ + "M1024 512c0-282.77-229.23-512-512-512s-512 229.23-512 512 229.23 512 512 512 512-229.23 512-512zM96 512c0-229.75 186.25-416 416-416s416 186.25 416 416-186.25 416-416 416-416-186.25-416-416z", + "M317.256 354.744l-90.512 90.512 285.256 285.254 285.254-285.256-90.508-90.508-194.746 194.744z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "circle-down", + "down", + "circle-bottom", + "arrow" + ], + "defaultCode": 59971, + "grid": 16 + }, + { + "id": 324, + "paths": [ + "M512 1024c282.77 0 512-229.23 512-512s-229.23-512-512-512-512 229.23-512 512 229.23 512 512 512zM512 96c229.75 0 416 186.25 416 416s-186.25 416-416 416-416-186.25-416-416 186.25-416 416-416z", + "M669.256 317.256l-90.512-90.512-285.254 285.256 285.256 285.254 90.508-90.508-194.744-194.746z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "circle-left", + "left", + "circle-previous", + "arrow" + ], + "defaultCode": 59972, + "grid": 16 + }, + { + "id": 325, + "paths": [ + "M960 0h64v512h-64v-512z", + "M0 512h64v512h-64v-512z", + "M320 704h704v128h-704v160l-224-224 224-224v160z", + "M704 320h-704v-128h704v-160l224 224-224 224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tab", + "arrows" + ], + "defaultCode": 59973, + "grid": 16 + }, + { + "id": 326, + "paths": [ + "M704 512v384h64v-384h160l-192-192-192 192z", + "M64 192h96v64h-96v-64z", + "M192 192h96v64h-96v-64z", + "M320 192h64v96h-64v-96z", + "M64 416h64v96h-64v-96z", + "M160 448h96v64h-96v-64z", + "M288 448h96v64h-96v-64z", + "M64 288h64v96h-64v-96z", + "M320 320h64v96h-64v-96z", + "M320 704v192h-192v-192h192zM384 640h-320v320h320v-320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "move-up", + "sort", + "arrange" + ], + "defaultCode": 59974, + "grid": 16 + }, + { + "id": 327, + "paths": [ + "M768 704v-384h-64v384h-160l192 192 192-192z", + "M320 256v192h-192v-192h192zM384 192h-320v320h320v-320z", + "M64 640h96v64h-96v-64z", + "M192 640h96v64h-96v-64z", + "M320 640h64v96h-64v-96z", + "M64 864h64v96h-64v-96z", + "M160 896h96v64h-96v-64z", + "M288 896h96v64h-96v-64z", + "M64 736h64v96h-64v-96z", + "M320 768h64v96h-64v-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "move-down", + "sort", + "arrange" + ], + "defaultCode": 59975, + "grid": 16 + }, + { + "id": 328, + "paths": [ + "M320 768v-768h-128v768h-160l224 224 224-224h-160z", + "M928 1024h-256c-11.8 0-22.644-6.496-28.214-16.9-5.566-10.404-4.958-23.030 1.59-32.85l222.832-334.25h-196.208c-17.672 0-32-14.328-32-32s14.328-32 32-32h256c11.8 0 22.644 6.496 28.214 16.9 5.566 10.404 4.958 23.030-1.59 32.85l-222.83 334.25h196.206c17.672 0 32 14.328 32 32s-14.328 32-32 32z", + "M1020.622 401.686l-192.002-384c-5.42-10.842-16.502-17.69-28.622-17.69-12.122 0-23.202 6.848-28.624 17.69l-191.996 384c-7.904 15.806-1.496 35.030 14.31 42.932 4.594 2.296 9.476 3.386 14.288 3.386 11.736 0 23.040-6.484 28.644-17.698l55.156-110.31h216.446l55.156 110.31c7.902 15.806 27.124 22.21 42.932 14.31 15.808-7.902 22.216-27.124 14.312-42.93zM723.778 255.996l76.22-152.446 76.224 152.446h-152.444z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sort-alpha-asc", + "arrange", + "alphabetic" + ], + "defaultCode": 59976, + "grid": 16 + }, + { + "id": 329, + "paths": [ + "M320 768v-768h-128v768h-160l224 224 224-224h-160z", + "M928 448h-256c-11.8 0-22.644-6.496-28.214-16.9-5.566-10.406-4.958-23.030 1.59-32.85l222.832-334.25h-196.208c-17.672 0-32-14.328-32-32s14.328-32 32-32h256c11.8 0 22.644 6.496 28.214 16.9 5.566 10.406 4.958 23.030-1.59 32.85l-222.83 334.25h196.206c17.672 0 32 14.328 32 32s-14.328 32-32 32z", + "M1020.622 977.69l-192.002-384c-5.42-10.842-16.502-17.69-28.622-17.69-12.122 0-23.202 6.848-28.624 17.69l-191.996 384c-7.904 15.806-1.496 35.030 14.31 42.932 4.594 2.296 9.476 3.386 14.288 3.386 11.736 0 23.040-6.484 28.644-17.698l55.158-110.31h216.446l55.156 110.31c7.902 15.806 27.124 22.21 42.932 14.31 15.806-7.902 22.214-27.124 14.31-42.93zM723.778 832l76.22-152.446 76.226 152.446h-152.446z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sort-alpha-desc", + "arrange", + "alphabetic" + ], + "defaultCode": 59977, + "grid": 16 + }, + { + "id": 330, + "paths": [ + "M320 768v-768h-128v768h-160l224 224 224-224h-160z", + "M864 448c-17.674 0-32-14.328-32-32v-352h-32c-17.674 0-32-14.328-32-32s14.326-32 32-32h64c17.674 0 32 14.328 32 32v384c0 17.672-14.326 32-32 32z", + "M928 576h-192c-17.674 0-32 14.326-32 32v192c0 17.674 14.326 32 32 32h160v128h-160c-17.674 0-32 14.326-32 32s14.326 32 32 32h192c17.674 0 32-14.326 32-32v-384c0-17.674-14.326-32-32-32zM768 640h128v128h-128v-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sort-numeric-asc", + "arrange" + ], + "defaultCode": 59978, + "grid": 16 + }, + { + "id": 331, + "paths": [ + "M320 768v-768h-128v768h-160l224 224 224-224h-160z", + "M864 1024c-17.674 0-32-14.328-32-32v-352h-32c-17.674 0-32-14.328-32-32s14.326-32 32-32h64c17.674 0 32 14.328 32 32v384c0 17.672-14.326 32-32 32z", + "M928 0h-192c-17.674 0-32 14.326-32 32v192c0 17.674 14.326 32 32 32h160v128h-160c-17.674 0-32 14.326-32 32s14.326 32 32 32h192c17.674 0 32-14.326 32-32v-384c0-17.674-14.326-32-32-32zM768 64h128v128h-128v-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sort-numberic-desc", + "arrange" + ], + "defaultCode": 59979, + "grid": 16 + }, + { + "id": 332, + "paths": [ + "M320 768v-768h-128v768h-160l224 224 224-224h-160z", + "M448 576h576v128h-576v-128z", + "M448 384h448v128h-448v-128z", + "M448 192h320v128h-320v-128z", + "M448 0h192v128h-192v-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sort-amount-asc", + "arrange" + ], + "defaultCode": 59980, + "grid": 16 + }, + { + "id": 333, + "paths": [ + "M320 768v-768h-128v768h-160l224 224 224-224h-160z", + "M448 0h576v128h-576v-128z", + "M448 192h448v128h-448v-128z", + "M448 384h320v128h-320v-128z", + "M448 576h192v128h-192v-128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sort-amount-desc", + "arrange" + ], + "defaultCode": 59981, + "grid": 16 + }, + { + "id": 334, + "paths": [ + "M736 896c-88.224 0-160-71.776-160-160v-96h-128v96c0 88.224-71.776 160-160 160s-160-71.776-160-160 71.776-160 160-160h96v-128h-96c-88.224 0-160-71.776-160-160s71.776-160 160-160 160 71.776 160 160v96h128v-96c0-88.224 71.776-160 160-160s160 71.776 160 160-71.776 160-160 160h-96v128h96c88.224 0 160 71.776 160 160s-71.774 160-160 160zM640 640v96c0 52.934 43.066 96 96 96s96-43.066 96-96-43.066-96-96-96h-96zM288 640c-52.934 0-96 43.066-96 96s43.066 96 96 96 96-43.066 96-96v-96h-96zM448 576h128v-128h-128v128zM640 384h96c52.934 0 96-43.066 96-96s-43.066-96-96-96-96 43.066-96 96v96zM288 192c-52.934 0-96 43.066-96 96s43.066 96 96 96h96v-96c0-52.934-43.064-96-96-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "command", + "cmd" + ], + "defaultCode": 59982, + "grid": 16 + }, + { + "id": 335, + "paths": [ + "M672 896h-320c-17.672 0-32-14.326-32-32v-352h-128c-12.942 0-24.612-7.796-29.564-19.754-4.954-11.958-2.214-25.722 6.936-34.874l320-320c12.498-12.496 32.758-12.496 45.254 0l320 320c9.152 9.152 11.89 22.916 6.938 34.874s-16.62 19.754-29.564 19.754h-128v352c0 17.674-14.326 32-32 32zM384 832h256v-352c0-17.672 14.326-32 32-32h82.744l-242.744-242.746-242.744 242.746h82.744c17.672 0 32 14.328 32 32v352z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "shift" + ], + "defaultCode": 59983, + "grid": 16 + }, + { + "id": 336, + "paths": [ + "M736.014 448c-8.908 0-17.77-3.698-24.096-10.928l-199.918-228.478-199.918 228.478c-11.636 13.3-31.856 14.65-45.154 3.010-13.3-11.638-14.648-31.854-3.010-45.154l224-256c6.076-6.944 14.854-10.928 24.082-10.928s18.006 3.984 24.082 10.928l224 256c11.638 13.3 10.292 33.516-3.010 45.154-6.070 5.312-13.582 7.918-21.058 7.918z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "ctrl", + "control" + ], + "defaultCode": 59984, + "grid": 16 + }, + { + "id": 337, + "paths": [ + "M928 832h-256c-12.646 0-24.106-7.448-29.242-19.004l-247.554-556.996h-299.204c-17.672 0-32-14.328-32-32s14.328-32 32-32h320c12.646 0 24.106 7.448 29.242 19.004l247.556 556.996h235.202c17.674 0 32 14.326 32 32s-14.326 32-32 32z", + "M928 256h-320c-17.674 0-32-14.328-32-32s14.326-32 32-32h320c17.674 0 32 14.328 32 32s-14.326 32-32 32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "opt", + "option", + "alt" + ], + "defaultCode": 59985, + "grid": 16 + }, + { + "id": 338, + "paths": [ + "M896 0h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-768c0-70.4-57.6-128-128-128zM448 794.51l-237.254-237.256 90.51-90.508 146.744 146.744 306.746-306.746 90.508 90.51-397.254 397.256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "checkbox-checked", + "checkbox", + "tick", + "checked", + "selected" + ], + "defaultCode": 59986, + "grid": 16 + }, + { + "id": 339, + "paths": [ + "M896 0h-768c-70.4 0-128 57.6-128 128v768c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-768c0-70.4-57.6-128-128-128zM896 896h-768v-768h768v768z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "checkbox-unchecked", + "checkbox", + "unchecked", + "square" + ], + "defaultCode": 59987, + "grid": 16 + }, + { + "id": 340, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 896c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.078 0 384 171.922 384 384s-171.922 384-384 384zM320 512c0-106.039 85.961-192 192-192s192 85.961 192 192c0 106.039-85.961 192-192 192s-192-85.961-192-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "radio-checked", + "radio-button" + ], + "defaultCode": 59988, + "grid": 16 + }, + { + "id": 341, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 640c-70.692 0-128-57.306-128-128 0-70.692 57.308-128 128-128 70.694 0 128 57.308 128 128 0 70.694-57.306 128-128 128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "radio-checked", + "radio-button" + ], + "defaultCode": 59989, + "grid": 16 + }, + { + "id": 342, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 896c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.078 0 384 171.922 384 384s-171.922 384-384 384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "radio-unchecked", + "radio-button", + "circle" + ], + "defaultCode": 59990, + "grid": 16 + }, + { + "id": 343, + "paths": [ + "M832 256l192-192-64-64-192 192h-448v-192h-128v192h-192v128h192v512h512v192h128v-192h192v-128h-192v-448zM320 320h320l-320 320v-320zM384 704l320-320v320h-320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "crop", + "resize", + "cut" + ], + "defaultCode": 59991, + "grid": 16 + }, + { + "id": 344, + "paths": [ + "M320 128h-128c-35.2 0-64 28.8-64 64v128c0 35.2 28.8 64 64 64h128c35.2 0 64-28.8 64-64v-128c0-35.2-28.8-64-64-64z", + "M704 384h128c35.2 0 64-28.8 64-64v-128c0-35.2-28.8-64-64-64h-128c-35.2 0-64 28.8-64 64v128c0 35.2 28.8 64 64 64zM704 192h128v128h-128v-128z", + "M320 640h-128c-35.2 0-64 28.8-64 64v128c0 35.2 28.8 64 64 64h128c35.2 0 64-28.8 64-64v-128c0-35.2-28.8-64-64-64zM320 832h-128v-128h128v128z", + "M832 640h-128c-35.2 0-64 28.8-64 64v128c0 35.2 28.8 64 64 64h128c35.2 0 64-28.8 64-64v-128c0-35.2-28.8-64-64-64z", + "M896 512h-64c-85.476 0-165.834-33.286-226.274-93.724-60.44-60.442-93.726-140.802-93.726-226.276v-64c0-70.4-57.6-128-128-128h-256c-70.4 0-128 57.6-128 128v256c0 70.4 57.6 128 128 128h64c85.476 0 165.834 33.286 226.274 93.724 60.44 60.442 93.726 140.802 93.726 226.276v64c0 70.4 57.6 128 128 128h256c70.4 0 128-57.6 128-128v-256c0-70.4-57.6-128-128-128zM960 896c0 16.954-6.696 32.986-18.856 45.144-12.158 12.16-28.19 18.856-45.144 18.856h-256c-16.954 0-32.986-6.696-45.144-18.856-12.16-12.158-18.856-28.19-18.856-45.144v-64c0-212.078-171.922-384-384-384h-64c-16.954 0-32.986-6.696-45.146-18.854-12.158-12.16-18.854-28.192-18.854-45.146v-256c0-16.954 6.696-32.986 18.854-45.146 12.16-12.158 28.192-18.854 45.146-18.854h256c16.954 0 32.986 6.696 45.146 18.854 12.158 12.16 18.854 28.192 18.854 45.146v64c0 212.078 171.922 384 384 384h64c16.954 0 32.986 6.696 45.144 18.856 12.16 12.158 18.856 28.19 18.856 45.144v256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "make-group" + ], + "defaultCode": 59992, + "grid": 16 + }, + { + "id": 345, + "paths": [ + "M384 464c0 26.4-21.6 48-48 48h-96c-26.4 0-48-21.6-48-48v-96c0-26.4 21.6-48 48-48h96c26.4 0 48 21.6 48 48v96z", + "M704 464c0 26.4-21.6 48-48 48h-96c-26.4 0-48-21.6-48-48v-96c0-26.4 21.6-48 48-48h96c26.4 0 48 21.6 48 48v96z", + "M384 784c0 26.4-21.6 48-48 48h-96c-26.4 0-48-21.6-48-48v-96c0-26.4 21.6-48 48-48h96c26.4 0 48 21.6 48 48v96z", + "M704 784c0 26.4-21.6 48-48 48h-96c-26.4 0-48-21.6-48-48v-96c0-26.4 21.6-48 48-48h96c26.4 0 48 21.6 48 48v96z", + "M912.082 160l111.918-111.916v-48.084h-48.082l-111.918 111.916-111.918-111.916h-48.082v48.084l111.918 111.916-111.918 111.916v48.084h48.082l111.918-111.916 111.918 111.916h48.082v-48.084z", + "M0 768h64v128h-64v-128z", + "M0 576h64v128h-64v-128z", + "M832 448h64v128h-64v-128z", + "M832 832h64v128h-64v-128z", + "M832 640h64v128h-64v-128z", + "M0 384h64v128h-64v-128z", + "M0 192h64v128h-64v-128z", + "M512 128h128v64h-128v-64z", + "M320 128h128v64h-128v-64z", + "M128 128h128v64h-128v-64z", + "M448 960h128v64h-128v-64z", + "M640 960h128v64h-128v-64z", + "M256 960h128v64h-128v-64z", + "M64 960h128v64h-128v-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "ungroup" + ], + "defaultCode": 59993, + "grid": 16 + }, + { + "id": 346, + "paths": [ + "M913.826 679.694c-66.684-104.204-181.078-150.064-255.51-102.434-6.428 4.116-12.334 8.804-17.744 13.982l-79.452-124.262 183.462-287.972c15.016-27.73 20.558-60.758 13.266-93.974-6.972-31.75-24.516-58.438-48.102-77.226l-12.278-7.808-217.468 340.114-217.47-340.114-12.276 7.806c-23.586 18.79-41.13 45.476-48.1 77.226-7.292 33.216-1.75 66.244 13.264 93.974l183.464 287.972-79.454 124.262c-5.41-5.178-11.316-9.868-17.744-13.982-74.432-47.63-188.826-1.77-255.51 102.434-66.68 104.2-60.398 227.286 14.032 274.914 74.43 47.632 188.824 1.77 255.508-102.432l164.286-257.87 164.288 257.872c66.684 104.202 181.078 150.064 255.508 102.432 74.428-47.63 80.71-170.716 14.030-274.914zM234.852 800.43c-30.018 46.904-68.534 69.726-94.572 75.446-0.004 0-0.004 0-0.004 0-8.49 1.868-20.294 3.010-28.324-2.128-8.898-5.694-14.804-20.748-15.8-40.276-1.616-31.644 9.642-68.836 30.888-102.034 30.014-46.906 68.53-69.726 94.562-75.444 8.496-1.866 20.308-3.010 28.336 2.126 8.898 5.694 14.802 20.75 15.798 40.272 1.618 31.65-9.64 68.84-30.884 102.038zM480 512c-17.672 0-32-14.328-32-32s14.328-32 32-32 32 14.328 32 32-14.328 32-32 32zM863.85 833.47c-0.996 19.528-6.902 34.582-15.8 40.276-8.030 5.138-19.834 3.996-28.324 2.128 0 0 0 0-0.004 0-26.040-5.718-64.554-28.542-94.572-75.446-21.244-33.198-32.502-70.388-30.884-102.038 0.996-19.522 6.9-34.578 15.798-40.272 8.028-5.136 19.84-3.992 28.336-2.126 26.034 5.716 64.548 28.538 94.562 75.444 21.246 33.198 32.502 70.39 30.888 102.034z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "scissors", + "cut" + ], + "defaultCode": 59994, + "grid": 16 + }, + { + "id": 347, + "paths": [ + "M512 0c-282.77 0-512 71.634-512 160v96l384 384v320c0 35.346 57.306 64 128 64 70.692 0 128-28.654 128-64v-320l384-384v-96c0-88.366-229.23-160-512-160zM94.384 138.824c23.944-13.658 57.582-26.62 97.278-37.488 87.944-24.076 201.708-37.336 320.338-37.336 118.628 0 232.394 13.26 320.338 37.336 39.696 10.868 73.334 23.83 97.28 37.488 15.792 9.006 24.324 16.624 28.296 21.176-3.972 4.552-12.506 12.168-28.296 21.176-23.946 13.658-57.584 26.62-97.28 37.488-87.942 24.076-201.708 37.336-320.338 37.336s-232.394-13.26-320.338-37.336c-39.696-10.868-73.334-23.83-97.278-37.488-15.792-9.008-24.324-16.624-28.298-21.176 3.974-4.552 12.506-12.168 28.298-21.176z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "filter", + "funnel" + ], + "defaultCode": 59995, + "grid": 16 + }, + { + "id": 348, + "paths": [ + "M799.596 16.208c-90.526 0-148.62-16.208-241.848-16.208-301.284 0-441.792 171.584-441.792 345.872 0 102.678 48.64 136.458 144.564 136.458-6.758-14.864-18.914-31.080-18.914-104.034 0-204.010 77.006-263.458 175.636-267.51 0 0-80.918 793.374-315.778 888.542v24.672h316.594l108.026-512h197.844l44.072-128h-214.908l51.944-246.19c59.446 12.156 117.542 24.316 167.532 24.316 62.148 0 118.894-18.914 149.968-162.126-37.826 12.16-78.362 16.208-122.94 16.208z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "font", + "typeface", + "typography", + "font-family", + "wysiwyg" + ], + "defaultCode": 59996, + "grid": 16 + }, + { + "id": 349, + "paths": [ + "M768 871.822c0-0.040 0.002-0.076 0.002-0.116l-0.344-436.562-127.492 6.19h-251.93v-21.494c0-81.542 5.8-162.976 17.24-194.716 7.896-21.948 22.598-41.744 43.698-58.836 20.618-16.702 41.178-25.17 61.11-25.17 16.772 0 30.702 2.878 41.402 8.554 15.026 8.562 29.716 22.964 43.67 42.818 36.95 52.504 51.99 66.454 60.094 72.376 13.804 10.094 30.512 15.212 49.658 15.212 18.668 0 34.962-6.97 48.436-20.714 13.372-13.636 20.15-30.682 20.15-50.666 0-21.452-8.916-44.204-26.502-67.622-17.184-22.888-43.708-41.742-78.834-56.032-34.322-13.964-72.94-21.044-114.778-21.044-60.716 0-116.012 14.596-164.356 43.384-48.424 28.834-85.558 68.952-110.37 119.24-22.994 46.604-21.334 134.706-22.732 214.712h-125.732v71.402h125.598v324.668c0 71.666-21.906 91.008-30.216 101.324-11.436 14.202-32.552 29.104-60.444 29.104h-38.654v56.166h385.326v-56.168h-6.708c-91.144 0-117.020-9.832-117.020-120.842 0-0.018 0-0.034 0-0.048l-0.038-334.206h140.204c74.404 0 91.496 3.444 95.392 4.924 4.706 1.79 10.798 4.832 13.084 9.144 0.868 1.684 5.194 25.008 5.194 82.972v250.67c0 58.454-7.124 77.896-11.45 84.402-9.248 14.194-20.41 22.066-54.66 22.904v56.248h293.61v-55.846c-91.608 0-101.608-9.82-101.608-96.332z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "ligature", + "typography", + "font" + ], + "defaultCode": 59997, + "grid": 16 + }, + { + "id": 350, + "paths": [ + "M855.328 917.454c-11.734 0-83.62-13.2-88.020-29.338-10.274-39.612-11.738-82.152-11.738-130.568v-540.974c0-80.686 16.138-127.632 16.138-127.632-1.468-7.334-8.804-23.472-17.604-23.472h-4.404c-4.4 0-55.746 32.276-102.692 32.276-38.14-0.002-61.89-33.746-105.902-33.746-185.106 0-271.942 150.31-271.942 363.032v11.072c0 4.402-2.934 8.804-7.336 8.804h-60.148c-7.336 0-22.006 41.078-22.006 60.148 0 5.87 1.466 8.8 4.4 8.8h77.754c4.402 0 7.336 5.872 7.336 10.27 0 130.566-1.466 259.298-1.466 259.298 0 20.54-1.466 66.016-10.27 102.692-4.4 16.138-71.884 29.338-89.488 29.338-7.334 0-7.334 35.212 0 42.546 60.148-2.934 99.758-7.334 159.908-7.334 55.75 0 98.292 4.4 156.974 7.334 2.934-8.802 2.934-42.546-4.4-42.546-11.736 0-83.624-13.2-88.022-29.338-10.27-39.612-10.27-82.152-11.738-130.568v-232.888c0-4.402 4.402-8.804 8.802-8.804h151.104c10.27-20.538 17.606-45.476 17.606-58.68 0-8.802 0-10.27-7.336-10.27h-162.84c-2.934 0-7.336-4.402-7.336-7.334v-52.82c0-130.568 53.482-245.538 142.97-245.538 63.372 0 118.666 41.060 118.666 197.922 0 0.006 0 0.012 0 0.018 0.208 4.036 0.314 7.294 0.314 9.452v436.816c0 20.54-1.47 66.016-10.27 102.692-4.404 16.138-71.884 29.338-89.492 29.338-7.336 0-7.336 35.212 0 42.546 60.15-2.934 99.762-7.334 159.912-7.334 55.746 0 98.288 4.4 156.972 7.334 2.928-8.8 2.928-42.544-4.406-42.544z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "ligature", + "typography", + "font" + ], + "defaultCode": 59998, + "grid": 16 + }, + { + "id": 351, + "paths": [ + "M896 768h128l-160 192-160-192h128v-512h-128l160-192 160 192h-128zM640 64v256l-64-128h-192v704h128v64h-384v-64h128v-704h-192l-64 128v-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "text-height", + "wysiwyg" + ], + "defaultCode": 59999, + "grid": 16 + }, + { + "id": 352, + "paths": [ + "M256 896v128l-192-160 192-160v128h512v-128l192 160-192 160v-128zM832 64v256l-64-128h-192v448h128v64h-384v-64h128v-448h-192l-64 128v-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "text-width", + "wysiwyg" + ], + "defaultCode": 60000, + "grid": 16 + }, + { + "id": 353, + "paths": [ + "M64 512h384v128h-128v384h-128v-384h-128zM960 256h-251.75v768h-136.5v-768h-251.75v-128h640z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "font-size", + "wysiwyg" + ], + "defaultCode": 60001, + "grid": 16 + }, + { + "id": 354, + "paths": [ + "M707.88 484.652c37.498-44.542 60.12-102.008 60.12-164.652 0-141.16-114.842-256-256-256h-320v896h384c141.158 0 256-114.842 256-256 0-92.956-49.798-174.496-124.12-219.348zM384 192h101.5c55.968 0 101.5 57.42 101.5 128s-45.532 128-101.5 128h-101.5v-256zM543 832h-159v-256h159c58.45 0 106 57.42 106 128s-47.55 128-106 128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "bold", + "wysiwyg" + ], + "defaultCode": 60002, + "grid": 16 + }, + { + "id": 355, + "paths": [ + "M704 64h128v416c0 159.058-143.268 288-320 288-176.73 0-320-128.942-320-288v-416h128v416c0 40.166 18.238 78.704 51.354 108.506 36.896 33.204 86.846 51.494 140.646 51.494s103.75-18.29 140.646-51.494c33.116-29.802 51.354-68.34 51.354-108.506v-416zM192 832h640v128h-640z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "underline", + "wysiwyg" + ], + "defaultCode": 60003, + "grid": 16 + }, + { + "id": 356, + "paths": [ + "M896 64v64h-128l-320 768h128v64h-448v-64h128l320-768h-128v-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "italic", + "wysiwyg" + ], + "defaultCode": 60004, + "grid": 16 + }, + { + "id": 357, + "paths": [ + "M1024 512v64h-234.506c27.504 38.51 42.506 82.692 42.506 128 0 70.878-36.66 139.026-100.58 186.964-59.358 44.518-137.284 69.036-219.42 69.036-82.138 0-160.062-24.518-219.42-69.036-63.92-47.938-100.58-116.086-100.58-186.964h128c0 69.382 87.926 128 192 128s192-58.618 192-128c0-69.382-87.926-128-192-128h-512v-64h299.518c-2.338-1.654-4.656-3.324-6.938-5.036-63.92-47.94-100.58-116.086-100.58-186.964s36.66-139.024 100.58-186.964c59.358-44.518 137.282-69.036 219.42-69.036 82.136 0 160.062 24.518 219.42 69.036 63.92 47.94 100.58 116.086 100.58 186.964h-128c0-69.382-87.926-128-192-128s-192 58.618-192 128c0 69.382 87.926 128 192 128 78.978 0 154.054 22.678 212.482 64h299.518z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "strikethrough", + "wysiwyg" + ], + "defaultCode": 60005, + "grid": 16 + }, + { + "id": 358, + "paths": [ + "M704 896h256l64-128v256h-384v-214.214c131.112-56.484 224-197.162 224-361.786 0-214.432-157.598-382.266-352-382.266-194.406 0-352 167.832-352 382.266 0 164.624 92.886 305.302 224 361.786v214.214h-384v-256l64 128h256v-32.59c-187.63-66.46-320-227.402-320-415.41 0-247.424 229.23-448 512-448s512 200.576 512 448c0 188.008-132.37 348.95-320 415.41v32.59z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "omega", + "wysiwyg", + "symbols" + ], + "defaultCode": 60006, + "grid": 16 + }, + { + "id": 359, + "paths": [ + "M941.606 734.708l44.394-94.708h38l-64 384h-960v-74.242l331.546-391.212-331.546-331.546v-227h980l44 256h-34.376l-18.72-38.88c-35.318-73.364-61.904-89.12-138.904-89.12h-662l353.056 353.056-297.42 350.944h542.364c116.008 0 146.648-41.578 173.606-97.292z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sigma", + "wysiwyg", + "symbols" + ], + "defaultCode": 60007, + "grid": 16 + }, + { + "id": 360, + "paths": [ + "M0 512h128v64h-128zM192 512h192v64h-192zM448 512h128v64h-128zM640 512h192v64h-192zM896 512h128v64h-128zM880 0l16 448h-768l16-448h32l16 384h640l16-384zM144 1024l-16-384h768l-16 384h-32l-16-320h-640l-16 320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "page-break", + "wysiwyg" + ], + "defaultCode": 60008, + "grid": 16 + }, + { + "id": 361, + "paths": [ + "M768 206v50h128v64h-192v-146l128-60v-50h-128v-64h192v146zM676 256h-136l-188 188-188-188h-136l256 256-256 256h136l188-188 188 188h136l-256-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "superscript", + "wysiwyg" + ], + "defaultCode": 60009, + "grid": 16 + }, + { + "id": 362, + "paths": [ + "M768 910v50h128v64h-192v-146l128-60v-50h-128v-64h192v146zM676 256h-136l-188 188-188-188h-136l256 256-256 256h136l188-188 188 188h136l-256-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "subscript", + "wysiwyg" + ], + "defaultCode": 60010, + "grid": 16 + }, + { + "id": 363, + "paths": [ + "M194.018 832l57.6-192h264.764l57.6 192h113.632l-192-640h-223.232l-192 640h113.636zM347.618 320h72.764l57.6 192h-187.964l57.6-192zM704 832l160-256 160 256h-320z", + "M864 128h-64c-17.644 0-32-14.356-32-32s14.356-32 32-32h128c17.674 0 32-14.328 32-32s-14.326-32-32-32h-128c-52.936 0-96 43.066-96 96 0 24.568 9.288 47.002 24.524 64 17.588 19.624 43.11 32 71.476 32h64c17.644 0 32 14.356 32 32s-14.356 32-32 32h-128c-17.674 0-32 14.328-32 32s14.326 32 32 32h128c52.936 0 96-43.066 96-96 0-24.568-9.288-47.002-24.524-64-17.588-19.624-43.108-32-71.476-32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "superscript", + "wysiwyg" + ], + "defaultCode": 60011, + "grid": 16 + }, + { + "id": 364, + "paths": [ + "M194.018 832l57.6-192h264.764l57.6 192h113.632l-192-640h-223.232l-192 640h113.636zM347.618 320h72.764l57.6 192h-187.964l57.6-192zM1024 192l-160 256-160-256h320z", + "M864 832h-64c-17.644 0-32-14.356-32-32s14.356-32 32-32h128c17.674 0 32-14.328 32-32s-14.326-32-32-32h-128c-52.936 0-96 43.066-96 96 0 24.568 9.29 47.002 24.524 64 17.588 19.624 43.112 32 71.476 32h64c17.644 0 32 14.356 32 32s-14.356 32-32 32h-128c-17.674 0-32 14.328-32 32s14.326 32 32 32h128c52.936 0 96-43.066 96-96 0-24.568-9.29-47.002-24.524-64-17.588-19.624-43.108-32-71.476-32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "subscript", + "wysiwyg" + ], + "defaultCode": 60012, + "grid": 16 + }, + { + "id": 365, + "paths": [ + "M322.018 832l57.6-192h264.764l57.6 192h113.632l-191.996-640h-223.236l-192 640h113.636zM475.618 320h72.764l57.6 192h-187.964l57.6-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "text-color", + "wysiwyg" + ], + "defaultCode": 60013, + "grid": 16 + }, + { + "id": 366, + "paths": [ + "M256 384v-384h768v384h-64v-320h-640v320zM1024 576v448h-768v-448h64v384h640v-384zM512 448h128v64h-128zM320 448h128v64h-128zM704 448h128v64h-128zM896 448h128v64h-128zM0 288l192 192-192 192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pagebreak", + "wysiwyg" + ], + "defaultCode": 60014, + "grid": 16 + }, + { + "id": 367, + "paths": [ + "M0 896h576v128h-576zM896 128h-302.56l-183.764 704h-132.288l183.762-704h-269.15v-128h704zM929.774 1024l-129.774-129.774-129.774 129.774-62.226-62.226 129.774-129.774-129.774-129.774 62.226-62.226 129.774 129.774 129.774-129.774 62.226 62.226-129.774 129.774 129.774 129.774z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "clear-formatting", + "wysiwyg", + "remove-style" + ], + "defaultCode": 60015, + "grid": 16 + }, + { + "id": 368, + "paths": [ + "M0 192v704h1024v-704h-1024zM384 640v-128h256v128h-256zM640 704v128h-256v-128h256zM640 320v128h-256v-128h256zM320 320v128h-256v-128h256zM64 512h256v128h-256v-128zM704 512h256v128h-256v-128zM704 448v-128h256v128h-256zM64 704h256v128h-256v-128zM704 832v-128h256v128h-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "table", + "wysiwyg" + ], + "defaultCode": 60016, + "grid": 16 + }, + { + "id": 369, + "paths": [ + "M0 64v896h1024v-896h-1024zM384 640v-192h256v192h-256zM640 704v192h-256v-192h256zM640 192v192h-256v-192h256zM320 192v192h-256v-192h256zM64 448h256v192h-256v-192zM704 448h256v192h-256v-192zM704 384v-192h256v192h-256zM64 704h256v192h-256v-192zM704 896v-192h256v192h-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "table", + "wysiwyg" + ], + "defaultCode": 60017, + "grid": 16 + }, + { + "id": 370, + "paths": [ + "M384 192h128v64h-128zM576 192h128v64h-128zM896 192v256h-192v-64h128v-128h-64v-64zM320 384h128v64h-128zM512 384h128v64h-128zM192 256v128h64v64h-128v-256h192v64zM384 576h128v64h-128zM576 576h128v64h-128zM896 576v256h-192v-64h128v-128h-64v-64zM320 768h128v64h-128zM512 768h128v64h-128zM192 640v128h64v64h-128v-256h192v64zM960 64h-896v896h896v-896zM1024 0v0 1024h-1024v-1024h1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "insert-template", + "wysiwyg" + ], + "defaultCode": 60018, + "grid": 16 + }, + { + "id": 371, + "paths": [ + "M384 0h512v128h-128v896h-128v-896h-128v896h-128v-512c-141.384 0-256-114.616-256-256s114.616-256 256-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pilcrow", + "wysiwyg" + ], + "defaultCode": 60019, + "grid": 16 + }, + { + "id": 372, + "paths": [ + "M512 0c-141.384 0-256 114.616-256 256s114.616 256 256 256v512h128v-896h128v896h128v-896h128v-128h-512zM0 704l256-256-256-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "ltr", + "wysiwyg", + "left-to-right", + "direction" + ], + "defaultCode": 60020, + "grid": 16 + }, + { + "id": 373, + "paths": [ + "M256 0c-141.384 0-256 114.616-256 256s114.616 256 256 256v512h128v-896h128v896h128v-896h128v-128h-512zM1024 192l-256 256 256 256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "rtl", + "wysiwyg", + "right-to-left", + "direction" + ], + "defaultCode": 60021, + "grid": 16 + }, + { + "id": 374, + "paths": [ + "M495.964 1024c-49.36 0-91.116-14.406-124.104-42.82-33.224-28.614-50.068-62.038-50.068-99.344 0-18.128 6.6-33.756 19.622-46.458 13.232-12.914 29.782-19.744 47.85-19.744 18.002 0 34.194 6.41 46.826 18.542 12.472 11.972 18.796 27.824 18.796 47.104 0 11.318-1.85 23.818-5.494 37.146-3.616 13.178-4.376 19.938-4.376 23.292 0 3.682 0.924 8.076 7.774 12.756 12.76 8.824 28.066 13.084 46.876 13.084 22.576 0 42.718-7.858 61.574-24.022 18.578-15.942 27.612-32.318 27.612-50.056 0-19.736-5.27-36.826-16.12-52.242-18.336-25.758-52.878-55.954-102.612-89.668-79.858-53.454-133.070-99.766-162.58-141.52-22.89-32.684-34.476-67.89-34.476-104.704 0-37.062 12.142-73.948 36.092-109.63 20.508-30.554 50.8-58.12 90.228-82.138-21.096-22.7-36.896-44.064-47.094-63.688-12.872-24.76-19.398-50.372-19.398-76.122 0-47.814 18.91-89.16 56.206-122.89 37.32-33.76 83.86-50.878 138.322-50.878 50.086 0 92.206 14.082 125.182 41.852 33.328 28.082 50.222 60.898 50.222 97.54 0 18.656-6.986 35.364-20.766 49.66l-0.276 0.282c-7.976 7.924-22.618 17.37-47.046 17.37-19.148 0-35.934-6.272-48.54-18.136-12.558-11.794-18.93-25.918-18.93-41.966 0-6.934 1.702-17.416 5.352-32.98 1.778-7.364 2.668-14.142 2.668-20.25 0-10.338-3.726-18.272-11.724-24.966-8.282-6.93-20.108-10.302-36.142-10.302-24.868 0-45.282 7.562-62.41 23.118-17.19 15.606-25.544 34.088-25.544 56.508 0 20.156 4.568 36.762 13.58 49.362 17.112 23.938 46.796 49.79 88.22 76.836 84.17 54.588 142.902 104.672 174.518 148.826 23.35 33.12 35.152 68.34 35.152 104.792 0 36.598-11.882 73.496-35.318 109.676-20.208 31.18-50.722 59.276-90.884 83.71 22.178 23.466 37.812 44.042 47.554 62.538 12.082 22.97 18.208 48.048 18.208 74.542 0 49.664-18.926 91.862-56.244 125.422-37.34 33.554-83.866 50.566-138.288 50.566zM446.416 356.346c-48.222 28.952-71.712 62.19-71.712 101.314 0 22.756 6.498 43.13 19.86 62.278 19.936 27.926 59.27 62.054 116.804 101.288 24.358 16.586 46.36 32.712 65.592 48.060 49.060-29.504 72.956-62.366 72.956-100.178 0-20.598-8.142-42.774-24.204-65.916-16.808-24.196-52.85-55.796-107.128-93.914-28.328-19.562-52.558-37.334-72.168-52.932z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "section", + "wysiwyg" + ], + "defaultCode": 60022, + "grid": 16 + }, + { + "id": 375, + "paths": [ + "M0 64h1024v128h-1024zM0 256h640v128h-640zM0 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "paragraph-left", + "wysiwyg", + "align-left", + "left" + ], + "defaultCode": 60023, + "grid": 16 + }, + { + "id": 376, + "paths": [ + "M0 64h1024v128h-1024zM192 256h640v128h-640zM192 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "paragraph-center", + "wysiwyg", + "align-center", + "center" + ], + "defaultCode": 60024, + "grid": 16 + }, + { + "id": 377, + "paths": [ + "M0 64h1024v128h-1024zM384 256h640v128h-640zM384 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "paragraph-right", + "wysiwyg", + "align-right", + "right" + ], + "defaultCode": 60025, + "grid": 16 + }, + { + "id": 378, + "paths": [ + "M0 64h1024v128h-1024zM0 256h1024v128h-1024zM0 448h1024v128h-1024zM0 640h1024v128h-1024zM0 832h1024v128h-1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "paragraph-justify", + "wysiwyg", + "justify" + ], + "defaultCode": 60026, + "grid": 16 + }, + { + "id": 379, + "paths": [ + "M0 64h1024v128h-1024zM384 256h640v128h-640zM384 448h640v128h-640zM384 640h640v128h-640zM0 832h1024v128h-1024zM0 704v-384l256 192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "indent-increase", + "wysiwyg" + ], + "defaultCode": 60027, + "grid": 16 + }, + { + "id": 380, + "paths": [ + "M0 64h1024v128h-1024zM384 256h640v128h-640zM384 448h640v128h-640zM384 640h640v128h-640zM0 832h1024v128h-1024zM256 320v384l-256-192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "indent-decrease", + "wysiwyg" + ], + "defaultCode": 60028, + "grid": 16 + }, + { + "id": 381, + "paths": [ + "M256 640c0 0 58.824-192 384-192v192l384-256-384-256v192c-256 0-384 159.672-384 320zM704 768h-576v-384h125.876c10.094-11.918 20.912-23.334 32.488-34.18 43.964-41.19 96.562-72.652 156.114-93.82h-442.478v640h832v-268.624l-128 85.334v55.29z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "share", + "out", + "external", + "outside" + ], + "defaultCode": 60029, + "grid": 16 + }, + { + "id": 382, + "paths": [ + "M192 64v768h768v-768h-768zM896 768h-640v-640h640v640zM128 896v-672l-64-64v800h800l-64-64h-672z", + "M352 256l160 160-192 192 96 96 192-192 160 160v-416z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "new-tab", + "out", + "external", + "outside", + "popout", + "link", + "blank" + ], + "defaultCode": 60030, + "grid": 16 + }, + { + "id": 383, + "paths": [ + "M576 736l96 96 320-320-320-320-96 96 224 224z", + "M448 288l-96-96-320 320 320 320 96-96-224-224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "embed", + "code", + "html", + "xml" + ], + "defaultCode": 60031, + "grid": 16 + }, + { + "id": 384, + "paths": [ + "M832 736l96 96 320-320-320-320-96 96 224 224z", + "M448 288l-96-96-320 320 320 320 96-96-224-224z", + "M701.298 150.519l69.468 18.944-191.987 704.026-69.468-18.944 191.987-704.026z" + ], + "width": 1280, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "embed", + "code", + "html", + "xml" + ], + "defaultCode": 60032, + "grid": 16 + }, + { + "id": 385, + "paths": [ + "M0 64v896h1024v-896h-1024zM960 896h-896v-768h896v768zM896 192h-768v640h768v-640zM448 512h-64v64h-64v64h-64v-64h64v-64h64v-64h-64v-64h-64v-64h64v64h64v64h64v64zM704 640h-192v-64h192v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "terminal", + "console", + "cmd", + "command-line" + ], + "defaultCode": 60033, + "grid": 16 + }, + { + "id": 386, + "paths": [ + "M864 704c-45.16 0-85.92 18.738-115.012 48.83l-431.004-215.502c1.314-8.252 2.016-16.706 2.016-25.328s-0.702-17.076-2.016-25.326l431.004-215.502c29.092 30.090 69.852 48.828 115.012 48.828 88.366 0 160-71.634 160-160s-71.634-160-160-160-160 71.634-160 160c0 8.622 0.704 17.076 2.016 25.326l-431.004 215.504c-29.092-30.090-69.852-48.83-115.012-48.83-88.366 0-160 71.636-160 160 0 88.368 71.634 160 160 160 45.16 0 85.92-18.738 115.012-48.828l431.004 215.502c-1.312 8.25-2.016 16.704-2.016 25.326 0 88.368 71.634 160 160 160s160-71.632 160-160c0-88.364-71.634-160-160-160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "share", + "social" + ], + "defaultCode": 60034, + "grid": 16 + }, + { + "id": 387, + "paths": [ + "M853.31 0h-682.62c-93.88 0-170.69 76.784-170.69 170.658v682.656c0 93.876 76.81 170.686 170.69 170.686h682.622c93.938 0 170.688-76.81 170.688-170.686v-682.656c0-93.874-76.75-170.658-170.69-170.658zM256 256h512c9.138 0 18.004 1.962 26.144 5.662l-282.144 329.168-282.144-329.17c8.14-3.696 17.006-5.66 26.144-5.66zM192 704v-384c0-1.34 0.056-2.672 0.14-4l187.664 218.94-185.598 185.6c-1.444-5.338-2.206-10.886-2.206-16.54zM768 768h-512c-5.654 0-11.202-0.762-16.54-2.206l182.118-182.118 90.422 105.496 90.424-105.494 182.116 182.118c-5.34 1.442-10.886 2.204-16.54 2.204zM832 704c0 5.654-0.762 11.2-2.206 16.54l-185.598-185.598 187.664-218.942c0.084 1.328 0.14 2.66 0.14 4v384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mail", + "contact", + "support", + "newsletter", + "letter", + "email", + "envelop", + "social" + ], + "defaultCode": 60035, + "grid": 16 + }, + { + "id": 388, + "paths": [ + "M853.342 0h-682.656c-93.874 0-170.686 76.81-170.686 170.69v682.622c0 93.938 76.812 170.688 170.686 170.688h682.656c93.876 0 170.658-76.75 170.658-170.69v-682.62c0-93.88-76.782-170.69-170.658-170.69zM853.342 128c7.988 0 15.546 2.334 22.020 6.342l-363.362 300.404-363.354-300.4c6.478-4.010 14.044-6.346 22.040-6.346h682.656zM170.686 896c-1.924 0-3.82-0.146-5.684-0.408l225.626-312.966-29.256-29.254-233.372 233.37v-611.138l384 464.396 384-464.394v611.136l-233.372-233.37-29.254 29.254 225.628 312.968c-1.858 0.26-3.746 0.406-5.662 0.406h-682.654z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mail", + "contact", + "support", + "newsletter", + "letter", + "email", + "envelop", + "social" + ], + "defaultCode": 60036, + "grid": 16 + }, + { + "id": 389, + "paths": [ + "M853.342 0h-682.656c-93.874 0-170.686 76.81-170.686 170.69v682.622c0 93.938 76.812 170.688 170.686 170.688h682.656c93.876 0 170.658-76.75 170.658-170.69v-682.62c0-93.88-76.782-170.69-170.658-170.69zM182.628 886.626l-77.256-77.254 256-256 29.256 29.254-208 304zM153.372 198.628l29.256-29.256 329.372 265.374 329.374-265.374 29.254 29.256-358.628 422.626-358.628-422.626zM841.374 886.626l-208-304 29.254-29.254 256 256-77.254 77.254z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mail", + "contact", + "support", + "newsletter", + "letter", + "email", + "envelop", + "social" + ], + "defaultCode": 60037, + "grid": 16 + }, + { + "id": 390, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM256 256h512c9.138 0 18.004 1.962 26.144 5.662l-282.144 329.168-282.144-329.17c8.14-3.696 17.006-5.66 26.144-5.66zM192 704v-384c0-1.34 0.056-2.672 0.14-4l187.664 218.942-185.598 185.598c-1.444-5.336-2.206-10.886-2.206-16.54zM768 768h-512c-5.654 0-11.202-0.762-16.54-2.208l182.118-182.118 90.422 105.498 90.424-105.494 182.116 182.12c-5.34 1.44-10.886 2.202-16.54 2.202zM832 704c0 5.654-0.762 11.2-2.206 16.54l-185.6-185.598 187.666-218.942c0.084 1.328 0.14 2.66 0.14 4v384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "mail", + "contact", + "support", + "newsletter", + "letter", + "email", + "envelop", + "social" + ], + "defaultCode": 60038, + "grid": 16 + }, + { + "id": 391, + "paths": [ + "M925.6 885.2c-112.2 82.8-274.6 126.8-414.6 126.8-196.2 0-372.8-72.4-506.4-193.2-10.4-9.4-1.2-22.4 11.4-15 144.2 84 322.6 134.4 506.8 134.4 124.2 0 260.8-25.8 386.6-79.2 18.8-8 34.8 12.6 16.2 26.2z", + "M972.2 832c-14.4-18.4-94.8-8.8-131-4.4-11 1.2-12.6-8.2-2.8-15.2 64.2-45 169.4-32 181.6-17 12.4 15.2-3.2 120.6-63.4 171-9.2 7.8-18 3.6-14-6.6 13.8-33.8 44-109.4 29.6-127.8z", + "M707.4 757.6l0.2 0.2c24.8-21.8 69.4-60.8 94.6-81.8 10-8 8.2-21.4 0.4-32.6-22.6-31.2-46.6-56.6-46.6-114.2v-192c0-81.4 5.6-156-54.2-212-47.2-45.2-125.6-61.2-185.6-61.2-117.2 0-248 43.8-275.4 188.6-3 15.4 8.4 23.6 18.4 25.8l119.4 13c11.2-0.6 19.2-11.6 21.4-22.8 10.2-49.8 52-74 99-74 25.4 0 54.2 9.2 69.2 32 17.2 25.4 15 60 15 89.4v16c-71.4 8-164.8 13.2-231.6 42.6-77.2 33.4-131.4 101.4-131.4 201.4 0 128 80.6 192 184.4 192 87.6 0 135.4-20.6 203-89.8 22.4 32.4 29.6 48.2 70.6 82.2 9.4 5 21 4.6 29.2-2.8zM583.2 457.2c0 48 1.2 88-23 130.6-19.6 34.8-50.6 56-85.2 56-47.2 0-74.8-36-74.8-89.2 0-105 94.2-124 183.2-124v26.6z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "amazon", + "brand" + ], + "defaultCode": 60039, + "grid": 16 + }, + { + "id": 392, + "paths": [ + "M522.2 438.8v175.6h290.4c-11.8 75.4-87.8 220.8-290.4 220.8-174.8 0-317.4-144.8-317.4-323.2s142.6-323.2 317.4-323.2c99.4 0 166 42.4 204 79l139-133.8c-89.2-83.6-204.8-134-343-134-283 0-512 229-512 512s229 512 512 512c295.4 0 491.6-207.8 491.6-500.2 0-33.6-3.6-59.2-8-84.8l-483.6-0.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "google", + "brand" + ], + "defaultCode": 60040, + "grid": 16 + }, + { + "id": 393, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM519.6 896c-212.2 0-384-171.8-384-384s171.8-384 384-384c103.6 0 190.4 37.8 257.2 100.4l-104.2 100.4c-28.6-27.4-78.4-59.2-153-59.2-131.2 0-238 108.6-238 242.4s107 242.4 238 242.4c152 0 209-109.2 217.8-165.6h-217.8v-131.6h362.6c3.2 19.2 6 38.4 6 63.6 0.2 219.4-146.8 375.2-368.6 375.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "google", + "brand" + ], + "defaultCode": 60041, + "grid": 16 + }, + { + "id": 394, + "paths": [ + "M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM519.6 896c-212.2 0-384-171.8-384-384s171.8-384 384-384c103.6 0 190.4 37.8 257.2 100.4l-104.2 100.4c-28.6-27.4-78.4-59.2-153-59.2-131.2 0-238 108.6-238 242.4s107 242.4 238 242.4c152 0 209-109.2 217.8-165.6h-217.8v-131.6h362.6c3.2 19.2 6 38.4 6 63.6 0.2 219.4-146.8 375.2-368.6 375.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "google", + "brand" + ], + "defaultCode": 60042, + "grid": 16 + }, + { + "id": 395, + "paths": [ + "M325.8 457.4v111.8h184.8c-7.4 48-55.8 140.6-184.8 140.6-111.2 0-202-92.2-202-205.8s90.8-205.8 202-205.8c63.4 0 105.6 27 129.8 50.2l88.4-85.2c-56.8-53-130.4-85.2-218.2-85.2-180.2 0.2-325.8 145.8-325.8 326s145.6 325.8 325.8 325.8c188 0 312.8-132.2 312.8-318.4 0-21.4-2.4-37.8-5.2-54h-307.6z", + "M1024 448h-96v-96h-96v96h-96v96h96v96h96v-96h96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "google-plus", + "brand", + "social" + ], + "defaultCode": 60043, + "grid": 16 + }, + { + "id": 396, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM384 768c-141.6 0-256-114.4-256-256s114.4-256 256-256c69.2 0 127 25.2 171.6 67l-69.6 66.8c-19-18.2-52.2-39.4-102-39.4-87.4 0-158.8 72.4-158.8 161.6s71.4 161.6 158.8 161.6c101.4 0 139.4-72.8 145.2-110.4h-145.2v-87.8h241.8c2.2 12.8 4 25.6 4 42.4 0 146.4-98 250.2-245.8 250.2zM896 512h-64v64h-64v-64h-64v-64h64v-64h64v64h64v64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "google-plus", + "brand", + "social" + ], + "defaultCode": 60044, + "grid": 16 + }, + { + "id": 397, + "paths": [ + "M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM384 768c-141.6 0-256-114.4-256-256s114.4-256 256-256c69.2 0 127 25.2 171.6 67l-69.6 66.8c-19-18.2-52.2-39.4-102-39.4-87.4 0-158.8 72.4-158.8 161.6s71.4 161.6 158.8 161.6c101.4 0 139.4-72.8 145.2-110.4h-145.2v-87.8h241.8c2.2 12.8 4 25.6 4 42.4 0 146.4-98 250.2-245.8 250.2zM832 512v64h-64v-64h-64v-64h64v-64h64v64h64v64h-64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "google-plus", + "brand", + "social" + ], + "defaultCode": 60045, + "grid": 16 + }, + { + "id": 398, + "paths": [ + "M511.8 0c-244.2 0-442.2 198-442.2 442.2 0 231.4 210.8 419 442.2 419v162.8c268.6-136.2 442.6-355.6 442.6-581.8 0-244.2-198.4-442.2-442.6-442.2zM448 512c0 53-28.6 96-64 96v-96h-128v-192h192v192zM768 512c0 53-28.6 96-64 96v-96h-128v-192h192v192z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hangouts", + "brand", + "social" + ], + "defaultCode": 60046, + "grid": 16 + }, + { + "id": 399, + "paths": [ + "M438 640l-184.6 320h580.6l184.6-320z", + "M992.4 576l-295.6-512h-369.6l295.6 512z", + "M290.2 128l-290.2 502.8 184.8 320 290.2-502.8z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "google-drive", + "brand" + ], + "defaultCode": 60047, + "grid": 16 + }, + { + "id": 400, + "paths": [ + "M608 192h160v-192h-160c-123.514 0-224 100.486-224 224v96h-128v192h128v512h192v-512h160l32-192h-192v-96c0-17.346 14.654-32 32-32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "facebook", + "brand", + "social" + ], + "defaultCode": 60048, + "grid": 16 + }, + { + "id": 401, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h416v-448h-128v-128h128v-64c0-105.8 86.2-192 192-192h128v128h-128c-35.2 0-64 28.8-64 64v64h192l-32 128h-160v448h288c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "facebook", + "brand", + "social" + ], + "defaultCode": 60049, + "grid": 16 + }, + { + "id": 402, + "paths": [ + "M512 92.2c136.8 0 153 0.6 206.8 3 50 2.2 77 10.6 95 17.6 23.8 9.2 41 20.4 58.8 38.2 18 18 29 35 38.4 58.8 7 18 15.4 45.2 17.6 95 2.4 54 3 70.2 3 206.8s-0.6 153-3 206.8c-2.2 50-10.6 77-17.6 95-9.2 23.8-20.4 41-38.2 58.8-18 18-35 29-58.8 38.4-18 7-45.2 15.4-95 17.6-54 2.4-70.2 3-206.8 3s-153-0.6-206.8-3c-50-2.2-77-10.6-95-17.6-23.8-9.2-41-20.4-58.8-38.2-18-18-29-35-38.4-58.8-7-18-15.4-45.2-17.6-95-2.4-54-3-70.2-3-206.8s0.6-153 3-206.8c2.2-50 10.6-77 17.6-95 9.2-23.8 20.4-41 38.2-58.8 18-18 35-29 58.8-38.4 18-7 45.2-15.4 95-17.6 53.8-2.4 70-3 206.8-3zM512 0c-139 0-156.4 0.6-211 3-54.4 2.4-91.8 11.2-124.2 23.8-33.8 13.2-62.4 30.6-90.8 59.2-28.6 28.4-46 57-59.2 90.6-12.6 32.6-21.4 69.8-23.8 124.2-2.4 54.8-3 72.2-3 211.2s0.6 156.4 3 211c2.4 54.4 11.2 91.8 23.8 124.2 13.2 33.8 30.6 62.4 59.2 90.8 28.4 28.4 57 46 90.6 59 32.6 12.6 69.8 21.4 124.2 23.8 54.6 2.4 72 3 211 3s156.4-0.6 211-3c54.4-2.4 91.8-11.2 124.2-23.8 33.6-13 62.2-30.6 90.6-59s46-57 59-90.6c12.6-32.6 21.4-69.8 23.8-124.2 2.4-54.6 3-72 3-211s-0.6-156.4-3-211c-2.4-54.4-11.2-91.8-23.8-124.2-12.6-34-30-62.6-58.6-91-28.4-28.4-57-46-90.6-59-32.6-12.6-69.8-21.4-124.2-23.8-54.8-2.6-72.2-3.2-211.2-3.2v0z", + "M512 249c-145.2 0-263 117.8-263 263s117.8 263 263 263 263-117.8 263-263c0-145.2-117.8-263-263-263zM512 682.6c-94.2 0-170.6-76.4-170.6-170.6s76.4-170.6 170.6-170.6c94.2 0 170.6 76.4 170.6 170.6s-76.4 170.6-170.6 170.6z", + "M846.8 238.6c0 33.91-27.49 61.4-61.4 61.4s-61.4-27.49-61.4-61.4c0-33.91 27.49-61.4 61.4-61.4s61.4 27.49 61.4 61.4z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "instagram", + "brand", + "social" + ], + "defaultCode": 60050, + "grid": 16 + }, + { + "id": 403, + "paths": [ + "M873 148.8c-95.8-96-223.2-148.8-359-148.8-279.6 0-507.2 227.6-507.2 507.4 0 89.4 23.4 176.8 67.8 253.6l-72 263 269-70.6c74.2 40.4 157.6 61.8 242.4 61.8h0.2c0 0 0 0 0 0 279.6 0 507.4-227.6 507.4-507.4 0-135.6-52.8-263-148.6-359zM514.2 929.6v0c-75.8 0-150-20.4-214.8-58.8l-15.4-9.2-159.6 41.8 42.6-155.6-10-16c-42.4-67-64.6-144.6-64.6-224.4 0-232.6 189.2-421.8 422-421.8 112.6 0 218.6 44 298.2 123.6 79.6 79.8 123.4 185.6 123.4 298.4-0.2 232.8-189.4 422-421.8 422zM745.4 613.6c-12.6-6.4-75-37-86.6-41.2s-20-6.4-28.6 6.4c-8.4 12.6-32.8 41.2-40.2 49.8-7.4 8.4-14.8 9.6-27.4 3.2s-53.6-19.8-102-63c-37.6-33.6-63.2-75.2-70.6-87.8s-0.8-19.6 5.6-25.8c5.8-5.6 12.6-14.8 19-22.2s8.4-12.6 12.6-21.2c4.2-8.4 2.2-15.8-1-22.2s-28.6-68.8-39-94.2c-10.2-24.8-20.8-21.4-28.6-21.8-7.4-0.4-15.8-0.4-24.2-0.4s-22.2 3.2-33.8 15.8c-11.6 12.6-44.4 43.4-44.4 105.8s45.4 122.6 51.8 131.2c6.4 8.4 89.4 136.6 216.6 191.4 30.2 13 53.8 20.8 72.2 26.8 30.4 9.6 58 8.2 79.8 5 24.4-3.6 75-30.6 85.6-60.2s10.6-55 7.4-60.2c-3-5.6-11.4-8.8-24.2-15.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "whatsapp", + "brand", + "social" + ], + "defaultCode": 60051, + "grid": 16 + }, + { + "id": 404, + "paths": [ + "M512 0c-281.6 0-512 230.4-512 512s230.4 512 512 512 512-230.4 512-512-227.8-512-512-512zM747.6 739.8c-10.2 15.4-28.2 20.4-43.6 10.2-120.4-74.2-271.4-89.6-450.6-48.6-18 5.2-33.2-7.6-38.4-23-5.2-18 7.6-33.2 23-38.4 194.6-43.6 363.6-25.6 496.6 56.4 18 7.6 20.6 28 13 43.4zM809 599c-12.8 18-35.8 25.6-53.8 12.8-138.2-84.4-348.2-110-509.4-58.8-20.4 5.2-43.6-5.2-48.6-25.6-5.2-20.4 5.2-43.6 25.6-48.6 186.8-56.4 417.2-28.2 576 69.2 15.2 7.6 23 33.2 10.2 51zM814 455.6c-163.8-97.2-437.8-107.6-594-58.8-25.6 7.6-51.2-7.6-58.8-30.8-7.6-25.6 7.6-51.2 30.8-58.8 181.8-53.8 481.2-43.6 670.8 69.2 23 12.8 30.8 43.6 18 66.6-13 17.8-43.6 25.4-66.8 12.6z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "spotify", + "brand", + "social" + ], + "defaultCode": 60052, + "grid": 16 + }, + { + "id": 405, + "paths": [ + "M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM763.6 351l-84 395.8c-5.8 28.2-22.8 34.8-46.4 21.8l-128-94.6-61.4 59.8c-7.2 7-12.8 12.8-25.6 12.8-16.6 0-13.8-6.2-19.4-22l-43.6-143.2-126.6-39.4c-27.4-8.4-27.6-27.2 6.2-40.6l493.2-190.4c22.4-10.2 44.2 5.4 35.6 40z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "telegram", + "brand", + "social" + ], + "defaultCode": 60053, + "grid": 16 + }, + { + "id": 406, + "paths": [ + "M1024 226.4c-37.6 16.8-78.2 28-120.6 33 43.4-26 76.6-67.2 92.4-116.2-40.6 24-85.6 41.6-133.4 51-38.4-40.8-93-66.2-153.4-66.2-116 0-210 94-210 210 0 16.4 1.8 32.4 5.4 47.8-174.6-8.8-329.4-92.4-433-219.6-18 31-28.4 67.2-28.4 105.6 0 72.8 37 137.2 93.4 174.8-34.4-1-66.8-10.6-95.2-26.2 0 0.8 0 1.8 0 2.6 0 101.8 72.4 186.8 168.6 206-17.6 4.8-36.2 7.4-55.4 7.4-13.6 0-26.6-1.4-39.6-3.8 26.8 83.4 104.4 144.2 196.2 146-72 56.4-162.4 90-261 90-17 0-33.6-1-50.2-3 93.2 59.8 203.6 94.4 322.2 94.4 386.4 0 597.8-320.2 597.8-597.8 0-9.2-0.2-18.2-0.6-27.2 41-29.4 76.6-66.4 104.8-108.6z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "twitter", + "brand", + "tweet", + "social" + ], + "defaultCode": 60054, + "grid": 16 + }, + { + "id": 407, + "paths": [ + "M960.8 509c-26.4 6-51.8 8.8-74.8 8.8-129.2 0-228.6-90.2-228.6-247.2 0-77 29.8-116.8 71.8-116.8 40 0 66.6 35.8 66.6 108.6 0 41.4-11 86.8-19.2 113.6 0 0 39.8 69.4 148.6 48.2 23.2-51.4 35.6-117.8 35.6-176 0-156.8-80-248.2-226.6-248.2-150.8 0-239 115.8-239 268.6 0 151.4 70.8 281.2 187.4 340.4-49 98.2-111.4 184.6-176.6 249.8-118-142.8-224.8-333.2-268.6-705h-174.2c80.6 619.2 320.4 816.4 384 854.2 35.8 21.6 66.8 20.6 99.6 2 51.6-29.2 206.2-184 292-365 36 0 79.2-4.2 122.2-14v-122z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "vine", + "brand", + "social" + ], + "defaultCode": 60055, + "grid": 16 + }, + { + "id": 408, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM829.4 716.8l-93.6 1.4c0 0-20.2 4-46.6-14.2-35-24-68-86.6-93.8-78.4-26 8.2-25.2 64.4-25.2 64.4s0.2 12-5.8 18.4c-6.4 7-19.2 8.4-19.2 8.4h-41.8c0 0-92.4 5.6-173.8-79.2-88.8-92.4-167.2-275.8-167.2-275.8s-4.6-12 0.4-17.8c5.6-6.6 20.6-7 20.6-7l100.2-0.6c0 0 9.4 1.6 16.2 6.6 5.6 4 8.6 11.8 8.6 11.8s16.2 41 37.6 78c41.8 72.2 61.4 88 75.6 80.4 20.6-11.2 14.4-102.2 14.4-102.2s0.4-33-10.4-47.6c-8.4-11.4-24.2-14.8-31-15.6-5.6-0.8 3.6-13.8 15.6-19.8 18-8.8 49.8-9.4 87.4-9 29.2 0.2 37.8 2.2 49.2 4.8 34.6 8.4 22.8 40.6 22.8 117.8 0 24.8-4.4 59.6 13.4 71 7.6 5 26.4 0.8 73.4-79 22.2-37.8 39-82.2 39-82.2s3.6-8 9.2-11.4c5.8-3.4 13.6-2.4 13.6-2.4l105.4-0.6c0 0 31.6-3.8 36.8 10.6 5.4 15-11.8 50-54.8 107.4-70.6 94.2-78.6 85.4-19.8 139.8 56 52 67.6 77.4 69.6 80.6 22.8 38.4-26 41.4-26 41.4z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "vk", + "brand", + "social" + ], + "defaultCode": 60056, + "grid": 16 + }, + { + "id": 409, + "paths": [ + "M425.2 10.6c-241.2 40.6-425.2 250.4-425.2 503.2 0 125.6 45.6 240.6 120.8 329.6 178.6-86.4 303.6-282 304.4-509.8v-323z", + "M598.8 10.6c241.2 40.6 425.2 250.4 425.2 503.2 0 125.6-45.6 240.6-120.8 329.6-178.6-86.4-303.6-282-304.4-509.8v-323z", + "M510.2 642.6c-31.8 131.6-126.8 244-245 318.8 72.8 39.8 156.2 62.6 245 62.6s172.2-22.8 245-62.6c-118.2-74.8-213.2-187.2-245-318.8z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "renren", + "brand", + "social" + ], + "defaultCode": 60057, + "grid": 16 + }, + { + "id": 410, + "paths": [ + "M430.2 898c-169.6 16.8-316-60-327-171.2-11-111.4 117.6-215 287-231.8 169.6-16.8 316 60 326.8 171.2 11.2 111.4-117.4 215.2-286.8 231.8zM769.2 528.6c-14.4-4.4-24.4-7.2-16.8-26.2 16.4-41.2 18-76.6 0.2-102-33.2-47.4-124.2-45-228.4-1.2 0 0-32.8 14.2-24.4-11.6 16-51.6 13.6-94.6-11.4-119.6-56.6-56.6-207 2.2-336 131.2-96.4 96.2-152.4 198.8-152.4 287.4 0 169.2 217.2 272.2 429.6 272.2 278.4 0 463.8-161.8 463.8-290.2 0-77.8-65.4-121.8-124.2-140z", + "M954.2 218.6c-67.2-74.6-166.4-103-258-83.6v0c-21.2 4.6-34.6 25.4-30 46.4 4.6 21.2 25.2 34.6 46.4 30 65.2-13.8 135.6 6.4 183.4 59.4s60.8 125.2 40.2 188.4v0c-6.6 20.6 4.6 42.6 25.2 49.4 20.6 6.6 42.6-4.6 49.4-25.2v-0.2c28.8-88.4 10.6-190-56.6-264.6z", + "M850.8 312c-32.8-36.4-81.2-50.2-125.6-40.6-18.2 3.8-29.8 22-26 40.2 4 18.2 22 29.8 40 25.8v0c21.8-4.6 45.4 2.2 61.4 19.8 16 17.8 20.4 42 13.4 63.2v0c-5.6 17.6 4 36.8 21.8 42.6 17.8 5.6 36.8-4 42.6-21.8 14-43.4 5.2-93-27.6-129.2z", + "M439.6 696.6c-6 10.2-19 15-29.2 10.8-10.2-4-13.2-15.6-7.4-25.4 6-9.8 18.6-14.6 28.6-10.8 10 3.6 13.6 15 8 25.4zM385.4 765.8c-16.4 26.2-51.6 37.6-78 25.6-26-11.8-33.8-42.2-17.4-67.8 16.2-25.4 50.2-36.8 76.4-25.8 26.6 11.4 35.2 41.6 19 68zM447 580.6c-80.6-21-171.8 19.2-206.8 90.2-35.8 72.4-1.2 153 80.2 179.4 84.4 27.2 184-14.6 218.6-92.6 34.2-76.6-8.4-155.2-92-177z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "sina-weibo", + "brand", + "social" + ], + "defaultCode": 60058, + "grid": 16 + }, + { + "id": 411, + "paths": [ + "M136.294 750.93c-75.196 0-136.292 61.334-136.292 136.076 0 75.154 61.1 135.802 136.292 135.802 75.466 0 136.494-60.648 136.494-135.802-0.002-74.742-61.024-136.076-136.494-136.076zM0.156 347.93v196.258c127.784 0 247.958 49.972 338.458 140.512 90.384 90.318 140.282 211.036 140.282 339.3h197.122c-0.002-372.82-303.282-676.070-675.862-676.070zM0.388 0v196.356c455.782 0 826.756 371.334 826.756 827.644h196.856c0-564.47-459.254-1024-1023.612-1024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "feed", + "rss", + "social" + ], + "defaultCode": 60059, + "grid": 16 + }, + { + "id": 412, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM279 831.2c-48 0-87-38.6-87-86.6 0-47.6 39-86.8 87-86.8 48.2 0 87 39.2 87 86.8 0 48-39 86.6-87 86.6zM497.4 832c0-81.8-31.8-158.8-89.4-216.4-57.8-57.8-134.4-89.6-216-89.6v-125.2c237.6 0 431.2 193.4 431.2 431.2h-125.8zM719.6 832c0-291-236.6-528-527.4-528v-125.2c360 0 653 293.2 653 653.2h-125.6z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "feed", + "rss", + "social" + ], + "defaultCode": 60060, + "grid": 16 + }, + { + "id": 413, + "paths": [ + "M1013.8 307.2c0 0-10-70.6-40.8-101.6-39-40.8-82.6-41-102.6-43.4-143.2-10.4-358.2-10.4-358.2-10.4h-0.4c0 0-215 0-358.2 10.4-20 2.4-63.6 2.6-102.6 43.4-30.8 31-40.6 101.6-40.6 101.6s-10.2 82.8-10.2 165.8v77.6c0 82.8 10.2 165.8 10.2 165.8s10 70.6 40.6 101.6c39 40.8 90.2 39.4 113 43.8 82 7.8 348.2 10.2 348.2 10.2s215.2-0.4 358.4-10.6c20-2.4 63.6-2.6 102.6-43.4 30.8-31 40.8-101.6 40.8-101.6s10.2-82.8 10.2-165.8v-77.6c-0.2-82.8-10.4-165.8-10.4-165.8zM406.2 644.8v-287.8l276.6 144.4-276.6 143.4z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "youtube", + "brand", + "social" + ], + "defaultCode": 60061, + "grid": 16 + }, + { + "id": 414, + "paths": [ + "M344.012 169.399c0.209-0.865 0.344-1.479 0.388-1.8l1.042-7.559-47.349-0.267c-42.779-0.242-55.87 0.007-57.047 1.084-0.565 0.516-15.333 56.633-41.655 158.273-12.556 48.484-23.124 87.206-23.487 86.051s-15.391-56.498-33.397-122.98c-18.006-66.482-33.104-121.243-33.55-121.692-0.623-0.623-57.98-0.9-104.417-0.502-6.735 0.056-10.477-13.11 60.021 211.133 9.759 31.041 24.371 74.997 32.469 97.679 9.333 26.141 15.989 46.323 20.534 63.173 8.038 32.067 8.319 52.163 6.565 75.625-2.026 27.101-2.321 218.438-0.342 221.638 1.512 2.449 91.223 3.589 99.712 1.268 1.358-0.372 2.265-1.691 2.87-8.928 2.119-6.219 2.286-30.969 2.286-133.744v-131.281l5.742-18.112c3.756-11.849 13.201-42.995 20.989-69.22 7.789-26.222 17.21-57.619 20.938-69.771 33.834-110.319 66.14-218.831 66.994-225.011l0.693-5.056z", + "M846.122 328.651l-0.021 6.838-1.065 0.014-0.595 188.993-0.577 183.227-14.666 14.929c-16.424 16.719-29.585 23.101-41.488 20.113-12.963-3.254-12.64 1.8-13.722-214.768l-0.998-199.347h-94.316v6.851h-1.086v216.289c0 231.737-0.007 231.599 11.752 254.875 9.366 18.536 23.010 27.559 46.391 30.671h0.002c30.79 4.1 64.001-9.849 94.77-39.809l13.373-13.022v22.445c0 19.396 0.554 22.601 4.070 23.58 5.756 1.605 77.173 1.707 84.89 0.126l6.396-1.314v-6.628l1.086-0.223v-495.098l-94.195 1.258z", + "M606.892 426.33c-8.935-38.341-25.68-64.115-53.233-81.939-43.281-27.999-92.718-30.957-138.586-8.291-33.425 16.515-54.951 43.914-66.071 84.083-1.326 4.786-2.298 8.812-3.033 14.815-2.83 14.184-3.163 35.351-3.889 133.951-1.121 151.928 0.616 170.003 19.643 204.51 18.664 33.848 57.403 58.661 99.572 63.782 12.696 1.54 38.43-0.858 53.23-4.961 33.632-9.326 65.864-35.906 80.118-66.078 6.158-13.033 9.875-22.096 12.115-38.651 4.175-22.617 4.47-59.175 4.47-152.375-0.002-118.875-0.379-131.862-4.337-148.847zM499.34 736.003c-7.907 6.028-21.734 8.649-32.983 6.249-8.656-1.847-20.338-15.419-23.934-27.801-4.479-15.436-4.823-229.985-0.954-272.059 6.379-21.054 24.19-32.050 43.635-26.813 15.157 4.082 22.915 13.575 27.336 33.457 3.282 14.754 3.67 33.129 2.972 141.26-0.46 71.701-0.716 106.742-3.058 125.553-2.382 11.87-6.319 15.047-13.015 20.154z", + "M2300.389 534.137h45.57l-0.726-41.281c-0.705-37.869-1.263-42.2-6.324-52.472-7.982-16.21-19.759-23.401-38.446-23.401-22.448 0-36.678 10.849-43.388 33.141-2.858 9.486-5.863 74.685-3.707 80.308 1.205 3.144 7.724 3.705 47.021 3.705z", + "M1995.795 440.237c-6.077-12.247-17.385-18.278-30.525-17.806-10.221 0.365-21.561 4.677-32.488 13.010l-8.14 6.177v296.598l8.14 6.177c18.429 14.052 38.674 17.031 52.619 7.703 5.519-3.691 9.117-8.779 11.919-16.861 3.647-10.524 3.965-24.003 3.489-148.772-0.495-130.043-0.781-137.702-5.014-146.226z", + "M2560.878 306.633c-9.080-108.842-16.303-144.165-38.751-189.544-29.729-60.101-72.692-91.788-133.876-98.747-47.309-5.379-225.315-12.97-390.044-16.631-285.188-6.338-754.057 5.858-813.939 21.173-27.673 7.077-48.426 19.11-70.022 40.604-37.844 37.662-60.391 91.679-69.452 166.396-20.692 170.606-21.134 376.727-1.188 553.515 8.577 76.041 26.243 125.443 59.41 166.159 20.694 25.406 56.352 46.998 88.26 53.442 22.385 4.523 134.42 10.798 297.605 16.668 24.306 0.874 88.667 2.379 143.030 3.344 113.301 2.012 321.627 0.821 440.719-2.519 80.127-2.249 226.201-8.172 253.5-10.282 7.677-0.593 25.469-1.728 39.537-2.523 47.277-2.67 77.353-12.568 105.596-34.76 36.553-28.718 64.857-81.795 76.815-144.037 11.314-58.894 18.887-163.773 20.422-282.851 1.284-99.491-0.426-153.175-7.621-239.409zM1425.273 267.192l-52.982 0.654-2.326 565.143-45.932 0.581c-35.525 0.488-46.307-0.044-47.167-2.326-0.616-1.626-1.356-129.020-1.672-283.153l-0.581-280.246-103.493-1.307v-88.304l305.829 1.235 1.307 87.069-52.982 0.654zM1750.216 591.117v243.035h-83.725v-25.583c0-19.247-0.735-25.583-2.979-25.583-1.64 0-9.226 6.344-16.861 14.098-16.557 16.817-36.171 30.367-52.91 36.63-34.662 12.968-67.589 5.4-81.618-18.75-12.838-22.11-13.082-27.052-13.082-256.335v-210.547h83.653l0.654 198.265c0.623 194.821 0.714 198.393 5.377 206.333 6.182 10.521 15.608 13.347 30.597 9.231 8.817-2.423 14.836-6.707 29.143-20.931l18.024-17.952v-374.946h83.725v243.035zM2076.757 799.41c-7.372 16.424-23.806 32.509-37.283 36.485-35.167 10.382-63.375 1.923-95.935-28.708-10.103-9.505-19.51-17.224-20.931-17.224-1.712 0-2.616 7.449-2.616 22.094v22.094h-83.725v-655.845h83.725v106.982c0 58.84 0.786 106.982 1.744 106.982s9.789-7.807 19.624-17.298c22.629-21.841 41.548-31.399 65.557-33.213 42.811-3.24 68.327 18.794 80.018 69.117 3.647 15.696 3.998 33.625 3.998 179.078-0.002 177.178-0.021 177.918-14.175 209.457zM2430.99 702.168c-0.744 18.226-2.954 39.137-4.942 46.514-11.642 43.167-42.635 73.731-87.432 86.269-60.315 16.878-126.704-10.777-153.205-63.812-14.875-29.769-15.408-35.706-15.408-181.185 0-118.617 0.419-133.171 4.214-149.354 10.747-45.788 37.392-75.422 82.49-91.865 13.068-4.765 26.708-7.207 40.337-7.486 48.672-0.998 96.984 25.18 117.229 67.808 13.659 28.76 15.35 41.060 16.717 122.099l1.235 72.678-178.497 1.235-0.654 48.84c-0.93 68.901 3.716 90.088 22.313 102.621 15.645 10.54 39.679 9.745 52.765-1.744 12.263-10.768 15.726-22.336 16.933-56.107l1.091-29.653h86.195l-1.381 33.143z" + ], + "width": 2569, + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "youtube", + "brand", + "social" + ], + "defaultCode": 60062, + "grid": 16 + }, + { + "id": 415, + "paths": [ + "M96 0l-96 160v736h256v128h128l128-128h160l288-288v-608h-864zM832 544l-160 160h-160l-128 128v-128h-192v-576h640v416z", + "M608 256h96v256h-96v-256z", + "M416 256h96v256h-96v-256z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "twitch", + "brand", + "social" + ], + "defaultCode": 60063, + "grid": 16 + }, + { + "id": 416, + "paths": [ + "M1023.6 274c-4.6 99.6-74.2 236.2-208.8 409.4-139.2 180.8-257 271.4-353.4 271.4-59.6 0-110.2-55-151.4-165.2-27.6-101-55-202-82.6-303-30.6-110.2-63.4-165.2-98.6-165.2-7.6 0-34.4 16.2-80.4 48.2l-48.2-62c50.6-44.4 100.4-88.8 149.4-133.2 67.4-58.2 118-88.8 151.8-92 79.6-7.6 128.8 46.8 147.2 163.4 19.8 125.8 33.6 204 41.4 234.6 23 104.4 48.2 156.6 75.8 156.6 21.4 0 53.6-33.8 96.6-101.6 42.8-67.6 65.8-119.2 69-154.6 6.2-58.4-16.8-87.8-69-87.8-24.6 0-49.8 5.6-75.8 16.8 50.4-164.8 146.4-244.8 288.4-240.2 105 2.8 154.6 71 148.6 204.4z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "vimeo", + "brand", + "social" + ], + "defaultCode": 60064, + "grid": 16 + }, + { + "id": 417, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM861.6 340c-3.2 72-53.6 170.6-151 295.8-100.6 130.8-185.8 196.2-255.4 196.2-43.2 0-79.6-39.8-109.4-119.4-20-73-39.8-146-59.8-219-22-79.6-45.8-119.4-71.2-119.4-5.6 0-25 11.6-58 34.8l-34.8-44.8c36.6-32 72.6-64.2 108-96.2 48.8-42 85.2-64.2 109.6-66.4 57.6-5.6 93 33.8 106.4 118 14.4 91 24.4 147.4 30 169.6 16.6 75.4 34.8 113 54.8 113 15.4 0 38.8-24.4 69.8-73.4s47.6-86.2 49.8-111.8c4.4-42.2-12.2-63.4-49.8-63.4-17.8 0-36 4-54.8 12.2 36.4-119 105.8-177 208.4-173.6 76 2.2 111.8 51.4 107.4 147.8z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "vimeo", + "brand", + "social" + ], + "defaultCode": 60065, + "grid": 16 + }, + { + "id": 418, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM822.4 768.8l-348.4 114c-79.6 26-87.6 21.8-123.6-89.6l-88-272.6c-21-64.6-85-238.6-95.8-272-20-62-20-65.4 97-103.4 91.6-30 95.4-29 128.6 74.4 26.8 83.2 44 150.4 71.6 235.4l75 232 239.6-78.4c47.2-15.6 63-14.8 76.4 43.4l9.6 44c11.2 51-14.6 64-42 72.8z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "lanyrd", + "brand" + ], + "defaultCode": 60066, + "grid": 16 + }, + { + "id": 419, + "paths": [ + "M0 544c0-123.712 100.288-224 224-224s224 100.288 224 224c0 123.712-100.288 224-224 224s-224-100.288-224-224zM576 544c0-123.712 100.288-224 224-224s224 100.288 224 224c0 123.712-100.288 224-224 224s-224-100.288-224-224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "flickr", + "brand", + "social" + ], + "defaultCode": 60067, + "grid": 16 + }, + { + "id": 420, + "paths": [ + "M800 416c-70.58 0-128 57.42-128 128s57.42 128 128 128c70.58 0 128-57.42 128-128s-57.42-128-128-128zM800 320v0c123.71 0 224 100.288 224 224 0 123.71-100.29 224-224 224s-224-100.29-224-224c0-123.712 100.29-224 224-224zM0 544c0-123.712 100.288-224 224-224s224 100.288 224 224c0 123.712-100.288 224-224 224s-224-100.288-224-224z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "flickr", + "brand", + "social" + ], + "defaultCode": 60068, + "grid": 16 + }, + { + "id": 421, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM288 672c-88.4 0-160-71.6-160-160s71.6-160 160-160 160 71.6 160 160-71.6 160-160 160zM736 672c-88.4 0-160-71.6-160-160s71.6-160 160-160c88.4 0 160 71.6 160 160s-71.6 160-160 160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "flickr", + "brand", + "social" + ], + "defaultCode": 60069, + "grid": 16 + }, + { + "id": 422, + "paths": [ + "M512 0c-282.77 0-512 230.796-512 515.5s229.23 515.5 512 515.5 512-230.796 512-515.5-229.23-515.5-512-515.5zM288 672c-88.366 0-160-71.634-160-160s71.634-160 160-160 160 71.634 160 160c0 88.366-71.634 160-160 160zM736 672c-88.368 0-160-71.634-160-160s71.632-160 160-160 160 71.634 160 160c0 88.366-71.632 160-160 160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "flickr", + "brand", + "social" + ], + "defaultCode": 60070, + "grid": 16 + }, + { + "id": 423, + "paths": [ + "M512 1024c-282.4 0-512-229.6-512-512s229.6-512 512-512c282.4 0 512 229.6 512 512s-229.6 512-512 512v0zM943.8 582c-15-4.8-135.4-40.6-272.4-18.6 57.2 157.2 80.4 285.2 85 311.8 98-66.4 168-171.4 187.4-293.2v0zM682.8 915.2c-6.6-38.4-31.8-172-93.2-331.6-1 0.4-2 0.6-2.8 1-246.8 86-335.4 257-343.2 273 74.2 57.8 167.4 92.4 268.4 92.4 60.6 0 118.4-12.4 170.8-34.8v0zM187 805c10-17 130-215.6 355.4-288.6 5.6-1.8 11.4-3.6 17.2-5.2-11-24.8-23-49.8-35.4-74.2-218.2 65.4-430.2 62.6-449.4 62.4-0.2 4.4-0.2 8.8-0.2 13.4 0 112.2 42.6 214.8 112.4 292.2v0zM84 423c19.6 0.2 199.8 1 404.4-53.2-72.4-128.8-150.6-237.2-162.2-253-122.4 57.8-214 170.6-242.2 306.2v0zM409.6 87.4c12 16.2 91.6 124.4 163.2 256 155.6-58.2 221.4-146.8 229.2-158-77.2-68.6-178.8-110.2-290-110.2-35.2 0.2-69.6 4.4-102.4 12.2v0zM850.6 236.2c-9.2 12.4-82.6 106.4-244.2 172.4 10.2 20.8 20 42 29 63.4 3.2 7.6 6.4 15 9.4 22.6 145.6-18.2 290.2 11 304.6 14-1-103.2-38-198-98.8-272.4v0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "dribbble", + "brand", + "social" + ], + "defaultCode": 60071, + "grid": 16 + }, + { + "id": 424, + "paths": [ + "M297 205.2c30.2 0 57.4 2.6 82.2 8 24.8 5.2 45.8 14 63.6 26 17.6 12 31.2 28 41.2 48 9.6 19.8 14.4 44.6 14.4 74 0 31.8-7.2 58.2-21.6 79.4-14.6 21.2-35.8 38.4-64.2 52 38.8 11.2 67.4 30.8 86.6 58.6 19.2 28 28.4 61.6 28.4 101.2 0 32-6.2 59.4-18.4 82.6-12.4 23.4-29.2 42.4-49.8 57-20.8 14.8-44.8 25.6-71.6 32.6-26.6 7-54 10.6-82.4 10.6h-305.4v-630h297zM279 459.6c24.6 0 45-5.8 61-17.6 16-11.6 23.6-30.8 23.6-57.2 0-14.6-2.6-26.8-7.8-36.2-5.4-9.4-12.4-16.8-21.4-22-8.8-5.4-18.8-9-30.6-11-11.4-2.2-23.4-3.2-35.6-3.2h-129.6v147.2h140.4zM286.6 727.8c13.6 0 26.6-1.2 38.8-4 12.4-2.8 23.4-7 32.6-13.4 9.2-6.2 17-14.4 22.6-25.2 5.6-10.6 8.2-24.2 8.2-40.8 0-32.4-9.2-55.6-27.4-69.6-18.2-13.8-42.4-20.6-72.4-20.6h-150.4v173.4h148z", + "M725.2 725.6c18.8 18.4 45.8 27.6 81 27.6 25.2 0 47.2-6.4 65.4-19.2s29.2-26.4 33.4-40.4h110.4c-17.8 55-44.6 94-81.4 117.6-36.2 23.6-80.6 35.6-132 35.6-36 0-68.2-5.8-97.2-17.2-29-11.6-53.2-27.8-73.6-49-19.8-21.2-35.4-46.4-46.4-76-10.8-29.4-16.4-62-16.4-97.2 0-34.2 5.6-66 16.8-95.4 11.4-29.6 27-55 47.8-76.4s45.2-38.4 74-50.8c28.6-12.4 60.2-18.6 95.2-18.6 38.6 0 72.4 7.4 101.4 22.6 28.8 15 52.6 35.2 71.2 60.4s31.8 54.2 40 86.6c8.2 32.4 11 66.2 8.8 101.6h-329.4c0 35.8 12 70 31 88.2zM869 486c-14.8-16.4-40.2-25.4-70.8-25.4-20 0-36.6 3.4-49.8 10.2-13 6.8-23.6 15.2-31.8 25.2-8 10-13.6 20.8-16.8 32.2-3.2 11-5.2 21.2-5.8 30h204c-3-32-14-55.6-29-72.2z", + "M668.4 256h255.4v62.2h-255.4v-62.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "behance", + "brand", + "social" + ], + "defaultCode": 60072, + "grid": 16 + }, + { + "id": 425, + "paths": [ + "M404.2 448.6c13-9.4 19.2-25 19.2-46.6 0-12-2-21.8-6.2-29.4-4.4-7.6-10-13.6-17.4-17.8-7.2-4.4-15.4-7.4-24.8-9-9.2-1.8-19-2.6-29-2.6h-105.4v119.6h114c20 0.2 36.6-4.6 49.6-14.2z", + "M422 556.6c-14.8-11.2-34.4-16.8-58.8-16.8h-122.6v141h120.2c11.2 0 21.6-1 31.6-3.2s19-5.6 26.6-10.8c7.6-5 13.8-11.8 18.4-20.4s6.8-19.8 6.8-33.2c0-26.4-7.4-45.2-22.2-56.6z", + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM671.2 269.4h207.4v50.6h-207.4v-50.6zM541.6 686.4c-10 19-23.6 34.4-40.4 46.4-17 12-36.4 20.8-58.2 26.6-21.6 5.8-44 8.6-66.8 8.6h-248.2v-511.8h241.2c24.4 0 46.6 2.2 66.8 6.4 20 4.2 37.2 11.4 51.6 21.2 14.2 9.8 25.4 22.8 33.4 39 7.8 16 11.8 36.2 11.8 60 0 25.8-5.8 47.2-17.6 64.4s-29 31.2-52.2 42.2c31.6 9 54.8 25 70.2 47.6 15.6 22.8 23.2 50.2 23.2 82.2 0.2 26.2-4.8 48.6-14.8 67.2zM959.4 607.2h-267.4c0 29.2 10 57 25.2 72 15.2 14.8 37.2 22.4 65.8 22.4 20.6 0 38.2-5.2 53.2-15.6 14.8-10.4 23.8-21.4 27.2-32.8h89.6c-14.4 44.6-36.2 76.4-66 95.6-29.4 19.2-65.4 28.8-107.2 28.8-29.2 0-55.4-4.8-79-14-23.6-9.4-43.2-22.6-59.8-39.8-16.2-17.2-28.6-37.8-37.6-61.8-8.8-23.8-13.4-50.4-13.4-79 0-27.8 4.6-53.6 13.6-77.6 9.2-24 22-44.8 38.8-62 16.8-17.4 36.8-31.2 60-41.4 23.2-10 48.8-15 77.2-15 31.4 0 58.8 6 82.4 18.4 23.4 12.2 42.6 28.6 57.8 49.2s25.8 44 32.6 70.4c6.6 26 8.8 53.4 7 82.2z", + "M776.6 463.8c-16.2 0-29.8 2.8-40.4 8.4s-19.2 12.4-25.8 20.4c-6.6 8.2-11 16.8-13.6 26.2-2.6 9-4.2 17.2-4.6 24.4h165.6c-2.4-26-11.4-45.2-23.4-58.6-12.4-13.6-32.8-20.8-57.8-20.8z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "behance", + "brand", + "social" + ], + "defaultCode": 60073, + "grid": 16 + }, + { + "id": 426, + "paths": [ + "M829 186.2v-186.2h-186.2l-18.6 18.8-88 167.4-27.6 18.6h-313.6v255.6h172.4l15.4 18.6-187.8 358.8v186.2h186.2l18.6-18.8 88-167.4 27.6-18.6h313.6v-255.6h-172.4l-15.4-18.8z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "deviantart", + "brand", + "social" + ], + "defaultCode": 60074, + "grid": 16 + }, + { + "id": 427, + "paths": [ + "M253 672.8c0.2 0.6 5.6 15.2 8.6 22.6 16.8 39.8 41 75.8 71.8 106.6s66.6 55 106.6 71.8c41.4 17.4 85.2 26.4 130.4 26.4s89.2-8.8 130.4-26.4c40-16.8 75.8-41 106.6-71.8s55-66.6 71.8-106.6c17.4-41.4 26.4-85.2 26.4-130.4s-8.8-89.2-26.4-130.4c-16.8-40-41-75.8-71.8-106.6s-66.6-55-106.6-71.8c-41.4-17.4-85.2-26.4-130.4-26.4-45.8 0-91.6 9.2-132.2 26.4-32.6 13.8-87.8 49.2-120 82.6l-0.2 0.2v-276h463.4c16.8-0.2 16.8-23.8 16.8-31.4 0-7.8 0-31.2-17-31.4h-501c-13.6 0-22 11.4-22 21.8v388.2c0 12.6 15.6 21.6 30.2 24.6 28.4 6 34.8-3 41.8-12.6l1-1.2c10.6-15.8 43.6-49 44-49.4 51.6-51.6 120.6-80 194.4-80 73.4 0 142.2 28.4 193.8 80 51.8 51.8 80.4 120.4 80.4 193.2 0 73-28.4 141.8-80 193.2-50.8 50.8-122 80-195 80-49.4 0-97.2-13.2-138.2-38.2l0.2-236c0-31.4 13.6-65.8 36.6-91.6 26.2-29.6 62.2-45.8 101.6-45.8 38 0 73.6 14.4 100.2 40.6 26.2 26 40.8 60.8 40.8 97.8 0 78.8-62 140.6-141.2 140.6-15.2 0-43-6.8-44.2-7-16-4.8-22.8 17.4-25 24.8-8.6 28.2 4.4 33.8 7 34.6 25.4 8 42.2 9.4 64.2 9.4 111.8 0 202.8-91 202.8-202.8 0-111-91-201.2-202.6-201.2-54.8 0-106.2 21-144.8 58.8-36.8 36.2-57.8 84.4-57.8 132.4v1.2c-0.2 6-0.2 147.6-0.4 194l-0.2-0.2c-21-23.2-41.8-58.8-55.6-95.2-5.4-14.2-17.6-11.8-34.2-6.6-8 2.2-30 9-25 25.2v0zM491.2 617.4c0 6.8 6.2 12.8 10 16.2l1.2 1.2c6.4 6.2 12.4 9.4 18 9.4 4.6 0 7.4-2.2 8.4-3.2 2.8-2.6 34.4-34.8 37.6-37.8l35.4 35.2c3.2 3.6 6.8 5.6 11 5.6 5.6 0 11.8-3.4 18.2-10 15.2-15.6 7.6-24 4-28l-35.8-35.8 37.4-37.6c8.2-8.8 1-18.2-6.2-25.4-10.4-10.4-20.6-13.2-27-7.2l-37.2 37.2-37.6-37.6c-2-2-4.6-3-7.2-3-5 0-11 3.4-17.6 10-11.6 11.6-14 19.6-8 26l37.6 37.4-37.4 37.4c-3.4 3.2-5 6.6-4.8 10zM573 109.8c-60 0-124 12.2-170.8 32.4-5 2-8 6-8.6 11.6-0.6 5.4 0.8 12.4 4.4 21.6 3 7.4 10.6 27.2 25.6 21.4 48-18.4 101.2-28.4 149.4-28.4 54.8 0 108 10.8 158 31.8 39.8 16.8 77.2 41.2 118 76.4 3 2.6 6.2 3.8 9.4 3.8 8 0 15.6-7.8 22.2-15.2 10.8-12.2 18.4-22.4 7.6-32.6-39-36.8-81.6-64.4-134.4-86.8-57.2-23.8-118.2-36-180.8-36zM896.4 851.2v0c-7.2-7.2-13.4-11.4-18.8-13s-10.4-0.4-14.2 3.4l-3.6 3.6c-37.2 37.2-80.6 66.4-128.8 86.8-50 21.2-103 31.8-157.6 31.8-54.8 0-107.8-10.8-157.6-31.8-48.2-20.4-91.6-49.6-128.8-86.8-38.8-38.8-68-82.2-86.8-128.8-18.4-45.6-24.4-79.8-26.4-91-0.2-1-0.4-1.8-0.4-2.4-2.6-13.2-14.8-14.2-32.2-11.4-7.2 1.2-29.4 4.6-27.4 20.4v0.4c5.8 37 16.2 73.2 30.8 107.6 23.4 55.4 57 105.2 99.8 148s92.6 76.2 148 99.8c57.4 24.2 118.4 36.6 181.2 36.6s123.8-12.4 181.2-36.6c55.4-23.4 105.2-57 148-99.8 0 0 2.4-2.4 3.8-3.8 4.4-5.4 8.6-14.4-10.2-33z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "500px", + "brand", + "social" + ], + "defaultCode": 60075, + "grid": 16 + }, + { + "id": 428, + "paths": [ + "M704 288c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96s-96-42.981-96-96zM958.392 129.608c-87.478-87.476-229.306-87.476-316.786 0-35.578 35.578-56.684 80.146-63.322 126.392v0l-204.694 310.228c-27.506 1.41-54.776 8.416-79.966 21.016l-157.892-123.424c-36.55-28.574-89.342-22.102-117.912 14.448-28.572 36.55-22.102 89.342 14.448 117.912l155.934 121.892c-16.96 66.782 0.672 140.538 52.93 192.794 78.906 78.904 206.832 78.904 285.736 0 48.466-48.466 67.15-115.428 56.076-178.166l249.054-222.986c46.248-6.638 90.816-27.744 126.394-63.322 87.478-87.476 87.478-229.306 0-316.784zM384 902.698c-74.39 0-134.698-60.304-134.698-134.698 0-0.712 0.042-1.414 0.054-2.124l66.912 52.304c15.36 12.006 33.582 17.824 51.674 17.824 24.962 0 49.672-11.080 66.238-32.272 28.572-36.55 22.102-89.342-14.448-117.912l-63.5-49.636c8.962-1.878 18.248-2.88 27.768-2.88 74.392 0 134.698 60.304 134.698 134.698s-60.306 134.696-134.698 134.696zM800 448c-88.366 0-160-71.634-160-160s71.634-160 160-160 160 71.634 160 160-71.634 160-160 160z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "steam", + "brand", + "social" + ], + "defaultCode": 60076, + "grid": 16 + }, + { + "id": 429, + "paths": [ + "M303.922 836.010c27.144 0 53.786-13.136 69.972-37.416 25.734-38.602 15.302-90.754-23.298-116.488l-66.074-44.048c11.308-3.080 23.194-4.756 35.478-4.756 74.392 0 134.696 60.304 134.696 134.698s-60.306 134.698-134.698 134.698c-72.404 0-131.444-57.132-134.548-128.774l71.954 47.968c14.322 9.548 30.506 14.118 46.518 14.118zM853.34 0c93.876 0 170.66 76.812 170.66 170.688v682.628c0 93.936-76.784 170.684-170.66 170.684h-682.652c-93.876 0-170.688-76.75-170.688-170.682v-203.028l121.334 80.888c-11.652 63.174 6.938 130.83 55.798 179.69 78.904 78.904 206.83 78.904 285.736 0 48.468-48.466 67.15-115.43 56.076-178.166l249.056-222.988c46.248-6.638 90.816-27.744 126.394-63.322 87.476-87.476 87.476-229.306 0-316.784-87.48-87.478-229.308-87.478-316.786 0-35.578 35.578-56.684 80.146-63.322 126.392v0l-204.694 310.23c-31.848 1.632-63.378 10.764-91.726 27.392l-217.866-145.244v-277.69c0-93.876 76.81-170.688 170.686-170.688h682.654zM896 288c0-88.366-71.634-160-160-160s-160 71.634-160 160 71.634 160 160 160 160-71.634 160-160zM640 288c0-53.020 42.98-96 96-96s96 42.98 96 96-42.98 96-96 96-96-42.98-96-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "steam", + "brand", + "social" + ], + "defaultCode": 60077, + "grid": 16 + }, + { + "id": 430, + "paths": [ + "M736 32l-224 192 288 192 224-192z", + "M512 224l-224-192-288 192 224 192z", + "M800 416l224 192-288 160-224-192z", + "M512 576l-288-160-224 192 288 160z", + "M728.156 845.57l-216.156-185.278-216.158 185.278-135.842-75.468v93.898l352 160 352-160v-93.898z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "dropbox", + "brand" + ], + "defaultCode": 60078, + "grid": 16 + }, + { + "id": 431, + "paths": [ + "M350.868 828.388c-60.274-15.060-93.856-62.97-93.962-134.064-0.032-22.726 1.612-33.62 7.286-48.236 13.908-35.834 50.728-62.872 99.176-72.822 24.11-4.95 31.536-10.266 31.536-22.572 0-3.862 2.872-15.36 6.378-25.552 15.932-46.306 45.43-84.91 76.948-100.702 32.99-16.526 49.642-20.254 89.548-20.040 56.674 0.304 84.952 12.598 124.496 54.128l21.75 22.842 19.484-6.742c94.3-32.636 188.306 22.916 195.888 115.756l2.072 25.398 18.57 6.65c53.032 19.004 77.96 58.904 73.442 117.556-2.958 38.358-20.89 68.98-49.3 84.184l-13.356 7.146-296.822 0.57c-228.094 0.44-300.6-0.368-313.134-3.5v0zM103.218 785.966c-36.176-9.086-74.506-42.854-92.48-81.47-10.196-21.906-10.738-25.128-10.738-63.88 0-36.864 0.87-42.778 8.988-61.080 17.11-38.582 49.894-66.46 91.030-77.408 8.684-2.312 16.842-6 18.128-8.196 1.29-2.198 2.722-14.164 3.182-26.592 2.866-77.196 50.79-145.214 117.708-167.056 36.154-11.8 83.572-12.898 122.896 3.726 12.47 5.274 11.068 6.404 37.438-30.14 15.594-21.612 45.108-44.49 70.9-58.18 27.838-14.776 56.792-21.584 91.412-21.494 96.768 0.252 180.166 64.22 211.004 161.848 9.854 31.192 9.362 39.926-2.26 40.184-5.072 0.112-19.604 3.064-32.292 6.558l-23.072 6.358-21.052-22.25c-59.362-62.734-156.238-76.294-238.592-33.396-32.9 17.138-59.34 41.746-79.31 73.81-14.236 22.858-32.39 65.504-32.39 76.094 0 7.51-5.754 11.264-30.332 19.782-76.094 26.376-120.508 87.282-120.476 165.218 0.010 28.368 6.922 63.074 16.52 82.956 3.618 7.494 5.634 14.622 4.484 15.836-2.946 3.106-97.608 2.060-110.696-1.228v0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "onedrive", + "brand", + "skydrive" + ], + "defaultCode": 60079, + "grid": 16 + }, + { + "id": 432, + "paths": [ + "M512.008 12.642c-282.738 0-512.008 229.218-512.008 511.998 0 226.214 146.704 418.132 350.136 485.836 25.586 4.738 34.992-11.11 34.992-24.632 0-12.204-0.48-52.542-0.696-95.324-142.448 30.976-172.504-60.41-172.504-60.41-23.282-59.176-56.848-74.916-56.848-74.916-46.452-31.778 3.51-31.124 3.51-31.124 51.4 3.61 78.476 52.766 78.476 52.766 45.672 78.27 119.776 55.64 149.004 42.558 4.588-33.086 17.852-55.68 32.506-68.464-113.73-12.942-233.276-56.85-233.276-253.032 0-55.898 20.004-101.574 52.76-137.428-5.316-12.9-22.854-64.972 4.952-135.5 0 0 43.006-13.752 140.84 52.49 40.836-11.348 84.636-17.036 128.154-17.234 43.502 0.198 87.336 5.886 128.256 17.234 97.734-66.244 140.656-52.49 140.656-52.49 27.872 70.528 10.35 122.6 5.036 135.5 32.82 35.856 52.694 81.532 52.694 137.428 0 196.654-119.778 239.95-233.79 252.624 18.364 15.89 34.724 47.046 34.724 94.812 0 68.508-0.596 123.644-0.596 140.508 0 13.628 9.222 29.594 35.172 24.566 203.322-67.776 349.842-259.626 349.842-485.768 0-282.78-229.234-511.998-511.992-511.998z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "github", + "brand", + "octacat", + "social" + ], + "defaultCode": 60080, + "grid": 16 + }, + { + "id": 433, + "paths": [ + "M0 0v1024h1024v-1024h-1024zM832 832h-128v-512h-192v512h-320v-640h640v640z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "npm", + "brand" + ], + "defaultCode": 60081, + "grid": 16 + }, + { + "id": 434, + "paths": [ + "M512 106.6c-186.8 0-330.8 156.4-412.4 309.6-46 86.2-78.2 180.6-93 277.2-1.6 11-3.2 22-4.4 33.2-0.6 6-1.2 12-1.6 18-0.6 7.6-0.2 10 3.8 16.4 12 19.4 26.2 37.4 42.2 53.6 32.8 33.6 72.6 59.4 114.8 79.4 96.2 45.4 204.8 61.8 310.4 65.4 109 3.6 221-5.4 325.2-39.4 89-29 174.8-79.6 224.2-161.4 5.4-8.8 1.6-21.8 0.6-32-1.2-12.2-2.8-24.2-4.8-36.2-3.6-23.6-8.4-46.8-14.2-70-11.6-47.2-27.4-93.6-46.6-138.2-69.6-161.6-198.4-334-381.6-369.6-20.6-4-41.6-6-62.6-6zM518.4 890.2c-114.2 0-238.6-10.2-341.4-65.2-40-21.4-80.8-52.4-100-95-5.6-12.4-3.6-17.2-1-31.8 1.8-9.4 2.6-18.6 6.8-27.4 5.8-12.2 11.8-24.2 18-36.2 21-40.6 43.6-80.8 69.8-118.6 13-18.6 26.8-37 42.8-53 11.2-11.2 24.8-23.2 40.6-27 48.4-11.6 85.4 44.4 114.8 72.6 14.2 13.6 33.2 29 54.4 26.4 14.6-1.8 27.6-13.2 38-22.6 35.4-31.8 63.8-71.2 93.2-108.2 14.6-18.2 29-36.6 44.8-54 10.6-11.8 22.2-25.2 36.4-32.8 25.4-13.8 57.8 14.6 75.4 29.2 30 25 56.6 54.2 82 83.8 24.2 28.2 47.6 56.8 68.2 87.8 31.8 48 59.4 99.2 84.6 151 5.4 11.2 7.2 18.8 9.2 31.2 1.2 6.8 3.8 14.6 2.8 21.6-1.4 9.8-8.2 20.4-13.2 28.4-12 19-28.2 35.4-46 49.2-74.6 57.8-175.6 77-267.4 85.6-37.6 3.6-75.2 5-112.8 5z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "basecamp", + "brand" + ], + "defaultCode": 60082, + "grid": 16 + }, + { + "id": 435, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM448 768c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64v-512c0-35.2 28.8-64 64-64h128c35.2 0 64 28.8 64 64v512zM832 576c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64v-320c0-35.2 28.8-64 64-64h128c35.2 0 64 28.8 64 64v320z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "trello", + "brand" + ], + "defaultCode": 60083, + "grid": 16 + }, + { + "id": 436, + "paths": [ + "M128 511.992c0 148.026 88.322 275.968 216.43 336.578l-183.178-488.784c-21.308 46.508-33.252 97.982-33.252 152.206zM771.228 493.128c0-46.234-17.054-78.236-31.654-103.142-19.458-30.82-37.72-56.894-37.72-87.716 0-34.374 26.766-66.376 64.486-66.376 1.704 0 3.32 0.204 4.976 0.302-68.316-60.97-159.34-98.196-259.308-98.196-134.16 0-252.186 67.046-320.844 168.568 9.010 0.282 17.506 0.454 24.712 0.454 40.154 0 102.34-4.752 102.34-4.752 20.69-1.182 23.132 28.434 2.458 30.822 0 0-20.81 2.368-43.952 3.55l139.834 405.106 84.044-245.456-59.822-159.65c-20.688-1.184-40.278-3.55-40.278-3.55-20.702-1.192-18.272-32.002 2.438-30.822 0 0 63.4 4.752 101.134 4.752 40.146 0 102.35-4.752 102.35-4.752 20.702-1.182 23.14 28.434 2.446 30.822 0 0-20.834 2.372-43.948 3.55l138.78 402.018 38.312-124.632c16.58-51.75 29.216-88.9 29.216-120.9zM518.742 544.704l-115.226 326.058c34.416 9.858 70.794 15.238 108.488 15.238 44.716 0 87.604-7.518 127.518-21.2-1.018-1.602-1.974-3.304-2.75-5.154l-118.030-314.942zM848.962 332.572c1.652 11.91 2.588 24.686 2.588 38.458 0 37.93-7.292 80.596-29.202 133.95l-117.286 330.272c114.162-64.828 190.938-185.288 190.938-323.258 0-65.030-17.060-126.16-47.038-179.422zM512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 960c-247.424 0-448-200.576-448-448s200.576-448 448-448 448 200.576 448 448-200.576 448-448 448z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wordpress", + "brand", + "social", + "cms" + ], + "defaultCode": 60084, + "grid": 16 + }, + { + "id": 437, + "paths": [ + "M266.004 276.678c32.832-32.844 86.002-32.844 118.804-0.032l7.826 7.868 101.104-101.156-7.874-7.88c-57.624-57.7-138.514-77.878-212.42-60.522-10.594-65.182-67.088-114.924-135.174-114.956-75.65 0-136.954 61.442-136.97 137.158 0 65.336 45.59 120 106.662 133.83-23.138 77.45-4.242 164.834 56.846 225.984l227.826 227.9 100.996-101.214-227.81-227.886c-32.682-32.722-32.742-86.126 0.184-119.094zM1022.712 137.158c0.016-75.762-61.318-137.158-136.984-137.158-69.234 0-126.478 51.444-135.682 118.238-77.074-22.664-163.784-3.496-224.64 57.408l-227.84 227.9 101.102 101.172 227.766-227.856c32.94-32.966 85.988-32.906 118.684-0.184 32.8 32.83 32.8 86.114-0.032 118.956l-7.794 7.836 101.010 101.248 7.858-7.928c60.458-60.566 79.678-146.756 57.612-223.638 67.15-8.834 118.94-66.364 118.94-135.994zM906.266 751.064c18.102-74.458-1.976-156.324-60.108-214.5l-227.49-227.992-101.102 101.122 227.52 228.012c32.94 32.996 32.864 86.096 0.184 118.848-32.802 32.814-86.004 32.814-118.836-0.030l-7.766-7.79-100.994 101.246 7.732 7.728c61.516 61.594 149.618 80.438 227.368 56.488 12.632 62.682 67.934 109.804 134.258 109.804 75.604 0 136.968-61.35 136.968-137.126 0-69.2-51.18-126.456-117.734-135.81zM612.344 528.684l-227.536 227.992c-32.71 32.768-86.034 32.828-118.944-0.124-32.818-32.904-32.832-86.098-0.044-118.97l7.808-7.774-101.086-101.124-7.734 7.712c-58.76 58.802-78.56 141.834-59.45 216.982-60.398 14.26-105.358 68.634-105.358 133.496-0.016 75.746 61.332 137.126 136.982 137.126 65.1-0.032 119.588-45.418 133.54-106.382 74.702 18.552 156.998-1.304 215.344-59.756l227.49-227.96-101.012-101.218z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "joomla", + "brand", + "cms" + ], + "defaultCode": 60085, + "grid": 16 + }, + { + "id": 438, + "paths": [ + "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM824.636 589.598c-36.798 142.716-165.358 242.402-312.63 242.402-147.282 0-275.85-99.686-312.654-242.42-6.232-24.158 8.352-48.886 32.512-55.124 3.71-0.958 7.528-1.446 11.338-1.446 20.624 0 38.628 13.972 43.788 33.976 26.512 102.748 119.042 174.51 225.014 174.51 105.978 0 198.502-71.76 225-174.51 5.152-20.006 23.15-33.982 43.766-33.982 3.822 0 7.65 0.49 11.376 1.456 11.692 3.016 21.526 10.418 27.668 20.842 6.142 10.416 7.854 22.596 4.822 34.296z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "ello", + "brand", + "social" + ], + "defaultCode": 60086, + "grid": 16 + }, + { + "id": 439, + "paths": [ + "M957.796 384h-57.406c-35.166 0-65.988-29.742-68.39-64v0c0.004-182.668-147.258-320-331.19-320h-167.824c-183.812 0-332.856 148-332.986 330.666v362.798c0 182.654 149.174 330.536 332.984 330.536h358.42c183.948 0 332.596-147.882 332.596-330.536v-234.382c0-36.502-29.44-75.082-66.204-75.082zM320 256h192c35.2 0 64 28.8 64 64s-28.8 64-64 64h-192c-35.2 0-64-28.8-64-64s28.8-64 64-64zM704 768h-384c-35.2 0-64-28.8-64-64s28.8-64 64-64h384c35.2 0 64 28.8 64 64s-28.8 64-64 64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "blogger", + "brand", + "social" + ], + "defaultCode": 60087, + "grid": 16 + }, + { + "id": 440, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM896 648c0 137-111.4 248-249.4 248h-268.8c-138 0-249.8-111-249.8-248v-272c0-137 111.8-248 249.8-248h125.8c138 0 248.4 103 248.4 240 1.8 25.6 25 48 51.2 48h43c27.6 0 49.6 29 49.6 56.4v175.6z", + "M704 640c0 35.2-28.8 64-64 64h-256c-35.2 0-64-28.8-64-64v0c0-35.2 28.8-64 64-64h256c35.2 0 64 28.8 64 64v0z", + "M576 384c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64v0c0-35.2 28.8-64 64-64h128c35.2 0 64 28.8 64 64v0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "blogger", + "brand", + "social" + ], + "defaultCode": 60088, + "grid": 16 + }, + { + "id": 441, + "paths": [ + "M576.032 448l-0.002 234.184c0 59.418-0.77 93.656 5.53 110.5 6.25 16.754 21.918 34.146 38.99 44.202 22.684 13.588 48.542 20.376 77.708 20.376 51.854 0 82.478-6.848 133.742-40.54v153.944c-43.7 20.552-81.866 32.594-117.324 40.922-35.5 8.242-73.86 12.406-115.064 12.406-46.828 0-74.456-5.886-110.41-17.656-35.958-11.868-66.66-28.806-92.020-50.54-25.45-21.922-43.022-45.208-52.848-69.832-9.826-24.636-14.716-60.414-14.716-107.244v-359.1h-137.426v-145.006c40.208-13.042 85.164-31.788 113.78-56.152 28.754-24.45 51.766-53.706 69.106-87.944 17.392-34.146 29.348-77.712 35.872-130.516h165.084l-0.002 255.996h255.968v192h-255.968z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tumblr", + "brand", + "social" + ], + "defaultCode": 60089, + "grid": 16 + }, + { + "id": 442, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM731.8 824.6c-30.2 14.2-57.6 24.2-82 30-24.4 5.6-51 8.6-79.4 8.6-32.4 0-51.4-4-76.2-12.2s-46-19.8-63.6-34.8c-17.6-15.2-29.6-31.2-36.4-48.2s-10.2-41.6-10.2-74v-247.8h-96v-100c27.8-9 60-22 79.6-38.8 19.8-16.8 35.8-37 47.6-60.6 12-23.6 20.2-53.6 24.8-90h100.4v163.2h163.6v126.2h-163.4v181.2c0 41-0.6 64.6 3.8 76.2s15.2 23.6 27 30.4c15.6 9.4 33.6 14 53.6 14 35.8 0 71.4-11.6 106.8-34.8v111.4z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tumblr", + "brand", + "social" + ], + "defaultCode": 60090, + "grid": 16 + }, + { + "id": 443, + "paths": [ + "M568.2 589v0c112.6-197.6 298.6-520 349.6-589-22.4 15-56.8 22.6-88.4 29.8l-47.8-29.8c-38.4 71.6-180 303-270.2 451.2-91.4-151.4-199.6-326.2-270.2-451.2-56 12-79.2 12.6-135 0v0 0c0 0 0 0 0 0v0c110.8 166.8 288.2 484.6 348.6 589v0l-8.2 435 64.8-29.8v-0.8l64.8 30.6-8-435z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "yahoo", + "brand", + "social" + ], + "defaultCode": 60091, + "grid": 16 + }, + { + "id": 444, + "paths": [ + "M513.2 69.6c-181 0-352-23.8-513.2-69.6 0 361.8 0 933.2 0 1024 161.4-45.8 332.4-69.6 513.2-69.6 178.8 0 349.4 23.2 510.8 69.6 0-348.4 0-649.8 0-1024-161.4 46.4-331.8 69.6-510.8 69.6zM796.8 157l-6.2 9.8c-5.8 9.2-11 17-18.2 28-9.6 14.4-27.6 43-49.2 79.8-6 10.2-13.4 22.4-21 35.6-14.6 24.6-31 52.4-44 74.4-5.4 9.4-10.8 19-16.4 28.6-14.4 25-29.2 50.8-43.4 75.6-14.6 25.8-29 51.2-43.4 76.4v25.4c0 35.2 0.8 73.6 2 107.8 0.6 15.6 1.2 43.4 2 72.8 0.8 35 1.6 71.2 2.6 89.6l0.2 5.6v0.6l-6-1.6c-2.4-0.6-4.6-1.2-7-1.8-7.2-1.6-15-2.8-22.6-3.6-4.6-0.4-9.4-0.6-14.2-0.6 0 0 0 0 0 0s0 0 0 0c-4.8 0-9.6 0.2-14.2 0.6-7.6 0.8-15.4 2-22.6 3.6-2.4 0.6-4.8 1.2-7 1.8l-6 1.6v-0.6l0.2-5.6c0.8-18.2 1.8-54.6 2.6-89.6 0.6-29.4 1.4-57.2 2-72.8 1.4-34.4 2-72.6 2-107.8v-25.4c-14.4-25.4-28.8-50.6-43.4-76.4-14.2-25-29-50.6-43.2-75.6-5.6-9.6-11-19.2-16.4-28.6-12.8-22.2-29.4-50-44-74.4-7.8-13-15.2-25.4-21-35.6-21.6-36.8-39.6-65.2-49.2-79.8-7.2-11-12.4-18.8-18.2-28l-6.2-9.8 11.2 3.2c14.2 4 28.8 6 44.4 6s30.6-2 44.6-6l3.4-1 1.8 3c27.6 49.8 101.8 171.8 146.2 244.8 15.2 25.2 27.4 45 33.4 55.2 0 0 0 0 0-0.2 0 0 0 0 0 0.2 6-10 18.2-30 33.4-55.2 44.4-72.8 118.6-194.8 146.2-244.8l1.8-3 3.4 1c14 4 29 6 44.6 6s30.2-2 44.4-6l10.6-3.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "yahoo" + ], + "defaultCode": 60092, + "grid": 16 + }, + { + "id": 445, + "paths": [ + "M567.656 736.916c-81.944 38.118-158.158 37.716-209.34 34.020-61.052-4.41-110.158-21.124-131.742-35.732-13.3-9.006-31.384-5.522-40.39 7.782-9.004 13.302-5.52 31.386 7.782 40.39 34.698 23.486 96.068 40.954 160.162 45.58 10.866 0.784 22.798 1.278 35.646 1.278 55.782 0 126.626-5.316 202.42-40.57 14.564-6.778 20.878-24.074 14.104-38.64-6.776-14.566-24.076-20.872-38.642-14.108zM890.948 693.816c2.786-252.688 28.762-730.206-454.97-691.612-477.6 38.442-350.964 542.968-358.082 711.95-6.308 89.386-35.978 198.648-77.896 309.846h129.1c13.266-47.122 23.024-93.72 27.232-138.15 7.782 5.428 16.108 10.674 24.994 15.7 14.458 8.518 26.884 19.844 40.040 31.834 30.744 28.018 65.59 59.774 133.712 63.752 4.572 0.262 9.174 0.394 13.676 0.394 68.896 0 116.014-30.154 153.878-54.382 18.14-11.612 33.818-21.64 48.564-26.452 41.91-13.12 78.532-34.296 105.904-61.252 4.276-4.208 8.242-8.538 11.962-12.948 15.246 55.878 36.118 118.758 59.288 181.504h275.65c-66.174-102.224-134.436-202.374-133.052-330.184zM124.11 556.352c0-0.016 0-0.030-0.002-0.046-4.746-82.462 34.71-151.832 88.126-154.936 53.412-3.106 100.56 61.228 105.304 143.692 0 0.014 0.004 0.030 0.004 0.044 0.256 4.446 0.368 8.846 0.37 13.206-16.924 4.256-32.192 10.436-45.872 17.63-0.052-0.612-0.092-1.216-0.152-1.83 0-0.008 0-0.018 0-0.026-4.57-46.81-29.572-82.16-55.852-78.958-26.28 3.204-43.88 43.75-39.312 90.558 0 0.010 0.004 0.018 0.004 0.026 1.992 20.408 7.868 38.636 16.042 52.444-2.034 1.604-7.784 5.812-14.406 10.656-4.97 3.634-11.020 8.058-18.314 13.43-19.882-26.094-33.506-63.58-35.94-105.89zM665.26 760.178c-1.9 43.586-58.908 84.592-111.582 101.044l-0.296 0.096c-21.9 7.102-41.428 19.6-62.104 32.83-34.732 22.224-70.646 45.208-122.522 45.208-3.404 0-6.894-0.104-10.326-0.296-47.516-2.778-69.742-23.032-97.88-48.676-14.842-13.526-30.19-27.514-49.976-39.124l-0.424-0.244c-42.706-24.104-69.212-54.082-70.908-80.194-0.842-12.98 4.938-24.218 17.182-33.4 26.636-19.972 44.478-33.022 56.284-41.658 13.11-9.588 17.068-12.48 20-15.264 2.096-1.986 4.364-4.188 6.804-6.562 24.446-23.774 65.36-63.562 128.15-63.562 38.404 0 80.898 14.8 126.17 43.902 21.324 13.878 39.882 20.286 63.38 28.4 16.156 5.578 34.468 11.902 58.992 22.404l0.396 0.164c22.88 9.404 49.896 26.564 48.66 54.932zM652.646 657.806c-4.4-2.214-8.974-4.32-13.744-6.286-22.106-9.456-39.832-15.874-54.534-20.998 8.116-15.894 13.16-35.72 13.624-57.242 0-0.010 0-0.022 0-0.030 1.126-52.374-25.288-94.896-58.996-94.976-33.71-0.078-61.95 42.314-63.076 94.686 0 0.010 0 0.018 0 0.028-0.038 1.714-0.042 3.416-0.020 5.11-20.762-9.552-41.18-16.49-61.166-20.76-0.092-1.968-0.204-3.932-0.244-5.92 0-0.016 0-0.036 0-0.050-1.938-95.412 56.602-174.39 130.754-176.402 74.15-2.014 135.828 73.7 137.772 169.11 0 0.018 0 0.038 0 0.052 0.874 43.146-10.66 82.866-30.37 113.678z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "tux", + "brand", + "linux" + ], + "defaultCode": 60093, + "grid": 16 + }, + { + "id": 446, + "paths": [ + "M791.498 544.092c-1.294-129.682 105.758-191.876 110.542-194.966-60.152-88.020-153.85-100.078-187.242-101.472-79.742-8.074-155.596 46.948-196.066 46.948-40.368 0-102.818-45.754-168.952-44.552-86.916 1.292-167.058 50.538-211.812 128.38-90.304 156.698-23.126 388.84 64.89 515.926 43.008 62.204 94.292 132.076 161.626 129.58 64.842-2.588 89.362-41.958 167.756-41.958s100.428 41.958 169.050 40.67c69.774-1.296 113.982-63.398 156.692-125.796 49.39-72.168 69.726-142.038 70.924-145.626-1.548-0.706-136.060-52.236-137.408-207.134zM662.562 163.522c35.738-43.358 59.86-103.512 53.28-163.522-51.478 2.096-113.878 34.29-150.81 77.55-33.142 38.376-62.148 99.626-54.374 158.436 57.466 4.484 116.128-29.204 151.904-72.464z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "apple", + "brand" + ], + "defaultCode": 60094, + "grid": 16 + }, + { + "id": 447, + "paths": [ + "M569.226 778.256c-0.002-0.044-0.002-0.088-0.004-0.132 0.002 0.044 0.002 0.088 0.004 0.132z", + "M570.596 814.538c-0.012-0.234-0.022-0.466-0.032-0.702 0.010 0.234 0.020 0.466 0.032 0.702z", + "M569.814 796.312c-0.006-0.178-0.012-0.356-0.020-0.536 0.010 0.182 0.016 0.358 0.020 0.536z", + "M960 0h-896c-35.2 0-64 28.8-64 64v896c0 35.2 28.8 64 64 64h493.832c0.044 0 0.088 0.006 0.132 0.006 0.042 0 0.084-0.006 0.126-0.006h401.91c35.2 0 64-28.8 64-64v-896c0-35.2-28.8-64-64-64zM192 224c0-17.672 14.328-32 32-32s32 14.328 32 32v64c0 17.672-14.328 32-32 32s-32-14.328-32-32v-64zM960 960h-375.058c-6.7-42.082-10.906-85.476-13.388-127.604 0.006 0.116 0.010 0.228 0.018 0.344-19.696 2.146-39.578 3.26-59.572 3.26-133.65 0-262.382-48.656-362.484-137.006-14.906-13.156-16.326-35.906-3.168-50.812 13.158-14.904 35.906-16.326 50.814-3.168 86.936 76.728 198.748 118.986 314.838 118.986 19.086 0 38.052-1.166 56.816-3.416-2.192-118.194 6.876-211.914 7.026-213.404 0.898-8.996-2.050-17.952-8.118-24.654-6.066-6.702-14.682-10.526-23.724-10.526h-95.174c1.384-34.614 5.082-93.814 14.958-160.188 18.864-126.76 51.994-225.77 96.152-287.812h400.064v896z", + "M800 320c-17.674 0-32-14.328-32-32v-64c0-17.672 14.326-32 32-32s32 14.328 32 32v64c0 17.672-14.326 32-32 32z", + "M540.496 835.232c-3.646 0.192-7.298 0.336-10.956 0.454 3.658-0.116 7.31-0.264 10.956-0.454z", + "M512 836c4.692 0 9.374-0.074 14.050-0.196-4.676 0.122-9.358 0.196-14.050 0.196z", + "M539.074 763.202c0.784-0.044 1.568-0.084 2.352-0.132-0.782 0.048-1.568 0.088-2.352 0.132z", + "M525.084 763.8c1.074-0.030 2.146-0.072 3.218-0.11-1.072 0.038-2.144 0.082-3.218 0.11z", + "M877.65 648.182c-13.156-14.91-35.908-16.322-50.812-3.168-72.642 64.114-162.658 104.136-258.022 115.57 0.43 23.278 1.294 47.496 2.754 72.156 111.954-12.21 217.786-58.614 302.912-133.746 14.908-13.156 16.326-35.906 3.168-50.812z", + "M571.498 832.748c-4.606 0.5-9.222 0.936-13.848 1.322 4.626-0.384 9.244-0.822 13.848-1.322z", + "M555.488 834.242c-3.906 0.312-7.822 0.576-11.742 0.806 3.92-0.226 7.834-0.496 11.742-0.806z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "finder", + "brand", + "mac", + "os" + ], + "defaultCode": 60095, + "grid": 16 + }, + { + "id": 448, + "paths": [ + "M896 384c-35.2 0-64 28.8-64 64v256c0 35.2 28.8 64 64 64s64-28.8 64-64v-256c0-35.2-28.8-64-64-64zM128 384c-35.2 0-64 28.8-64 64v256c0 35.2 28.8 64 64 64s64-28.8 64-64v-256c0-35.2-28.802-64-64-64zM224 736c0 53.020 42.98 96 96 96v0 128c0 35.2 28.8 64 64 64s64-28.8 64-64v-128h128v128c0 35.2 28.8 64 64 64s64-28.8 64-64v-128c53.020 0 96-42.98 96-96v-352h-576v352z", + "M798.216 320.002c-9.716-87.884-59.004-163.792-129.62-209.646l32.024-64.046c7.904-15.806 1.496-35.028-14.31-42.932s-35.030-1.496-42.932 14.312l-32.142 64.286-8.35-3.316c-28.568-9.502-59.122-14.66-90.886-14.66-31.762 0-62.316 5.158-90.888 14.656l-8.348 3.316-32.142-64.282c-7.904-15.808-27.128-22.212-42.932-14.312-15.808 7.904-22.214 27.126-14.312 42.932l32.022 64.046c-70.616 45.852-119.904 121.762-129.622 209.644v32h574.222v-31.998h-1.784zM416 256c-17.674 0-32-14.328-32-32 0-17.648 14.288-31.958 31.93-31.996 0.032 0 0.062 0.002 0.094 0.002 0.018 0 0.036-0.002 0.052-0.002 17.638 0.042 31.924 14.35 31.924 31.996 0 17.672-14.326 32-32 32zM608 256c-17.674 0-32-14.328-32-32 0-17.646 14.286-31.954 31.924-31.996 0.016 0 0.034 0.002 0.050 0.002 0.032 0 0.064-0.002 0.096-0.002 17.64 0.038 31.93 14.348 31.93 31.996 0 17.672-14.326 32-32 32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "android", + "brand", + "os", + "mobile" + ], + "defaultCode": 60096, + "grid": 16 + }, + { + "id": 449, + "paths": [ + "M412.23 511.914c-47.708-24.518-94.086-36.958-137.88-36.958-5.956 0-11.952 0.18-17.948 0.708-55.88 4.624-106.922 19.368-139.75 30.828-8.708 3.198-17.634 6.576-26.83 10.306l-89.822 311.394c61.702-22.832 116.292-33.938 166.27-33.938 80.846 0 139.528 30.208 187.992 61.304 22.962-77.918 78.044-266.090 94.482-322.324-11.95-7.284-24.076-14.57-36.514-21.32zM528.348 591.070l-90.446 314.148c26.832 15.372 117.098 64.050 186.212 64.050 55.792 0 118.252-14.296 190.834-43.792l86.356-301.976c-58.632 18.922-114.876 28.52-167.464 28.52-95.95 0-163.114-31.098-205.492-60.95zM292.822 368.79c77.118 0.798 134.152 30.208 181.416 60.502l92.752-317.344c-19.546-11.196-70.806-39.094-107.858-48.6-24.386-5.684-50.020-8.616-77.204-8.616-51.796 0.976-108.388 13.946-172.888 39.8l-88.44 310.596c64.808-24.436 120.644-36.34 172.086-36.34 0.046 0.002 0.136 0.002 0.136 0.002zM1024 198.124c-58.814 22.832-116.208 34.466-171.028 34.466-91.686 0-159.292-31.802-203.094-62.366l-91.95 318.236c61.746 39.708 128.29 59.878 198.122 59.878 56.948 0 115.94-13.68 175.462-40.688l-0.182-2.222 3.734-0.886 88.936-306.418z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "windows", + "brand", + "os" + ], + "defaultCode": 60097, + "grid": 16 + }, + { + "id": 450, + "paths": [ + "M0.35 512l-0.35-312.074 384-52.144v364.218zM448 138.482l511.872-74.482v448h-511.872zM959.998 576l-0.126 448-511.872-72.016v-375.984zM384 943.836l-383.688-52.594-0.020-315.242h383.708z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "windows8", + "brand", + "os" + ], + "defaultCode": 60098, + "grid": 16 + }, + { + "id": 451, + "paths": [ + "M891.96 514.204c-18.086 0-35.348 3.52-51.064 9.856-10.506-114.358-110.29-204.060-232-204.060-29.786 0-58.682 5.63-84.318 15.164-9.96 3.702-12.578 7.52-12.578 14.916v402.714c0 7.766 6.24 14.234 14.124 14.996 0.336 0.034 363.536 0.21 365.89 0.21 72.904 0 131.986-56.816 131.986-126.894s-59.134-126.902-132.040-126.902zM400 768h32l16-224.22-16-223.78h-32l-16 223.78zM304 768h-32l-16-162.75 16-157.25h32l16 160zM144 768h32l16-128-16-128h-32l-16 128zM16 704h32l16-64-16-64h-32l-16 64z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "soundcloud", + "brand", + "social" + ], + "defaultCode": 60099, + "grid": 16 + }, + { + "id": 452, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM176 704h-32l-16-96 16-96h32l16 96-16 96zM304 704h-32l-16-128 16-128h32l16 128-16 128zM432 704h-32l-16-192 16-192h32l16 192-16 192zM825.2 704c-2 0-301.2-0.2-301.4-0.2-6.4-0.6-11.6-6.2-11.8-12.8v-345.2c0-6.4 2.2-9.6 10.4-12.8 21.2-8.2 45-13 69.6-13 100.2 0 182.4 76.8 191.2 175 13-5.4 27.2-8.4 42-8.4 60 0 108.8 48.8 108.8 108.8s-48.8 108.6-108.8 108.6z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "soundcloud", + "brand", + "social" + ], + "defaultCode": 60100, + "grid": 16 + }, + { + "id": 453, + "paths": [ + "M425.6 37.4c-1.6-1-3.4-1.8-5-2.6-1.8 0.4-3.4 0.6-5.2 1l10.2 1.6z", + "M36.8 421c-0.4 1.8-0.6 3.6-0.8 5.2 1 1.6 1.6 3.2 2.6 4.8l-1.8-10z", + "M986.8 602.6c0.4-1.8 0.6-3.6 1-5.4-1-1.6-1.6-3.2-2.6-4.8l1.6 10.2z", + "M592 983c1.6 1 3.4 1.8 5 2.6 1.8-0.4 3.6-0.6 5.4-0.8l-10.4-1.8z", + "M987.8 597.2c-0.4 1.8-0.6 3.6-1 5.4l-1.8-10.4c1 1.8 1.8 3.4 2.8 5 5.2-28.8 8-58.2 8-87.6 0-65.2-12.8-128.6-38-188.2-24.4-57.6-59.2-109.4-103.6-153.8s-96.2-79.2-153.6-103.6c-59.6-25.2-123-38-188.2-38-30.8 0-61.6 2.8-91.6 8.6 0 0-0.2 0-0.2 0 1.6 0.8 3.4 1.6 5 2.6l-10.2-1.6c1.8-0.4 3.4-0.6 5.2-1-41.2-21.8-87.4-33.6-134.2-33.6-76.4 0-148.4 29.8-202.4 83.8s-83.8 126-83.8 202.4c0 48.6 12.6 96.6 36 138.8 0.4-1.8 0.6-3.6 0.8-5.2l1.8 10.2c-1-1.6-1.8-3.2-2.6-4.8-4.8 27.4-7.2 55.4-7.2 83.4 0 65.2 12.8 128.6 38 188.2 24.4 57.6 59.2 109.2 103.6 153.6s96.2 79.2 153.8 103.6c59.6 25.2 123 38 188.2 38 28.4 0 56.8-2.6 84.6-7.6-1.6-1-3.2-1.8-5-2.6l10.4 1.8c-1.8 0.4-3.6 0.6-5.4 0.8 42.8 24.2 91.4 37.2 140.8 37.2 76.4 0 148.4-29.8 202.4-83.8s83.8-126 83.8-202.4c-0.2-48.6-12.8-96.6-36.4-139.2zM514.2 805.8c-171.8 0-248.6-84.4-248.6-147.8 0-32.4 24-55.2 57-55.2 73.6 0 54.4 105.6 191.6 105.6 70.2 0 109-38.2 109-77.2 0-23.4-11.6-49.4-57.8-60.8l-152.8-38.2c-123-30.8-145.4-97.4-145.4-160 0-129.8 122.2-178.6 237-178.6 105.8 0 230.4 58.4 230.4 136.4 0 33.4-29 52.8-62 52.8-62.8 0-51.2-86.8-177.6-86.8-62.8 0-97.4 28.4-97.4 69s49.6 53.6 92.6 63.4l113.2 25.2c123.8 27.6 155.2 100 155.2 168 0 105.4-81 184.2-244.4 184.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "skype", + "brand", + "social" + ], + "defaultCode": 60101, + "grid": 16 + }, + { + "id": 454, + "paths": [ + "M256 640c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM640 640c0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64s-64-28.654-64-64zM643.112 776.778c16.482-12.986 40.376-10.154 53.364 6.332s10.152 40.378-6.334 53.366c-45.896 36.158-115.822 59.524-178.142 59.524-62.322 0-132.248-23.366-178.144-59.522-16.486-12.99-19.32-36.882-6.332-53.368 12.99-16.482 36.882-19.318 53.366-6.332 26.422 20.818 78.722 43.222 131.11 43.222s104.688-22.404 131.112-43.222zM1024 512c0-70.692-57.308-128-128-128-48.116 0-89.992 26.57-111.852 65.82-65.792-35.994-145.952-59.246-233.28-64.608l76.382-171.526 146.194 42.2c13.152 37.342 48.718 64.114 90.556 64.114 53.020 0 96-42.98 96-96s-42.98-96-96-96c-36.56 0-68.342 20.442-84.554 50.514l-162.906-47.024c-18.224-5.258-37.538 3.722-45.252 21.052l-103.77 233.026c-85.138 5.996-163.262 29.022-227.636 64.236-21.864-39.25-63.766-65.804-111.882-65.804-70.692 0-128 57.308-128 128 0 52.312 31.402 97.254 76.372 117.102-8.070 24.028-12.372 49.104-12.372 74.898 0 176.73 200.576 320 448 320 247.422 0 448-143.27 448-320 0-25.792-4.3-50.862-12.368-74.886 44.97-19.85 76.368-64.802 76.368-117.114zM864 188c19.882 0 36 16.118 36 36s-16.118 36-36 36-36-16.118-36-36 16.118-36 36-36zM64 512c0-35.29 28.71-64 64-64 25.508 0 47.572 15.004 57.846 36.646-33.448 25.366-61.166 54.626-81.666 86.738-23.524-9.47-40.18-32.512-40.18-59.384zM512 948c-205.45 0-372-109.242-372-244s166.55-244 372-244c205.45 0 372 109.242 372 244s-166.55 244-372 244zM919.82 571.384c-20.5-32.112-48.218-61.372-81.666-86.738 10.276-21.642 32.338-36.646 57.846-36.646 35.29 0 64 28.71 64 64 0 26.872-16.656 49.914-40.18 59.384z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "reddit", + "brand", + "social" + ], + "defaultCode": 60102, + "grid": 16 + }, + { + "id": 455, + "paths": [ + "M0 0v1024h1024v-1024h-1024zM544 584v216h-64v-216l-175-328h72.6l134.4 252 134.4-252h72.6l-175 328z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "hackernews", + "brand", + "ycombinator", + "yc", + "social" + ], + "defaultCode": 60103, + "grid": 16 + }, + { + "id": 456, + "paths": [ + "M966.8 233.6c0 3.2-1 6.2-3 9-2 2.6-4.2 4-6.8 4-20 2-36.4 8.4-49 19.2-12.8 10.8-25.8 31.8-39.2 62.4l-206.4 465.4c-1.4 4.4-5.2 6.4-11.4 6.4-4.8 0-8.6-2.2-11.4-6.4l-115.8-242-133.2 242c-2.8 4.4-6.4 6.4-11.4 6.4-6 0-9.8-2.2-11.8-6.4l-202.6-465.2c-12.6-28.8-26-49-40-60.4s-33.6-18.6-58.6-21.2c-2.2 0-4.2-1.2-6-3.4-2-2.2-2.8-4.8-2.8-7.8 0-7.6 2.2-11.4 6.4-11.4 18 0 37 0.8 56.8 2.4 18.4 1.6 35.6 2.4 51.8 2.4 16.4 0 36-0.8 58.4-2.4 23.4-1.6 44.2-2.4 62.4-2.4 4.4 0 6.4 3.8 6.4 11.4s-1.4 11.2-4 11.2c-18 1.4-32.4 6-42.8 13.8s-15.6 18-15.6 30.8c0 6.4 2.2 14.6 6.4 24.2l167.4 378.4 95.2-179.6-88.6-185.8c-16-33.2-29-54.6-39.2-64.2s-25.8-15.4-46.6-17.6c-2 0-3.6-1.2-5.4-3.4s-2.6-4.8-2.6-7.8c0-7.6 1.8-11.4 5.6-11.4 18 0 34.6 0.8 49.8 2.4 14.6 1.6 30 2.4 46.6 2.4 16.2 0 33.2-0.8 51.4-2.4 18.6-1.6 37-2.4 55-2.4 4.4 0 6.4 3.8 6.4 11.4s-1.2 11.2-4 11.2c-36.2 2.4-54.2 12.8-54.2 30.8 0 8 4.2 20.6 12.6 37.6l58.6 119 58.4-108.8c8-15.4 12.2-28.4 12.2-38.8 0-24.8-18-38-54.2-39.6-3.2 0-4.8-3.8-4.8-11.2 0-2.8 0.8-5.2 2.4-7.6s3.2-3.6 4.8-3.6c13 0 28.8 0.8 47.8 2.4 18 1.6 33 2.4 44.6 2.4 8.4 0 20.6-0.8 36.8-2 20.4-1.8 37.6-2.8 51.4-2.8 3.2 0 4.8 3.2 4.8 9.6 0 8.6-3 13-8.8 13-21 2.2-38 8-50.8 17.4s-28.8 30.8-48 64.4l-78.2 143.2 105.2 214.4 155.4-361.4c5.4-13.2 8-25.4 8-36.4 0-26.4-18-40.4-54.2-42.2-3.2 0-4.8-3.8-4.8-11.2 0-7.6 2.4-11.4 7.2-11.4 13.2 0 28.8 0.8 47 2.4 16.8 1.6 30.8 2.4 42 2.4 12 0 25.6-0.8 41.2-2.4 16.2-1.6 30.8-2.4 43.8-2.4 4 0 6 3.2 6 9.6z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "wikipedia", + "brand" + ], + "defaultCode": 60104, + "grid": 16 + }, + { + "id": 457, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM384 832h-128v-448h128v448zM320 320c-35.4 0-64-28.6-64-64s28.6-64 64-64c35.4 0 64 28.6 64 64s-28.6 64-64 64zM832 832h-128v-256c0-35.4-28.6-64-64-64s-64 28.6-64 64v256h-128v-448h128v79.4c26.4-36.2 66.8-79.4 112-79.4 79.6 0 144 71.6 144 160v288z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "linkedin", + "brand", + "social" + ], + "defaultCode": 60105, + "grid": 16 + }, + { + "id": 458, + "paths": [ + "M384 384h177.106v90.782h2.532c24.64-44.194 84.958-90.782 174.842-90.782 186.946 0 221.52 116.376 221.52 267.734v308.266h-184.61v-273.278c0-65.184-1.334-149.026-96.028-149.026-96.148 0-110.82 70.986-110.82 144.292v278.012h-184.542v-576z", + "M64 384h192v576h-192v-576z", + "M256 224c0 53.019-42.981 96-96 96s-96-42.981-96-96c0-53.019 42.981-96 96-96s96 42.981 96 96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "linkedin", + "brand", + "social" + ], + "defaultCode": 60106, + "grid": 16 + }, + { + "id": 459, + "paths": [ + "M451.6 766.2l-37.6-102c0 0-61 68-152.4 68-81 0-138.4-70.4-138.4-183 0-144.2 72.8-195.8 144.2-195.8 103.2 0 136 66.8 164.2 152.4l37.6 117.2c37.6 113.8 108 205.2 310.8 205.2 145.4 0 244-44.6 244-161.8 0-95-54-144.2-154.8-167.8l-75-16.4c-51.6-11.8-66.8-32.8-66.8-68 0-39.8 31.6-63.4 83.2-63.4 56.4 0 86.8 21.2 91.4 71.6l117.2-14c-9.4-105.6-82.2-149-201.8-149-105.6 0-208.8 39.8-208.8 167.8 0 79.8 38.8 130.2 136 153.6l79.8 18.8c59.8 14 79.8 38.8 79.8 72.8 0 43.4-42.2 61-122 61-118.4 0-167.8-62.2-195.8-147.8l-38.8-117.2c-49-152.6-127.6-208.8-283.6-208.8-172.4 0-264 109-264 294.4 0 178.2 91.4 274.4 255.8 274.4 132.4 0 195.8-62.2 195.8-62.2v0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "lastfm", + "brand", + "social" + ], + "defaultCode": 60107, + "grid": 16 + }, + { + "id": 460, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM746.6 760.8c-177.6 0-239.2-80-272-179.6l-32.8-102.6c-24.6-75-53.4-133.4-143.6-133.4-62.6 0-126.2 45.2-126.2 171.4 0 98.6 50.2 160.2 121.2 160.2 80 0 133.4-59.6 133.4-59.6l32.8 89.2c0 0-55.4 54.4-171.4 54.4-144 0-224-84-224-240 0-162.2 80-257.6 231-257.6 136.6 0 205.2 49.2 248.4 182.6l33.8 102.6c24.6 75 67.8 129.4 171.4 129.4 69.8 0 106.8-15.4 106.8-53.4 0-29.8-17.4-51.4-69.8-63.6l-69.8-16.4c-85.2-20.6-119-64.6-119-134.4 0-111.8 90.4-146.8 182.6-146.8 104.6 0 168.4 38 176.6 130.4l-102.6 12.4c-4.2-44.2-30.8-62.6-80-62.6-45.2 0-72.8 20.6-72.8 55.4 0 30.8 13.4 49.2 58.4 59.6l65.6 14.4c88.2 20.6 135.4 63.6 135.4 146.8 0 102.2-86.2 141.2-213.4 141.2z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "lastfm", + "brand", + "social" + ], + "defaultCode": 60108, + "grid": 16 + }, + { + "id": 461, + "paths": [ + "M0 0v1024h1024v-1024h-1024zM512 960v-448h-448v-448h448v448h448v448h-448z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "delicious", + "brand", + "social" + ], + "defaultCode": 60109, + "grid": 16 + }, + { + "id": 462, + "paths": [ + "M512 320c-35.2 0-64 28.8-64 64v256c0 105.8-86.2 192-192 192s-192-86.2-192-192v-128h128v128c0 35.2 28.8 64 64 64s64-28.8 64-64v-256c0-105.8 86.2-192 192-192s192 86.2 192 178v62l-82 24-46-24v-62c0-21.2-28.8-50-64-50z", + "M960 640c0 105.8-86.2 192-192 192s-192-86.2-192-206v-124l46 24 82-24v124c0 49.2 28.8 78 64 78s64-28.8 64-64v-128h128v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stumbleupon", + "brand", + "social" + ], + "defaultCode": 60110, + "grid": 16 + }, + { + "id": 463, + "paths": [ + "M852 0h-680c-94.6 0-172 77.4-172 172v680c0 94.6 77.4 172 172 172h680c94.6 0 172-77.4 172-172v-680c0-94.6-77.4-172-172-172zM512 320c-35.29 0-64 28.71-64 64v256c0 105.872-86.13 192-192 192s-192-86.128-192-192v-128h128v128c0 35.29 28.71 64 64 64s64-28.71 64-64v-256c0-105.87 86.13-192 192-192s192 86.13 192 178v62l-82 24-46-24v-62c0-21.29-28.71-50-64-50zM960 640c0 105.872-86.13 192-192 192s-192-86.128-192-206v-124l46 24 82-24v124c0 49.29 28.71 78 64 78s64-28.71 64-64v-128h128v128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stumbleupon", + "brand", + "social" + ], + "defaultCode": 60111, + "grid": 16 + }, + { + "id": 464, + "paths": [ + "M1024 640v384h-1024v-384h128v256h768v-256zM192 704h640v128h-640zM207.152 565.466l27.698-124.964 624.832 138.496-27.698 124.964zM279.658 308.558l54.092-116.006 580.032 270.464-54.092 116.006zM991.722 361.476l-77.922 101.55-507.746-389.608 56.336-73.418h58.244z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "stackoverflow", + "brand", + "social" + ], + "defaultCode": 60112, + "grid": 16 + }, + { + "id": 465, + "paths": [ + "M512 68.4c-245 0-443.6 198.6-443.6 443.6 0 188 117 348.4 282 413-3.8-35-7.4-89 1.6-127.2 8-34.6 52-220.4 52-220.4s-13.2-26.6-13.2-65.8c0-61.6 35.8-107.8 80.2-107.8 37.8 0 56.2 28.4 56.2 62.4 0 38-24.2 95-36.8 147.6-10.6 44.2 22 80.2 65.6 80.2 78.8 0 139.4-83.2 139.4-203.2 0-106.2-76.4-180.4-185.2-180.4-126.2 0-200.2 94.6-200.2 192.6 0 38.2 14.6 79 33 101.2 3.6 4.4 4.2 8.2 3 12.8-3.4 14-10.8 44.2-12.4 50.4-2 8.2-6.4 9.8-14.8 6-55.4-25.8-90-106.8-90-171.8 0-140 101.6-268.4 293-268.4 153.8 0 273.4 109.6 273.4 256.2 0 152.8-96.4 276-230.2 276-45 0-87.2-23.4-101.6-51 0 0-22.2 84.6-27.6 105.4-10 38.6-37 86.8-55.2 116.2 41.6 12.8 85.6 19.8 131.4 19.8 245 0 443.6-198.6 443.6-443.6 0-245.2-198.6-443.8-443.6-443.8z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pinterest", + "brand", + "social" + ], + "defaultCode": 60113, + "grid": 16 + }, + { + "id": 466, + "paths": [ + "M512 0c-282.4 0-512 229.6-512 512s229.6 512 512 512 512-229.6 512-512-229.6-512-512-512zM512 955.6c-45.8 0-89.8-7-131.4-19.8 18-29.4 45.2-77.8 55.2-116.2 5.4-20.8 27.6-105.4 27.6-105.4 14.4 27.6 56.8 51 101.6 51 133.8 0 230.2-123 230.2-276 0-146.6-119.6-256.2-273.4-256.2-191.4 0-293 128.6-293 268.4 0 65 34.6 146 90 171.8 8.4 4 12.8 2.2 14.8-6 1.4-6.2 9-36.2 12.4-50.4 1-4.4 0.6-8.4-3-12.8-18.4-22.2-33-63.2-33-101.2 0-97.8 74-192.6 200.2-192.6 109 0 185.2 74.2 185.2 180.4 0 120-60.6 203.2-139.4 203.2-43.6 0-76.2-36-65.6-80.2 12.6-52.8 36.8-109.6 36.8-147.6 0-34-18.2-62.4-56.2-62.4-44.6 0-80.2 46-80.2 107.8 0 39.2 13.2 65.8 13.2 65.8s-44 185.8-52 220.4c-9 38.4-5.4 92.2-1.6 127.2-165-64.4-282-224.8-282-412.8 0-245 198.6-443.6 443.6-443.6s443.6 198.6 443.6 443.6c0 245-198.6 443.6-443.6 443.6z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "pinterest", + "brand", + "social" + ], + "defaultCode": 60114, + "grid": 16 + }, + { + "id": 467, + "paths": [ + "M928 0h-832c-52.8 0-96 43.2-96 96v832c0 52.8 43.2 96 96 96h832c52.8 0 96-43.2 96-96v-832c0-52.8-43.2-96-96-96zM312.6 666h-110.6c-6.6 0-11.6-3-14.4-7.6-3-4.8-3-10.8 0-17l117.6-207.6c0.2-0.2 0.2-0.4 0-0.6l-74.8-129.6c-3-6.2-3.6-12.2-0.6-17 2.8-4.6 8.4-7 15.2-7h110.8c17 0 25.4 11 30.8 20.8 0 0 75.6 132 76.2 132.8-4.4 8-119.6 211.4-119.6 211.4-6 10.4-14 21.4-30.6 21.4zM836.4 152.2l-245.2 433.6c-0.2 0.2-0.2 0.6 0 0.8l156.2 285.2c3 6.2 3.2 12.4 0.2 17.2-2.8 4.6-8 7-14.8 7h-110.6c-17 0-25.4-11.2-31-21 0 0-157-288-157.4-288.8 7.8-13.8 246.4-437 246.4-437 6-10.6 13.2-21 29.6-21h112.2c6.6 0 12 2.6 14.8 7 2.8 4.6 2.8 10.8-0.4 17z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "xing", + "brand", + "social" + ], + "defaultCode": 60115, + "grid": 16 + }, + { + "id": 468, + "paths": [ + "M155.6 202.2c-8.8 0-16.4 3.2-20.2 9.2-3.8 6.4-3.2 14.4 0.8 22.6l99.8 172.8c0.2 0.4 0.2 0.6 0 0.8l-156.8 277.2c-4 8.2-3.8 16.4 0 22.6 3.8 6 10.4 10 19.2 10h147.6c22 0 32.8-15 40.2-28.6 0 0 153.4-271.4 159.4-282-0.6-1-101.6-177-101.6-177-7.4-13-18.4-27.6-41.2-27.6h-147.2z", + "M776 0c-22 0-31.6 13.8-39.6 28.2 0 0-318.2 564.2-328.6 582.8 0.6 1 209.8 385 209.8 385 7.4 13 18.6 28.2 41.2 28.2h147.6c8.8 0 15.8-3.4 19.6-9.4 4-6.4 3.8-14.6-0.4-22.8l-208-380.6c-0.2-0.4-0.2-0.6 0-1l327-578.2c4-8.2 4.2-16.4 0.4-22.8-3.8-6-10.8-9.4-19.6-9.4h-149.4z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "xing", + "brand", + "social" + ], + "defaultCode": 60116, + "grid": 16 + }, + { + "id": 469, + "paths": [ + "M367.562 0c-243.358 0-367.562 140.162-367.562 401.856v0 549.034l238.39-238.628v-278.896c0-108.416 28.73-177.406 125.118-192.894v0c33.672-6.584 103.75-4.278 148.306-4.278v0 165.596c0 1.51 0.208 4.206 0.594 5.586v0c1.87 6.704 7.93 11.616 15.116 11.63v0c4.062 0.008 7.868-2.104 11.79-5.97v0l413.122-412.974-584.874-0.062zM785.61 311.746v278.89c0 108.414-28.736 177.414-125.116 192.894v0c-33.672 6.582-103.756 4.278-148.312 4.278v0-165.594c0-1.5-0.206-4.204-0.594-5.582v0c-1.864-6.712-7.922-11.622-15.112-11.63v0c-4.064-0.008-7.866 2.112-11.79 5.966v0l-413.124 412.966 584.874 0.066c243.354 0 367.564-140.168 367.564-401.852v0-549.028l-238.39 238.626z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "flattr", + "brand", + "donate", + "social" + ], + "defaultCode": 60117, + "grid": 16 + }, + { + "id": 470, + "paths": [ + "M851.564 90.090c-12.060-16.404-31.204-26.090-51.564-26.090h-608c-35.346 0-64 28.654-64 64v768c0 25.884 15.592 49.222 39.508 59.128 7.918 3.28 16.234 4.874 24.478 4.874 16.656 0 33.026-6.504 45.268-18.748l237.256-237.254h165.49c27.992 0 52.736-18.192 61.086-44.91l160-512c6.074-19.432 2.538-40.596-9.522-57zM672.948 320h-224.948c-35.346 0-64 28.654-64 64s28.654 64 64 64h184.948l-40 128h-144.948c-16.974 0-33.252 6.742-45.254 18.746l-146.746 146.744v-549.49h456.948l-40 128z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "foursquare", + "brand", + "social" + ], + "defaultCode": 60118, + "grid": 16 + }, + { + "id": 471, + "paths": [ + "M608.876 653.468c-17.282 17.426-2.668 49.128-2.668 49.128l130.090 217.218c0 0 21.36 28.64 39.864 28.64 18.59 0 36.954-15.27 36.954-15.27l102.844-147.008c0 0 10.36-18.546 10.598-34.792 0.372-23.106-34.454-29.434-34.454-29.434l-243.488-78.192c-0.002 0.004-23.858-6.328-39.74 9.71zM596.532 543.984c12.46 21.128 46.828 14.972 46.828 14.972l242.938-71.006c0 0 33.106-13.466 37.832-31.418 4.64-17.954-5.46-39.622-5.46-39.622l-116.098-136.752c0 0-10.062-17.292-30.938-19.032-23.016-1.958-37.18 25.898-37.18 25.898l-137.27 216.010c0 0.004-12.134 21.516-0.652 40.95zM481.754 459.768c28.608-7.044 33.148-48.604 33.148-48.604l-1.944-345.87c0 0-4.314-42.666-23.486-54.232-30.070-18.242-38.982-8.718-47.596-7.444l-201.696 74.944c0 0-19.754 6.536-30.042 23.018-14.69 23.352 14.928 57.544 14.928 57.544l209.644 285.756c0 0 20.69 21.396 47.044 14.888zM431.944 599.738c0.722-26.676-32.030-42.7-32.030-42.7l-216.796-109.524c0 0-32.126-13.246-47.722-4.016-11.95 7.060-22.536 19.84-23.572 31.134l-14.12 173.812c0 0-2.116 30.114 5.69 43.82 11.054 19.442 47.428 5.902 47.428 5.902l253.096-55.942c9.832-6.61 27.074-7.204 28.026-42.486zM494.88 693.542c-21.726-11.156-47.724 11.95-47.724 11.95l-169.468 186.566c0 0-21.144 28.528-15.768 46.050 5.066 16.418 13.454 24.578 25.318 30.328l170.192 53.726c0 0 20.634 4.286 36.258-0.242 22.18-6.43 18.094-41.152 18.094-41.152l3.848-252.602c-0.002 0.002-0.868-24.334-20.75-34.624z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "yelp", + "brand", + "social" + ], + "defaultCode": 60119, + "grid": 16 + }, + { + "id": 472, + "paths": [ + "M930 308.6c-47.8 212.2-195.4 324.2-428 324.2h-77.4l-53.8 341.6h-64.8l-3.4 22c-2.2 14.6 9 27.6 23.6 27.6h165.6c19.6 0 36.2-14.2 39.4-33.6l1.6-8.4 31.2-197.8 2-10.8c3-19.4 19.8-33.6 39.4-33.6h24.6c160.4 0 286-65.2 322.8-253.6 13.8-71.6 8.6-132.4-22.8-177.6z", + "M831 77.2c-47.4-54-133.2-77.2-242.8-77.2h-318.2c-22.4 0-41.6 16.2-45 38.4l-132.6 840.4c-2.6 16.6 10.2 31.6 27 31.6h196.6l49.4-313-1.6 9.8c3.4-22.2 22.4-38.4 44.8-38.4h93.4c183.4 0 327-74.4 369-290 1.2-6.4 2.4-12.6 3.2-18.6 12.4-79.6 0-134-43.2-183z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "paypal", + "brand", + "donate" + ], + "defaultCode": 60120, + "grid": 16 + }, + { + "id": 473, + "paths": [ + "M258.278 446.542l-146.532-253.802c93.818-117.464 238.234-192.74 400.254-192.74 187.432 0 351.31 100.736 440.532 251h-417.77c-7.504-0.65-15.092-1-22.762-1-121.874 0-224.578 83.644-253.722 196.542zM695.306 325h293.46c22.74 57.93 35.234 121.004 35.234 187 0 280.826-226.1 508.804-506.186 511.926l209.394-362.678c29.48-42.378 46.792-93.826 46.792-149.248 0-73.17-30.164-139.42-78.694-187zM326 512c0-102.56 83.44-186 186-186s186 83.44 186 186c0 102.56-83.44 186-186 186s-186-83.44-186-186zM582.182 764.442l-146.578 253.878c-246.532-36.884-435.604-249.516-435.604-506.32 0-91.218 23.884-176.846 65.696-251.024l209.030 362.054c41.868 89.112 132.476 150.97 237.274 150.97 24.3 0 47.836-3.34 70.182-9.558z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "chrome", + "browser", + "internet", + "brand" + ], + "defaultCode": 60121, + "grid": 16 + }, + { + "id": 474, + "paths": [ + "M1022.526 334.14l-11.86 76.080c0 0-16.954-140.856-37.732-193.514-31.846-80.688-46.014-80.040-46.108-79.922 21.33 54.204 17.462 83.324 17.462 83.324s-37.792-102.998-137.712-135.768c-110.686-36.282-170.57-26.364-177.488-24.486-1.050-0.008-2.064-0.010-3.030-0.010 0.818 0.062 1.612 0.146 2.426 0.212-0.034 0.020-0.090 0.042-0.082 0.052 0.45 0.548 122.306 21.302 143.916 50.996 0 0-51.76 0-103.272 14.842-2.328 0.666 189.524 23.964 228.746 215.674 0 0-21.030-43.876-47.040-51.328 17.106 52.036 12.714 150.776-3.576 199.85-2.096 6.312-4.24-27.282-36.328-41.75 10.28 73.646-0.616 190.456-51.708 222.632-3.982 2.504 32.030-115.31 7.242-69.762-142.708 218.802-311.404 100.972-387.248 49.11 38.866 8.462 112.654-1.318 145.314-25.612 0.042-0.030 0.078-0.056 0.118-0.086 35.468-24.252 56.472-41.964 75.334-37.772 18.874 4.214 31.438-14.726 16.78-31.53-14.676-16.838-50.314-39.978-98.524-27.366-34 8.904-76.134 46.522-140.448 8.432-49.364-29.25-54.012-53.546-54.45-70.376 1.218-5.966 2.754-11.536 4.576-16.624 5.682-15.87 22.912-20.658 32.494-24.438 16.256 2.792 30.262 7.862 44.968 15.406 0.19-4.894 0.252-11.39-0.018-18.76 1.41-2.802 0.538-11.252-1.722-21.58-1.302-10.308-3.42-20.974-6.752-30.692 0.012-0.002 0.020-0.010 0.030-0.014 0.056-0.018 0.108-0.040 0.156-0.070 0.078-0.044 0.146-0.112 0.208-0.19 0.012-0.020 0.030-0.034 0.044-0.052 0.082-0.124 0.154-0.272 0.198-0.466 1.020-4.618 12.022-13.524 25.718-23.1 12.272-8.58 26.702-17.696 38.068-24.752 10.060-6.248 17.72-10.882 19.346-12.098 0.618-0.466 1.358-1.012 2.164-1.636 0.15-0.116 0.3-0.232 0.454-0.354 0.094-0.074 0.19-0.148 0.286-0.226 5.41-4.308 13.484-12.448 15.178-29.578 0.004-0.042 0.010-0.080 0.012-0.122 0.050-0.504 0.092-1.014 0.13-1.534 0.028-0.362 0.050-0.726 0.072-1.096 0.014-0.284 0.032-0.566 0.044-0.856 0.030-0.674 0.050-1.364 0.060-2.064 0-0.040 0.002-0.076 0.004-0.116 0.022-1.658-0.006-3.386-0.104-5.202-0.054-1.014-0.126-1.93-0.298-2.762-0.008-0.044-0.018-0.092-0.028-0.136-0.018-0.082-0.036-0.164-0.058-0.244-0.036-0.146-0.076-0.292-0.122-0.43-0.006-0.018-0.010-0.032-0.016-0.046-0.052-0.16-0.112-0.314-0.174-0.464-0.004-0.006-0.004-0.010-0.006-0.016-1.754-4.108-8.32-5.658-35.442-6.118-0.026-0.002-0.050-0.002-0.076-0.002v0c-11.066-0.188-25.538-0.194-44.502-0.118-33.25 0.134-51.628-32.504-57.494-45.132 8.040-44.46 31.276-76.142 69.45-97.626 0.722-0.406 0.58-0.742-0.274-0.978 7.464-4.514-90.246-0.124-135.186 57.036-39.888-9.914-74.654-9.246-104.616-2.214-5.754-0.162-12.924-0.88-21.434-2.652-19.924-18.056-48.448-51.402-49.976-91.208 0 0-0.092 0.072-0.252 0.204-0.020-0.382-0.056-0.76-0.072-1.142 0 0-60.716 46.664-51.628 173.882-0.022 2.036-0.064 3.986-0.12 5.874-16.432 22.288-24.586 41.020-25.192 45.156-14.56 29.644-29.334 74.254-41.356 141.98 0 0 8.408-26.666 25.284-56.866-12.412 38.022-22.164 97.156-16.436 185.856 0 0 1.514-19.666 6.874-47.994 4.186 55.010 22.518 122.924 68.858 202.788 88.948 153.32 225.67 230.74 376.792 242.616 26.836 2.212 54.050 2.264 81.424 0.186 2.516-0.178 5.032-0.364 7.55-0.574 30.964-2.174 62.134-6.852 93.238-14.366 425.172-102.798 378.942-616.198 378.942-616.198z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "firefox", + "browser", + "internet", + "brand" + ], + "defaultCode": 60122, + "grid": 16 + }, + { + "id": 475, + "paths": [ + "M734.202 628.83h236.050c1.82-16.37 2.548-33.098 2.548-50.196 0-80.224-21.534-155.468-59.124-220.266 38.88-103.308 37.492-190.988-14.556-243.39-49.496-49.28-182.29-41.28-332.412 25.198-11.104-0.84-22.318-1.272-33.638-1.272-206.048 0-378.926 141.794-426.708 332.85 64.638-82.754 132.638-142.754 223.478-186.448-8.26 7.74-56.454 55.652-64.56 63.764-239.548 239.478-315.090 552.306-233.806 633.604 61.786 61.774 173.758 51.342 302.376-11.648 59.806 30.458 127.5 47.63 199.218 47.63 193.134 0 356.804-124.316 416.090-297.448h-237.868c-32.734 60.382-96.748 101.48-170.218 101.48-73.468 0-137.484-41.098-170.216-101.48-14.55-27.274-22.914-58.554-22.914-91.656v-0.722h386.26zM348.302 512.804c5.456-97.11 86.2-174.584 184.766-174.584s179.312 77.472 184.766 174.584h-369.532zM896.966 163.808c33.526 33.88 32.688 96.214 4.012 174.022-49.136-74.908-120.518-133.936-204.792-167.64 90.106-38.638 163.406-43.756 200.78-6.382zM93.482 967.256c-42.782-42.796-29.884-132.618 25.23-240.832 34.308 96.27 101.156 177.090 187.336 229.154-95.43 43.318-173.536 50.674-212.566 11.678z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "IE", + "browser", + "internet-explorer", + "brand" + ], + "defaultCode": 60123, + "grid": 16 + }, + { + "id": 476, + "paths": [ + "M15.4 454.6c30-236.8 191.6-451.6 481.2-454.6 174.8 3.4 318.6 82.6 404.2 233.6 43 78.8 56.4 161.6 59.2 253v107.4h-642.6c3 265 390 256 556.6 139.2v215.8c-97.6 58.6-319 111-490.4 43.6-146-54.8-250-207.6-249.4-354.6-4.8-190.6 94.8-316.8 249.4-388.6-32.8 40.6-57.8 85.4-70.8 163h362.8c0 0 21.2-216.8-205.4-216.8-213.6 7.4-367.6 131.6-454.8 259v0z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "edge", + "browser", + "brand" + ], + "defaultCode": 60124, + "grid": 16 + }, + { + "id": 477, + "paths": [ + "M512 0c-282.8 0-512 229.2-512 512s229.2 512 512 512 512-229.2 512-512-229.2-512-512-512zM958.4 472.8l-1-10.6c0.2 3.6 0.6 7 1 10.6zM888.4 268.8l-7.2-10.8c2.4 3.6 4.8 7.2 7.2 10.8zM860.6 230.6l-4.4-5.4c1.6 1.8 3 3.6 4.4 5.4zM798.6 167.6l-5.4-4.4c2 1.6 3.6 3 5.4 4.4zM766 142.8l-10.8-7.2c3.6 2.4 7.2 4.8 10.8 7.2zM561.8 66.8l-10.8-1c3.6 0.2 7.2 0.6 10.8 1zM472.8 65.6l-10.8 1c3.6-0.2 7.2-0.6 10.8-1zM268.8 135.6l-10.8 7.2c3.6-2.4 7.2-4.8 10.8-7.2zM230.6 163.4l-5.2 4.2c1.8-1.4 3.4-2.8 5.2-4.2zM167.6 225.4l-4.4 5.4c1.6-1.8 3-3.6 4.4-5.4zM142.8 258l-7.2 10.8c2.4-3.6 4.8-7.2 7.2-10.8zM66.8 462.2l-1 10.8c0.2-3.6 0.6-7.2 1-10.8zM65.6 551.2l1 10.8c-0.2-3.6-0.6-7.2-1-10.8zM135.6 755l7.2 10.8c-2.4-3.4-4.8-7-7.2-10.8zM144 767.6l79.8-53.4-8.8-13.4-79.8 53.4c-36.2-56.2-60-120.8-68-190.4l47.8-4.8-1.6-16-47.8 4.8c-0.8-9.2-1.2-18.6-1.4-28h96v-16h-96c0.2-9.4 0.6-18.6 1.4-28l47.8 4.6 1.6-16-47.8-4.6c8-69.6 32-134.2 68.2-190.4l79.8 53.4 8.8-13.4-80-53c5.4-7.6 10.8-15.2 16.6-22.4l37 30.4 10.2-12.4-37-30.4c6-7.2 12.4-14 18.8-20.8l67.8 67.8 11.4-11.4-67.8-67.8c6.8-6.4 13.6-12.8 20.6-18.8l30.4 37.2 12.4-10.2-30.4-37c7.4-5.8 14.8-11.4 22.4-16.8l53.4 79.8 13.4-8.8-53.4-79.8c56.2-36.2 120.8-60 190.4-68l4.8 47.8 16-1.6-4.8-47.8c9.2-0.8 18.6-1.2 28-1.4v96h16v-96c9.4 0.2 18.6 0.6 28 1.4l-4.6 47.8 16 1.6 4.6-47.8c69.6 8 134.2 32 190.4 68.2l-53.4 79.8 13.4 8.8 53.4-79.8c7.6 5.4 15.2 10.8 22.4 16.6l-30.4 37 12.4 10.2 30.4-37c7.2 6 14 12.4 20.8 18.8l-25.6 25-350 233.4-233.4 350-25 25c-6.4-6.8-12.8-13.6-18.8-20.6l37-30.4-10.2-12.4-37 30.4c-5.8-7.2-11.2-14.8-16.6-22.4zM167.6 798.6c-1.4-1.8-2.8-3.4-4.2-5.2l4.2 5.2zM225.4 856.4l5.2 4.2c-1.8-1.4-3.4-2.8-5.2-4.2zM258 881l10.8 7.2c-3.6-2.2-7.2-4.6-10.8-7.2zM462.2 957.2l10.8 1c-3.6-0.2-7.2-0.6-10.8-1zM551.2 958.4l10.6-1c-3.6 0.2-7 0.6-10.6 1zM755.2 888.4l10.8-7.2c-3.6 2.4-7.2 4.8-10.8 7.2zM793.4 860.6l5.4-4.4c-1.8 1.6-3.6 3-5.4 4.4zM828.4 829.2l0.8-0.8c-0.2 0.2-0.6 0.6-0.8 0.8zM856.4 798.6l4.4-5.4c-1.6 1.8-3 3.6-4.4 5.4zM863.4 790l-37-30.4-10.2 12.4 37 30.4c-6 7.2-12.4 14-18.8 20.8l-67.8-67.8-11.4 11.4 67.8 67.8c-6.8 6.4-13.6 12.8-20.6 18.8l-30.4-37.2-12.4 10.2 30.4 37c-7.4 5.8-14.8 11.4-22.4 16.8l-53.4-79.8-13.4 8.8 53.4 79.8c-56.2 36.2-120.8 60-190.4 68l-4.8-47.8-16 1.6 4.8 47.8c-9.2 0.8-18.6 1.2-28 1.4v-96h-16v96c-9.4-0.2-18.6-0.6-28-1.4l4.6-47.8-16-1.6-4.6 47.8c-69.6-8-134.2-32-190.4-68.2l53.4-79.8-13.4-8.8-53 79.8c-7.6-5.4-15.2-10.8-22.4-16.6l30.4-37-12.4-10.2-30.4 37c-7.2-6-14-12.4-20.8-18.8l25.2-25 350-233.4 233.4-350 25-25c6.4 6.8 12.8 13.6 18.8 20.6l-37 30.4 10.2 12.4 37-30.4c5.8 7.4 11.4 14.8 16.8 22.4l-79.8 53.4 8.8 13.4 79.8-53.4c36.2 56.2 60 120.8 68 190.4l-47.8 4.8 1.6 16 47.8-4.8c0.8 9.2 1.2 18.6 1.4 28h-96v16h96c-0.2 9.4-0.6 18.6-1.4 28l-47.8-4.6-1.6 16 47.8 4.6c-8 69.6-32 134.2-68.2 190.4l-79.8-53.4-8.8 13.4 79.8 53.4c-5.2 7.2-10.8 14.6-16.6 22zM958.4 551c-0.4 3.6-0.6 7.2-1 10.8l1-10.8zM888.4 755.2c-2.4 3.6-4.8 7.2-7.2 10.8l7.2-10.8z", + "M432.535 71.075l18.73 94.157-15.693 3.122-18.73-94.157 15.693-3.122z", + "M591.656 952.95l-18.73-94.157 15.693-3.122 18.73 94.157-15.693 3.122z", + "M389.628 80.89l13.939 45.931-15.31 4.646-13.939-45.931 15.31-4.646z", + "M634.434 942.887l-13.939-45.931 15.31-4.646 13.939 45.931-15.31 4.646z", + "M348.014 95.099l36.739 88.694-14.782 6.123-36.739-88.694 14.782-6.123z", + "M676.123 928.965l-36.739-88.694 14.782-6.123 36.739 88.694-14.782 6.123z", + "M293.62 120.659l14.11-7.544 22.632 42.331-14.11 7.544-22.632-42.331z", + "M730.101 903.289l-14.11 7.544-22.632-42.331 14.11-7.544 22.632 42.331z", + "M120.601 293.826l42.336 22.622-7.541 14.112-42.336-22.622 7.541-14.112z", + "M903.244 730.195l-42.336-22.622 7.541-14.112 42.336 22.622-7.541 14.112z", + "M183.811 384.623l-88.694-36.739 6.123-14.782 88.694 36.739-6.123 14.782z", + "M840.32 639.301l88.694 36.739-6.123 14.782-88.694-36.739 6.123-14.782z", + "M85.543 374.387l45.936 13.93-4.643 15.312-45.936-13.93 4.643-15.312z", + "M938.308 649.667l-45.936-13.93 4.643-15.312 45.936 13.93-4.643 15.312z", + "M74.069 416.782l94.157 18.73-3.122 15.693-94.157-18.73 3.122-15.693z", + "M949.741 607.243l-94.157-18.73 3.122-15.693 94.157 18.73-3.122 15.693z", + "M70.965 591.548l94.157-18.73 3.122 15.693-94.157 18.73-3.122-15.693z", + "M952.842 432.427l-94.157 18.73-3.122-15.693 94.157-18.73 3.122 15.693z", + "M80.974 634.514l45.931-13.939 4.646 15.31-45.931 13.939-4.646-15.31z", + "M942.969 389.707l-45.931 13.939-4.646-15.31 45.931-13.939 4.646 15.31z", + "M101.142 690.912l-6.123-14.782 88.694-36.739 6.123 14.782-88.694 36.739z", + "M922.794 333.231l6.122 14.782-88.694 36.73-6.122-14.782 88.694-36.73z", + "M120.824 730.267l-7.544-14.11 42.331-22.632 7.544 14.11-42.331 22.632z", + "M903.455 293.785l7.544 14.11-42.331 22.632-7.544-14.11 42.331-22.632z", + "M307.878 910.846l-14.11-7.542 22.627-42.331 14.11 7.542-22.627 42.331z", + "M716.073 113.074l14.112 7.541-22.622 42.336-14.112-7.541 22.622-42.336z", + "M333.267 922.799l36.739-88.694 14.782 6.123-36.739 88.694-14.782-6.123z", + "M690.884 101.11l-36.739 88.694-14.782-6.123 36.739-88.694 14.782 6.123z", + "M389.634 943.028l-15.31-4.645 13.934-45.931 15.31 4.645-13.934 45.931z", + "M634.349 80.882l15.312 4.642-13.925 45.936-15.312-4.642 13.925-45.936z", + "M432.472 952.839l-15.693-3.122 18.73-94.157 15.693 3.122-18.73 94.157z", + "M591.536 70.969l15.693 3.122-18.73 94.157-15.693-3.122 18.73-94.157z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "safari", + "browser", + "internet", + "brand" + ], + "defaultCode": 60125, + "grid": 16 + }, + { + "id": 478, + "paths": [ + "M1024 512v0 0c0 151.6-66 288-170.8 381.6-131.4 64-253.8 19.2-294.2-8.8 129-28.2 226.4-184.2 226.4-372.8s-97.4-344.6-226.4-373c40.6-28 163-72.8 294.2-8.8 104.8 93.8 170.8 230.2 170.8 381.8v0 0z", + "M343.4 223.4c-56.6 66.8-93.2 165.6-95.6 276.6 0 0.2 0 23.8 0 24.2 2.4 110.8 39.2 209.6 95.8 276.4 73.4 95.4 182.6 155.8 304.6 155.8 75 0 145.2-22.8 205.2-62.6-90.8 81-210.4 130.2-341.4 130.2-8.2 0-16.4-0.2-24.4-0.6-271.4-12.8-487.6-236.8-487.6-511.4 0-282.8 229.2-512 512-512 0.6 0 1.2 0 2 0 130.4 0.4 249.2 49.6 339.4 130.4-60-39.8-130.2-62.8-205.2-62.8-122 0-231.2 60.4-304.8 155.8z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "opera", + "browser", + "internet", + "brand" + ], + "defaultCode": 60126, + "grid": 16 + }, + { + "id": 479, + "paths": [ + "M842.012 589.48c-13.648-13.446-43.914-20.566-89.972-21.172-31.178-0.344-68.702 2.402-108.17 7.928-17.674-10.198-35.892-21.294-50.188-34.658-38.462-35.916-70.568-85.772-90.576-140.594 1.304-5.12 2.414-9.62 3.448-14.212 0 0 21.666-123.060 15.932-164.666-0.792-5.706-1.276-7.362-2.808-11.796l-1.882-4.834c-5.894-13.592-17.448-27.994-35.564-27.208l-10.916-0.344c-20.202 0-36.664 10.332-40.986 25.774-13.138 48.434 0.418 120.892 24.98 214.738l-6.288 15.286c-17.588 42.876-39.63 86.060-59.078 124.158l-2.528 4.954c-20.46 40.040-39.026 74.028-55.856 102.822l-17.376 9.188c-1.264 0.668-31.044 16.418-38.028 20.644-59.256 35.38-98.524 75.542-105.038 107.416-2.072 10.17-0.53 23.186 10.014 29.212l16.806 8.458c7.292 3.652 14.978 5.502 22.854 5.502 42.206 0 91.202-52.572 158.698-170.366 77.93-25.37 166.652-46.458 244.412-58.090 59.258 33.368 132.142 56.544 178.142 56.544 8.168 0 15.212-0.78 20.932-2.294 8.822-2.336 16.258-7.368 20.792-14.194 8.926-13.432 10.734-31.932 8.312-50.876-0.72-5.622-5.21-12.574-10.068-17.32zM211.646 814.048c7.698-21.042 38.16-62.644 83.206-99.556 2.832-2.296 9.808-8.832 16.194-14.902-47.104 75.124-78.648 105.066-99.4 114.458zM478.434 199.686c13.566 0 21.284 34.194 21.924 66.254s-6.858 54.56-16.158 71.208c-7.702-24.648-11.426-63.5-11.426-88.904 0 0-0.566-48.558 5.66-48.558v0zM398.852 637.494c9.45-16.916 19.282-34.756 29.33-53.678 24.492-46.316 39.958-82.556 51.478-112.346 22.91 41.684 51.444 77.12 84.984 105.512 4.186 3.542 8.62 7.102 13.276 10.65-68.21 13.496-127.164 29.91-179.068 49.862v0zM828.902 633.652c-4.152 2.598-16.052 4.1-23.708 4.1-24.708 0-55.272-11.294-98.126-29.666 16.468-1.218 31.562-1.838 45.102-1.838 24.782 0 32.12-0.108 56.35 6.072 24.228 6.18 24.538 18.734 20.382 21.332v0z", + "M917.806 229.076c-22.21-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.886 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924v0zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.982 17.78 50.678 41.878 81.374 72.572v0zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.324 32 32 32h224v624z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-pdf", + "file", + "file-format" + ], + "defaultCode": 60127, + "grid": 16 + }, + { + "id": 480, + "paths": [ + "M690.22 471.682c-60.668-28.652-137.97-34.42-194.834 6.048 69.14-6.604 144.958 4.838 195.106 57.124 48-55.080 124.116-65.406 192.958-59.732-57.488-38.144-133.22-33.024-193.23-3.44v0zM665.646 605.75c-68.376-1.578-134.434 23.172-191.1 60.104-107.176-45.588-242.736-37.124-334.002 38.982 26.33-0.934 52.006-7.446 78.056-10.792 95.182-9.488 196.588 14.142 268.512 79.824 29.772-43.542 71.644-78.242 119.652-99.922 63.074-30.52 134.16-33.684 202.82-34.52-41.688-28.648-94.614-33.954-143.938-33.676z", + "M917.806 229.076c-22.21-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.886 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924v0zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.982 17.78 50.678 41.878 81.374 72.572v0zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.324 32 32 32h224v624z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-openoffice", + "file", + "file-format" + ], + "defaultCode": 60128, + "grid": 16 + }, + { + "id": 481, + "paths": [ + "M639.778 475.892h44.21l-51.012 226.178-66.324-318.010h-106.55l-77.114 318.010-57.816-318.010h-111.394l113.092 511.88h108.838l76.294-302.708 68.256 302.708h100.336l129.628-511.88h-170.446v91.832z", + "M917.806 229.076c-22.21-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.886 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924v0zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.982 17.78 50.678 41.878 81.374 72.572v0zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.324 32 32 32h224v624z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-word", + "file", + "file-format", + "word", + "docx" + ], + "defaultCode": 60129, + "grid": 16 + }, + { + "id": 482, + "paths": [ + "M743.028 384h-135.292l-95.732 141.032-95.742-141.032h-135.29l162.162 242.464-182.972 269.536h251.838v-91.576h-50.156l50.156-74.994 111.396 166.57h140.444l-182.976-269.536 162.164-242.464z", + "M917.806 229.076c-22.21-30.292-53.174-65.7-87.178-99.704s-69.412-64.964-99.704-87.178c-51.574-37.82-76.592-42.194-90.924-42.194h-496c-44.112 0-80 35.888-80 80v864c0 44.112 35.886 80 80 80h736c44.112 0 80-35.888 80-80v-624c0-14.332-4.372-39.35-42.194-90.924v0zM785.374 174.626c30.7 30.7 54.8 58.398 72.58 81.374h-153.954v-153.946c22.982 17.78 50.678 41.878 81.374 72.572v0zM896 944c0 8.672-7.328 16-16 16h-736c-8.672 0-16-7.328-16-16v-864c0-8.672 7.328-16 16-16 0 0 495.956-0.002 496 0v224c0 17.672 14.324 32 32 32h224v624z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "file-excel", + "file", + "file-format", + "xlc" + ], + "defaultCode": 60130, + "grid": 16 + }, + { + "id": 483, + "paths": [ + "M534.626 22.628c-12.444-12.444-37.026-22.628-54.626-22.628h-384c-17.6 0-32 14.4-32 32v960c0 17.6 14.4 32 32 32h768c17.6 0 32-14.4 32-32v-576c0-17.6-10.182-42.182-22.626-54.626l-338.748-338.746zM832 960h-704v-896h351.158c2.916 0.48 8.408 2.754 10.81 4.478l337.556 337.554c1.722 2.402 3.996 7.894 4.476 10.81v543.158zM864 0h-192c-17.6 0-21.818 10.182-9.374 22.626l210.746 210.746c12.446 12.446 22.628 8.228 22.628-9.372v-192c0-17.6-14.4-32-32-32z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "libreoffice", + "file", + "file-format" + ], + "defaultCode": 60131, + "grid": 16 + }, + { + "id": 484, + "paths": [ + "M60.538 0l82.144 921.63 368.756 102.37 369.724-102.524 82.3-921.476h-902.924zM784.63 301.428h-432.54l10.302 115.75h411.968l-31.042 347.010-231.844 64.254-231.572-64.254-15.83-177.512h113.494l8.048 90.232 125.862 33.916 0.278-0.078 125.934-33.992 13.070-146.55h-391.74l-30.494-341.8h566.214l-10.108 113.024z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "html-five", + "w3c" + ], + "defaultCode": 60132, + "grid": 16 + }, + { + "id": 485, + "paths": [ + "M60.538 0l82.144 921.63 368.756 102.37 369.724-102.524 82.3-921.476h-902.924zM810.762 862.824l-297.226 82.376v0.466l-0.776-0.234-0.782 0.234v-0.466l-297.222-82.376-70.242-787.486h736.496l-70.248 787.486zM650.754 530.204l-13.070 146.552-126.21 34.070-125.862-33.916-8.050-90.234h-113.49l15.83 177.512 232.076 64.176 231.342-64.176 31.040-347.012h-411.966l-10.302-115.748h432.534l10.112-113.026h-566.218l30.498 341.802z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "html-five", + "w3c" + ], + "defaultCode": 60133, + "grid": 16 + }, + { + "id": 486, + "paths": [ + "M152.388 48.522l-34.36 171.926h699.748l-21.884 111.054h-700.188l-33.892 171.898h699.684l-39.018 196.064-282.012 93.422-244.4-93.422 16.728-85.042h-171.898l-40.896 206.352 404.226 154.704 466.006-154.704 153.768-772.252z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "css3", + "w3c" + ], + "defaultCode": 60134, + "grid": 16 + }, + { + "id": 487, + "paths": [ + "M1004.692 466.394l-447.096-447.080c-25.738-25.754-67.496-25.754-93.268 0l-103.882 103.876 78.17 78.17c12.532-5.996 26.564-9.36 41.384-9.36 53.020 0 96 42.98 96 96 0 14.82-3.364 28.854-9.362 41.386l127.976 127.974c12.532-5.996 26.566-9.36 41.386-9.36 53.020 0 96 42.98 96 96s-42.98 96-96 96-96-42.98-96-96c0-14.82 3.364-28.854 9.362-41.386l-127.976-127.974c-3.042 1.456-6.176 2.742-9.384 3.876v266.968c37.282 13.182 64 48.718 64 90.516 0 53.020-42.98 96-96 96s-96-42.98-96-96c0-41.796 26.718-77.334 64-90.516v-266.968c-37.282-13.18-64-48.72-64-90.516 0-14.82 3.364-28.852 9.36-41.384l-78.17-78.17-295.892 295.876c-25.75 25.776-25.75 67.534 0 93.288l447.12 447.080c25.738 25.75 67.484 25.75 93.268 0l445.006-445.006c25.758-25.762 25.758-67.54-0.002-93.29z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "git", + "brand" + ], + "defaultCode": 60135, + "grid": 16 + }, + { + "id": 488, + "paths": [ + "M945.75 368.042l-448-298.666c-10.748-7.166-24.752-7.166-35.5 0l-448 298.666c-8.902 5.934-14.25 15.926-14.25 26.624v298.666c0 10.7 5.348 20.692 14.25 26.624l448 298.666c5.374 3.584 11.562 5.376 17.75 5.376s12.376-1.792 17.75-5.376l448-298.666c8.902-5.934 14.25-15.926 14.25-26.624v-298.666c0-10.698-5.348-20.69-14.25-26.624zM480 654.876l-166.312-110.876 166.312-110.874 166.312 110.874-166.312 110.876zM512 377.542v-221.75l358.31 238.876-166.31 110.874-192-128zM448 377.542l-192 128-166.312-110.874 358.312-238.876v221.75zM198.312 544l-134.312 89.542v-179.082l134.312 89.54zM256 582.458l192 128v221.748l-358.312-238.872 166.312-110.876zM512 710.458l192-128 166.312 110.876-358.312 238.874v-221.75zM761.688 544l134.312-89.54v179.084l-134.312-89.544z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "codepen", + "brand" + ], + "defaultCode": 60136, + "grid": 16 + }, + { + "id": 489, + "paths": [ + "M928 416c-28.428 0-53.958 12.366-71.536 32h-189.956l134.318-134.318c26.312 1.456 53.11-7.854 73.21-27.956 37.49-37.49 37.49-98.274 0-135.764s-98.274-37.49-135.766 0c-20.102 20.102-29.41 46.898-27.956 73.21l-134.314 134.318v-189.954c19.634-17.578 32-43.108 32-71.536 0-53.020-42.98-96-96-96s-96 42.98-96 96c0 28.428 12.366 53.958 32 71.536v189.954l-134.318-134.318c1.454-26.312-7.856-53.11-27.958-73.21-37.49-37.49-98.274-37.49-135.764 0-37.49 37.492-37.49 98.274 0 135.764 20.102 20.102 46.898 29.412 73.212 27.956l134.32 134.318h-189.956c-17.578-19.634-43.108-32-71.536-32-53.020 0-96 42.98-96 96s42.98 96 96 96c28.428 0 53.958-12.366 71.536-32h189.956l-134.318 134.318c-26.314-1.456-53.11 7.854-73.212 27.956-37.49 37.492-37.49 98.276 0 135.766 37.492 37.49 98.274 37.49 135.764 0 20.102-20.102 29.412-46.898 27.958-73.21l134.316-134.32v189.956c-19.634 17.576-32 43.108-32 71.536 0 53.020 42.98 96 96 96s96-42.98 96-96c0-28.428-12.366-53.958-32-71.536v-189.956l134.318 134.318c-1.456 26.312 7.854 53.11 27.956 73.21 37.492 37.49 98.276 37.49 135.766 0s37.49-98.274 0-135.766c-20.102-20.102-46.898-29.41-73.21-27.956l-134.32-134.316h189.956c17.576 19.634 43.108 32 71.536 32 53.020 0 96-42.98 96-96s-42.982-96-96.002-96z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "svg" + ], + "defaultCode": 60137, + "grid": 16 + }, + { + "id": 490, + "paths": [ + "M259.544 511.998c0-65.416 53.030-118.446 118.446-118.446s118.446 53.030 118.446 118.446c0 65.416-53.030 118.446-118.446 118.446s-118.446-53.030-118.446-118.446zM512.004 0c-282.774 0-512.004 229.232-512.004 512s229.226 512 512.004 512c282.764 0 511.996-229.23 511.996-512 0-282.768-229.23-512-511.996-512zM379.396 959.282c-153.956-89.574-257.468-256.324-257.468-447.282s103.512-357.708 257.462-447.282c154.010 89.562 257.59 256.288 257.59 447.282 0 190.988-103.58 357.718-257.584 447.282z" + ], + "attrs": [], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "IcoMoon", + "icomoon", + "brand" + ], + "defaultCode": 60138, + "grid": 16 + } + ] + }, + { + "selection": [ + { + "order": 2, + "id": 0, + "name": "database", + "prevSize": 32, + "code": 59649, + "tempChar": "" + } + ], + "id": 2, + "metadata": { + "name": "Plugin Icons", + "importSize": { + "width": 16, + "height": 18 + } + }, + "height": 1024, + "prevSize": 32, + "icons": [ + { + "id": 0, + "paths": [ + "M455.111 0c-251.449 0-455.111 101.831-455.111 227.556s203.662 227.556 455.111 227.556 455.111-101.831 455.111-227.556-203.662-227.556-455.111-227.556zM0 341.333v170.667c0 125.724 203.662 227.556 455.111 227.556s455.111-101.831 455.111-227.556v-170.667c0 125.724-203.662 227.556-455.111 227.556s-455.111-101.831-455.111-227.556zM0 625.778v170.667c0 125.724 203.662 227.556 455.111 227.556s455.111-101.831 455.111-227.556v-170.667c0 125.724-203.662 227.556-455.111 227.556s-455.111-101.831-455.111-227.556z" + ], + "attrs": [ + {} + ], + "width": 910, + "isMulticolor": false, + "isMulticolor2": false, + "grid": 0, + "tags": [ + "plugins" + ] + } + ], + "invisible": false, + "colorThemes": [] + } + ], + "preferences": { + "showGlyphs": true, + "showQuickUse": true, + "showQuickUse2": true, + "showSVGs": true, + "fontPref": { + "prefix": "plugicon-", + "metadata": { + "fontFamily": "pluginicons", + "majorVersion": 1, + "minorVersion": 0 + }, + "metrics": { + "emSize": 1024, + "baseline": 6.25, + "whitespace": 50 + }, + "embed": false, + "showSelector": false, + "showMetrics": false, + "showMetadata": false, + "showVersion": false + }, + "imagePref": { + "prefix": "icon-", + "png": true, + "useClassSelector": true, + "color": 0, + "bgColor": 16777215, + "classSelector": ".icon" + }, + "historySize": 50, + "showCodes": true, + "gridSize": 16 + }, + "uid": -1 +} \ No newline at end of file diff --git a/assets/fonts/README.md b/assets/fonts/README.md new file mode 100644 index 00000000000..b6b98ea4322 --- /dev/null +++ b/assets/fonts/README.md @@ -0,0 +1,29 @@ +How to upgrade icons in icon-fonts on Nightscout +================================================ + +This guide is fol developers regarding how to add new icon to Nightscout. + +Nightscout use icon fonts to render icons. Each icon is glyph (like - letter, or more like emoji character) inside custom made font file. +That way we have nice, vector icons, that are small, scalable, looks good on each platform, and are easy to embed inside CSS. + +To extend existing icon set.: + +1. Prepare minimalist, black & white icon in SVG tool of choice, and optimize it (you can use Inkscape) to be small in size and render good at small sizes. +2. Use https://icomoon.io/app and import accompanied JSON project file (`Nightscout Plugin Icons.json`) +3. Add SVG as new glyph. Remember to take care to set proper character code and CSS name +4. Save new version of JSON project file and store in this folder +5. Generate font, download zip file and unpack it to get `fonts/pluginicons.svg` and `fonts/pluginicons.woff` +6. Update `statc/css/main.css` file + * In section of `@font-face` with `font-family: 'pluginicons'` + * update part after `data:application/font-woff;charset=utf-8;base64,` with Base64-encoded content of just generated `pluginicons.woff` font + * update part after `data:application/font-svg;charset=utf-8;base64,` with Base64-encoded content of just generated `pluginicons.svg` font + * copy/update all entries `.plugicon-****:before { content: "****"; }` from generated font `style.css` into `statc/css/main.css` +7. Do not forget to update `Nightscout Plugin Icons.json` in this repo (´download updated project from icomoon.io) + +Hints +----- + +* You can find many useful online tools to encode file into Base64, like: https://base64.guru/converter/encode/file +* Do not split Base64 output - it should be one LONG line +* Since update process is **manual** and generated fonts & updated CSS sections are **binary** - try to avoid **git merge conflicts** by speaking with other developers if you plan to add new icon +* When in doubt - check `git log` and reach last contributor for guidelines :) diff --git a/azuredeploy.json b/azuredeploy.json new file mode 100644 index 00000000000..1ec7524c56c --- /dev/null +++ b/azuredeploy.json @@ -0,0 +1,343 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "type": "string" + }, + "hostingPlanName": { + "type": "string" + }, + "siteLocation": { + "type": "string" + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + }, + "repoUrl": { + "type": "string" + + }, + "branch": { + "type": "string" + }, + "mongoConnection": { + "type": "string" + }, + "mongoCollection": { + "type": "string", + "defaultValue": "entries" + }, + "displayUnits": { + "type": "string", + "allowedValues": [ + "mg/dl", + "mmol" + ], + "defaultValue": "mg/dl" + }, + "apiSecret": { + "type": "string", + "minLength": "12", + "defaultValue": "Enter an API secret. Must be at least 12 characters" + }, + "theme": { + "type": "string", + "allowedValues": [ + "default", + "colors", + "colorblindfriendly" + ], + "defaultValue": "colors" + }, + "time_format": { + "type": "string", + "allowedValues": [ + "12", + "24" + ], + "defaultValue": "24" + }, + "language": { + "type": "string", + "allowedValues": [ + "bg", + "cs", + "dk", + "de", + "el", + "en", + "es", + "fr", + "he", + "hr", + "it", + "nl", + "nb", + "pl", + "pt", + "ro", + "ru", + "sk", + "sv", + "fi", + "ko" + ], + "defaultValue": "en" + }, + "custom_title": { + "type": "string", + "defaultValue": "Nightscout" + }, + "alarm_high": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off" + }, + "alarm_low": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off" + }, + "alarm_timeago_urgent": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off" + }, + "alarm_timeago_warn": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off" + }, + "alarm_urgent_high": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off" + }, + "alarm_urgent_low": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off" + }, + "basal_render": { + "type": "string", + "allowedValues": [ + "none", + "default", + "icicle" + ], + "defaultValue": "none" + }, + "scale_y": { + "type": "string", + "allowedValues": [ + "log", + "linear", + "log-dynamic" + ], + "defaultValue": "log" + }, + "enable": { + "type": "string", + "defaultValue": "basal bwp cage careportal iob cob rawbg sage iage treatmentnotify boluscalc profile food dbsize" + }, + "night_mode": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off" + }, + "show_plugins": { + "type": "string", + "defaultValue": "careportal dbsize" + }, + "show_rawbg": { + "type": "string", + "allowedValues": [ + "always", + "never", + "noise" + ], + "defaultValue": "never" + }, + "devicestatus_advanced": { + "type": "string", + "allowedValues": [ + "true", + "false" + ], + "defaultValue": "false" + }, + "profile_multiple": { + "type": "string", + "allowedValues": [ + "on", + "off" + ], + "defaultValue": "off" + }, + "auth_default_roles": { + "type": "string", + "defaultValue": "readable devicestatus-upload" + }, + "WEBSITE_NODE_DEFAULT_VERSION": { + "type": "string", + "defaultValue": "16.16.0" + } + }, + "resources": [{ + "apiVersion": "2015-04-01", + "name": "[parameters('hostingPlanName')]", + "type": "Microsoft.Web/serverFarms", + "location": "[parameters('siteLocation')]", + "properties": { + "sku": "[parameters('sku')]", + "workerSize": "[parameters('workerSize')]", + "numberOfWorkers": 1 + } + }, { + "apiVersion": "2015-08-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/Sites", + "location": "[parameters('siteLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty" + }, + "properties": { + "serverFarmId": "[parameters('hostingPlanName')]", + "siteConfig": { + "appSettings": [{ + "name": "MONGO_CONNECTION", + "value": "[parameters('mongoConnection')]" + }, + { + "name": "MONGO_COLLECTION", + "value": "[parameters('mongoCollection')]" + }, { + "name": "DISPLAY_UNITS", + "value": "[parameters('displayUnits')]" + }, { + "name": "API_SECRET", + "value": "[parameters('apiSecret')]" + }, { + "name": "THEME", + "value": "[parameters('theme')]" + }, { + "name": "TIME_FORMAT", + "value": "[parameters('time_format')]" + }, { + "name": "LANGUAGE", + "value": "[parameters('language')]" + }, { + "name": "CUSTOM_TITLE", + "value": "[parameters('custom_title')]" + }, { + "name": "ALARM_HIGH", + "value": "[parameters('alarm_high')]" + }, { + "name": "ALARM_LOW", + "value": "[parameters('alarm_low')]" + }, { + "name": "ALARM_TIMEAGO_URGENT", + "value": "[parameters('alarm_timeago_urgent')]" + }, { + "name": "ALARM_TIMEAGO_WARN", + "value": "[parameters('alarm_timeago_warn')]" + }, { + "name": "ALARM_URGENT_HIGH", + "value": "[parameters('alarm_urgent_high')]" + }, { + "name": "ALARM_URGENT_LOW", + "value": "[parameters('alarm_urgent_low')]" + }, { + "name": "BASAL_RENDER", + "value": "[parameters('basal_render')]" + }, { + "name": "SCALE_Y", + "value": "[parameters('scale_y')]" + }, { + "name": "ENABLE", + "value": "[parameters('enable')]" + }, { + "name": "NIGHT_MODE", + "value": "[parameters('night_mode')]" + }, { + "name": "SHOW_PLUGINS", + "value": "[parameters('show_plugins')]" + }, { + "name": "SHOW_RAWBG", + "value": "[parameters('show_rawbg')]" + }, { + "name": "DEVICESTATUS_ADVANCED", + "value": "[parameters('devicestatus_advanced')]" + }, { + "name": "PROFILE_MULTIPLE", + "value": "[parameters('profile_multiple')]" + }, { + "name": "AUTH_DEFAULT_ROLES", + "value": "[parameters('auth_default_roles')]" + }, { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "8.11.1" + }, { + "name": "SCM_COMMAND_IDLE_TIMEOUT", + "value": "300" + } + + ] + } + }, + "resources": [{ + "apiVersion": "2015-08-01", + "name": "web", + "type": "sourcecontrols", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" + ], + "properties": { + "RepoUrl": "[parameters('repoUrl')]", + "branch": "[parameters('branch')]", + "IsManualIntegration": true + } + }] + }] +} diff --git a/deploy.sh b/bin/azure-deploy.sh similarity index 91% rename from deploy.sh rename to bin/azure-deploy.sh index 29fbdc49d2b..5ffbee022ab 100755 --- a/deploy.sh +++ b/bin/azure-deploy.sh @@ -78,7 +78,7 @@ selectNodeVersion () { exitWithMessageOnError "getting node version failed" fi - if [[ -e "$DEPLOYMENT_TEMP/.tmp" ]]; then + if [[ -e "$DEPLOYMENT_TEMP/__npmVersion.tmp" ]]; then NPM_JS_PATH=`cat "$DEPLOYMENT_TEMP/__npmVersion.tmp"` exitWithMessageOnError "getting npm version failed" fi @@ -103,7 +103,7 @@ echo "\"$SCM_COMMIT_ID\"" > $DEPLOYMENT_SOURCE/scm-commit-id.json # 1. KuduSync if [[ "$IN_PLACE_DEPLOYMENT" -ne "1" ]]; then - "$KUDU_SYNC_CMD" -v 50 -f "$DEPLOYMENT_SOURCE" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;deploy.sh" + "$KUDU_SYNC_CMD" -v 50 -f "$DEPLOYMENT_SOURCE" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;bin/azure-deploy.sh" exitWithMessageOnError "Kudu Sync failed" fi @@ -113,7 +113,10 @@ selectNodeVersion # 3. Install npm packages if [ -e "$DEPLOYMENT_TARGET/package.json" ]; then cd "$DEPLOYMENT_TARGET" - eval $NPM_CMD install --production + echo Installing webpack and webpack-command and yargs + eval $NPM_CMD install -g webpack webpack-command + eval $NPM_CMD install yargs + eval $NPM_CMD install --production --scripts-prepend-node-path exitWithMessageOnError "npm failed" cd - > /dev/null fi diff --git a/bin/generateCacheBuster.js b/bin/generateCacheBuster.js new file mode 100644 index 00000000000..02058daf844 --- /dev/null +++ b/bin/generateCacheBuster.js @@ -0,0 +1,4 @@ + +var randomToken = require('random-token'); +var token = randomToken(16); +console.log(token); diff --git a/bin/generateRandomString.js b/bin/generateRandomString.js new file mode 100644 index 00000000000..deb307ab26d --- /dev/null +++ b/bin/generateRandomString.js @@ -0,0 +1,5 @@ + +require('crypto').randomBytes(1024, function(err, buffer) { + var token = buffer.toString('hex'); + console.log(token); +}); diff --git a/bin/post-sgv.sh b/bin/post-sgv.sh index 9b48e5dfcc9..354637525e8 100755 --- a/bin/post-sgv.sh +++ b/bin/post-sgv.sh @@ -1,9 +1,10 @@ #!/bin/sh -# "date": "1413782506964" +# Date is epoch in nanosecods...ie linux echo $(($(date +%s%N)/1000000)) +# $API_SECRET needs to be a hashed value of your secret key...ie linux echo -n "" | sha1sum curl -H "Content-Type: application/json" -H "api-secret: $API_SECRET" -XPOST 'http://localhost:1337/api/v1/entries/' -d '{ "sgv": 100, "type": "sgv", "direction": "Flat", - "date": "1415950912800" + "date": 1449872210706 }' diff --git a/prep_repo_release.sh b/bin/prep_repo_release.sh similarity index 100% rename from prep_repo_release.sh rename to bin/prep_repo_release.sh diff --git a/bin/setup.sh b/bin/setup.sh new file mode 100755 index 00000000000..d77c8f3d1de --- /dev/null +++ b/bin/setup.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - +sudo apt-get update +sudo apt-get install -y nodejs build-essential git + +npm install \ No newline at end of file diff --git a/bin/testdatarunner.js b/bin/testdatarunner.js new file mode 100644 index 00000000000..a5043a67c83 --- /dev/null +++ b/bin/testdatarunner.js @@ -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 '); + 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); + } + })(); + diff --git a/bower.json b/bower.json deleted file mode 100644 index b51c0e2cdad..00000000000 --- a/bower.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "nightscout", - "version": "0.8.2-beta2", - "dependencies": { - "jquery": "2.1.0", - "jQuery-Storage-API": "~1.7.2", - "tipsy-jmalonzo": "~1.0.1", - "jquery-ui": "~1.11.3", - "jquery-flot": "0.8.3", - "swagger-ui": "~2.1.2" - }, - "resolutions": { - "jquery": "2.1.0" - } -} diff --git a/bundle/bundle.clocks.source.js b/bundle/bundle.clocks.source.js new file mode 100644 index 00000000000..a200f467330 --- /dev/null +++ b/bundle/bundle.clocks.source.js @@ -0,0 +1,9 @@ + +$ = require("jquery"); + +window.Nightscout = { + client: require('../lib/client/clock-client'), + units: require('../lib/units')(), +}; + +console.info('Nightscout clock bundle ready'); \ No newline at end of file diff --git a/bundle/bundle.reports.source.js b/bundle/bundle.reports.source.js new file mode 100644 index 00000000000..265cf87e34b --- /dev/null +++ b/bundle/bundle.reports.source.js @@ -0,0 +1,16 @@ +import './bundle.source'; + +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'); + +// Needed for Hot Module Replacement +if(typeof(module.hot) !== 'undefined') { + module.hot.accept() +} diff --git a/bundle/bundle.source.js b/bundle/bundle.source.js index d78ecb3c868..2504c9bfed5 100644 --- a/bundle/bundle.source.js +++ b/bundle/bundle.source.js @@ -1,19 +1,48 @@ -(function () { +import '../static/css/drawer.css'; +import '../static/css/dropdown.css'; +import '../static/css/sgv.css'; - window._ = require('lodash'); - window.$ = window.jQuery = require('jquery'); - window.moment = require('moment-timezone'); - window.Nightscout = window.Nightscout || {}; +$ = require("jquery"); - window.Nightscout = { - client: require('../lib/client') - , units: require('../lib/units')() - , plugins: require('../lib/plugins/')().registerClientDefaults() - , report_plugins: require('../lib/report_plugins/')() - , admin_plugins: require('../lib/admin_plugins/')() - }; +require('jquery-ui-bundle'); - console.info('Nightscout bundle ready'); +window._ = require('lodash'); +window.d3 = require('d3'); -})(); +require('jquery.tooltips'); +window.Storage = require('js-storage'); + +require('flot'); +require('../node_modules/flot/jquery.flot.time'); +require('../node_modules/flot/jquery.flot.pie'); +require('../node_modules/flot/jquery.flot.fillbetween'); + +const moment = require('moment-timezone'); + +window.moment = moment; + +window.Nightscout = window.Nightscout || {}; + +var ctx = { + moment: moment +}; + +window.Nightscout = { + client: require('../lib/client'), + units: require('../lib/units')(), + admin_plugins: require('../lib/admin_plugins/')(ctx) +}; + +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 +if(typeof(module.hot) !== 'undefined') { + module.hot.accept() +} diff --git a/bundle/index.js b/bundle/index.js deleted file mode 100644 index 4a8cd1563f2..00000000000 --- a/bundle/index.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -var browserify_express = require('browserify-express'); - -function bundle() { - return browserify_express({ - entry: __dirname + '/bundle.source.js', - watch: __dirname + '/../lib/', - mount: '/public/js/bundle.js', - verbose: true, - //minify: true, - bundle_opts: { debug: true }, // enable inline sourcemap on js files - write_file: __dirname + '/bundle.out.js' - }); -} - -module.exports = bundle; \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000000..fb25b791ec4 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /translations/en/*.json + translation: /translations/%locale_with_underscore%.json diff --git a/database_configuration.json b/database_configuration.json deleted file mode 100644 index d768358bbb1..00000000000 --- a/database_configuration.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "url": null, - "collection": null -} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..6e0e5dd1ab5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,86 @@ +version: '3' + +x-logging: + &default-logging + options: + max-size: '10m' + max-file: '5' + driver: json-file + +services: + mongo: + image: mongo:4.4 + volumes: + - ${NS_MONGO_DATA_DIR:-./mongo-data}:/data/db:cached + logging: *default-logging + + nightscout: + image: nightscout/cgm-remote-monitor:latest + container_name: nightscout + restart: always + depends_on: + - mongo + labels: + - 'traefik.enable=true' + # Change the below Host from `localhost` to be the web address where Nightscout is running. + # Also change the email address in the `traefik` service below. + - 'traefik.http.routers.nightscout.rule=Host(`localhost`)' + - 'traefik.http.routers.nightscout.entrypoints=websecure' + - 'traefik.http.routers.nightscout.tls.certresolver=le' + logging: *default-logging + environment: + ### Variables for the container + NODE_ENV: production + TZ: Etc/UTC + + ### Overridden variables for Docker Compose setup + # The `nightscout` service can use HTTP, because we use `traefik` to serve the HTTPS + # and manage TLS certificates + INSECURE_USE_HTTP: 'true' + + # For all other settings, please refer to the Environment section of the README + ### Required variables + # MONGO_CONNECTION - The connection string for your Mongo database. + # Something like mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout + # The default connects to the `mongo` included in this docker-compose file. + # If you change it, you probably also want to comment out the entire `mongo` service block + # and `depends_on` block above. + MONGO_CONNECTION: mongodb://mongo:27017/nightscout + + # API_SECRET - A secret passphrase that must be at least 12 characters long. + API_SECRET: change_me + + ### Features + # ENABLE - Used to enable optional features, expects a space delimited list, such as: careportal rawbg iob + # See https://github.com/nightscout/cgm-remote-monitor#plugins for details + ENABLE: careportal rawbg iob + + # AUTH_DEFAULT_ROLES (readable) - possible values readable, denied, or any valid role name. + # When readable, anyone can view Nightscout without a token. Setting it to denied will require + # a token from every visit, using status-only will enable api-secret based login. + AUTH_DEFAULT_ROLES: denied + + # For all other settings, please refer to the Environment section of the README + # https://github.com/nightscout/cgm-remote-monitor#environment + + traefik: + image: traefik:latest + container_name: 'traefik' + command: + - '--providers.docker=true' + - '--providers.docker.exposedbydefault=false' + - '--entrypoints.web.address=:80' + - '--entrypoints.web.http.redirections.entrypoint.to=websecure' + - '--entrypoints.websecure.address=:443' + - "--certificatesresolvers.le.acme.httpchallenge=true" + - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web" + - '--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json' + # Change the below to match your email address + - '--certificatesresolvers.le.acme.email=example@example.com' + ports: + - '443:443' + - '80:80' + volumes: + - './letsencrypt:/letsencrypt' + - '/var/run/docker.sock:/var/run/docker.sock:ro' + logging: *default-logging diff --git a/docs/example-template.env b/docs/example-template.env new file mode 100644 index 00000000000..da73b065fb2 --- /dev/null +++ b/docs/example-template.env @@ -0,0 +1,15 @@ +CUSTOMCONNSTR_mongo=mongodb://.... +CUSTOMCONNSTR_mongo_collection= +MONGO_PROFILE_COLLECTION= +API_SECRET=1234567890abc +HOSTNAME=0.0.0.0 +ENABLE="devicestatus rawbg upbat careportal iob profile cage bage avg cob basal treatments sage boluscalc pump openaps iage speech" +SHOW_PLUGINS="rawbg-on careportal upbat iob profile cage cob basal avg treatments boluscalc pump openaps iage speech" +DISPLAY_UNITS="mmol" +TIME_FORMAT=24 +ALARM_TYPES="predict" +LANGUAGE=en +INSECURE_USE_HTTP=true +PORT=1337 +NODE_ENV=development +AUTH_FAIL_DELAY=50 \ No newline at end of file diff --git a/docs/issue_template.md b/docs/issue_template.md new file mode 100644 index 00000000000..e3ae7d24d3e --- /dev/null +++ b/docs/issue_template.md @@ -0,0 +1,11 @@ +Please include the following information with your bug report: + +Is this a new bug with the latest release, have you seen this earlier? + +Which device / browser version was used to reproduce the bug? + +Does this happen every time you launch Nightscout, or sometimes? + +Steps how to reproduce (if you can’t reproduce the issue, please don’t report the issue / if we can't reproduce the bug, we can't fix) + +Please include 1 or more screenshots of the issue appearing, ideally with an annotation on what's wrong diff --git a/docs/plugins/add-virtual-assistant-support-to-plugin.md b/docs/plugins/add-virtual-assistant-support-to-plugin.md new file mode 100644 index 00000000000..6c581013b9e --- /dev/null +++ b/docs/plugins/add-virtual-assistant-support-to-plugin.md @@ -0,0 +1,61 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Adding Virtual Assistant Support to a Plugin](#adding-virtual-assistant-support-to-a-plugin) + - [Intent Handlers](#intent-handlers) + - [Rollup handlers](#rollup-handlers) + + + +Adding Virtual Assistant Support to a Plugin +========================================= + +To add virtual assistant support to a plugin, the `init` method of the plugin should return an object that contains a `virtAsst` key. Here is an example: + +```javascript +iob.virtAsst = { + intentHandlers: [{ + intent: "MetricNow" + , metrics: ["iob"] + , intentHandler: virtAsstIOBIntentHandler + }] + , rollupHandlers: [{ + rollupGroup: "Status" + , rollupName: "current iob" + , rollupHandler: virtAsstIOBRollupHandler + }] +}; +``` + +There are 2 types of handlers that you can supply: +* Intent handler - Enables you to "teach" the virtual assistant how to respond to a user's question. +* A rollup handler - Enables you to create a command that aggregates information from multiple plugins. This would be akin to the a "flash briefing". An example would be a status report that contains your current bg, iob, and your current basal. + +### Intent Handlers + +A plugin can expose multiple intent handlers (e.g. useful when it can supply multiple kinds of metrics). Each intent handler should be structured as follows: ++ `intent` - This is the intent this handler is built for. Right now, the templates used by both Alexa and Google Home use only the `"MetricNow"` intent (used for getting the present value of the requested metric) ++ `metrics` - An array of metric name(s) the handler will supply. e.g. "What is my `metric`" - iob, bg, cob, etc. Although this value *is* an array, you really should only supply one (unique) value, and then add aliases or synonyms to that value in the list of metrics for the virtual assistant. We keep this value as an array for backwards compatibility. + - **IMPORTANT NOTE:** There is no protection against overlapping metric names, so PLEASE make sure your metric name is unique! ++ `intenthandler` - This is a callback function that receives 3 arguments: + - `callback` Call this at the end of your function. It requires 2 arguments: + - `title` - Title of the handler. This is the value that will be displayed on the Alexa card (for devices with a screen). The Google Home response doesn't currently display a card, so it doesn't use this value. + - `text` - This is text that the virtual assistant should speak (and show, for devices with a screen). + - `slots` - These are the slots (Alexa) or parameters (Google Home) that the virtual assistant detected (e.g. `pwd` as seen in the templates is a slot/parameter. `metric` is technically a slot, too). + - `sandbox` - This is the Nightscout sandbox that allows access to various functions. + +### Rollup handlers + +A plugin can also expose multiple rollup handlers ++ `rollupGroup` - This is the key that is used to aggregate the responses when the intent is invoked ++ `rollupName` - This is the name of the handler. Primarily used for debugging ++ `rollupHandler` - This is a callback function that receives 3 arguments + - `slots` - These are the values of the slots. Make sure to add these values to the appropriate custom slot + - `sandbox` - This is the nightscout sandbox that allows access to various functions. + - `callback` - + - `error` - This would be an error message + - `response` - A simple object that expects a `results` string and a `priority` integer. Results should be the text (speech) that is added to the rollup and priority affects where in the rollup the text should be added. The lowest priority is spoken first. An example callback: + ```javascript + callback(null, {results: "Hello world", priority: 1}); + ``` diff --git a/docs/plugins/alexa-plugin.md b/docs/plugins/alexa-plugin.md new file mode 100644 index 00000000000..97f522ed632 --- /dev/null +++ b/docs/plugins/alexa-plugin.md @@ -0,0 +1,166 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Nightscout Alexa Plugin](#nightscout-alexa-plugin) + - [Overview](#overview) + - [Activate the Nightscout Alexa Plugin](#activate-the-nightscout-alexa-plugin) + - [Create Your Alexa Skill](#create-your-alexa-skill) + - [Get an Amazon Developer account](#get-an-amazon-developer-account) + - [Create a new Alexa skill](#create-a-new-alexa-skill) + - [Define the interaction model](#define-the-interaction-model) + - [Point your skill at your site](#point-your-skill-at-your-site) + - [Do you use Authentication Roles?](#do-you-use-authentication-roles) + - [Test your skill out with the test tool](#test-your-skill-out-with-the-test-tool) + - [What questions can you ask it?](#what-questions-can-you-ask-it) + - [Activate the skill on your Echo or other device](#activate-the-skill-on-your-echo-or-other-device) + - [Updating your skill with new features](#updating-your-skill-with-new-features) + - [Adding support for additional languages](#adding-support-for-additional-languages) + - [Adding Alexa support to a plugin](#adding-alexa-support-to-a-plugin) + + + +Nightscout Alexa Plugin +====================================== + +## Overview + +To add Alexa support for your Nightscout site, here's what you need to do: + +1. [Activate the `alexa` plugin](#activate-the-nightscout-alexa-plugin) on your Nightscout site, so your site will respond correctly to Alexa's requests. +1. [Create a custom Alexa skill](#create-your-alexa-skill) that points at your site and defines certain questions you want to be able to ask. (You'll copy and paste a basic template for this, to keep things simple.) + +To add Alexa support for a plugin, [check this out](#adding-alexa-support-to-a-plugin). + +## Activate the Nightscout Alexa Plugin + +1. Your Nightscout site needs to be new enough that it supports the `alexa` plugin. It needs to be [version 0.9.1 (Grilled Cheese)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/0.9.1) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version. +1. Add `alexa` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) +1. The Alexa plugin pulls its units preferences from your site's defaults. If you don't have a `DISPLAY_UNITS` entry, it will default to `mg/dl`. If you want it to use mmol/L, make sure you have a `DISPLAY_UNITS` line, and set it to `mmol` (*not* `mmol/l`). + +## Create Your Alexa Skill + +### Get an Amazon Developer account + +1. Sign up for a free [Amazon Developer account](https://developer.amazon.com/) if you don't already have one. +1. [Register](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) your Alexa-enabled device with your Developer account. +1. Sign in and go to the [Alexa developer portal](https://developer.amazon.com/alexa/console/ask). + +### Create a new Alexa skill + +1. Select "Alexa Skills Kit" in the main menu bar. +1. Click the "Start a Skill" button. This will take you to the Skills console. +1. Click the "Create Skill" button on the Skills console page. +1. Name your new skill "Nightscout" (or something else, if you like). Use English (US) as your language. Click "Next". +1. Choose a model to add to your skill. Click "Select" under "Custom" model, then click "Create skill" on the upper right. +1. Congrats! Your empty custom skill should now be created. + +### Define the interaction model + +Your Alexa skill's "interaction model" defines how your spoken questions get translated into requests to your Nightscout site, and how your Nightscout site's responses get translated into the audio responses that Alexa says back to you. + +To get up and running with an interaction model, which will allow you to ask Alexa a few basic questions about your Nightscout site, you can copy and paste the configuration code for your language from [the list of templates](alexa-templates/). + +- If you're language doesn't have a template, please consider starting with [the en-us template](alexa-templates/en-us.json), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. + +Select "JSON Editor" in the left-hand menu on your skill's edit page (which you should be on if you followed the above instructions). Replace everything in the textbox with the code from your chosen template. Then click "Save Model" at the top. A success message should appear indicating that the model was saved. + +Next you need to build your custom model. Click "Build Model" at the top of the same page. It'll take a minute to build, and then you should see another success message, "Build Successful". + +You now have a custom Alexa skill that knows how to talk to a Nightscout site. + +### Point your skill at your site + +Now you need to point your skill at *your* Nightscout site. + +1. In the left-hand menu for your skill, there's an option called "Endpoint". Click it. +1. Under "Service Endpoint Type", select "HTTPS". +1. You only need to set up the Default Region. In the box that says "Enter URI...", put in `https://{yourdomain}/api/v1/alexa`. (So if your Nightscout site is at `mynightscoutsite.herokuapp.com`, you'll enter `https://mynightscoutsite.herokuapp.com/api/v1/alexa` in the box.) + - If you use Authentication Roles, you'll need to add a bit to the end of your URL. See [the section](#do-you-use-authentication-roles) below. +1. In the dropdown under the previous box, select "My development endpoint is a sub-domain of a domain that has a wildcard certificate from a certificate authority". +1. Click the "Save Endpoints" button at the top. +1. You should see a success message pop up when the save succeeds. + +### Do you use Authentication Roles? ### + +If you use Authentication Roles, you will need to add a token to the end of your Nightscout URL when configuring your Endpoint. + +1. In your Nightscout Admin Tools, add a new subject and give it the "readable" role. + - If you **really** would like to be super specific, you could create a new role and set the permissions to `api:*:read`. +1. After the new subject is created, copy the "Access Token" value for the new row in your subject table (**don't** copy the link, just copy the text). +1. At the end of your Nighscout URL, add `?token={yourtoken}`, where `{yourtoken}` is the Access Token you just copied. Your new URL should look like `https://{yourdomain}/api/v1/alexa?token={yourtoken}`. + +### Test your skill out with the test tool + +Click on the "Test" tab on the top menu. This will take you to the page where you can test your new skill. + +Enable testing for your skill (click the toggle). As indicated on this page, when testing is enabled, you can interact with the development version of your skill in the Alexa simulator and on all devices linked to your Alexa developer account. (Your skill will always be a development version. There's no need to publish it to the public.) + +After you enable testing, you can also use the Alexa Simulator in the left column, to try out the skill. You can type in questions and see the text your skill would reply with. When typing your test question, only type what you would verbally say to an Alexa device after the wake word. (e.g. you would verbally say "Alexa, ask Nightscout how am I doing", so you would type only "ask Nightscout how am I doing") You can also hold the microphone icon to ask questions using your microphone, and you'll get the audio and text responses back. + +##### What questions can you ask it? + +See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. + +### Activate the skill on your Echo or other device + +If your device is [registered](https://developer.amazon.com/docs/devconsole/test-your-skill.html#h2_register) with your developer account, you should be able to use your skill right away. Try it by asking Alexa one of the above questions using your device. + +## Updating your skill with new features + +As more work is done on Nightscout, new ways to interact with Nighscout via Alexa may be made available. To be able to use these new features, you first will need to [update your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version), and then you can follow the steps below to update your Alexa skill. + +1. Make sure you've [updated your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) first. +1. Open [the latest skill template](alexa-templates/) in your language. You'll be copying the contents of the file later. + - If your language doesn't include the latest features you're looking for, you're help [translating those new features](#adding-support-for-additional-languages) would be greatly appreciated! +1. Sign in to the [Alexa developer portal](https://developer.amazon.com/alexa/console/ask). +1. Open your Nightscout skill. +1. Open the "JSON Editor" in the left navigation pane. +1. Select everything in the text box (Ctrl + A on Windows, Cmd + A on Mac) and delete it. +1. Copy the contents of the updated template and paste it in the text box in the JSON Editor page. +1. Click the "Save Model" button near the top of the page, and then click the "Build Model" button. +1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. If you gave your skill name something other than "night scout," you will need to go to the "Invocation" page in the left navigation pane and change the Skill Invocation Name back to your preferred name. Make sure to click the "Save Model" button followed by the "Build Model" button after you change the name. +1. Enjoy the new features! + +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. + +If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the "JSON Editor" in the left navigation pane. + +1. Open the Build tab of your Alexa Skill. + - Get to your list of Alexa Skills at https://developer.amazon.com/alexa/console/ask and click on the name of the skill. +1. Click on the language drop-down box in the upper right corner of the window. +1. Click "Language settings". +1. Add your desired language. +1. Click the "Save" button. +1. Navigate to "CUSTOM" in the left navigation pane. +1. Select your new language in the language drop-down box. +1. Go to "JSON Editor" (just above "Interfaces" in the left navigation pane). +1. Remove the existing contents in the text box, and copy and paste the configuration code from a familiar language in [the list of templates](alexa-templates/). +1. Click "Save Model". +1. Click the "Add" button next to the "Slot Types" section in the left pane. +1. Click the radio button for "Use an existing slot type from Alexa's built-in library" +1. In the search box just below that option, search for "first name" +1. If your language has an option, click the "Add Slot Type" button for that option. + - If your language doesn't have an option, you won't be able to ask Nightscout a question that includes a name. +1. For each Intent listed in the left navigation pane (e.g. "NSStatus" and "MetricNow"): + 1. Click on the Intent name. + 1. Scroll down to the "Slots" section + 1. If there's a slot with the name "pwd", change the Slot Type to the one found above. + - If you didn't find one above, you'll have to see if another language gets close enough for you, or delete the slot. + 1. If there's a slot with the name "metric", click the "Edit Dialog" link on the right. This is where you set Alexa's questions and your answers if you happen to ask a question about metrics but don't include which metric you want to know. + 1. Set the "Alexa speech prompts" in your language, and remove the old ones. + 1. Under "User utterances", set the phrases you would say in response to the questions Alexa would pose from the previous step. MAKE SURE that your example phrases include where you would say the name of the metric. You do this by typing the left brace (`{`) and then selecting `metric` in the popup. + 1. Click on the Intent name (just to the left of "metric") to return to the previous screen. + 1. For each Sample Utterance, add an equivalent phrase in your language. If the phrase you're replacing has a `metric` slot, make sure to include that in your replacement phrase. Same goes for the `pwd` slot, unless you had to delete that slot a couple steps ago, in which case you need to modify the phrase to not use a first name, or not make a replacement phrase. After you've entered your replacement phrase, delete the phrase you're replacing. +1. Navigate to the "LIST_OF_METRICS" under the Slot Types section. +1. For each metric listed, add synonyms in your language, and delete the old synonyms. + - What ever you do, **DO NOT** change the text in the "VALUE" column! Nightscout will be looking for these exact values. Only change the synonyms. +1. Click "Save Model" at the top, and then click on "Build Model". +1. You should be good to go! Feel free to try it out using the "Test" tab near the top of the window, or start asking your Alexa-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Alexa. + +## Adding Alexa support to a plugin + +See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/alexa-templates/en-us.json b/docs/plugins/alexa-templates/en-us.json new file mode 100644 index 00000000000..20bf8c290d1 --- /dev/null +++ b/docs/plugins/alexa-templates/en-us.json @@ -0,0 +1,311 @@ +{ + "interactionModel": { + "languageModel": { + "invocationName": "night scout", + "intents": [ + { + "name": "NSStatus", + "slots": [], + "samples": [ + "How am I doing" + ] + }, + { + "name": "LastLoop", + "slots": [], + "samples": [ + "When was my last loop" + ] + }, + { + "name": "MetricNow", + "slots": [ + { + "name": "metric", + "type": "LIST_OF_METRICS", + "samples": [ + "what {pwd} {metric} is", + "what my {metric} is", + "how {pwd} {metric} is", + "how my {metric} is", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "how much {metric}", + "{pwd} {metric}", + "{metric}", + "my {metric}" + ] + }, + { + "name": "pwd", + "type": "AMAZON.US_FIRST_NAME" + } + ], + "samples": [ + "how much {metric} does {pwd} have left", + "what's {metric}", + "what's my {metric}", + "how much {metric} is left", + "what's {pwd} {metric}", + "how much {metric}", + "how is {metric}", + "how is my {metric}", + "how is {pwd} {metric}", + "how my {metric} is", + "what is {metric}", + "how much {metric} do I have", + "how much {metric} does {pwd} have", + "how much {metric} I have", + "what is my {metric}", + "what my {metric} is", + "what is {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": [ + "blood glucose delta", + "blood sugar delta", + "bg delta" + ] + } + }, + { + "name": { + "value": "uploader battery", + "synonyms": [ + "uploader battery remaining", + "uploader battery power" + ] + } + }, + { + "name": { + "value": "pump reservoir", + "synonyms": [ + "remaining insulin", + "insulin remaining", + "insulin is left", + "insulin left", + "insulin in my pump", + "insulin" + ] + } + }, + { + "name": { + "value": "pump battery", + "synonyms": [ + "pump battery remaining", + "pump battery power" + ] + } + }, + { + "name": { + "value": "bg", + "synonyms": [ + "number", + "blood sugar", + "blood glucose" + ] + } + }, + { + "name": { + "value": "iob", + "synonyms": [ + "insulin on board" + ] + } + }, + { + "name": { + "value": "basal", + "synonyms": [ + "current basil", + "basil", + "current basal" + ] + } + }, + { + "name": { + "value": "cob", + "synonyms": [ + "carbs", + "carbs on board", + "carboydrates", + "carbohydrates on board" + ] + } + }, + { + "name": { + "value": "forecast", + "synonyms": [ + "ar2 forecast", + "loop forecast" + ] + } + }, + { + "name": { + "value": "raw bg", + "synonyms": [ + "raw number", + "raw blood sugar", + "raw blood glucose" + ] + } + }, + { + "name": { + "value": "cgm noise" + } + }, + { + "name": { + "value": "cgm tx age", + "synonyms": [ + "tx age", + "transmitter age", + "cgm transmitter age" + ] + } + }, + { + "name": { + "value": "cgm tx status", + "synonyms": [ + "tx status", + "transmitter status", + "cgm transmitter status" + ] + } + }, + { + "name": { + "value": "cgm battery", + "synonyms": [ + "cgm battery level", + "cgm battery levels", + "cgm batteries", + "cgm transmitter battery", + "cgm transmitter battery level", + "cgm transmitter battery levels", + "cgm transmitter batteries", + "transmitter battery", + "transmitter battery level", + "transmitter battery levels", + "transmitter batteries" + ] + } + }, + { + "name": { + "value": "cgm session age", + "synonyms": [ + "session age" + ] + } + }, + { + "name": { + "value": "cgm status" + } + }, + { + "name": { + "value": "cgm mode" + } + }, + { + "name": { + "value": "db size", + "synonyms": [ + "database size", + "data size", + "file size" + ] + } + } + ] + } + ] + }, + "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.US_FIRST_NAME", + "confirmationRequired": false, + "elicitationRequired": false, + "prompts": {} + } + ] + } + ], + "delegationStrategy": "ALWAYS" + }, + "prompts": [ + { + "id": "Elicit.Slot.1421281086569.34001419564", + "variations": [ + { + "type": "PlainText", + "value": "What metric are you looking for?" + }, + { + "type": "PlainText", + "value": "What value are you looking for?" + }, + { + "type": "PlainText", + "value": "What metric do you want to know?" + }, + { + "type": "PlainText", + "value": "What value do you want to know?" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/docs/plugins/alexa-templates/es-es.json b/docs/plugins/alexa-templates/es-es.json new file mode 100644 index 00000000000..dd705d91bd9 --- /dev/null +++ b/docs/plugins/alexa-templates/es-es.json @@ -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?" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/example-profiles.md b/docs/plugins/example-profiles.md similarity index 100% rename from example-profiles.md rename to docs/plugins/example-profiles.md diff --git a/docs/plugins/google-home-templates/de-DE.zip b/docs/plugins/google-home-templates/de-DE.zip new file mode 100644 index 00000000000..760a24b95bc Binary files /dev/null and b/docs/plugins/google-home-templates/de-DE.zip differ diff --git a/docs/plugins/google-home-templates/en-us.zip b/docs/plugins/google-home-templates/en-us.zip new file mode 100644 index 00000000000..551e51307f0 Binary files /dev/null and b/docs/plugins/google-home-templates/en-us.zip differ diff --git a/docs/plugins/googlehome-plugin.md b/docs/plugins/googlehome-plugin.md new file mode 100644 index 00000000000..fc76117059e --- /dev/null +++ b/docs/plugins/googlehome-plugin.md @@ -0,0 +1,172 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Nightscout Google Home/DialogFlow Plugin](#nightscout-google-homedialogflow-plugin) + - [Overview](#overview) + - [Activate the Nightscout Google Home Plugin](#activate-the-nightscout-google-home-plugin) + - [Create Your DialogFlow Agent](#create-your-dialogflow-agent) + - [Do you use Authentication Roles?](#do-you-use-authentication-roles) + - [What questions can you ask it?](#what-questions-can-you-ask-it) + - [Using the Alpha Tester feature](#using-the-alpha-tester-feature) + - [Updating your agent with new features](#updating-your-agent-with-new-features) + - [Adding support for additional languages](#adding-support-for-additional-languages) + - [Adding Google Home support to a plugin](#adding-google-home-support-to-a-plugin) + + + +Nightscout Google Home/DialogFlow Plugin +======================================== + +## Overview + +To add Google Home support for your Nightscout site, here's what you need to do: + +1. [Activate the `googlehome` plugin](#activate-the-nightscout-google-home-plugin) on your Nightscout site, so your site will respond correctly to Google's requests. +1. [Create a custom DialogFlow agent](#create-your-dialogflow-agent) that points at your site and defines certain questions you want to be able to ask. + +## Activate the Nightscout Google Home Plugin + +1. Your Nightscout site needs to be new enough that it supports the `googlehome` plugin. It needs to be [version 13.0.0 (Ketchup)](https://github.com/nightscout/cgm-remote-monitor/releases/tag/13.0.0) or later. See [updating my version](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) if you need a newer version. +1. Add `googlehome` to the list of plugins in your `ENABLE` setting. ([Environment variables](https://github.com/nightscout/cgm-remote-monitor#environment) are set in the configuration section for your monitor. Typically Azure, Heroku, etc.) + +## Create Your DialogFlow Agent + +**BEFORE YOU GET STARTED:** Please read [the section below on the **highly-recommended** use of the Alpha tester feature](#using-the-alpha-tester-feature). + +1. Download the agent template in your language for Google Home [here](google-home-templates/). + - If you're language doesn't have a template, please consider starting with [the en-us template](google-home-templates/en-us.zip), then [modifying it to work with your language](#adding-support-for-additional-languages), and [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. +1. [Sign in to Google's Action Console](https://console.actions.google.com) + - Make sure to use the same account that is connected to your Google Home device, Android smartphone, Android tablet, etc. +1. Click on the "New Project" button. +1. If prompted, agree to the Terms of Service. +1. Give your project a name (e.g. "Nightscout") and then click "Create project". +1. When asked what kind of Action you want to build, select "Custom" and then click the "Next" button. +1. When selecting how you want to build the project, scroll down to the bottom of the screen and click the link to build it using DialogFlow. +1. Click on the "Develop" tab at the top of the sreen. +1. Click on "Invocation" in the left navigation pane. +1. Set the display name (e.g. "Night Scout") of your Action and set your Google Assistant voice. + - Unfortunately, the Action name needs to be two words, and is required to be unique across all of Google, even though you won't be publishing this for everyone on Google to use. So you'll have to be creative with the name since "Night Scout" is already taken. +1. Click "Save" in the upper right corner. +1. Navigate to "Actions" in the left nagivation pane, then click on the "Add your first action" button. +1. Make sure you're on "Cutom intent" and then click "Build" to open DialogFlow in a new tab. +1. Sign in with the same Google account you used to sign in to the Actions Console. + - You'll have to go through the account setup steps if this is your first time using DialogFlow. +1. Verify the name for your agent (e.g. "Nightscout") and click "CREATE". +1. In the navigation pane on the left, click the gear icon next to your agent name. +1. Click on the "Export and Import" tab in the main area of the page. +1. Click the "IMPORT FROM ZIP" button. +1. Select the template file downloaded in step 1. +1. Type "IMPORT" where requested and then click the "IMPORT" button. +1. After the import finishes, click the "DONE" button followed by the "SAVE" button. +1. In the navigation pane on the left, click on "Fulfillment". +1. Enable the toggle for "Webhook" and then fill in the URL field with your Nightscout URL: `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome` + - If you use Authentication Roles, you'll need to add a bit to the end of your URL. See [the section](#do-you-use-authentication-roles) below. +1. Scroll down to the bottom of the page and click the "SAVE" button. +1. Click on "Integrations" in the navigation pane. +1. Click on "INTEGRATION SETTINGS" for "Google Assistant". +1. Under "Implicit invocation", add every intent listed. +1. Turn on the toggle for "Auto-preview changes". +1. Click "CLOSE". + +That's it! Now try asking Google "Hey Google, ask *your Action's name* how am I doing?" + +### Do you use Authentication Roles? ### + +If you use Authentication Roles, you will need to add a token to the end of your Nightscout URL when configuring your Webhook. + +1. In your Nightscout Admin Tools, add a new subject and give it the "readable" role. + - If you **really** would like to be super specific, you could create a new role and set the permissions to `api:*:read`. +1. After the new subject is created, copy the "Access Token" value for the new row in your subject table (**don't** copy the link, just copy the text). +1. At the end of your Nighscout URL, add `?token=YOUR-TOKEN`, where `YOUR-TOKEN` is the Access Token you just copied. Your new URL should look like `https://YOUR-NIGHTSCOUT-SITE/api/v1/googlehome?token=YOUR-TOKEN`. + +### What questions can you ask it? + +See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. + +### Using the Alpha Tester feature + +If you use your Google Action on the same account as the one you used to create it, you will find that Google disables Test Mode for the action after some period of time, and you have to log in to the Actions console, and open the testing tab to re-enable testing for you to continue to use it. To overcome this limitation, you can use the Alpa Testers feature of Google Actions. To do so, you need to follow a few extra steps: + +1. Figure out which Google account you use for your Google Assistant. +1. Use or create a different account to follow the instructions to create your Google Action and DialogFlow agent. +1. Once you verify your Action is working, navigate to the [Actions Console](https://console.actions.google.com/), and open your project. +1. Navigate to the "Deploy" tab, then open the "Release" page in the left navigation pane. +1. Expand the "Alpha" section, then click on the "Create a release" button, and then follow the directions to create a release. + - You may need to fill out some extra information, such as a Privacy Policy ([example](https://docs.google.com/document/d/1RP32ooEol97UyPiJ9vUskhLb6XC6PHhtVTwh0siUZV0/view)), descriptions (e.g. both could be "Tools and stuff to help my partner and I do things better"), and testing instructions (e.g. "Since this is built entirely for myself and my partner, don't expect too much out of this one."). Because this will be for personal use, it's recommended that you keep all of these as vague as possible. +1. Click the "Manage Alpha Testers" button. +1. Enter the email address of the account you use for your Google Assistant (found in step 1). Enter the emails of any other people you would like to have access to talk to your Action (e.g. a spouse). +1. Click the "Save" button. +1. Copy the "Opt-in link" and open it on a device logged in with your account from step 1. Repeat for any other emails you added two steps ago. If the link doesn't work right away, try again in a couple hours. +1. Follow any directions to setup the account as an Alpha Tester. + +## Updating your agent with new features + +As more work is done on Nightscout, new ways to interact with Nighscout via Google Home may be made available. To be able to use these new features, you first will need to [update your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version), and then you can follow the steps below to update your DialogFlow agent. + +1. Make sure you've [updated your Nightscout site](https://github.com/nightscout/cgm-remote-monitor#updating-my-version) first. +1. Download [the latest skill template](google-home-templates/) in your language. + - If your language doesn't include the latest features you're looking for, you're help [translating those new features](#adding-support-for-additional-languages) would be greatly appreciated! +1. Sign in to the [DialogFlow developer portal](https://dialogflow.cloud.google.com/). +1. Make sure you're viewing your Nightscout agent (there's a drop-down box immediately below the DialogFlow logo where you can select your agent). +1. Click on the gear icon next to your agent name, then click on the "Export and Import" tab. +1. Click the "RESTORE FROM ZIP" button. +1. Select the template file you downloaded earlier, then type "RESTORE" in the text box as requested, and click the "RESTORE" button. +1. After the import is completed, click the "DONE" button. +1. Make sure to follow any directions specific to the Nightscout update. If there are any, they will be noted in the [release notes](https://github.com/nightscout/cgm-remote-monitor/releases). +1. If you use the Alpha Testers feature (see [Using the Alpha Tester feature](#using-the-alpha-tester-feature) above), create a new release. +1. Enjoy the new features! + +## Adding support for additional languages + +If the translations in Nightscout are configured correctly for the desired language code, Nightscout *should* automatically respond in that language after following the steps below. + +If you add support for another language, please consider [making a pull request](/CONTRIBUTING.md) or [submitting an issue](https://github.com/nightscout/cgm-remote-monitor/issues) with your translated template to share it with others. You can export your translated template by going to the settings of your DialogFlow agent (the gear icon next to the project's name in the left nagivation pane), going to the "Export and Import" tab, and clicking "EXPORT AS ZIP". + +1. Open your DialogFlow agent. + - Get to your list of agents at https://console.dialogflow.com/api-client/#/agents and click on the name of your Nightscout agent. +1. Click on the "Languages" tab. +1. Click the "Add Additional Language" drop-down box. +1. Select your desired language. +1. Click the "SAVE" button. + - Note the new language code below the agent's name. e.g. if you're using the English template and you added Spanish, you would see two buttons: "en" and "es". +1. Click on "Intents" in the left navigation pane. +1. For each intent in the list (NOT including those that start with "Default" in the name): + 1. Click on the intent name. + 1. Note the phrases used in the "Training phrases" section. + - If the phrase has a colored block (e.g. `metric` or `pwd`), click the phrase (but NOT the colored block) and note the "PARAMETER NAME" of the item with the same-colored "ENTITY". + 1. Click on the new language code (beneath the agent name near the top of the navigation pane). + 1. Add equivalent or similar training phrases as those you noted a couple steps ago. + - If the phrase in the orginal language has a colored block with a word in it, that needs to be included. When adding the phrase to the new language, follow these steps to add the colored block: + 1. When typing that part of the training phrase, don't translate the word in the block; just keep it as-is. + 1. After typing the phrase (DON'T push the Enter key yet!) highlight/select the word. + 1. A box will pop up with a list of parameter types, some of which end with a colon (`:`) and a parameter name. Click the option that has the same parameter name as the one you determined just a few steps ago. + 1. Press the Enter key to add the phrase. + 1. Click the "SAVE" button. + 1. Go back and forth between your starting language and your new language, adding equivalent phrase(s) to the new language. Continue once you've added all the equivalent phrases you can think of. + 1. Scroll down to the "Action and parameters" section. + 1. If any of the items in that list have the "REQUIRED" option checked: + 1. Click the "Define prompts..." link on the right side of that item. + 1. Add phrases that Google will ask if you happen to say something similar to a training phrase, but don't include this parameter (e.g. if you ask about a metric but don't say what metric you want to know about). + 1. Click "CLOSE". + 1. Scroll down to the "Responses" section. + 1. Set just one phrase here. This will be what Google says if it has technical difficulties getting a response from your Nightscout website. + 1. Click the "SAVE" button at the top of the window. +1. Click on the "Entities" section in the navigation pane. +1. For each entity listed: + 1. Click the entity name. + 1. Switch to the starting language (beneath the agent name near the top of the left navigation pane). + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". + 1. Select all the text in the text box and copy it. + 1. Switch back to your new language. + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to raw mode". + 1. In the text box, paste the text you just copied. + 1. Click the menu icon to the right of the "SAVE" button and click "Switch to editor mode". + 1. For each item in the list, replace the items on the RIGHT side of the list with equivalent words and phrases in your language. + - What ever you do, **DO NOT** change the values on the left side of the list. Nightscout will be looking for these exact values. Only change the items on the right side of the list. + 1. Click the "SAVE" button. +1. You should be good to go! Feel free to try it out by click the "See how it works in Google Assistant" link in the right navigation pane, or start asking your Google-Home-enabled device some questions. See [Interacting with Virtual Assistants](interacting-with-virtual-assistants.md) for details on what you can do with Google Home. + +## Adding Google Home support to a plugin + +See [Adding Virtual Assistant Support to a Plugin](add-virtual-assistant-support-to-plugin.md) \ No newline at end of file diff --git a/docs/plugins/interacting-with-virtual-assistants.md b/docs/plugins/interacting-with-virtual-assistants.md new file mode 100644 index 00000000000..3fe67bee2fb --- /dev/null +++ b/docs/plugins/interacting-with-virtual-assistants.md @@ -0,0 +1,77 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Interacting with Virtual Assistants](#interacting-with-virtual-assistants) +- [Alexa vs. Google Home](#alexa-vs-google-home) +- [What questions can you ask it?](#what-questions-can-you-ask-it) + - [A note about names](#a-note-about-names) + + + +Interacting with Virtual Assistants +=================================== + +# Alexa vs. Google Home + +Although these example phrases reference Alexa, the exact same questions could be asked of Google. +Just replace "Alexa, ask Nightscout ..." with "Hey Google, ask *your action's name* ..." + +# What questions can you ask it? + +This list is not meant to be comprehensive, nor does it include every way you can ask the questions. To get the full picture, in the respective console for your virtual assistant, check the example phrases for each `intent`, and the values (including synonyms) of the "metric" `slot` (Alexa) or `entity` (Google Home). You can also just experiement with asking different questions to see what works. + +*Forecast:* + +- "Alexa, ask Nightscout how am I doing" +- "Alexa, ask Nightscout how I'm doing" + +*Uploader Battery:* + +- "Alexa, ask Nightscout how is my uploader battery" + +*Pump Battery:* + +- "Alexa, ask Nightscout how is my pump battery" + +*Metrics:* + +- "Alexa, ask Nightscout what my bg is" +- "Alexa, ask Nightscout what my blood glucose is" +- "Alexa, ask Nightscout what my number is" +- "Alexa, ask Nightscout what is my insulin on board" +- "Alexa, ask Nightscout what is my basal" +- "Alexa, ask Nightscout what is my current basal" +- "Alexa, ask Nightscout what is my cob" +- "Alexa, ask Nightscout what is my delta" +- "Alexa, ask Nightscout what is Charlie's carbs on board" +- "Alexa, ask Nightscout what is Sophie's carbohydrates on board" +- "Alexa, ask Nightscout what is Harper's loop forecast" +- "Alexa, ask Nightscout what is Alicia's ar2 forecast" +- "Alexa, ask Nightscout what is Peter's forecast" +- "Alexa, ask Nightscout what is Arden's raw bg" +- "Alexa, ask Nightscout what is Dana's raw blood glucose" + +*CGM Info:* (when using the [`xdripjs` plugin](/README.md#xdripjs-xdrip-js)) + +- "Alexa, ask Nightscout what's my CGM status" +- "Alexa, ask Nightscout what's my CGM session age" +- "Alexa, ask Nightscout what's my CGM transmitter age" +- "Alexa, ask Nightscout what's my CGM mode" +- "Alexa, ask Nightscout what's my CGM noise" +- "Alexa, ask Nightscout what's my CGM battery" + +*Insulin Remaining:* + +- "Alexa, ask Nightscout how much insulin do I have left" +- "Alexa, ask Nightscout how much insulin do I have remaining" +- "Alexa, ask Nightscout how much insulin does Dana have left? +- "Alexa, ask Nightscout how much insulin does Arden have remaining? + +*Last Loop:* + +- "Alexa, ask Nightscout when was my last loop" + +## A note about names + +All the formats with specific names will respond to questions for any first name. You don't need to configure anything with your PWD's name. \ No newline at end of file diff --git a/docs/plugins/maker-setup-images/action_search.jpg b/docs/plugins/maker-setup-images/action_search.jpg new file mode 100644 index 00000000000..b3b85b0b7c1 Binary files /dev/null and b/docs/plugins/maker-setup-images/action_search.jpg differ diff --git a/docs/plugins/maker-setup-images/blue_that.jpg b/docs/plugins/maker-setup-images/blue_that.jpg new file mode 100644 index 00000000000..6a52475bcb1 Binary files /dev/null and b/docs/plugins/maker-setup-images/blue_that.jpg differ diff --git a/docs/plugins/maker-setup-images/config_vars_enable.jpg b/docs/plugins/maker-setup-images/config_vars_enable.jpg new file mode 100644 index 00000000000..508464f156c Binary files /dev/null and b/docs/plugins/maker-setup-images/config_vars_enable.jpg differ diff --git a/docs/plugins/maker-setup-images/config_vars_maker.jpg b/docs/plugins/maker-setup-images/config_vars_maker.jpg new file mode 100644 index 00000000000..fdb8765f836 Binary files /dev/null and b/docs/plugins/maker-setup-images/config_vars_maker.jpg differ diff --git a/docs/plugins/maker-setup-images/maker_key.jpg b/docs/plugins/maker-setup-images/maker_key.jpg new file mode 100644 index 00000000000..8e570032762 Binary files /dev/null and b/docs/plugins/maker-setup-images/maker_key.jpg differ diff --git a/docs/plugins/maker-setup-images/notification_message.jpg b/docs/plugins/maker-setup-images/notification_message.jpg new file mode 100644 index 00000000000..3db0278bc69 Binary files /dev/null and b/docs/plugins/maker-setup-images/notification_message.jpg differ diff --git a/docs/plugins/maker-setup-images/service_search.jpg b/docs/plugins/maker-setup-images/service_search.jpg new file mode 100644 index 00000000000..49910be5e06 Binary files /dev/null and b/docs/plugins/maker-setup-images/service_search.jpg differ diff --git a/docs/plugins/maker-setup-images/service_settings_search.jpg b/docs/plugins/maker-setup-images/service_settings_search.jpg new file mode 100644 index 00000000000..2db00726482 Binary files /dev/null and b/docs/plugins/maker-setup-images/service_settings_search.jpg differ diff --git a/docs/plugins/maker-setup-images/set_trigger.jpg b/docs/plugins/maker-setup-images/set_trigger.jpg new file mode 100644 index 00000000000..27884f8ac04 Binary files /dev/null and b/docs/plugins/maker-setup-images/set_trigger.jpg differ diff --git a/docs/plugins/maker-setup-images/webhooks_settings.jpg b/docs/plugins/maker-setup-images/webhooks_settings.jpg new file mode 100644 index 00000000000..e94f4485c43 Binary files /dev/null and b/docs/plugins/maker-setup-images/webhooks_settings.jpg differ diff --git a/docs/plugins/maker-setup.md b/docs/plugins/maker-setup.md new file mode 100644 index 00000000000..f40c94f161c --- /dev/null +++ b/docs/plugins/maker-setup.md @@ -0,0 +1,128 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Nightscout/IFTTT Maker](#nightscoutifttt-maker) + - [Overview](#overview) + - [Note: There have been some recent reports of the IFTTT service delaying Nightscout alarms. Be sure to test your implementation before relying solely on its alerts. Pushover is an alternate push notification service that might be worth considering as well.](#note-there-have-been-some-recent-reports-of-the-ifttt-service-delaying-nightscout-alarms-be-sure-to-test-your-implementation-before-relying-solely-on-its-alerts-pushover-is-an-alternate-push-notification-service-that-might-be-worth-considering-as-well) + - [Events](#events) + - [Creating an Applet](#creating-an-applet) + - [1. Choose a Service](#1-choose-a-service) + - [2. Choose a Trigger](#2-choose-a-trigger) + - [3. Complete the Trigger field](#3-complete-the-trigger-field) + - [4. Create an Action](#4-create-an-action) + - [5. Complete Action Fields](#5-complete-action-fields) + - [6. Review and Finish](#6-review-and-finish) + - [7. Get your Maker Key](#7-get-your-maker-key) + - [8. Configure your Nightscout site](#8-configure-your-nightscout-site) + - [9. Configure the IFTTT mobile app](#9-configure-the-ifttt-mobile-app) + + + +**Table of Contents** + +- [Nightscout/IFTTT Maker](#nightscoutifttt-maker) + - [Overview](#overview) + - [Events](#events) + - [Creating an Applet](#creating-an-applet) + - [1. Choose a Service](#1.-Choose-a-Service) + - [2. Choose a Trigger](#2.-Choose-a-Trigger) + - [3. Complete the Trigger field](#3.-Complete-the-Trigger-field) + - [4. Create an Action](#4.-Create-an-Action) + - [5. Complete Action Fields](#5.-Complete-Action-Fields) + - [6. Review and Finish](#6.-Review-and-Finish) + - [7. Get your Maker Key](#7.-Get-your-Maker-Key) + - [8. Configure your Nightscout site](#8.-Configure-your-Nightscout-site) + - [9. Configure the IFTTT mobile app](#9.-Configure-the-IFTTT-mobile-app) + +Nightscout/IFTTT Maker +====================================== + +## Overview + +In addition to Nightscout's web-based alarms, your site can also trigger push notifications (or other actions) through the [IFTTT Maker](https://ifttt.com/maker) service. With Maker you are able to integrate with all the other [IFTTT Services](https://ifttt.com/channels). For example, you can send a tweet when there is an alarm, change the color of a smart light, send an email, send a text, and much more. + +#### Note: There have been some recent reports of the IFTTT service delaying Nightscout alarms. Be sure to test your implementation before relying solely on its alerts. [Pushover](https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#pushover) is an alternate push notification service that might be worth considering as well. + +## Events + + Plugins can create custom events, but all events sent to Maker will be prefixed with `ns-`. The core events are: + + * `ns-event` - This event is sent on *all* alarms and notifications. This is good catch-all event for general logging. + * `ns-allclear` - This event is sent when an alarm has been acknowledged or when the server starts up without triggering any alarms. (For example, you could use this event to turn a light to green.) + * `ns-info` - All notifications at the `info` level will cause this event to be triggered. + * `ns-warning` - All notifications at the `warning` level will cause this event to be triggered. + * `ns-urgent` - All notifications at the `urgent` level will cause this event to be triggered. + * `ns-warning-high` - This event is triggered when crossing the `BG_TARGET_TOP` threshold. + * `ns-urgent-high` - This event is triggered when crossing the `BG_HIGH` threshold. + * `ns-warning-low` - This event is triggered when crossing the `BG_TARGET_BOTTOM` threshold. + * `ns-urgent-low` - This event is triggered when crossing the `BG_LOW` threshold. + * `ns-info-treatmentnotify` - This event is triggered when a treatment is entered into the `careportal`. + * `ns-warning-bwp` - This event is triggered when the `bwp` plugin generates a warning alarm. + * `ns-urgent-bwp` - This event is triggered when the `bwp` plugin generates an urgent alarm. + +## Creating an Applet +Set up an [IFTTT](https://ifttt.com/) account, and log into it. + +### 1. Choose a Service +On the "My Applets" page, click "New Applet", then click the blue `+this`. Search for "webhooks" among the services, and click it. + +![service_search](./maker-setup-images/service_search.jpg) + +### 2. Choose a Trigger +Click on the "Receive a web request" box. + +### 3. Complete the Trigger field +Enter one of the Nightscout events listed above (like `ns-urgent-low`), and click "Create Trigger". + +![set_trigger](./maker-setup-images/set_trigger.jpg) + +### 4. Create an Action +Click on the blue `+that`. + +![blue_that](./maker-setup-images/blue_that.jpg) + +Search for the action you'd like this event to trigger. In this example, we'll choose the `Notifications` action to send a push notification. + +![action_search](./maker-setup-images/action_search.jpg) + +Choose the "Send a notification from the IFTTT app" action type for a basic push alert. You can experiment with the "rich" notifications later. + +### 5. Complete Action Fields +Enter the message that will display in this push notification. In this example, it was triggered on an `ns-urgent-low`, so we'll write something like "Urgent Low!". We can also display the current BG by including the `Value2` ingredient (via the "Add ingredient" button). + +Click the "Create action" button when you're done. + +![notification_message](./maker-setup-images/notification_message.jpg) + +### 6. Review and Finish +Click the "Finish" button if your applet looks good. + +### 7. Get your Maker Key + +Click on "My Applets" in the main menu, then click the "Services" tab, then search for "Webhooks" and select it. + +![service_settings_search](./maker-setup-images/service_settings_search.jpg) + +Go to the `Settings` for this service, in the upper right. + +![webhooks_settings](./maker-setup-images/webhooks_settings.jpg) + +The string of characters at the end of the URL here is your `MAKER_KEY`. Copy it from there, so we can paste it into your Config Vars. + +![maker_key](./maker-setup-images/maker_key.jpg) + +### 8. Configure your Nightscout site +From your [Heroku dashboard](https://dashboard.heroku.com), go to your app's Settings page, then click the "Reveal Config Vars" button. Find the `MAKER_KEY` entry, and edit its value, pasting in your Maker Key. If you don't already have a `MAKER_KEY` line, add it to the bottom of the list. + +![config_vars_maker](./maker-setup-images/config_vars_maker.jpg) + +Find your `ENABLE` line, and add `maker` to the list of enabled plugins. + +![config_vars_enable](./maker-setup-images/config_vars_enable.jpg) + +### 9. Configure the IFTTT mobile app +That's all of the services complete. In order to receive push notifications on a mobile device, you'll need to have the IFTTT app installed and logged into the same account you set up the actions in. + +To add more alerts for different events, just create a new applet for each trigger. + diff --git a/docs/update.png b/docs/update.png new file mode 100644 index 00000000000..af60ebadd54 Binary files /dev/null and b/docs/update.png differ diff --git a/env.js b/env.js deleted file mode 100644 index 03b53f84094..00000000000 --- a/env.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var fs = require('fs'); -var crypto = require('crypto'); -var consts = require('./lib/constants'); - -var env = { - settings: require('./lib/settings')() -}; - -// Module to constrain all config and environment parsing to one spot. -// See the -function config ( ) { - /* - * See README.md for info about all the supported ENV VARs - */ - env.DISPLAY_UNITS = readENV('DISPLAY_UNITS', 'mg/dl'); - env.PORT = readENV('PORT', 1337); - env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/'); - - setSSL(); - setAPISecret(); - setVersion(); - setMongo(); - updateSettings(); - - // require authorization for entering treatments - env.treatments_auth = readENV('TREATMENTS_AUTH',false); - - return env; -} - -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) - }; - if (env.SSL_CA) { - env.ca = fs.readFileSync(env.SSL_CA); - } - } -} - -// A little ugly, but we don't want to read the secret into a var -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; - // if a passphrase was provided, get the hex digest to mint a single token - if (useSecret) { - if (readENV('API_SECRET').length < consts.MIN_PASSPHRASE_LENGTH) { - var msg = ['API_SECRET should be at least', consts.MIN_PASSPHRASE_LENGTH, 'characters']; - throw new Error(msg.join(' ')); - } - var shasum = crypto.createHash('sha1'); - shasum.update(readENV('API_SECRET')); - env.api_secret = shasum.digest('hex'); - } -} - -function setVersion() { - var software = require('./package.json'); - var git = require('git-rev'); - - if (readENV('APPSETTING_ScmType') === readENV('ScmType') && readENV('ScmType') === 'GitHub') { - env.head = require('./scm-commit-id.json'); - console.log('SCM COMMIT ID', env.head); - } else { - git.short(function record_git_head(head) { - console.log('GIT HEAD', head); - env.head = head || readENV('SCM_COMMIT_ID') || readENV('COMMIT_HASH', ''); - }); - } - env.version = software.version; - env.name = software.name; -} - -function setMongo() { - env.mongo = readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI'); - env.mongo_collection = readENV('MONGO_COLLECTION', 'entries'); - env.MQTT_MONITOR = readENV('MQTT_MONITOR', null); - if (env.MQTT_MONITOR) { - var hostDbCollection = [env.mongo.split('mongodb://').pop().split('@').pop(), env.mongo_collection].join('/'); - var mongoHash = crypto.createHash('sha1'); - mongoHash.update(hostDbCollection); - //some MQTT servers only allow the client id to be 23 chars - env.mqtt_client_id = mongoHash.digest('base64').substring(0, 23); - console.info('Using Mongo host/db/collection to create the default MQTT client_id', hostDbCollection); - if (env.MQTT_MONITOR.indexOf('?clientId=') === -1) { - console.info('Set MQTT client_id to: ', env.mqtt_client_id); - } else { - console.info('MQTT configured to use a custom client id, it will override the default: ', env.mqtt_client_id); - } - } - env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); - env.profile_collection = readENV('MONGO_PROFILE_COLLECTION', 'profile'); - env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); - env.food_collection = readENV('MONGO_FOOD_COLLECTION', 'food'); - - // 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.mongo, - DB_COLLECTION = DB.collection ? DB.collection : env.mongo_collection; - env.mongo = DB_URL; - env.mongo_collection = DB_COLLECTION; -} - -function updateSettings() { - - var envNameOverrides = { - UNITS: 'DISPLAY_UNITS' - }; - - env.settings.eachSettingAsEnv(function settingFromEnv (name) { - var envName = envNameOverrides[name] || name; - return readENV(envName); - }); - - //should always find extended settings last - env.extendedSettings = findExtendedSettings(process.env); -} - -function readENV(varName, defaultValue) { - //for some reason Azure uses this prefix, maybe there is a good reason - var value = process.env['CUSTOMCONNSTR_' + varName] - || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] - || process.env[varName] - || process.env[varName.toLowerCase()]; - - if (typeof value === 'string' && value.toLowerCase() === 'on') { value = true; } - if (typeof value === 'string' && value.toLowerCase() === 'off') { value = false; } - - return value != null ? value : defaultValue; -} - -function findExtendedSettings (envs) { - var extended = {}; - - function normalizeEnv (key) { - return key.toUpperCase().replace('CUSTOMCONNSTR_', ''); - } - - _.each(env.settings.enable, function eachEnable(enable) { - if (_.trim(enable)) { - _.forIn(envs, function eachEnvPair (value, key) { - var env = normalizeEnv(key); - if (_.startsWith(env, enable.toUpperCase() + '_')) { - var split = env.indexOf('_'); - if (split > -1 && split <= env.length) { - var exts = extended[enable] || {}; - extended[enable] = exts; - var ext = _.camelCase(env.substring(split + 1).toLowerCase()); - if (!isNaN(value)) { value = Number(value); } - exts[ext] = value; - } - } - }); - } - }); - return extended; -} - -module.exports = config; diff --git a/lib/admin_plugins/cleanentriesdb.js b/lib/admin_plugins/cleanentriesdb.js new file mode 100644 index 00000000000..c13bfc48109 --- /dev/null +++ b/lib/admin_plugins/cleanentriesdb.js @@ -0,0 +1,81 @@ +'use strict'; + +var moment; + +var cleanentriesdb = { + name: 'cleanentriesdb' + , label: 'Clean Mongo entries (glucose entries) database' + , pluginType: 'admin' +}; + +function init(ctx) { + moment = ctx.moment; + return cleanentriesdb; +} + +module.exports = init; + +cleanentriesdb.actions = [ + { + name: 'Delete all documents from entries collection older than 180 days' + , description: 'This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.' + , buttonLabel: 'Delete old documents' + , confirmText: 'Delete old documents from entries collection?' + , preventClose: true + } + ]; + +cleanentriesdb.actions[0].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanentriesdb.name + '_0_status'); + + $status.hide(); + + var numDays = '
' + + ''; + + $('#admin_' + cleanentriesdb.name + '_0_html').html(numDays); + + if (callback) { callback(); } +}; + +cleanentriesdb.actions[0].code = function deleteOldRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanentriesdb.name + '_0_status'); + var numDays = Number($('#admin_entries_days').val()); + + if (isNaN(numDays) || (numDays < 3)) { + alert(translate('%1 is not a valid number - must be more than 2', { params: [$('#admin_entries_days').val()] })); + if (callback) { callback(); } + return; + } + var endDate = moment().subtract(numDays, 'days'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + } + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + $.ajax('/api/v1/entries/?find[date][$lte]=' + endDate.valueOf(), { + method: 'DELETE' + , headers: client.headers() + , success: function (retVal) { + $status.hide().text(translate('%1 records deleted',{ params: [retVal.n] })).fadeIn('slow'); + } + , error: function () { + $status.hide().text(translate('Error')).fadeIn('slow'); + } + }).done(function success () { + if (callback) { callback(); } + }).fail(function fail() { + if (callback) { callback(); } + }); + +}; diff --git a/lib/admin_plugins/cleanstatusdb.js b/lib/admin_plugins/cleanstatusdb.js index 86e61e987f8..44772f0de47 100644 --- a/lib/admin_plugins/cleanstatusdb.js +++ b/lib/admin_plugins/cleanstatusdb.js @@ -1,70 +1,134 @@ 'use strict'; +var moment; + var cleanstatusdb = { name: 'cleanstatusdb' , label: 'Clean Mongo status database' , pluginType: 'admin' }; -function init() { +function init (ctx) { + moment = ctx.moment; return cleanstatusdb; } module.exports = init; cleanstatusdb.actions = [ - { - name: 'Delete all documents from devicestatus collection' - , description: 'This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.' - , buttonLabel: 'Delete all documents' - , confirmText: 'Delete all documents from devicestatus collection?' + { + name: 'Delete all documents from devicestatus collection' + , description: 'This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.' + , buttonLabel: 'Delete all documents' + , confirmText: 'Delete all documents from devicestatus collection?' + } + , { + name: 'Delete all documents from devicestatus collection older than 30 days' + , description: 'This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.' + , buttonLabel: 'Delete old documents' + , confirmText: 'Delete old documents from devicestatus collection?' + , preventClose: true } ]; -cleanstatusdb.actions[0].init = function init(client, callback) { +cleanstatusdb.actions[0].init = function init (client, callback) { var translate = client.translate; var $status = $('#admin_' + cleanstatusdb.name + '_0_status'); - + $status.hide().text(translate('Loading database ...')).fadeIn('slow'); $.ajax('/api/v1/devicestatus.json?count=500', { - success: function (records) { + headers: client.headers() + , success: function(records) { var recs = (records.length === 500 ? '500+' : records.length); - $status.hide().text(translate('Database contains %1 records',{ params: [recs] })).fadeIn('slow'); - }, - error: function () { + $status.hide().text(translate('Database contains %1 records', { params: [recs] })).fadeIn('slow'); + } + , error: function() { $status.hide().text(translate('Error loading database')).fadeIn('slow'); } - }).done(function () { if (callback) { callback(); } }); + }).done(function() { if (callback) { callback(); } }); }; -cleanstatusdb.actions[0].code = function deleteRecords(client, callback) { +cleanstatusdb.actions[0].code = function deleteRecords (client, callback) { var translate = client.translate; var $status = $('#admin_' + cleanstatusdb.name + '_0_status'); - + if (!client.hashauth.isAuthenticated()) { alert(translate('Your device is not authenticated yet')); if (callback) { callback(); } return; - }; + } $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); $.ajax({ - method: 'DELETE' + method: 'DELETE' , url: '/api/v1/devicestatus/*' - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function success () { $status.hide().text(translate('All records removed ...')).fadeIn('slow'); if (callback) { callback(); } - }).fail(function fail() { + }).fail(function fail () { $status.hide().text(translate('Error')).fadeIn('slow'); if (callback) { callback(); } }); }; + +cleanstatusdb.actions[1].init = function init (client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanstatusdb.name + '_1_status'); + + $status.hide(); + + var numDays = '
' + + ''; + + $('#admin_' + cleanstatusdb.name + '_1_html').html(numDays); + + if (callback) { callback(); } +}; + +cleanstatusdb.actions[1].code = function deleteOldRecords (client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanstatusdb.name + '_1_status'); + var numDays = Number($('#admin_devicestatus_days').val()); + + if (isNaN(numDays) || (numDays < 1)) { + alert(translate('%1 is not a valid number', { params: [$('#admin_devicestatus_days').val()] })); + if (callback) { callback(); } + return; + } + var endDate = moment().subtract(numDays, 'days'); + var dateStr = endDate.format('YYYY-MM-DD'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + } + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + $.ajax('/api/v1/devicestatus/?find[created_at][$lte]=' + dateStr, { + method: 'DELETE' + , headers: client.headers() + , success: function(retVal) { + $status.text(translate('%1 records deleted', { params: [retVal.n] })); + } + , error: function() { + $status.hide().text(translate('Error')).fadeIn('slow'); + } + }).done(function success () { + if (callback) { callback(); } + }).fail(function fail () { + if (callback) { callback(); } + }); +}; diff --git a/lib/admin_plugins/cleantreatmentsdb.js b/lib/admin_plugins/cleantreatmentsdb.js new file mode 100644 index 00000000000..bd5d3c52087 --- /dev/null +++ b/lib/admin_plugins/cleantreatmentsdb.js @@ -0,0 +1,82 @@ +'use strict'; + +var moment; + +var cleantreatmentsdb = { + name: 'cleantreatmentsdb' + , label: 'Clean Mongo treatments database' + , pluginType: 'admin' +}; + +function init(ctx) { + moment = ctx.moment; + return cleantreatmentsdb; +} + +module.exports = init; + +cleantreatmentsdb.actions = [ + { + name: 'Delete all documents from treatments collection older than 180 days' + , description: 'This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.' + , buttonLabel: 'Delete old documents' + , confirmText: 'Delete old documents from treatments collection?' + , preventClose: true + } + ]; + +cleantreatmentsdb.actions[0].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleantreatmentsdb.name + '_0_status'); + + $status.hide(); + + var numDays = '
' + + ''; + + $('#admin_' + cleantreatmentsdb.name + '_0_html').html(numDays); + + if (callback) { callback(); } +}; + +cleantreatmentsdb.actions[0].code = function deleteOldRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleantreatmentsdb.name + '_0_status'); + var numDays = Number($('#admin_treatments_days').val()); + + if (isNaN(numDays) || (numDays < 3)) { + alert(translate('%1 is not a valid number - must be more than 2', { params: [$('#admin_treatments_days').val()] })); + if (callback) { callback(); } + return; + } + var endDate = moment().subtract(numDays, 'days'); + var dateStr = endDate.format('YYYY-MM-DD'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + } + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + $.ajax('/api/v1/treatments/?find[created_at][$lte]=' + dateStr, { + method: 'DELETE' + , headers: client.headers() + , success: function (retVal) { + $status.hide().text(translate('%1 records deleted',{ params: [retVal.n] })).fadeIn('slow'); + } + , error: function () { + $status.hide().text(translate('Error')).fadeIn('slow'); + } + }).done(function success () { + if (callback) { callback(); } + }).fail(function fail() { + if (callback) { callback(); } + }); + +}; diff --git a/lib/admin_plugins/futureitems.js b/lib/admin_plugins/futureitems.js index ab888aff0a6..4ea613a53c2 100644 --- a/lib/admin_plugins/futureitems.js +++ b/lib/admin_plugins/futureitems.js @@ -6,166 +6,164 @@ var futureitems = { , pluginType: 'admin' }; -function init() { +function init () { return futureitems; } module.exports = init; futureitems.actions = [ - { - name: 'Find and remove treatments in the future' - , description: 'This task find and remove treatments in the future.' - , buttonLabel: 'Remove treatments in the future' + { + name: 'Find and remove treatments in the future' + , description: 'This task find and remove treatments in the future.' + , buttonLabel: 'Remove treatments in the future' } - , { - name: 'Find and remove entries in the future' - , description: 'This task find and remove CGM data in the future created by uploader with wrong date/time.' - , buttonLabel: 'Remove entries in the future' + + , { + name: 'Find and remove entries in the future' + , description: 'This task find and remove CGM data in the future created by uploader with wrong date/time.' + , buttonLabel: 'Remove entries in the future' } ]; -futureitems.actions[0].init = function init(client, callback) { +futureitems.actions[0].init = function init (client, callback) { var translate = client.translate; var $status = $('#admin_' + futureitems.name + '_0_status'); - + function valueOrEmpty (value) { return value ? value : ''; } - + function showOneTreatment (tr, table) { - table.append($('').css('background-color','#0f0f0f') - .append($('').attr('width','20%').append(new Date(tr.created_at).toLocaleString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'))) - .append($('').attr('width','20%').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) : '')) - .append($('').attr('width','10%').attr('align','center').append(tr.glucose ? tr.glucose + ' ('+translate(tr.glucoseType)+')' : '')) - .append($('').attr('width','10%').attr('align','center').append(valueOrEmpty(tr.insulin))) - .append($('').attr('width','10%').attr('align','center').append(valueOrEmpty(tr.carbs))) - .append($('').attr('width','10%').append(valueOrEmpty(tr.enteredBy))) - .append($('').attr('width','20%').append(valueOrEmpty(tr.notes))) + table.append($('').css('background-color', '#0f0f0f') + .append($('').attr('width', '20%').append(new Date(tr.created_at).toLocaleString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'))) + .append($('').attr('width', '20%').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) : '')) + .append($('').attr('width', '10%').attr('align', 'center').append(tr.glucose ? tr.glucose + ' (' + translate(tr.glucoseType) + ')' : '')) + .append($('').attr('width', '10%').attr('align', 'center').append(valueOrEmpty(tr.insulin))) + .append($('').attr('width', '10%').attr('align', 'center').append(valueOrEmpty(tr.carbs))) + .append($('').attr('width', '10%').append(valueOrEmpty(tr.enteredBy))) + .append($('').attr('width', '20%').append(valueOrEmpty(tr.notes))) ); } - - function showTreatments(treatments, table) { - table.append($('').css('background','#040404') - .append($('').css('width','80px').attr('align','left').append(translate('Time'))) - .append($('').css('width','150px').attr('align','left').append(translate('Event Type'))) - .append($('').css('width','150px').attr('align','left').append(translate('Blood Glucose'))) - .append($('').css('width','50px').attr('align','left').append(translate('Insulin'))) - .append($('').css('width','50px').attr('align','left').append(translate('Carbs'))) - .append($('').css('width','150px').attr('align','left').append(translate('Entered By'))) - .append($('').css('width','300px').attr('align','left').append(translate('Notes'))) + + function showTreatments (treatments, table) { + table.append($('').css('background', '#040404') + .append($('').css('width', '80px').attr('align', 'left').append(translate('Time'))) + .append($('').css('width', '150px').attr('align', 'left').append(translate('Event Type'))) + .append($('').css('width', '150px').attr('align', 'left').append(translate('Blood Glucose'))) + .append($('').css('width', '50px').attr('align', 'left').append(translate('Insulin'))) + .append($('').css('width', '50px').attr('align', 'left').append(translate('Carbs'))) + .append($('').css('width', '150px').attr('align', 'left').append(translate('Entered By'))) + .append($('').css('width', '300px').attr('align', 'left').append(translate('Notes'))) ); - for (var t=0; t').css('margin-top','10px'); + $status.hide().text(translate('Database contains %1 future records', { params: [records.length] })).fadeIn('slow'); + var table = $('').css('margin-top', '10px'); $('#admin_' + futureitems.name + '_0_html').append(table); showTreatments(records, table); futureitems.actions[0].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); - }, - error: function () { + } + , error: function() { $status.hide().text(translate('Error loading database')).fadeIn('slow'); futureitems.treatmentrecords = []; } - }).done(function () { if (callback) { callback(); } }); + }).done(function() { if (callback) { callback(); } }); }; -futureitems.actions[0].code = function deleteRecords(client, callback) { +futureitems.actions[0].code = function deleteRecords (client, callback) { var translate = client.translate; var $status = $('#admin_' + futureitems.name + '_0_status'); - + if (!client.hashauth.isAuthenticated()) { alert(translate('Your device is not authenticated yet')); if (callback) { callback(); } return; - }; + } function deleteRecordById (_id) { $.ajax({ - method: 'DELETE' + method: 'DELETE' , url: '/api/v1/treatments/' + _id - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function success () { $status.text(translate('Record %1 removed ...', { params: [_id] })); - }).fail(function fail() { - $status.text(translate('Error removing record %1', { params: [_id] })); + }).fail(function fail () { + $status.text(translate('Error removing record %1', { params: [_id] })); }); } - + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); for (var i = 0; i < futureitems.treatmentrecords.length; i++) { deleteRecordById(futureitems.treatmentrecords[i]._id); } $('#admin_' + futureitems.name + '_0_html').html(''); - + if (callback) { callback(); } }; -futureitems.actions[1].init = function init(client, callback) { +futureitems.actions[1].init = function init (client, callback) { var translate = client.translate; var $status = $('#admin_' + futureitems.name + '_1_status'); - + $status.hide().text(translate('Loading database ...')).fadeIn('slow'); var now = new Date().getTime(); - $.ajax('/api/v1/entries.json?&find[date][$gte]=' + now, { - success: function (records) { + $.ajax('/api/v1/entries.json?&find[date][$gte]=' + now + '&count=288', { + headers: client.headers() + , success: function(records) { futureitems.entriesrecords = records; - $status.hide().text(translate('Database contains %1 future records',{ params: [records.length] })).fadeIn('slow'); + $status.hide().text(translate('Database contains %1 future records', { params: [records.length] })).fadeIn('slow'); futureitems.actions[1].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); - }, - error: function () { + } + , error: function() { $status.hide().text(translate('Error loading database')).fadeIn('slow'); futureitems.entriesrecords = []; } - }).done(function () { if (callback) { callback(); } }); + }).done(function() { if (callback) { callback(); } }); }; -futureitems.actions[1].code = function deleteRecords(client, callback) { +futureitems.actions[1].code = function deleteRecords (client, callback) { var translate = client.translate; var $status = $('#admin_' + futureitems.name + '_1_status'); - + if (!client.hashauth.isAuthenticated()) { alert(translate('Your device is not authenticated yet')); if (callback) { callback(); } return; - }; - + } + function deteleteRecordById (_id) { $.ajax({ - method: 'DELETE' + method: 'DELETE' , url: '/api/v1/entries/' + _id - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function success () { $status.text(translate('Record %1 removed ...', { params: [_id] })); - }).fail(function fail() { - $status.text(translate('Error removing record %1', { params: [_id] })); + }).fail(function fail () { + $status.text(translate('Error removing record %1', { params: [_id] })); }); } - $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); for (var i = 0; i < futureitems.entriesrecords.length; i++) { deteleteRecordById(futureitems.entriesrecords[i]._id); } - + if (callback) { callback(); } diff --git a/lib/admin_plugins/index.js b/lib/admin_plugins/index.js index f7a5c74133d..8bc3f1ae0ba 100644 --- a/lib/admin_plugins/index.js +++ b/lib/admin_plugins/index.js @@ -1,23 +1,28 @@ 'use strict'; -var _ = require('lodash'); +var _find = require('lodash/find'); +var _each = require('lodash/each'); -function init() { +function init(ctx) { var allPlugins = [ - require('./cleanstatusdb')() - , require('./futureitems')() + require('./subjects')(ctx) + , require('./roles')(ctx) + , require('./cleanstatusdb')(ctx) + , require('./cleantreatmentsdb')(ctx) + , require('./cleanentriesdb')(ctx) + , require('./futureitems')(ctx) ]; function plugins(name) { if (name) { - return _.find(allPlugins, {name: name}); + return _find(allPlugins, {name: name}); } else { return plugins; } } plugins.eachPlugin = function eachPlugin(f) { - _.each(allPlugins, f); + _each(allPlugins, f); }; plugins.createHTML = function createHTML(client) { @@ -32,8 +37,10 @@ function init() { } var a = p.actions[i]; // add main plugin html - fs.append($('').css('text-decoration','underline').append(translate(a.name))); - fs.append('
'); + if (a.name) { + fs.append($('').css('text-decoration','underline').append(translate(a.name))); + fs.append('
'); + } fs.append($('').append(translate(a.description))); fs.append($('
').attr('id','admin_' + p.name + '_' + i + '_html')); fs.append($('
').css('margin-top', '10px'); + $('#admin_' + roles.name + '_0_html').append(table).append(genDialog(client)); + reload(client, callback); + } + , preventClose: true + , code: function createNewRole (client, callback) { + var role = {}; + openDialog(role, client, callback); + } +}]; + +function createOrSaveRole (role, client, callback) { + + var method = _.isEmpty(role._id) ? 'POST' : 'PUT'; + + $.ajax({ + method: method + , url: '/api/v2/authorization/roles/' + , headers: client.headers() + , data: role + }).done(function success () { + reload(client, callback); + }).fail(function fail (err) { + console.error('Unable to ' + method + ' Role', err.responseText); + window.alert(client.translate('Unable to save Role')); + if (callback) { + callback(err); + } + }); +} + +function deleteRole (role, client, callback) { + $.ajax({ + method: 'DELETE' + , url: '/api/v2/authorization/roles/' + role._id + , headers: client.headers() + }).done(function success () { + reload(client, callback); + }).fail(function fail (err) { + console.error('Unable to delete Role', err.responseText); + window.alert(client.translate('Unable to delete Role')); + if (callback) { + callback(err); + } + }); +} + +function reload (client, callback) { + $.ajax({ + method: 'GET' + , url: '/api/v2/authorization/roles' + , headers: client.headers() + }).done(function success (records) { + roles.records = records; + $status.hide().text(client.translate('Database contains %1 roles', { params: [records.length] })).fadeIn('slow'); + showRoles(records, client); + if (callback) { + callback(); + } + }).fail(function fail (err) { + $status.hide().text(client.translate('Error loading database')).fadeIn('slow'); + roles.records = []; + if (callback) { + callback(err); + } + }); +} + +function genDialog (client) { + var ret = + ''; + + return $(ret); +} + +function openDialog (role, client) { + $('#editroledialog').dialog({ + width: 360 + , height: 360 + , buttons: [ + { + text: client.translate('Save') + , class: 'leftButton' + , click: function() { + + role.name = $('#edrole_name').val(); + role.permissions = + _.chain($('#edrole_permissions').val().toLowerCase().split(/[;, ]/)) + .map(_.trim) + .reject(_.isEmpty) + .sort() + .value(); + role.notes = $('#edrole_notes').val(); + + var self = this; + delete role.autoGenerated; + createOrSaveRole(role, client, function callback () { + $(self).dialog('close'); + }); + } + } + , { + text: client.translate('Cancel') + , click: function() { $(this).dialog('close'); } + } + ] + , open: function() { + $(this).parent().css('box-shadow', '20px 20px 20px 0px black'); + $(this).parent().find('.ui-dialog-buttonset').css({ 'width': '100%', 'text-align': 'right' }); + $(this).parent().find('button:contains("' + client.translate('Save') + '")').css({ 'float': 'left' }); + $('#edrole_name').val(role.name || '').focus(); + $('#edrole_permissions').val(role.permissions ? role.permissions.join(' ') : ''); + $('#edrole_notes').val(role.notes || ''); + } + + }); + +} + +function showRole (role, table, client) { + var editIcon = $(''); + editIcon.click(function clicked () { + openDialog(role, client); + }); + + var deleteIcon = ''; + if (role._id) { + deleteIcon = $(''); + deleteIcon.click(function clicked () { + var ok = window.confirm(client.translate('Are you sure you want to delete: ') + role.name); + if (ok) { + deleteRole(role, client); + } + }); + } + + table.append($('').css('background-color', '#0f0f0f') + .append($('').css('background', '#040404') + .append($('
').attr('width', '20%').append(editIcon).append(deleteIcon).append(role.name)) + .append($('').attr('width', '20%').append(_.isEmpty(role.permissions) ? '[none]' : role.permissions.join(' '))) + .append($('').attr('width', '10%').append(role._id ? (role.notes ? role.notes : '') : '[system default]')) + ); +} + +function showRoles (roles, client) { + var table = $('#admin_roles_table'); + table.empty().append($('
').css('width', '100px').attr('align', 'left').append(client.translate('Name'))) + .append($('').css('width', '150px').attr('align', 'left').append(client.translate('Permissions'))) + .append($('').css('width', '150px').attr('align', 'left').append(client.translate('Notes'))) + ); + for (var t = 0; t < roles.length; t++) { + showRole(roles[t], table, client); + } +} diff --git a/lib/admin_plugins/subjects.js b/lib/admin_plugins/subjects.js new file mode 100644 index 00000000000..b1123029eaf --- /dev/null +++ b/lib/admin_plugins/subjects.js @@ -0,0 +1,185 @@ +'use strict'; + +const _ = require('lodash'); + +var subjects = { + name: 'subjects' + , label: 'Subjects - People, Devices, etc' + , pluginType: 'admin' +}; + +function init () { + return subjects; +} + +module.exports = init; + +var $status = null; + +subjects.actions = [{ + description: 'Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.' + , buttonLabel: 'Add new Subject' + , init: function init (client, callback) { + $status = $('#admin_' + subjects.name + '_0_status'); + $status.hide().text(client.translate('Loading database ...')).fadeIn('slow'); + var table = $('').css('margin-top', '10px'); + $('#admin_' + subjects.name + '_0_html').append(table).append(genDialog(client)); + reload(client, callback); + } + , preventClose: true + , code: function createNewSubject (client, callback) { + openDialog({}, client, callback); + } +}]; + +function createOrSaveSubject (subject, client, callback) { + + var method = _.isEmpty(subject._id) ? 'POST' : 'PUT'; + + $.ajax({ + method: method + , url: '/api/v2/authorization/subjects/' + , headers: client.headers() + , data: subject + }).done(function success () { + reload(client, callback); + }).fail(function fail (err) { + console.error('Unable to ' + method + ' Subject', err.responseText); + window.alert(client.translate('Unable to save Subject')); + if (callback) { + callback(); + } + }); +} + +function deleteSubject (subject, client, callback) { + $.ajax({ + method: 'DELETE' + , url: '/api/v2/authorization/subjects/' + subject._id + , headers: client.headers() + }).done(function success () { + reload(client, callback); + }).fail(function fail (err) { + console.error('Unable to delete Subject', err.responseText); + window.alert(client.translate('Unable to delete Subject')); + if (callback) { + callback(); + } + }); +} + +function reload (client, callback) { + $.ajax({ + method: 'GET' + , url: '/api/v2/authorization/subjects' + , headers: client.headers() + }).done(function success (records) { + subjects.records = records; + $status.hide().text(client.translate('Database contains %1 subjects', { params: [records.length] })).fadeIn('slow'); + showSubjects(records, client); + if (callback) { + callback(); + } + }).fail(function fail (err) { + $status.hide().text(client.translate('Error loading database')).fadeIn('slow'); + subjects.records = []; + if (callback) { + callback(err); + } + }); +} + +function genDialog (client) { + var ret = + ''; + + return $(ret); +} + +function openDialog (subject, client) { + $('#editsubjectdialog').dialog({ + width: 360 + , height: 300 + , buttons: [ + { + text: client.translate('Save') + , class: 'leftButton' + , click: function() { + subject.name = $('#edsub_name').val(); + subject.roles = + _.chain($('#edsub_roles').val().toLowerCase().split(/[;, ]/)) + .map(_.trim) + .reject(_.isEmpty) + .sort() + .value(); + subject.notes = $('#edsub_notes').val(); + + var self = this; + createOrSaveSubject(subject, client, function callback () { + $(self).dialog('close'); + }); + } + } + , { + text: client.translate('Cancel') + , click: function() { $(this).dialog('close'); } + } + ] + , open: function() { + $(this).parent().css('box-shadow', '20px 20px 20px 0px black'); + $(this).parent().find('.ui-dialog-buttonset').css({ 'width': '100%', 'text-align': 'right' }); + $(this).parent().find('button:contains("' + client.translate('Save') + '")').css({ 'float': 'left' }); + $('#edsub_name').val(subject.name || '').focus(); + $('#edsub_roles').val(subject.roles ? subject.roles.join(', ') : ''); + $('#edsub_notes').val(subject.notes || ''); + } + + }); + +} + +function showSubject (subject, table, client) { + var editIcon = $(''); + editIcon.click(function clicked () { + openDialog(subject, client); + }); + var deleteIcon = $(''); + deleteIcon.click(function clicked () { + var ok = window.confirm(client.translate('Are you sure you want to delete: ') + subject.name); + if (ok) { + deleteSubject(subject, client); + } + }); + table.append($('').css('background-color', '#0f0f0f') + .append($('').css('background', '#040404') + .append($(''); var select = $('').appendTo(table); + if (positiveTemps > 0 || negativeTemps > 0) { + $('').appendTo(table); + } + if (positiveTemps > 0) { + $('').appendTo(table); + } + if (negativeTemps < 0) { + $('').appendTo(table); } + var totalBasalInsulin = baseBasalInsulin + positiveTemps + negativeTemps; + $('').appendTo(table); + var totalDailyInsulin = bolusInsulin + baseBasalInsulin + positiveTemps + negativeTemps; + $('').appendTo(table); + + $('').appendTo(table); + $('').appendTo(table); + $('').appendTo(table); + + $('').appendTo(table); + $('#daytodaystatchart-' + day).append(table); + + var chartData = [ + { + label: translate('Basal') + , count: totalBasalInsulin + , pct: (totalBasalInsulin / totalDailyInsulin * 100).toFixed(0) + } + , { label: translate('Bolus'), count: bolusInsulin, pct: (bolusInsulin / totalDailyInsulin * 100).toFixed(0) } + ]; + + // Insulin distribution chart + var width = 120; + var height = 120; + var radius = Math.min(width, height) / 2; + + var color = d3.scaleOrdinal().range([basalcolor, boluscolor]); + + var labelArc = d3.arc() + .outerRadius(radius / 2) + .innerRadius(radius / 2); + + var svg = d3.select('#daytodaystatinsulinpiechart-' + day) + .append('svg') + .attr('width', width) + .attr('height', height) + .append('g') + .attr('transform', 'translate(' + (width / 2) + + ',' + (height / 2) + ')'); + + var arc = d3.arc() + .innerRadius(0) + .outerRadius(radius); + + var pie = d3.pie() + .value(function(d) { + return d.count; + }) + .sort(null); + + var insulg = svg.selectAll('.insulinarc') + .data(pie(chartData)) + .enter() + .append('g') + .attr('class', 'insulinarc'); + + insulg.append('path') + .attr('d', arc) + .attr('opacity', '0.5') + .attr('fill', function(d) { + return color(d.data.label); + }); + + insulg.append('text') + .attr('transform', function(d) { + return 'translate(' + labelArc.centroid(d) + ')'; + }) + .attr('dy', '.15em') + .style('font-weight', 'bold') + .attr('text-anchor', 'middle') + .text(function(d) { + return d.data.pct + '%'; + }); + + // Carbs pie chart + + var carbscolor = d3.scaleOrdinal().range(['red']); + + var carbsData = [ + { label: translate('Carbs'), count: data.dailyCarbs } + ]; + + var carbssvg = d3.select('#daytodaystatcarbspiechart-' + day) + .append('svg') + .attr('width', width) + .attr('height', height) + .append('g') + .attr('transform', 'translate(' + (width / 2) + + ',' + (height / 2) + ')'); + + var carbsarc = d3.arc() + .outerRadius(radius * data.dailyCarbs / options.maxDailyCarbsValue); + + var carbspie = d3.pie() + .value(function(d) { + return d.count; + }) + .sort(null); + + var carbsg = carbssvg.selectAll('.carbsarc') + .data(carbspie(carbsData)) + .enter() + .append('g') + .attr('class', 'carbsarc'); + + carbsg.append('path') + .attr('d', carbsarc) + .attr('opacity', '0.5') + .attr('fill', function(d) { + return carbscolor(d.data.label); + }); + + carbsg.append('text') + .attr('transform', function() { + return 'translate(0,0)'; + }) + .attr('dy', '.15em') + .style('font-weight', 'bold') + .attr('text-anchor', 'middle') + .text(function(d) { + return d.data.count + 'g'; + }); + } + + tddSum += totalDailyInsulin; + basalSum += totalBasalInsulin; + baseBasalSum += baseBasalInsulin; + bolusSum += bolusInsulin; + carbsSum += data.dailyCarbs; + proteinSum += data.dailyProtein; + fatSum += data.dailyFat; + + appendProfileSwitch(context, { + //eventType: 'Profile Switch' + profile: client.profilefunctions.activeProfileToTime(from) + , mills: from + times.mins(15).msecs + , first: true }); + + function appendProfileSwitch (context, treatment) { + + if (!treatment.cutting && !treatment.profile) { return; } + + var sign = treatment.first ? '▲▲▲' : '▬▬▬'; + var text; + if (treatment.cutting) { + text = sign + ' ' + client.profilefunctions.profileSwitchName(treatment.cutting) + ' ' + '►►►' + ' ' + client.profilefunctions.profileSwitchName(treatment.profile) + ' ' + sign; + } else { + text = sign + ' ' + client.profilefunctions.profileSwitchName(treatment.profile) + ' ' + sign; + } + context.append('text') + .style('font-size', 12) + .style('font-weight', 'bold') + .attr('fill', '#0099ff') + .attr('text-anchor', 'start') + .attr('dy', '.35em') + .attr('transform', 'rotate(-90 ' + (xScale2(treatment.mills) + padding.left) + ',' + (yScaleBasals(0) + padding.top - 10) + ') ' + + 'translate(' + (xScale2(treatment.mills) + padding.left) + ',' + (yScaleBasals(0) + padding.top - 10) + ')') + .text(text); + } + + console.log("Rendering " + day, new Date().getTime() - timestart.getTime(), "msecs"); + } + + function hideTooltip () { + client.tooltip.style('opacity', 0); } }; diff --git a/lib/report_plugins/glucosedistribution.js b/lib/report_plugins/glucosedistribution.js index 30f62761b3a..21d5213e9c9 100644 --- a/lib/report_plugins/glucosedistribution.js +++ b/lib/report_plugins/glucosedistribution.js @@ -1,87 +1,267 @@ 'use strict'; +var consts = require('../constants'); + var glucosedistribution = { name: 'glucosedistribution' , label: 'Distribution' , pluginType: 'report' }; -function init() { +function init () { return glucosedistribution; } module.exports = init; -glucosedistribution.html = function html(client) { +glucosedistribution.html = function html (client) { var translate = client.translate; var ret = - '

' - + translate('Glucose distribution') - + ' (' - + ' ' - + ' )' - + '

' - + '
' - + '
' - + '
' - + '
' - + '* ' + translate('This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from: Nathan, David M., et al. "Translating the A1C assay into estimated average glucose values." Diabetes care 31.8 (2008): 1473-1478.') - ; + '

' + + translate('Glucose distribution') + + ' (' + + '' + + ')' + + '

' + + '
').attr('width', '20%').append(editIcon).append(deleteIcon).append(subject.name)) + .append($('').attr('width', '20%').append(subject.roles ? subject.roles.join(', ') : '[none]')) + .append($('').attr('width', '20%').append('' + subject.accessToken + '')) + .append($('').attr('width', '10%').append(subject.notes ? subject.notes : '')) + ); +} + +function showSubjects (subjects, client) { + var table = $('#admin_subjects_table'); + table.empty().append($('
').css('width', '100px').attr('align', 'left').append(client.translate('Name'))) + .append($('').css('width', '150px').attr('align', 'left').append(client.translate('Roles'))) + .append($('').css('width', '150px').attr('align', 'left').append(client.translate('Access Token'))) + .append($('').css('width', '150px').attr('align', 'left').append(client.translate('Notes'))) + ); + for (var t = 0; t < subjects.length; t++) { + showSubject(subjects[t], table, client); + } +} diff --git a/lib/adminnotifies.js b/lib/adminnotifies.js new file mode 100644 index 00000000000..7c3603609f6 --- /dev/null +++ b/lib/adminnotifies.js @@ -0,0 +1,57 @@ +'use strict'; + +const _ = require('lodash'); + +function init (ctx) { + + const adminnotifies = {}; + + adminnotifies.addNotify = function addnotify (notify) { + if (!ctx.settings.adminNotifiesEnabled) { + console.log('Admin notifies disabled, skipping notify', notify); + return; + } + + 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; diff --git a/lib/api/activity/index.js b/lib/api/activity/index.js new file mode 100644 index 00000000000..7d67926b4d0 --- /dev/null +++ b/lib/api/activity/index.js @@ -0,0 +1,120 @@ +'use strict'; + +var _forEach = require('lodash/forEach'); +var _isNil = require('lodash/isNil'); +var _isArray = require('lodash/isArray'); + +var consts = require('../../constants'); +var moment = require('moment'); + +function configure(app, wares, ctx) { + var express = require('express') + , api = express.Router(); + + api.use(wares.compression()); + // text body types get handled as raw buffer stream + api.use(wares.rawParser); + // json body types get handled as parsed json + api.use(wares.bodyParser.json({ + limit: '50Mb' + })); + // also support url-encoded content-type + api.use(wares.urlencodedParser); + // invoke common middleware + api.use(wares.sendJSONStatus); + + api.use(ctx.authorization.isPermitted('api:activity:read')); + + // List activity data available + api.get('/activity', function(req, res) { + var ifModifiedSince = req.get('If-Modified-Since'); + ctx.activity.list(req.query, function(err, results) { + var d1 = null; + + _forEach(results, function clean(t) { + + var d2 = null; + + if (Object.prototype.hasOwnProperty.call(t, 'created_at')) { + d2 = new Date(t.created_at); + } else { + if (Object.prototype.hasOwnProperty.call(t, 'timestamp')) { + d2 = new Date(t.timestamp); + } + } + + if (d2 == null) { return; } + + if (d1 == null || d2.getTime() > d1.getTime()) { + d1 = d2; + } + }); + + if (!_isNil(d1)) res.setHeader('Last-Modified', d1.toUTCString()); + + if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) { + res.status(304).send({ + status: 304 + , message: 'Not modified' + , type: 'internal' + }); + return; + } else { + return res.json(results); + } + }); + }); + + function config_authed(app, api, wares, ctx) { + + function post_response(req, res) { + var activity = req.body; + + if (!_isArray(activity)) { + activity = [activity]; + } + + ctx.activity.create(activity, function(err, created) { + if (err) { + console.log('Error adding activity data', err); + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + console.log('Activity measure created'); + res.json(created); + } + }); + } + + api.post('/activity/', ctx.authorization.isPermitted('api:activity:create'), post_response); + + api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity:delete'), function(req, res) { + ctx.activity.remove(req.params._id, function() { + res.json({}); + }); + }); + + // update record + api.put('/activity/', ctx.authorization.isPermitted('api:activity:update'), function(req, res) { + var data = req.body; + ctx.activity.save(data, function(err, created) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + console.log('Error saving activity'); + console.log(err); + } else { + res.json(created); + console.log('Activity measure saved', data); + } + }); + }); + } + + if (app.enabled('api') && app.enabled('careportal')) { + config_authed(app, api, wares, ctx); + } + + return api; +} + +module.exports = configure; + diff --git a/lib/api/adminnotifiesapi.js b/lib/api/adminnotifiesapi.js new file mode 100644 index 00000000000..7579d8e3a79 --- /dev/null +++ b/lib/api/adminnotifiesapi.js @@ -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; diff --git a/lib/api/alexa/index.js b/lib/api/alexa/index.js new file mode 100644 index 00000000000..fcdaa2c012b --- /dev/null +++ b/lib/api/alexa/index.js @@ -0,0 +1,109 @@ +'use strict'; + +var _ = require('lodash'); +var moment = require('moment'); + +function configure (app, wares, ctx, env) { + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.rawParser); + // json body types get handled as parsed json + api.use(wares.jsonParser); + // also support url-encoded content-type + api.use(wares.urlencodedParser); + // text body types get handled as raw buffer stream + + ctx.virtAsstBase.setupVirtAsstHandlers(ctx.alexa); + + api.post('/alexa', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Alexa'); + var locale = _.get(req, 'body.request.locale'); + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } + + switch (req.body.request.type) { + case 'SessionEndedRequest': + onSessionEnded(function () { + res.json(''); + next( ); + }); + break; + case 'LaunchRequest': + if (!req.body.request.intent) { + onLaunch(function () { + res.json(ctx.alexa.buildSpeechletResponse( + translate('virtAsstTitleLaunch'), + translate('virtAsstLaunch'), + translate('virtAsstLaunch'), + false + )); + next( ); + }); + break; + } + // if intent is set then fallback to IntentRequest + case 'IntentRequest': // eslint-disable-line no-fallthrough + onIntent(req.body.request.intent, function (title, response) { + res.json(ctx.alexa.buildSpeechletResponse(title, response, '', true)); + next( ); + }); + break; + } + }); + + ctx.virtAsstBase.setupMutualIntents(ctx.alexa); + + function onLaunch(next) { + console.log('Session launched'); + next( ); + } + + function onIntent(intent, next) { + console.log('Received intent request'); + console.log(JSON.stringify(intent)); + handleIntent(intent.name, intent.slots, next); + } + + function onSessionEnded(next) { + console.log('Session ended'); + next( ); + } + + function handleIntent(intentName, slots, next) { + var metric; + if (slots) { + var slotStatus = _.get(slots, 'metric.resolutions.resolutionsPerAuthority[0].status.code'); + var slotName = _.get(slots, 'metric.resolutions.resolutionsPerAuthority[0].values[0].value.name'); + if (slotStatus == "ER_SUCCESS_MATCH" && slotName) { + metric = slotName; + } else { + next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); + return; + } + } + + var handler = ctx.alexa.getIntentHandler(intentName, metric); + if (handler){ + var sbx = ctx.sbx; + handler(next, slots, sbx); + return; + } else { + next(translate('virtAsstUnknownIntentTitle'), translate('virtAsstUnknownIntentText')); + return; + } + } + + return api; +} + +module.exports = configure; diff --git a/lib/api/const.json b/lib/api/const.json new file mode 100644 index 00000000000..cb1421d8520 --- /dev/null +++ b/lib/api/const.json @@ -0,0 +1,4 @@ +{ + "API1_VERSION": "1.0.0", + "API2_VERSION": "2.0.0" +} \ No newline at end of file diff --git a/lib/api/devicestatus/index.js b/lib/api/devicestatus/index.js index dc45e5bc061..c6e981da46a 100644 --- a/lib/api/devicestatus/index.js +++ b/lib/api/devicestatus/index.js @@ -1,19 +1,45 @@ 'use strict'; -var consts = require('../../constants'); +const consts = require('../../constants'); +const moment = require('moment'); +const { query } = require('express'); +const _take = require('lodash/take'); +const _ = require('lodash'); -function configure (app, wares, ctx) { - var express = require('express'), - api = express.Router( ); +function configure (app, wares, ctx, env) { + var express = require('express') + , api = express.Router(); // invoke common middleware api.use(wares.sendJSONStatus); // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw( )); + api.use(wares.rawParser); // json body types get handled as parsed json - api.use(wares.bodyParser.json( )); + api.use(wares.jsonParser); // also support url-encoded content-type - api.use(wares.bodyParser.urlencoded({ extended: true })); + api.use(wares.urlencodedParser); + // text body types get handled as raw buffer stream + + api.use(ctx.authorization.isPermitted('api:devicestatus:read')); + + function processDates(results) { + // Support date de-normalization for older clients + + if (env.settings.deNormalizeDates) { + const r = []; + results.forEach(function(e) { + if (e.created_at && Object.prototype.hasOwnProperty.call(e, 'utcOffset')) { + const d = moment(e.created_at).utcOffset(e.utcOffset); + e.created_at = d.toISOString(true); + delete e.utcOffset; + } + r.push(e); + }); + return r; + } else { + return results; + } + } // List settings available api.get('/devicestatus/', function(req, res) { @@ -21,37 +47,90 @@ function configure (app, wares, ctx) { if (!q.count) { q.count = 10; } - ctx.devicestatus.list(q, function (err, results) { - return res.json(results); + + const inMemoryData = ctx.cache.devicestatus ? ctx.cache.devicestatus : []; + const canServeFromMemory = inMemoryData.length >= q.count && Object.keys(q).length == 1 ? true : false; + + if (canServeFromMemory) { + const sorted = _.sortBy(inMemoryData, function(item) { + return -item.mills; + }); + + return res.json(processDates(_take(sorted, q.count))); + } + + ctx.devicestatus.list(q, function(err, results) { + return res.json(processDates(results)); }); }); function config_authed (app, api, wares, ctx) { - api.post('/devicestatus/', /*TODO: auth disabled for quick UI testing... wares.verifyAuthorization, */ function(req, res) { + function doPost (req, res) { var obj = req.body; - ctx.devicestatus.create(obj, function (err, created) { + + ctx.purifier.purifyObject(obj); + + ctx.devicestatus.create(obj, function(err, created) { if (err) { res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); } else { res.json(created); } }); - }); - // delete record - api.delete('/devicestatus/:_id', wares.verifyAuthorization, function(req, res) { - ctx.devicestatus.remove(req.params._id, function (err, removed) { + } + + api.post('/devicestatus/', ctx.authorization.isPermitted('api:devicestatus:create'), doPost); + + /** + * @function delete_records + * Delete devicestatus. The query logic works the same way as find/list. This + * endpoint uses same search logic to remove records from the database. + */ + function delete_records (req, res, next) { + var query = req.query; + if (!query.count) { + query.count = 10 + } + + console.log('Delete records with query: ', query); + + // remove using the query + ctx.devicestatus.remove(query, function(err, stat) { if (err) { - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); - } else { - res.json(removed); + console.log('devicestatus delete error: ', err); + return next(err); } + // yield some information about success of operation + res.json(stat); + + console.log('devicestatus records deleted'); + + return next(); }); - }); + } + api.delete('/devicestatus/:id', ctx.authorization.isPermitted('api:devicestatus:delete'), function(req, res, next) { + if (!req.query.find) { + req.query.find = { + _id: req.params.id + }; + } else { + req.query.find._id = req.params.id; + } + + if (req.query.find._id === '*') { + // match any record id + delete req.query.find._id; + } + next(); + }, delete_records); + + // delete record that match query + api.delete('/devicestatus/', ctx.authorization.isPermitted('api:devicestatus:delete'), delete_records); } - if (app.enabled('api') || true /*TODO: auth disabled for quick UI testing...*/) { + if (app.enabled('api')) { config_authed(app, api, wares, ctx); } @@ -59,4 +138,3 @@ function configure (app, wares, ctx) { } module.exports = configure; - diff --git a/lib/api/entries/index.js b/lib/api/entries/index.js index 2d285324786..0f1113ab2c6 100644 --- a/lib/api/entries/index.js +++ b/lib/api/entries/index.js @@ -1,608 +1,819 @@ 'use strict'; -var consts = require('../../constants'); -var es = require('event-stream'); -var sgvdata = require('sgvdata'); -var expand = require('expand-braces'); +const _last = require('lodash/last'); +const _isNil = require('lodash/isNil'); +const _first = require('lodash/first'); +const _includes = require('lodash/includes'); +const _ = require('lodash'); +const moment = require('moment'); + +const consts = require('../../constants'); +const es = require('event-stream'); +const braces = require('braces'); +const expand = braces.expand; + +const ID_PATTERN = /^[a-f\d]{24}$/; -var ID_PATTERN = /^[a-f\d]{24}$/; function isId (value) { - //TODO: why did we need tht length check? - return value && ID_PATTERN.test(value) && value.length === 24; + return ID_PATTERN.test(value); } /** - * @module Entries - * Entries module - */ + * @module Entries + * Entries module + */ /** - * @method configure - * Configure the entries module, given an existing express app, common - * middlewares, and the global app's `ctx`. - * @param Express app The express app we'll mount onto. - * @param Object wares Common middleware used by lots of apps. - * @param Object ctx The global ctx with all modules, storage, and event buses - * configured. - */ -function configure (app, wares, ctx) { + * @method configure + * Configure the entries module, given an existing express app, common + * middlewares, and the global app's `ctx`. + * @param Express app The express app we'll mount onto. + * @param Object wares Common middleware used by lots of apps. + * @param Object ctx The global ctx with all modules, storage, and event buses + * configured. + */ +function configure (app, wares, ctx, env) { // default storage biased towards entries. - var entries = ctx.entries; - var express = require('express'), - api = express.Router( ) - ; - - // invoke common middleware - api.use(wares.sendJSONStatus); - // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw()); - // json body types get handled as parsed json - api.use(wares.bodyParser.json()); - // shortcut to use extension to specify output content-type - api.use(wares.extensions([ - 'json', 'svg', 'csv', 'txt', 'png', 'html', 'tsv' + const entries = ctx.entries; + const express = require('express') + , api = express.Router(); + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.rawParser); + // json body types get handled as parsed json + api.use(wares.bodyParser.json({ + limit: '50Mb' + })); + // also support url-encoded content-type + api.use(wares.urlencodedParser); + // text body types get handled as raw buffer stream + // shortcut to use extension to specify output content-type + api.use(wares.extensions([ + 'json', 'svg', 'csv', 'txt', 'png', 'html', 'tsv' ])); - // also support url-encoded content-type - api.use(wares.bodyParser.urlencoded({ extended: true })); + api.use(ctx.authorization.isPermitted('api:entries:read')); + /** + * @method force_typed_data + * @returns {Stream} Creates a through stream which validates that all + * elements on the stream have a `type` field. + * Generate a stream that ensures elements have a `type` field. + */ + function force_typed_data (opts) { /** - * @method force_typed_data - * @returns {Stream} Creates a through stream which validates that all - * elements on the stream have a `type` field. - * Generate a stream that ensures elements have a `type` field. - */ - function force_typed_data (opts) { - /** - * @function sync - * Iterate over every element in the stream, enforcing some data type. - */ - function sync (data, next) { - // if element has no data type, but we know what the type should be - if (!data.type && opts.type) { - // bless absence with known type - data.type = opts.type; - } - // continue control flow to next element in the stream - next(null, data); + * @function sync + * Iterate over every element in the stream, enforcing some data type. + */ + function sync (data, next) { + // if element has no data type, but we know what the type should be + if (!data.type && opts.type) { + // bless absence with known type + // TODO: this doesn't work, it assigns the query rather tham the type + data.type = opts.type; } - // return configured stream - return es.map(sync); - } - /** - * @method format_entries - * A final middleware to send payloads assembled by previous middlewares - * out to the http client. - * We expect a payload to be attached to `res.entries`. - // Middleware to format any response involving entries. - */ - function format_entries (req, res) { - // deduce what type of records we might expect - var type_params = { - type: (req.query && req.query.find && req.query.find.type - && req.query.find.type !== req.params.model) - ? req.query.find.type : req.params.model - }; - // prepare a stream of elements from some prepared payload - var output = es.readArray(res.entries || [ ]); - // on other hand, if there's been some error, report that - if (res.entries_err) { - return res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', res.entries_err); - } - // if no error, format the payload - // The general pattern here is to create an output stream that reformats - // the data correctly into the desired representation. - // The stream logic allows some streams to ensure that some basic rules, - // such as enforcing a type property to exist, are followed. - return res.format({ - text: function ( ) { - // sgvdata knows how to format sgv entries as text - es.pipeline(output, sgvdata.format( ), res); - }, - json: function ( ) { - // so long as every element has a `type` field, and some kind of - // date, we'll consider it valid - es.pipeline(output, force_typed_data(type_params), es.writeArray(function (err, out) { - res.json(out); - })); + // Support date de-normalization for older clients + if (env.settings.deNormalizeDates) { + if (data.dateString && Object.prototype.hasOwnProperty.call(data, 'utcOffset')) { + const d = moment(data.dateString).utcOffset(data.utcOffset); + data.dateString = d.toISOString(true); + delete data.utcOffset; } - }); - } - - /** - * @method insert_entries - * middleware to process "uploads" of sgv data - * This inspects the http requests's incoming payload. This creates a - * validating stream for the appropriate type of payload, which is piped - * into the configured storage layer, saving the results in mongodb. - */ - // middleware to process "uploads" of sgv data - function insert_entries (req, res, next) { - // list of incoming records - var incoming = [ ]; - // Potentially a single json encoded body. - // This can happen from either an url-encoded or json content-type. - if ('date' in req.body) { - // add it to the incoming list - incoming.push(req.body); - } - // potentially a list of json entries - if (req.body.length) { - // add them to the list - incoming = incoming.concat(req.body); } - /** - * @function inputs - * @returns {ReadableStream} Readable stream with all incoming elements - * in the stream. - * in node, pipe is the most interoperable interface - * inputs returns a readable stream representing all the potential - * records from the HTTP body. - * Most content-types are handled by express middeware. - * However, text/* types are given to us as a raw buffer, this - * function switches between these two variants to find the - * correct input stream. - * stream, so use svgdata to handle those. - * The inputs stream always emits sgv json objects. - */ - function inputs ( ) { - var input; - // handle all text types - if (req.is('text/*')) { - // re-use the svgdata parsing stream - input = es.pipeline(req, sgvdata.parse( )); - return input; - } - // use established list - return es.readArray(incoming); - } + // continue control flow to next element in the stream + next(null, data); + } + // return configured stream + return es.map(sync); + } - /** - * @function persist - * @returns {WritableStream} a writable persistent storage stream - * Sends stream elements into storage layer. - * Configures the storage layer stream. - */ - function persist (fn) { - if (req.persist_entries) { - // store everything - return entries.persist(fn); - } - // support a preview mode, just lint everything - return es.pipeline(entries.map( ), es.writeArray(fn)); - } + // check for last modified from in-memory data - /** - * @function done - * Final callback store results on `res.entries`, after all I/O is done. - * store results and move to the next middleware - */ - function done (err, result) { - // assign payload - res.entries = result; - res.entries_err = err; - return next( ); - } + function ifModifiedSinceCTX (req, res, next) { + + var lastEntry = _last(ctx.ddata.sgvs); + var lastEntryDate = null; - // pipe everything to persistent storage - // when finished, pass to the next piece of middleware - es.pipeline(inputs( ), persist(done)); + if (!_isNil(lastEntry)) { + lastEntryDate = new Date(_last(ctx.ddata.sgvs).mills); + res.setHeader('Last-Modified', lastEntryDate.toUTCString()); } - /** - * @function prepReqModel - * @param {Request} req The request to inspect - * @param {String} model The name of the model to use if not found. - * Sets `req.query.find.type` to your chosen model. - */ - function prepReqModel(req, model) { - var type = model || 'sgv'; - if (!req.query.find) { - req.query.find = { - type: type - }; - } else { - req.query.find.type = type; - } + var ifModifiedSince = req.get('If-Modified-Since'); + if (!ifModifiedSince) { + return next(); } - /** - * @param model - * Prepare model based on explicit choice in route/path parameter. - */ - api.param('model', function (req, res, next, model) { - prepReqModel(req, model); - next( ); - }); + // console.log('CGM Entry request with If-Modified-Since: ', ifModifiedSince); - /** - * @module get#/entries/current - * @route - * Get last entry. - * @response /definitions/Entries - */ - api.get('/entries/current', function(req, res, next) { - //assume sgv - req.params.model = 'sgv'; - entries.list({count: 1}, function(err, records) { - res.entries = records; - res.entries_err = err; - return next( ); + if (lastEntryDate && lastEntryDate.getTime() <= Date.parse(ifModifiedSince)) { + console.log('Sending Not Modified'); + res.status(304).send({ + status: 304 + , message: 'Not modified' + , type: 'internal' }); - }, format_entries); + return; + } - /** - * @module get#/entries/:spec - * @route - * Fetch one entry by id - * @response /definitions/Entries - * @param String spec :spec is either the id of a record or model name to - * search. If it is an id, only the record with that id will be in the - * response. If the string is a model name, like `sgv`, `mbg`, et al, the - * usual query logic is performed biased towards that model type. - * Useful for filtering by type. - */ - api.get('/entries/:spec', function(req, res, next) { - if (isId(req.params.spec)) { - entries.getEntry(req.params.spec, function(err, entry) { - if (err) { return next(err); } - res.entries = [entry]; - res.entries_err = err; - req.query.find = req.query.find || {}; - if (entry) { - req.query.find.type = entry.type; - } else { - res.entries_err = 'No such id: \'' + req.params.spec + '\''; - } - next(); - }); - } else { - req.params.model = req.params.spec; - prepReqModel(req, req.params.model); - query_models(req, res, next); - } - }, format_entries); + return next(); + } + + /** + * @method format_entries + * A final middleware to send payloads assembled by previous middlewares + * out to the http client. + * We expect a payload to be attached to `res.entries`. + // Middleware to format any response involving entries. + */ + function format_entries (req, res) { + // deduce what type of records we might expect + var type_params = { + type: (req.query && req.query.find && req.query.find.type && + req.query.find.type !== req.params.model) ? + req.query.find.type : req.params.model + }; + + // f there's been some error, report that + if (res.entries_err) { + return res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', res.entries_err); + } + // IF-Modified-Since support - /** - * @module get#/entries - * @route - * @response /definitions/Entries - * Use the `find` parameter to generate mongo queries. - * Default is `count=10`, for only 10 latest entries, reverse sorted by - * `find[date]`. - * - */ - api.get('/entries', query_models, format_entries); + function compare (a, b) { + var a_field = a.mills ? a.mills : a.date; + var b_field = b.mills ? b.mills : b.date; - /** - * @function echo_query - * Output the generated query object itself, instead of the query results. - * Useful for understanding how REST api parameters translate into mongodb - * queries. - */ - function echo_query (req, res) { - var query = req.query; - // make a depth-wise copy of the original raw input - var input = JSON.parse(JSON.stringify(query)); - - // If "?count=" is present, use that number to decided how many to return. - if (!query.count) { - query.count = 10; - } - // bias towards entries, but allow expressing preference of storage layer - var storage = req.params.echo || 'entries'; + if (a_field > b_field) + return -1; + if (a_field < b_field) + return 1; + return 0; + } - // send payload with information about query itself - res.json({ query: ctx[storage].query_for(query), input: input, params: req.params, storage: storage}); + res.entries.sort(compare); + + var lastEntry = _first(res.entries); + var lastEntryDate = null; + + // prepare a stream of elements from some prepared payload + var output = es.readArray(res.entries || []); + + if (!_isNil(lastEntry)) { + if (lastEntry.mills) lastEntryDate = new Date(lastEntry.mills); + if (!lastEntry.mills && lastEntry.date) lastEntryDate = new Date(lastEntry.date); + res.setHeader('Last-Modified', lastEntryDate.toUTCString()); } - /** - * @function query_models - * Perform the standard query logic, translating API parameters into mongo - * db queries in a fairly regimented manner. - * This middleware executes the query, assigning the payload to results on - * `res.entries`. - */ - function query_models (req, res, next) { - var query = req.query; - - // If "?count=" is present, use that number to decided how many to return. - if (!query.count) { - query.count = 10; - } + var ifModifiedSince = req.get('If-Modified-Since'); - // bias to entries, but allow expressing a preference - var storage = req.storage || ctx.entries; - // perform the query - storage.list(query, function payload (err, entries) { - // assign payload - res.entries = entries; - res.entries_err = err; - return next( ); + if (lastEntryDate !== null && ifModifiedSince !== null && lastEntryDate.getTime() <= new Date(ifModifiedSince).getTime()) { + console.log('If-Modified-Since: ' + new Date(ifModifiedSince) + ' Last-Modified', lastEntryDate); + res.status(304).send({ + status: 304 + , message: 'Not modified' + , type: 'internal' }); + return; } - /** - * @function delete_records - * Delete entries. The query logic works the same way as find/list. This - * endpoint uses same search logic to remove records from the database. - */ - function delete_records (req, res, next) { - // bias towards model, but allow expressing a preference - if (!req.model) { - req.model = ctx.entries; + function formatWithSeparator (data, separator) { + if (data === null || data.constructor !== Array || data.length == 0) return ""; + + var outputdata = []; + data.forEach(function(e) { + var entry = { + "dateString": e.dateString + , "date": e.date + , "sgv": e.sgv + , "direction": e.direction + , "device": e.device + }; + outputdata.push(entry); + }); + + var fields = Object.keys(outputdata[0]); + var replacer = function(key, value) { + return value === null ? '' : value } - var query = req.query; - if (!query.count) { query.count = 10 } - // remove using the query - req.model.remove(query, function(err, stat) { - if (err) { - return next(err); - } - // yield some information about success of operation - res.json(stat); - return next( ); + var csv = outputdata.map(function(row) { + return fields.map(function(fieldName) { + return JSON.stringify(row[fieldName], replacer) + }).join(separator) }); + return csv.join('\r\n'); } - /** - * @param spec - * Middleware that prepares the :spec parameter in the routed path. - */ - api.param('spec', function (req, res, next, spec) { - if (isId(spec)) { - prepReqModel(req, req.params.model); - req.query = {find: {_id: req.params.spec}}; - } else { - prepReqModel(req, req.params.model); + // console.log(JSON.stringify(req.headers)); + + // if no error, format the payload + // The general pattern here is to create an output stream that reformats + // the data correctly into the desired representation. + // The stream logic allows some streams to ensure that some basic rules, + // such as enforcing a type property to exist, are followed. + return res.format({ + 'text/plain': function() { + es.pipeline(output, force_typed_data(type_params), es.writeArray(function(err, out) { + var output = formatWithSeparator(out, "\t"); + res.send(output); + })); } - next( ); - }); - - /** - * @param echo - * The echo parameter in the path routing parameters allows the echo - * endpoints to customize the storage layer. - */ - api.param('echo', function (req, res, next, echo) { - console.log('echo', echo); - if (!echo) { - req.params.echo = 'entries'; + , 'text/tab-separated-values': function() { + es.pipeline(output, force_typed_data(type_params), es.writeArray(function(err, out) { + var output = formatWithSeparator(out, '\t'); + res.send(output); + })); + } + , 'text/csv': function() { + es.pipeline(output, force_typed_data(type_params), es.writeArray(function(err, out) { + var output = formatWithSeparator(out, ','); + res.send(output); + })); + } + , 'application/json': function() { + // so long as every element has a `type` field, and some kind of + // date, we'll consider it valid + es.pipeline(output, force_typed_data(type_params), es.writeArray(function(err, out) { + res.json(out); + })); + } + , 'default': function() { + // Default to JSON output + // so long as every element has a `type` field, and some kind of + // date, we'll consider it valid + es.pipeline(output, force_typed_data(type_params), es.writeArray(function(err, out) { + res.json(out); + })); } - next( ); }); + } + + /** + * @method insert_entries + * middleware to process "uploads" of sgv data + * This inspects the http requests's incoming payload. This creates a + * validating stream for the appropriate type of payload, which is piped + * into the configured storage layer, saving the results in mongodb. + */ + // middleware to process "uploads" of sgv data + function insert_entries (req, res, next) { + // list of incoming records + var incoming = []; + // Potentially a single json encoded body. + // This can happen from either an url-encoded or json content-type. + if ('date' in req.body) { + // add it to the incoming list + incoming.push(req.body); + } + // potentially a list of json entries + if (req.body.length) { + // add them to the list + incoming = incoming.concat(req.body); + } - /** - * @module get#/echo/:echo/:model/:spec - * @routed - * Echo information about model/spec queries. - * Useful in understanding how REST API prepares queries against mongo. - */ - api.get('/echo/:echo/:model?/:spec?', echo_query); + for (let i = 0; i < incoming.length; i++) { + const e = incoming[i]; + ctx.purifier.purifyObject(e); + } /** - * Prepare regexp patterns based on `prefix`, and `regex` parameters. - * Translates `/:prefix/:regex` strings into fancy mongo queries. - * @method prep_patterns - * @params String prefix - * @params String regex - * This performs bash style brace/glob pattern expansion in order to generate flexible series of regex patterns. - * Very useful in querying across days, but constrained hours of time. - * Consider the following examples: -``` -curl -s -g 'http://localhost:1337/api/v1/times/2015-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv -curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv -curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv - -``` - */ - function prep_patterns (req, res, next) { - // initialize empty pattern list. - var pattern = [ ]; - // initialize a basic prefix - // also perform bash brace/glob-style expansion - var prefix = expand(req.params.prefix || '.*'); - - // if expansion leads to more than one prefix - if (prefix.length > 1) { - // pre-pend the prefix to the pattern list and wait to expand it as - // part of the full pattern - pattern.push('^' + req.params.prefix); - } - // append any regex parameters - if (req.params.regex) { - // prepend "match any" rule to their rule - pattern.push('.*' + req.params.regex); + * @function persist + * @returns {WritableStream} a writable persistent storage stream + * Sends stream elements into storage layer. + * Configures the storage layer stream. + */ + function persist (fn) { + if (req.persist_entries) { + // store everything + return entries.persist(fn); } - // create a single pattern with all inputs considered - // expand the pattern using bash/glob style brace expansion to generate - // an array of patterns. - pattern = expand(pattern.join('')); + // support a preview mode, just lint everything + return es.pipeline(entries.map(), es.writeArray(fn)); + } - /** - * Factory function to customize creation of RegExp patterns. - * @method iter_regex - * @param String prefix Default null - * @param String suffix Default null - * @returns function(pat) which turns the given pattern into a new - * RegExp with the prefix and suffix prepended, and appended, - * respectively. - */ - function iter_regex (prefix, suffix) { - /** - * @function make - * @returns RegExp Make a RegExp with configured prefix and suffix - */ - function make (pat) { - // concat the prefix, pattern, and suffix. - pat = (prefix ? prefix : '') + pat + (suffix ? suffix : ''); - // return RegExp. - return new RegExp(pat); - } - // return functor - return make; - } + /** + * @function done + * Final callback store results on `res.entries`, after all I/O is done. + * store results and move to the next middleware + */ + function done (err, result) { + // assign payload + res.entries = result; + res.entries_err = err; + return next(); + } - // save pattern for other middlewares, eg echo, query, etc. - req.pattern = pattern; - var matches = pattern.map(iter_regex( )); - // prepare the query against a configurable field name. - var field = req.patternField; - var query = { }; - query[field] = { - // $regex: prefix, - // configure query to perform regex against list of potential regexp - $in: matches + // pipe everything to persistent storage + // when finished, pass to the next piece of middleware + es.pipeline(es.readArray(incoming), persist(done)); + } + + /** + * @function prepReqModel + * @param {Request} req The request to inspect + * @param {String} model The name of the model to use if not found. + * Sets `req.query.find.type` to your chosen model. + */ + function prepReqModel (req, model) { + var type = model || 'sgv'; + if (!req.query.find) { + + req.query.find = { + type: type }; - if (prefix.length === 1) { - // If there is a single prefix pattern, mongo can optimize this against - // an indexed field - query[field].$regex = prefix.map(iter_regex('^')).pop( ); - } + } else { + req.query.find.type = type; + } + } + + /** + * @param model + * Prepare model based on explicit choice in route/path parameter. + */ + api.param('model', function(req, res, next, model) { + prepReqModel(req, model); + next(); + }); + + /** + * @module get#/entries/current + * @route + * Get last entry. + * @response /definitions/Entries + */ + api.get('/entries/current', function(req, res, next) { + //assume sgv + req.params.model = 'sgv'; + entries.list({ + count: 1 + }, function(err, records) { + res.entries = records; + res.entries_err = err; + return next(); + }); + }, wares.obscure_device, format_entries); + + /** + * @module get#/entries/:spec + * @route + * Fetch one entry by id + * @response /definitions/Entries + * @param String spec :spec is either the id of a record or model name to + * search. If it is an id, only the record with that id will be in the + * response. If the string is a model name, like `sgv`, `mbg`, et al, the + * usual query logic is performed biased towards that model type. + * Useful for filtering by type. + */ + api.get('/entries/:spec', function(req, res, next) { + if (isId(req.params.spec)) { + entries.getEntry(req.params.spec, function(err, entry) { + if (err) { + return next(err); + } + res.entries = [entry]; + res.entries_err = err; + req.query.find = req.query.find || {}; + if (entry) { + req.query.find.type = entry.type; + } else { + res.entries_err = 'No such id: \'' + req.params.spec + '\''; + } + next(); + }); + } else { + req.params.model = req.params.spec; + prepReqModel(req, req.params.model); + query_models(req, res, next); + } + }, wares.obscure_device, format_entries); + + /** + * @module get#/entries + * @route + * @response /definitions/Entries + * Use the `find` parameter to generate mongo queries. + * Default is `count=10`, for only 10 latest entries, reverse sorted by + * `find[date]`. + * + */ + api.get('/entries', ifModifiedSinceCTX, query_models, wares.obscure_device, format_entries); + + /** + * @function echo_query + * Output the generated query object itself, instead of the query results. + * Useful for understanding how REST api parameters translate into mongodb + * queries. + */ + function echo_query (req, res) { + var query = req.query; + // make a depth-wise copy of the original raw input + var input = JSON.parse(JSON.stringify(query)); + + // If "?count=" is present, use that number to decided how many to return. + if (!query.count) { + query.count = consts.ENTRIES_DEFAULT_COUNT; + } + // bias towards entries, but allow expressing preference of storage layer + var storage = req.params.echo || 'entries'; + + // send payload with information about query itself + res.json({ + query: ctx[storage].query_for(query) + , input: input + , params: req.params + , storage: storage + }); + } + + /** + * @function query_models + * Perform the standard query logic, translating API parameters into mongo + * db queries in a fairly regimented manner. + * This middleware executes the query, assigning the payload to results on + * `res.entries`. + * If the query can be served from data in the runtime ddata, use the cached + * data and don't query the database. + */ + function query_models (req, res, next) { + var query = req.query; + + // If "?count=" is present, use that number to decided how many to return. + if (!query.count) { + query.count = consts.ENTRIES_DEFAULT_COUNT; + } + + // Check if we can process the query using in-memory data only + let inMemoryPossible = true; + let typeQuery; - // Merge into existing query structure. - if (req.query.find) { - if (req.query.find[field]) { - req.query.find[field].$in = query[field].$in; + if (query.find) { + Object.keys(query.find).forEach(function(key) { + if (key == 'type') { + typeQuery = query.find[key]["$eq"]; + if (!typeQuery) typeQuery = query.find.type; } else { - req.query.find[field] = query[field]; + inMemoryPossible = false; } - } else { - req.query.find = query; + }); + } + + let inMemoryCollection; + + if (typeQuery) { + inMemoryCollection= _.filter(ctx.cache.entries, function checkType (object) { + if (typeQuery == 'sgv') return 'sgv' in object; + if (typeQuery == 'mbg') return 'mbg' in object; + if (typeQuery == 'cal') return object.type === 'cal'; + return false; + }); + } else { + inMemoryCollection = ctx.cache.getData('entries'); + } + + if (inMemoryPossible && query.count <= inMemoryCollection.length) { + res.entries = _.cloneDeep(_.take(inMemoryCollection,query.count)); + + for (let i = 0; i < res.entries.length; i++) { + let e = res.entries[i]; + e.mills = e.mills || e.date; } - // Also assist in querying for the requested type. - if (req.params.type) { - req.query.find.type = req.params.type; + + res.entries_err = null; + return next(); + } + + // If we get this far, query the database + // bias to entries, but allow expressing a preference + var storage = req.storage || ctx.entries; + // perform the query + storage.list(query, function payload (err, entries) { + // assign payload + res.entries = entries; + res.entries_err = err; + return next(); + }); + } + + function count_records (req, res, next) { + var query = req.query; + var storage = req.storage || ctx.entries; + storage.aggregate(query, function payload (err, entries) { + // assign payload + res.entries = entries; + res.entries_err = err; + return next(err); + }); + } + + function format_results (req, res, next) { + res.json(res.entries); + next(); + } + + /** + * @function delete_records + * Delete entries. The query logic works the same way as find/list. This + * endpoint uses same search logic to remove records from the database. + */ + function delete_records (req, res, next) { + // bias towards model, but allow expressing a preference + if (!req.model) { + req.model = ctx.entries; + } + var query = req.query; + if (!query.count) { + query.count = consts.ENTRIES_DEFAULT_COUNT; + } + // remove using the query + req.model.remove(query, function(err, stat) { + if (err) { + return next(err); } - next( ); + // yield some information about success of operation + res.json(stat); + return next(); + }); + } + + /** + * @param spec + * Middleware that prepares the :spec parameter in the routed path. + */ + api.param('spec', function(req, res, next, spec) { + if (isId(spec)) { + prepReqModel(req, req.params.model); + req.query = { + find: { + _id: req.params.spec + } + }; + } else { + prepReqModel(req, req.params.model); + } + next(); + }); + + /** + * @param echo + * The echo parameter in the path routing parameters allows the echo + * endpoints to customize the storage layer. + */ + api.param('echo', function(req, res, next, echo) { + console.log('echo', echo); + if (!echo) { + req.params.echo = 'entries'; + } + next(); + }); + + /** + * @module get#/echo/:echo/:model/:spec + * @routed + * Echo information about model/spec queries. + * Useful in understanding how REST API prepares queries against mongo. + */ + api.get('/echo/:echo/:model?/:spec?', echo_query); + + /** + * Prepare regexp patterns based on `prefix`, and `regex` parameters. + * Translates `/:prefix/:regex` strings into fancy mongo queries. + * @method prep_patterns + * @params String prefix + * @params String regex + * This performs bash style brace/glob pattern expansion in order to generate flexible series of regex patterns. + * Very useful in querying across days, but constrained hours of time. + * Consider the following examples: + ``` + curl -s -g 'http://localhost:1337/api/v1/times/2015-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv + curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv + curl -s -g 'http://localhost:1337/api/v1/times/20{14..15}/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120' | json -a dateString sgv + + ``` + */ + function prep_patterns (req, res, next) { + // initialize empty pattern list. + var pattern = []; + // initialize a basic prefix + // also perform bash brace/glob-style expansion + var prefix = expand(req.params.prefix || '.*'); + + // if expansion leads to more than one prefix + if (prefix.length > 1) { + // pre-pend the prefix to the pattern list and wait to expand it as + // part of the full pattern + pattern.push('^' + req.params.prefix); } + // append any regex parameters + if (req.params.regex) { + // prepend "match any" rule to their rule + pattern.push('.*' + req.params.regex); + } + // create a single pattern with all inputs considered + // expand the pattern using bash/glob style brace expansion to generate + // an array of patterns. + + pattern = expand(pattern.join('')); + if (pattern.length == 0) pattern = ['']; /** - * @method prep_pattern_field - * Ensure that `req.patternField` is set to assist other middleware in - * deciding which field to generate queries against. - * Default is `dateString`, because that's the iso8601 field for sgv - * entries. - */ - function prep_pattern_field (req, res, next) { - // If req.params.field from routed path parameter is available use it. - if (req.params.field) { - req.patternField = req.params.field; - } else { - // Default is `dateString`. - req.patternField = 'dateString'; + * Factory function to customize creation of RegExp patterns. + * @method iter_regex + * @param String prefix Default null + * @param String suffix Default null + * @returns function(pat) which turns the given pattern into a new + * RegExp with the prefix and suffix prepended, and appended, + * respectively. + */ + function iter_regex (prefix, suffix) { + /** + * @function make + * @returns RegExp Make a RegExp with configured prefix and suffix + */ + function make (pat) { + // concat the prefix, pattern, and suffix. + pat = (prefix ? prefix : '') + pat + (suffix ? suffix : ''); + // return RegExp. + return new RegExp(pat); } - next( ); + // return functor + return make; } - /** - * @method prep_storage - * Prep storage layer for other middleware by setting `req.storage`. - * Some routed paths have a `storage` parameter available, when this is - * set, `req.storage will be set to that value. The default otherwise is - * the entries storage layer, because that's where sgv records are stored - * by default. - */ - function prep_storage (req, res, next) { - if (req.params.storage) { - req.storage = ctx[req.params.storage]; + // save pattern for other middlewares, eg echo, query, etc. + req.pattern = pattern; + var matches = pattern.map(iter_regex()); + // prepare the query against a configurable field name. + var field = req.patternField; + var query = {}; + query[field] = { + // $regex: prefix, + // configure query to perform regex against list of potential regexp + $in: matches + }; + if (prefix.length === 1) { + // If there is a single prefix pattern, mongo can optimize this against + // an indexed field + query[field].$regex = prefix.map(iter_regex('^')).pop(); + } + + // Merge into existing query structure. + if (req.query.find) { + if (req.query.find[field]) { + req.query.find[field].$in = query[field].$in; } else { - req.storage = ctx.entries; + req.query.find[field] = query[field]; } - next( ); + } else { + req.query.find = query; } - - /** - * @module get#/times/echo/:prefix/:regex - * Echo interface for the regex pattern generator. - * @routed - * Useful for understanding how the `/:prefix/:regex` route generates - * mongodb queries. - */ - api.get('/times/echo/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, prep_patterns, function (req, res) { - res.json({ req: { params: req.params, query: req.query}, pattern: req.pattern}); + // Also assist in querying for the requested type. + if (req.params.type) { + req.query.find.type = req.params.type; + } + next(); + } + + /** + * @method prep_pattern_field + * Ensure that `req.patternField` is set to assist other middleware in + * deciding which field to generate queries against. + * Default is `dateString`, because that's the iso8601 field for sgv + * entries. + */ + function prep_pattern_field (req, res, next) { + // If req.params.field from routed path parameter is available use it. + if (req.params.field) { + req.patternField = req.params.field; + } else { + // Default is `dateString`. + req.patternField = 'dateString'; + } + next(); + } + + /** + * @method prep_storage + * Prep storage layer for other middleware by setting `req.storage`. + * Some routed paths have a `storage` parameter available, when this is + * set, `req.storage will be set to that value. The default otherwise is + * the entries storage layer, because that's where sgv records are stored + * by default. + */ + function prep_storage (req, res, next) { + if (req.params.storage && _includes(['entries', 'treatments', 'devicestatus'], req.params.storage)) { + req.storage = ctx[req.params.storage]; + } else { + req.storage = ctx.entries; + } + next(); + } + + /** + * @module get#/times/echo/:prefix/:regex + * Echo interface for the regex pattern generator. + * @routed + * Useful for understanding how the `/:prefix/:regex` route generates + * mongodb queries. + */ + api.get('/times/echo/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, prep_patterns, function(req, res) { + res.json({ + req: { + params: req.params + , query: req.query + } + , pattern: req.pattern }); - - /** - * @module get#/times/:prefix/:regex - * Allows searching for modal times of day across days and months. -``` -/api/v1/times/2015-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 -/api/v1/times/20{14..15}-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 -/api/v1/times/20{14..15}/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 -``` - * @routed - * @response 200 /definitions/Entries - */ - api.get('/times/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, prep_patterns, query_models, format_entries); - + }); + + /** + * @module get#/times/:prefix/:regex + * Allows searching for modal times of day across days and months. + ``` + /api/v1/times/2015-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 + /api/v1/times/20{14..15}-04/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 + /api/v1/times/20{14..15}/T{13..18}:{00..15}'.json'?find[sgv][$gte]=120 + ``` + * @routed + * @response 200 /definitions/Entries + */ + api.get('/times/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, prep_patterns, query_models, wares.obscure_device, format_entries); + + api.get('/count/:storage/where', prep_storage, count_records, format_results); + + /** + * @module get#/slice/:storage/:field/:type/:prefix/:regex + * @routed + * @response 200 /definitions/Entries + * Allows searching for modal times of day across days and months. + * Also allows specifying field to perform regexp on, the storage layer to + * use, as well as which type of model to look for. + ``` + /api/v1/slice/entries/dateString/mbg/2015.json + ``` + */ + api.get('/slice/:storage/:field/:type?/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, query_models, wares.obscure_device, format_entries); + + /** + * @module post#/entries/preview + * Allow previewing your post content, just echos everything you + * posted back out. + * Similar to the echo api, useful to lint/debug upload problems. + */ + api.post('/entries/preview', ctx.authorization.isPermitted('api:entries:create'), function(req, res, next) { + // setting this flag tells insert_entries to not actually store the results + req.persist_entries = false; + next(); + }, insert_entries, wares.obscure_device, format_entries); + + // Protect endpoints with authenticated api. + if (app.enabled('api')) { + // Create and store new sgv entries /** - * @module get#/slice/:storage/:field/:type/:prefix/:regex - * @routed - * @response 200 /definitions/Entries - * Allows searching for modal times of day across days and months. - * Also allows specifying field to perform regexp on, the storage layer to - * use, as well as which type of model to look for. -``` -/api/v1/slice/entries/dateString/mbg/2015.json -``` - */ - api.get('/slice/:storage/:field/:type?/:prefix?/:regex?', prep_storage, prep_pattern_field, prep_patterns, query_models, format_entries); + * @module post#/entries + * Allow posting content to store. + * Stores incoming payload that follows basic rules about having a + * `type` field in `entries` storage layer. + */ + api.post('/entries/', ctx.authorization.isPermitted('api:entries:create'), function(req, res, next) { + // setting this flag tells insert_entries to store the results + req.persist_entries = true; + next(); + }, insert_entries, wares.obscure_device, format_entries); /** - * @module post#/entries/preview - * Allow previewing your post content, just echos everything you - * posted back out. - * Similar to the echo api, useful to lint/debug upload problems. - */ - api.post('/entries/preview', function (req, res, next) { - // setting this flag tells insert_entries to not actually store the results - req.persist_entries = false; - next( ); - }, insert_entries, format_entries); - - // Protect endpoints with authenticated api. - if (app.enabled('api')) { - // Create and store new sgv entries - /** - * @module post#/entries - * Allow posting content to store. - * Stores incoming payload that follows basic rules about having a - * `type` field in `entries` storage layer. - */ - api.post('/entries/', wares.verifyAuthorization, function (req, res, next) { - // setting this flag tells insert_entries to store the results - req.persist_entries = true; - next( ); - }, insert_entries, format_entries); - - /** - * @module delete#/entries/:spec - * @route - * Delete entries. The query logic works the same way as find/list. This - * endpoint uses same search logic to remove records from the database. - */ - api.delete('/entries/:spec', wares.verifyAuthorization, function (req, res, next) { - // if ID, prepare to query for one record - if (isId(req.params.spec)) { - prepReqModel(req, req.params.model); - req.query = {find: {_id: req.params.spec}}; - } else { - req.params.model = req.params.spec; - prepReqModel(req, req.params.model); - if (req.query.find.type === '*') { - delete req.query.find.type; + * @module delete#/entries/:spec + * @route + * Delete entries. The query logic works the same way as find/list. This + * endpoint uses same search logic to remove records from the database. + */ + api.delete('/entries/:spec', ctx.authorization.isPermitted('api:entries:delete'), function(req, res, next) { + // if ID, prepare to query for one record + if (isId(req.params.spec)) { + prepReqModel(req, req.params.model); + req.query = { + find: { + _id: req.params.spec } + }; + } else { + req.params.model = req.params.spec; + prepReqModel(req, req.params.model); + if (req.query.find.type === '*') { + delete req.query.find.type; } - next( ); - }, delete_records); - + } + next(); + }, delete_records); - } + // delete record that match query + api.delete('/entries/', ctx.authorization.isPermitted('api:entries:delete'), delete_records); + } - return api; + return api; } // expose module diff --git a/lib/api/experiments/index.js b/lib/api/experiments/index.js index 4ccd13ff596..8c7b7fd2e6c 100644 --- a/lib/api/experiments/index.js +++ b/lib/api/experiments/index.js @@ -1,17 +1,14 @@ 'use strict'; -function configure (app, wares) { +function configure (app, wares, ctx) { var express = require('express'), api = express.Router( ) ; if (app.enabled('api')) { api.use(wares.sendJSONStatus); - api.get('/:secret/test', wares.verifyAuthorization, function (req, res) { - return res.json({status: 'ok'}); - }); - api.get('/test', wares.verifyAuthorization, function (req, res) { + api.get('/test', ctx.authorization.isPermitted('authorization:debug:test'), function (req, res) { return res.json({status: 'ok'}); }); } diff --git a/lib/api/food/index.js b/lib/api/food/index.js index 8d8f32dbc36..5db962db403 100644 --- a/lib/api/food/index.js +++ b/lib/api/food/index.js @@ -9,11 +9,15 @@ function configure (app, wares, ctx) { // invoke common middleware api.use(wares.sendJSONStatus); // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw( )); + api.use(wares.rawParser); // json body types get handled as parsed json - api.use(wares.bodyParser.json( )); + api.use(wares.jsonParser); // also support url-encoded content-type - api.use(wares.bodyParser.urlencoded({ extended: true })); + api.use(wares.urlencodedParser); + // text body types get handled as raw buffer stream + // shortcut to use extension to specify output content-type + + api.use(ctx.authorization.isPermitted('api:food:read')); // List foods available api.get('/food/', function(req, res) { @@ -37,7 +41,7 @@ function configure (app, wares, ctx) { function config_authed (app, api, wares, ctx) { // create new record - api.post('/food/', wares.verifyAuthorization, function(req, res) { + api.post('/food/', ctx.authorization.isPermitted('api:food:create'), function(req, res) { var data = req.body; ctx.food.create(data, function (err, created) { if (err) { @@ -52,7 +56,7 @@ function configure (app, wares, ctx) { }); // update record - api.put('/food/', wares.verifyAuthorization, function(req, res) { + api.put('/food/', ctx.authorization.isPermitted('api:food:update'), function(req, res) { var data = req.body; ctx.food.save(data, function (err, created) { if (err) { @@ -66,7 +70,7 @@ function configure (app, wares, ctx) { }); }); // delete record - api.delete('/food/:_id', wares.verifyAuthorization, function(req, res) { + api.delete('/food/:_id', ctx.authorization.isPermitted('api:food:delete'), function(req, res) { ctx.food.remove(req.params._id, function ( ) { res.json({ }); }); diff --git a/lib/api/googlehome/index.js b/lib/api/googlehome/index.js new file mode 100644 index 00000000000..d5f8eefdb53 --- /dev/null +++ b/lib/api/googlehome/index.js @@ -0,0 +1,52 @@ +'use strict'; + +var _ = require('lodash'); +var moment = require('moment'); + +function configure (app, wares, ctx, env) { + var express = require('express') + , api = express.Router( ); + var translate = ctx.language.translate; + + // invoke common middleware + api.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + api.use(wares.rawParser); + // json body types get handled as parsed json + api.use(wares.jsonParser); + + + ctx.virtAsstBase.setupVirtAsstHandlers(ctx.googleHome); + + api.post('/googlehome', ctx.authorization.isPermitted('api:*:read'), function (req, res, next) { + console.log('Incoming request from Google Home'); + var locale = _.get(req, 'body.queryResult.languageCode'); + if(locale){ + if(locale.length > 2) { + locale = locale.substr(0, 2); + } + ctx.language.set(locale); + moment.locale(locale); + } + + var handler = ctx.googleHome.getIntentHandler(req.body.queryResult.intent.displayName, req.body.queryResult.parameters.metric); + if (handler){ + var sbx = ctx.sbx; + handler(function (title, response) { + res.json(ctx.googleHome.buildSpeechletResponse(response, false)); + next( ); + return; + }, req.body.queryResult.parameters, sbx); + } else { + res.json(ctx.googleHome.buildSpeechletResponse(translate('virtAsstUnknownIntentText'), true)); + next( ); + return; + } + }); + + ctx.virtAsstBase.setupMutualIntents(ctx.googleHome); + + return api; +} + +module.exports = configure; diff --git a/lib/api/index.js b/lib/api/index.js index 29798b1550d..a9e5e25f873 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -1,32 +1,30 @@ 'use strict'; function create (env, ctx) { - var _ = require('lodash') + var _each = require('lodash/each') , express = require('express') , app = express( ) ; - var wares = require('../middleware/')(env); + const wares = ctx.wares; // set up express app with our options app.set('name', env.name); app.set('version', env.version); - // app.set('head', env.head); - function get_head ( ) { - return env.head; - } - wares.get_head = get_head; + 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', env.api_secret); + if (env.enclave.isApiKeySet()) { + console.log('API KEY present, enabling API'); app.enable('api'); + } else { + console.log('API KEY has not been set, API disabled'); } if (env.settings.enable) { app.extendedClientSettings = ctx.plugins && ctx.plugins.extendedClientSettings ? ctx.plugins.extendedClientSettings(env.extendedSettings) : {}; - _.each(env.settings.enable, function (enable) { + _each(env.settings.enable, function (enable) { console.info('enabling feature:', enable); app.enable(enable); }); @@ -34,24 +32,48 @@ function create (env, ctx) { app.set('title', [app.get('name'), 'API', app.get('version')].join(' ')); - app.set('treatments_auth', env.treatments_auth); - // Start setting up routes if (app.enabled('api')) { // experiments - app.use('/experiments', require('./experiments/')(app, wares)); + app.use('/experiments', require('./experiments/')(app, wares, ctx)); } + + app.use(wares.extensions([ + 'json', 'svg', 'csv', 'txt', 'png', 'html', 'tsv' + ])); + var entriesRouter = require('./entries/')(app, wares, ctx, env); // Entries and settings - app.use('/', require('./entries/')(app, wares, ctx)); - app.use('/', require('./treatments/')(app, wares, ctx)); - app.use('/', require('./profile/')(app, wares, ctx)); - app.use('/', require('./devicestatus/')(app, wares, ctx)); - app.use('/', require('./notifications-api')(app, wares, ctx)); - app.use('/', require('./verifyauth')(app, env)); - app.use('/', require('./food/')(app, wares, ctx)); - // Status - app.use('/', require('./status')(app, wares, env)); + app.all('/entries*', entriesRouter); + app.all('/echo/*', entriesRouter); + app.all('/times/*', entriesRouter); + app.all('/slice/*', entriesRouter); + app.all('/count/*', entriesRouter); + + app.all('/treatments*', require('./treatments/')(app, wares, ctx, env)); + app.all('/profile*', require('./profile/')(app, wares, ctx)); + app.all('/devicestatus*', require('./devicestatus/')(app, wares, ctx, env)); + app.all('/notifications*', require('./notifications-api')(app, wares, 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 + app.all('/status*', require('./status')(app, wares, env, ctx)); + + if (ctx.alexa) { + app.all('/alexa*', require('./alexa/')(app, wares, ctx, env)); + } + + if (ctx.googleHome) { + app.all('/googlehome*', require('./googlehome/')(app, wares, ctx, env)); + } + return app; } diff --git a/lib/api/notifications-api.js b/lib/api/notifications-api.js index 4eeed2e9306..7d8ebafd8dd 100644 --- a/lib/api/notifications-api.js +++ b/lib/api/notifications-api.js @@ -1,27 +1,33 @@ 'use strict'; +var consts = require('../constants'); +var bodyParser = require('body-parser'); + function configure (app, wares, ctx) { var express = require('express') , api = express.Router( ) ; + app.use(bodyParser.urlencoded({extended : true})); + app.use(bodyParser.json()); + api.post('/notifications/pushovercallback', function (req, res) { - console.info('GOT Pushover callback', req.body); if (ctx.pushnotify.pushoverAck(req.body)) { - res.sendStatus(200); + res.sendStatus(consts.HTTP_OK); } else { - res.sendStatus(500); + res.sendStatus(consts.HTTP_INTERNAL_ERROR); } }); if (app.enabled('api')) { // Create and store new sgv entries - api.get('/notifications/ack', wares.verifyAuthorization, function (req, res) { + api.get('/notifications/ack', ctx.authorization.isPermitted('notifications:*:ack'), function (req, res) { var level = Number(req.query.level); + var group = req.query.group || 'default'; var time = req.query.time && Number(req.query.time); console.info('got api ack, level: ', level, ', time: ', time, ', query: ', req.query); - ctx.notifications.ack(level, time, true); - res.sendStatus(200); + ctx.notifications.ack(level, group, time, true); + res.sendStatus(consts.HTTP_OK); }); } diff --git a/lib/api/profile/index.js b/lib/api/profile/index.js index 38b5f57d7c4..57cce59e69a 100644 --- a/lib/api/profile/index.js +++ b/lib/api/profile/index.js @@ -9,17 +9,45 @@ function configure (app, wares, ctx) { // invoke common middleware api.use(wares.sendJSONStatus); // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw( )); + api.use(wares.rawParser); // json body types get handled as parsed json - api.use(wares.bodyParser.json( )); + api.use(wares.jsonParser); // also support url-encoded content-type - api.use(wares.bodyParser.urlencoded({ extended: true })); + api.use(wares.urlencodedParser); + // text body types get handled as raw buffer stream + + api.use(ctx.authorization.isPermitted('api:profile:read')); + + + /** + * @function query_models + * Perform the standard query logic, translating API parameters into mongo + * db queries in a fairly regimented manner. + * This middleware executes the query, returning the results as JSON + */ + function query_models (req, res, next) { + var query = req.query; + + // If "?count=" is present, use that number to decide how many to return. + if (!query.count) { + query.count = consts.PROFILES_DEFAULT_COUNT; + } + + // perform the query + ctx.profile.list_query(query, function payload(err, profiles) { + return res.json(profiles); + }); + } + + // List profiles available + api.get('/profiles/', query_models); // List profiles available api.get('/profile/', function(req, res) { + const limit = req.query && req.query.count ? Number(req.query.count) : consts.PROFILES_DEFAULT_COUNT; ctx.profile.list(function (err, attribute) { return res.json(attribute); - }); + }, limit); }); // List current active record (in current state LAST record is current active) @@ -32,8 +60,9 @@ function configure (app, wares, ctx) { function config_authed (app, api, wares, ctx) { // create new record - api.post('/profile/', wares.verifyAuthorization, function(req, res) { + 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); @@ -43,12 +72,11 @@ function configure (app, wares, ctx) { res.json(created.ops); console.log('Profile created', created); } - }); }); // update record - api.put('/profile/', wares.verifyAuthorization, function(req, res) { + api.put('/profile/', ctx.authorization.isPermitted('api:profile:update'), function(req, res) { var data = req.body; ctx.profile.save(data, function (err, created) { if (err) { @@ -63,7 +91,7 @@ function configure (app, wares, ctx) { }); }); - api.delete('/profile/:_id', wares.verifyAuthorization, function(req, res) { + api.delete('/profile/:_id', ctx.authorization.isPermitted('api:profile:delete'), function(req, res) { ctx.profile.remove(req.params._id, function ( ) { res.json({ }); }); diff --git a/lib/api/root.js b/lib/api/root.js new file mode 100644 index 00000000000..275660eeae8 --- /dev/null +++ b/lib/api/root.js @@ -0,0 +1,23 @@ +'use strict'; + +function configure () { + const express = require('express') + , api = express.Router( ) + , apiConst = require('./const') + , api3Const = require('../api3/const') + ; + + api.get('/versions', function getVersion (req, res) { + + const versions = [ + { version: apiConst.API1_VERSION, url: '/api/v1' }, + { version: apiConst.API2_VERSION, url: '/api/v2' }, + { version: api3Const.API3_VERSION, url: '/api/v3' } + ]; + + res.json(versions); + }); + + return api; +} +module.exports = configure; diff --git a/lib/api/status.js b/lib/api/status.js index 1debeac785d..fe736b00c7c 100644 --- a/lib/api/status.js +++ b/lib/api/status.js @@ -1,26 +1,46 @@ 'use strict'; -function configure (app, wares, env) { +function configure (app, wares, env, ctx) { var express = require('express'), + forwarded = require('forwarded-for'), api = express.Router( ) ; + api.use(wares.sendJSONStatus); api.use(wares.extensions([ 'json', 'svg', 'csv', 'txt', 'png', 'html', 'js' ])); + + api.use(ctx.authorization.isPermitted('api:status:read')); + // Status badge/text/json api.get('/status', function (req, res) { + + let extended = env.settings.filteredSettings(app.extendedClientSettings); + let settings = env.settings.filteredSettings(env.settings); + + var authToken = req.query.token || req.query.secret || ''; + + function getRemoteIP (req) { + const address = forwarded(req, req.headers); + return address.ip; + } + + var date = new Date(); var info = { status: 'ok' , name: app.get('name') , version: app.get('version') - , serverTime: new Date().toISOString() + , serverTime: date.toISOString() + , serverTimeEpoch: date.getTime() , apiEnabled: app.enabled('api') , careportalEnabled: app.enabled('api') && env.settings.enable.indexOf('careportal') > -1 , boluscalcEnabled: app.enabled('api') && env.settings.enable.indexOf('boluscalc') > -1 - , head: wares.get_head( ) - , settings: env.settings - , extendedSettings: app.extendedClientSettings + , settings: settings + , extendedSettings: extended + , authorized: ctx.authorization.authorize(authToken, getRemoteIP(req)) + , runtimeState: ctx.runtimeState }; + var badge = 'http://img.shields.io/badge/Nightscout-OK-green'; return res.format({ html: function ( ) { @@ -33,10 +53,9 @@ function configure (app, wares, env) { res.redirect(302, badge + '.svg'); }, js: function ( ) { - var head = 'this.serverSettings ='; - var body = JSON.stringify(info); - var tail = ';'; - res.send([head, body, tail].join(' ')); + var parts = ['this.serverSettings =', JSON.stringify(info), ';']; + + res.send(parts.join(' ')); }, text: function ( ) { res.send('STATUS OK'); diff --git a/lib/api/treatments/index.js b/lib/api/treatments/index.js index 65a7cccdba6..996c646a02d 100644 --- a/lib/api/treatments/index.js +++ b/lib/api/treatments/index.js @@ -1,60 +1,202 @@ 'use strict'; -var consts = require('../../constants'); +const _forEach = require('lodash/forEach'); +const _isNil = require('lodash/isNil'); +const _isArray = require('lodash/isArray'); +const _take = require('lodash/take'); -function configure (app, wares, ctx) { - var express = require('express'), - api = express.Router( ); +const constants = require('../../constants'); +const moment = require('moment'); + +function configure (app, wares, ctx, env) { + var express = require('express') + , api = express.Router(); + + api.use(wares.compression()); - // invoke common middleware - api.use(wares.sendJSONStatus); // text body types get handled as raw buffer stream - api.use(wares.bodyParser.raw( )); + api.use(wares.rawParser); // json body types get handled as parsed json - api.use(wares.bodyParser.json( )); + api.use(wares.bodyParser.json({ + limit: '50Mb' + })); // also support url-encoded content-type - api.use(wares.bodyParser.urlencoded({ extended: true })); + api.use(wares.urlencodedParser); - // List treatments available - api.get('/treatments/', function(req, res) { - ctx.treatments.list(req.query, function (err, results) { - return res.json(results); + // invoke common middleware + api.use(wares.sendJSONStatus); + + api.use(ctx.authorization.isPermitted('api:treatments:read')); + + function serveTreatments(req,res, err, results) { + + var ifModifiedSince = req.get('If-Modified-Since'); + + var d1 = null; + + const deNormalizeDates = env.settings.deNormalizeDates; + + _forEach(results, function clean (t) { + t.carbs = Number(t.carbs); + t.insulin = Number(t.insulin); + + if (deNormalizeDates && Object.prototype.hasOwnProperty.call(t, 'utcOffset')) { + const d = moment(t.created_at).utcOffset(t.utcOffset); + t.created_at = d.toISOString(true); + delete t.utcOffset; + } + + var d2 = null; + + if (Object.prototype.hasOwnProperty.call(t, 'created_at')) { + d2 = new Date(t.created_at); + } else { + if (Object.prototype.hasOwnProperty.call(t, 'timestamp')) { + d2 = new Date(t.timestamp); + } + } + + if (d2 == null) { return; } + + if (d1 == null || d2.getTime() > d1.getTime()) { + d1 = d2; + } }); + + if (!_isNil(d1)) { + res.setHeader('Last-Modified', d1.toUTCString()); + + if (ifModifiedSince && d1.getTime() <= moment(ifModifiedSince).valueOf()) { + res.status(304).send({ + status: 304 + , message: 'Not modified' + , type: 'internal' + }); + return; + } + } + + return res.json(results); + } + + // List treatments available + api.get('/treatments', function(req, res) { + var query = req.query; + if (!query.count) { + // If there's a date search involved, default to a higher number of objects + query.count = query.find ? 1000 : 100; + } + + const inMemoryData = ctx.cache.treatments; + const canServeFromMemory = inMemoryData && inMemoryData.length >= query.count && Object.keys(query).length == 1 ? true : false; + + if (canServeFromMemory) { + serveTreatments(req, res, null, _take(inMemoryData,query.count)); + } else { + ctx.treatments.list(query, function(err, results) { + serveTreatments(req,res,err,results); + }); + } }); function config_authed (app, api, wares, ctx) { - function post_response(req, res) { - var treatment = req.body; - ctx.treatments.create(treatment, function (err, created) { + function post_response (req, res) { + var treatments = req.body; + + if (!_isArray(treatments)) { + treatments = [treatments]; + } + + for (let i = 0; i < treatments.length; i++) { + const t = treatments[i]; + + if (!t.created_at) { + 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); + res.sendJSONStatus(res, constants.HTTP_VALIDATION_ERROR, 'Treatments must contain created_at'); + return; + } + const d = moment(t.created_at); + if (!d.isValid()) { + console.log('Trying to insert date with invalid created_at', t); + res.sendJSONStatus(res, constants.HTTP_VALIDATION_ERROR, 'Treatments created_at must be an ISO-8601 date'); + return; + } + */ + + } + + ctx.treatments.create(treatments, function(err, created) { if (err) { - console.log('Error adding treatment'); - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + console.log('Error adding treatment', err); + res.sendJSONStatus(res, constants.HTTP_INTERNAL_ERROR, 'Mongo Error', err); } else { - console.log('Treatment created'); + console.log('REST API treatment created', created); res.json(created); } }); } - if (app.settings.treatments_auth) { - api.post('/treatments/', wares.verifyAuthorization, post_response); - } else { - api.post('/treatments/', post_response); - } - api.delete('/treatments/:_id', wares.verifyAuthorization, function(req, res) { - ctx.treatments.remove(req.params._id, function ( ) { - res.json({ }); + + api.post('/treatments/', ctx.authorization.isPermitted('api:treatments:create'), post_response); + + /** + * @function delete_records + * Delete treatments. The query logic works the same way as find/list. This + * endpoint uses same search logic to remove records from the database. + */ + function delete_records (req, res, next) { + var query = req.query; + if (!query.count) { + query.count = 10 + } + + // remove using the query + ctx.treatments.remove(query, function(err, stat) { + if (err) { + console.log('treatments delete error: ', err); + return next(err); + } + + // yield some information about success of operation + res.json(stat); + + return next(); }); - }); + } + + api.delete('/treatments/:id', ctx.authorization.isPermitted('api:treatments:delete'), function(req, res, next) { + if (!req.query.find) { + req.query.find = { + _id: req.params.id + }; + } else { + req.query.find._id = req.params.id; + } + + if (req.query.find._id === '*') { + // match any record id + delete req.query.find._id; + } + next(); + }, delete_records); + + // delete record that match query + api.delete('/treatments/', ctx.authorization.isPermitted('api:treatments:delete'), delete_records); // update record - api.put('/treatments/', wares.verifyAuthorization, function(req, res) { + api.put('/treatments/', ctx.authorization.isPermitted('api:treatments:update'), function(req, res) { var data = req.body; - ctx.treatments.save(data, function (err, created) { + ctx.treatments.save(data, function(err, created) { if (err) { - res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); - console.log('Error saving treatment'); - console.log(err); + res.sendJSONStatus(res, constants.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + console.log('Error saving treatment', err); } else { res.json(created); console.log('Treatment saved', data); @@ -71,4 +213,3 @@ function configure (app, wares, ctx) { } module.exports = configure; - diff --git a/lib/api/verifyauth.js b/lib/api/verifyauth.js index d92646928a1..849ce9bef9a 100644 --- a/lib/api/verifyauth.js +++ b/lib/api/verifyauth.js @@ -2,19 +2,36 @@ var consts = require('../constants'); -function configure (app, env) { +function configure (ctx) { var express = require('express'), api = express.Router( ); api.get('/verifyauth', function(req, res) { - var api_secret = env.api_secret; - var secret = req.params.secret ? req.params.secret : req.header('api-secret'); - var authorized = (app.enabled('api') && api_secret && api_secret.length > 12) ? (secret === api_secret) : false; - res.sendJSONStatus(res, consts.HTTP_OK, authorized ? 'OK' : 'UNAUTHORIZED'); + ctx.authorization.resolveWithRequest(req, function resolved (err, result) { + // this is used to see if req has api-secret equivalent authorization + var canRead = !err && + ctx.authorization.checkMultiple('*:*:read', result.shiros); + var canWrite = !err && + ctx.authorization.checkMultiple('*:*:write', result.shiros); + var isAdmin = !err && + ctx.authorization.checkMultiple('*:*:admin', result.shiros); + var authorized = canRead && !result.defaults; + + var response = { + canRead, + canWrite, + isAdmin, + message: authorized ? 'OK' : 'UNAUTHORIZED', + rolefound: result.subject ? 'FOUND' : 'NOTFOUND', + permissions: result.defaults ? 'DEFAULT' : 'ROLE' + }; + + + res.sendJSONStatus(res, consts.HTTP_OK, response); + }); }); return api; } module.exports = configure; - diff --git a/lib/api2/index.js b/lib/api2/index.js new file mode 100644 index 00000000000..9e5653047c1 --- /dev/null +++ b/lib/api2/index.js @@ -0,0 +1,22 @@ +'use strict'; + +function create (env, ctx, apiv1) { + var express = require('express') + , app = express( ) + ; + + const ddata = require('../data/endpoints')(env, ctx); + const notificationsV2 = require('./notifications-v2')(app, ctx); + const summary = require('./summary')(env, ctx); + + app.use('/', apiv1); + app.use('/properties', ctx.properties); + app.use('/authorization', ctx.authorization.endpoints); + app.use('/ddata', ddata); + app.use('/notifications', notificationsV2); + app.use('/summary', summary); + + return app; +} + +module.exports = create; diff --git a/lib/api2/notifications-v2.js b/lib/api2/notifications-v2.js new file mode 100644 index 00000000000..e21677486bc --- /dev/null +++ b/lib/api2/notifications-v2.js @@ -0,0 +1,30 @@ +'use strict'; + +var consts = require('../constants'); + +function configure (app, ctx) { + var express = require('express') + , api = express.Router( ) + ; + + api.use(ctx.wares.compression()); + api.use(ctx.wares.rawParser); + api.use(ctx.wares.bodyParser.json({ + limit: '50Mb' + })); + api.use(ctx.wares.urlencodedParser); + + api.post('/loop', ctx.authorization.isPermitted('notifications:loop:push'), function (req, res) { + ctx.loop.sendNotification(req.body, req.connection.remoteAddress, function (error) { + if (error) { + res.status(consts.HTTP_INTERNAL_ERROR).send(error) + console.log("error sending notification to Loop: ", error); + } else { + res.sendStatus(consts.HTTP_OK); + } + }); + }); + + return api; +} +module.exports = configure; diff --git a/lib/api2/properties.js b/lib/api2/properties.js new file mode 100644 index 00000000000..61334f8a3f3 --- /dev/null +++ b/lib/api2/properties.js @@ -0,0 +1,60 @@ +'use strict'; + +var _isEmpty = require('lodash/isEmpty'); +var _filter = require('lodash/filter'); +var _pick = require('lodash/pick'); + +var express = require('express'); +var sandbox = require('../sandbox')(); + +function create (env, ctx) { + var properties = express( ); + + /** + * Supports the paths: + * /v2/properties - All properties + * /v2/properties/prop1 - Only prop1 + * /v2/properties/prop1,prop3 - Only prop1 and prop3 + * + * Expecting to define extended syntax and support for several query params + */ + properties.use(ctx.authorization.isPermitted('api:entries:read'), + ctx.authorization.isPermitted('api:treatments:read')); + properties.get(['/', '/*'], function getProperties (req, res) { + + if (!ctx.sbx) res.json({}); + + function notEmpty (part) { + return ! _isEmpty(part); + } + + var segments = _filter(req.path.split('/'), notEmpty); + + var selected = [ ]; + + if (segments.length > 0) { + selected = _filter(segments[0].split(','), notEmpty); + } + + var result = ctx.sbx.properties; + + if (selected.length > 0) { + result = _pick(ctx.sbx.properties, selected); + } + + result = env.settings.filteredSettings(result); + + if (req.query && req.query.pretty) { + res.setHeader('Content-Type', 'application/json'); + res.send(JSON.stringify(result, null, 2)); + } else { + res.json(result); + } + + }); + + + return properties; +} + +module.exports = create; diff --git a/lib/api2/summary/basaldataprocessor.js b/lib/api2/summary/basaldataprocessor.js new file mode 100644 index 00000000000..67ff6c8b99a --- /dev/null +++ b/lib/api2/summary/basaldataprocessor.js @@ -0,0 +1,139 @@ +const { data } = require("jquery"); + +const dataProcessor = {}; + +function _hhmmAfter (hhmm, mills) { + var date = new Date(mills); + var withSameDate = new Date( + 1900 + date.getYear() + , date.getMonth() + , date.getDate() + , parseInt(hhmm.substr(0, 2), 10) + , parseInt(hhmm.substr(3, 5), 10) + ).getTime(); + return withSameDate > date ? withSameDate : withSameDate + 24 * 60 * 60 * 1000; +} + +// Outputs temp basal objects describing the profile temps for the duration +function _profileBasalsInWindow (basals, start, end) { + if (basals.length === 0) { + return []; + } + + var i; + var out = []; + + function nextProfileBasal () { + i = (i + 1) % basals.length; + var lastStart = out[out.length - 1].start; + return { + start: _hhmmAfter(basals[i]['time'], lastStart) + , absolute: parseFloat(basals[i]['value']) + , profile: 1 + }; + } + + i = 0; + var startHHMM = new Date(start).toTimeString().substr(0, 5); + while (i < basals.length - 1 && basals[i + 1]['time'] <= startHHMM) { + i++; + } + out.push({ + start: start + , absolute: parseFloat(basals[i]['value']) + , }); + + var next = nextProfileBasal(); + while (next.start < end) { + out.push(next); + next = nextProfileBasal(); + } + + return out; +} + +dataProcessor.filterSameAbsTemps = function filterSameAbsTemps (tempdata) { + + var out = []; + var j = 0; + + for (let i = 0; i < tempdata.length; i++) { + const temp = tempdata[i]; + + if (i == tempdata.length - 1) { + // If last was merged, skip + if (j != i) { + out.push(temp); + } + break; + } + + const nextTemp = tempdata[i + 1]; + + if (temp.duration && (temp.start + temp.duration) >= nextTemp.start) { + if (temp.absolute == nextTemp.absolute) { + // Merge and skip next + temp.duration = nextTemp.start - temp.start + nextTemp.duration; + i += 1; + j = i; + } else { + // Adjust duration + temp.duration = nextTemp.start - temp.start; + } + } + out.push(temp); + } + return out; +} + +dataProcessor.processTempBasals = function processTempBasals (profile, tempBasals, dataCap) { + var profileBasals = profile.basal; + var temps = tempBasals.map(function(temp) { + return { + start: new Date(temp['created_at']).getTime() + , duration: temp['duration'] === undefined ? 0 : parseInt(temp['duration'], 10) * 60 * 1000 + , absolute: temp['absolute'] === undefined ? 0 : parseFloat(temp['absolute']) + }; + }).concat([ + { start: Date.now() - 24 * 60 * 60 * 1000, duration: 0 } + , { start: Date.now(), duration: 0} + ]).sort(function(a, b) { + return a.start - b.start; + }); + + var out = []; + temps.forEach(function(temp) { + var last = out[out.length - 1]; + if (last && last.duration !== undefined && last.start + last.duration < temp.start) { + Array.prototype.push.apply(out, _profileBasalsInWindow(profileBasals, last.start + last.duration, temp.start)); + } + if (temp.duration) out.push(temp); + }); + + var o2 = out; + var prevLength = 1; + var newLength = 0; + + while (prevLength != newLength) { + prevLength = o2.length; + o2 = dataProcessor.filterSameAbsTemps(o2); + newLength = o2.length; + } + + var o3 = []; + + // Return temps from last hours + for (var i = 0; i < o2.length; i++) { + if ((o2[i].start + o2[i].duration) > dataCap) o3.push(o2[i]); + } + + // Convert durations to seconds + + for (var i = 0; i < o3.length; i++) { + o3[i].duration = o3[i].duration / 1000; + } + + return o3; +} + +module.exports = dataProcessor; diff --git a/lib/api2/summary/index.js b/lib/api2/summary/index.js new file mode 100644 index 00000000000..31deec2c76b --- /dev/null +++ b/lib/api2/summary/index.js @@ -0,0 +1,135 @@ +'use strict'; + +function configure (env, ctx) { + const _ = require('lodash') + , basalProcessor = require('./basaldataprocessor') + , express = require('express') + , api = express.Router(); + + const defaultHours = 6; + + api.use(ctx.wares.compression()); + + function removeProps(obj,keys){ + if(Array.isArray(obj)){ + obj.forEach(function(item){ + removeProps(item,keys) + }); + } + else if(typeof obj === 'object' && obj != null){ + Object.getOwnPropertyNames(obj).forEach(function(key){ + if(keys.indexOf(key) !== -1)delete obj[key]; + else removeProps(obj[key],keys); + }); + } + } + + function processSGVs(sgvs, hours) { + + const bgData = []; + const dataCap = Date.now() - (hours * 60 * 60 * 1000); + + for (let i = 0; i < sgvs.length; i++) { + const bg = sgvs[i]; + if (bg.mills >= dataCap) { + + let item = { + sgv: bg.mgdl + , mills: bg.mills + }; + + // only push noise data if there is noise + if (bg.noise != 1) { item.noise = bg.noise; } + bgData.push(item); + + } + } + return bgData; + } + + // Collect treatments that contain insulin or carbs, temp basals + function processTreatments(treatments, profile, hours) { + + const rVal = { + tempBasals: [], + treatments: [], + targets: [] + }; + + let _temps = []; + const dataCap = Date.now() - (hours * 60 * 60 * 1000); + + for (let i = 0; i < treatments.length; i++) { + const t = treatments[i]; + + if (t.eventType == 'Temp Basal') { + _temps.push(t); + continue; + } + if (t.eventType == 'Temporary Target') { + rVal.targets.push({ + targetTop: Math.round(t.targetTop), + targetBottom: Math.round(t.targetBottom), + duration: t.duration*60, + mills: t.mills + }); + continue; + } + + if (t.insulin || t.carbs) { + if (t.mills >= dataCap) { + const _t = { + mills: t.mills + }; + if (!isNaN(t.carbs)) _t.carbs = t.carbs; + if (!isNaN(t.insulin)) _t.insulin = t.insulin; + rVal.treatments.push(_t); + } + continue; + } + } + + rVal.tempBasals = basalProcessor.processTempBasals(profile,_temps, dataCap); + + return rVal; + } + + function constructState() { + + const p = _.get(ctx, 'sbx.properties'); + + const state = { + iob: Math.round(_.get(p,'iob.iob')*100)/100, + cob: Math.round(_.get(p,'cob.cob')), + bwp: Math.round(_.get(p,'bwp.bolusEstimate')*100)/100, + cage: _.get(p,'cage.age'), + sage: _.get(p,'sage.age'), + iage: _.get(p,'iage.age'), + bage: _.get(p,'bage.age'), + battery: _.get(p,'upbat.level') + } + return state; + } + + api.get('/', ctx.authorization.isPermitted('api:*:read'), function (req, res) { + + const hours = req.query.hours || defaultHours; + const sgvs = processSGVs(ctx.ddata.sgvs, hours); + const profile = _.clone(ctx.sbx.data.profile.getCurrentProfile()); + removeProps(profile,['timeAsSeconds']); + const treatments = processTreatments(ctx.ddata.treatments, profile, hours); + const state = constructState(); + + res.setHeader('content-type', 'application/json'); + res.write(JSON.stringify({ + sgvs, + treatments, + profile, + state + })); + res.end( ); + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/alarmSocket.js b/lib/api3/alarmSocket.js new file mode 100644 index 00000000000..88bb699a553 --- /dev/null +++ b/lib/api3/alarmSocket.js @@ -0,0 +1,198 @@ +'use strict'; + +const apiConst = require('./const'); +const forwarded = require('forwarded-for'); + +function getRemoteIP (req) { + const address = forwarded(req, req.headers); + return address.ip; +} + +/** + * Socket.IO broadcaster of alarm and annoucements + */ +function AlarmSocket (app, env, ctx) { + + const self = this; + + var levels = ctx.levels; + + const LOG_GREEN = '\x1B[32m' + , LOG_MAGENTA = '\x1B[35m' + , LOG_RESET = '\x1B[0m' + , LOG = LOG_GREEN + 'ALARM SOCKET: ' + LOG_RESET + , LOG_ERROR = LOG_MAGENTA + 'ALARM SOCKET: ' + LOG_RESET + , NAMESPACE = '/alarm' + ; + + + /** + * Initialize socket namespace and bind the events + * @param {Object} io Socket.IO object to multiplex namespaces + */ + self.init = function init (io) { + self.io = io; + + self.namespace = io.of(NAMESPACE); + self.namespace.on('connection', function onConnected (socket) { + + const remoteIP = getRemoteIP(socket.request); + console.log(LOG + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP); + + socket.on('disconnect', function onDisconnect () { + console.log(LOG + 'Disconnected client ID: ', socket.client.id); + }); + + socket.on('subscribe', function onSubscribe (message, returnCallback) { + self.subscribe(socket, message, returnCallback); + }); + + }); + + // Turns all notifications on the event bus back into events to be + // broadcast to clients. + ctx.bus.on('notification', self.emitNotification); + }; + + + /** + * Authorize Socket.IO client and subscribe him to authorized rooms + * + * Support webclient authorization with api_secret is added + * + * @param {Object} socket + * @param {Object} message input message from the client + * @param {Function} returnCallback function for returning a value back to the client + */ + self.subscribe = function subscribe (socket, message, returnCallback) { + const shouldCallBack = typeof(returnCallback) === 'function'; + + // Native client + if (message && message.accessToken) { + return ctx.authorization.resolveAccessToken(message.accessToken, function resolveFinishForToken (err, auth) { + if (err) { + console.log(`${LOG_ERROR} Authorization failed for accessToken:`, message.accessToken); + + if (shouldCallBack) { + returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN }); + } + return err; + } else { + // Subscribe for acking alarms + // Client sends ack, which sends a notificaiton through our internal bus + socket.on('ack', function onAck (level, group, silenceTime) { + ctx.notifications.ack(level, group, silenceTime, true); + console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); + }); + + var okResponse = { success: true, message: 'Subscribed for alarms' } + if (shouldCallBack) { + returnCallback(okResponse); + } + return okResponse; + } + }); + } + + if (!message) { message = {}; } + // Web client (jwt access token or api_hash) + /* + * On the web: a client may have saved a secret or using a jwtToken, or may have none. + * Some pages will automatically prompt for authorization, when needed. + * To make the main homepage require authorization as well, set + * AUTHENTICATION_PROMPT_ON_LOAD=true. + * + * If there is missing authorization when authorization is required, + * rejecting the attempt in order to trigger a prompt on the client. + * If there is no authorization required, or there are available + * credentials, attempt to resolve the available permissions. + * When processing ACK messages that dismiss alarms, Authorization should be + * required. + */ + var shouldTry = true; + if (env.settings.authenticationPromptOnLoad) { + if (!message.jwtToken && !message.secret) { + shouldTry = false; + } + } + + if (message && shouldTry) { + return ctx.authorization.resolve({ api_secret: message.secret, token: message.jwtToken, ip: getRemoteIP(socket.request) }, function resolveFinish (err, auth) { + + if (err) { + console.log(`${LOG_ERROR} Authorization failed for jwtToken:`, message.jwtToken); + + if (shouldCallBack) { + returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN }); + } + return err; + } else { + var perms = { + read: ctx.authorization.checkMultiple('api:*:read', auth.shiros) + , ack: ctx.authorization.checkMultiple('notifications:*:ack', auth.shiros) + }; + // Subscribe for acking alarms + // TODO: does this produce double ACK after the authorizing? Only if reconnecting? + // TODO: how will perms get updated after authorizing? + socket.on('ack', function onAck (level, group, silenceTime) { + if (perms.ack) { + // This goes through the server-wide event bus. + ctx.notifications.ack(level, group, silenceTime, true); + console.info(LOG + 'ack received ' + level + ' ' + group + ' ' + silenceTime); + } else { + // TODO: send a message to client to silence locally, but not + // globally, and request authorization. + // This won't go through th event bus. + // var acked = { silenceTime, group, level }; + // socket.emit('authorization_needed', acked); + } + }); + /* TODO: need to know when to update the permissions. + // Can we use + socket.on('resubscribe', function update_permissions ( ) { + // perms = { ... }; + }); + */ + + var okResponse = { success: true, message: 'Subscribed for alarms', ...perms }; + if (shouldCallBack) { + returnCallback(okResponse); + } + return okResponse; + } + }); + } + + console.log(`${LOG_ERROR} Authorization failed for message:`, message); + if (shouldCallBack) { + returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN}); + } + }; + + + /** + * Emit alarm to subscribed clients + * @param {Object} notofication to emit + */ + + self.emitNotification = function emitNotification (notify) { + if (notify.clear) { + self.namespace.emit('clear_alarm', notify); + console.info(LOG + 'emitted clear_alarm to all clients'); + } else if (notify.level === levels.WARN) { + self.namespace.emit('alarm', notify); + console.info(LOG + 'emitted alarm to all clients'); + } else if (notify.level === levels.URGENT) { + self.namespace.emit('urgent_alarm', notify); + console.info(LOG + 'emitted urgent_alarm to all clients'); + } else if (notify.isAnnouncement) { + self.namespace.emit('announcement', notify); + console.info(LOG + 'emitted announcement to all clients'); + } else { + self.namespace.emit('notification', notify); + console.info(LOG + 'emitted notification to all clients'); + } + }; +} + +module.exports = AlarmSocket; diff --git a/lib/api3/const.json b/lib/api3/const.json new file mode 100644 index 00000000000..5f63c63679a --- /dev/null +++ b/lib/api3/const.json @@ -0,0 +1,52 @@ +{ + "API3_VERSION": "3.0.3-alpha", + "API3_SECURITY_ENABLE": true, + "API3_DEDUP_FALLBACK_ENABLED": true, + "API3_CREATED_AT_FALLBACK_ENABLED": true, + "API3_MAX_LIMIT": 1000, + + "HTTP": { + "OK": 200, + "CREATED": 201, + "NO_CONTENT": 204, + "NOT_MODIFIED": 304, + "BAD_REQUEST": 400, + "UNAUTHORIZED": 401, + "FORBIDDEN": 403, + "NOT_FOUND": 404, + "NOT_ACCEPTABLE": 406, + "GONE": 410, + "PRECONDITION_FAILED": 412, + "UNPROCESSABLE_ENTITY": 422, + "INTERNAL_ERROR": 500 + }, + + "MSG": { + "HTTP_400_BAD_LAST_MODIFIED": "Bad or missing Last-Modified header/parameter", + "HTTP_400_BAD_LIMIT": "Parameter limit out of tolerance", + "HTTP_400_BAD_REQUEST_BODY": "Bad or missing request body", + "HTTP_400_BAD_FIELD_IDENTIFIER": "Bad or missing identifier field", + "HTTP_400_BAD_FIELD_DATE": "Bad or missing date field", + "HTTP_400_BAD_FIELD_UTC": "Bad or missing utcOffset field", + "HTTP_400_BAD_FIELD_APP": "Bad or missing app field", + "HTTP_400_BAD_SKIP": "Parameter skip out of tolerance", + "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_TOKEN": "Bad access token or JWT", + "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", + "STORAGE_ERROR": "Database error", + "SOCKET_MISSING_OR_BAD_ACCESS_TOKEN": "Missing or bad accessToken", + "SOCKET_UNAUTHORIZED_TO_ANY": "Unauthorized to receive any collection" + }, + + "MIN_TIMESTAMP": 946684800000, + "MIN_UTC_OFFSET": -1440, + "MAX_UTC_OFFSET": 1440 +} diff --git a/lib/api3/doc/alarmsockets.md b/lib/api3/doc/alarmsockets.md new file mode 100644 index 00000000000..54d86e93a97 --- /dev/null +++ b/lib/api3/doc/alarmsockets.md @@ -0,0 +1,151 @@ +# APIv3: Socket.IO alarm channel + +### Complete sample client code +```html + + + + + + + + APIv3 Socket.IO sample for alarms + + + + + + + + + + +``` + +### Subscription (authorization) +The client must first subscribe to the channel that is exposed at `alarm` namespace, ie the `/alarm` subadress of the base Nightscout's web address (without `/api/v3` subaddress). +```javascript +const socket = io('https://nsapiv3.herokuapp.com/alarm'); +``` + + +Subscription is requested by emitting `subscribe` event to the server, while including document with parameter: +* `accessToken`: required valid accessToken of the security subject, which has been prepared in *Admin Tools* of Nightscout. + +```javascript +socket.on('connect', function () { + socket.emit('subscribe', { + accessToken: 'testadmin-ad3b1f9d7b3f59d5' + }, ... +``` + + +On the server, the subject is identified and authenticated (by the accessToken). Ne special rights are required. + +If the authentication was successful `success` = `true` is set in the response object and the field `message` contains a text response. +In other case `success` = `false` is set in the response object and the field `message` contains an error message. + +```javascript +function (data) { + if (data.success) { + console.log('subscribed for alarms', data.message); + } + else { + console.error(data.message); + } + }); +}); +``` + +### Acking alarms and announcements +If the client is successfully subscribed it can ack alarms and announcements by emitting `ack` message. + +```javascript + socket.emit('ack', level, group, silenceTimeInMilliseconds); +``` + +where `level` and `group` are values from alarm being acked and `silenceTimeInMilliseconds` is duration. During this time alarms of the same type are not emmited. + +### Receiving events +After the successful subscription the client can start listening to `announcement`, `alarm` , `urgent_alarm` and/or `clear_alarm` events of the socket. + + +##### announcement + +The received object contains similiar json: + +```javascript + { + "level":0, + "title":"Announcement", + "message":"test", + "plugin":{"name":"treatmentnotify","label":"Treatment Notifications","pluginType":"notification","enabled":true}, + "group":"Announcement", + "isAnnouncement":true, + "key":"9ac46ad9a1dcda79dd87dae418fce0e7955c68da" + } +``` + + +##### alarm, urgent_alarm + +The received object contains similiar json: + +```javascript + { + "level":1, + "title":"Warning HIGH", + "message":"BG Now: 5 -0.2 → mmol\/L\nRaw BG: 4.8 mmol\/L Čistý\nBG 15m: 4.8 mmol\/L\nIOB: -0.02U\nCOB: 0g", + "eventName":"high", + "plugin":{"name":"simplealarms","label":"Simple Alarms","pluginType":"notification","enabled":true}, + "pushoverSound":"climb", + "debug":{"lastSGV":5,"thresholds":{"bgHigh":180,"bgTargetTop":75,"bgTargetBottom":72,"bgLow":70}}, + "group":"default", + "key":"simplealarms_1" + } +``` + + +##### clear_alarm + +The received object contains similiar json: + +```javascript + { + "clear":true, + "title":"All Clear", + "message":"default - Urgent was ack'd", + "group":"default" + } +``` \ No newline at end of file diff --git a/lib/api3/doc/formats.md b/lib/api3/doc/formats.md new file mode 100644 index 00000000000..a0c9f73b3fa --- /dev/null +++ b/lib/api3/doc/formats.md @@ -0,0 +1,91 @@ +# APIv3: Output formats + +### Choosing output format +In APIv3, the standard content type is JSON for both HTTP request and HTTP response. +However, in HTTP response, the response content type can be changed to XML or CSV +for READ, SEARCH, and HISTORY operations. + +The response content type can be requested in one of the following ways: +- add a file type extension to the URL, eg. + `/api/v3/entries.csv?...` + or `/api/v3/treatments/95e1a6e3-1146-5d6a-a3f1-41567cae0895.xml?...` +- set `Accept` HTTP request header to `text/csv` or `application/xml` + +The server replies with `406 Not Acceptable` HTTP status in case of not supported content type. + + +### JSON + +Default content type is JSON, output can look like this: + +```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 + +Sample output: + +``` + + + + sgv + 171 + 2014-07-19T02:44:15.000-07:00 + 1405763055000 + dexcom + Flat + 5c5a2404e0196f4d3d9a718a + 1405763055000 + 1405763055000 + + + sgv + 176 + 2014-07-19T03:09:15.000-07:00 + 1405764555000 + dexcom + Flat + 5c5a2404e0196f4d3d9a7187 + 1405764555000 + 1405764555000 + + +``` + +### CSV + +Sample output: + +``` +type,sgv,dateString,date,device,direction,identifier,srvModified,srvCreated +sgv,171,2014-07-19T02:44:15.000-07:00,1405763055000,dexcom,Flat,5c5a2404e0196f4d3d9a718a,1405763055000,1405763055000 +sgv,176,2014-07-19T03:09:15.000-07:00,1405764555000,dexcom,Flat,5c5a2404e0196f4d3d9a7187,1405764555000,1405764555000 +``` diff --git a/lib/api3/doc/security.md b/lib/api3/doc/security.md new file mode 100644 index 00000000000..99972fe8830 --- /dev/null +++ b/lib/api3/doc/security.md @@ -0,0 +1,31 @@ +# APIv3: Security + +### Enforcing HTTPS +APIv3 is ment to run only under SSL version of HTTP protocol, which provides: +- **message secrecy** - once the secure channel between client and server is closed the communication cannot be eavesdropped by any third party +- **message consistency** - each request/response is protected against modification by any third party (any forgery would be detected) +- **authenticity of identities** - once the client and server establish the secured channel, it is guaranteed that the identity of the client or server does not change during the whole session + +HTTPS (in use with APIv3) does not address the true identity of the client, but ensures the correct identity of the server. Furthermore, HTTPS does not prevent the resending of previously intercepted encrypted messages by an attacker. + + +--- +### Authentication and authorization +In APIv3, *API_SECRET* can no longer be used for authentication or authorization. Instead, a roles/permissions security model is used, which is managed in the *Admin tools* section of the web application. + + +The identity of the client is represented by the *subject* to whom the access level is set by assigning security *roles*. One or more *permissions* can be assigned to each role. Permissions are used in an [Apache Shiro-like style](http://shiro.apache.org/permissions.html "Apache Shiro-like style"). + + +For each security *subject*, the system automatically generates an *access token* that is difficult to guess since it is derived from the secret *API_SECRET*. The *access token* must be included in every secured API operation to decode the client's identity and determine its authorization level. In this way, it is then possible to resolve whether the client has the permission required by a particular API operation. + + +There is only one way to authorize API calls: +- use so-called [JSON Web Tokens](https://jwt.io "JSON Web Tokens") + - at first let the `/api/v2/authorization/request` generates you a particular JWT, eg. `GET https://nsapiv3.herokuapp.com/api/v2/authorization/request/testreadab-76eaff2418bfb7e0` + - 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) + + +--- +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! diff --git a/lib/api3/doc/socket.md b/lib/api3/doc/socket.md new file mode 100644 index 00000000000..802a85e0235 --- /dev/null +++ b/lib/api3/doc/socket.md @@ -0,0 +1,142 @@ +# APIv3: Socket.IO storage modifications channel + +APIv3 has the ability to broadcast events about all created, edited and deleted documents, using Socket.IO library. + +This provides a real-time data exchange experience in combination with API REST operations. + +### Complete sample client code +```html + + + + + + + + APIv3 Socket.IO sample + + + + + + + + + + +``` + +**Important notice: Only changes made via APIv3 are being broadcasted. All direct database or APIv1 modifications are not included by this channel.** + +### Subscription (authorization) +The client must first subscribe to the channel that is exposed at `storage` namespace, ie the `/storage` subadress of the base Nightscout's web address (without `/api/v3` subaddress). +```javascript +const socket = io('https://nsapiv3.herokuapp.com/storage'); +``` + + +Subscription is requested by emitting `subscribe` event to the server, while including document with parameters: +* `accessToken`: required valid accessToken of the security subject, which has been prepared in *Admin Tools* of Nightscout. +* `collections`: optional array of collections which the client wants to subscribe to, by default all collections are requested) + +```javascript +socket.on('connect', function () { + socket.emit('subscribe', { + accessToken: 'testadmin-ad3b1f9d7b3f59d5', + collections: [ 'entries', 'treatments' ] + }, +``` + + +On the server, the subject is first identified and authenticated (by the accessToken) and then a verification takes place, if the subject has read access to each required collection. + +An exception is the `settings` collection for which `api:settings:admin` permission is required, for all other collections `api::read` permission is required. + + +If the authentication was successful and the client has read access to at least one collection, `success` = `true` is set in the response object and the field `collections` contains an array of collections which were actually subscribed (granted). +In other case `success` = `false` is set in the response object and the field `message` contains an error message. + +```javascript +function (data) { + if (data.success) { + console.log('subscribed for collections', data.collections); + } + else { + console.error(data.message); + } + }); +}); +``` + +### Receiving events +After the successful subscription the client can start listening to `create`, `update` and/or `delete` events of the socket. + + +##### create +`create` event fires each time a new document is inserted into the storage, regardless of whether it was CREATE or UPDATE operation of APIv3 (both of these operations are upserting/deduplicating, so they are "insert capable"). If the document already existed in the storage, the `update` event would be fired instead. + +The received object contains: +* `colName` field with the name of the affected collection +* the inserted document in `doc` field + +```javascript +socket.on('create', function (data) { + console.log(`${data.colName}:created document`, data.doc); +}); +``` + + +##### update +`update` event fires each time an existing document is modified in the storage, regardless of whether it was CREATE, UPDATE or PATCH operation of APIv3 (all of these operations are "update capable"). If the document did not yet exist in the storage, the `create` event would be fired instead. + +The received object contains: +* `colName` field with the name of the affected collection +* the new version of the modified document in `doc` field + +```javascript +socket.on('update', function (data) { + console.log(`${data.colName}:updated document`, data.doc); +}); +``` + + +##### delete +`delete` event fires each time an existing document is deleted in the storage, regardless of whether it was "soft" (marking as invalid) or permanent deleting. + +The received object contains: +* `colName` field with the name of the affected collection +* the identifier of the deleted document in the `identifier` field + +```javascript +socket.on('delete', function (data) { + console.log(`${data.colName}:deleted document with identifier`, data.identifier); +}); +``` \ No newline at end of file diff --git a/lib/api3/doc/tutorial.md b/lib/api3/doc/tutorial.md new file mode 100644 index 00000000000..7ea482f6cff --- /dev/null +++ b/lib/api3/doc/tutorial.md @@ -0,0 +1,426 @@ +# APIv3: Basics tutorial + +Nightscout API v3 is a component of [cgm-remote-monitor](https://github.com/nightscout/cgm-remote-monitor) project. +It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange. + +There is a list of REST operations that the API v3 offers (inside `/api/v3` relative URL namespace), we will briefly introduce them in this file. + +Each NS instance with API v3 contains self-included OpenAPI specification at [/api/v3/swagger-ui-dist/](https://nsapiv3.herokuapp.com/api/v3/swagger-ui-dist/) relative URL. + + +--- +### VERSION + +[VERSION](https://nsapiv3.herokuapp.com/api3-docs/#/other/get_version) operation gets you basic information about software packages versions. +It is public (there is no need to add authorization parameters/headers). + +Sample GET `/version` client code (to get actual versions): +```javascript +const axios = require('axios'); +axios.get(`https://nsapiv3.herokuapp.com/api/v3/version`) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200, + "result": { + "version": "14.2.0", + "apiVersion": "3.0.4-alpha", + "srvDate": 1613056980085, + "storage": { + "storage": "mongodb", + "version": "4.4.3" + } + } +} +``` + + +--- +### STATUS + +[STATUS](https://nsapiv3.herokuapp.com/api3-docs/#/other/get_status) operation gets you basic information about software packages versions. +It is public (there is no need to add authorization parameters/headers). + +Sample GET `/status` client code (to get my actual permissions): +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios.get(`https://nsapiv3.herokuapp.com/api/v3/status`, + { + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200, + "result": { + "version": "14.2.0", + "apiVersion": "3.0.4-alpha", + "srvDate": 1613057148579, + "storage": { + "storage": "mongodb", + "version": "4.4.3" + }, + "apiPermissions": { + "devicestatus": "crud", + "entries": "crud", + "food": "crud", + "profile": "crud", + "settings": "crud", + "treatments": "crud" + } + } +} +``` +`"crud"` represents create + read + update + delete permissions for the collection. + + +--- +### SEARCH + +[SEARCH](https://nsapiv3insecure.herokuapp.com/api3-docs/#/generic/SEARCH) operation filters, sorts, paginates and projects documents from the collection. + +Sample GET `/entries` client code (to retrieve last 3 BG values): +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios.get(`https://nsapiv3.herokuapp.com/api/v3/entries?sort$desc=date&limit=3&fields=dateString,sgv,direction`, + { + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200, + "result": [ + { + "dateString": "2021-02-11T15:25:28.928Z", + "sgv": 116, + "direction": "FortyFiveDown" + }, + { + "dateString": "2021-02-11T15:20:28.239Z", + "sgv": 124, + "direction": "FortyFiveDown" + }, + { + "dateString": "2021-02-11T15:15:28.225Z", + "sgv": 130, + "direction": "Flat" + } + ] +} +``` + + +--- +### CREATE + +[CREATE](https://nsapiv3.herokuapp.com/api3-docs/#/generic/post__collection_) operation inserts a new document into the collection. + +Sample POST `/treatments` client code: +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +const doc = { + date: 1613057404186, // (new Date()).getTime(), + app: 'AndroidAPS', + device: 'Samsung XCover 4-861536030196001', + eventType: 'Correction Bolus', + insulin: 0.3 +}; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments`, + { + method: 'post', + data: doc, + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 201, + "identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975", + "lastModified": 1613057523148 +} +``` + + +--- +### READ + +[READ](https://nsapiv3.herokuapp.com/api3-docs/#/generic/get__collection___identifier_) operation retrieves you a single document from the collection by its identifier. + +Sample GET `/treatments/{identifier}` client code: +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975'; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios.get(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`, + { + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200, + "result": { + "date": 1613057404186, + "app": "AndroidAPS", + "device": "Samsung XCover 4-861536030196001", + "eventType": "Correction Bolus", + "insulin": 0.3, + "utcOffset": 0, + "created_at": "2021-02-11T15:30:04.186Z", + "identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975", + "srvModified": 1613057523148, + "srvCreated": 1613057523148, + "subject": "test-admin" + } +} +``` + + +--- +### LAST MODIFIED + +[LAST MODIFIED](https://nsapiv3insecure.herokuapp.com/api3-docs/#/other/LAST-MODIFIED) operation finds the date of last modification for each collection. + +Sample GET `/lastModified` client code (to get latest modification dates): +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios.get(`https://nsapiv3.herokuapp.com/api/v3/lastModified`, + { + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200, + "result": { + "srvDate": 1613057924021, + "collections": { + "devicestatus": 1613057731281, + "entries": 1613057728148, + "profile": 1580337948416, + "treatments": 1613057523148 + } + } +} +``` + + +--- +### UPDATE + +[UPDATE](https://nsapiv3insecure.herokuapp.com/api3-docs/#/generic/put__collection___identifier_) operation updates existing document in the collection. + +Sample PUT `/treatments/{identifier}` client code (to update `insulin` from 0.3 to 0.4): +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975'; +const doc = { + date: 1613057404186, + app: 'AndroidAPS', + device: 'Samsung XCover 4-861536030196001', + eventType: 'Correction Bolus', + insulin: 0.4 +}; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`, + { + method: 'put', + data: doc, + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200, + "lastModified": 1613058295307 +} +``` + + +--- +### PATCH + +[PATCH](https://nsapiv3insecure.herokuapp.com/api3-docs/#/generic/patch__collection___identifier_) operation partially updates existing document in the collection. + +Sample PATCH `/treatments/{identifier}` client code (to update `insulin` from 0.4 to 0.5): +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975'; +const doc = { + insulin: 0.5 +}; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`, + { + method: 'patch', + data: doc, + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200 +} +``` + + +--- +### DELETE + +[DELETE](https://nsapiv3insecure.herokuapp.com/api3-docs/#/generic/delete__collection___identifier_) operation deletes existing document from the collection. + +Sample DELETE `/treatments/{identifier}` client code: +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +const identifier = '5b0f7124-475f-5db0-824c-a73c5eea0975'; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/${identifier}`, + { + method: 'delete', + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200 +} +``` + + +--- +### HISTORY + +[HISTORY](https://nsapiv3insecure.herokuapp.com/api3-docs/#/generic/HISTORY2) operation queries all changes since the timestamp. + +Sample HISTORY `/treatments/history/{lastModified}` client code: +```javascript +const axios = require('axios'); +const accessToken = 'token=testadmin-ad3b1f9d7b3f59d5'; +const lastModified = 1613057520148; +axios.get(`https://nsapiv3.herokuapp.com/api/v2/authorization/request/${accessToken}`) + .then(res => { + const jwt = res.data.token; + return axios(`https://nsapiv3.herokuapp.com/api/v3/treatments/history/${lastModified}`, + { + headers: { + 'Authorization': `Bearer ${jwt}` + } + }); + }) + .then(res => { + console.log(res.data); + }); +``` +Sample result: +```json +{ + "status": 200, + "result": [ + { + "date": 1613057404186, + "app": "AndroidAPS", + "device": "Samsung XCover 4-861536030196001", + "eventType": "Correction Bolus", + "insulin": 0.5, + "utcOffset": 0, + "created_at": "2021-02-11T15:30:04.186Z", + "identifier": "5b0f7124-475f-5db0-824c-a73c5eea0975", + "srvModified": 1613058548149, + "srvCreated": 1613057523148, + "subject": "test-admin", + "modifiedBy": "test-admin", + "isValid": false + } + ] +} +``` +Notice the `"isValid":false` field marking the deletion of the document. diff --git a/lib/api3/generic/collection.js b/lib/api3/generic/collection.js new file mode 100644 index 00000000000..015af2a13f4 --- /dev/null +++ b/lib/api3/generic/collection.js @@ -0,0 +1,197 @@ +'use strict'; + +const apiConst = require('../const.json') + , _ = require('lodash') + , dateTools = require('../shared/dateTools') + , opTools = require('../shared/operationTools') + , stringTools = require('../shared/stringTools') + , MongoCollectionStorage = require('../storage/mongoCollection') + , CachedCollectionStorage = require('../storage/mongoCachedCollection') + , searchOperation = require('./search/operation') + , createOperation = require('./create/operation') + , readOperation = require('./read/operation') + , updateOperation = require('./update/operation') + , patchOperation = require('./patch/operation') + , deleteOperation = require('./delete/operation') + , historyOperation = require('./history/operation') + ; + +/** + * Generic collection (abstraction over each collection specifics) + * @param {string} colName - name of the collection inside the storage system + * @param {function} fallbackGetDate - function that tries to create srvModified virtually from other fields of document + * @param {Array} dedupFallbackFields - fields that all need to be matched to identify document via fallback deduplication + * @param {function} fallbackHistoryFilter - function that creates storage filter for all newer records (than the timestamp from first function parameter) + */ +function Collection ({ ctx, env, app, colName, storageColName, fallbackGetDate, dedupFallbackFields, + fallbackDateField }) { + + const self = this; + + self.colName = colName; + self.fallbackGetDate = fallbackGetDate; + self.dedupFallbackFields = app.get('API3_DEDUP_FALLBACK_ENABLED') ? dedupFallbackFields : []; + self.autoPruneDays = app.setENVTruthy('API3_AUTOPRUNE_' + colName.toUpperCase()); + self.nextAutoPrune = new Date(); + + const baseStorage = new MongoCollectionStorage(ctx, env, storageColName); + self.storage = new CachedCollectionStorage(ctx, env, colName, baseStorage); + + self.fallbackDateField = fallbackDateField; + + self.mapRoutes = function mapRoutes () { + const prefix = '/' + colName + , prefixId = prefix + '/:identifier' + , prefixHistory = prefix + '/history' + ; + + + // GET /{collection} + app.get(prefix, searchOperation(ctx, env, app, self)); + + // POST /{collection} + app.post(prefix, createOperation(ctx, env, app, self)); + + // GET /{collection}/history + app.get(prefixHistory, historyOperation(ctx, env, app, self)); + + // GET /{collection}/history + app.get(prefixHistory + '/:lastModified', historyOperation(ctx, env, app, self)); + + // GET /{collection}/{identifier} + app.get(prefixId, readOperation(ctx, env, app, self)); + + // PUT /{collection}/{identifier} + app.put(prefixId, updateOperation(ctx, env, app, self)); + + // PATCH /{collection}/{identifier} + app.patch(prefixId, patchOperation(ctx, env, app, self)); + + // DELETE /{collection}/{identifier} + app.delete(prefixId, deleteOperation(ctx, env, app, self)); + }; + + + /** + * Parse limit (max document count) from query string + */ + self.parseLimit = function parseLimit (req, res) { + const maxLimit = app.get('API3_MAX_LIMIT'); + let limit = maxLimit; + + if (req.query.limit) { + if (!isNaN(req.query.limit) && req.query.limit > 0 && req.query.limit <= maxLimit) { + limit = parseInt(req.query.limit); + } + else { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LIMIT); + return null; + } + } + + return limit; + }; + + + + /** + * Fetch modified date from document (with possible fallback and back-fill to srvModified/srvCreated) + * @param {Object} doc - document loaded from database + */ + self.resolveDates = function resolveDates (doc) { + let modifiedDate; + try { + if (doc.srvModified) { + modifiedDate = new Date(doc.srvModified); + } + else { + if (typeof (self.fallbackGetDate) === 'function') { + modifiedDate = self.fallbackGetDate(doc); + if (modifiedDate) { + doc.srvModified = modifiedDate.getTime(); + } + } + } + + if (doc.srvModified && !doc.srvCreated) { + doc.srvCreated = modifiedDate.getTime(); + } + } + catch (error) { + console.warn(error); + } + return modifiedDate; + }; + + + /** + * Deletes old documents from the collection if enabled (for this collection) + * in the background (asynchronously) + * */ + self.autoPrune = function autoPrune () { + + if (!stringTools.isNumberInString(self.autoPruneDays)) + return; + + const autoPruneDays = parseFloat(self.autoPruneDays); + if (autoPruneDays <= 0) + return; + + if (new Date() > self.nextAutoPrune) { + + const deleteBefore = new Date(new Date().getTime() - (autoPruneDays * 24 * 3600 * 1000)); + + const filter = [ + { field: 'srvCreated', operator: 'lt', value: deleteBefore.getTime() }, + { field: 'created_at', operator: 'lt', value: deleteBefore.toISOString() }, + { field: 'date', operator: 'lt', value: deleteBefore.getTime() } + ]; + + // let's autoprune asynchronously (we won't wait for the result) + self.storage.deleteManyOr(filter, function deleteDone (err, result) { + if (err || !result) { + console.error(err); + } + + if (result.deleted) { + console.info('Auto-pruned ' + result.deleted + ' documents from ' + self.colName + ' collection '); + } + }); + + self.nextAutoPrune = new Date(new Date().getTime() + (3600 * 1000)); + } + }; + + + /** + * Parse date and utcOffset + optional created_at fallback + * @param {Object} doc + */ + self.parseDate = function parseDate (doc) { + if (!_.isEmpty(doc)) { + + let values = app.get('API3_CREATED_AT_FALLBACK_ENABLED') + ? [doc.date, doc.created_at] + : [doc.date]; + + let m = dateTools.parseToMoment(values); + if (m && m.isValid()) { + doc.date = m.valueOf(); + + if (typeof doc.utcOffset === 'undefined') { + doc.utcOffset = m.utcOffset(); + } + + if (app.get('API3_CREATED_AT_FALLBACK_ENABLED')) { + doc.created_at = m.toISOString(); + } + else { + if (doc.created_at) + delete doc.created_at; + } + } + } + } +} + +module.exports = Collection; diff --git a/lib/api3/generic/create/insert.js b/lib/api3/generic/create/insert.js new file mode 100644 index 00000000000..56cb608cdf2 --- /dev/null +++ b/lib/api3/generic/create/insert.js @@ -0,0 +1,53 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + , path = require('path') + , opTools = require('../../shared/operationTools') + ; + +/** + * Insert new document into the collection + * @param {Object} opCtx + * @param {Object} doc + */ +async function insert (opCtx, doc) { + + const { ctx, auth, col, req, res } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:create`); + + if (validate(opCtx, doc) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + doc.srvCreated = doc.srvModified; + + if (auth && auth.subject && auth.subject.name) { + doc.subject = auth.subject.name; + } + + const identifier = await col.storage.insertOne(doc); + + if (!identifier) + throw new Error('empty identifier'); + + res.setHeader('Last-Modified', now.toUTCString()); + res.setHeader('Location', path.posix.join(req.baseUrl, req.path, identifier)); + + + 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(); + ctx.bus.emit('data-received'); +} + + +module.exports = insert; diff --git a/lib/api3/generic/create/operation.js b/lib/api3/generic/create/operation.js new file mode 100644 index 00000000000..39986a87ebd --- /dev/null +++ b/lib/api3/generic/create/operation.js @@ -0,0 +1,63 @@ +'use strict'; + +const _ = require('lodash') + , apiConst = require('../../const.json') + , security = require('../../security') + , insert = require('./insert') + , replace = require('../update/replace') + , opTools = require('../../shared/operationTools') + ; + + +/** + * CREATE: Inserts a new document into the collection + */ +async function create (opCtx) { + + const { col, req, res } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + col.parseDate(doc); + opTools.resolveIdentifier(doc); + const identifyingFilter = col.storage.identifyingFilter(doc.identifier, doc, col.dedupFallbackFields); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('empty result'); + + if (result.length > 0) { + const storageDoc = result[0]; + await replace(opCtx, doc, storageDoc, { isDeduplication: true }); + } + else { + await insert(opCtx, doc); + } +} + + +function createOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await create(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = createOperation; \ No newline at end of file diff --git a/lib/api3/generic/create/validate.js b/lib/api3/generic/create/validate.js new file mode 100644 index 00000000000..e978a3955e5 --- /dev/null +++ b/lib/api3/generic/create/validate.js @@ -0,0 +1,26 @@ +'use strict'; + +const apiConst = require('../../const.json') + , stringTools = require('../../shared/stringTools') + , opTools = require('../../shared/operationTools') + ; + + +/** + * Validation of document to create + * @param {Object} opCtx + * @param {Object} doc + * @returns string with error message if validation fails, true in case of success + */ +function validate (opCtx, doc) { + + const { res } = opCtx; + + if (typeof(doc.identifier) !== 'string' || stringTools.isNullOrWhitespace(doc.identifier)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_IDENTIFIER); + } + + return opTools.validateCommon(doc, res); +} + +module.exports = validate; \ No newline at end of file diff --git a/lib/api3/generic/delete/operation.js b/lib/api3/generic/delete/operation.js new file mode 100644 index 00000000000..f5e8c3b5b27 --- /dev/null +++ b/lib/api3/generic/delete/operation.js @@ -0,0 +1,122 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + ; + +/** + * DELETE: Deletes a document from the collection + */ +async function doDelete (opCtx) { + + const { col, req } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:delete`); + + if (await validateDelete(opCtx) !== true) + return; + + if (req.query.permanent && req.query.permanent === "true") { + await deletePermanently(opCtx); + } else { + await markAsDeleted(opCtx); + } +} + + +async function validateDelete (opCtx) { + + const { col, req, res } = opCtx; + + const identifier = req.params.identifier; + const result = await col.storage.findOne(identifier); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND }); + } + else { + const storageDoc = result[0]; + + if (storageDoc.isReadOnly === true || storageDoc.readOnly === true || storageDoc.readonly === true) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, + apiConst.MSG.HTTP_422_READONLY_MODIFICATION); + } + } + + return true; +} + + +async function deletePermanently (opCtx) { + + const { ctx, col, req, res } = opCtx; + + const identifier = req.params.identifier; + const result = await col.storage.deleteOne(identifier); + + if (!result) + throw new Error('empty result'); + + if (!result.deleted) { + 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 opTools.sendJSON({ res, status: apiConst.HTTP.OK }); +} + + +async function markAsDeleted (opCtx) { + + const { ctx, col, req, res, auth } = opCtx; + + const identifier = req.params.identifier; + const setFields = { 'isValid': false, 'srvModified': (new Date).getTime() }; + + if (auth && auth.subject && auth.subject.name) { + setFields.modifiedBy = auth.subject.name; + } + + const result = await col.storage.updateOne(identifier, setFields); + + if (!result) + throw new Error('empty result'); + + if (!result.updated) { + 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 opTools.sendJSON({ res, status: apiConst.HTTP.OK }); +} + + +function deleteOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await doDelete(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = deleteOperation; diff --git a/lib/api3/generic/history/operation.js b/lib/api3/generic/history/operation.js new file mode 100644 index 00000000000..e374f981c47 --- /dev/null +++ b/lib/api3/generic/history/operation.js @@ -0,0 +1,150 @@ +'use strict'; + +const dateTools = require('../../shared/dateTools') + , renderer = require('../../shared/renderer') + , apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , FieldsProjector = require('../../shared/fieldsProjector') + , _ = require('lodash') + ; + +/** + * HISTORY: Retrieves incremental changes since timestamp + */ +async function history (opCtx, fieldsProjector) { + + const { req, res, col } = opCtx; + + let filter = parseFilter(opCtx) + , limit = col.parseLimit(req, res) + , projection = fieldsProjector.storageProjection() + , sort = prepareSort() + , skip = 0 + , onlyValid = false + , logicalOperator = 'or' + ; + + if (filter !== null && limit !== null && projection !== null) { + + const result = await col.storage.findMany({ filter + , sort + , limit + , skip + , projection + , onlyValid + , logicalOperator }); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + res.status(apiConst.HTTP.OK); + return renderer.render(res, result); + } + + _.each(result, col.resolveDates); + + const srvModifiedValues = _.map(result, function mapSrvModified (item) { + return item.srvModified; + }) + , maxSrvModified = _.max(srvModifiedValues); + + res.setHeader('Last-Modified', (new Date(maxSrvModified)).toUTCString()); + res.setHeader('ETag', 'W/"' + maxSrvModified + '"'); + + _.each(result, fieldsProjector.applyProjection); + + res.status(apiConst.HTTP.OK); + renderer.render(res, result); + } +} + + +/** + * Parse history filtering criteria from Last-Modified header + */ +function parseFilter (opCtx) { + + const { req, res } = opCtx; + + let lastModified = null + , lastModifiedParam = req.params.lastModified + , operator = null; + + if (lastModifiedParam) { + + // using param in URL as a source of timestamp + const m = dateTools.parseToMoment(lastModifiedParam); + + if (m === null || !m.isValid()) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + + lastModified = m.toDate(); + operator = 'gt'; + } + else { + // using request HTTP header as a source of timestamp + const lastModifiedHeader = req.get('Last-Modified'); + if (!lastModifiedHeader) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + + try { + lastModified = dateTools.floorSeconds(new Date(lastModifiedHeader)); + } catch (err) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_LAST_MODIFIED); + return null; + } + operator = 'gte'; + } + + return [ + { field: 'srvModified', operator: operator, value: lastModified.getTime() } + ]; +} + + + +/** + * Prepare sorting for storage query + */ +function prepareSort () { + return { + srvModified: 1 + }; +} + + +function historyOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + if (col.colName === 'settings') { + await security.demandPermission(opCtx, `api:${col.colName}:admin`); + } else { + await security.demandPermission(opCtx, `api:${col.colName}:read`); + } + + const fieldsProjector = new FieldsProjector(req.query.fields); + + await history(opCtx, fieldsProjector); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = historyOperation; diff --git a/lib/api3/generic/patch/operation.js b/lib/api3/generic/patch/operation.js new file mode 100644 index 00000000000..dff18c70dbe --- /dev/null +++ b/lib/api3/generic/patch/operation.js @@ -0,0 +1,120 @@ +'use strict'; + +const _ = require('lodash') + , apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + , opTools = require('../../shared/operationTools') + , dateTools = require('../../shared/dateTools') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + +/** + * PATCH: Partially updates document in the collection + */ +async function patch (opCtx) { + + const { req, res, col } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + await security.demandPermission(opCtx, `api:${col.colName}:update`); + + // parseDate is not valid for patch operation + // (it is adding new fields) + // col.parseDate(doc); + const identifier = req.params.identifier + , identifyingFilter = col.storage.identifyingFilter(identifier); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('result empty'); + + if (result.length > 0) { + + const storageDoc = result[0]; + if (storageDoc.isValid === false) { + return opTools.sendJSONStatus(res, apiConst.HTTP.GONE); + } + + const modifiedDate = col.resolveDates(storageDoc) + , ifUnmodifiedSince = req.get('If-Unmodified-Since'); + + if (ifUnmodifiedSince + && dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) { + return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED); + } + + await applyPatch(opCtx, identifier, doc, storageDoc); + } + else { + return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_FOUND); + } +} + + +/** + * Patch existing document in the collection + * @param {Object} opCtx + * @param {string} identifier + * @param {Object} doc - fields and values to patch + * @param {Object} storageDoc - original (database) version of document + */ +async function applyPatch (opCtx, identifier, doc, storageDoc) { + + const { ctx, res, col, auth } = opCtx; + + if (validate(opCtx, doc, storageDoc) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + + if (auth && auth.subject && auth.subject.name) { + doc.modifiedBy = auth.subject.name; + } + + const matchedCount = await col.storage.updateOne(identifier, doc); + + if (!matchedCount) + throw new Error('matchedCount empty'); + + res.setHeader('Last-Modified', now.toUTCString()); + opTools.sendJSONStatus(res, apiConst.HTTP.OK); + + const fieldsProjector = new FieldsProjector('_all'); + const patchedDocs = await col.storage.findOne(identifier, fieldsProjector); + const patchedDoc = patchedDocs[0]; + fieldsProjector.applyProjection(patchedDoc); + ctx.bus.emit('storage-socket-update', { colName: col.colName, doc: patchedDoc }); + + col.autoPrune(); + ctx.bus.emit('data-received'); +} + + +function patchOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await patch(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = patchOperation; diff --git a/lib/api3/generic/patch/validate.js b/lib/api3/generic/patch/validate.js new file mode 100644 index 00000000000..057bb5c39e8 --- /dev/null +++ b/lib/api3/generic/patch/validate.js @@ -0,0 +1,19 @@ +'use strict'; + +const updateValidate = require('../update/validate') + ; + + +/** + * Validate document to patch + * @param {Object} opCtx + * @param {Object} doc + * @param {Object} storageDoc + * @returns string - null if validation fails + */ +function validate (opCtx, doc, storageDoc) { + + return updateValidate(opCtx, doc, storageDoc, { isPatching: true }); +} + +module.exports = validate; \ No newline at end of file diff --git a/lib/api3/generic/read/operation.js b/lib/api3/generic/read/operation.js new file mode 100644 index 00000000000..3f4efb75dd9 --- /dev/null +++ b/lib/api3/generic/read/operation.js @@ -0,0 +1,77 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , dateTools = require('../../shared/dateTools') + , renderer = require('../../shared/renderer') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + +/** + * READ: Retrieves a single document from the collection + */ +async function read (opCtx) { + + const { req, res, col } = opCtx; + + await security.demandPermission(opCtx, `api:${col.colName}:read`); + + const fieldsProjector = new FieldsProjector(req.query.fields); + + const result = await col.storage.findOne(req.params.identifier + , fieldsProjector.storageProjection()); + + if (!result) + throw new Error('empty result'); + + if (result.length === 0) { + return opTools.sendJSON({ res, status: apiConst.HTTP.NOT_FOUND }); + } + + const doc = result[0]; + if (doc.isValid === false) { + return opTools.sendJSON({ res, status: apiConst.HTTP.GONE }); + } + + + const modifiedDate = col.resolveDates(doc); + if (modifiedDate) { + res.setHeader('Last-Modified', modifiedDate.toUTCString()); + + const ifModifiedSince = req.get('If-Modified-Since'); + + if (ifModifiedSince + && dateTools.floorSeconds(modifiedDate) <= dateTools.floorSeconds(new Date(ifModifiedSince))) { + return res.status(apiConst.HTTP.NOT_MODIFIED).end(); + } + } + + fieldsProjector.applyProjection(doc); + + res.status(apiConst.HTTP.OK); + renderer.render(res, doc); +} + + +function readOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await read(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = readOperation; diff --git a/lib/api3/generic/search/input.js b/lib/api3/generic/search/input.js new file mode 100644 index 00000000000..dbd37356760 --- /dev/null +++ b/lib/api3/generic/search/input.js @@ -0,0 +1,140 @@ +'use strict'; + +const apiConst = require('../../const.json') + , dateTools = require('../../shared/dateTools') + , stringTools = require('../../shared/stringTools') + , opTools = require('../../shared/operationTools') + ; + +const filterRegex = /(.*)\$([a-zA-Z]+)/; + + +/** + * Parse value of the parameter (to the correct data type) + */ +function parseValue(param, value) { + + value = stringTools.isNumberInString(value) ? parseFloat(value) : value; // convert number from string + + // convert boolean from string + if (value === 'true') + value = true; + + if (value === 'false') + value = false; + + // unwrap string in single quotes + if (typeof(value) === 'string' && value.startsWith('\'') && value.endsWith('\'')) { + value = value.substr(1, value.length - 2); + } + + if (['date', 'srvModified', 'srvCreated'].includes(param)) { + let m = dateTools.parseToMoment(value); + if (m && m.isValid()) { + value = m.valueOf(); + } + } + + if (param === 'created_at') { + let m = dateTools.parseToMoment(value); + if (m && m.isValid()) { + value = m.toISOString(); + } + } + + return value; +} + + +/** + * Parse filtering criteria from query string + */ +function parseFilter (req, res) { + const filter = [] + , reservedParams = ['token', 'sort', 'sort$desc', 'limit', 'skip', 'fields', 'now'] + , operators = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 're'] + ; + + for (let param in req.query) { + if (!Object.prototype.hasOwnProperty.call(req.query, param) + || reservedParams.includes(param)) continue; + + let field = param + , operator = 'eq' + ; + + const match = filterRegex.exec(param); + if (match != null) { + operator = match[2]; + field = match[1]; + + if (!operators.includes(operator)) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, + apiConst.MSG.HTTP_400_UNSUPPORTED_FILTER_OPERATOR.replace('{0}', operator)); + return null; + } + } + const value = parseValue(field, req.query[param]); + + filter.push({ field, operator, value }); + } + + return filter; +} + + +/** + * Parse sorting from query string + */ +function parseSort (req, res) { + let sort = {} + , sortDirection = 1; + + if (req.query.sort && req.query.sort$desc) { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_SORT_SORT_DESC); + return null; + } + + if (req.query.sort$desc) { + sortDirection = -1; + sort[req.query.sort$desc] = sortDirection; + } + else { + if (req.query.sort) { + sort[req.query.sort] = sortDirection; + } + } + + sort.identifier = sortDirection; + sort.created_at = sortDirection; + sort.date = sortDirection; + + return sort; +} + + +/** + * Parse skip (offset) from query string + */ +function parseSkip (req, res) { + let skip = 0; + + if (req.query.skip) { + if (!isNaN(req.query.skip) && req.query.skip >= 0) { + skip = parseInt(req.query.skip); + } + else { + opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_SKIP); + return null; + } + } + + return skip; +} + + +module.exports = { + parseFilter, + parseSort, + parseSkip +}; \ No newline at end of file diff --git a/lib/api3/generic/search/operation.js b/lib/api3/generic/search/operation.js new file mode 100644 index 00000000000..179f357b503 --- /dev/null +++ b/lib/api3/generic/search/operation.js @@ -0,0 +1,78 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , opTools = require('../../shared/operationTools') + , renderer = require('../../shared/renderer') + , input = require('./input') + , _each = require('lodash/each') + , FieldsProjector = require('../../shared/fieldsProjector') + ; + + +/** + * SEARCH: Search documents from the collection + */ +async function search (opCtx) { + + const { req, res, col } = opCtx; + + if (col.colName === 'settings') { + await security.demandPermission(opCtx, `api:${col.colName}:admin`); + } else { + await security.demandPermission(opCtx, `api:${col.colName}:read`); + } + + const fieldsProjector = new FieldsProjector(req.query.fields); + + const filter = input.parseFilter(req, res) + , sort = input.parseSort(req, res) + , limit = col.parseLimit(req, res) + , skip = input.parseSkip(req, res) + , projection = fieldsProjector.storageProjection() + , onlyValid = true + ; + + + if (filter !== null && sort !== null && limit !== null && skip !== null && projection !== null) { + const result = await col.storage.findMany({ filter + , sort + , limit + , skip + , projection + , onlyValid }); + + if (!result) + throw new Error('empty result'); + + _each(result, col.resolveDates); + + _each(result, fieldsProjector.applyProjection); + + res.status(apiConst.HTTP.OK); + renderer.render(res, result); + } +} + + +function searchOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await search(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = searchOperation; diff --git a/lib/api3/generic/setup.js b/lib/api3/generic/setup.js new file mode 100644 index 00000000000..17e118658dd --- /dev/null +++ b/lib/api3/generic/setup.js @@ -0,0 +1,103 @@ +'use strict'; + +const _ = require('lodash') + , dateTools = require('../shared/dateTools') + , Collection = require('./collection') + ; + + +function fallbackDate (doc) { + const m = dateTools.parseToMoment(doc.date); + return m == null || !m.isValid() + ? null + : m.toDate(); +} + + +function fallbackCreatedAt (doc) { + const m = dateTools.parseToMoment(doc.created_at); + return m == null || !m.isValid() + ? null + : m.toDate(); +} + + +function setupGenericCollections (ctx, env, app) { + const cols = { } + , enabledCols = app.get('enabledCollections'); + + if (_.includes(enabledCols, 'devicestatus')) { + cols.devicestatus = new Collection({ + ctx, env, app, + colName: 'devicestatus', + storageColName: env.devicestatus_collection || 'devicestatus', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at', 'device'], + fallbackDateField: 'created_at' + }); + } + + const entriesCollection = new Collection({ + ctx, env, app, + colName: 'entries', + storageColName: env.entries_collection || 'entries', + fallbackGetDate: fallbackDate, + dedupFallbackFields: ['date', 'type'], + fallbackDateField: 'date' + }); + app.set('entriesCollection', entriesCollection); + + if (_.includes(enabledCols, 'entries')) { + cols.entries = entriesCollection; + } + + if (_.includes(enabledCols, 'food')) { + cols.food = new Collection({ + ctx, env, app, + colName: 'food', + storageColName: env.food_collection || 'food', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at'], + fallbackDateField: 'created_at' + }); + } + + if (_.includes(enabledCols, 'profile')) { + cols.profile = new Collection({ + ctx, env, app, + colName: 'profile', + storageColName: env.profile_collection || 'profile', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at'], + fallbackDateField: 'created_at' + }); + } + + if (_.includes(enabledCols, 'settings')) { + cols.settings = new Collection({ + ctx, env, app, + colName: 'settings', + storageColName: env.settings_collection || 'settings' + }); + } + + if (_.includes(enabledCols, 'treatments')) { + cols.treatments = new Collection({ + ctx, env, app, + colName: 'treatments', + storageColName: env.treatments_collection || 'treatments', + fallbackGetDate: fallbackCreatedAt, + dedupFallbackFields: ['created_at', 'eventType'], + fallbackDateField: 'created_at' + }); + } + + _.forOwn(cols, function forMember (col) { + col.mapRoutes(); + }); + + app.set('collections', cols); +} + + +module.exports = setupGenericCollections; diff --git a/lib/api3/generic/update/operation.js b/lib/api3/generic/update/operation.js new file mode 100644 index 00000000000..5098ffa1773 --- /dev/null +++ b/lib/api3/generic/update/operation.js @@ -0,0 +1,86 @@ +'use strict'; + +const _ = require('lodash') + , dateTools = require('../../shared/dateTools') + , apiConst = require('../../const.json') + , security = require('../../security') + , insert = require('../create/insert') + , replace = require('./replace') + , opTools = require('../../shared/operationTools') + ; + +/** + * UPDATE: Updates a document in the collection + */ +async function update (opCtx) { + + const { col, req, res } = opCtx; + const doc = req.body; + + if (_.isEmpty(doc)) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_REQUEST_BODY); + } + + col.parseDate(doc); + opTools.resolveIdentifier(doc); + + const identifier = req.params.identifier + , identifyingFilter = col.storage.identifyingFilter(identifier); + + const result = await col.storage.findOneFilter(identifyingFilter, { }); + + if (!result) + throw new Error('empty result'); + + doc.identifier = identifier; + + if (result.length > 0) { + await updateConditional(opCtx, doc, result[0]); + } + else { + await insert(opCtx, doc); + } +} + + +async function updateConditional (opCtx, doc, storageDoc) { + + const { col, req, res } = opCtx; + + if (storageDoc.isValid === false) { + return opTools.sendJSONStatus(res, apiConst.HTTP.GONE); + } + + const modifiedDate = col.resolveDates(storageDoc) + , ifUnmodifiedSince = req.get('If-Unmodified-Since'); + + if (ifUnmodifiedSince + && dateTools.floorSeconds(modifiedDate) > dateTools.floorSeconds(new Date(ifUnmodifiedSince))) { + return opTools.sendJSONStatus(res, apiConst.HTTP.PRECONDITION_FAILED); + } + + await replace(opCtx, doc, storageDoc); +} + + +function updateOperation (ctx, env, app, col) { + + return async function operation (req, res) { + + const opCtx = { app, ctx, env, col, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await update(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }; +} + +module.exports = updateOperation; diff --git a/lib/api3/generic/update/replace.js b/lib/api3/generic/update/replace.js new file mode 100644 index 00000000000..c0c76c4b6eb --- /dev/null +++ b/lib/api3/generic/update/replace.js @@ -0,0 +1,62 @@ +'use strict'; + +const apiConst = require('../../const.json') + , security = require('../../security') + , validate = require('./validate.js') + , path = require('path') + , opTools = require('../../shared/operationTools') + ; + +/** + * Replace existing document in the collection + * @param {Object} opCtx + * @param {any} doc - new version of document to set + * @param {any} storageDoc - old version of document (existing in the storage) + * @param {Object} options + */ +async function replace (opCtx, doc, storageDoc, options) { + + const { ctx, auth, col, req, res } = opCtx; + const { isDeduplication } = options || {}; + + await security.demandPermission(opCtx, `api:${col.colName}:update`); + + if (validate(opCtx, doc, storageDoc, { isDeduplication }) !== true) + return; + + const now = new Date; + doc.srvModified = now.getTime(); + doc.srvCreated = storageDoc.srvCreated || doc.srvModified; + + if (auth && auth.subject && auth.subject.name) { + doc.subject = auth.subject.name; + } + + const matchedCount = await col.storage.replaceOne(storageDoc.identifier, doc); + + if (!matchedCount) + 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; + } + } + + opTools.sendJSON({ res, status: apiConst.HTTP.OK, fields }); + + ctx.bus.emit('storage-socket-update', { colName: col.colName, doc }); + col.autoPrune(); + ctx.bus.emit('data-received'); +} + + +module.exports = replace; diff --git a/lib/api3/generic/update/validate.js b/lib/api3/generic/update/validate.js new file mode 100644 index 00000000000..65233dddb4f --- /dev/null +++ b/lib/api3/generic/update/validate.js @@ -0,0 +1,48 @@ +'use strict'; + +const apiConst = require('../../const.json') + , opTools = require('../../shared/operationTools') + ; + + +/** + * Validation of document to update + * @param {Object} opCtx + * @param {Object} doc + * @param {Object} storageDoc + * @param {Object} options + * @returns string with error message if validation fails, true in case of success + */ +function validate (opCtx, doc, storageDoc, options) { + + const { res } = opCtx; + const { isPatching, isDeduplication } = options || {}; + + const immutable = ['identifier', 'date', 'utcOffset', 'eventType', 'device', 'app', + 'srvCreated', 'subject', 'srvModified', 'modifiedBy', 'isValid']; + + if (storageDoc.isReadOnly === true || storageDoc.readOnly === true || storageDoc.readonly === true) { + return opTools.sendJSONStatus(res, apiConst.HTTP.UNPROCESSABLE_ENTITY, + apiConst.MSG.HTTP_422_READONLY_MODIFICATION); + } + + for (const field of immutable) { + + // change of identifier is allowed in deduplication (for APIv1 documents) + if (field === 'identifier' && isDeduplication) + continue; + + // changing deleted document is without restrictions + if (storageDoc.isValid === false) + continue; + + if (typeof(doc[field]) !== 'undefined' && doc[field] !== storageDoc[field]) { + return opTools.sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, + apiConst.MSG.HTTP_400_IMMUTABLE_FIELD.replace('{0}', field)); + } + } + + return opTools.validateCommon(doc, res, { isPatching }); +} + +module.exports = validate; diff --git a/lib/api3/index.js b/lib/api3/index.js new file mode 100644 index 00000000000..2ba0aa762b1 --- /dev/null +++ b/lib/api3/index.js @@ -0,0 +1,118 @@ +'use strict'; + +const express = require('express') + , bodyParser = require('body-parser') + , renderer = require('./shared/renderer') + , storageSocket = require('./storageSocket') + , alarmSocket = require('./alarmSocket') + , apiConst = require('./const.json') + , security = require('./security') + , genericSetup = require('./generic/setup') + , opTools = require('./shared/operationTools') + ; + +function configure (env, ctx) { + + const self = { } + , app = express() + ; + + self.setENVTruthy = function setENVTruthy (varName, defaultValue) { + //for some reason Azure uses this prefix, maybe there is a good reason + let value = process.env['CUSTOMCONNSTR_' + varName] + || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] + || process.env[varName] + || process.env[varName.toLowerCase()]; + + value = value != null ? value : defaultValue; + + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + + app.set(varName, value); + return value; + }; + app.setENVTruthy = self.setENVTruthy; + + + self.setupApiEnvironment = function setupApiEnvironment () { + + app.use(bodyParser.json({ + limit: 1048576 * 50 + }), function errorHandler (err, req, res, next) { + console.error(err); + res.status(apiConst.HTTP.INTERNAL_ERROR).json({ + status: apiConst.HTTP.INTERNAL_ERROR, + message: apiConst.MSG.HTTP_500_INTERNAL_ERROR + }); + if (next) { // we need 4th parameter next to behave like error handler, but we have to use it to prevent "unused variable" message + } + }); + + app.use(renderer.extension2accept); + + // we don't need these here + app.set('etag', false); + app.set('x-powered-by', false); // this seems to be unreliable + app.use(function (req, res, next) { + res.removeHeader('x-powered-by'); + next(); + }); + + app.set('name', env.name); + app.set('version', env.version); + app.set('apiVersion', apiConst.API3_VERSION); + app.set('units', env.DISPLAY_UNITS); + app.set('ci', process.env['CI'] ? true: false); + app.set('enabledCollections', ['devicestatus', 'entries', 'food', 'profile', 'settings', 'treatments']); + + self.setENVTruthy('API3_SECURITY_ENABLE', apiConst.API3_SECURITY_ENABLE); + 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); + }; + + + self.setupApiRoutes = function setupApiRoutes () { + + app.get('/version', require('./specific/version')(app, ctx, env)); + + if (app.get('env') === 'development' || app.get('ci')) { // for development and testing purposes only + app.get('/test', async function test (req, res) { + + try { + const opCtx = {app, ctx, env, req, res}; + opCtx.auth = await security.authenticate(opCtx); + await security.demandPermission(opCtx, 'api:entries:read'); + res.status(apiConst.HTTP.OK).end(); + } catch (error) { + console.error(error); + } + }); + } + + app.get('/lastModified', require('./specific/lastModified')(app, ctx, env)); + + app.get('/status', require('./specific/status')(app, ctx, env)); + }; + + + self.setupApiEnvironment(); + genericSetup(ctx, env, app); + self.setupApiRoutes(); + + app.use('/swagger-ui-dist', (req, res) => { + 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); + ctx.alarmSocket = new alarmSocket(app, env, ctx); + + return app; +} + +module.exports = configure; diff --git a/lib/api3/security.js b/lib/api3/security.js new file mode 100644 index 00000000000..c42e5a114bd --- /dev/null +++ b/lib/api3/security.js @@ -0,0 +1,92 @@ +'use strict'; + +const apiConst = require('./const.json') + , _ = require('lodash') + , shiroTrie = require('shiro-trie') + , opTools = require('./shared/operationTools') + , forwarded = require('forwarded-for') + ; + + +function getRemoteIP (req) { + const address = forwarded(req, req.headers); + return address.ip; +} + + +function authenticate (opCtx) { + return new Promise(function promise (resolve, reject) { + + let { app, ctx, req, res } = opCtx; + + if (!app.get('API3_SECURITY_ENABLE')) { + const adminShiro = shiroTrie.new(); + adminShiro.add('*'); + return resolve({ shiros: [ adminShiro ] }); + } + + let token + if (req.header('Authorization')) { + const parts = req.header('Authorization').split(' '); + if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') { + token = parts[1]; + } + } + + if (!token) { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN)); + } + + ctx.authorization.resolve({ token, ip: getRemoteIP(req) }, function resolveFinish (err, result) { + if (err) { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.UNAUTHORIZED, apiConst.MSG.HTTP_401_BAD_TOKEN)); + } + else { + return resolve(result); + } + }); + }); +} + + +/** + * Checks for the permission from the authorization without error response sending + * @param {any} auth + * @param {any} permission + */ +function checkPermission (auth, permission) { + + if (auth) { + const found = _.find(auth.shiros, function checkEach (shiro) { + return shiro && shiro.check(permission); + }); + return _.isObject(found); + } + else { + return false; + } +} + + + +function demandPermission (opCtx, permission) { + return new Promise(function promise (resolve, reject) { + const { auth, res } = opCtx; + + if (checkPermission(auth, permission)) { + return resolve(true); + } else { + return reject( + opTools.sendJSONStatus(res, apiConst.HTTP.FORBIDDEN, apiConst.MSG.HTTP_403_MISSING_PERMISSION.replace('{0}', permission))); + } + }); +} + + +module.exports = { + authenticate, + checkPermission, + demandPermission +}; diff --git a/lib/api3/shared/dateTools.js b/lib/api3/shared/dateTools.js new file mode 100644 index 00000000000..14b67f9e109 --- /dev/null +++ b/lib/api3/shared/dateTools.js @@ -0,0 +1,78 @@ +'use strict'; + +const moment = require('moment') + , stringTools = require('./stringTools') + , apiConst = require('../const.json') + ; + + +/** + * Floor date to whole seconds (cut off milliseconds) + * @param {Date} date + */ +function floorSeconds (date) { + let ms = date.getTime(); + ms -= ms % 1000; + return new Date(ms); +} + + +/** + * Parse date as moment object from value or array of values. + * @param {any} value + */ +function parseToMoment (value) +{ + if (!value) + return null; + + if (Array.isArray(value)) { + for (let item of value) { + let m = parseToMoment(item); + + if (m !== null) + return m; + } + } + else { + + if (typeof value === 'string' && stringTools.isNumberInString(value)) { + value = parseFloat(value); + } + + if (typeof value === 'number') { + let m = moment(value); + + if (!m.isValid()) + return null; + + if (m.valueOf() < apiConst.MIN_TIMESTAMP) + m = moment.unix(m); + + if (!m.isValid() || m.valueOf() < apiConst.MIN_TIMESTAMP) + return null; + + return m; + } + + if (typeof value === 'string') { + let m = moment.parseZone(value, moment.ISO_8601); + + if (!m.isValid()) + m = moment.parseZone(value, moment.RFC_2822); + + if (!m.isValid() || m.valueOf() < apiConst.MIN_TIMESTAMP) + return null; + + return m; + } + } + + // no parsing option succeeded => failure + return null; +} + +module.exports = { + floorSeconds, + parseToMoment +}; diff --git a/lib/api3/shared/fieldsProjector.js b/lib/api3/shared/fieldsProjector.js new file mode 100644 index 00000000000..921c7cc6df8 --- /dev/null +++ b/lib/api3/shared/fieldsProjector.js @@ -0,0 +1,82 @@ +'use strict'; + +const _each = require('lodash/each'); + +/** + * Decoder of 'fields' parameter providing storage projections + * @param {string} fieldsString - fields parameter from user + */ +function FieldsProjector (fieldsString) { + + const self = this + , exclude = []; + let specific = null; + + switch (fieldsString) + { + case '_all': + break; + + default: + if (fieldsString) { + specific = fieldsString.split(','); + } + } + + const systemFields = ['identifier', 'srvCreated', 'created_at', 'date']; + + /** + * Prepare projection definition for storage query + * */ + self.storageProjection = function storageProjection () { + const projection = { }; + + if (specific) { + _each(specific, function include (field) { + projection[field] = 1; + }); + + _each(systemFields, function include (field) { + projection[field] = 1; + }); + } + else { + _each(exclude, function exclude (field) { + projection[field] = 0; + }); + + _each(exclude, function exclude (field) { + if (systemFields.indexOf(field) >= 0) { + delete projection[field]; + } + }); + } + + return projection; + }; + + + /** + * Cut off unwanted fields from given document + * @param {Object} doc + */ + self.applyProjection = function applyProjection (doc) { + + if (specific) { + for(const field in doc) { + if (specific.indexOf(field) === -1) { + delete doc[field]; + } + } + } + else { + _each(exclude, function include (field) { + if (typeof(doc[field]) !== 'undefined') { + delete doc[field]; + } + }); + } + }; +} + +module.exports = FieldsProjector; \ No newline at end of file diff --git a/lib/api3/shared/operationTools.js b/lib/api3/shared/operationTools.js new file mode 100644 index 00000000000..c8b8a008f6d --- /dev/null +++ b/lib/api3/shared/operationTools.js @@ -0,0 +1,135 @@ +'use strict'; + +const apiConst = require('../const.json') + , stringTools = require('./stringTools') + , uuid = require('uuid') + , 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 + }; + + if (title) { json.message = title } + + if (description) { json.description = description } + + // Add optional warning message. + if (warning) { json.warning = warning; } + + res.status(status) + .json(json); + + return title; +} + + +/** + * Validate document's common fields + * @param {Object} doc + * @param {any} res + * @param {Object} options + * @returns {any} - string with error message if validation fails, true in case of success + */ +function validateCommon (doc, res, options) { + + const { isPatching } = options || {}; + + + if ((!isPatching || typeof(doc.date) !== 'undefined') + + && (typeof(doc.date) !== 'number' + || doc.date <= apiConst.MIN_TIMESTAMP) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_DATE); + } + + + if ((!isPatching || typeof(doc.utcOffset) !== 'undefined') + + && (typeof(doc.utcOffset) !== 'number' + || doc.utcOffset < apiConst.MIN_UTC_OFFSET + || doc.utcOffset > apiConst.MAX_UTC_OFFSET) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_UTC); + } + + + if ((!isPatching || typeof(doc.app) !== 'undefined') + + && (typeof(doc.app) !== 'string' + || stringTools.isNullOrWhitespace(doc.app)) + ) { + return sendJSONStatus(res, apiConst.HTTP.BAD_REQUEST, apiConst.MSG.HTTP_400_BAD_FIELD_APP); + } + + return true; +} + + +/** + * Calculate identifier for the document + * @param {Object} doc + * @returns string + */ +function calculateIdentifier (doc) { + if (!doc) + return undefined; + + let key = doc.device + '_' + doc.date; + if (doc.eventType) { + key += '_' + doc.eventType; + } + + return uuid.v5(key, uuidNamespace); +} + + +/** + * Validate identifier in the document + * @param {Object} doc + */ +function resolveIdentifier (doc) { + + let identifier = calculateIdentifier(doc); + if (doc.identifier) { + if (doc.identifier !== identifier) { + console.warn(`APIv3: Identifier mismatch (expected: ${identifier}, received: ${doc.identifier})`); + console.log(doc); + } + } + else { + doc.identifier = identifier; + } +} + + +module.exports = { + sendJSON, + sendJSONStatus, + validateCommon, + calculateIdentifier, + resolveIdentifier +}; diff --git a/lib/api3/shared/renderer.js b/lib/api3/shared/renderer.js new file mode 100644 index 00000000000..842785709b6 --- /dev/null +++ b/lib/api3/shared/renderer.js @@ -0,0 +1,112 @@ +'use strict'; + +const apiConst = require('../const.json') + , mime = require('mime') + , url = require('url') + , opTools = require('./operationTools') + , EasyXml = require('easyxml') + , csvStringify = require('csv-stringify') + ; + + +/** + * Middleware that converts url's extension to Accept HTTP request header + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +function extension2accept (req, res, next) { + + const pathSplit = req.path.split('.'); + + if (pathSplit.length < 2) + return next(); + + const pathBase = pathSplit[0] + , extension = pathSplit.slice(1).join('.'); + + if (!extension) + return next(); + + const mimeType = mime.getType(extension); + if (!mimeType) + return opTools.sendJSONStatus(res, apiConst.HTTP.NOT_ACCEPTABLE, apiConst.MSG.HTTP_406_UNSUPPORTED_FORMAT); + + req.extToAccept = { + url: req.url, + accept: req.headers.accept + }; + + req.headers.accept = mimeType; + const parsed = url.parse(req.url); + parsed.pathname = pathBase; + req.url = url.format(parsed); + + next(); +} + + +/** + * Sends data to output using the client's desired format + * @param {Object} res + * @param {any} data + */ +function render (res, data) { + res.format({ + 'json': () => renderJson(res, data), + 'csv': () => renderCsv(res, data), + 'xml': () => renderXml(res, data), + 'default': () => + opTools.sendJSONStatus(res, apiConst.HTTP.NOT_ACCEPTABLE, apiConst.MSG.HTTP_406_UNSUPPORTED_FORMAT) + }); +} + + +/** + * 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 + * @param {any} data + */ +function renderCsv (res, data) { + const csvSource = Array.isArray(data) ? data : [data]; + csvStringify(csvSource, { + header: true + }, + function csvStringified (err, output) { + res.send(output); + }); +} + + +/** + * Format data to output as .xml + * @param {Object} res + * @param {any} data + */ +function renderXml (res, data) { + const serializer = new EasyXml({ + rootElement: 'item', + dateFormat: 'ISO', + manifest: true + }); + res.send(serializer.render(data)); +} + + +module.exports = { + extension2accept, + render +}; diff --git a/lib/api3/shared/storageTools.js b/lib/api3/shared/storageTools.js new file mode 100644 index 00000000000..b7d9dca6776 --- /dev/null +++ b/lib/api3/shared/storageTools.js @@ -0,0 +1,63 @@ +'use strict'; + +function getStorageVersion (app) { + + return new Promise(function (resolve, reject) { + + try { + const storage = app.get('entriesCollection').storage; + let storageVersion = app.get('storageVersion'); + + if (storageVersion) { + process.nextTick(() => { + resolve(storageVersion); + }); + } else { + storage.version() + .then(storageVersion => { + + app.set('storageVersion', storageVersion); + resolve(storageVersion); + }, reject); + } + } catch (error) { + reject(error); + } + }); +} + + +function getVersionInfo(app) { + + return new Promise(function (resolve, reject) { + + try { + const srvDate = new Date() + , info = { version: app.get('version') + , apiVersion: app.get('apiVersion') + , srvDate: srvDate.getTime() + }; + + getStorageVersion(app) + .then(storageVersion => { + + if (!storageVersion) + throw new Error('empty storageVersion'); + + info.storage = storageVersion; + + resolve(info); + + }, reject); + + } catch(error) { + reject(error); + } + }); +} + + +module.exports = { + getStorageVersion, + getVersionInfo +}; diff --git a/lib/api3/shared/stringTools.js b/lib/api3/shared/stringTools.js new file mode 100644 index 00000000000..b71a4b4f1a6 --- /dev/null +++ b/lib/api3/shared/stringTools.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * Check the string for strictly valid number (no other characters present) + * @param {any} str + */ +function isNumberInString (str) { + return !isNaN(parseFloat(str)) && isFinite(str); +} + + +/** + * Check the string for non-whitespace characters presence + * @param {any} input + */ +function isNullOrWhitespace (input) { + + if (typeof input === 'undefined' || input == null) return true; + + return input.replace(/\s/g, '').length < 1; +} + + + +module.exports = { + isNumberInString, + isNullOrWhitespace +}; diff --git a/lib/api3/specific/lastModified.js b/lib/api3/specific/lastModified.js new file mode 100644 index 00000000000..f550d56f89c --- /dev/null +++ b/lib/api3/specific/lastModified.js @@ -0,0 +1,101 @@ +'use strict'; + +function configure (app, ctx, env) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , security = require('../security') + , opTools = require('../shared/operationTools') + ; + + api.get('/lastModified', async function getLastModified (req, res) { + + async function getLastModified (col) { + + let result; + const lastModified = await col.storage.getLastModified('srvModified'); + + if (lastModified) { + result = lastModified.srvModified ? lastModified.srvModified : null; + } + + if (col.fallbackDateField) { + + const lastModified = await col.storage.getLastModified(col.fallbackDateField); + + if (lastModified && lastModified[col.fallbackDateField]) { + let timestamp = lastModified[col.fallbackDateField]; + if (typeof(timestamp) === 'string') { + timestamp = (new Date(timestamp)).getTime(); + } + + if (result === null || result < timestamp) { + result = timestamp; + } + } + } + + return { colName: col.colName, lastModified: result }; + } + + + async function collectionsAsync (auth) { + + const cols = app.get('collections') + , promises = [] + , output = {} + ; + + for (const colName in cols) { + const col = cols[colName]; + + if (security.checkPermission(auth, 'api:' + col.colName + ':read')) { + promises.push(getLastModified(col)); + } + } + + const results = await Promise.all(promises); + + for (const result of results) { + if (result.lastModified) + output[result.colName] = result.lastModified; + } + + return output; + } + + + async function operation (opCtx) { + + const { res, auth } = opCtx; + const srvDate = new Date(); + + let info = { + srvDate: srvDate.getTime(), + collections: { } + }; + + info.collections = await collectionsAsync(auth); + + opTools.sendJSON({ res, result: info }); + } + + + const opCtx = { app, ctx, env, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await operation(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/specific/status.js b/lib/api3/specific/status.js new file mode 100644 index 00000000000..5ec28929fbe --- /dev/null +++ b/lib/api3/specific/status.js @@ -0,0 +1,71 @@ +'use strict'; + +function configure (app, ctx, env) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , storageTools = require('../shared/storageTools') + , security = require('../security') + , opTools = require('../shared/operationTools') + ; + + api.get('/status', async function getStatus (req, res) { + + function permsForCol (col, auth) { + let colPerms = ''; + + if (security.checkPermission(auth, 'api:' + col.colName + ':create')) { + colPerms += 'c'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':read')) { + colPerms += 'r'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':update')) { + colPerms += 'u'; + } + + if (security.checkPermission(auth, 'api:' + col.colName + ':delete')) { + colPerms += 'd'; + } + + return colPerms; + } + + + async function operation (opCtx) { + const cols = app.get('collections'); + + let info = await storageTools.getVersionInfo(app); + + info.apiPermissions = {}; + for (let col in cols) { + const colPerms = permsForCol(col, opCtx.auth); + if (colPerms) { + info.apiPermissions[col] = colPerms; + } + } + + opTools.sendJSON({ res, result: info }); + } + + + const opCtx = { app, ctx, env, req, res }; + + try { + opCtx.auth = await security.authenticate(opCtx); + + await operation(opCtx); + + } catch (err) { + console.error(err); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/specific/version.js b/lib/api3/specific/version.js new file mode 100644 index 00000000000..ba7685fe486 --- /dev/null +++ b/lib/api3/specific/version.js @@ -0,0 +1,28 @@ +'use strict'; + +function configure (app) { + const express = require('express') + , api = express.Router( ) + , apiConst = require('../const.json') + , storageTools = require('../shared/storageTools') + , opTools = require('../shared/operationTools') + ; + + api.get('/version', async function getVersion (req, res) { + + try { + const versionInfo = await storageTools.getVersionInfo(app); + + opTools.sendJSON({ res, result: versionInfo }); + + } catch(error) { + console.error(error); + if (!res.headersSent) { + return opTools.sendJSONStatus(res, apiConst.HTTP.INTERNAL_ERROR, apiConst.MSG.STORAGE_ERROR); + } + } + }); + + return api; +} +module.exports = configure; diff --git a/lib/api3/storage/mongoCachedCollection/index.js b/lib/api3/storage/mongoCachedCollection/index.js new file mode 100644 index 00000000000..b2fd8f21a55 --- /dev/null +++ b/lib/api3/storage/mongoCachedCollection/index.js @@ -0,0 +1,147 @@ +'use strict'; + +const _ = require('lodash') + +/** + * Storage implementation which wraps mongo baseStorage with caching + * @param {Object} ctx + * @param {Object} env + * @param {string} colName - name of the collection in mongo database + * @param {Object} baseStorage - wrapped mongo storage implementation + */ +function MongoCachedCollection (ctx, env, colName, baseStorage) { + + const self = this; + + self.colName = colName; + + self.identifyingFilter = baseStorage.identifyingFilter; + + self.findOne = (...args) => baseStorage.findOne(...args); + + self.findOneFilter = (...args) => baseStorage.findOneFilter(...args); + + self.findMany = (...args) => baseStorage.findMany(...args); + + + self.insertOne = async (doc) => { + const result = await baseStorage.insertOne(doc, { normalize: false }); + + if (cacheSupported()) { + updateInCache([doc]); + } + + if (doc._id) { + delete doc._id; + } + return result; + } + + + self.replaceOne = async (identifier, doc) => { + const result = await baseStorage.replaceOne(identifier, doc); + + if (cacheSupported()) { + const rawDocs = await baseStorage.findOne(identifier, null, { normalize: false }) + updateInCache([rawDocs[0]]) + } + + return result; + } + + + self.updateOne = async (identifier, setFields) => { + const result = await baseStorage.updateOne(identifier, setFields); + + if (cacheSupported()) { + const rawDocs = await baseStorage.findOne(identifier, null, { normalize: false }) + + if (rawDocs[0].isValid === false) { + deleteInCache(rawDocs) + } + else { + updateInCache([rawDocs[0]]) + } + } + + return result; + } + + self.deleteOne = async (identifier) => { + let invalidateDocs + if (cacheSupported()) { + invalidateDocs = await baseStorage.findOne(identifier, { _id: 1 }, { normalize: false }) + } + + const result = await baseStorage.deleteOne(identifier); + + if (cacheSupported()) { + deleteInCache(invalidateDocs) + } + + return result; + } + + self.deleteManyOr = async (filter) => { + let invalidateDocs + if (cacheSupported()) { + invalidateDocs = await baseStorage.findMany({ filter, + limit: 1000, + skip: 0, + projection: { _id: 1 }, + options: { normalize: false } }); + } + + const result = await baseStorage.deleteManyOr(filter); + + if (cacheSupported()) { + deleteInCache(invalidateDocs) + } + + return result; + } + + self.version = (...args) => baseStorage.version(...args); + + self.getLastModified = (...args) => baseStorage.getLastModified(...args); + + function cacheSupported () { + return ctx.cache + && ctx.cache[colName] + && _.isArray(ctx.cache[colName]); + } + + function updateInCache (doc) { + if (doc && doc.isValid === false) { + deleteInCache([doc._id]) + } + else { + ctx.bus.emit('data-update', { + type: colName + , op: 'update' + , changes: doc + }); + } + } + + function deleteInCache (docs) { + let changes + if (_.isArray(docs)) { + if (docs.length === 0) { + return + } + else if (docs.length === 1 && docs[0]._id) { + const _id = docs[0]._id.toString() + changes = [ _id ] + } + } + + ctx.bus.emit('data-update', { + type: colName + , op: 'remove' + , changes + }); + } +} + +module.exports = MongoCachedCollection; diff --git a/lib/api3/storage/mongoCollection/find.js b/lib/api3/storage/mongoCollection/find.js new file mode 100644 index 00000000000..013d008ec98 --- /dev/null +++ b/lib/api3/storage/mongoCollection/find.js @@ -0,0 +1,101 @@ +'use strict'; + +const utils = require('./utils') + , _ = require('lodash') + ; + + +/** + * Find single document by identifier + * @param {Object} col + * @param {string} identifier + * @param {Object} projection + * @param {Object} options + */ +function findOne (col, identifier, projection, options) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.find(filter) + .project(projection) + .sort({ identifier: -1 }) // document with identifier first (not the fallback one) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + if (!options || options.normalize !== false) { + _.each(result, utils.normalizeDoc); + } + resolve(result); + } + }); + }); +} + + +/** + * Find single document by query filter + * @param {Object} col + * @param {Object} filter specific filter + * @param {Object} projection + * @param {Object} options + */ +function findOneFilter (col, filter, projection, options) { + + return new Promise(function (resolve, reject) { + + col.find(filter) + .project(projection) + .sort({ identifier: -1 }) // document with identifier first (not the fallback one) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + if (!options || options.normalize !== false) { + _.each(result, utils.normalizeDoc); + } + resolve(result); + } + }); + }); +} + + +/** + * Find many documents matching the filtering criteria + */ +function findMany (col, args) { + const logicalOperator = args.logicalOperator || 'and'; + return new Promise(function (resolve, reject) { + + const filter = utils.parseFilter(args.filter, logicalOperator, args.onlyValid); + + col.find(filter) + .sort(args.sort) + .limit(args.limit) + .skip(args.skip) + .project(args.projection) + .toArray(function mongoDone (err, result) { + + if (err) { + reject(err); + } else { + if (!args.options || args.options.normalize !== false) { + _.each(result, utils.normalizeDoc); + } + resolve(result); + } + }); + }); +} + + +module.exports = { + findOne, + findOneFilter, + findMany +}; diff --git a/lib/api3/storage/mongoCollection/index.js b/lib/api3/storage/mongoCollection/index.js new file mode 100644 index 00000000000..ef041ce9c1f --- /dev/null +++ b/lib/api3/storage/mongoCollection/index.js @@ -0,0 +1,90 @@ +'use strict'; + +/** + * Storage implementation using mongoDB + * @param {Object} ctx + * @param {Object} env + * @param {string} colName - name of the collection in mongo database + */ +function MongoCollection (ctx, env, colName) { + + const self = this + , utils = require('./utils') + , find = require('./find') + , modify = require('./modify') + ; + + self.colName = colName; + + self.col = ctx.store.collection(colName); + + ctx.store.ensureIndexes(self.col, [ 'identifier', + 'srvModified', + 'isValid' + ]); + + + self.identifyingFilter = utils.identifyingFilter; + + self.findOne = (...args) => find.findOne(self.col, ...args); + + self.findOneFilter = (...args) => find.findOneFilter(self.col, ...args); + + self.findMany = (...args) => find.findMany(self.col, ...args); + + self.insertOne = (...args) => modify.insertOne(self.col, ...args); + + self.replaceOne = (...args) => modify.replaceOne(self.col, ...args); + + self.updateOne = (...args) => modify.updateOne(self.col, ...args); + + self.deleteOne = (...args) => modify.deleteOne(self.col, ...args); + + self.deleteManyOr = (...args) => modify.deleteManyOr(self.col, ...args); + + + /** + * Get server version + */ + self.version = function version () { + + return new Promise(function (resolve, reject) { + + ctx.store.db.admin().buildInfo({}, function mongoDone (err, result) { + + err + ? reject(err) + : resolve({ + storage: 'mongodb', + version: result.version + }); + }); + }); + }; + + + /** + * Get timestamp (e.g. srvModified) of the last modified document + */ + self.getLastModified = function getLastModified (fieldName) { + + return new Promise(function (resolve, reject) { + + self.col.find() + + .sort({ [fieldName]: -1 }) + + .limit(1) + + .project({ [fieldName]: 1 }) + + .toArray(function mongoDone (err, [ result ]) { + err + ? reject(err) + : resolve(result); + }); + }); + } +} + +module.exports = MongoCollection; diff --git a/lib/api3/storage/mongoCollection/modify.js b/lib/api3/storage/mongoCollection/modify.js new file mode 100644 index 00000000000..7183f1c971a --- /dev/null +++ b/lib/api3/storage/mongoCollection/modify.js @@ -0,0 +1,127 @@ +'use strict'; + +const utils = require('./utils') + ; + +/** + * Insert single document + * @param {Object} col + * @param {Object} doc + * @param {Object} options + */ +function insertOne (col, doc, options) { + + return new Promise(function (resolve, reject) { + + col.insertOne(doc, function mongoDone(err, result) { + + if (err) { + reject(err); + } else { + const identifier = doc.identifier || result.insertedId.toString(); + + if (!options || options.normalize !== false) { + delete doc._id; + } + resolve(identifier); + } + }); + }); +} + + +/** + * Replace single document + * @param {Object} col + * @param {string} identifier + * @param {Object} doc + */ +function replaceOne (col, identifier, doc) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.replaceOne(filter, doc, { upsert: true }, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve(result.matchedCount); + } + }); + }); +} + + +/** + * Update single document by identifier + * @param {Object} col + * @param {string} identifier + * @param {object} setFields + */ +function updateOne (col, identifier, setFields) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.updateOne(filter, { $set: setFields }, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ updated: result.result.nModified }); + } + }); + }); +} + + +/** + * Permanently remove single document by identifier + * @param {Object} col + * @param {string} identifier + */ +function deleteOne (col, identifier) { + + return new Promise(function (resolve, reject) { + + const filter = utils.filterForOne(identifier); + + col.deleteOne(filter, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ deleted: result.result.n }); + } + }); + }); +} + + +/** + * Permanently remove many documents matching any of filtering criteria + */ +function deleteManyOr (col, filterDef) { + + return new Promise(function (resolve, reject) { + + const filter = utils.parseFilter(filterDef, 'or'); + + col.deleteMany(filter, function mongoDone(err, result) { + if (err) { + reject(err); + } else { + resolve({ deleted: result.deletedCount }); + } + }); + }); +} + + +module.exports = { + insertOne, + replaceOne, + updateOne, + deleteOne, + deleteManyOr +}; diff --git a/lib/api3/storage/mongoCollection/utils.js b/lib/api3/storage/mongoCollection/utils.js new file mode 100644 index 00000000000..a2f7b16520c --- /dev/null +++ b/lib/api3/storage/mongoCollection/utils.js @@ -0,0 +1,177 @@ +'use strict'; + +const _ = require('lodash') + , checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$") + , ObjectID = require('mongodb').ObjectID +; + + +/** + * Normalize document (make it mongoDB independent) + * @param {Object} doc - document loaded from mongoDB + */ +function normalizeDoc (doc) { + if (!doc.identifier) { + doc.identifier = doc._id.toString(); + } + + delete doc._id; +} + + +/** + * Parse filter definition array into mongoDB filtering object + * @param {any} filterDef + * @param {string} logicalOperator + * @param {bool} onlyValid + */ +function parseFilter (filterDef, logicalOperator, onlyValid) { + let filter = { }; + if (!filterDef) + return filter; + + if (!_.isArray(filterDef)) { + return filterDef; + } + + let clauses = []; + + for (const itemDef of filterDef) { + let item; + + switch (itemDef.operator) { + case 'eq': + item = itemDef.value; + break; + + case 'ne': + item = { $ne: itemDef.value }; + break; + + case 'gt': + item = { $gt: itemDef.value }; + break; + + case 'gte': + item = { $gte: itemDef.value }; + break; + + case 'lt': + item = { $lt: itemDef.value }; + break; + + case 'lte': + item = { $lte: itemDef.value }; + break; + + case 'in': + item = { $in: itemDef.value.toString().split('|') }; + break; + + case 'nin': + item = { $nin: itemDef.value.toString().split('|') }; + break; + + case 're': + item = { $regex: itemDef.value.toString() }; + break; + + default: + throw new Error('Unsupported or missing filter operator ' + itemDef.operator); + } + + if (logicalOperator === 'or') { + let clause = { }; + clause[itemDef.field] = item; + clauses.push(clause); + } + else { + filter[itemDef.field] = item; + } + } + + if (logicalOperator === 'or') { + filter = { $or: clauses }; + } + + if (onlyValid) { + filter.isValid = { $ne: false }; + } + + return filter; +} + + +/** + * Create query filter for single document with identifier fallback + * @param {string} identifier + */ +function filterForOne (identifier) { + + const filterOpts = [ { identifier } ]; + + // fallback to "identifier = _id" + if (checkForHexRegExp.test(identifier)) { + filterOpts.push({ _id: ObjectID(identifier) }); + } + + return { $or: filterOpts }; +} + + +/** + * Create query filter to check whether the document already exists in the storage. + * This function resolves eventual fallback deduplication. + * @param {string} identifier - identifier of document to check its existence in the storage + * @param {Object} doc - document to check its existence in the storage + * @param {Array} dedupFallbackFields - fields that all need to be matched to identify document via fallback deduplication + * @returns {Object} - query filter for mongo or null in case of no identifying possibility + */ +function identifyingFilter (identifier, doc, dedupFallbackFields) { + + const filterItems = []; + + if (identifier) { + // standard identifier field (APIv3) + filterItems.push({ identifier: identifier }); + + // fallback to "identifier = _id" (APIv1) + if (checkForHexRegExp.test(identifier)) { + filterItems.push({ identifier: { $exists: false }, _id: ObjectID(identifier) }); + } + } + + // let's deal with eventual fallback deduplication + if (!_.isEmpty(doc) && _.isArray(dedupFallbackFields) && dedupFallbackFields.length > 0) { + let dedupFilterItems = []; + + _.each(dedupFallbackFields, function addDedupField (field) { + + if (doc[field] !== undefined) { + + let dedupFilterItem = { }; + dedupFilterItem[field] = doc[field]; + dedupFilterItems.push(dedupFilterItem); + } + }); + + if (dedupFilterItems.length === dedupFallbackFields.length) { // all dedup fields are present + + dedupFilterItems.push({ identifier: { $exists: false } }); // force not existing identifier for fallback deduplication + filterItems.push({ $and: dedupFilterItems }); + } + } + + if (filterItems.length > 0) + return { $or: filterItems }; + else + return null; // we don't have any filtering rule to identify the document in the storage +} + + +module.exports = { + normalizeDoc, + parseFilter, + filterForOne, + identifyingFilter +}; diff --git a/lib/api3/storageSocket.js b/lib/api3/storageSocket.js new file mode 100644 index 00000000000..d4ebb1254f3 --- /dev/null +++ b/lib/api3/storageSocket.js @@ -0,0 +1,151 @@ +'use strict'; + +const apiConst = require('./const'); +const forwarded = require('forwarded-for'); + +function getRemoteIP (req) { + const address = forwarded(req, req.headers); + return address.ip; +} + +/** + * Socket.IO broadcaster of any storage change + */ +function StorageSocket (app, env, ctx) { + + const self = this; + + const LOG_GREEN = '\x1B[32m' + , LOG_MAGENTA = '\x1B[35m' + , LOG_RESET = '\x1B[0m' + , LOG = LOG_GREEN + 'STORAGE SOCKET: ' + LOG_RESET + , LOG_ERROR = LOG_MAGENTA + 'STORAGE SOCKET: ' + LOG_RESET + , NAMESPACE = '/storage' + ; + + + /** + * Initialize socket namespace and bind the events + * @param {Object} io Socket.IO object to multiplex namespaces + */ + self.init = function init (io) { + self.io = io; + + self.namespace = io.of(NAMESPACE); + self.namespace.on('connection', function onConnected (socket) { + + const remoteIP = getRemoteIP(socket.request); + console.log(LOG + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP); + + socket.on('disconnect', function onDisconnect () { + console.log(LOG + 'Disconnected client ID: ', socket.client.id); + }); + + socket.on('subscribe', function onSubscribe (message, returnCallback) { + self.subscribe(socket, message, returnCallback); + }); + }); + + ctx.bus.on('storage-socket-create', self.emitCreate); + ctx.bus.on('storage-socket-update', self.emitUpdate); + ctx.bus.on('storage-socket-delete', self.emitDelete); + }; + + + /** + * Authorize Socket.IO client and subscribe him to authorized rooms + * @param {Object} socket + * @param {Object} message input message from the client + * @param {Function} returnCallback function for returning a value back to the client + */ + self.subscribe = function subscribe (socket, message, returnCallback) { + const shouldCallBack = typeof(returnCallback) === 'function'; + + if (message && message.accessToken) { + return ctx.authorization.resolveAccessToken(message.accessToken, function resolveFinish (err, auth) { + if (err) { + console.log(`${LOG_ERROR} Authorization failed for accessToken:`, message.accessToken); + + if (shouldCallBack) { + returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN }); + } + return err; + } + else { + return self.subscribeAuthorized(socket, message, auth, returnCallback); + } + }); + } + + console.log(`${LOG_ERROR} Authorization failed for message:`, message); + if (shouldCallBack) { + returnCallback({ success: false, message: apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN}); + } + }; + + + /** + * Subscribe already authorized Socket.IO client to his rooms + * @param {Object} socket + * @param {Object} message input message from the client + * @param {Object} auth authorization of the client + * @param {Function} returnCallback function for returning a value back to the client + */ + self.subscribeAuthorized = function subscribeAuthorized (socket, message, auth, returnCallback) { + const shouldCallBack = typeof(returnCallback) === 'function'; + const enabledCols = app.get('enabledCollections'); + const cols = Array.isArray(message.collections) ? message.collections : enabledCols; + const subscribed = []; + + for (const col of cols) { + if (enabledCols.includes(col)) { + const permission = (col === 'settings') ? `api:${col}:admin` : `api:${col}:read`; + + if (ctx.authorization.checkMultiple(permission, auth.shiros)) { + socket.join(col); + subscribed.push(col); + } + } + } + + const doc = subscribed.length > 0 + ? { success: true, collections: subscribed } + : { success: false, message: apiConst.MSG.SOCKET_UNAUTHORIZED_TO_ANY }; + if (shouldCallBack) { + returnCallback(doc); + } + return doc; + }; + + + /** + * Emit create event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitCreate = function emitCreate (event) { + self.namespace.to(event.colName) + .emit('create', event); + }; + + + /** + * Emit update event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitUpdate = function emitUpdate (event) { + self.namespace.to(event.colName) + .emit('update', event); + }; + + + /** + * Emit delete event to the subscribers (of the collection's room) + * @param {Object} event + */ + self.emitDelete = function emitDelete (event) { + self.namespace.to(event.colName) + .emit('delete', event); + } +} + +module.exports = StorageSocket; diff --git a/lib/api3/swagger.json b/lib/api3/swagger.json new file mode 100644 index 00000000000..20a8830a214 --- /dev/null +++ b/lib/api3/swagger.json @@ -0,0 +1,2514 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Nightscout API", + "description": "Nightscout API v3 is a component of cgm-remote-monitor project. It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange.\n\nAPI v3 uses these environment variables, among other things:\n- Security switch (optional, default = `true`)
API3_SECURITY_ENABLE=true
You can turn the whole security mechanism off, e.g. for debugging or development purposes, but this should never be set to false in production.\n\n- Maximum limit count of documents retrieved from single query
API3_MAX_LIMIT=1000
\n\n- Autopruning of obsolete documents (optional, default is only `DEVICESTATUS`=60)
API3_AUTOPRUNE_DEVICESTATUS=60\nAPI3_AUTOPRUNE_ENTRIES=365\nAPI3_AUTOPRUNE_TREATMENTS=120 
You can specify for which collections autopruning will be activated and length of retention period in days, e.g. \"Hold 60 days of devicestatus, automatically delete older documents, hold 365 days of treatments and entries, automatically delete older documents.\"\n\n- Fallback deduplication switch (optional, default = true)
API3_DEDUP_FALLBACK_ENABLED=true
API3 uses the `identifier` field for document identification and mutual distinction within a single collection. There is automatic deduplication implemented matching the equal `identifier` field. E.g. `CREATE` operation for document having the same `identifier` as another one existing in the database is automatically transformed into `UPDATE` operation of the document found in the database.\nDocuments not created via API v3 usually does not have any `identifier` field, but we would like to have some form of deduplication for them, too. This fallback deduplication is turned on by having set `API3_DEDUP_FALLBACK_ENABLED` to `true`. When searching the collection in database, the document is found to be a duplicate only when either he has equal `identifier` or he has no `identifier` and meets:
`devicestatus` collection: equal combination of `created_at` and `device`\n`entries` collection:      equal combination of `date` and `type`\n`food` collection:         equal `created_at`\n`profile` collection:      equal `created_at`\n`treatments` collection:   equal combination of `created_at` and `eventType` 
\n\n- Fallback switch for adding `created_at` field along the `date` field (optional, default = true)
API3_CREATED_AT_FALLBACK_ENABLED=true
Standard APIv3 document model uses only `date` field for storing a timestamp of the event recorded by the document. But there is a fallback option to fill `created_at` field as well automatically on each insert/update, just to keep all older components working.", + "contact": { + "name": "NS development discussion channel", + "url": "https://gitter.im/nightscout/public" + }, + "license": { + "name": "AGPL 3", + "url": "https://www.gnu.org/licenses/agpl.txt" + }, + "version": "3.0.4" + }, + "servers": [ + { + "url": "/api/v3" + } + ], + "tags": [ + { + "name": "generic", + "description": "Generic operations with each database collection (devicestatus, entries, food, profile, settings, treatments)" + }, + { + "name": "other", + "description": "All other various operations" + } + ], + "paths": { + "/{collection}": { + "get": { + "tags": [ + "generic" + ], + "summary": "SEARCH: Search documents from the collection", + "description": "General search operation through documents of one collection, matching the specified filtering criteria. You can apply:\n\n1) filtering - combining any number of filtering parameters\n\n2) ordering - using `sort` or `sort$desc` parameter\n\n3) paging - using `limit` and `skip` parameters\n\nIf successful, HTTP 200 code is returned with JSON array of matching documents as a response content (it may be empty).\n\nThis operation requires `read` permission for the API and the collection (e.g. `*:*:read`, `api:*:read`, `*:treatments:read`, `api:treatments:read`).\n\nThe only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings.", + "operationId": "SEARCH", + "parameters": [ + { + "name": "collection", + "in": "path", + "description": "Collection to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramCollection" + } + }, + { + "name": "filter_parameters", + "in": "query", + "description": "Any number of filtering operators.\n\nEach filtering operator has name like `$`, e.g. `carbs$gt=2` which represents filtering rule \"The field carbs must be present and greater than 2\".\n\nYou can choose from operators:\n\n`eq`=equals, `insulin$eq=1.5`\n\n`ne`=not equals, `insulin$ne=1.5`\n\n`gt`=greater than, `carbs$gt=30`\n\n`gte`=greater than or equal, `carbs$gte=30`\n\n`lt`=less than, `carbs$lt=30`\n\n`lte`=less than or equal, `carbs$lte=30`\n\n`in`=in specified set, `type$in=sgv|mbg|cal`\n\n`nin`=not in specified set, `eventType$nin=Temp%20Basal|Temporary%20Target`\n\n`re`=regex pattern, `eventType$re=Temp.%2A`\n\nWhen filtering by field `date`, `created_at`, `srvModified` or `srvCreated`, you can choose from three input formats\n- Unix epoch in milliseconds (1525383610088)\n- Unix epoch in seconds (1525383610)\n- ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00')\n\nThe date is always queried in a normalized form - UTC with zero offset and with the correct format (1525383610088 for `date`, '2018-05-03T21:40:10.088Z' for `created_at`).", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "description": "Field name by which the sorting of documents is performed. This parameter cannot be combined with `sort$desc` parameter.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + { + "name": "sort$desc", + "in": "query", + "description": "Field name by which the descending (reverse) sorting of documents is performed. This parameter cannot be combined with `sort` parameter.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of documents to get in result array", + "required": false, + "style": "form", + "explode": true, + "schema": { + "minimum": 1, + "type": "integer", + "example": 100 + } + }, + { + "name": "skip", + "in": "query", + "description": "Number of documents to skip from collection query before loading them into result array (used for pagination)", + "required": false, + "style": "form", + "explode": true, + "schema": { + "minimum": 0, + "type": "integer", + "example": 0, + "default": 0 + } + }, + { + "name": "fields", + "in": "query", + "description": "A chosen set of fields to return in response. Either you can enumerate specific fields of interest or use the predefined set. Sample parameter values:\n\n_all: All fields will be returned (default value)\n\ndate,insulin: Only fields `date` and `insulin` will be returned", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "default": "_all" + }, + "examples": { + "all": { + "summary": "All fields will be returned (default behaviour)", + "value": "_all" + }, + "customSet": { + "summary": "Only fields date and insulin will be returned", + "value": "date,insulin" + } + } + } + ], + "responses": { + "200": { + "description": "Successful operation returning array of documents matching the filtering criteria", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + } + } + }, + "400": { + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "406": { + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + }, + "post": { + "tags": [ + "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. `identifier` is included in response body or it can be parsed from the `Location` response header.\n\nWhen 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.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `create` (and/or `update` for deduplication) permission for the API and the collection (e.g. `api:treatments:create` and `api:treatments:update`)", + "parameters": [ + { + "name": "collection", + "in": "path", + "description": "Collection to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramCollection" + } + } + ], + "requestBody": { + "description": "JSON with new document to insert", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentToPost" + } + } + }, + "required": true + }, + "responses": { + "200": { + "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": { + "$ref": "#/components/schemas/inline_response_200_1" + } + } + } + }, + "201": { + "description": "Successfully created a new document in collection", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModified" + }, + "Location": { + "$ref": "#/components/schemas/headerLocation" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_201" + } + } + } + }, + "400": { + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "422": { + "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": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + } + }, + "/{collection}/{identifier}": { + "get": { + "tags": [ + "generic" + ], + "summary": "READ: Retrieves a single document from the collection", + "description": "Basically this operation looks for a document matching the `identifier` field returning 200 or 404 HTTP status code.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.\n\nWhen `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. With `If-Modified-Since` header and less or equal timestamp `srvModified` a normal 200 HTTP status with full response is returned.\n\nThis operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`)", + "parameters": [ + { + "name": "collection", + "in": "path", + "description": "Collection to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramCollection" + } + }, + { + "name": "identifier", + "in": "path", + "description": "Identifier of the document to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramIdentifier" + } + }, + { + "name": "If-Modified-Since", + "in": "header", + "description": "Timestamp (defined with respect to server's clock) of the last document modification formatted as:\n\n<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n\nIf this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock.\n\nExample:\n\n
If-Modified-Since: Wed, 17 Oct 2018 05:13:00 GMT
", + "required": false, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "in": "query", + "description": "A chosen set of fields to return in response. Either you can enumerate specific fields of interest or use the predefined set. Sample parameter values:\n\n_all: All fields will be returned (default value)\n\ndate,insulin: Only fields `date` and `insulin` will be returned", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "default": "_all" + }, + "examples": { + "all": { + "summary": "All fields will be returned (default behaviour)", + "value": "_all" + }, + "customSet": { + "summary": "Only fields date and insulin will be returned", + "value": "date,insulin" + } + } + } + ], + "responses": { + "200": { + "description": "The document has been succesfully found and its JSON form returned in the response content.", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModified" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_2" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/Document" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Document" + } + } + } + }, + "304": { + "description": "The document has not been modified on the server since timestamp specified in If-Modified-Since header", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModified" + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "406": { + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } + }, + "410": { + "description": "The requested document has already been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_410" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + }, + "put": { + "tags": [ + "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 200 HTTP status code will be returned.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.\n\nWhen 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).\n\nYou can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `update` (and/or `create`) permission for the API and the collection (e.g. `api:treatments:update` and `api:treatments:create`)", + "parameters": [ + { + "name": "collection", + "in": "path", + "description": "Collection to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramCollection" + } + }, + { + "name": "identifier", + "in": "path", + "description": "Identifier of the document to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramIdentifier" + } + }, + { + "name": "If-Unmodified-Since", + "in": "header", + "description": "Timestamp (defined with respect to server's clock) of the last document modification formatted as:\n\n<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n\nIf this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock.\n\nExample:\n\n
If-Unmodified-Since: Wed, 17 Oct 2018 05:13:00 GMT
", + "required": false, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "JSON of new version of document (`identifier` in JSON is ignored if present)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentToPost" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The request was successfully processed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_3" + } + } + } + }, + "201": { + "description": "Successfully created a new document in collection", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModified" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_201" + } + } + } + }, + "400": { + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "410": { + "description": "The requested document has already been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_410" + } + } + } + }, + "412": { + "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_412" + } + } + } + }, + "422": { + "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": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + }, + "delete": { + "tags": [ + "generic" + ], + "summary": "DELETE: Deletes a document from the collection", + "description": "If the document has already been deleted, the operation will succeed anyway. Normally, documents are not really deleted from the collection but they are only marked as deleted. For special cases the deletion can be irreversible using `permanent` parameter.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `delete` permission for the API and the collection (e.g. `api:treatments:delete`)", + "parameters": [ + { + "name": "collection", + "in": "path", + "description": "Collection to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramCollection" + } + }, + { + "name": "identifier", + "in": "path", + "description": "Identifier of the document to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramIdentifier" + } + }, + { + "name": "permanent", + "in": "query", + "description": "If true, the deletion will be irreversible and it will not appear in `HISTORY` operation. Normally there is no reason for setting this flag.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "The request was successfully processed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_3" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "422": { + "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": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + }, + "patch": { + "tags": [ + "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 200 HTTP status code will be returned.\n\nIf the document has been found in the collection but it had already been deleted, 410 HTTP status code is to be returned.\n\nWhen no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code.\n\nYou can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems.\n\n`PATCH` operation can save some bandwidth for incremental document updates in comparison with `GET` - `UPDATE` operation sequence.\n\nWhile patching the document, the field `modifiedBy` is automatically set to the authorized subject's name.\n\nThis operation provides autopruning of the collection (if autopruning is enabled).\n\nThis operation requires `update` permission for the API and the collection (e.g. `api:treatments:update`)", + "parameters": [ + { + "name": "collection", + "in": "path", + "description": "Collection to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramCollection" + } + }, + { + "name": "identifier", + "in": "path", + "description": "Identifier of the document to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramIdentifier" + } + }, + { + "name": "If-Unmodified-Since", + "in": "header", + "description": "Timestamp (defined with respect to server's clock) of the last document modification formatted as:\n\n<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n\nIf this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock.\n\nExample:\n\n
If-Unmodified-Since: Wed, 17 Oct 2018 05:13:00 GMT
", + "required": false, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "JSON of new version of document (`identifier` in JSON is ignored if present)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentToPost" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The request was successfully processed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_3" + } + } + } + }, + "400": { + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "410": { + "description": "The requested document has already been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_410" + } + } + } + }, + "412": { + "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_412" + } + } + } + }, + "422": { + "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": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + } + }, + "/{collection}/history": { + "get": { + "tags": [ + "generic" + ], + "summary": "HISTORY: Retrieves incremental changes since timestamp", + "description": "HISTORY operation is intended for continuous data synchronization with other systems.\nEvery insertion, update and deletion will be included in the resulting JSON array of documents (since timestamp in `Last-Modified` request header value). All changes are listed chronologically in response with 200 HTTP status code. The maximum listed `srvModified` timestamp is also stored in `Last-Modified` and `ETag` response headers that you can use for future, directly following synchronization. You can also limit the array's length using `limit` parameter.\n\nDeleted documents will appear with `isValid` = `false` field.\n\nHISTORY 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).\n\nThis operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`)\n\nThe only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings.", + "operationId": "HISTORY", + "parameters": [ + { + "name": "collection", + "in": "path", + "description": "Collection to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramCollection" + } + }, + { + "name": "Last-Modified", + "in": "header", + "description": "Starting timestamp (defined with respect to server's clock) since which the changes in documents are to be listed, formatted as:\n\n<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n\nExample:\n\n
Last-Modified: Wed, 17 Oct 2018 05:13:00 GMT
", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of documents to get in result array", + "required": false, + "style": "form", + "explode": true, + "schema": { + "minimum": 1, + "type": "integer", + "example": 100 + } + }, + { + "name": "fields", + "in": "query", + "description": "A chosen set of fields to return in response. Either you can enumerate specific fields of interest or use the predefined set. Sample parameter values:\n\n_all: All fields will be returned (default value)\n\ndate,insulin: Only fields `date` and `insulin` will be returned", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "default": "_all" + }, + "examples": { + "all": { + "summary": "All fields will be returned (default behaviour)", + "value": "_all" + }, + "customSet": { + "summary": "Only fields date and insulin will be returned", + "value": "date,insulin" + } + } + } + ], + "responses": { + "200": { + "description": "Changed documents since specified timestamp", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModifiedMaximum" + }, + "ETag": { + "$ref": "#/components/schemas/headerEtagLastModifiedMaximum" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + } + } + }, + "400": { + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "406": { + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + } + }, + "/{collection}/history/{lastModified}": { + "get": { + "tags": [ + "generic" + ], + "summary": "HISTORY: Retrieves incremental changes since timestamp", + "description": "This HISTORY operation variant is more precise than the previous one with `Last-Modified` request HTTP header), because it does not loose milliseconds precision.\n\nSince this variant queries for changed documents by timestamp precisely and exclusively, the last modified document does not repeat itself in following calls. That is the reason why is this variant more suitable for continuous synchronization with other systems.\n\nThis variant behaves quite the same as the previous one in all other aspects.", + "operationId": "HISTORY2", + "parameters": [ + { + "name": "collection", + "in": "path", + "description": "Collection to which the operation is targeted", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/paramCollection" + } + }, + { + "name": "lastModified", + "in": "path", + "description": "Starting timestamp (in UNIX epoch format, defined with respect to server's clock) since which the changes in documents are to be listed. Query for modified documents is made using \"greater than\" operator (not including equal timestamps).", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of documents to get in result array", + "required": false, + "style": "form", + "explode": true, + "schema": { + "minimum": 1, + "type": "integer", + "example": 100 + } + }, + { + "name": "fields", + "in": "query", + "description": "A chosen set of fields to return in response. Either you can enumerate specific fields of interest or use the predefined set. Sample parameter values:\n\n_all: All fields will be returned (default value)\n\ndate,insulin: Only fields `date` and `insulin` will be returned", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "default": "_all" + }, + "examples": { + "all": { + "summary": "All fields will be returned (default behaviour)", + "value": "_all" + }, + "customSet": { + "summary": "Only fields date and insulin will be returned", + "value": "date,insulin" + } + } + } + ], + "responses": { + "200": { + "description": "Changed documents since specified timestamp", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModifiedMaximum" + }, + "ETag": { + "$ref": "#/components/schemas/headerEtagLastModifiedMaximum" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + } + } + }, + "400": { + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "406": { + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + } + }, + "/version": { + "get": { + "tags": [ + "other" + ], + "summary": "VERSION: Returns actual version information", + "description": "No authentication is needed for this commnad (it is public)", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Version" + } + } + } + } + } + } + }, + "/status": { + "get": { + "tags": [ + "other" + ], + "summary": "STATUS: Returns actual version information and all permissions granted for API", + "description": "This operation requires authorization in contrast with VERSION operation.", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Status" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + } + }, + "/lastModified": { + "get": { + "tags": [ + "other" + ], + "summary": "LAST MODIFIED: Retrieves timestamp of the last modification of every collection", + "description": "LAST MODIFIED operation inspects collections separately (in parallel) and for each of them it finds the date of any last modification (insertion, update, deletion).\nNot only `srvModified`, but also `date` and `created_at` fields are inspected (as a fallback to previous API).\n\nThis 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.", + "operationId": "LAST-MODIFIED", + "responses": { + "200": { + "description": "Successful operation returning the timestamps", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_4" + } + } + } + }, + "401": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_401" + } + } + } + }, + "403": { + "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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + } + }, + "security": [ + { + "jwtoken": [] + } + ] + } + } + }, + "components": { + "schemas": { + "headerLocation": { + "type": "string", + "description": "Location of document - the relative part of URL. This can be used to parse the identifier of just created document.\nExample=/api/v3/treatments/53409478-105f-11e9-ab14-d663bd873d93" + }, + "headerLastModified": { + "type": "string", + "description": "Timestamp of the last document modification on the server, formatted as\n', :: GMT'.\nThis field is relevant only for documents which were somehow modified by API v3 (inserted, updated or deleted) and it was generated using server's clock.\nExample='Wed, 17 Oct 2018 05:13:00 GMT'" + }, + "headerLastModifiedMaximum": { + "type": "string", + "description": "The latest (maximum) `srvModified` field of all returning documents, formatted as\n', :: GMT'.\nExample='Wed, 17 Oct 2018 05:13:00 GMT'" + }, + "headerEtagLastModifiedMaximum": { + "type": "string", + "description": "The latest (maximum) `srvModified` field of all returning documents. This header does not loose milliseconds from the date (unlike the `Last-Modified` header).\nExample='W/\"1525383610088\"'" + }, + "paramCollection": { + "type": "string", + "example": "treatments", + "enum": [ + "devicestatus", + "entries", + "food", + "profile", + "settings", + "treatments" + ] + }, + "paramIdentifier": { + "type": "string", + "example": "53409478-105f-11e9-ab14-d663bd873d93" + }, + "identifierField": { + "type": "string", + "description": "Identifier of created or modified document", + "example": "53409478-105f-11e9-ab14-d663bd873d93" + }, + "lastModifiedField": { + "type": "integer", + "description": "Timestamp of the last document modification on the server, formatted as\nUnix epoch in milliseconds (1525383610088)", + "format": "int64", + "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": { + "required": [ + "app", + "date" + ], + "properties": { + "identifier": { + "type": "string", + "description": "Main addressing, required field that identifies document in the collection.\n\nThe client should not create the identifier, the server automatically assigns it when the document is inserted.\n\nThe server calculates the identifier in such a way that duplicate records are automatically merged (deduplicating is made by `date`, `device` and `eventType` fields).\n\nThe best practise for all applications is not to loose identifiers from received documents, but save them carefully for other consumer applications/systems.\n\nAPI v3 has a fallback mechanism in place, for documents without `identifier` field the `identifier` is set to internal `_id`, when reading or addressing these documents.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "example": "53409478-105f-11e9-ab14-d663bd873d93" + }, + "date": { + "type": "integer", + "description": "Required timestamp when the record or event occured, you can choose from three input formats\n- Unix epoch in milliseconds (1525383610088)\n- Unix epoch in seconds (1525383610)\n- ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00')\n\nThe date is always stored in a normalized form - UTC with zero offset. If UTC offset was present, it is going to be set in the `utcOffset` field.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "format": "int64", + "example": 1525383610088 + }, + "utcOffset": { + "type": "integer", + "description": "Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the `date` field.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "example": 120 + }, + "app": { + "type": "string", + "description": "Application or system in which the record was entered by human or device for the first time.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "example": "xdrip" + }, + "device": { + "type": "string", + "description": "The device from which the data originated (including serial number of the device, if it is relevant and safe).\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "example": "dexcom G5" + }, + "_id": { + "type": "string", + "description": "Internally assigned database id. This field is for internal server purposes only, clients communicate with API by using identifier field.", + "example": "58e9dfbc166d88cc18683aac" + }, + "srvCreated": { + "type": "integer", + "description": "The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "format": "int64", + "example": 1525383610088 + }, + "subject": { + "type": "string", + "description": "Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed JWT.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "example": "uploader" + }, + "srvModified": { + "type": "integer", + "description": "The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted).\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "format": "int64", + "example": 1525383610088 + }, + "modifiedBy": { + "type": "string", + "description": "Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "example": "admin" + }, + "isValid": { + "type": "boolean", + "description": "A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value)\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "example": false + }, + "isReadOnly": { + "type": "boolean", + "description": "A flag set by client that locks the document from any changes. Every document marked with `isReadOnly=true` is forever immutable and cannot even be deleted.\n\nAny attempt to modify the read-only document will end with status 422 UNPROCESSABLE ENTITY.", + "example": true + } + }, + "description": "Shared base for all documents" + }, + "DeviceStatus": { + "description": "State of physical device, which is a technical part of the whole T1D compensation system", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentBase" + }, + { + "type": "object", + "properties": { + "some_property": { + "type": "string", + "description": "..." + } + } + } + ] + }, + "Entry": { + "description": "Blood glucose measurements and CGM calibrations", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentBase" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "sgv, mbg, cal, etc" + }, + "sgv": { + "type": "number", + "description": "The glucose reading. (only available for sgv types)" + }, + "direction": { + "type": "string", + "description": "Direction of glucose trend reported by CGM. (only available for sgv types)", + "example": "\"DoubleDown\", \"SingleDown\", \"FortyFiveDown\", \"Flat\", \"FortyFiveUp\", \"SingleUp\", \"DoubleUp\", \"NOT COMPUTABLE\", \"RATE OUT OF RANGE\" for xdrip" + }, + "noise": { + "type": "number", + "description": "Noise level at time of reading. (only available for sgv types)" + }, + "filtered": { + "type": "number", + "description": "The raw filtered value directly from CGM transmitter. (only available for sgv types)" + }, + "unfiltered": { + "type": "number", + "description": "The raw unfiltered value directly from CGM transmitter. (only available for sgv types)" + }, + "rssi": { + "type": "number", + "description": "The signal strength from CGM transmitter. (only available for sgv types)" + }, + "units": { + "type": "string", + "description": "The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field.", + "example": "\"mg\", \"mmol\"" + } + } + } + ] + }, + "Food": { + "description": "Nutritional values of food", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentBase" + }, + { + "type": "object", + "properties": { + "food": { + "type": "string", + "description": "food, quickpick" + }, + "category": { + "type": "string", + "description": "Name for a group of related records" + }, + "subcategory": { + "type": "string", + "description": "Name for a second level of groupping" + }, + "name": { + "type": "string", + "description": "Name of the food described" + }, + "portion": { + "type": "number", + "description": "Number of units (e.g. grams) of the whole portion described" + }, + "unit": { + "type": "string", + "description": "Unit for the portion", + "example": "\"g\", \"ml\", \"oz\"" + }, + "carbs": { + "type": "number", + "description": "Amount of carbs in the portion in grams" + }, + "fat": { + "type": "number", + "description": "Amount of fat in the portion in grams" + }, + "protein": { + "type": "number", + "description": "Amount of proteins in the portion in grams" + }, + "energy": { + "type": "number", + "description": "Amount of energy in the portion in kJ" + }, + "gi": { + "type": "number", + "description": "Glycemic index (1=low, 2=medium, 3=high)" + }, + "hideafteruse": { + "type": "boolean", + "description": "Flag used for quickpick" + }, + "hidden": { + "type": "boolean", + "description": "Flag used for quickpick" + }, + "position": { + "type": "number", + "description": "Ordering field for quickpick" + }, + "portions": { + "type": "number", + "description": "component multiplier if defined inside quickpick compound" + }, + "foods": { + "type": "array", + "description": "Neighbour documents (from food collection) that together make a quickpick compound", + "items": { + "$ref": "#/components/schemas/Food" + } + } + } + } + ] + }, + "Profile": { + "description": "Parameters describing body functioning relative to T1D + compensation parameters", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentBase" + }, + { + "type": "object", + "properties": { + "some_property": { + "type": "string", + "description": "..." + } + } + } + ] + }, + "Settings": { + "description": "A document representing persisted settings of some application or system (it could by Nightscout itself as well). This pack of options serves as a backup or as a shared centralized storage for multiple client instances. It is a probably good idea to `PATCH` the document instead of `UPDATE` operation, e.g. when changing one settings option in a client application.\n\n`identifier` represents a client application name here, e.g. `xdrip` or `aaps`.\n\n`Settings` collection has a more specific authorization required. For the `SEARCH` operation within this collection, you need an `admin` permission, such as `api:settings:admin`. The goal is to isolate individual client application settings.", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentBase" + }, + { + "type": "object", + "properties": { + "some_property": { + "type": "string", + "description": "..." + } + } + } + ] + }, + "Treatment": { + "description": "T1D compensation action", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentBase" + }, + { + "type": "object", + "properties": { + "eventType": { + "type": "string", + "description": "The type of treatment event.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + "example": "\"BG Check\", \"Snack Bolus\", \"Meal Bolus\", \"Correction Bolus\", \"Carb Correction\", \"Combo Bolus\", \"Announcement\", \"Note\", \"Question\", \"Exercise\", \"Site Change\", \"Sensor Start\", \"Sensor Change\", \"Pump Battery Change\", \"Insulin Change\", \"Temp Basal\", \"Profile Switch\", \"D.A.D. Alert\", \"Temporary Target\", \"OpenAPS Offline\", \"Bolus Wizard\"" + }, + "glucose": { + "type": "string", + "description": "Current glucose." + }, + "glucoseType": { + "type": "string", + "description": "Method used to obtain glucose, Finger or Sensor.", + "example": "\"Sensor\", \"Finger\", \"Manual\"" + }, + "units": { + "type": "string", + "description": "The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field when `glucose` is entered.", + "example": "\"mg/dl\", \"mmol/l\"" + }, + "carbs": { + "type": "number", + "description": "Amount of carbs given." + }, + "protein": { + "type": "number", + "description": "Amount of protein given." + }, + "fat": { + "type": "number", + "description": "Amount of fat given." + }, + "insulin": { + "type": "number", + "description": "Amount of insulin, if any." + }, + "duration": { + "type": "number", + "description": "Duration in minutes." + }, + "preBolus": { + "type": "number", + "description": "How many minutes the bolus was given before the meal started." + }, + "splitNow": { + "type": "number", + "description": "Immediate part of combo bolus (in percent)." + }, + "splitExt": { + "type": "number", + "description": "Extended part of combo bolus (in percent)." + }, + "percent": { + "type": "number", + "description": "Eventual basal change in percent." + }, + "absolute": { + "type": "number", + "description": "Eventual basal change in absolute value (insulin units per hour)." + }, + "targetTop": { + "type": "number", + "description": "Top limit of temporary target." + }, + "targetBottom": { + "type": "number", + "description": "Bottom limit of temporary target." + }, + "profile": { + "type": "string", + "description": "Name of the profile to which the pump has been switched." + }, + "reason": { + "type": "string", + "description": "For example the reason why the profile has been switched or why the temporary target has been set." + }, + "notes": { + "type": "string", + "description": "Description/notes of treatment." + }, + "enteredBy": { + "type": "string", + "description": "Who entered the treatment." + } + } + } + ] + }, + "DocumentToPost": { + "type": "object", + "description": "Single document", + "example": { + "identifier": "53409478-105f-11e9-ab14-d663bd873d93", + "date": 1532936118000, + "utcOffset": 120, + "carbs": 10, + "insulin": 1, + "eventType": "Snack Bolus", + "app": "xdrip", + "subject": "uploader" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/DeviceStatus" + }, + { + "$ref": "#/components/schemas/Entry" + }, + { + "$ref": "#/components/schemas/Food" + }, + { + "$ref": "#/components/schemas/Profile" + }, + { + "$ref": "#/components/schemas/Settings" + }, + { + "$ref": "#/components/schemas/Treatment" + } + ] + }, + "Document": { + "type": "object", + "description": "Single document", + "example": { + "identifier": "53409478-105f-11e9-ab14-d663bd873d93", + "date": 1532936118000, + "utcOffset": 120, + "carbs": 10, + "insulin": 1, + "eventType": "Snack Bolus", + "srvCreated": 1532936218000, + "srvModified": 1532936218000, + "app": "xdrip", + "subject": "uploader", + "modifiedBy": "admin" + }, + "xml": { + "name": "item" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/DeviceStatus" + }, + { + "$ref": "#/components/schemas/Entry" + }, + { + "$ref": "#/components/schemas/Food" + }, + { + "$ref": "#/components/schemas/Profile" + }, + { + "$ref": "#/components/schemas/Settings" + }, + { + "$ref": "#/components/schemas/Treatment" + } + ] + }, + "DeviceStatusArray": { + "type": "array", + "description": "Array of documents", + "items": { + "$ref": "#/components/schemas/DeviceStatus" + } + }, + "EntryArray": { + "type": "array", + "description": "Array of documents", + "items": { + "$ref": "#/components/schemas/Entry" + } + }, + "FoodArray": { + "type": "array", + "description": "Array of documents", + "items": { + "$ref": "#/components/schemas/Food" + } + }, + "ProfileArray": { + "type": "array", + "description": "Array of documents", + "items": { + "$ref": "#/components/schemas/Profile" + } + }, + "SettingsArray": { + "type": "array", + "description": "Array of settings", + "items": { + "$ref": "#/components/schemas/Settings" + } + }, + "TreatmentArray": { + "type": "array", + "description": "Array of documents", + "items": { + "$ref": "#/components/schemas/Treatment" + } + }, + "DocumentArray": { + "type": "object", + "xml": { + "name": "items" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/DeviceStatusArray" + }, + { + "$ref": "#/components/schemas/EntryArray" + }, + { + "$ref": "#/components/schemas/FoodArray" + }, + { + "$ref": "#/components/schemas/ProfileArray" + }, + { + "$ref": "#/components/schemas/SettingsArray" + }, + { + "$ref": "#/components/schemas/TreatmentArray" + } + ] + }, + "Version": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/statusField" + }, + "result": { + "$ref": "#/components/schemas/VersionResult" + } + }, + "description": "Information about versions" + }, + "VersionResult": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "The whole Nightscout instance version", + "example": "0.10.2-release-20171201" + }, + "apiVersion": { + "type": "string", + "description": "API v3 subsystem version", + "example": "3.0.0" + }, + "srvDate": { + "type": "number", + "description": "Actual server date and time in UNIX epoch format", + "example": 1532936118000 + }, + "storage": { + "$ref": "#/components/schemas/VersionResult_storage" + } + } + }, + "Status": { + "properties": { + "status": { + "$ref": "#/components/schemas/statusField" + }, + "result": { + "$ref": "#/components/schemas/StatusResult" + } + }, + "description": "Information about versions and API permissions" + }, + "StatusResult": { + "allOf": [ + { + "$ref": "#/components/schemas/VersionResult" + }, + { + "type": "object", + "properties": { + "apiPermissions": { + "$ref": "#/components/schemas/StatusResult_apiPermissions" + } + } + } + ] + }, + "LastModifiedResult": { + "properties": { + "srvDate": { + "type": "integer", + "description": "Actual storage server date (Unix epoch in ms).", + "format": "int64", + "example": 1556260878776 + }, + "collections": { + "$ref": "#/components/schemas/LastModifiedResult_collections" + } + }, + "description": "Result of LAST MODIFIED operation" + }, + "inline_response_200": { + "properties": { + "status": { + "type": "integer", + "example": 200 + }, + "result": { + "$ref": "#/components/schemas/DocumentArray" + } + } + }, + "inline_response_400": { + "properties": { + "status": { + "type": "integer", + "example": 400 + } + } + }, + "inline_response_401": { + "properties": { + "status": { + "type": "integer", + "example": 401 + } + } + }, + "inline_response_403": { + "properties": { + "status": { + "type": "integer", + "example": 403 + } + } + }, + "inline_response_404": { + "properties": { + "status": { + "type": "integer", + "example": 404 + } + } + }, + "inline_response_406": { + "properties": { + "status": { + "type": "integer", + "example": 406 + } + } + }, + "inline_response_200_1": { + "properties": { + "status": { + "type": "integer", + "example": 200 + }, + "identifier": { + "$ref": "#/components/schemas/identifierField" + }, + "isDeduplication": { + "$ref": "#/components/schemas/isDeduplicationField" + }, + "deduplicatedIdentifier": { + "$ref": "#/components/schemas/deduplicatedIdentifierField" + } + } + }, + "inline_response_201": { + "properties": { + "status": { + "type": "integer", + "example": 201 + }, + "identifier": { + "$ref": "#/components/schemas/identifierField" + }, + "lastModified": { + "$ref": "#/components/schemas/lastModifiedField" + } + } + }, + "inline_response_422": { + "properties": { + "status": { + "type": "integer", + "example": 422 + } + } + }, + "inline_response_200_2": { + "properties": { + "status": { + "type": "integer", + "example": 200 + }, + "result": { + "$ref": "#/components/schemas/Document" + } + } + }, + "inline_response_410": { + "properties": { + "status": { + "type": "integer", + "example": 410 + } + } + }, + "inline_response_200_3": { + "properties": { + "status": { + "type": "integer", + "example": 200 + } + } + }, + "inline_response_412": { + "properties": { + "status": { + "type": "integer", + "example": 412 + } + } + }, + "inline_response_200_4": { + "properties": { + "status": { + "type": "integer", + "example": 200 + }, + "result": { + "$ref": "#/components/schemas/LastModifiedResult" + } + } + }, + "VersionResult_storage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of storage engine used", + "example": "mongodb" + }, + "version": { + "type": "string", + "description": "Version of the storage engine", + "example": "4.0.6" + } + } + }, + "StatusResult_apiPermissions": { + "type": "object", + "properties": { + "devicestatus": { + "type": "string", + "example": "crud" + }, + "entries": { + "type": "string", + "example": "r" + }, + "food": { + "type": "string", + "example": "crud" + }, + "profile": { + "type": "string", + "example": "r" + }, + "treatments": { + "type": "string", + "example": "crud" + } + } + }, + "LastModifiedResult_collections": { + "type": "object", + "properties": { + "devicestatus": { + "type": "integer", + "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", + "format": "int64", + "example": 1556260760974 + }, + "treatments": { + "type": "integer", + "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", + "format": "int64", + "example": 1553374184169 + }, + "entries": { + "type": "integer", + "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", + "format": "int64", + "example": 1556260758768 + }, + "profile": { + "type": "integer", + "description": "Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found.", + "format": "int64", + "example": 1548524042744 + } + }, + "description": "Collections which the user have read access to." + } + }, + "responses": { + "200Ok": { + "description": "The request was successfully processed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_3" + } + } + } + }, + "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": { + "$ref": "#/components/schemas/inline_response_200_1" + } + } + } + }, + "201Created": { + "description": "Successfully created a new document in collection", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModified" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_201" + } + } + } + }, + "201CreatedLocation": { + "description": "Successfully created a new document in collection", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModified" + }, + "Location": { + "$ref": "#/components/schemas/headerLocation" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_201" + } + } + } + }, + "304NotModified": { + "description": "The document has not been modified on the server since timestamp specified in If-Modified-Since header", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModified" + } + } + }, + "400BadRequest": { + "description": "The request is malformed. There may be some required parameters missing or there are unrecognized parameters present.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_400" + } + } + } + }, + "401Unauthorized": { + "description": "The request was not successfully authenticated using JWT, so that the request cannot continue due to the security policy.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_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": { + "$ref": "#/components/schemas/inline_response_403" + } + } + } + }, + "404NotFound": { + "description": "The collection or document specified was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_404" + } + } + } + }, + "406NotAcceptable": { + "description": "The requested content type (in `Accept` header) is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_406" + } + } + } + }, + "412PreconditionFailed": { + "description": "The document has already been modified on the server since specified timestamp (in If-Unmodified-Since header).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_412" + } + } + } + }, + "410Gone": { + "description": "The requested document has already been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_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": { + "$ref": "#/components/schemas/inline_response_422" + } + } + } + }, + "search200": { + "description": "Successful operation returning array of documents matching the filtering criteria", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + } + } + }, + "read200": { + "description": "The document has been succesfully found and its JSON form returned in the response content.", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModified" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_2" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/Document" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Document" + } + } + } + }, + "history200": { + "description": "Changed documents since specified timestamp", + "headers": { + "Last-Modified": { + "$ref": "#/components/schemas/headerLastModifiedMaximum" + }, + "ETag": { + "$ref": "#/components/schemas/headerEtagLastModifiedMaximum" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/DocumentArray" + } + } + } + }, + "lastModified200": { + "description": "Successful operation returning the timestamps", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inline_response_200_4" + } + } + } + } + }, + "parameters": { + "limitParam": { + "name": "limit", + "in": "query", + "description": "Maximum number of documents to get in result array", + "required": false, + "style": "form", + "explode": true, + "schema": { + "minimum": 1, + "type": "integer", + "example": 100 + } + }, + "skipParam": { + "name": "skip", + "in": "query", + "description": "Number of documents to skip from collection query before loading them into result array (used for pagination)", + "required": false, + "style": "form", + "explode": true, + "schema": { + "minimum": 0, + "type": "integer", + "example": 0, + "default": 0 + } + }, + "sortParam": { + "name": "sort", + "in": "query", + "description": "Field name by which the sorting of documents is performed. This parameter cannot be combined with `sort$desc` parameter.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + "sortDescParam": { + "name": "sort$desc", + "in": "query", + "description": "Field name by which the descending (reverse) sorting of documents is performed. This parameter cannot be combined with `sort` parameter.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + "permanentParam": { + "name": "permanent", + "in": "query", + "description": "If true, the deletion will be irreversible and it will not appear in `HISTORY` operation. Normally there is no reason for setting this flag.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "boolean" + } + }, + "fieldsParam": { + "name": "fields", + "in": "query", + "description": "A chosen set of fields to return in response. Either you can enumerate specific fields of interest or use the predefined set. Sample parameter values:\n\n_all: All fields will be returned (default value)\n\ndate,insulin: Only fields `date` and `insulin` will be returned", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "default": "_all" + }, + "examples": { + "all": { + "summary": "All fields will be returned (default behaviour)", + "value": "_all" + }, + "customSet": { + "summary": "Only fields date and insulin will be returned", + "value": "date,insulin" + } + } + }, + "filterParams": { + "name": "filter_parameters", + "in": "query", + "description": "Any number of filtering operators.\n\nEach filtering operator has name like `$`, e.g. `carbs$gt=2` which represents filtering rule \"The field carbs must be present and greater than 2\".\n\nYou can choose from operators:\n\n`eq`=equals, `insulin$eq=1.5`\n\n`ne`=not equals, `insulin$ne=1.5`\n\n`gt`=greater than, `carbs$gt=30`\n\n`gte`=greater than or equal, `carbs$gte=30`\n\n`lt`=less than, `carbs$lt=30`\n\n`lte`=less than or equal, `carbs$lte=30`\n\n`in`=in specified set, `type$in=sgv|mbg|cal`\n\n`nin`=not in specified set, `eventType$nin=Temp%20Basal|Temporary%20Target`\n\n`re`=regex pattern, `eventType$re=Temp.%2A`\n\nWhen filtering by field `date`, `created_at`, `srvModified` or `srvCreated`, you can choose from three input formats\n- Unix epoch in milliseconds (1525383610088)\n- Unix epoch in seconds (1525383610)\n- ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00')\n\nThe date is always queried in a normalized form - UTC with zero offset and with the correct format (1525383610088 for `date`, '2018-05-03T21:40:10.088Z' for `created_at`).", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string" + } + }, + "lastModifiedRequiredHeader": { + "name": "Last-Modified", + "in": "header", + "description": "Starting timestamp (defined with respect to server's clock) since which the changes in documents are to be listed, formatted as:\n\n<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n\nExample:\n\n
Last-Modified: Wed, 17 Oct 2018 05:13:00 GMT
", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + } + }, + "ifModifiedSinceHeader": { + "name": "If-Modified-Since", + "in": "header", + "description": "Timestamp (defined with respect to server's clock) of the last document modification formatted as:\n\n<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n\nIf this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock.\n\nExample:\n\n
If-Modified-Since: Wed, 17 Oct 2018 05:13:00 GMT
", + "required": false, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + } + }, + "ifUnmodifiedSinceHeader": { + "name": "If-Unmodified-Since", + "in": "header", + "description": "Timestamp (defined with respect to server's clock) of the last document modification formatted as:\n\n<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT\n\nIf this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock.\n\nExample:\n\n
If-Unmodified-Since: Wed, 17 Oct 2018 05:13:00 GMT
", + "required": false, + "style": "simple", + "explode": false, + "schema": { + "type": "string" + } + } + }, + "securitySchemes": { + "jwtoken": { + "type": "http", + "description": "Use this if you know the temporary json webtoken.", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + } +} diff --git a/lib/api3/swagger.yaml b/lib/api3/swagger.yaml new file mode 100644 index 00000000000..332b00e86bf --- /dev/null +++ b/lib/api3/swagger.yaml @@ -0,0 +1,1720 @@ +openapi: 3.0.0 +servers: + - url: '/api/v3' +info: + version: 3.0.4 + title: Nightscout API + contact: + name: NS development discussion channel + url: https://gitter.im/nightscout/public + license: + name: AGPL 3 + url: 'https://www.gnu.org/licenses/agpl.txt' + description: + Nightscout API v3 is a component of cgm-remote-monitor project. It aims to provide lightweight, secured and HTTP REST compliant interface for your T1D treatment data exchange. + + + API v3 uses these environment variables, among other things: + + - Security switch (optional, default = `true`) +
API3_SECURITY_ENABLE=true
+ You can turn the whole security mechanism off, e.g. for debugging or development purposes, + but this should never be set to false in production. + + + - Maximum limit count of documents retrieved from single query +
API3_MAX_LIMIT=1000
+ + + - Autopruning of obsolete documents (optional, default is only `DEVICESTATUS`=60) +
API3_AUTOPRUNE_DEVICESTATUS=60
+
+      API3_AUTOPRUNE_ENTRIES=365
+
+      API3_AUTOPRUNE_TREATMENTS=120
+      
+ You can specify for which collections autopruning will be activated and length of retention period in days, e.g. "Hold 60 days of devicestatus, automatically delete older documents, hold 365 days of treatments and entries, automatically delete older documents." + + + - Fallback deduplication switch (optional, default = true) +
API3_DEDUP_FALLBACK_ENABLED=true
+ API3 uses the `identifier` field for document identification and mutual distinction within a single collection. There is automatic deduplication implemented matching the equal `identifier` field. E.g. `CREATE` operation for document having the same `identifier` as another one existing in the database is automatically transformed into `UPDATE` operation of the document found in the database. + + Documents not created via API v3 usually does not have any `identifier` field, but we would like to have some form of deduplication for them, too. This fallback deduplication is turned on by having set `API3_DEDUP_FALLBACK_ENABLED` to `true`. + When searching the collection in database, the document is found to be a duplicate only when either he has equal `identifier` or he has no `identifier` and meets: +
`devicestatus` collection: equal combination of `created_at` and `device`
+
+      `entries` collection:      equal combination of `date` and `type`
+
+      `food` collection:         equal `created_at`
+
+      `profile` collection:      equal `created_at`
+
+      `treatments` collection:   equal combination of `created_at` and `eventType`
+      
+ + + - Fallback switch for adding `created_at` field along the `date` field (optional, default = true) +
API3_CREATED_AT_FALLBACK_ENABLED=true
+ Standard APIv3 document model uses only `date` field for storing a timestamp of the event recorded by the document. But there is a fallback option to fill `created_at` field as well automatically on each insert/update, just to keep all older components working. + +tags: + - name: generic + description: Generic operations with each database collection (devicestatus, entries, food, profile, settings, treatments) + - name: other + description: All other various operations + + +paths: + /{collection}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + ###################################################################################### + get: + tags: + - generic + summary: 'SEARCH: Search documents from the collection' + operationId: SEARCH + description: General search operation through documents of one collection, matching the specified filtering criteria. You can apply: + + + 1) filtering - combining any number of filtering parameters + + + 2) ordering - using `sort` or `sort$desc` parameter + + + 3) paging - using `limit` and `skip` parameters + + + 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`). + + + The only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings. + + + parameters: + - $ref: '#/components/parameters/filterParams' + - $ref: '#/components/parameters/sortParam' + - $ref: '#/components/parameters/sortDescParam' + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/skipParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/search200' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' + + + ###################################################################################### + post: + tags: + - 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. + `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 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). + + + This operation requires `create` (and/or `update` for deduplication) permission for the API and the collection (e.g. `api:treatments:create` and `api:treatments:update`) + + requestBody: + description: JSON with new document to insert + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/200Deduplication' + 201: + $ref: '#/components/responses/201CreatedLocation' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 422: + $ref: '#/components/responses/422UnprocessableEntity' + + + #return HTTP STATUS 400 for all other verbs (PUT, PATCH, DELETE,...) + + + /{collection}/{identifier}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + - in: path + name: identifier + description: Identifier of the document to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramIdentifier' + + ###################################################################################### + get: + tags: + - generic + summary: 'READ: Retrieves a single document from the collection' + description: + 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 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. + With `If-Modified-Since` header and less or equal timestamp `srvModified` a normal 200 HTTP status with full response is returned. + + + This operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`) + + parameters: + - $ref: '#/components/parameters/ifModifiedSinceHeader' + - $ref: '#/components/parameters/fieldsParam' + + security: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/read200' + 304: + $ref: '#/components/responses/304NotModified' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' + 410: + $ref: '#/components/responses/410Gone' + + + ###################################################################################### + put: + tags: + - 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 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 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). + + + You can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `update` (and/or `create`) permission for the API and the collection (e.g. `api:treatments:update` and `api:treatments:create`) + + parameters: + - $ref: '#/components/parameters/ifUnmodifiedSinceHeader' + + requestBody: + description: JSON of new version of document (`identifier` in JSON is ignored if present) + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/200Ok' + 201: + $ref: '#/components/responses/201Created' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 412: + $ref: '#/components/responses/412PreconditionFailed' + 410: + $ref: '#/components/responses/410Gone' + 422: + $ref: '#/components/responses/422UnprocessableEntity' + + + ###################################################################################### + patch: + tags: + - 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 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 is to be returned. + + + When no document with `identifier` has been found in the collection, then the operation ends with 404 HTTP status code. + + + You can also specify `If-Unmodified-Since` request header including your timestamp of document's last modification. If the document has been modified by somebody else on the server afterwards (and you do not know about it), the 412 HTTP status code is returned cancelling the update operation. You can use this feature to prevent race condition problems. + + + `PATCH` operation can save some bandwidth for incremental document updates in comparison with `GET` - `UPDATE` operation sequence. + + + While patching the document, the field `modifiedBy` is automatically set to the authorized subject's name. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `update` permission for the API and the collection (e.g. `api:treatments:update`) + + parameters: + - $ref: '#/components/parameters/ifUnmodifiedSinceHeader' + + requestBody: + description: JSON of new version of document (`identifier` in JSON is ignored if present) + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentToPost' + + security: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/200Ok' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 410: + $ref: '#/components/responses/410Gone' + 412: + $ref: '#/components/responses/412PreconditionFailed' + 422: + $ref: '#/components/responses/422UnprocessableEntity' + + + ###################################################################################### + delete: + tags: + - generic + summary: 'DELETE: Deletes a document from the collection' + description: + If the document has already been deleted, the operation will succeed anyway. Normally, documents are not really deleted from the collection but they are only marked as deleted. For special cases the deletion can be irreversible using `permanent` parameter. + + + This operation provides autopruning of the collection (if autopruning is enabled). + + + This operation requires `delete` permission for the API and the collection (e.g. `api:treatments:delete`) + + + parameters: + - $ref: '#/components/parameters/permanentParam' + + security: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/200Ok' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 422: + $ref: '#/components/responses/422UnprocessableEntity' + + + ###################################################################################### + /{collection}/history: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + get: + tags: + - generic + summary: 'HISTORY: Retrieves incremental changes since timestamp' + operationId: HISTORY + description: + HISTORY operation is intended for continuous data synchronization with other systems. + + Every insertion, update and deletion will be included in the resulting JSON array of documents (since timestamp in `Last-Modified` request header value). All changes are listed chronologically in response with 200 HTTP status code. The maximum listed `srvModified` timestamp is also stored in `Last-Modified` and `ETag` response headers that you can use for future, directly following synchronization. You can also limit the array's length using `limit` parameter. + + + Deleted documents will appear with `isValid` = `false` field. + + + 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). + + + This operation requires `read` permission for the API and the collection (e.g. `api:treatments:read`) + + + The only exception is the `settings` collection which requires `admin` permission (`api:settings:admin`), because the settings of each application should be isolated and kept secret. You need to know the concrete identifier to access the app's settings. + + + parameters: + - $ref: '#/components/parameters/lastModifiedRequiredHeader' + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/history200' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' + + + ###################################################################################### + /{collection}/history/{lastModified}: + parameters: + - in: path + name: collection + description: Collection to which the operation is targeted + required: true + schema: + $ref: '#/components/schemas/paramCollection' + + - in: path + name: lastModified + description: Starting timestamp (in UNIX epoch format, defined with respect to server's clock) since which the changes in documents are to be listed. Query for modified documents is made using "greater than" operator (not including equal timestamps). + required: true + schema: + type: integer + format: int64 + + get: + tags: + - generic + summary: 'HISTORY: Retrieves incremental changes since timestamp' + operationId: HISTORY2 + description: + This HISTORY operation variant is more precise than the previous one with `Last-Modified` request HTTP header), because it does not loose milliseconds precision. + + + Since this variant queries for changed documents by timestamp precisely and exclusively, the last modified document does not repeat itself in following calls. That is the reason why is this variant more suitable for continuous synchronization with other systems. + + + This variant behaves quite the same as the previous one in all other aspects. + + + parameters: + - $ref: '#/components/parameters/limitParam' + - $ref: '#/components/parameters/fieldsParam' + + security: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/history200' + 400: + $ref: '#/components/responses/400BadRequest' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + 404: + $ref: '#/components/responses/404NotFound' + 406: + $ref: '#/components/responses/406NotAcceptable' + + + ###################################################################################### + /version: + + get: + tags: + - other + summary: 'VERSION: Returns actual version information' + description: No authentication is needed for this commnad (it is public) + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + + + ###################################################################################### + /status: + + get: + tags: + - other + summary: 'STATUS: Returns actual version information and all permissions granted for API' + description: + This operation requires authorization in contrast with VERSION operation. + + security: + - jwtoken: [] + + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + + ###################################################################################### + /lastModified: + get: + tags: + - other + summary: 'LAST MODIFIED: Retrieves timestamp of the last modification of every collection' + operationId: LAST-MODIFIED + description: + LAST MODIFIED operation inspects collections separately (in parallel) and for each of them it finds the date of any last modification (insertion, update, deletion). + + Not only `srvModified`, but also `date` and `created_at` fields are inspected (as a fallback to previous API). + + + 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: + - jwtoken: [] + + responses: + 200: + $ref: '#/components/responses/lastModified200' + 401: + $ref: '#/components/responses/401Unauthorized' + 403: + $ref: '#/components/responses/403Forbidden' + +###################################################################################### +components: + + parameters: + + + limitParam: + in: query + name: limit + schema: + type: integer + minimum: 1 + default: stored in API3_MAX_LIMIT environment variable (usually 1000) + example: 100 + description: Maximum number of documents to get in result array + + skipParam: + in: query + name: skip + schema: + type: integer + minimum: 0 + default: 0 + example: 0 + description: + Number of documents to skip from collection query before + loading them into result array (used for pagination) + + sortParam: + in: query + name: sort + schema: + type: string + required: false + description: + Field name by which the sorting of documents is performed. This parameter cannot be combined with `sort$desc` parameter. + + sortDescParam: + in: query + name: sort$desc + schema: + type: string + required: false + description: + Field name by which the descending (reverse) sorting of documents is performed. This parameter cannot be combined with `sort` parameter. + + permanentParam: + in: query + name: permanent + schema: + type: boolean + required: false + description: + If true, the deletion will be irreversible and it will not appear in `HISTORY` operation. Normally there is no reason for setting this flag. + + + fieldsParam: + in: query + name: fields + schema: + type: string + default: '_all' + required: false + examples: + all: + value: '_all' + summary: All fields will be returned (default behaviour) + customSet: + value: 'date,insulin' + summary: Only fields date and insulin will be returned + description: A chosen set of fields to return in response. Either you can enumerate specific fields of interest or use the predefined set. Sample parameter values: + + + _all: All fields will be returned (default value) + + + date,insulin: Only fields `date` and `insulin` will be returned + + + filterParams: + in: query + name: filter_parameters + schema: + type: string + description: + Any number of filtering operators. + + + Each filtering operator has name like `$`, e.g. `carbs$gt=2` which represents filtering rule "The field carbs must be present and greater than 2". + + + You can choose from operators: + + + `eq`=equals, `insulin$eq=1.5` + + + `ne`=not equals, `insulin$ne=1.5` + + + `gt`=greater than, `carbs$gt=30` + + + `gte`=greater than or equal, `carbs$gte=30` + + + `lt`=less than, `carbs$lt=30` + + + `lte`=less than or equal, `carbs$lte=30` + + + `in`=in specified set, `type$in=sgv|mbg|cal` + + + `nin`=not in specified set, `eventType$nin=Temp%20Basal|Temporary%20Target` + + + `re`=regex pattern, `eventType$re=Temp.%2A` + + + When filtering by field `date`, `created_at`, `srvModified` or `srvCreated`, you can choose from three input formats + + - Unix epoch in milliseconds (1525383610088) + + - Unix epoch in seconds (1525383610) + + - ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + + + The date is always queried in a normalized form - UTC with zero offset and with the correct format (1525383610088 for `date`, '2018-05-03T21:40:10.088Z' for `created_at`). + + lastModifiedRequiredHeader: + in: header + name: Last-Modified + schema: + type: string + required: true + description: + Starting timestamp (defined with respect to server's clock) since which the changes in documents are to be listed, formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + Example: + + +
Last-Modified: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ifModifiedSinceHeader: + in: header + name: If-Modified-Since + schema: + type: string + required: false + description: + Timestamp (defined with respect to server's clock) of the last document modification formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + If this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock. + + + Example: + + +
If-Modified-Since: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ifUnmodifiedSinceHeader: + in: header + name: If-Unmodified-Since + schema: + type: string + required: false + description: + Timestamp (defined with respect to server's clock) of the last document modification formatted as: + + + <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT + + + If this header is present, the operation will compare its value with the srvModified timestamp of the document at first and the operation result then may differ. The srvModified timestamp was defined by server's clock. + + + Example: + + +
If-Unmodified-Since: Wed, 17 Oct 2018 05:13:00 GMT
+ + + ###################################################################################### + 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 + 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 + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + 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 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: + properties: + status: + type: integer + example: 200 + result: + $ref: '#/components/schemas/DocumentArray' + text/csv: + schema: + $ref: '#/components/schemas/DocumentArray' + application/xml: + schema: + $ref: '#/components/schemas/DocumentArray' + + read200: + description: The document has been succesfully found and its JSON form returned in the response content. + content: + application/json: + schema: + properties: + status: + type: integer + example: 200 + result: + $ref: '#/components/schemas/Document' + text/csv: + schema: + $ref: '#/components/schemas/Document' + application/xml: + schema: + $ref: '#/components/schemas/Document' + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModified' + + history200: + description: + Changed documents since specified timestamp + content: + application/json: + schema: + properties: + status: + type: integer + example: 200 + result: + $ref: '#/components/schemas/DocumentArray' + text/csv: + schema: + $ref: '#/components/schemas/DocumentArray' + application/xml: + schema: + $ref: '#/components/schemas/DocumentArray' + headers: + 'Last-Modified': + $ref: '#/components/schemas/headerLastModifiedMaximum' + 'ETag': + $ref: '#/components/schemas/headerEtagLastModifiedMaximum' + + lastModified200: + description: Successful operation returning the timestamps + content: + application/json: + schema: + properties: + status: + type: integer + example: 200 + result: + $ref: '#/components/schemas/LastModifiedResult' + + + ###################################################################################### + schemas: + + headerLocation: + type: string + description: + Location of document - the relative part of URL. This can be used to parse the identifier + of just created document. + + Example=/api/v3/treatments/53409478-105f-11e9-ab14-d663bd873d93 + + headerLastModified: + type: string + description: + Timestamp of the last document modification on the server, formatted as + + ', :: GMT'. + + This field is relevant only for documents which were somehow modified by API v3 + (inserted, updated or deleted) and it was generated using server's clock. + + Example='Wed, 17 Oct 2018 05:13:00 GMT' + + headerLastModifiedMaximum: + type: string + description: + The latest (maximum) `srvModified` field of all returning documents, formatted as + + ', :: GMT'. + + Example='Wed, 17 Oct 2018 05:13:00 GMT' + + headerEtagLastModifiedMaximum: + type: string + description: + The latest (maximum) `srvModified` field of all returning documents. + This header does not loose milliseconds from the date (unlike the `Last-Modified` header). + + Example='W/"1525383610088"' + + paramCollection: + type: string + enum: + - devicestatus + - entries + - food + - profile + - settings + - treatments + example: 'treatments' + + paramIdentifier: + type: string + 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: + identifier: + description: + Main addressing, required field that identifies document in the collection. + + + The client should not create the identifier, the server automatically assigns it when the document is inserted. + + + The server calculates the identifier in such a way that duplicate records are automatically merged (deduplicating is made by `date`, `device` and `eventType` fields). + + + The best practise for all applications is not to loose identifiers from received documents, but save them carefully for other consumer applications/systems. + + + API v3 has a fallback mechanism in place, for documents without `identifier` field the `identifier` is set to internal `_id`, when reading or addressing these documents. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + type: string + example: '53409478-105f-11e9-ab14-d663bd873d93' + + date: + type: integer + format: int64 + description: + Required timestamp when the record or event occured, you can choose from three input formats + + - Unix epoch in milliseconds (1525383610088) + + - Unix epoch in seconds (1525383610) + + - ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00') + + + The date is always stored in a normalized form - UTC with zero offset. If UTC offset was present, it is going to be set in the `utcOffset` field. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + utcOffset: + type: integer + description: + Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the `date` field. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 120 + + app: + type: string + description: + Application or system in which the record was entered by human or device for the first time. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: xdrip + + device: + type: string + description: + The device from which the data originated (including serial number of the device, if it is relevant and safe). + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 'dexcom G5' + + _id: + description: Internally assigned database id. This field is for internal server purposes only, clients communicate with API by using identifier field. + type: string + example: '58e9dfbc166d88cc18683aac' + + srvCreated: + type: integer + format: int64 + description: + The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + subject: + type: string + description: + Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed JWT. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 'uploader' + + srvModified: + type: integer + format: int64 + description: + The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: 1525383610088 + + modifiedBy: + type: string + description: + Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: admin + + isValid: + type: boolean + description: + A flag set by the server only for deleted documents. This field appears + only within history operation and for documents which were deleted by API v3 (and they always have a false value) + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + example: false + + + isReadOnly: + type: boolean + description: + A flag set by client that locks the document from any changes. Every document marked with `isReadOnly=true` is forever immutable and cannot even be deleted. + + + Any attempt to modify the read-only document will end with status 422 UNPROCESSABLE ENTITY. + + + example: true + + required: + - date + - app + + + DeviceStatus: + description: State of physical device, which is a technical part of the whole T1D compensation system + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Entry: + description: Blood glucose measurements and CGM calibrations + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + + type: + type: string + description: 'sgv, mbg, cal, etc' + + sgv: + type: number + description: The glucose reading. (only available for sgv types) + + direction: + type: string + description: Direction of glucose trend reported by CGM. (only available for sgv types) + example: '"DoubleDown", "SingleDown", "FortyFiveDown", "Flat", "FortyFiveUp", "SingleUp", "DoubleUp", "NOT COMPUTABLE", "RATE OUT OF RANGE" for xdrip' + + noise: + type: number + description: Noise level at time of reading. (only available for sgv types) + example: 'xdrip: 0, 1, 2=high, 3=high_for_predict, 4=very high, 5=extreme' + + filtered: + type: number + description: The raw filtered value directly from CGM transmitter. (only available for sgv types) + + unfiltered: + type: number + description: The raw unfiltered value directly from CGM transmitter. (only available for sgv types) + + rssi: + type: number + description: The signal strength from CGM transmitter. (only available for sgv types) + + units: + type: string + example: '"mg", "mmol"' + description: The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field. + + + Food: + description: Nutritional values of food + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + + food: + type: string + description: 'food, quickpick' + + category: + type: string + description: Name for a group of related records + + subcategory: + type: string + description: Name for a second level of groupping + + name: + type: string + description: Name of the food described + + portion: + type: number + description: Number of units (e.g. grams) of the whole portion described + + unit: + type: string + example: '"g", "ml", "oz"' + description: Unit for the portion + + carbs: + type: number + description: Amount of carbs in the portion in grams + + fat: + type: number + description: Amount of fat in the portion in grams + + protein: + type: number + description: Amount of proteins in the portion in grams + + energy: + type: number + description: Amount of energy in the portion in kJ + + gi: + type: number + description: 'Glycemic index (1=low, 2=medium, 3=high)' + + hideafteruse: + type: boolean + description: Flag used for quickpick + + hidden: + type: boolean + description: Flag used for quickpick + + position: + type: number + description: Ordering field for quickpick + + portions: + type: number + description: component multiplier if defined inside quickpick compound + + foods: + type: array + description: Neighbour documents (from food collection) that together make a quickpick compound + items: + $ref: '#/components/schemas/Food' + + + Profile: + description: Parameters describing body functioning relative to T1D + compensation parameters + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Settings: + description: + A document representing persisted settings of some application or system (it could by Nightscout itself as well). This pack of options serves as a backup or as a shared centralized storage for multiple client instances. It is a probably good idea to `PATCH` the document instead of `UPDATE` operation, e.g. when changing one settings option in a client application. + + + `identifier` represents a client application name here, e.g. `xdrip` or `aaps`. + + + `Settings` collection has a more specific authorization required. For the `SEARCH` operation within this collection, you need an `admin` permission, such as `api:settings:admin`. The goal is to isolate individual client application settings. + + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + some_property: + type: string + description: ... + + + Treatment: + description: T1D compensation action + allOf: + - $ref: '#/components/schemas/DocumentBase' + - type: object + properties: + eventType: + type: string + example: '"BG Check", "Snack Bolus", "Meal Bolus", "Correction Bolus", "Carb Correction", "Combo Bolus", "Announcement", "Note", "Question", "Exercise", "Site Change", "Sensor Start", "Sensor Change", "Pump Battery Change", "Insulin Change", "Temp Basal", "Profile Switch", "D.A.D. Alert", "Temporary Target", "OpenAPS Offline", "Bolus Wizard"' + description: The type of treatment event. + + + Note: this field is immutable by the client (it cannot be updated or patched) + + + # created_at: + # type: string + # description: The date of the event, might be set retroactively. + glucose: + type: string + description: Current glucose. + glucoseType: + type: string + example: '"Sensor", "Finger", "Manual"' + description: Method used to obtain glucose, Finger or Sensor. + units: + type: string + example: '"mg/dl", "mmol/l"' + description: The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field when `glucose` is entered. + carbs: + type: number + description: Amount of carbs given. + protein: + type: number + description: Amount of protein given. + fat: + type: number + description: Amount of fat given. + insulin: + type: number + description: Amount of insulin, if any. + duration: + type: number + description: Duration in minutes. + preBolus: + type: number + description: How many minutes the bolus was given before the meal started. + splitNow: + type: number + description: Immediate part of combo bolus (in percent). + splitExt: + type: number + description: Extended part of combo bolus (in percent). + percent: + type: number + description: Eventual basal change in percent. + absolute: + type: number + description: Eventual basal change in absolute value (insulin units per hour). + targetTop: + type: number + description: Top limit of temporary target. + targetBottom: + type: number + description: Bottom limit of temporary target. + profile: + type: string + description: Name of the profile to which the pump has been switched. + reason: + type: string + description: For example the reason why the profile has been switched or why the temporary target has been set. + notes: + type: string + description: Description/notes of treatment. + enteredBy: + type: string + description: Who entered the treatment. + + + DocumentToPost: + description: Single document + type: object + oneOf: + - $ref: '#/components/schemas/DeviceStatus' + - $ref: '#/components/schemas/Entry' + - $ref: '#/components/schemas/Food' + - $ref: '#/components/schemas/Profile' + - $ref: '#/components/schemas/Settings' + - $ref: '#/components/schemas/Treatment' + example: + 'identifier': '53409478-105f-11e9-ab14-d663bd873d93' + 'date': 1532936118000 + 'utcOffset': 120 + 'carbs': 10 + 'insulin': 1 + 'eventType': 'Snack Bolus' + 'app': 'xdrip' + 'subject': 'uploader' + + + Document: + description: Single document + xml: + name: 'item' + type: object + oneOf: + - $ref: '#/components/schemas/DeviceStatus' + - $ref: '#/components/schemas/Entry' + - $ref: '#/components/schemas/Food' + - $ref: '#/components/schemas/Profile' + - $ref: '#/components/schemas/Settings' + - $ref: '#/components/schemas/Treatment' + example: + 'identifier': '53409478-105f-11e9-ab14-d663bd873d93' + 'date': 1532936118000 + 'utcOffset': 120 + 'carbs': 10 + 'insulin': 1 + 'eventType': 'Snack Bolus' + 'srvCreated': 1532936218000 + 'srvModified': 1532936218000 + 'app': 'xdrip' + 'subject': 'uploader' + 'modifiedBy': 'admin' + + + DeviceStatusArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/DeviceStatus' + + + EntryArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Entry' + + + FoodArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Food' + + + ProfileArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Profile' + + + SettingsArray: + description: Array of settings + type: array + items: + $ref: '#/components/schemas/Settings' + + + TreatmentArray: + description: Array of documents + type: array + items: + $ref: '#/components/schemas/Treatment' + + + DocumentArray: + type: object + xml: + name: 'items' + oneOf: + - $ref: '#/components/schemas/DeviceStatusArray' + - $ref: '#/components/schemas/EntryArray' + - $ref: '#/components/schemas/FoodArray' + - $ref: '#/components/schemas/ProfileArray' + - $ref: '#/components/schemas/SettingsArray' + - $ref: '#/components/schemas/TreatmentArray' + + + Version: + description: Information about versions + type: object + properties: + + status: + $ref: '#/components/schemas/statusField' + + result: + $ref: '#/components/schemas/VersionResult' + + + VersionResult: + type: object + properties: + + version: + description: The whole Nightscout instance version + type: string + example: '0.10.2-release-20171201' + + apiVersion: + description: API v3 subsystem version + type: string + example: '3.0.0' + + srvDate: + description: Actual server date and time in UNIX epoch format + type: number + example: 1532936118000 + + storage: + type: object + properties: + + type: + description: Type of storage engine used + type: string + example: 'mongodb' + + version: + description: Version of the storage engine + type: string + example: '4.0.6' + + + Status: + description: Information about versions and API permissions + properties: + status: + $ref: '#/components/schemas/statusField' + + result: + $ref: '#/components/schemas/StatusResult' + + + StatusResult: + allOf: + - $ref: '#/components/schemas/VersionResult' + - type: object + properties: + + apiPermissions: + type: object + properties: + devicestatus: + type: string + example: 'crud' + entries: + type: string + example: 'r' + food: + type: string + example: 'crud' + profile: + type: string + example: 'r' + treatments: + type: string + example: 'crud' + + LastModifiedResult: + description: Result of LAST MODIFIED operation + properties: + srvDate: + description: + Actual storage server date (Unix epoch in ms). + type: integer + format: int64 + example: 1556260878776 + + collections: + type: object + description: + Collections which the user have read access to. + properties: + devicestatus: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1556260760974 + treatments: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1553374184169 + entries: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1556260758768 + profile: + description: + Timestamp of the last modification (Unix epoch in ms), `null` when there is no timestamp found. + type: integer + format: int64 + example: 1548524042744 + + ###################################################################################### + securitySchemes: + + jwtoken: + type: http + scheme: bearer + description: Use this if you know the temporary json webtoken. + bearerFormat: JWT diff --git a/lib/authorization/delaylist.js b/lib/authorization/delaylist.js new file mode 100644 index 00000000000..fe6c8096153 --- /dev/null +++ b/lib/authorization/delaylist.js @@ -0,0 +1,58 @@ +'use strict'; + +const _ = require('lodash'); + +function init (env) { + + const ipDelayList = {}; + + const DELAY_ON_FAIL = _.get(env, 'settings.authFailDelay') || 5000; + const FAIL_AGE = 60000; + + ipDelayList.addFailedRequest = function addFailedRequest (ip) { + const ipString = String(ip); + let entry = ipDelayList[ipString]; + const now = Date.now(); + if (!entry) { + ipDelayList[ipString] = now + DELAY_ON_FAIL; + return; + } + if (now >= entry) { entry = now; } + ipDelayList[ipString] = entry + DELAY_ON_FAIL; + }; + + ipDelayList.shouldDelayRequest = function shouldDelayRequest (ip) { + const ipString = String(ip); + const entry = ipDelayList[ipString]; + let now = Date.now(); + if (entry) { + if (now < entry) { + return entry - now; + } + } + return false; + }; + + ipDelayList.requestSucceeded = function requestSucceeded (ip) { + const ipString = String(ip); + if (ipDelayList[ipString]) { + delete ipDelayList[ipString]; + } + }; + + // Clear items older than a minute + + setTimeout(function clearList () { + for (var key in ipDelayList) { + if (ipDelayList.hasOwnProperty(key)) { + if (Date.now() > ipDelayList[key] + FAIL_AGE) { + delete ipDelayList[key]; + } + } + } + }, 30000); + + return ipDelayList; +} + +module.exports = init; diff --git a/lib/authorization/endpoints.js b/lib/authorization/endpoints.js new file mode 100644 index 00000000000..194a97b0113 --- /dev/null +++ b/lib/authorization/endpoints.js @@ -0,0 +1,119 @@ +'use strict'; + +var _ = require('lodash'); +var express = require('express'); + +var consts = require('./../constants'); + +function init (env, authorization) { + var endpoints = express( ); + + var wares = require('./../middleware/index')(env); + + endpoints.use(wares.sendJSONStatus); + // text body types get handled as raw buffer stream + endpoints.use(wares.bodyParser.raw()); + // json body types get handled as parsed json + endpoints.use(wares.bodyParser.json()); + // also support url-encoded content-type + endpoints.use(wares.bodyParser.urlencoded({ extended: true })); + + endpoints.get('/request/:accessToken', function requestAuthorize (req, res) { + var authorized = authorization.authorize(req.params.accessToken); + + if (authorized) { + res.json(authorized); + } else { + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); + } + }); + + endpoints.get('/permissions', authorization.isPermitted('admin:api:permissions:read'), function getSubjects (req, res) { + res.json(authorization.seenPermissions); + }); + + endpoints.get('/permissions/trie', authorization.isPermitted('admin:api:permissions:read'), function getSubjects (req, res) { + res.json(authorization.expandedPermissions()); + }); + + endpoints.get('/subjects', authorization.isPermitted('admin:api:subjects:read'), function getSubjects (req, res) { + res.json(_.map(authorization.storage.subjects, function eachSubject (subject) { + return _.pick(subject, ['_id', 'name', 'accessToken', 'roles']); + })); + }); + + endpoints.post('/subjects', authorization.isPermitted('admin:api:subjects:create'), function createSubject (req, res) { + authorization.storage.createSubject(req.body, function created (err, created) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json(created); + } + }); + }); + + endpoints.put('/subjects', authorization.isPermitted('admin:api:subjects:update'), function saveSubject (req, res) { + authorization.storage.saveSubject(req.body, function saved (err, saved) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json(saved); + } + }); + }); + + endpoints.delete('/subjects/:_id', authorization.isPermitted('admin:api:subjects:delete'), function deleteSubject (req, res) { + authorization.storage.removeSubject(req.params._id, function deleted (err) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json({ }); + } + }); + }); + + endpoints.get('/roles', authorization.isPermitted('admin:api:roles:list'), function getRoles (req, res) { + res.json(authorization.storage.roles); + }); + + endpoints.post('/roles', authorization.isPermitted('admin:api:roles:create'), function createSubject (req, res) { + authorization.storage.createRole(req.body, function created (err, created) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json(created); + } + }); + }); + + endpoints.put('/roles', authorization.isPermitted('admin:api:roles:update'), function saveRole (req, res) { + authorization.storage.saveRole(req.body, function saved (err, saved) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json(saved); + } + }); + }); + + endpoints.delete('/roles/:_id', authorization.isPermitted('admin:api:roles:delete'), function deleteRole (req, res) { + authorization.storage.removeRole(req.params._id, function deleted (err) { + if (err) { + res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Mongo Error', err); + } else { + res.json({ }); + } + }); + }); + + endpoints.get('/debug/check/:permission', function check (req, res, next) { + authorization.isPermitted(req.params.permission)(req, res, next); + }, function debug (req, res) { + res.json({check: true}); + }); + + + return endpoints; +} + +module.exports = init; diff --git a/lib/authorization/index.js b/lib/authorization/index.js new file mode 100644 index 00000000000..d14a7a6afac --- /dev/null +++ b/lib/authorization/index.js @@ -0,0 +1,320 @@ +'use strict'; + +const _ = require('lodash'); +const jwt = require('jsonwebtoken'); +const shiroTrie = require('shiro-trie'); + +const consts = require('./../constants'); +const sleep = require('util').promisify(setTimeout); +const forwarded = require('forwarded-for'); + +function getRemoteIP (req) { + const address = forwarded(req, req.headers); + return address.ip; +} + +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(/[, :]/); + + /** + * Loads JWT from request + * + * @param {*} req + */ + function extractJWTfromRequest (req) { + + if (req.auth_token) return req.auth_token; + + let token; + + if (req.header('Authorization')) { + const parts = req.header('Authorization').split(' '); + if (parts.length === 2 && parts[0] === 'Bearer') { + token = parts[1]; + } + } + + if (!token) { + let accessToken = req.query.token; + if (!accessToken && req.body) { + if (_.isArray(req.body) && req.body.length > 0 && req.body[0].token) { + accessToken = req.body[0].token; + delete req.body[0].token; + } else if (req.body.token) { + accessToken = req.body.token; + delete req.body.token; + } + } + + if (accessToken) { + // validate and parse the token + const authed = authorization.authorize(accessToken); + if (authed && authed.token) { + token = authed.token; + } + } + } + + if (token) { req.auth_token = token; } + + return token; + } + + authorization.extractToken = extractJWTfromRequest; + + /** + * Fetches the API_SECRET from the request + * + * @param {*} req Express request object + */ + function apiSecretFromRequest (req) { + + if (req.api_secret) return req.api_secret; + + let secret = req.query && req.query.secret ? req.query.secret : req.header('api-secret'); + + if (!secret && req.body) { + // try to get the secret from the body, but don't leave it there + if (_.isArray(req.body) && req.body.length > 0 && req.body[0].secret) { + secret = req.body[0].secret; + delete req.body[0].secret; + } else if (req.body.secret) { + secret = req.body.secret; + delete req.body.secret; + } + } + + // store the secret hash on the request since the req may get processed again + if (secret) { req.api_secret = secret; } + return secret; + } + + function authorizeAdminSecret (secret) { + return env.enclave.isApiKey(secret); + } + + authorization.seenPermissions = []; + + authorization.expandedPermissions = function expandedPermissions () { + var permissions = shiroTrie.new(); + permissions.add(authorization.seenPermissions); + return permissions; + }; + + authorization.resolveWithRequest = function resolveWithRequest (req, callback) { + const resolveData = { + api_secret: apiSecretFromRequest(req) + , token: extractJWTfromRequest(req) + , ip: getRemoteIP(req) + }; + authorization.resolve(resolveData, callback); + }; + + /** + * Check if the Apache Shiro-style permission object includes the permission. + * + * Returns a boolean true / false depending on if the permission is found. + * + * @param {*} permission Desired permission + * @param {*} shiros Shiros + */ + + authorization.checkMultiple = function checkMultiple (permission, shiros) { + var found = _.find(shiros, function checkEach (shiro) { + return shiro && shiro.check(permission); + }); + return _.isObject(found); + }; + + /** + * Resolve an API secret or token and return the permissions associated with + * the secret / token + * + * @param {*} data + * @param {*} callback + */ + authorization.resolve = async function resolve (data, callback) { + + if (!data.ip) { + console.error('Trying to authorize without IP information'); + return callback(null, { shiros: [] }); + } + + data.api_secret = data.api_secret || null; + + if (data.api_secret == 'null') { // TODO find what's sending this anomaly + data.api_secret = null; + } + + const requestDelay = shouldDelayRequest(data.ip); + + if (requestDelay) { + await sleep(requestDelay); + } + + const authAttempted = (data.api_secret || data.token) ? true : false; + const defaultShiros = storage.rolesToShiros(defaultRoles); + + // If there is no token or secret, return default permissions + if (!authAttempted) { + const result = { shiros: defaultShiros, defaults: true }; + if (callback) { callback(null, result); } + return result; + } + + // Check for API_SECRET first as that allows bailing out fast + + if (data.api_secret && authorizeAdminSecret(data.api_secret)) { + requestSucceeded(data.ip); + var admin = shiroTrie.new(); + admin.add(['*']); + const result = { shiros: [admin] }; + if (callback) { callback(null, result); } + return result; + } + + // If we reach this point, we must be dealing with a role based token + + let token = null; + + // Tokens have to be well formed JWTs + try { + const verified = env.enclave.verifyJWT(data.token); + token = verified.accessToken; + } catch (err) {} + + // Check if there's a token in the secret + + if (!token && data.api_secret) { + if (storage.doesAccessTokenExist(data.api_secret)) { + token = data.api_secret; + } + } + + if (token) { + requestSucceeded(data.ip); + const results = authorization.resolveAccessToken(token, null, defaultShiros); + if (callback) { callback(null, results); } + return results; + } + + console.error('Resolving secret/token to permissions failed'); + addFailedRequest(data.ip); + + 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 {}; + + }; + + authorization.resolveAccessToken = function resolveAccessToken (accessToken, callback, defaultShiros) { + + if (!defaultShiros) { + defaultShiros = storage.rolesToShiros(defaultRoles); + } + + let resolved = storage.resolveSubjectAndPermissions(accessToken); + if (!resolved || !resolved.subject) { + if (callback) { callback('Subject not found', null); } + return null; + } + + let shiros = resolved.shiros.concat(defaultShiros); + const result = { shiros, subject: resolved.subject }; + if (callback) { callback(null, result); } + return result; + }; + + /** + * Check if the client has a permission execute an action, + * based on an API KEY or JWT in the request. + * + * Used to authorize API calls + * + * @param {*} permission Permission being checked + */ + authorization.isPermitted = function isPermitted (permission) { + + authorization.seenPermissions = _.chain(authorization.seenPermissions) + .push(permission) + .sort() + .uniq() + .value(); + + async function check (req, res, next) { + + var remoteIP = getRemoteIP(req); + var secret = apiSecretFromRequest(req); + var token = extractJWTfromRequest(req); + + const data = { api_secret: secret, token, ip: remoteIP }; + + const permissions = await authorization.resolve(data); + const permitted = authorization.checkMultiple(permission, permissions.shiros); + + if (permitted) { + next(); + return; + } + + res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'Invalid/Missing'); + } + + return check; + + }; + + /** + * Generates a JWT based on an access token / authorizes an existing token + * + * @param {*} accessToken token to be used for generating a JWT for the client + */ + authorization.authorize = function authorize (accessToken) { + + + let userToken = accessToken; + const decodedToken = env.enclave.verifyJWT(accessToken); + + if (decodedToken && decodedToken.accessToken) { + userToken = decodedToken.accessToken; + } + + var subject = storage.findSubject(userToken); + var authorized = null; + + if (subject) { + const token = env.enclave.signJWT({ accessToken: subject.accessToken }); + const decoded = env.enclave.verifyJWT(token); + + var roles = subject.roles ? _.uniq(subject.roles.concat(defaultRoles)) : defaultRoles; + + authorized = { + token + , sub: subject.name + , permissionGroups: _.map(roles, storage.roleToPermissions) + , iat: decoded.iat + , exp: decoded.exp + }; + } + + return authorized; + }; + + authorization.endpoints = require('./endpoints')(env, authorization); + + return authorization; +} + +module.exports = init; diff --git a/lib/authorization/storage.js b/lib/authorization/storage.js new file mode 100644 index 00000000000..614f8c78051 --- /dev/null +++ b/lib/authorization/storage.js @@ -0,0 +1,263 @@ +'use strict'; + +var _ = require('lodash'); +var crypto = require('crypto'); +var shiroTrie = require('shiro-trie'); +var ObjectID = require('mongodb').ObjectID; + +var find_options = require('../server/query'); + +function init (env, ctx) { + var storage = { }; + + var rolesCollection = ctx.store.collection(env.authentication_collections_prefix + 'roles'); + var subjectsCollection = ctx.store.collection(env.authentication_collections_prefix + 'subjects'); + + storage.queryOpts = { + dateField: 'created_at' + , noDateFilter: true + }; + + function query_for (opts) { + return find_options(opts, storage.queryOpts); + } + + function create (collection) { + function doCreate(obj, fn) { + if (!Object.prototype.hasOwnProperty.call(obj, 'created_at')) { + obj.created_at = (new Date()).toISOString(); + } + collection.insert(obj, function (err, doc) { + if (err != null && err.message) { + console.log('Data insertion error', err.message); + fn(err.message, null); + return; + } + storage.reload(function loaded() { + fn(null, doc.ops); + }); + }); + } + return doCreate; + } + + function list (collection) { + function doList(opts, fn) { + // these functions, find, sort, and limit, are used to + // dynamically configure the request, based on the options we've + // been given + + // determine sort options + function sort() { + return opts && opts.sort || {date: -1}; + } + + // configure the limit portion of the current query + function limit() { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + // handle all the results + function toArray(err, entries) { + fn(err, entries); + } + + // now just stitch them all together + limit.call(collection + .find(query_for(opts)) + .sort(sort()) + ).toArray(toArray); + } + + return doList; + } + + function remove (collection) { + function doRemove (_id, callback) { + collection.remove({ '_id': new ObjectID(_id) }, function (err) { + storage.reload(function loaded() { + callback(err, null); + }); + }); + } + return doRemove; + } + + function save (collection) { + function doSave (obj, callback) { + obj._id = new ObjectID(obj._id); + if (!obj.created_at) { + obj.created_at = (new Date()).toISOString(); + } + collection.save(obj, function (err) { + //id should be added for new docs + storage.reload(function loaded() { + callback(err, obj); + }); + }); + } + return doSave; + } + + storage.createSubject = create(subjectsCollection); + storage.saveSubject = save(subjectsCollection); + storage.removeSubject = remove(subjectsCollection); + storage.listSubjects = list(subjectsCollection); + + storage.createRole = create(rolesCollection); + storage.saveRole = save(rolesCollection); + storage.removeRole = remove(rolesCollection); + storage.listRoles = list(rolesCollection); + + storage.defaultRoles = [ + { name: 'admin', permissions: ['*'] } + , { name: 'denied', permissions: [ ] } + , { name: 'status-only', permissions: [ 'api:status:read' ] } + , { name: 'readable', permissions: [ '*:*:read' ] } + , { name: 'careportal', permissions: [ 'api:treatments:create' ] } + , { name: 'devicestatus-upload', permissions: [ 'api:devicestatus:create' ] } + , { name: 'activity', permissions: [ 'api:activity:create' ] } + ]; + + storage.ensureIndexes = function ensureIndexes() { + ctx.store.ensureIndexes(rolesCollection, ['name']); + ctx.store.ensureIndexes(subjectsCollection, ['name']); + } + + storage.getSHA1 = function getSHA1 (message) { + var shasum = crypto.createHash('sha1'); + shasum.update(message); + return shasum.digest('hex'); + } + + storage.reload = function reload (callback) { + + storage.listRoles({sort: {name: 1}}, function listResults (err, results) { + if (err) { + return callback && callback(err); + } + + storage.roles = results || [ ]; + + _.forEach(storage.defaultRoles, function eachRole (role) { + if (_.isEmpty(_.find(storage.roles, {name: role.name}))) { + storage.roles.push(role); + } + }); + + storage.roles = _.sortBy(storage.roles, 'name'); + + storage.listSubjects({sort: {name: 1}}, function listResults (err, results) { + if (err) { + return callback && callback(err); + } + + storage.subjects = _.map(results, function eachSubject (subject) { + if (env.enclave.isApiKeySet()) { + subject.digest = env.enclave.getSubjectHash(subject._id.toString()); + var abbrev = subject.name.toLowerCase().replace(/[\W]/g, '').substring(0, 10); + subject.accessToken = abbrev + '-' + subject.digest.substring(0, 16); + subject.accessTokenDigest = storage.getSHA1(subject.accessToken); + } + + return subject; + }); + + if (callback) { + callback( ); + } + }); + }); + + }; + + storage.findRole = function findRole (roleName) { + return _.find(storage.roles, {name: roleName}); + }; + + storage.roleToShiro = function roleToShiro (roleName) { + var shiro = null; + + var role = storage.findRole(roleName); + if (role) { + shiro = shiroTrie.new(); + shiro.add(role.permissions); + } + + return shiro; + }; + + storage.rolesToShiros = function roleToShiro (roleNames) { + return _.chain(roleNames) + .map(storage.roleToShiro) + .reject(_.isEmpty) + .value(); + }; + + storage.roleToPermissions = function roleToPermissions (roleName) { + var permissions = [ ]; + + var role = storage.findRole(roleName); + if (role) { + permissions = role.permissions; + } + + return permissions; + }; + + storage.findSubject = function findSubject (accessToken) { + + if (!accessToken) return null; + + function checkToken(accessToken) { + var split_token = accessToken.split('-'); + var prefix = split_token ? _.last(split_token) : ''; + + if (prefix.length < 16) { + return null; + } + + return _.find(storage.subjects, function matches (subject) { + return subject.accessTokenDigest.indexOf(accessToken) === 0 || subject.digest.indexOf(prefix) === 0; + }); + } + + if (!Array.isArray(accessToken)) accessToken = [accessToken]; + + for (let i=0; i < accessToken.length; i++) { + const subject = checkToken(accessToken[i]); + if (subject) return subject; + } + + return null; + }; + + storage.doesAccessTokenExist = function doesAccessTokenExist(accessToken) { + if (storage.findSubject(accessToken)) { + return true; + } + return false; + } + + storage.resolveSubjectAndPermissions = function resolveSubjectAndPermissions (accessToken) { + var shiros = []; + + var subject = storage.findSubject(accessToken); + if (subject) { + shiros = storage.rolesToShiros(subject.roles); + } + + return { + subject: subject + , shiros: shiros + }; + }; + + return storage; + +} + +module.exports = init; diff --git a/lib/bootevent.js b/lib/bootevent.js deleted file mode 100644 index 3d88910e567..00000000000 --- a/lib/bootevent.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -var _ = require('lodash'); - -var UPDATE_THROTTLE = 1000; - -function boot (env) { - - function setupMongo (ctx, next) { - require('./storage')(env, function ready ( err, store ) { - // FIXME, error is always null, if there is an error, the storage.js will throw an exception - console.log('Storage system ready'); - ctx.store = store; - - next( ); - }); - } - - function setupInternals (ctx, next) { - /////////////////////////////////////////////////// - // api and json object variables - /////////////////////////////////////////////////// - ctx.plugins = require('./plugins')().registerServerDefaults().init(env.settings); - - ctx.pushover = require('./plugins/pushover')(env); - ctx.maker = require('./plugins/maker')(env); - ctx.pushnotify = require('./pushnotify')(env, ctx); - - ctx.entries = require('./entries')(env, ctx); - ctx.treatments = require('./treatments')(env, ctx); - ctx.devicestatus = require('./devicestatus')(env.devicestatus_collection, ctx); - ctx.profile = require('./profile')(env.profile_collection, ctx); - ctx.food = require('./food')(env, ctx); - ctx.pebble = require('./pebble')(env, ctx); - ctx.bus = require('./bus')(env, ctx); - ctx.data = require('./data')(env, ctx); - ctx.notifications = require('./notifications')(env, ctx); - - next( ); - } - - function ensureIndexes (ctx, next) { - console.info('Ensuring indexes'); - ctx.store.ensureIndexes(ctx.entries( ), ctx.entries.indexedFields); - ctx.store.ensureIndexes(ctx.treatments( ), ctx.treatments.indexedFields); - ctx.store.ensureIndexes(ctx.devicestatus( ), ctx.devicestatus.indexedFields); - ctx.store.ensureIndexes(ctx.profile( ), ctx.profile.indexedFields); - ctx.store.ensureIndexes(ctx.food( ), ctx.food.indexedFields); - - next( ); - } - - function setupListeners (ctx, next) { - var updateData = _.debounce(function debouncedUpdateData ( ) { - ctx.data.update(function dataUpdated () { - ctx.bus.emit('data-loaded'); - }); - }, UPDATE_THROTTLE); - - ctx.bus.on('tick', function timedReloadData (tick) { - console.info('tick', tick.now); - updateData(); - }); - - ctx.bus.on('data-received', function forceReloadData ( ) { - console.info('got data-received event, requesting reload'); - updateData(); - }); - - ctx.bus.on('data-loaded', function updatePlugins ( ) { - var sbx = require('./sandbox')().serverInit(env, ctx); - ctx.plugins.setProperties(sbx); - ctx.notifications.initRequests(); - ctx.plugins.checkNotifications(sbx); - ctx.notifications.process(sbx); - ctx.bus.emit('data-processed'); - }); - - ctx.bus.on('notification', ctx.pushnotify.emitNotification); - - next( ); - } - - function setupBridge (ctx, next) { - ctx.bridge = require('./plugins/bridge')(env); - if (ctx.bridge) { - ctx.bridge.startEngine(ctx.entries); - } - next( ); - } - - function setupMMConnect (ctx, next) { - ctx.mmconnect = require('./plugins/mmconnect').init(env, ctx.entries); - if (ctx.mmconnect) { - ctx.mmconnect.run(); - } - next( ); - } - - function finishBoot (ctx, next) { - ctx.bus.uptime( ); - - next( ); - } - - return require('bootevent')( ) - .acquire(setupMongo) - .acquire(setupInternals) - .acquire(ensureIndexes) - .acquire(setupListeners) - .acquire(setupBridge) - .acquire(setupMMConnect) - .acquire(finishBoot); -} - -module.exports = boot; diff --git a/lib/bus.js b/lib/bus.js index 11dd7337939..65faeea21a6 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -1,11 +1,11 @@ 'use strict'; - var Stream = require('stream'); -function init (env) { +function init (settings) { var beats = 0; var started = new Date( ); - var interval = env.settings.heartbeat * 1000; + var interval = settings.heartbeat * 1000; + let busInterval; var stream = new Stream; @@ -24,9 +24,15 @@ function init (env) { stream.emit('tick', ictus( )); } + stream.teardown = function ( ) { + console.log('Initiating server teardown'); + clearInterval(busInterval); + stream.emit('teardown'); + }; + stream.readable = true; stream.uptime = repeat; - setInterval(repeat, interval); + busInterval = setInterval(repeat, interval); return stream; } module.exports = init; diff --git a/lib/client/adminnotifiesclient.js b/lib/client/adminnotifiesclient.js new file mode 100644 index 00000000000..e301d6c7c5b --- /dev/null +++ b/lib/client/adminnotifiesclient.js @@ -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 = '

' + title + '

' + message + '

'; + + 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 += '

' + additional + '

' + return html; + } + + notifies.prepare = function prepare() { + + var translate = client.translate; + + var html = '
'; + var messages = client.notifies.notifies; + var messageCount = client.notifies.notifyCount; + + if (messages && messages.length > 0) { + html += '

' + translate('You have administration messages') + '

'; + 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 += '
'; + 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; diff --git a/lib/client/boluscalc.js b/lib/client/boluscalc.js index 9bb21f5a31d..67637ad4d30 100644 --- a/lib/client/boluscalc.js +++ b/lib/client/boluscalc.js @@ -1,17 +1,17 @@ 'use strict'; var _ = require('lodash'); -var moment = require('moment-timezone'); var times = require('../times'); - -function init(client, $, plugins) { - var boluscalc = { }; - +var Storages = require('js-storage'); + +function init (client, $) { + var boluscalc = {}; + var translate = client.translate; - var storage = $.localStorage; - var iob = plugins('iob'); - var cob = plugins('cob'); - + var storage = Storages.localStorage; + var iob = client.plugins('iob'); + var cob = client.plugins('cob'); + var eventTime = $('#bc_eventTimeValue'); var eventDate = $('#bc_eventDateValue'); @@ -26,29 +26,35 @@ function init(client, $, plugins) { } return 0; } - + function maybePrevent (event) { if (event) { event.preventDefault(); } } - function isTouch() { - try { document.createEvent('TouchEvent'); return true; } - catch (e) { return false; } + function isProfileEnabled (profiles) { + return client.settings.enable.indexOf('profile') > -1 && + client.settings.extendedSettings.profile && + client.settings.extendedSettings.profile.multiple && + profiles.length > 1; + } + + function isTouch () { + try { document.createEvent('TouchEvent'); return true; } catch (e) { return false; } } function setDateAndTime (time) { - time = time || moment(); - eventTime.val(time.format('HH:mm')); - eventDate.val(time.format('YYYY-MM-DD')); + time = time || new Date(); + eventTime.val(time.getHours() + ":" + time.getMinutes()); + eventDate.val(time.toISOString().split('T')[0]); } - function mergeDateAndTime ( ) { + function mergeDateAndTime () { return client.utils.mergeInputTime(eventTime.val(), eventDate.val()); } - function updateTime(ele, time) { + function updateTime (ele, time) { ele.attr('oldminutes', time.minutes()); ele.attr('oldhours', time.hours()); } @@ -68,35 +74,37 @@ function init(client, $, plugins) { sensorbg = 0; } } - + // Set BG if ($('#bc_bgfromsensor').is(':checked')) { $('#bc_bg').val(sensorbg ? sensorbg : ''); } } - + boluscalc.updateVisualisations = function updateVisualisations (sbx) { - boluscalc.lastsbx = sbx; + // update BG in GUI + setBG(sbx.lastSGVEntry(), mergeDateAndTime().toDate()); + + if (client.browserUtils.getLastOpenedDrawer !== '#boluscalcDrawer') { + return; + } if ($('#bc_nowtime').is(':checked')) { - // update BG in GUI - setBG(_.last(sbx.data.sgvs), mergeDateAndTime().toDate()); - // Update time setDateAndTime(); - + boluscalc.calculateInsulin(); } }; - boluscalc.dateTimeFocus = function dateTimeFocus(event) { + boluscalc.dateTimeFocus = function dateTimeFocus (event) { $('#bc_othertime').prop('checked', true); updateTime($(this), mergeDateAndTime()); maybePrevent(event); }; - boluscalc.dateTimeChange = function dateTimeChange(event) { + boluscalc.dateTimeChange = function dateTimeChange (event) { $('#bc_othertime').prop('checked', true); -// client.utils.setYAxisOffset(50); //50% of extend + // client.utils.setYAxisOffset(50); //50% of extend var ele = $(this); var merged = mergeDateAndTime(); @@ -106,37 +114,38 @@ function init(client, $, plugins) { if (ele.attr('oldminutes') === '0' && merged.minutes() === 59) { merged.add(-1, 'hours'); } - + setDateAndTime(merged); updateTime(ele, merged); boluscalc.eventTimeTypeChange(); - + // update BG from sgv to this time setBG(findClosestSGVToPastTime(merged.toDate()), merged.toDate()); boluscalc.calculateInsulin(); maybePrevent(event); -// Nightscout.utils.updateBrushToTime(moment.toDate()); }; - boluscalc.eventTimeTypeChange = function eventTimeTypeChange(event) { + boluscalc.eventTimeTypeChange = function eventTimeTypeChange (event) { if ($('#bc_othertime').is(':checked')) { $('#bc_eventTimeValue').focus(); - $('#bc_retro').css('display',''); - if (mergeDateAndTime()moment()) { - $('#bc_retro').css('background-color','blue').text(translate('IN THE FUTURE')); + $('#bc_retro').css('display', ''); + if (mergeDateAndTime() < Date.now()) { + $('#bc_retro').css('background-color', 'red').text(translate('RETRO MODE')); + } else if (mergeDateAndTime() > Date.now()) { + $('#bc_retro').css('background-color', 'blue').text(translate('IN THE FUTURE')); } else { - $('#bc_retro').css('display','none'); + $('#bc_retro').css('display', 'none'); } } else { - $('#bc_retro').css('display','none'); + $('#bc_retro').css('display', 'none'); setDateAndTime(); - boluscalc.updateVisualisations(boluscalc.lastsbx); - boluscalc.calculateInsulin(); -// Nightscout.utils.setYAxisOffset(50); //50% of extend -// Nightscout.utils.updateBrushToTime(Nightscout.utils.mergeInputTime($('#bc_eventTimeValue').val(), $('#bc_eventDateValue').val()).toDate()); + boluscalc.updateVisualisations(client.sbx); + if (event) { + boluscalc.calculateInsulin(); + } + // Nightscout.utils.setYAxisOffset(50); //50% of extend + // Nightscout.utils.updateBrushToTime(Nightscout.utils.mergeInputTime($('#bc_eventTimeValue').val(), $('#bc_eventDateValue').val()).toDate()); } maybePrevent(event); }; @@ -147,47 +156,48 @@ function init(client, $, plugins) { maybePrevent(event); }; - boluscalc.prepare = function prepare( ) { + boluscalc.prepare = function prepare () { foods = []; $('#bc_profile').empty(); - client.profilefunctions.listBasalProfiles().forEach(function (p) { + var profiles = client.profilefunctions.listBasalProfiles(); + profiles.forEach(function(p) { $('#bc_profile').append(''); }); - $('#bc_usebg').prop('checked','checked'); - $('#bc_usecarbs').prop('checked','checked'); - $('#bc_usecob').prop('checked',''); - $('#bc_useiob').prop('checked','checked'); - $('#bc_bgfromsensor').prop('checked','checked'); + $('#bc_profileLabel').toggle(isProfileEnabled(profiles)); + + $('#bc_usebg').prop('checked', 'checked'); + $('#bc_usecarbs').prop('checked', 'checked'); + $('#bc_usecob').prop('checked', ''); + $('#bc_useiob').prop('checked', 'checked'); + $('#bc_bgfromsensor').prop('checked', 'checked'); $('#bc_carbs').val(''); $('#bc_quickpick').val(-1); $('#bc_preBolus').val(0); $('#bc_notes').val(''); - $('#bc_enteredBy').val($.localStorage.get('enteredBy') || ''); + $('#bc_enteredBy').val(Storages.localStorage.get('enteredBy') || ''); $('#bc_nowtime').prop('checked', true); $('#bc_othercorrection').val(0); $('#bc_profile').val(client.profilefunctions.activeProfileToTime()); setDateAndTime(); boluscalc.eventTimeTypeChange(); - if (boluscalc.lastsbx) { - boluscalc.updateVisualisations(boluscalc.lastsbx); - } + boluscalc.updateVisualisations(client.sbx); boluscalc.calculateInsulin(); }; - + boluscalc.calculateInsulin = function calculateInsulin (event) { maybePrevent(event); - boluscalc.gatherBoluscalcData( ); + boluscalc.gatherBoluscalcData(); boluscalc.updateGui(boluscalc.record); - return boluscalc.record; + return boluscalc.record; }; - + boluscalc.updateGui = function updateGui (record) { record = record || boluscalc.record; if (record.eventTime === undefined) { return; } - + var targetBGLow = record.targetBGLow; var targetBGHigh = record.targetBGHigh; var isf = record.isf; @@ -223,101 +233,115 @@ function init(client, $, plugins) { $('#bc_bg').css('background-color', ''); } $('#bc_inzulinbg').text(record.insulinbg.toFixed(2)); - $('#bc_inzulinbg').attr('title', - 'Target BG range: '+targetBGLow + ' - ' + targetBGHigh + - '\nISF: ' + isf + - '\nBG diff: ' + record.bgdiff.toFixed(1) - ); + $('#bc_inzulinbg').attr('title' + , 'Target BG range: ' + targetBGLow + ' - ' + targetBGHigh + + '\nISF: ' + isf + + '\nBG diff: ' + record.bgdiff.toFixed(1) + ); } else { $('#bc_inzulinbgtd').css('background-color', ''); $('#bc_bg').css('background-color', ''); $('#bc_inzulinbg').text(''); $('#bc_inzulinbg').attr('title', ''); } - + // Show foods if (record.foods.length) { var html = ''; var carbs = 0; - for (var fi=0; fi'; + html += ''; } html += ''; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; html += ''; } html += '
'+ f.name + ''+ (f.portion*f.portions).toFixed(1) + ' ' + translate(f.unit) + '('+ (f.carbs*f.portions).toFixed(1) + ' g)' + f.name + '' + (f.portion * f.portions).toFixed(1) + ' ' + translate(f.unit) + '(' + (f.carbs * f.portions).toFixed(1) + ' g)
'; $('#bc_food').html(html); $('.deleteFoodRecord').click(deleteFoodRecord); $('#bc_carbs').val(carbs.toFixed(0)); - $('#bc_carbs').attr('disabled',true); - $('#bc_gi').css('display','none'); - $('#bc_gicalculated').css('display',''); + $('#bc_carbs').attr('disabled', true); + $('#bc_gi').css('display', 'none'); + $('#bc_gicalculated').css('display', ''); $('#bc_gicalculated').text(record.gi); } else { $('#bc_food').html(''); - $('#bc_carbs').attr('disabled',false); - $('#bc_gi').css('display',''); - $('#bc_gicalculated').css('display','none'); + $('#bc_carbs').attr('disabled', false); + $('#bc_gi').css('display', ''); + $('#bc_gicalculated').css('display', 'none'); $('#bc_gicalculated').text(''); } // Show Carbs if ($('#bc_usecarbs').is(':checked')) { if ($('#bc_carbs').val() === '') { - $('#bc_carbs').css('background-color',''); - } else if (isNaN(parseInt($('#bc_carbs').val().replace(',','.')))) { - $('#bc_carbs').css('background-color','red'); + $('#bc_carbs').css('background-color', ''); + } else if (isNaN(parseInt($('#bc_carbs').val().replace(',', '.')))) { + $('#bc_carbs').css('background-color', 'red'); } else { - $('#bc_carbs').css('background-color',''); + $('#bc_carbs').css('background-color', ''); } $('#bc_inzulincarbs').text(record.insulincarbs.toFixed(2)); - $('#bc_inzulincarbs').attr('title','IC: ' + ic); + $('#bc_inzulincarbs').attr('title', 'IC: ' + ic); } else { - $('#bc_carbs').css('background-color',''); + $('#bc_carbs').css('background-color', ''); $('#bc_inzulincarbs').text(''); - $('#bc_inzulincarbs').attr('title',''); + $('#bc_inzulincarbs').attr('title', ''); $('#bc_carbs').text(''); } - + // Show Total $('#bc_rouding').text(record.roundingcorrection.toFixed(2)); $('#bc_insulintotal').text(record.insulin.toFixed(2)); - - // Carbs needed if too much iob - if (record.insulin<0) { - $('#bc_carbsneeded').text(record.carbsneeded+' g'); + + // Carbs needed if too much iob or in range message when nothing entered and in range + var outcome = record.bg - record.iob * isf; + if (record.othercorrection === 0 && record.carbs === 0 && record.cob === 0 && record.bg > 0 && outcome > targetBGLow && outcome < targetBGHigh) { + $('#bc_carbsneeded').text(''); + $('#bc_insulinover').text(''); + $('#bc_carbsneededtr').css('display', 'none'); + $('#bc_insulinneededtr').css('display', 'none'); + $('#bc_calculationintarget').css('display', ''); + } else if (record.insulin < 0) { + $('#bc_carbsneeded').text(record.carbsneeded + ' g'); $('#bc_insulinover').text(record.insulin.toFixed(2)); - $('#bc_carbsneededtr').css('display',''); - $('#bc_insulinneededtr').css('display','none'); + $('#bc_carbsneededtr').css('display', ''); + $('#bc_insulinneededtr').css('display', 'none'); + $('#bc_calculationintarget').css('display', 'none'); } else { $('#bc_carbsneeded').text(''); $('#bc_insulinover').text(''); - $('#bc_carbsneededtr').css('display','none'); - $('#bc_insulinneededtr').css('display',''); + $('#bc_carbsneededtr').css('display', 'none'); + $('#bc_insulinneededtr').css('display', ''); + $('#bc_calculationintarget').css('display', 'none'); } // Show basal rate var basal = client.sbx.data.profile.getTempBasal(record.eventTime); - $('#bc_basal').text((basal.treatment ? 'T ' : '') + basal.tempbasal.toFixed(3)); + var tempMark = ''; + tempMark += basal.treatment ? 'T' : ''; + tempMark += basal.combobolustreatment ? 'C' : ''; + tempMark += tempMark ? ': ' : ''; + $('#bc_basal').text(tempMark + basal.totalbasal.toFixed(3)); }; - - boluscalc.gatherBoluscalcData = function gatherBoluscalcData() { + + boluscalc.gatherBoluscalcData = function gatherBoluscalcData () { boluscalc.record = {}; var record = boluscalc.record; - + if (!client.sbx) { console.log('No sandbox data yet. Exiting gatherBoluscalcData()'); return; } - + record.profile = $('#bc_profile').val(); if (!record.profile) { delete record.profile; @@ -325,7 +349,6 @@ function init(client, $, plugins) { return; } - // Calculate event time from date & time record.eventTime = new Date(); if ($('#bc_othertime').is(':checked')) { @@ -347,41 +370,41 @@ function init(client, $, plugins) { record.ic = ic; if (targetBGLow === 0 || targetBGHigh === 0 || isf === 0 || ic === 0) { - $('#bc_inzulinbgtd').css('background-color','red'); + $('#bc_inzulinbgtd').css('background-color', 'red'); boluscalc.record = {}; return; } else { - $('#bc_inzulinbgtd').css('background-color',''); + $('#bc_inzulinbgtd').css('background-color', ''); } if (ic === 0) { - $('#bc_inzulincarbstd').css('background-color','red'); + $('#bc_inzulincarbstd').css('background-color', 'red'); boluscalc.record = {}; return; } else { - $('#bc_inzulincarbstd').css('background-color',''); + $('#bc_inzulincarbstd').css('background-color', ''); } // Load IOB record.iob = 0; if ($('#bc_useiob').is(':checked')) { - record.iob = roundTo(iob.calcTotal(client.sbx.data.treatments, client.sbx.data.profile, record.eventTime, record.profile).iob, 0.01); + record.iob = roundTo(iob.calcTotal(client.sbx.data.treatments, client.sbx.data.devicestatus, client.sbx.data.profile, record.eventTime, record.profile).iob, 0.01); } // Load COB record.cob = 0; record.insulincob = 0; if ($('#bc_usecob').is(':checked')) { - record.cob = roundTo(cob.cobTotal(client.sbx.data.treatments, client.sbx.data.profile, record.eventTime, record.profile).cob, 0.01); + record.cob = roundTo(cob.cobTotal(client.sbx.data.treatments, client.sbx.data.devicestatus, client.sbx.data.profile, record.eventTime, record.profile).cob, 0.01); record.insulincob = roundTo(record.cob / ic, 0.01); } - + // Load BG record.bg = 0; record.insulinbg = 0; record.bgdiff = 0; if ($('#bc_usebg').is(':checked')) { - record.bg = parseFloat($('#bc_bg').val().replace(',','.')); + record.bg = parseFloat($('#bc_bg').val().replace(',', '.')); if (isNaN(record.bg)) { record.bg = 0; } @@ -391,17 +414,18 @@ function init(client, $, plugins) { record.bgdiff = record.bg - targetBGHigh; } record.bgdiff = roundTo(record.bgdiff, 0.1); - if (record.bg !== 0){ + if (record.bg !== 0) { record.insulinbg = roundTo(record.bgdiff / isf, 0.01); } } - + // Load foods record.carbs = 0; record.foods = _.cloneDeep(foods); if (record.foods.length) { var gisum = 0; - for (var fi=0; fi= 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); + }); } } - + boluscalc.calculateInsulin(); maybePrevent(event); } - + var categories = []; var foodlist = []; var databaseloaded = false; var filter = { - category: '' + category: '' , subcategory: '' , name: '' }; - - boluscalc.loadFoodDatabase = function loadFoodDatabase(event, callback) { + + boluscalc.loadFoodDatabase = function loadFoodDatabase (event, callback) { categories = []; foodlist = []; - $.ajax('/api/v1/food/regular.json', { - success: function (records) { - records.forEach(function (r) { - foodlist.push(r); - if (r.category && !categories[r.category]) { - categories[r.category] = {}; - } - if (r.category && r.subcategory) { - categories[r.category][r.subcategory] = true; - } - }); - databaseloaded = true; - console.log('Food database loaded'); - fillForm(); + var records = client.sbx.data.food || []; + records.forEach(function(r) { + if (r.type == 'food') { + foodlist.push(r); + if (r.category && !categories[r.category]) { + categories[r.category] = {}; + } + if (r.category && r.subcategory) { + categories[r.category][r.subcategory] = true; + } } - }).done(function() { if (callback) { callback(); } }); + }); + databaseloaded = true; + console.log('Food database loaded'); + fillForm(); maybePrevent(event); - } + if (callback) { callback(); } + }; - boluscalc.loadFoodQuickpicks = function loadFoodQuickpicks( ) { + boluscalc.loadFoodQuickpicks = function loadFoodQuickpicks () { // Load quickpicks - $.ajax('/api/v1/food/quickpicks.json', { - success: function (records) { - quickpicks = records; - $('#bc_quickpick').empty().append(''); - for (var i=0; i' + r.name + ' (' + r.carbs + ' g)'); - }; - $('#bc_quickpick').val(-1); - $('#bc_quickpick').change(quickpickChange); + quickpicks = []; + var records = client.sbx.data.food || []; + records.forEach(function(r) { + if (r.type == 'quickpick') { + quickpicks.push(r); } }); + $('#bc_quickpick').empty().append(''); + 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(''); + } + $('#bc_quickpick').val(-1); + $('#bc_quickpick').change(quickpickChange); }; - - function fillForm(event) { + + function fillForm (event) { $('#bc_filter_category').empty().append(''); - Object.keys(categories).forEach( function eachCategory(s) { - $('#bc_filter_category').append(''); + Object.keys(categories).forEach(function eachCategory (s) { + $('#bc_filter_category').append(''); }); filter.category = ''; fillSubcategories(); - + $('#bc_filter_category').change(fillSubcategories); $('#bc_filter_subcategory').change(doFilter); - $('#bc_filter_name').on('input',doFilter); - + $('#bc_filter_name').on('input', doFilter); + maybePrevent(event); return false; } - function fillSubcategories(event) { + function fillSubcategories (event) { maybePrevent(event); filter.category = $('#bc_filter_category').val(); filter.subcategory = ''; $('#bc_filter_subcategory').empty().append(''); if (filter.category !== '') { - Object.keys(categories[filter.category]).forEach( function eachSubcategory(s) { - $('#bc_filter_subcategory').append(''); + Object.keys(categories[filter.category]).forEach(function eachSubcategory (s) { + $('#bc_filter_subcategory').append(''); }); } doFilter(); } - - function doFilter(event) { + + function doFilter (event) { if (event) { filter.category = $('#bc_filter_category').val(); filter.subcategory = $('#bc_filter_subcategory').val(); filter.name = $('#bc_filter_name').val(); } $('#bc_data').empty(); - for (var i=0; i' + o + ''); + o += 'Carbs: ' + foodlist[i].carbs + ' g'; + $('#bc_data').append(''); + /* eslint-enable security/detect-object-injection */ // verified false positive } $('#bc_addportions').val('1'); - + maybePrevent(event); } - - function addFoodFromDatabase(event) { + + function addFoodFromDatabase (event) { if (!databaseloaded) { boluscalc.loadFoodDatabase(event, addFoodFromDatabase); return; } - + $('#bc_addportions').val('1'); $('#bc_addfooddialog').dialog({ - width: 640 + width: 640 , height: 400 - , buttons: [ - { text: translate('Add'), - click: function() { - var index = $('#bc_data').val(); - var portions = parseFloat($('#bc_addportions').val().replace(',','.')); - if (index !== null && !isNaN(portions) && portions >0) { - foodlist[index].portions = portions; - foods.push(_.cloneDeep(foodlist[index])); - $( this ).dialog( 'close' ); - boluscalc.calculateInsulin(); + , buttons: [ + { + text: translate('Add') + , click: function() { + 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(); + } } - } - }, - { text: translate('Reload database'), - class: 'leftButton', - click: boluscalc.loadFoodDatabase + } + , { + text: translate('Reload database') + , class: 'leftButton' + , click: boluscalc.loadFoodDatabase } ] - , open : function() { + , open: function() { $(this).parent().css('box-shadow', '20px 20px 20px 0px black'); - $(this).parent().find('.ui-dialog-buttonset' ).css({'width':'100%','text-align':'right'}); - $(this).parent().find('button:contains("'+translate('Add')+'")').css({'float':'left'}); + $(this).parent().find('.ui-dialog-buttonset').css({ 'width': '100%', 'text-align': 'right' }); + $(this).parent().find('button:contains("' + translate('Add') + '")').css({ 'float': 'left' }); $('#bc_filter_name').focus(); } @@ -723,12 +759,12 @@ function init(client, $, plugins) { return false; } - function findClosestSGVToPastTime(time) { - var nowData = client.data.filter(function(d) { + function findClosestSGVToPastTime (time) { + var nowData = client.entries.filter(function(d) { return d.type === 'sgv' && d.mills <= time.getTime(); }); var focusPoint = _.last(nowData); - + if (!focusPoint || focusPoint.mills + times.mins(10).mills < time.getTime()) { return null; } @@ -739,19 +775,17 @@ function init(client, $, plugins) { // Make it faster on mobile devices $('.insulincalculationpart').change(boluscalc.calculateInsulin); } else { - $('.insulincalculationpart').on('input',boluscalc.calculateInsulin); + $('.insulincalculationpart').on('input', boluscalc.calculateInsulin); $('input:checkbox.insulincalculationpart').change(boluscalc.calculateInsulin); } $('#bc_bgfrommeter').change(boluscalc.calculateInsulin); $('#bc_addfromdatabase').click(addFoodFromDatabase); - $('#bc_bgfromsensor').change(function bc_bgfromsensor_click(event) { - if (boluscalc.lastsbx) { - boluscalc.updateVisualisations(boluscalc.lastsbx); - } + $('#bc_bgfromsensor').change(function bc_bgfromsensor_click (event) { + boluscalc.updateVisualisations(client.sbx); boluscalc.calculateInsulin(); maybePrevent(event); }); - + $('#boluscalcDrawerToggle').click(boluscalc.toggleDrawer); $('#boluscalcDrawer').find('button').click(boluscalc.submit); $('#bc_eventTime input:radio').change(boluscalc.eventTimeTypeChange); @@ -762,6 +796,6 @@ function init(client, $, plugins) { setDateAndTime(); return boluscalc; -}; +} -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/client/browser-settings.js b/lib/client/browser-settings.js index 30e4bb79edb..91fd37dbc07 100644 --- a/lib/client/browser-settings.js +++ b/lib/client/browser-settings.js @@ -2,18 +2,46 @@ var _ = require('lodash'); -function init (client, plugins, serverSettings, $) { +// VERSION 1 - 0.9.0 - 2015-Nov-07 - initial version +var STORAGE_VERSION = 1; +var Storages = require('js-storage'); - var storage = $.localStorage; +function init (client, serverSettings, $) { + + serverSettings = serverSettings || { settings: {} }; + + var storage = Storages.localStorage; var settings = require('../settings')(); - function loadForm ( ) { - var utils = require('../utils')(settings); - var language = require('../language')(); - language.set(settings.language); + 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( + $('') + .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; var translate = language.translate; - function appendThresholdValue(threshold) { + function appendThresholdValue (threshold) { return settings.alarmTypes.indexOf('simple') === -1 ? '' : ' (' + utils.scaleMgdl(threshold) + ')'; } @@ -30,8 +58,10 @@ function init (client, plugins, serverSettings, $) { $('#alarm-timeagowarnmins-browser').val(settings.alarmTimeagoWarnMins); $('#alarm-timeagourgent-browser').prop('checked', settings.alarmTimeagoUrgent); $('#alarm-timeagourgentmins-browser').val(settings.alarmTimeagoUrgentMins); + $('#alarm-pumpbatterylow-browser').prop('checked', settings.alarmPumpBatteryLow); $('#nightmode-browser').prop('checked', settings.nightMode); + $('#editmode-browser').prop('checked', settings.editMode); if (settings.isEnabled('rawbg')) { $('#show-rawbg-option').show(); @@ -45,14 +75,16 @@ function init (client, plugins, serverSettings, $) { if (settings.theme === 'colors') { $('#theme-colors-browser').prop('checked', true); + } else if (settings.theme === 'colorblindfriendly') { + $('#theme-colorblindfriendly-browser').prop('checked', true); } else { $('#theme-default-browser').prop('checked', true); } var langSelect = $('#language'); - _.each(language.languages, function eachLanguage(lang) { - langSelect.append(''); + _.each(language.languages, function eachLanguage (lang) { + langSelect.append(''); }); langSelect.val(settings.language); @@ -61,6 +93,8 @@ function init (client, plugins, serverSettings, $) { $('#basalrender').val(settings.extendedSettings.basal ? settings.extendedSettings.basal.render : 'none'); + updateBolusRender(); + if (settings.timeFormat === 24) { $('#24-browser').prop('checked', true); } else { @@ -69,8 +103,11 @@ function init (client, plugins, serverSettings, $) { var showPluginsSettings = $('#show-plugins'); var hasPluginsToShow = false; - plugins.eachEnabledPlugin(function each(plugin) { - if (plugins.specialPlugins.indexOf(plugin.name) > -1) { + + const pluginPrefs = []; + + client.plugins.eachEnabledPlugin(function each (plugin) { + if (client.plugins.specialPlugins.indexOf(plugin.name) > -1) { //ignore these, they are always on for now } else { var id = 'plugin-' + plugin.name; @@ -79,60 +116,138 @@ function init (client, plugins, serverSettings, $) { dd.find('input').prop('checked', settings.showPlugins.indexOf(plugin.name) > -1); hasPluginsToShow = true; } + + if (plugin.getClientPrefs) { + const prefs = plugin.getClientPrefs(); + pluginPrefs.push({ + plugin + , prefs + }) + } }); showPluginsSettings.toggle(hasPluginsToShow); - $('#editprofilelink').toggle(settings.isEnabled(['iob', 'cob', 'bwp', 'basal'])); + const bs = $('#browserSettings'); + const toggleCheckboxes = []; + + if (pluginPrefs.length > 0) { + pluginPrefs.forEach(function(e) { + // Only show settings if plugin is visible + if (settings.showPlugins.indexOf(e.plugin.name) > -1) { + const label = e.plugin.label; + const dl = $('
'); + dl.append(`
` + translate(label) + `
`); + e.prefs.forEach(function(p) { + const id = e.plugin.name + "-" + p.id; + const label = p.label; + if (p.type == 'boolean') { + const html = $(`
`); + dl.append(html); + const settingsBase = settings.extendedSettings[e.plugin.name]; + if (settingsBase[p.id] == true) { + toggleCheckboxes.push(id); + } + } + }); + bs.append(dl); + } + }); + } + toggleCheckboxes.forEach(function(cb) { + $('#' + cb).prop('checked', true); + }); + + $('#editprofilelink').toggle(settings.isEnabled('iob') || settings.isEnabled('cob') || settings.isEnabled('bwp') || settings.isEnabled('basal')); + + //fetches token from url + var parts = (location.search || '?').substring(1).split('&'); + var token = ''; + parts.forEach(function (val) { + if (val.startsWith('token=')) { + token = val.substring('token='.length); + } + }); + + //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); + $('#editprofilelink').attr('href', 'profile' + token); + $('#admintoolslink').attr('href', 'admin' + token); + $('#editfoodlink').attr('href', 'food' + token); + } } - function wireForm ( ) { + function wireForm () { $('#useDefaults').click(function(event) { settings.eachSetting(function clearEachSetting (name) { storage.remove(name); }); storage.remove('basalrender'); + storage.remove('bolus'); event.preventDefault(); client.browserUtils.reload(); }); $('#save').click(function(event) { - function checkedPluginNames() { + function checkedPluginNames () { var checkedPlugins = []; - $('#show-plugins input:checked').each(function eachPluginCheckbox(index, checkbox) { + $('#show-plugins input:checked').each(function eachPluginCheckbox (index, checkbox) { checkedPlugins.push($(checkbox).val()); }); return checkedPlugins.join(' '); } - function storeInBrowser(data) { - for (var k in data) { - if (data.hasOwnProperty(k)) { - storage.set(k, data[k]); - } + client.plugins.eachEnabledPlugin(function each (plugin) { + if (plugin.getClientPrefs) { + const prefs = plugin.getClientPrefs(); + + prefs.forEach(function(p) { + const id = plugin.name + "-" + p.id; + if (p.type == 'boolean') { + const val = $("#" + id).prop('checked'); + storage.set(id, val); + } + }); } + }); + + function storeInBrowser (data) { + Object.keys(data).forEach(k => { + /* eslint-disable-next-line security/detect-object-injection */ // verified false positive + storage.set(k, data[k]); + }); } storeInBrowser({ - units: $('input:radio[name=units-browser]:checked').val(), - alarmUrgentHigh: $('#alarm-urgenthigh-browser').prop('checked'), - alarmHigh: $('#alarm-high-browser').prop('checked'), - alarmLow: $('#alarm-low-browser').prop('checked'), - alarmUrgentLow: $('#alarm-urgentlow-browser').prop('checked'), - alarmTimeagoWarn: $('#alarm-timeagowarn-browser').prop('checked'), - alarmTimeagoWarnMins: parseInt($('#alarm-timeagowarnmins-browser').val()) || 15, - alarmTimeagoUrgent: $('#alarm-timeagourgent-browser').prop('checked'), - alarmTimeagoUrgentMins: parseInt($('#alarm-timeagourgentmins-browser').val()) || 30, - nightMode: $('#nightmode-browser').prop('checked'), - showRawbg: $('input:radio[name=show-rawbg]:checked').val(), - customTitle: $('input#customTitle').prop('value'), - theme: $('input:radio[name=theme-browser]:checked').val(), - timeFormat: parseInt($('input:radio[name=timeformat-browser]:checked').val()), - language: $('#language').val(), - scaleY: $('#scaleY').val(), - basalrender: $('#basalrender').val(), - showPlugins: checkedPluginNames() + units: $('input:radio[name=units-browser]:checked').val() + , alarmUrgentHigh: $('#alarm-urgenthigh-browser').prop('checked') + , alarmHigh: $('#alarm-high-browser').prop('checked') + , alarmLow: $('#alarm-low-browser').prop('checked') + , alarmUrgentLow: $('#alarm-urgentlow-browser').prop('checked') + , alarmTimeagoWarn: $('#alarm-timeagowarn-browser').prop('checked') + , alarmTimeagoWarnMins: parseInt($('#alarm-timeagowarnmins-browser').val()) || 15 + , alarmTimeagoUrgent: $('#alarm-timeagourgent-browser').prop('checked') + , alarmTimeagoUrgentMins: parseInt($('#alarm-timeagourgentmins-browser').val()) || 30 + , nightMode: $('#nightmode-browser').prop('checked') + , editMode: $('#editmode-browser').prop('checked') + , showRawbg: $('input:radio[name=show-rawbg]:checked').val() + , customTitle: $('input#customTitle').prop('value') + , theme: $('input:radio[name=theme-browser]:checked').val() + , timeFormat: parseInt($('input:radio[name=timeformat-browser]:checked').val()) + , language: $('#language').val() + , scaleY: $('#scaleY').val() + , basalrender: $('#basalrender').val() + , bolus: { + renderOver: $('#bolusRenderOver').val() + , renderFormat: $('#bolusRenderFormat').val() + , renderFormatSmall: $('#bolusRenderFormatSmall').val() + } + , showPlugins: checkedPluginNames() + , storageVersion: STORAGE_VERSION }); event.preventDefault(); @@ -140,45 +255,105 @@ function init (client, plugins, serverSettings, $) { }); } - function showLocalstorageError ( ) { + function showLocalstorageError () { var msg = 'Settings are disabled.

Please enable cookies so you may customize your Nightscout site.'; - $('.browserSettings').html('Settings'+msg+''); + $('.browserSettings').html('Settings' + msg + ''); $('#save').hide(); } - settings.extendedSettings = serverSettings.extendedSettings; + function handleStorageVersions () { + var previousVersion = parseInt(storage.get('storageVersion')); + + //un-versioned settings + if (isNaN(previousVersion)) { + //special showPlugins handling for careportal + //prevent careportal from being hidden by old stored settings + if (settings.isEnabled('careportal')) { + var storedShowPlugins = storage.get('showPlugins'); + if (storedShowPlugins && storedShowPlugins.indexOf('careportal') === -1) { + settings.showPlugins += ' careportal'; + } + } + } + } + + settings.extendedSettings = serverSettings.extendedSettings || { settings: {} }; 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]; }); + if (serverSettings.settings.thresholds) { + settings.thresholds = serverSettings.settings.thresholds; + } + + + if (serverSettings.settings.enable) { + settings.enable = serverSettings.settings.enable; + } + + if (settings.enable.indexOf('ar2') < 0) { + settings.enable += ' ar2'; + } + handleStorageVersions(); if (!settings.extendedSettings.basal) { settings.extendedSettings.basal = {}; } - var stored = storage.get('basalrender'); - settings.extendedSettings.basal.render = stored !== null ? stored : settings.extendedSettings.basal.render; - } catch(err) { + var basalStored = storage.get('basalrender'); + settings.extendedSettings.basal.render = basalStored !== null ? basalStored : settings.extendedSettings.basal.render; + + if (!settings.extendedSettings.bolus) { + settings.extendedSettings.bolus = { + renderOver: 0 + , renderFormat: 'default' + , renderFormatSmall: 'default' + }; + } + + 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); showLocalstorageError(); } - settings.thresholds = serverSettings.settings.thresholds; - settings.enable = serverSettings.settings.enable; + init.loadAndWireForm = function loadAndWireForm () { + loadForm(); + wireForm(); + }; - if (settings.enable.indexOf('ar2') < 0) { - settings.enable += ' ar2'; - } + init.loadPluginSettings = function loadPluginSettings (client) { - plugins.init(settings); + client.plugins.eachEnabledPlugin(function each (plugin) { + if (plugin.getClientPrefs) { + const prefs = plugin.getClientPrefs(); - loadForm(); - wireForm(); + if (!settings.extendedSettings[plugin.name]) { + settings.extendedSettings[plugin.name] = {}; + } + + const settingsBase = settings.extendedSettings[plugin.name]; + + prefs.forEach(function(p) { + const id = plugin.name + "-" + p.id; + const stored = storage.get(id); + if (stored !== null) { + settingsBase[p.id] = stored; + } + }); + } + }); + + } return settings; } - -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/client/browser-utils.js b/lib/client/browser-utils.js index d39b501a2c3..e587480a6f2 100644 --- a/lib/client/browser-utils.js +++ b/lib/client/browser-utils.js @@ -1,32 +1,23 @@ 'use strict'; -var lastOpenedDrawer = null; +var SMALL_SCREEN = 500; function init ($) { + var lastOpenedDrawer = null; // Tooltips can remain in the way on touch screens. if (!isTouch()) { - $('.tip').tipsy(); + $('.tip').tooltip(); } else { // Drawer info tips should be displayed on touchscreens. - $('#drawer').find('.tip').tipsy(); + $('#drawer').find('.tip').tooltip(); } - $.fn.tipsy.defaults = { - fade: true, - gravity: 'n', - opacity: 0.75 + $.fn.tooltip.defaults = { + fade: true + , gravity: 'n' + , 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(); @@ -37,42 +28,49 @@ function init ($) { event.preventDefault(); }); - $('.navigation a').click(function navigationClick ( ) { + $('.navigation a').click(function navigationClick () { closeDrawer('#drawer'); }); - function reload() { + function reload () { //strip '#' so form submission does not fail var url = window.location.href; url = url.replace(/#$/, ''); - window.location = url; + window.location.href = url; } - - function queryParms() { + function queryParms () { var params = {}; - if (location.search) { + if ((typeof location !== 'undefined') && location.search) { location.search.substr(1).split('&').forEach(function(item) { + // eslint-disable-next-line no-useless-escape params[item.split('=')[0]] = item.split('=')[1].replace(/[_\+]/g, ' '); }); } return params; } - function isTouch() { - try { document.createEvent('TouchEvent'); return true; } - catch (e) { return false; } + function isTouch () { + try { document.createEvent('TouchEvent'); return true; } catch (e) { return false; } } - function closeDrawer(id, callback) { + function closeLastOpenedDrawer (callback) { + if (lastOpenedDrawer) { + closeDrawer(lastOpenedDrawer, callback); + } else if (callback) { + callback(); + } + } + + function closeDrawer (id, callback) { lastOpenedDrawer = null; $('html, body').css({ scrollTop: 0 }); - $(id).css({display: 'none', right: '-300px'}); + $(id).css({ display: 'none', right: '-300px' }); if (callback) { callback(); } } - function openDrawer(id, prepare) { - function closeOpenDraw(callback) { + function openDrawer (id, prepare) { + function closeOpenDraw (callback) { if (lastOpenedDrawer) { closeDrawer(lastOpenedDrawer, callback); } else { @@ -80,15 +78,37 @@ function init ($) { } } - closeOpenDraw(function () { + closeOpenDraw(function() { lastOpenedDrawer = id; if (prepare) { prepare(); } - $(id).css({display:'block', right: '0'}); + + var style = { display: 'block', right: '0' }; + + var windowWidth = $(window).width(); + var windowHeight = $(window).height(); + //var chartTop = $('#chartContainer').offset().top - 45; + //var chartHeight = windowHeight - chartTop - 45; + if (windowWidth < SMALL_SCREEN || (windowHeight < SMALL_SCREEN) && windowWidth < 800) { + style.top = '0px'; + style.height = windowHeight + 'px'; + style.width = windowWidth + 'px'; + //TODO: maybe detect iOS and do this, doesn't work good with android + //if (chartHeight > windowHeight * 0.4) { + // style.top = chartTop + 'px'; + // style.height = chartHeight + 'px'; + //} + } else { + style.top = '0px'; + style.height = (windowHeight - 45) + 'px'; + style.width = '350px'; + } + + $(id).css(style); }); } - function toggleDrawer(id, openPrepare, closeCallback) { + function toggleDrawer (id, openPrepare, closeCallback) { if (lastOpenedDrawer === id) { closeDrawer(id, closeCallback); } else { @@ -96,13 +116,13 @@ function init ($) { } } - function closeNotification() { + function closeNotification () { var notify = $('#notification'); notify.hide(); notify.find('span').html(''); } - function showNotification(note, type) { + function showNotification (note, type) { var notify = $('#notification'); notify.hide(); @@ -112,17 +132,25 @@ function init ($) { notify.addClass(type ? type : 'urgent'); notify.find('span').html(note); - notify.css('left', 'calc(50% - ' + (notify.width() / 2) + 'px)'); + var windowWidth = $(window).width(); + var left = (windowWidth - notify.width()) / 2; + notify.css('left', left + 'px'); notify.show(); } + function getLastOpenedDrawer () { + return lastOpenedDrawer; + } + return { reload: reload , queryParms: queryParms , closeDrawer: closeDrawer + , closeLastOpenedDrawer: closeLastOpenedDrawer , toggleDrawer: toggleDrawer , closeNotification: closeNotification , showNotification: showNotification + , getLastOpenedDrawer: getLastOpenedDrawer }; } diff --git a/lib/client/careportal.js b/lib/client/careportal.js index 36d556332b3..514dbf5d650 100644 --- a/lib/client/careportal.js +++ b/lib/client/careportal.js @@ -1,51 +1,32 @@ 'use strict'; -var moment = require('moment-timezone'); var _ = require('lodash'); var parse_duration = require('parse-duration'); // https://www.npmjs.com/package/parse-duration var times = require('../times'); +var consts = require('../constants'); +var Storages = require('js-storage'); function init (client, $) { - var careportal = { }; + var careportal = {}; var translate = client.translate; - var storage = $.localStorage; - - careportal.events = [ - { val: '', name: '' } - , { val: 'BG Check', name: 'BG Check' } - , { val: 'Snack Bolus', name: 'Snack Bolus' } - , { val: 'Meal Bolus', name: 'Meal Bolus' } - , { val: 'Correction Bolus', name: 'Correction Bolus' } - , { val: 'Carb Correction', name: 'Carb Correction' } - , { val: 'Announcement', name: 'Announcement' } - , { val: 'Note', name: 'Note' } - , { val: 'Question', name: 'Question' } - , { val: 'Exercise', name: 'Exercise' } - , { val: 'Site Change', name: 'Pump Site Change' } - , { val: 'Sensor Start', name: 'Dexcom Sensor Start' } - , { val: 'Sensor Change', name: 'Dexcom Sensor Change' } - , { val: 'Insulin Change', name: 'Insulin Cartridge Change' } - , { val: 'Temp Basal Start', name: 'Temp Basal Start' } - , { val: 'Temp Basal End', name: 'Temp Basal End' } - , { val: 'Profile Switch', name: 'Profile Switch' } - , { val: 'D.A.D. Alert', name: 'D.A.D. Alert' } - ]; - + var storage = Storages.localStorage; + var units = client.settings.units; + var eventTime = $('#eventTimeValue'); var eventDate = $('#eventDateValue'); function setDateAndTime (time) { - time = time || moment(); - eventTime.val(time.format('HH:mm')); - eventDate.val(time.format('YYYY-MM-DD')); + time = time || client.ctx.moment(); + eventTime.val(time.hours() + ":" + time.minutes()); + eventDate.val(time.toISOString().split('T')[0]); } - function mergeDateAndTime ( ) { + function mergeDateAndTime () { return client.utils.mergeInputTime(eventTime.val(), eventDate.val()); } - function updateTime(ele, time) { + function updateTime (ele, time) { ele.attr('oldminutes', time.minutes()); ele.attr('oldhours', time.hours()); } @@ -56,30 +37,30 @@ function init (client, $) { } } - var inputMatrix = { - '': { bg: true, insulin: true, carbs: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false } - , 'BG Check': { bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Snack Bolus': { bg: true, insulin: true, carbs: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false } - , 'Meal Bolus': { bg: true, insulin: true, carbs: true, prebolus: true, duration: false, percent: false, absolute: false, profile: false } - , 'Correction Bolus': { bg: true, insulin: true, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Carb Correction': { bg: true, insulin: false, carbs: true, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Announcement': { bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Note': { bg: true, insulin: false, carbs: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false } - , 'Question': { bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Exercise': { bg: false, insulin: false, carbs: false, prebolus: false, duration: true, percent: false, absolute: false, profile: false } - , 'Site Change': { bg: false, insulin: true, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Sensor Start': { bg: false, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Sensor Change': { bg: false, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Insulin Change': { bg: false, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Temp Basal Start': { bg: true, insulin: false, carbs: false, prebolus: false, duration: true, percent: true, absolute: true, profile: false } - , 'Temp Basal End': { bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - , 'Profile Switch': { bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: true } - , 'D.A.D. Alert': { bg: true, insulin: false, carbs: false, prebolus: false, duration: false, percent: false, absolute: false, profile: false } - }; + var inputMatrix = {}; + var submitHooks = {}; + + function refreshEventTypes() { + careportal.allEventTypes = client.plugins.getAllEventTypes(client.sbx); + + careportal.events = _.map(careportal.allEventTypes, function each (event) { + return _.pick(event, ['val', 'name']); + }); + + inputMatrix = {}; + 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', 'sensor', 'reasons', 'targets']); + submitHooks[event.val] = event.submitHook; + }); + } + + refreshEventTypes(); - careportal.filterInputs = function filterInputs ( event ) { + careportal.filterInputs = function filterInputs (event) { var eventType = $('#eventType').val(); - + function displayType (enabled) { if (enabled) { return ''; @@ -87,45 +68,141 @@ function init (client, $) { return 'none'; } } - - function resetIfHidden(visible, id) { + + function resetIfHidden (visible, id) { if (!visible) { $(id).val(''); } } - - $('#bg').css('display',displayType(inputMatrix[eventType]['bg'])); - $('#insulinGivenLabel').css('display',displayType(inputMatrix[eventType]['insulin'])); - $('#carbsGivenLabel').css('display',displayType(inputMatrix[eventType]['carbs'])); - $('#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() === '')); - $('#profileLabel').css('display',displayType(inputMatrix[eventType]['profile'])); - $('#preBolusLabel').css('display',displayType(inputMatrix[eventType]['prebolus'])); - + + // 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'])); + + $('#otpLabel').css('display', displayType(inputMatrix[eventType]['otp'])); + $('#remoteCarbsLabel').css('display', displayType(inputMatrix[eventType]['remoteCarbs'])); + $('#remoteAbsorptionLabel').css('display', displayType(inputMatrix[eventType]['remoteAbsorption'])); + $('#remoteBolusLabel').css('display', displayType(inputMatrix[eventType]['remoteBolus'])); + + $('#bg').css('display', displayType(inputMatrix[eventType]['bg'])); + $('#insulinGivenLabel').css('display', displayType(inputMatrix[eventType]['insulin'])); + + $('#carbsGivenLabel').css('display', displayType(inputMatrix[eventType]['carbs'])); + $('#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() === '')); + $('#profileLabel').css('display', displayType(inputMatrix[eventType]['profile'])); + $('#preBolusLabel').css('display', displayType(inputMatrix[eventType]['prebolus'])); + $('#insulinSplitLabel').css('display', displayType(inputMatrix[eventType]['split'])); + + $('#reason').empty(); + _.each(reasons, function eachReason (reason) { + $('#reason').append(''); + }); + + careportal.reasonable(); + + resetIfHidden(inputMatrix[eventType]['otp'], '#otp'); + resetIfHidden(inputMatrix[eventType]['remoteCarbs'], '#remoteCarbs'); + resetIfHidden(inputMatrix[eventType]['remoteAbsorption'], '#remoteAbsorption'); + resetIfHidden(inputMatrix[eventType]['remoteBolus'], '#remoteBolus'); + resetIfHidden(inputMatrix[eventType]['insulin'], '#insulinGiven'); 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.prepareEvents = function prepareEvents ( ) { + careportal.reasonable = function reasonable () { + var eventType = $('#eventType').val(); + 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) { + return r.name === selected; + }); + + if (reason && reason.targetTop) { + $('#targetTop').val(reason.targetTop); + } else { + $('#targetTop').val(''); + } + + if (reason && reason.targetBottom) { + $('#targetBottom').val(reason.targetBottom); + } else { + $('#targetBottom').val(''); + } + + if (reason) { + if (reason.duration) { + $('#duration').val(reason.duration); + } else { + $('#duration').val(''); + } + } + }; + + careportal.prepareEvents = function prepareEvents () { $('#eventType').empty(); - _.each(careportal.events, function eachEvent(event) { - $('#eventType').append(''); + _.each(careportal.events, function eachEvent (event) { + $('#eventType').append(''); }); $('#eventType').change(careportal.filterInputs); - $('#percent').change(careportal.filterInputs); - $('#absolute').change(careportal.filterInputs); + $('#reason').change(careportal.reasonable); + $('#percent').on('input', careportal.filterInputs); + $('#absolute').on('input', careportal.filterInputs); + $('#insulinSplitNow').on('input', careportal.adjustSplit); + $('#insulinSplitExt').on('input', careportal.adjustSplit); careportal.filterInputs(); + careportal.adjustSplit(); }; - careportal.resolveEventName = function resolveEventName(value) { - _.each(careportal.events, function eachEvent(e) { + careportal.adjustSplit = function adjustSplit (event) { + if ($(this).attr('id') === 'insulinSplitNow') { + var nowval = parseInt($('#insulinSplitNow').val()) || 0; + $('#insulinSplitExt').val(100 - nowval); + $('#insulinSplitNow').val(nowval); + } else { + var extval = parseInt($('#insulinSplitExt').val()) || 0; + $('#insulinSplitNow').val(100 - extval); + $('#insulinSplitExt').val(extval); + } + + maybePrevent(event); + }; + + careportal.resolveEventName = function resolveEventName (value) { + _.each(careportal.events, function eachEvent (e) { if (e.val === value) { value = e.name; } @@ -133,16 +210,28 @@ function init (client, $) { return value; }; - careportal.prepare = function prepare ( ) { + careportal.prepare = function prepare () { + refreshEventTypes(); + $('#profile').empty(); - client.profilefunctions.listBasalProfiles().forEach(function (p) { + client.profilefunctions.listBasalProfiles().forEach(function(p) { $('#profile').append(''); }); careportal.prepareEvents(); $('#eventType').val(''); $('#glucoseValue').val('').attr('placeholder', translate('Value in') + ' ' + client.settings.units); $('#meter').prop('checked', true); + + $('#otp').val(''); + $('#remoteCarbs').val(''); + $('#remoteAbsorption').val(''); + $('#remoteBolus').val(''); + $('#carbsGiven').val(''); + $('#proteinGiven').val(''); + $('#fatGiven').val(''); + $('#sensorCode').val(''); + $('#transmitterId').val(''); $('#insulinGiven').val(''); $('#duration').val(''); $('#percent').val(''); @@ -150,27 +239,67 @@ function init (client, $) { $('#profile').val(client.profilefunctions.activeProfileToTime()); $('#preBolus').val(0); $('#notes').val(''); - $('#enteredBy').val(storage.get('enteredBy') || ''); + $('#enteredBy').val(client.authorized ? client.authorized.sub : storage.get('enteredBy') || ''); $('#nowtime').prop('checked', true); setDateAndTime(); }; - function gatherData ( ) { + function gatherData () { + var eventType = $('#eventType').val(); + var selectedReason = $('#reason').val(); + var data = { enteredBy: $('#enteredBy').val() - , eventType: $('#eventType').val() - , glucose: $('#glucoseValue').val().replace(',','.') - , glucoseType: $('#treatment-form').find('input[name=glucoseType]:checked').val() - , carbs: $('#carbsGiven').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() - , profile: $('#profile').val() - , preBolus: parseInt($('#preBolus').val()) - , notes: $('#notes').val() - , units: client.settings.units + , eventType: eventType + , otp: $('#otp').val() + , remoteCarbs: $('#remoteCarbs').val() + , remoteAbsorption: $('#remoteAbsorption').val() + , remoteBolus: $('#remoteBolus').val() + , glucose: $('#glucoseValue').val().replace(',', '.') + , reason: selectedReason + , targetTop: $('#targetTop').val().replace(',', '.') + , targetBottom: $('#targetBottom').val().replace(',', '.') + , glucoseType: $('#treatment-form').find('input[name=glucoseType]:checked').val() + , 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() + , profile: $('#profile').val() + , preBolus: $('#preBolus').val() + , notes: $('#notes').val() + , units: client.settings.units }; + data.preBolus = parseInt(data.preBolus); + + if (isNaN(data.preBolus)) { + delete data.preBolus; + } + + 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; + }); + + if (reason) { + data.reasonDisplay = reason.displayName; + } + + if (units == "mmol") { + data.targetTop = data.targetTop * consts.MMOL_TO_MGDL; + data.targetBottom = data.targetBottom * consts.MMOL_TO_MGDL; + } + //special handling for absolute to support temp to 0 var absolute = $('#absolute').val(); if ('' !== absolute && !isNaN(absolute)) { @@ -180,7 +309,9 @@ function init (client, $) { if ($('#othertime').is(':checked')) { data.eventTime = mergeDateAndTime().toDate(); } - + + data.created_at = data.eventTime ? data.eventTime.toISOString() : new Date().toISOString(); + if (!inputMatrix[data.eventType].profile) { delete data.profile; } @@ -189,7 +320,28 @@ function init (client, $) { data.eventType = 'Temp Basal'; } - return data; + if (data.eventType.indexOf('Temporary Target Cancel') > -1) { + data.duration = 0; + data.eventType = 'Temporary Target'; + data.targetBottom = ""; + data.targetTop = ""; + } + + if (data.eventType.indexOf('Combo Bolus') > -1) { + data.splitNow = parseInt($('#insulinSplitNow').val()) || 0; + data.splitExt = parseInt($('#insulinSplitExt').val()) || 0; + } + + 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] + } + /* eslint-enable security/detect-object-injection */ // verified false positive + }); + + return d; } careportal.save = function save (event) { @@ -198,7 +350,61 @@ function init (client, $) { maybePrevent(event); }; - function buildConfirmText(data) { + function validateData (data) { + + let allOk = true; + let messages = []; + + console.log('Validating careportal entry: ', data.eventType); + + if (data.duration !== 0 && data.eventType == 'Temporary Target') { + if (isNaN(data.targetTop) || isNaN(data.targetBottom) || !data.targetBottom || !data.targetTop) { + console.log('Bottom or Top target missing'); + allOk = false; + messages.push("Please enter a valid value for both top and bottom target to save a Temporary Target"); + } else { + + let targetTop = parseInt(data.targetTop); + let targetBottom = parseInt(data.targetBottom); + + let minTarget = 4 * consts.MMOL_TO_MGDL; + let maxTarget = 18 * consts.MMOL_TO_MGDL; + + if (units == "mmol") { + targetTop = Math.round(targetTop / consts.MMOL_TO_MGDL * 10) / 10; + targetBottom = Math.round(targetBottom / consts.MMOL_TO_MGDL * 10) / 10; + minTarget = Math.round(minTarget / consts.MMOL_TO_MGDL * 10) / 10; + maxTarget = Math.round(maxTarget / consts.MMOL_TO_MGDL * 10) / 10; + } + + if (targetTop > maxTarget) { + allOk = false; + messages.push("Temporary target high is too high"); + } + + if (targetBottom < minTarget) { + allOk = false; + messages.push("Temporary target low is too low"); + } + + if (targetTop < targetBottom || targetBottom > targetTop) { + allOk = false; + messages.push("The low target must be lower than the high target and high target must be higher than the low target."); + } + + } + } + + // TODO: add check for remote (Bolus, Carbs, Absorption) + + return { + allOk + , messages + }; + + } + + function buildConfirmText (data) { var text = [ translate('Please verify that the data entered is correct') + ': ' , translate('Event Type') + ': ' + translate(careportal.resolveEventName(data.eventType)) @@ -210,14 +416,41 @@ function init (client, $) { } } + if (data.duration === 0 && data.eventType === 'Temporary Target') { + text[text.length - 1] += ' ' + translate('Cancel'); + } + + pushIf(data.remoteCarbs, translate('Remote Carbs') + ': ' + data.remoteCarbs); + pushIf(data.remoteAbsorption, translate('Remote Absorption') + ': ' + data.remoteAbsorption); + pushIf(data.remoteBolus, translate('Remote Bolus') + ': ' + data.remoteBolus); + pushIf(data.otp, translate('One Time Pascode') + ': ' + data.otp); + pushIf(data.glucose, translate('Blood Glucose') + ': ' + data.glucose); pushIf(data.glucose, translate('Measurement Method') + ': ' + translate(data.glucoseType)); + pushIf(data.reason, translate('Reason') + ': ' + data.reason); + + var targetTop = data.targetTop; + var targetBottom = data.targetBottom; + + if (units == "mmol") { + targetTop = Math.round(data.targetTop / consts.MMOL_TO_MGDL * 10) / 10; + targetBottom = Math.round(data.targetBottom / consts.MMOL_TO_MGDL * 10) / 10; + } + + pushIf(data.targetTop, translate('Target Top') + ': ' + targetTop); + pushIf(data.targetBottom, translate('Target Bottom') + ': ' + targetBottom); + 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.duration, translate('Duration') + ': ' + data.duration); + pushIf(data.eventType === 'Combo Bolus', translate('Combo Bolus') + ': ' + data.splitNow + '% : ' + data.splitExt + '%'); + pushIf(data.duration, translate('Duration') + ': ' + data.duration + ' ' + translate('mins')); pushIf(data.percent, translate('Percent') + ': ' + data.percent); - pushIf(data.absolute, translate('Basal value') + ': ' + data.absolute); + pushIf('absolute' in data, translate('Basal value') + ': ' + data.absolute); pushIf(data.profile, translate('Profile') + ': ' + data.profile); pushIf(data.preBolus, translate('Carb Time') + ': ' + data.preBolus + ' ' + translate('mins')); pushIf(data.notes, translate('Notes') + ': ' + data.notes); @@ -227,26 +460,60 @@ function init (client, $) { return text.join('\n'); } - function confirmPost(data) { - if (window.confirm(buildConfirmText(data))) { - $.ajax({ - method: 'POST', - url: '/api/v1/treatments/' - , headers: { - 'api-secret': client.hashauth.hash() - } - , data: data - }).done(function treatmentSaved (response) { - console.info('treatment saved', response); - }).fail(function treatmentSaveFail (response) { - console.info('treatment saved', response); - alert(translate('Entering record failed') + '. ' + translate('Status') + ': ' + response.status); + function confirmPost (data) { + + const validation = validateData(data); + + if (!validation.allOk) { + + let messages = ""; + + validation.messages.forEach(function(m) { + messages += translate(m) + "\n"; }); - storage.set('enteredBy', data.enteredBy); + window.alert(messages); + } else { + if (window.confirm(buildConfirmText(data))) { + var submitHook = submitHooks[data.eventType]; + if (submitHook) { + submitHook(client, data, function (error) { + if (error) { + console.log("submit error = ", error); + alert(translate('Error') + ': ' + error); + } else { + client.browserUtils.closeDrawer('#treatmentDrawer'); + } + }); + } else { + postTreatment(data); + } + } + } + } - client.browserUtils.closeDrawer('#treatmentDrawer'); + function postTreatment (data) { + if (data.eventType === 'Combo Bolus') { + data.enteredinsulin = data.insulin; + data.insulin = data.enteredinsulin * data.splitNow / 100; + data.relative = data.enteredinsulin * data.splitExt / 100 / data.duration * 60; } + + $.ajax({ + method: 'POST' + , url: '/api/v1/treatments/' + , headers: client.headers() + , data: data + }).done(function treatmentSaved (response) { + console.info('treatment saved', response); + }).fail(function treatmentSaveFail (response) { + console.info('treatment saved', response); + alert(translate('Entering record failed') + '. ' + translate('Status') + ': ' + response.status); + }); + + storage.set('enteredBy', data.enteredBy); + + client.browserUtils.closeDrawer('#treatmentDrawer'); } careportal.dateTimeFocus = function dateTimeFocus (event) { @@ -257,6 +524,11 @@ function init (client, $) { careportal.dateTimeChange = function dateTimeChange (event) { $('#othertime').prop('checked', true); + + // Can't decipher why the following logic was in place + // and it's now bugging out and resetting any date set manually + // so I'm disabling this + /* var ele = $(this); var merged = mergeDateAndTime(); @@ -269,6 +541,8 @@ function init (client, $) { setDateAndTime(merged); updateTime(ele, merged); + */ + maybePrevent(event); }; @@ -296,4 +570,3 @@ function init (client, $) { } module.exports = init; - diff --git a/lib/client/chart.js b/lib/client/chart.js index e5f074ab3c7..5c417916bc2 100644 --- a/lib/client/chart.js +++ b/lib/client/chart.js @@ -2,128 +2,223 @@ var _ = require('lodash'); var times = require('../times'); -var d3locales = require('../d3locales'); +var d3locales = require('./d3locales'); +var scrolling = false + , scrollNow = 0 + , scrollBrushExtent = null + , scrollRange = null; -var DEBOUNCE_MS = 10 - , padding = { bottom: 30 } - ; +var PADDING_BOTTOM = 30 + , OPEN_TOP_HEIGHT = 8 + , CONTEXT_MAX = 420 + , CONTEXT_MIN = 36 + , FOCUS_MAX = 510 + , FOCUS_MIN = 30; + +var loadTime = Date.now(); function init (client, d3, $) { - var chart = { }; + var chart = {}; var utils = client.utils; var renderer = client.renderer; - var localeFormatter = d3.locale(d3locales.locale(client.settings.language)); - - function brushStarted ( ) { + var defs = d3.select('body').append('svg').append('defs'); + + // add defs for combo boluses + var dashWidth = 5; + defs.append('pattern') + .attr('id', 'hash') + .attr('patternUnits', 'userSpaceOnUse') + .attr('width', 6) + .attr('height', 6) + .attr('x', 0) + .attr('y', 0) + .append('g') + .style('fill', 'none') + .style('stroke', '#0099ff') + .style('stroke-width', 2) + .append('path').attr('d', 'M0,0 l' + dashWidth + ',' + dashWidth) + .append('path').attr('d', 'M' + dashWidth + ',0 l-' + dashWidth + ',' + dashWidth); + + // arrow head + defs.append('marker') + .attr('id', 'arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 5) + .attr('refY', 0) + .attr('markerWidth', 8) + .attr('markerHeight', 8) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('class', 'arrowHead'); + + var localeFormatter = d3.timeFormatLocale(d3locales.locale(client.settings.language)); + + function beforeBrushStarted () { + // go ahead and move the brush because + // a single click will not execute the brush event + var now = new Date(); + var dx = chart.xScale2(now) - chart.xScale2(new Date(now.getTime() - client.focusRangeMS)); + + var cx = d3.mouse(this)[0]; + var x0 = cx - dx / 2; + var x1 = cx + dx / 2; + + var range = chart.xScale2.range(); + var X0 = range[0]; + var X1 = range[1]; + + var brush = x0 < X0 ? [X0, X0 + dx] : x1 > X1 ? [X1 - dx, X1] : [x0, x1]; + + chart.theBrush.call(chart.brush.move, brush); + } + + function brushStarted () { // update the opacity of the context data points to brush extent chart.context.selectAll('circle') - .data(client.data) + .data(client.entries) .style('opacity', 1); } - function brushEnded ( ) { + function brushEnded () { // update the opacity of the context data points to brush extent + var selectedRange = chart.createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); + chart.context.selectAll('circle') - .data(client.data) - .style('opacity', function (d) { return renderer.highlightBrushPoints(d) }); + .data(client.entries) + .style('opacity', function(d) { return renderer.highlightBrushPoints(d, from, to) }); } var extent = client.dataExtent(); var yScaleType; if (client.settings.scaleY === 'linear') { - yScaleType = d3.scale.linear; + yScaleType = d3.scaleLinear; } else { - yScaleType = d3.scale.log; + yScaleType = d3.scaleLog; } - var focusYDomain = [utils.scaleMgdl(30), utils.scaleMgdl(510)]; - var contextYDomain = [utils.scaleMgdl(36), utils.scaleMgdl(420)]; + var focusYDomain = [utils.scaleMgdl(FOCUS_MIN), utils.scaleMgdl(FOCUS_MAX)]; + var contextYDomain = [utils.scaleMgdl(CONTEXT_MIN), utils.scaleMgdl(CONTEXT_MAX)]; - function dynamicDomain() { - var mult = 1.3 + function dynamicDomain () { + // allow y-axis to extend all the way to the top of the basal area, but leave room to display highest value + var mult = 1.15 , targetTop = client.settings.thresholds.bgTargetTop - , mgdlMax = d3.max(client.data, function (d) { return d.mgdl; }); + // filter to only use actual SGV's (not rawbg's) to set the view window. + // can switch to Logarithmic (non-dynamic) to see anything that doesn't fit in the dynamicDomain + , mgdlMax = d3.max(client.entries, function(d) { if (d.type === 'sgv') { return d.mgdl; } }); + // use the 99th percentile instead of max to avoid rescaling for 1 flukey data point + // need to sort client.entries by mgdl first + //, mgdlMax = d3.quantile(client.entries, 0.99, function (d) { return d.mgdl; }); return [ - utils.scaleMgdl(30) + utils.scaleMgdl(FOCUS_MIN) , Math.max(utils.scaleMgdl(mgdlMax * mult), utils.scaleMgdl(targetTop * mult)) ]; } - function dynamicDomainOrElse(defaultDomain) { - if (client.settings.scaleY === 'linear' || client.settings.scaleY === 'log-dynamic') { + function dynamicDomainOrElse (defaultDomain) { + if (client.entries && (client.entries.length > 0) && (client.settings.scaleY === 'linear' || client.settings.scaleY === 'log-dynamic')) { return dynamicDomain(); } else { return defaultDomain; } } - + // define the parts of the axis that aren't dependent on width or height - var xScale = chart.xScale = d3.time.scale().domain(extent); + var xScale = chart.xScale = d3.scaleTime().domain(extent); + focusYDomain = dynamicDomainOrElse(focusYDomain); var yScale = chart.yScale = yScaleType() - .domain(dynamicDomainOrElse(focusYDomain)); + .domain(focusYDomain); - var xScale2 = chart.xScale2 = d3.time.scale().domain(extent); + var xScale2 = chart.xScale2 = d3.scaleTime().domain(extent); + + contextYDomain = dynamicDomainOrElse(contextYDomain); var yScale2 = chart.yScale2 = yScaleType() - .domain(dynamicDomainOrElse(contextYDomain)); + .domain(contextYDomain); - chart.xScaleBasals = d3.time.scale().domain(extent); + chart.xScaleBasals = d3.scaleTime().domain(extent); - chart.yScaleBasals = d3.scale.linear() + chart.yScaleBasals = d3.scaleLinear() .domain([0, 5]); - var tickFormat = localeFormatter.timeFormat.multi( [ - ['.%L', function(d) { return d.getMilliseconds(); }], - [':%S', function(d) { return d.getSeconds(); }], - ['%I:%M', function(d) { return d.getMinutes(); }], - [client.settings.timeFormat === 24 ? '%H:%M' : '%-I %p', function(d) { return d.getHours(); }], - ['%a %d', function(d) { return d.getDay() && d.getDate() !== 1; }], - ['%b %d', function(d) { return d.getDate() !== 1; }], - ['%B', function(d) { return d.getMonth(); }], - ['%Y', function() { return true; }] - ]); + var formatMillisecond = localeFormatter.format('.%L') + , formatSecond = localeFormatter.format(':%S') + , formatMinute = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : + localeFormatter.format('%-I:%M') + , formatHour = client.settings.timeFormat === 24 ? localeFormatter.format('%H:%M') : + localeFormatter.format('%-I %p') + , formatDay = localeFormatter.format('%a %d') + , formatWeek = localeFormatter.format('%b %d') + , formatMonth = localeFormatter.format('%B') + , formatYear = localeFormatter.format('%Y'); + + var tickFormat = function(date) { + return (d3.timeSecond(date) < date ? formatMillisecond : + d3.timeMinute(date) < date ? formatSecond : + d3.timeHour(date) < date ? formatMinute : + d3.timeDay(date) < date ? formatHour : + d3.timeMonth(date) < date ? (d3.timeWeek(date) < date ? formatDay : formatWeek) : + d3.timeYear(date) < date ? formatMonth : + formatYear)(date); + }; var tickValues = client.ticks(client); - chart.xAxis = d3.svg.axis() - .scale(xScale) + chart.xAxis = d3.axisBottom(xScale) + + chart.xAxis = d3.axisBottom(xScale) .tickFormat(tickFormat) - .ticks(4) - .orient('bottom'); + .ticks(6); - chart.yAxis = d3.svg.axis() - .scale(yScale) + chart.yAxis = d3.axisLeft(yScale) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('left'); + .tickValues(tickValues); - chart.xAxis2 = d3.svg.axis() - .scale(xScale2) + chart.xAxis2 = d3.axisBottom(xScale2) .tickFormat(tickFormat) - .ticks(6) - .orient('bottom'); + .ticks(6); - chart.yAxis2 = d3.svg.axis() - .scale(yScale2) + chart.yAxis2 = d3.axisRight(yScale2) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('right'); + .tickValues(tickValues); - // setup a brush - chart.brush = d3.svg.brush() - .x(xScale2) - .on('brushstart', brushStarted) - .on('brush', client.brushed) - .on('brushend', brushEnded); + d3.select('tick') + .style('z-index', '10000'); - chart.futureOpacity = d3.scale.linear( ) - .domain([times.mins(25).msecs, times.mins(60).msecs]) - .range([0.8, 0.1]); + // setup a brush + chart.brush = d3.brushX() + .on('start', brushStarted) + .on('brush', function brush (time) { + // layouting the graph causes a brushed event + // ignore retro data load the first two seconds + if (Date.now() - loadTime > 2000) client.loadRetroIfNeeded(); + client.brushed(time); + }) + .on('end', brushEnded); + + chart.theBrush = null; + + chart.futureOpacity = (function() { + var scale = d3.scaleLinear() + .domain([times.mins(25).msecs, times.mins(60).msecs]) + .range([0.8, 0.1]); + + return function(delta) { + if (delta < 0) { + return null; + } else { + return scale(delta); + } + }; + })(); // create svg and g to contain the chart contents chart.charts = d3.select('#chartContainer').append('svg') @@ -131,60 +226,88 @@ function init (client, d3, $) { .attr('class', 'chartContainer'); chart.basals = chart.charts.append('g').attr('class', 'chart-basals'); - chart.basals.attr('display','none'); chart.focus = chart.charts.append('g').attr('class', 'chart-focus'); + chart.drag = chart.focus.append('g').attr('class', 'drag-area'); // create the x axis container chart.focus.append('g') - .attr('class', 'x axis'); + .attr('class', 'x axis') + .style("font-size", "16px"); // create the y axis container chart.focus.append('g') - .attr('class', 'y axis'); + .attr('class', 'y axis') + .style("font-size", "16px"); - chart.context = chart.charts.append('g').attr('class', 'chart-context'); + chart.context = chart.charts.append('g') + .attr('class', 'chart-context'); // create the x axis container chart.context.append('g') - .attr('class', 'x axis'); + .attr('class', 'x axis') + .style("font-size", "16px"); // create the y axis container chart.context.append('g') - .attr('class', 'y axis'); + .attr('class', 'y axis') + .style("font-size", "16px"); + + chart.createBrushedRange = function() { + var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null; + + var range = brushedRange && brushedRange.map(chart.xScale2.invert); + var dataExtent = client.dataExtent(); - function createAdjustedRange() { - var range = chart.brush.extent().slice(); + if (!brushedRange) { + // console.log('No current brushed range. Setting range to last focusRangeMS amount of available data'); + range = dataExtent; + range[0] = new Date(range[1].getTime() - client.focusRangeMS); + } - var end = range[1].getTime() + client.forecastTime; + var end = range[1].getTime(); if (!chart.inRetroMode()) { - var lastSGVMills = client.latestSGV ? client.latestSGV.mills : client.now; - end += (client.now - lastSGVMills); + end = client.now > dataExtent[1].getTime() ? client.now : dataExtent[1].getTime(); } range[1] = new Date(end); + range[0] = new Date(end - client.focusRangeMS); + + // console.log('createBrushedRange: ', brushedRange, range); return range; } - chart.inRetroMode = function inRetroMode() { - if (!chart.brush || !chart.xScale2) { + chart.createAdjustedRange = function() { + var adjustedRange = chart.createBrushedRange(); + + adjustedRange[1] = new Date(Math.max(adjustedRange[1].getTime(), client.forecastTime)); + + return adjustedRange; + } + + chart.inRetroMode = function inRetroMode () { + var brushedRange = chart.theBrush && d3.brushSelection(chart.theBrush.node()) || null; + + if (!brushedRange || !chart.xScale2) { return false; } - var brushTime = chart.brush.extent()[1].getTime(); var maxTime = chart.xScale2.domain()[1].getTime(); + var brushTime = chart.xScale2.invert(brushedRange[1]).getTime(); return brushTime < maxTime; }; // called for initial update and updates for resize - chart.update = _.debounce(function debouncedUpdateChart(init) { + chart.update = function update (init) { if (client.documentHidden && !init) { console.info('Document Hidden, not updating - ' + (new Date())); return; } + chart.setForecastTime(); + var chartContainer = $('#chartContainer'); if (chartContainer.length < 1) { @@ -196,32 +319,39 @@ function init (client, d3, $) { var dataRange = client.dataExtent(); var chartContainerRect = chartContainer[0].getBoundingClientRect(); var chartWidth = chartContainerRect.width; - var chartHeight = chartContainerRect.height - padding.bottom; + var chartHeight = chartContainerRect.height - PADDING_BOTTOM; // get the height of each chart based on its container size ratio var focusHeight = chart.focusHeight = chartHeight * .7; - var contextHeight = chart.contextHeight = chartHeight * .2; + var contextHeight = chart.contextHeight = chartHeight * .3; chart.basalsHeight = focusHeight / 4; // get current brush extent - var currentBrushExtent = createAdjustedRange(); + var currentRange = chart.createAdjustedRange(); + var currentBrushExtent = chart.createBrushedRange(); // only redraw chart if chart size has changed - if ((chart.prevChartWidth !== chartWidth) || (chart.prevChartHeight !== chartHeight)) { + var widthChanged = (chart.prevChartWidth !== chartWidth); + if (widthChanged || (chart.prevChartHeight !== chartHeight)) { + + //if rotated + if (widthChanged) { + client.browserUtils.closeLastOpenedDrawer(); + } chart.prevChartWidth = chartWidth; chart.prevChartHeight = chartHeight; //set the width and height of the SVG element chart.charts.attr('width', chartWidth) - .attr('height', chartHeight + padding.bottom); + .attr('height', chartHeight + PADDING_BOTTOM); // ranges are based on the width and height available so reset chart.xScale.range([0, chartWidth]); chart.xScale2.range([0, chartWidth]); chart.xScaleBasals.range([0, chartWidth]); chart.yScale.range([focusHeight, 0]); - chart.yScale2.range([chartHeight, chartHeight - contextHeight]); + chart.yScale2.range([contextHeight, 0]); chart.yScaleBasals.range([0, focusHeight / 4]); if (init) { @@ -236,50 +366,49 @@ function init (client, d3, $) { .call(chart.yAxis); // if first run then just display axis with no transition + chart.context + .attr('transform', 'translate(0,' + focusHeight + ')') + chart.context.select('.x') - .attr('transform', 'translate(0,' + chartHeight + ')') + .attr('transform', 'translate(0,' + contextHeight + ')') .call(chart.xAxis2); -// chart.basals.select('.y') -// .attr('transform', 'translate(0,' + 0 + ')') -// .call(chart.yAxisBasals); - - chart.context.append('g') + chart.theBrush = chart.context.append('g') .attr('class', 'x brush') - .call(d3.svg.brush().x(chart.xScale2).on('brush', client.brushed)) - .selectAll('rect') - .attr('y', focusHeight) - .attr('height', chartHeight - focusHeight); + .call(chart.brush) + .call(g => g.select(".overlay") + .datum({ type: 'selection' }) + .on('mousedown touchstart', beforeBrushStarted)); + + chart.theBrush.selectAll('rect') + .attr('y', 0) + .attr('height', contextHeight) + .attr('width', '100%'); // disable resizing of brush - d3.select('.x.brush').select('.background').style('cursor', 'move'); - d3.select('.x.brush').select('.resize.e').style('cursor', 'move'); - d3.select('.x.brush').select('.resize.w').style('cursor', 'move'); - - // create a clipPath for when brushing - chart.clip = chart.charts.append('defs') - .append('clipPath') - .attr('id', 'clip') - .append('rect') - .attr('height', chartHeight) - .attr('width', chartWidth); + chart.context.select('.x.brush').select('.overlay').style('cursor', 'move'); + chart.context.select('.x.brush').selectAll('.handle') + .style('cursor', 'move'); + + chart.context.select('.x.brush').select('.selection') + .style('visibility', 'hidden'); // add a line that marks the current time chart.focus.append('line') .attr('class', 'now-line') .attr('x1', chart.xScale(new Date(client.now))) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) + .attr('y1', chart.yScale(focusYDomain[0])) .attr('x2', chart.xScale(new Date(client.now))) - .attr('y2', chart.yScale(utils.scaleMgdl(420))) + .attr('y2', chart.yScale(focusYDomain[1])) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); // add a y-axis line that shows the high bg threshold chart.focus.append('line') .attr('class', 'high-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) .style('stroke-dasharray', ('1, 6')) .attr('stroke', '#777'); @@ -287,9 +416,9 @@ function init (client, d3, $) { // add a y-axis line that shows the high bg threshold chart.focus.append('line') .attr('class', 'target-top-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -297,9 +426,9 @@ function init (client, d3, $) { // add a y-axis line that shows the low bg threshold chart.focus.append('line') .attr('class', 'target-bottom-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -307,9 +436,9 @@ function init (client, d3, $) { // add a y-axis line that shows the low bg threshold chart.focus.append('line') .attr('class', 'low-line') - .attr('x1', chart.xScale(dataRange[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) - .attr('x2', chart.xScale(dataRange[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) .style('stroke-dasharray', ('1, 6')) .attr('stroke', '#777'); @@ -318,7 +447,7 @@ function init (client, d3, $) { chart.context.append('line') .attr('class', 'open-top') .attr('stroke', '#111') - .attr('stroke-width', 12); + .attr('stroke-width', OPEN_TOP_HEIGHT); // add a x-axis line that closes the the brush container on left side chart.context.append('line') @@ -334,9 +463,9 @@ function init (client, d3, $) { chart.context.append('line') .attr('class', 'now-line') .attr('x1', chart.xScale(new Date(client.now))) - .attr('y1', chart.yScale2(utils.scaleMgdl(36))) + .attr('y1', chart.yScale2(contextYDomain[0])) .attr('x2', chart.xScale(new Date(client.now))) - .attr('y2', chart.yScale2(utils.scaleMgdl(420))) + .attr('y2', chart.yScale2(contextYDomain[1])) .style('stroke-dasharray', ('3, 3')) .attr('stroke', 'grey'); @@ -363,103 +492,82 @@ function init (client, d3, $) { } else { // for subsequent updates use a transition to animate the axis to the new position - var focusTransition = chart.focus.transition(); - focusTransition.select('.x') + chart.focus.select('.x') .attr('transform', 'translate(0,' + focusHeight + ')') .call(chart.xAxis); - focusTransition.select('.y') + chart.focus.select('.y') .attr('transform', 'translate(' + chartWidth + ', 0)') .call(chart.yAxis); - var contextTransition = chart.context.transition(); + chart.context + .attr('transform', 'translate(0,' + focusHeight + ')') - contextTransition.select('.x') - .attr('transform', 'translate(0,' + chartHeight + ')') + chart.context.select('.x') + .attr('transform', 'translate(0,' + contextHeight + ')') .call(chart.xAxis2); - chart.basals.transition(); - -// basalsTransition.select('.y') -// .attr('transform', 'translate(0,' + 0 + ')') -// .call(chart.yAxisBasals); - - if (chart.clip) { - // reset clip to new dimensions - chart.clip.transition() - .attr('width', chartWidth) - .attr('height', chartHeight); - } + chart.basals; // reset brush location - chart.context.select('.x.brush') - .selectAll('rect') - .attr('y', focusHeight) - .attr('height', chartHeight - focusHeight); + chart.theBrush.selectAll('rect') + .attr('y', 0) + .attr('height', contextHeight); - // clear current brushs - d3.select('.brush').call(chart.brush.clear()); + // console.log('chart.update(): Redrawing old brush with new dimensions: ', currentBrushExtent); // redraw old brush with new dimensions - d3.select('.brush').transition().call(chart.brush.extent(currentBrushExtent)); + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); // transition lines to correct location chart.focus.select('.high-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgHigh))); chart.focus.select('.target-top-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))); chart.focus.select('.target-bottom-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))); chart.focus.select('.low-line') - .transition() - .attr('x1', chart.xScale(currentBrushExtent[0])) + .attr('x1', chart.xScale.range()[0]) .attr('y1', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))) - .attr('x2', chart.xScale(currentBrushExtent[1])) + .attr('x2', chart.xScale.range()[1]) .attr('y2', chart.yScale(utils.scaleMgdl(client.settings.thresholds.bgLow))); // transition open-top line to correct location chart.context.select('.open-top') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[0])) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) - .attr('x2', chart.xScale2(currentBrushExtent[1])) - .attr('y2', chart.yScale(utils.scaleMgdl(30))); + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX)) + Math.floor(OPEN_TOP_HEIGHT/2.0)-1) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(utils.scaleMgdl(CONTEXT_MAX)) + Math.floor(OPEN_TOP_HEIGHT/2.0)-1); // transition open-left line to correct location chart.context.select('.open-left') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[0])) - .attr('y1', focusHeight) - .attr('x2', chart.xScale2(currentBrushExtent[0])) - .attr('y2', chartHeight); + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[0])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition open-right line to correct location chart.context.select('.open-right') - .transition() - .attr('x1', chart.xScale2(currentBrushExtent[1])) - .attr('y1', focusHeight) - .attr('x2', chart.xScale2(currentBrushExtent[1])) - .attr('y2', chartHeight); + .attr('x1', chart.xScale2(currentRange[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition high line to correct location chart.context.select('.high-line') - .transition() .attr('x1', chart.xScale2(dataRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetTop))) .attr('x2', chart.xScale2(dataRange[1])) @@ -467,7 +575,6 @@ function init (client, d3, $) { // transition low line to correct location chart.context.select('.low-line') - .transition() .attr('x1', chart.xScale2(dataRange[0])) .attr('y1', chart.yScale2(utils.scaleMgdl(client.settings.thresholds.bgTargetBottom))) .attr('x2', chart.xScale2(dataRange[1])) @@ -475,64 +582,83 @@ function init (client, d3, $) { } } - // update domain - chart.xScale2.domain(dataRange); + chart.updateContext(dataRange); + chart.xScaleBasals.domain(dataRange); - var updateBrush = d3.select('.brush').transition(); - updateBrush - .call(chart.brush.extent([new Date(dataRange[1].getTime() - client.foucusRangeMS), dataRange[1]])); - client.brushed(true); + // console.log('chart.update(): Redrawing brush due to update: ', currentBrushExtent); + + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); + }; + + chart.updateContext = function(dataRange_) { + if (client.documentHidden) { + console.info('Document Hidden, not updating - ' + (new Date())); + return; + } + + // get current data range + var dataRange = dataRange_ || client.dataExtent(); + + // update domain + chart.xScale2.domain(dataRange); renderer.addContextCircles(); // update x axis domain chart.context.select('.x').call(chart.xAxis2); + }; - }, DEBOUNCE_MS); + function scrollUpdate () { + var nowDate = scrollNow; - chart.scroll = function scroll (nowDate) { - chart.xScale.domain(createAdjustedRange()); - chart.yScale.domain(dynamicDomainOrElse(focusYDomain)); - chart.xScaleBasals.domain(createAdjustedRange()); + var currentBrushExtent = scrollBrushExtent; + var currentRange = scrollRange; + + chart.setForecastTime(); + + chart.xScale.domain(currentRange); + + focusYDomain = dynamicDomainOrElse(focusYDomain); + + chart.yScale.domain(focusYDomain); + chart.xScaleBasals.domain(currentRange); // remove all insulin/carb treatment bubbles so that they can be redrawn to correct location d3.selectAll('.path').remove(); // transition open-top line to correct location chart.context.select('.open-top') - .attr('x1', chart.xScale2(chart.brush.extent()[0])) - .attr('y1', chart.yScale(utils.scaleMgdl(30))) - .attr('x2', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y2', chart.yScale(utils.scaleMgdl(30))); + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[1]) + Math.floor(OPEN_TOP_HEIGHT / 2.0)-1) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1]) + Math.floor(OPEN_TOP_HEIGHT / 2.0)-1); // transition open-left line to correct location chart.context.select('.open-left') - .attr('x1', chart.xScale2(chart.brush.extent()[0])) - .attr('y1', chart.focusHeight) - .attr('x2', chart.xScale2(chart.brush.extent()[0])) - .attr('y2', chart.prevChartHeight); + .attr('x1', chart.xScale2(currentRange[0])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[0])) + .attr('y2', chart.yScale2(contextYDomain[1])); // transition open-right line to correct location chart.context.select('.open-right') - .attr('x1', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y1', chart.focusHeight) - .attr('x2', chart.xScale2(new Date(chart.brush.extent()[1].getTime() + client.forecastTime))) - .attr('y2', chart.prevChartHeight); + .attr('x1', chart.xScale2(currentRange[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentRange[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); chart.focus.select('.now-line') - .transition() .attr('x1', chart.xScale(nowDate)) - .attr('y1', chart.yScale(utils.scaleMgdl(36))) + .attr('y1', chart.yScale(focusYDomain[0])) .attr('x2', chart.xScale(nowDate)) - .attr('y2', chart.yScale(utils.scaleMgdl(420))); + .attr('y2', chart.yScale(focusYDomain[1])); chart.context.select('.now-line') - .transition() - .attr('x1', chart.xScale2(chart.brush.extent()[1])) - .attr('y1', chart.yScale2(utils.scaleMgdl(36))) - .attr('x2', chart.xScale2(chart.brush.extent()[1])) - .attr('y2', chart.yScale2(utils.scaleMgdl(420))); + .attr('x1', chart.xScale2(currentBrushExtent[1])) + .attr('y1', chart.yScale2(contextYDomain[0])) + .attr('x2', chart.xScale2(currentBrushExtent[1])) + .attr('y2', chart.yScale2(contextYDomain[1])); // update x,y axis chart.focus.select('.x.axis').call(chart.xAxis); @@ -541,22 +667,83 @@ function init (client, d3, $) { renderer.addBasals(client); renderer.addFocusCircles(); - renderer.addTreatmentCircles(); + renderer.addTreatmentCircles(nowDate); renderer.addTreatmentProfiles(client); + renderer.drawTreatments(client); + + // console.log('scrollUpdate(): Redrawing brush due to update: ', currentBrushExtent); - // add treatment bubbles - chart.focus.selectAll('circle') - .data(client.treatments) - .each(function (d) { - renderer.drawTreatment(d, { - scale: renderer.bubbleScale() - , showLabels: true - }); - }); + chart.theBrush.call(chart.brush.move, currentBrushExtent.map(chart.xScale2)); + scrolling = false; + } + + chart.scroll = function scroll (nowDate) { + scrollNow = nowDate; + scrollBrushExtent = chart.createBrushedRange(); + scrollRange = chart.createAdjustedRange(); + + if (!scrolling) { + requestAnimationFrame(scrollUpdate); + } + + scrolling = true; + }; + + chart.getMaxForecastMills = function getMaxForecastMills () { + // limit lookahead to the same as lookback + var selectedRange = chart.createBrushedRange(); + var to = selectedRange[1].getTime(); + return to + client.focusRangeMS; + }; + + chart.getForecastData = function getForecastData () { + + var maxForecastAge = chart.getMaxForecastMills(); + 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] || []); + }, [] ); + + return _.filter(points, function isShown (point) { + return point.mills < maxForecastAge; + }); + + }; + + chart.setForecastTime = function setForecastTime () { + + if (client.sbx.pluginBase.forecastPoints) { + var shownForecastPoints = chart.getForecastData(); + + // Get maximum time we will allow projected forward in time + // based on the number of hours the user has selected to show. + var maxForecastMills = chart.getMaxForecastMills(); + + var selectedRange = chart.createBrushedRange(); + var to = selectedRange[1].getTime(); + + // Default min forecast projection times to the default amount of time to forecast + var minForecastMills = to + client.defaultForecastTime; + var availForecastMills = 0; + + // Determine what the maximum forecast time is that is available in the forecast data + if (shownForecastPoints.length > 0) { + availForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills })); + } + + // Limit the amount shown to the maximum time allowed to be projected forward based + // on the number of hours the user has selected to show + var forecastMills = Math.min(availForecastMills, maxForecastMills); + + // Don't allow the forecast time to go below the minimum forecast time + client.forecastTime = Math.max(forecastMills, minForecastMills); + } }; return chart; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/client/clock-client.js b/lib/client/clock-client.js new file mode 100644 index 00000000000..e5294e9bd6f --- /dev/null +++ b/lib/client/clock-client.js @@ -0,0 +1,216 @@ +'use strict'; + +var browserSettings = require('./browser-settings'); +var client = {}; +var latestProperties = {}; + +client.query = function query () { + var parts = (location.search || '?').substring(1).split('&'); + var token = ''; + parts.forEach(function(val) { + if (val.startsWith('token=')) { + token = val.substring('token='.length); + } + }); + + var secret = localStorage.getItem('apisecrethash'); + var src = '/api/v2/properties'; // Use precalculated data from the backend + + if (secret) { + var s = '?secret=' + secret; + src += s; + } else if (token) { + var s2 = '?token=' + token; + src += s2; + } + + $.ajax(src, { + error: function gotError (err) { + console.error(err); + } + , success: function gotData (data) { + latestProperties = data; + client.render(); + } + }); +}; + +client.render = function render () { + + if (!latestProperties.bgnow && !latestProperties.bgnow.sgvs) { + console.error('BG data not available'); + return; + } + + let rec = latestProperties.bgnow.sgvs[0]; + let deltaDisplayValue; + + if (latestProperties.delta) { + deltaDisplayValue = latestProperties.delta.display; + } + + let $errorMessage = $('#errorMessage'); + let $inner = $('#inner'); + + // If no one measured value found => show "-?-" + if (!rec) { + if (!$errorMessage.length) { + $inner.after('
-?-
') + } else { + $errorMessage.show(); + } + $inner.hide(); + return; + } else { + $errorMessage.length && $errorMessage.hide(); + $inner.show(); + } + + //Parse face parameters + let face = $inner.data('face').toLowerCase(); + + // Backward compatible + if (face === 'clock-color') { + face = 'c' + (window.serverSettings.settings.showClockLastTime ? 'y' : 'n') + '13-sg35-' + (window.serverSettings.settings.showClockDelta ? 'dt14-' : '') + 'nl-ar25-nl-ag6'; + } else if (face === 'clock') { + face = 'bn0-sg40'; + } else if (face === 'bgclock') { + face = 'b' + (window.serverSettings.settings.showClockLastTime ? 'y' : 'n') + '13-sg35-' + (window.serverSettings.settings.showClockDelta ? 'dt14-' : '') + 'nl-ar25-nl-ag6'; + } else if (face === 'config') { + face = $inner.attr('data-face-config'); + $inner.empty(); + } + + let faceParams = face.split('-'); + let bgColor = false; + let staleMinutes = 13; + let alwaysShowTime = false; + + let clockCreated = ($inner.children().length > 0); + + for (let param in faceParams) { + if (param === '0') { + /* 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) { + /* eslint-disable-next-line security/detect-object-injection */ // verified false positive + let faceParam = faceParams[param]; + let div = '
0) ? ' style="' + ((faceParam.substr(0, 2) === 'ar') ? 'height' : 'font-size') + ':' + faceParam.substr(2, 2) + 'vmin"' : '') + '>
'; + $inner.append(div); + } + } + + let displayValue = rec.scaled; + + // Insert the delta value text. + $('.dt').html(deltaDisplayValue); + + // Color background + if (bgColor) { + + // These are the particular shades of red, yellow, green, and blue. + let red = 'rgba(213,9,21,1)'; + let yellow = 'rgba(234,168,0,1)'; + let green = 'rgba(134,207,70,1)'; + let blue = 'rgba(78,143,207,1)'; + + // Threshold values + let bgHigh = client.settings.thresholds.bgHigh; + let bgLow = client.settings.thresholds.bgLow; + let bgTargetBottom = client.settings.thresholds.bgTargetBottom; + let bgTargetTop = client.settings.thresholds.bgTargetTop; + + let bgNum = parseFloat(rec.mgdl); + + // Threshold background coloring. + if (bgNum < bgLow) { + $('body').css('background-color', red); + } + if ((bgLow <= bgNum) && (bgNum < bgTargetBottom)) { + $('body').css('background-color', blue); + } + if ((bgTargetBottom <= bgNum) && (bgNum < bgTargetTop)) { + $('body').css('background-color', green); + } + if ((bgTargetTop <= bgNum) && (bgNum < bgHigh)) { + $('body').css('background-color', yellow); + } + if (bgNum >= bgHigh) { + $('body').css('background-color', red); + } + + } else { + $('body').css('background-color', 'black'); + } + + // Time before data considered stale. + let threshold = 1000 * 60 * staleMinutes; + + var elapsedms = Date.now() - rec.mills; + let elapsedMins = Math.floor((elapsedms / 1000) / 60); + let thresholdReached = (elapsedms > threshold) && threshold > 0; + + // Insert the BG value text, toggle stale if necessary. + $('.sg').toggleClass('stale', thresholdReached).html(displayValue); + + if (thresholdReached || alwaysShowTime) { + let staleTimeText; + if (elapsedMins === 0) { + staleTimeText = 'Just now'; + } else if (elapsedMins === 1) { + staleTimeText = '1 minute ago'; + } else { + staleTimeText = elapsedMins + ' minutes ago'; + } + + $('.ag').html(staleTimeText); + } else { + $('.ag').html(''); + } + + // Insert the trend arrow. + let arrow = $('arrow').attr('src', '/images/' + (!rec.direction || rec.direction === 'NOT COMPUTABLE' ? 'NONE' : rec.direction) + '.svg'); + + // Restyle body bg + if (thresholdReached) { + $('body').css('background-color', 'grey').css('color', 'black'); + $('.ar').css('filter', 'brightness(0%)').html(arrow); + } else { + $('body').css('color', bgColor ? 'white' : 'grey'); + $('.ar').css('filter', bgColor ? 'brightness(100%)' : 'brightness(50%)').html(arrow); + } + + updateClock(); + +}; + +function updateClock () { + let timeDivisor = parseInt(client.settings.timeFormat ? client.settings.timeFormat : 12, 10); + let today = new Date() + , h = today.getHours() % timeDivisor; + if (timeDivisor === 12) { + h = (h === 0) ? 12 : h; // In the case of 00:xx, change to 12:xx for 12h time + } + if (timeDivisor === 24) { + h = (h < 10) ? ("0" + h) : h; // Pad the hours with a 0 in 24h time + } + let m = today.getMinutes(); + if (m < 10) m = "0" + m; + $('.tm').html(h + ":" + m); +} + +client.init = function init () { + + console.log('Initializing clock'); + client.settings = browserSettings(client, window.serverSettings, $); + client.query(); + setInterval(client.query, 20 * 1000); // update every 20 seconds + + // time update + setInterval(updateClock, 1000); +}; + +module.exports = client; diff --git a/lib/d3locales.js b/lib/client/d3locales.js similarity index 90% rename from lib/d3locales.js rename to lib/client/d3locales.js index afb04cbad5c..0e050edf380 100644 --- a/lib/d3locales.js +++ b/lib/client/d3locales.js @@ -142,6 +142,21 @@ d3locales.it_IT = { shortMonths: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'] }; +d3locales.pl_PL = { + decimal: '.', + thousands: ',', + grouping: [3], + currency: ['', 'zł'], + dateTime: '%a %b %e %X %Y', + date: '%d.%m.%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], // unused + days: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'], + shortDays: ['Nie', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'So'], + months: ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'], + shortMonths: ['Sty', 'Lu', 'Mar', 'Kw', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Pa', 'Lis', 'Gru'] +}; + d3locales.pt_BR = { decimal: ',', thousands: '.', @@ -212,15 +227,21 @@ d3locales.locale = function locale (language) { , fr: 'fr_FR' , he: 'he_IL' , it: 'it_IT' + , pl: 'pl_PL' , pt: 'pt_BR' , ro: 'ro_RO' , ru: 'ru_RU' ,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]; }; diff --git a/lib/client/hashauth.js b/lib/client/hashauth.js new file mode 100644 index 00000000000..1f9b101877a --- /dev/null +++ b/lib/client/hashauth.js @@ -0,0 +1,266 @@ +'use strict'; + +var crypto = require('crypto'); +var Storages = require('js-storage'); + +var hashauth = { + initialized: false +}; + +hashauth.init = function init (client, $) { + + hashauth.apisecret = ''; + hashauth.storeapisecret = false; + hashauth.apisecrethash = null; + hashauth.authenticated = false; + hashauth.tokenauthenticated = false; + hashauth.hasReadPermission = false; + hashauth.isAdmin = false; + hashauth.hasWritePermission = false; + hashauth.permissionlevel = 'NONE'; + + hashauth.verifyAuthentication = function verifyAuthentication (next) { + hashauth.authenticated = false; + $.ajax({ + method: 'GET' + , url: '/api/v1/verifyauth?t=' + Date.now() //cache buster + , headers: client.headers() + }).done(function verifysuccess (response) { + + + var message = response.message; + + if (message.canRead) { hashauth.hasReadPermission = true; } + if (message.canWrite) { hashauth.hasWritePermission = true; } + if (message.isAdmin) { hashauth.isAdmin = true; } + if (message.permissions) { hashauth.permissionlevel = message.permissions; } + + if (message.rolefound == 'FOUND') { + hashauth.tokenauthenticated = true; + console.log('Token Authentication passed.'); + next(true); + return; + } + + if (response.message === 'OK' || message.message === 'OK') { + hashauth.authenticated = true; + console.log('Authentication passed.'); + next(true); + return; + } + + console.log('Authentication failed!', response); + hashauth.removeAuthentication(); + next(false); + return; + + }).fail(function verifyfail (err) { + console.log('Authentication failure', err); + hashauth.removeAuthentication(); + next(false); + }); + }; + + hashauth.injectHtml = function injectHtml () { + if (!hashauth.injectedHtml) { + $('#authentication_placeholder').html(hashauth.inlineCode()); + hashauth.injectedHtml = true; + } + }; + + hashauth.initAuthentication = function initAuthentication (next) { + hashauth.apisecrethash = hashauth.apisecrethash || Storages.localStorage.get('apisecrethash') || null; + hashauth.verifyAuthentication(function() { + hashauth.injectHtml(); + if (next) { next(hashauth.isAuthenticated()); } + }); + return hashauth; + }; + + hashauth.removeAuthentication = function removeAuthentication (event) { + + Storages.localStorage.remove('apisecrethash'); + + if (hashauth.authenticated || hashauth.tokenauthenticated) { + client.browserUtils.reload(); + } + + // clear everything just in case + hashauth.apisecret = null; + hashauth.apisecrethash = null; + hashauth.authenticated = false; + + if (event) { + event.preventDefault(); + } + return false; + }; + + hashauth.requestAuthentication = function requestAuthentication (eventOrNext) { + var translate = client.translate; + hashauth.injectHtml(); + + var clientWidth = window.innerWidth || + document.documentElement.clientWidth || + document.body.clientWidth; + + clientWidth = Math.min(400, clientWidth); + + $('#requestauthenticationdialog').dialog({ + width: clientWidth + , height: 270 + , closeText: '' + , buttons: [ + { + id: 'requestauthenticationdialog-btn' + , text: translate('Authenticate') + , click: function() { + var dialog = this; + hashauth.processSecret($('#apisecret').val(), $('#storeapisecret').is(':checked'), function done (close) { + if (close) { + if (eventOrNext && eventOrNext.call) { + eventOrNext(true); + } else { + client.afterAuth(true); + } + $(dialog).dialog('close'); + } else { + $('#apisecret').val('').focus(); + } + }); + } + } + ] + , open: function open () { + $('#apisecret').off('keyup').on('keyup', function pressed (e) { + if (e.keyCode === $.ui.keyCode.ENTER) { + $('#requestauthenticationdialog-btn').trigger('click'); + } + }); + $('#apisecret').val('').focus(); + } + + }); + + if (eventOrNext && eventOrNext.preventDefault) { + eventOrNext.preventDefault(); + } + return false; + }; + + hashauth.processSecret = function processSecret (apisecret, storeapisecret, callback) { + var translate = client.translate; + + hashauth.apisecret = apisecret; + hashauth.storeapisecret = storeapisecret; + if (!hashauth.apisecret || hashauth.apisecret.length < 12) { + window.alert(translate('Too short API secret')); + if (callback) { + callback(false); + } + } else { + var shasum = crypto.createHash('sha1'); + shasum.update(hashauth.apisecret); + hashauth.apisecrethash = shasum.digest('hex'); + + hashauth.verifyAuthentication(function(isok) { + if (isok) { + if (hashauth.storeapisecret) { + Storages.localStorage.set('apisecrethash', hashauth.apisecrethash); + // TODO show dialog first, then reload + if (hashauth.tokenauthenticated) client.browserUtils.reload(); + } + $('#authentication_placeholder').html(hashauth.inlineCode()); + if (callback) { + callback(true); + } + } else { + alert(translate('Wrong API secret')); + if (callback) { + callback(false); + } + } + }); + } + }; + + hashauth.inlineCode = function inlineCode () { + var translate = client.translate; + + var status = null; + + if (!hashauth.isAdmin) { + $('.needsadminaccess').hide(); + } else { + $('.needsadminaccess').show(); + } + + if (client.updateAdminMenu) client.updateAdminMenu(); + + if (client.authorized || hashauth.tokenauthenticated) { + status = translate('Authorized by token'); + if (client.authorized && client.authorized.sub) { + status += '
' + translate('Auth role') + ': ' + client.authorized.sub; + if (hashauth.hasReadPermission) { status += '
' + translate('Data reads enabled'); } + if (hashauth.hasWritePermission) { status += '
' + translate('Data writes enabled'); } + if (!hashauth.hasWritePermission) { status += '
' + translate('Data writes not enabled'); } + } + if (hashauth.apisecrethash) { + status += '
(' + translate('Remove stored token') + ')'; + } else { + status += '
(' + translate('view without token') + ')'; + } + + } else if (hashauth.isAuthenticated()) { + status = translate('Admin authorized') + ' (' + translate('Remove') + ')'; + } else { + status = translate('Unauthorized') + + '
' + + translate('Reads enabled in default permissions') + + '
' + + ' (' + + translate('Authenticate') + ')'; + } + + var html = + '' + + '
' + status + '
'; + + return html; + }; + + hashauth.updateSocketAuth = function updateSocketAuth () { + client.socket.emit( + 'authorize' + , { + client: 'web' + , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() + , token: client.authorized && client.authorized.token + } + , function authCallback (data) { + if (!data.read && !client.authorized) { + hashauth.requestAuthentication(); + } + } + ); + }; + + hashauth.hash = function hash () { + return hashauth.apisecrethash; + }; + + hashauth.isAuthenticated = function isAuthenticated () { + return hashauth.authenticated || hashauth.tokenauthenticated; + }; + + hashauth.initialized = true; + + return hashauth; +} + +module.exports = hashauth; diff --git a/lib/client/index.js b/lib/client/index.js index 8db328840e0..c4c83b32a01 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -3,114 +3,348 @@ var _ = require('lodash'); var $ = (global && global.$) || require('jquery'); var d3 = (global && global.d3) || require('d3'); +var shiroTrie = require('shiro-trie'); + +var Storages = require('js-storage'); var language = require('../language')(); var sandbox = require('../sandbox')(); -var profile = require('../profilefunctions')(); var units = require('../units')(); var levels = require('../levels'); var times = require('../times'); +var receiveDData = require('./receiveddata'); + +var brushing = false; + +var browserSettings; +var moment = window.moment; +var timezones = moment.tz.names(); + +var client = {}; + +var hashauth = require('./hashauth'); +client.hashauth = hashauth.init(client, $); + +$('#loadingMessageText').html('Connecting to server'); + +client.headers = function headers () { + if (client.authorized) { + return { + Authorization: 'Bearer ' + client.authorized.token + }; + } else if (client.hashauth) { + return { + 'api-secret': client.hashauth.hash() + }; + } else { + return {}; + } +}; + +client.crashed = function crashed () { + $('#centerMessagePanel').show(); + $('#loadingMessageText').html('It appears the server has crashed. Please go to Heroku or Azure and reboot the server.'); +} + +client.init = function init (callback) { + + client.browserUtils = require('./browser-utils')($); + + var token = client.browserUtils.queryParms().token; + var secret = client.hashauth.apisecrethash || Storages.localStorage.get('apisecrethash'); + + var src = '/api/v1/status.json?t=' + new Date().getTime(); -var client = { }; + if (secret) { + src += '&secret=' + secret; + } else if (token) { + src += '&token=' + token; + } -client.init = function init(serverSettings, plugins) { + $.ajax({ + method: 'GET' + , url: src + , headers: client.headers() + }).done(function success (serverSettings) { + if (serverSettings.runtimeState !== 'loaded') { + console.log('Server is still loading data'); + $('#loadingMessageText').html('Server is starting and still loading data, retrying load in 5 seconds'); + window.setTimeout(window.Nightscout.client.init, 5000); + return; + } + client.settingsFailed = false; + client.loadLanguage(serverSettings, callback); + }).fail(function fail (jqXHR) { + + // check if we couldn't reach the server at all, show offline message + if (!jqXHR.readyState) { + console.log('Application appears to be OFFLINE'); + $('#loadingMessageText').html('Connecting to Nightscout server failed, retrying every 5 seconds'); + window.setTimeout(window.Nightscout.client.init(), 5000); + return; + } - var UPDATE_TRANS_MS = 750 // milliseconds - , FORMAT_TIME_12 = '%-I:%M %p' + //no server setting available, use defaults, auth, etc + if (client.settingsFailed) { + console.log('Already tried to get settings after auth, but failed'); + } else { + client.settingsFailed = true; + + // detect browser language + var lang = Storages.localStorage.get('language') || (navigator.language || navigator.userLanguage).toLowerCase(); + if (lang !== 'zh_cn' && lang !== 'zh-cn' && lang !== 'zh_tw' && lang !== 'zh-tw') { + lang = lang.substring(0, 2); + } else { + lang = lang.replace('-', '_'); + } + if (language.languages.find(l => l.code === lang)) { + language.set(lang); + } else { + language.set('en'); + } + + client.translate = language.translate; + // auth failed, hide loader and request for key + $('#centerMessagePanel').hide(); + client.hashauth.requestAuthentication(function afterRequest () { + window.setTimeout(client.init(callback), 5000); + }); + } + }); + +}; + +client.loadLanguage = function loadLanguage (serverSettings, callback) { + + $('#loadingMessageText').html('Loading language file'); + + browserSettings = require('./browser-settings'); + client.settings = browserSettings(client, serverSettings, $); + console.log('language is', client.settings.language); + + let filename = language.getFilename(client.settings.language); + + $.ajax({ + method: 'GET' + , url: '/translations/' + filename + }).done(function success (localization) { + language.offerTranslations(localization); + console.log('Application appears to be online'); + $('#centerMessagePanel').hide(); + client.load(serverSettings, callback); + }).fail(function fail () { + console.error('Loading localization failed, continuing with English'); + console.log('Application appears to be online'); + $('#centerMessagePanel').hide(); + client.load(serverSettings, callback); + }); + +} + +client.load = function load (serverSettings, callback) { + + var FORMAT_TIME_12 = '%-I:%M %p' , FORMAT_TIME_12_COMPACT = '%-I:%M' , FORMAT_TIME_24 = '%H:%M%' , FORMAT_TIME_12_SCALE = '%-I %p' - , FORMAT_TIME_24_SCALE = '%H' - ; + , FORMAT_TIME_24_SCALE = '%H'; + + var history = 48; var chart , socket + , alarmSocket , isInitialData = false - , SGVdata = [] - , MBGdata = [] - , latestUpdateTime - , prevSGV - , devicestatusData - , opacity = {current: 1, DAY: 1, NIGHT: 0.5} + , opacity = { current: 1, DAY: 1, NIGHT: 0.5 } , clientAlarms = {} , alarmInProgress = false , alarmMessage - , currentAlarmType = null + , currentNotify , currentAnnouncement , alarmSound = 'alarm.mp3' , urgentAlarmSound = 'alarm2.mp3' - ; + , previousNotifyTimestamp; - client.entryToDate = function entryToDate (entry) { return new Date(entry.mills) }; + client.entryToDate = function entryToDate (entry) { + if (entry.date) return entry.date; + entry.date = new Date(entry.mills); + return entry.date; + }; client.now = Date.now(); - client.forecastTime = times.mins(30).msecs; - client.data = []; - client.browserUtils = require('./browser-utils')($); - client.settings = require('./browser-settings')(client, plugins, serverSettings, $); - client.utils = require('../utils')(client.settings); + client.dataLastUpdated = 0; + client.lastPluginUpdateTime = 0; + client.ddata = require('../data/ddata')(); + client.defaultForecastTime = times.mins(30).msecs; + client.forecastTime = client.now + client.defaultForecastTime; + client.entries = []; client.ticks = require('./ticks'); - client.sbx = sandbox.clientInit(client.settings, client.now); - client.rawbg = plugins('rawbg'); - client.delta = plugins('delta'); - client.direction = plugins('direction'); - client.errorcodes = plugins('errorcodes'); - - language.set(client.settings.language).DOMtranslate($); - client.translate = language.translate; - - client.hashauth = require('../hashauth'); - client.hashauth.init(client, $).initAuthentication(); + //containers + var container = $('.container') + , bgStatus = $('.bgStatus') + , currentBG = $('.bgStatus .currentBG') + , majorPills = $('.bgStatus .majorPills') + , minorPills = $('.bgStatus .minorPills') + , statusPills = $('.status .statusPills') + , primary = $('.primary') + , editButton = $('#editbutton'); client.tooltip = d3.select('body').append('div') .attr('class', 'tooltip') .style('opacity', 0); - client.foucusRangeMS = times.hours(3).msecs; + client.settings = browserSettings(client, serverSettings, $); + + language.set(client.settings.language).DOMtranslate($); + client.translate = language.translate; + client.language = language; + + client.plugins = require('../plugins/')({ + settings: client.settings + , extendedSettings: client.settings.extendedSettings + , language: language + , levels: levels + , moment: moment + }).registerClientDefaults(); + + browserSettings.loadPluginSettings(client); + + client.utils = require('../utils')({ + settings: client.settings + , language: language + , moment: moment + }); + + client.rawbg = client.plugins('rawbg'); + client.delta = client.plugins('delta'); + client.timeago = client.plugins('timeago'); + client.direction = client.plugins('direction'); + client.errorcodes = client.plugins('errorcodes'); + + client.ctx = { + data: {} + , bus: require('../bus')(client.settings, client.ctx) + , settings: client.settings + , pluginBase: client.plugins.base(majorPills, minorPills, statusPills, bgStatus, client.tooltip, Storages.localStorage) + , moment: moment + , timezones: timezones + }; + + client.ctx.language = language; + levels.translate = language.translate; + client.ctx.levels = levels; + + client.ctx.notifications = require('../notifications')(client.settings, client.ctx); + + client.sbx = sandbox.clientInit(client.ctx, client.now); + client.renderer = require('./renderer')(client, d3, $); + + //After plugins are initialized with browser settings; + browserSettings.loadAndWireForm(); + + client.adminnotifies = require('./adminnotifiesclient')(client, $); + + if (serverSettings && serverSettings.authorized) { + client.authorized = serverSettings.authorized; + client.authorized.lat = Date.now(); + client.authorized.shiros = _.map(client.authorized.permissionGroups, function toShiro (group) { + var shiro = shiroTrie.new(); + _.forEach(group, function eachPermission (permission) { + shiro.add(permission); + }); + return shiro; + }); + + client.authorized.check = function check (permission) { + var found = _.find(client.authorized.shiros, function checkEach (shiro) { + return shiro.check(permission); + }); + + return _.isObject(found); + }; + } + + client.afterAuth = function afterAuth (isAuthenticated) { + + var treatmentCreateAllowed = client.authorized ? client.authorized.check('api:treatments:create') : isAuthenticated; + var treatmentUpdateAllowed = client.authorized ? client.authorized.check('api:treatments:update') : isAuthenticated; + + $('#lockedToggle').click(client.hashauth.requestAuthentication).toggle(!treatmentCreateAllowed && client.settings.showPlugins.indexOf('careportal') > -1); + $('#treatmentDrawerToggle').toggle(treatmentCreateAllowed && client.settings.showPlugins.indexOf('careportal') > -1); + $('#boluscalcDrawerToggle').toggle(treatmentCreateAllowed && client.settings.showPlugins.indexOf('boluscalc') > -1); + + if (isAuthenticated) client.notifies.updateAdminNotifies(); + + // Edit mode + editButton.toggle(client.settings.editMode && treatmentUpdateAllowed); + editButton.click(function editModeClick (event) { + client.editMode = !client.editMode; + if (client.editMode) { + client.renderer.drawTreatments(client); + editButton.find('i').addClass('selected'); + } else { + chart.focus.selectAll('.draggable-treatment') + .style('cursor', 'default') + .on('mousedown.drag', null); + editButton.find('i').removeClass('selected'); + } + if (event) { event.preventDefault(); } + }); + }; + + client.hashauth.initAuthentication(client.afterAuth); + + client.focusRangeMS = times.hours(client.settings.focusHours).msecs; + $('.focus-range li[data-hours=' + client.settings.focusHours + ']').addClass('selected'); client.brushed = brushed; client.formatTime = formatTime; client.dataUpdate = dataUpdate; - client.renderer = require('./renderer')(client, d3, $); client.careportal = require('./careportal')(client, $); - client.boluscalc = require('./boluscalc')(client, $, plugins); + client.boluscalc = require('./boluscalc')(client, $); + + var profile = require('../profilefunctions')(null, client.ctx); client.profilefunctions = profile; - - var timeAgo = client.utils.timeAgo; - var container = $('.container') - , bgStatus = $('.bgStatus') - , currentBG = $('.bgStatus .currentBG') - , majorPills = $('.bgStatus .majorPills') - , minorPills = $('.bgStatus .minorPills') - , statusPills = $('.status .statusPills') - ; + client.editMode = false; + + //TODO: use the bus for updates and notifications + //client.ctx.bus.on('tick', function timedReload (tick) { + // console.info('tick', tick.now); + //}); + //broadcast 'tock' event each minute, start a new setTimeout each time it fires make it happen on the minute + //see updateClock + //start the bus after setting up listeners + //client.ctx.bus.uptime( ); - client.dataExtent = function dataExtent ( ) { - return client.data.length > 0 ? - d3.extent(client.data, client.entryToDate) - : d3.extent([new Date(client.now - times.hours(48).msecs), new Date(client.now)]); + client.dataExtent = function dataExtent () { + if (client.entries.length > 0) { + return [client.entryToDate(client.entries[0]), client.entryToDate(client.entries[client.entries.length - 1])]; + } else { + return [new Date(client.now - times.hours(history).msecs), new Date(client.now)]; + } }; - client.bottomOfPills = function bottomOfPills ( ) { + client.bottomOfPills = function bottomOfPills () { //the offset's might not exist for some tests + var bottomOfPrimary = primary.offset() ? primary.offset().top + primary.height() : 0; var bottomOfMinorPills = minorPills.offset() ? minorPills.offset().top + minorPills.height() : 0; var bottomOfStatusPills = statusPills.offset() ? statusPills.offset().top + statusPills.height() : 0; - return Math.max(bottomOfMinorPills, bottomOfStatusPills); + return Math.max(bottomOfPrimary, bottomOfMinorPills, bottomOfStatusPills); }; - function formatTime(time, compact) { + function formatTime (time, compact) { var timeFormat = getTimeFormat(false, compact); - time = d3.time.format(timeFormat)(time); + time = d3.timeFormat(timeFormat)(time); if (client.settings.timeFormat !== 24) { time = time.toLowerCase(); } return time; } - function getTimeFormat(isForScale, compact) { + function getTimeFormat (isForScale, compact) { var timeFormat = FORMAT_TIME_12; if (client.settings.timeFormat === 24) { timeFormat = isForScale ? FORMAT_TIME_24_SCALE : FORMAT_TIME_24; @@ -122,7 +356,7 @@ client.init = function init(serverSettings, plugins) { } //TODO: replace with utils.scaleMgdl and/or utils.roundBGForDisplay - function scaleBg(bg) { + function scaleBg (bg) { if (client.settings.units === 'mmol') { return units.mgdlToMMOL(bg); } else { @@ -130,35 +364,38 @@ client.init = function init(serverSettings, plugins) { } } - function generateTitle ( ) { - function s(value, sep) { return value ? value + ' ' : sep || ''; } + function generateTitle () { + function s (value, sep) { return value ? value + ' ' : sep || ''; } var title = ''; - var time = client.latestSGV ? client.latestSGV.mills : (prevSGV ? prevSGV.mills : -1) - , ago = timeAgo(time); + var status = client.timeago.checkStatus(client.sbx); - if (ago && ago.status !== 'current') { - title = s(ago.value) + s(ago.label, ' - ') + title; + if (status !== 'current') { + var ago = client.timeago.calcDisplay(client.sbx.lastSGVEntry(), client.sbx.time); + title = s(ago.value) + s(ago.label, ' - ') + title; } else if (client.latestSGV) { var currentMgdl = client.latestSGV.mgdl; if (currentMgdl < 39) { title = s(client.errorcodes.toDisplay(currentMgdl), ' - ') + title; } else { - var deltaDisplay = client.delta.calc(prevSGV, client.latestSGV, client.sbx).display; - title = s(scaleBg(currentMgdl)) + s(deltaDisplay) + s(client.direction.info(client.latestSGV).label) + title; + var delta = client.nowSBX.properties.delta; + if (delta) { + var deltaDisplay = delta.display; + title = s(scaleBg(currentMgdl)) + s(deltaDisplay) + s(client.direction.info(client.latestSGV).label) + title; + } } } return title; } - function resetCustomTitle ( ) { + function resetCustomTitle () { var customTitle = client.settings.customTitle || 'Nightscout'; $('.customTitle').text(customTitle); } - function checkAnnouncement() { + function checkAnnouncement () { var result = { inProgress: currentAnnouncement ? Date.now() - currentAnnouncement.received < times.mins(5).msecs : false }; @@ -175,19 +412,19 @@ client.init = function init(serverSettings, plugins) { return result; } - function updateTitle ( ) { + function updateTitle () { var windowTitle; var announcementStatus = checkAnnouncement(); if (alarmMessage && alarmInProgress) { $('.customTitle').text(alarmMessage); - if (!isTimeAgoAlarmType(currentAlarmType)) { + if (!isTimeAgoAlarmType()) { windowTitle = alarmMessage + ': ' + generateTitle(); } } else if (announcementStatus.inProgress && announcementStatus.message) { windowTitle = announcementStatus.message + ': ' + generateTitle(); - } else { + } else { resetCustomTitle(); } @@ -196,50 +433,74 @@ client.init = function init(serverSettings, plugins) { $(document).attr('title', windowTitle || generateTitle()); } -// clears the current user brush and resets to the current real time data - function updateBrushToNow(skipBrushing) { - - // get current time range - var dataRange = client.dataExtent(); + // clears the current user brush and resets to the current real time data + function updateBrushToNow (skipBrushing) { // update brush and focus chart with recent data - d3.select('.brush') - .transition() - .duration(UPDATE_TRANS_MS) - .call(chart.brush.extent([new Date(dataRange[1].getTime() - client.foucusRangeMS), dataRange[1]])); + var brushExtent = client.dataExtent(); + + brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); + + // console.log('updateBrushToNow(): Resetting brush: ', brushExtent); + + if (chart.theBrush) { + chart.theBrush.call(chart.brush) + chart.theBrush.call(chart.brush.move, brushExtent.map(chart.xScale2)); + } if (!skipBrushing) { brushed(); } } - function alarmingNow() { + function alarmingNow () { return container.hasClass('alarming'); } - function inRetroMode() { + function inRetroMode () { return chart && chart.inRetroMode(); } - function brushed ( ) { + function brushed () { + // Brush not initialized + if (!chart.theBrush) { + return; + } + + if (brushing) { + return; + } + + brushing = true; - var brushExtent = chart.brush.extent(); + // default to most recent focus period + var brushExtent = client.dataExtent(); + brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); + + var brushedRange = d3.brushSelection(chart.theBrush.node()); + + // console.log("brushed(): coordinates: ", brushedRange); + + if (brushedRange) { + brushExtent = brushedRange.map(chart.xScale2.invert); + } - // ensure that brush extent is fixed at 3.5 hours - if (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.foucusRangeMS) { + // console.log('brushed(): Brushed to: ', brushExtent); + + if (!brushedRange || (brushExtent[1].getTime() - brushExtent[0].getTime() !== client.focusRangeMS)) { // ensure that brush updating is with the time range - if (brushExtent[0].getTime() + client.foucusRangeMS > client.dataExtent()[1].getTime()) { - brushExtent[0] = new Date(brushExtent[1].getTime() - client.foucusRangeMS); - d3.select('.brush') - .call(chart.brush.extent([brushExtent[0], brushExtent[1]])); + if (brushExtent[0].getTime() + client.focusRangeMS > client.dataExtent()[1].getTime()) { + brushExtent[0] = new Date(brushExtent[1].getTime() - client.focusRangeMS); } else { - brushExtent[1] = new Date(brushExtent[0].getTime() + client.foucusRangeMS); - d3.select('.brush') - .call(chart.brush.extent([brushExtent[0], brushExtent[1]])); + brushExtent[1] = new Date(brushExtent[0].getTime() + client.focusRangeMS); } + + // console.log('brushed(): updating to: ', brushExtent); + + chart.theBrush.call(chart.brush.move, brushExtent.map(chart.xScale2)); } - function adjustCurrentSGVClasses(value, isCurrent) { + function adjustCurrentSGVClasses (value, isCurrent) { var reallyCurrentAndNotAlarming = isCurrent && !inRetroMode() && !alarmingNow(); bgStatus.toggleClass('current', alarmingNow() || reallyCurrentAndNotAlarming); @@ -252,13 +513,11 @@ client.init = function init(serverSettings, plugins) { currentBG.toggleClass('icon-hourglass', value === 9); currentBG.toggleClass('error-code', value < 39); currentBG.toggleClass('bg-limit', value === 39 || value > 400); - container.removeClass('loading'); } function updateCurrentSGV (entry) { var value = entry.mgdl - , ago = timeAgo(entry.mills) - , isCurrent = ago.status === 'current'; + , isCurrent = 'current' === client.timeago.checkStatus(client.sbx); if (value === 9) { currentBG.text(''); @@ -275,43 +534,104 @@ client.init = function init(serverSettings, plugins) { adjustCurrentSGVClasses(value, isCurrent); } - function updatePlugins (sgvs, time) { - var pluginBase = plugins.base(majorPills, minorPills, statusPills, bgStatus, client.tooltip, $.localStorage); + function mergeDeviceStatus (retro, ddata) { + if (!retro) { + return ddata; + } + + var result = retro.map(x => Object.assign(x, ddata.find(y => y._id == x._id))); + + var missingInRetro = ddata.filter(y => !retro.find(x => x._id == y._id)); + + result.push(...missingInRetro); + + return result; + } + + function updatePlugins (time) { + + if (time > client.lastPluginUpdateTime && time > client.dataLastUpdated) { + if ((time - client.lastPluginUpdateTime) < 1000) { + return; // Don't update the plugins more than once a second + } + client.lastPluginUpdateTime = time; + } + + //TODO: doing a clone was slow, but ok to let plugins muck with data? + //var ddata = client.ddata.clone(); + + client.ddata.inRetroMode = inRetroMode(); + client.ddata.profile = profile; + + // retro data only ever contains device statuses + // Cleate a clone of the data for the sandbox given to plugins + + var mergedStatuses = client.ddata.devicestatus; + + if (client.retro.data) { + mergedStatuses = mergeDeviceStatus(client.retro.data.devicestatus, client.ddata.devicestatus); + } + + var clonedData = _.clone(client.ddata); + clonedData.devicestatus = mergedStatuses; client.sbx = sandbox.clientInit( - client.settings + client.ctx , new Date(time).getTime() //make sure we send a timestamp - , pluginBase, { - sgvs: sgvs - , cals: [client.cal] - , treatments: client.treatments - , profile: profile - , uploaderBattery: devicestatusData && devicestatusData.uploaderBattery - , inRetroMode: inRetroMode() - }); + , clonedData + ); //all enabled plugins get a chance to set properties, even if they aren't shown - plugins.setProperties(client.sbx); + client.plugins.setProperties(client.sbx); //only shown plugins get a chance to update visualisations - plugins.updateVisualisations(client.sbx); + client.plugins.updateVisualisations(client.sbx); + + var viewMenu = $('#viewMenu'); + viewMenu.empty(); + + _.each(client.sbx.pluginBase.forecastInfos, function eachInfo (info) { + var forecastOption = $('
  • '); + var forecastLabel = $('
  • Silence for ' + mins + ' minutes
  • '); + var snoozeOption = $('
  • ' + client.translate('Silence for %1 minutes', { params: [mins] }) + '
  • '); snoozeOption.click(snoozeAlarm); silenceBtn.append(snoozeOption); }); @@ -429,7 +760,7 @@ client.init = function init(serverSettings, plugins) { event.preventDefault(); } - function playAlarm(audio) { + function playAlarm (audio) { // ?mute=true disables alarms to testers. if (client.browserUtils.queryParms().mute !== 'true') { audio.play(); @@ -438,11 +769,11 @@ client.init = function init(serverSettings, plugins) { } } - function stopAlarm(isClient, silenceTime) { + function stopAlarm (isClient, silenceTime, notify) { alarmInProgress = false; alarmMessage = null; container.removeClass('urgent warning'); - d3.selectAll('audio.playing').each(function () { + d3.selectAll('audio.playing').each(function() { var audio = this; audio.pause(); $(this).removeClass('playing'); @@ -453,26 +784,75 @@ client.init = function init(serverSettings, plugins) { updateTitle(); - // only emit ack if client invoke by button press - if (isClient) { - if (isTimeAgoAlarmType(currentAlarmType)) { + silenceTime = silenceTime || times.mins(5).msecs; + + var alarm = null; + + if (notify) { + if (notify.level) { + alarm = getClientAlarm(notify.level, notify.group); + } else if (notify.group) { + alarm = getClientAlarm(currentNotify.level, notify.group); + } else { + alarm = getClientAlarm(currentNotify.level, currentNotify.group); + } + } else if (currentNotify) { + alarm = getClientAlarm(currentNotify.level, currentNotify.group); + } + + if (alarm) { + alarm.lastAckTime = Date.now(); + alarm.silenceTime = silenceTime; + if (alarm.group === 'Time Ago') { container.removeClass('alarming-timeago'); - var alarm = getClientAlarm(currentAlarmType); - alarm.lastAckTime = Date.now(); - alarm.silenceTime = silenceTime; } - socket.emit('ack', currentAlarmType || 'alarm', silenceTime); + } else { + console.info('No alarm to ack for', notify || currentNotify); + } + + // only emit ack if client invoke by button press + if (isClient && currentNotify) { + alarmSocket.emit('ack', currentNotify.level, currentNotify.group, silenceTime); } + currentNotify = null; + brushed(); } - function updateClock() { + function refreshAuthIfNeeded () { + var clientToken = client.authorized ? client.authorized.token : null; + var token = client.browserUtils.queryParms().token || clientToken; + if (token && client.authorized) { + var renewTime = (client.authorized.exp * 1000) - times.mins(15).msecs - Math.abs((client.authorized.iat * 1000) - client.authorized.lat); + var refreshIn = Math.round((renewTime - client.now) / 1000); + if (client.now > renewTime) { + console.info('Refreshing authorization renewal'); + $.ajax('/api/v2/authorization/request/' + token, { + success: function(authorized) { + if (authorized) { + console.info('Got new authorization', authorized); + authorized.lat = client.now; + client.authorized = authorized; + } + } + }); + } else if (refreshIn < times.mins(5).secs) { + console.info('authorization refresh in ' + refreshIn + 's'); + } + } + } + + function updateClock () { updateClockDisplay(); - var interval = (60 - (new Date()).getSeconds()) * 1000 + 5; - setTimeout(updateClock,interval); + // Update at least every 15 seconds + var interval = Math.min(15 * 1000, (60 - (new Date()).getSeconds()) * 1000 + 5); + setTimeout(updateClock, interval); updateTimeAgo(); + if (chart) { + brushed(); + } // Dim the screen by reducing the opacity when at nighttime if (client.settings.nightMode) { @@ -483,9 +863,13 @@ client.init = function init(serverSettings, plugins) { $('body').css({ 'opacity': opacity.DAY }); } } + refreshAuthIfNeeded(); + if (client.resetRetroIfNeeded) { + client.resetRetroIfNeeded(); + } } - function updateClockDisplay() { + function updateClockDisplay () { if (inRetroMode()) { return; } @@ -493,95 +877,88 @@ client.init = function init(serverSettings, plugins) { $('#currentTime').text(formatTime(new Date(client.now), true)).css('text-decoration', ''); } - function getClientAlarm(type) { - var alarm = clientAlarms[type]; + function getClientAlarm (level, group) { + var key = level + '-' + group; + var alarm = null; + // validate the key before getting the alarm + if (Object.prototype.hasOwnProperty.call(clientAlarms, key)) { + /* eslint-disable-next-line security/detect-object-injection */ // verified false positive + alarm = clientAlarms[key]; + } if (!alarm) { - alarm = { type: type }; - clientAlarms[type] = alarm; + alarm = { level: level, group: group }; + /* eslint-disable-next-line security/detect-object-injection */ // verified false positive + clientAlarms[key] = alarm; } return alarm; } - function isTimeAgoAlarmType(alarmType) { - return alarmType === 'warnTimeAgo' || alarmType === 'urgentTimeAgo'; + function isTimeAgoAlarmType () { + return currentNotify && currentNotify.group === 'Time Ago'; } - function isStale (ago) { - return client.settings.alarmTimeagoWarn && ago.status === 'warn' - || client.settings.alarmTimeagoUrgent && ago.status === 'urgent'; + function isStale (status) { + return client.settings.alarmTimeagoWarn && status === 'warn' || + client.settings.alarmTimeagoUrgent && status === 'urgent'; } function notAcked (alarm) { return Date.now() >= (alarm.lastAckTime || 0) + (alarm.silenceTime || 0); } - function checkTimeAgoAlarm(ago) { - var level = ago.status - , alarm = getClientAlarm(level + 'TimeAgo'); + function checkTimeAgoAlarm (status) { + var level = status === 'urgent' ? levels.URGENT : levels.WARN; + var alarm = getClientAlarm(level, 'Time Ago'); - if (isStale(ago) && notAcked(alarm)) { - currentAlarmType = alarm.type; - console.info('generating timeAgoAlarm', alarm.type); + if (isStale(status) && notAcked(alarm)) { + console.info('generating timeAgoAlarm', alarm); container.addClass('alarming-timeago'); + var display = client.timeago.calcDisplay(client.sbx.lastSGVEntry(), client.sbx.time); + var translate = client.translate; var notify = { - title: 'Last data received ' + [ago.value, ago.label].join(' ') - , level: level === 'urgent' ? 2 : 1 + title: translate('Last data received') + ' ' + display.value + ' ' + translate(display.label) + , level: status === 'urgent' ? 2 : 1 + , group: 'Time Ago' }; - var sound = level === 'warn' ? alarmSound : urgentAlarmSound; + var sound = status === 'warn' ? alarmSound : urgentAlarmSound; generateAlarm(sound, notify); } - container.toggleClass('alarming-timeago', ago.status !== 'current'); + container.toggleClass('alarming-timeago', status !== 'current'); - if (alarmingNow() && ago.status === 'current' && isTimeAgoAlarmType(currentAlarmType)) { - stopAlarm(true, times.min().msecs); + if (status === 'warn') { + container.addClass('warn'); + } else if (status === 'urgent') { + container.addClass('urgent'); } - } - function updateTimeAgo() { - var lastEntry = $('#lastEntry') - , time = client.latestSGV ? client.latestSGV.mills : -1 - , ago = timeAgo(time) - , retroMode = inRetroMode(); - - function updateTimeAgoPill() { - if (retroMode || !ago.value) { - lastEntry.find('em').hide(); - } else { - lastEntry.find('em').show().text(ago.value); - } - if (retroMode || ago.label) { - lastEntry.find('label').show().text(retroMode ? 'RETRO' : ago.label); - } else { - lastEntry.find('label').hide(); - } + if (alarmingNow() && status === 'current' && isTimeAgoAlarmType()) { + stopAlarm(true, times.min().msecs); } + } - lastEntry.removeClass('current warn urgent'); - lastEntry.addClass(ago.status); - - if (ago.status !== 'current') { + function updateTimeAgo () { + var status = client.timeago.checkStatus(client.sbx); + if (status !== 'current') { updateTitle(); } - checkTimeAgoAlarm(ago); - - updateTimeAgoPill(); + checkTimeAgoAlarm(status); } - function updateTimeAgoSoon() { - setTimeout(function updatingTimeAgoNow() { + function updateTimeAgoSoon () { + setTimeout(function updatingTimeAgoNow () { updateTimeAgo(); }, times.secs(10).msecs); } - function refreshChart(updateToNow) { + function refreshChart (updateToNow) { if (updateToNow) { updateBrushToNow(); } chart.update(false); } - (function watchVisibility ( ) { + (function watchVisibility () { // Set the name of the hidden property and the change event for visibility var hidden, visibilityChange; if (typeof document.hidden !== 'undefined') { @@ -598,8 +975,9 @@ client.init = function init(serverSettings, plugins) { visibilityChange = 'webkitvisibilitychange'; } - document.addEventListener(visibilityChange, function visibilityChanged ( ) { + 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) { @@ -614,7 +992,7 @@ client.init = function init(serverSettings, plugins) { updateClock(); updateTimeAgoSoon(); - function Dropdown(el) { + function Dropdown (el) { this.ddmenuitem = 0; this.$el = $(el); @@ -622,139 +1000,272 @@ client.init = function init(serverSettings, plugins) { $(document).click(function() { that.close(); }); } - Dropdown.prototype.close = function () { + Dropdown.prototype.close = function() { if (this.ddmenuitem) { this.ddmenuitem.css('visibility', 'hidden'); this.ddmenuitem = 0; } }; - Dropdown.prototype.open = function (e) { + Dropdown.prototype.open = function(e) { this.close(); this.ddmenuitem = $(this.$el).css('visibility', 'visible'); e.stopPropagation(); }; - var silenceDropdown = new Dropdown('.dropdown-menu'); + var silenceDropdown = new Dropdown('#silenceBtn'); + var viewDropdown = new Dropdown('#viewMenu'); - $('.bgButton').click(function (e) { + $('.bgButton').click(function(e) { if (alarmingNow()) { + /* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive silenceDropdown.open(e); } }); $('.focus-range li').click(function(e) { var li = $(e.target); - $('.focus-range li').removeClass('selected'); - li.addClass('selected'); - var hours = Number(li.data('hours')); - client.foucusRangeMS = times.hours(hours).msecs; - refreshChart(); + if (li.attr('data-hours')) { + $('.focus-range li').removeClass('selected'); + li.addClass('selected'); + var hours = Number(li.data('hours')); + client.focusRangeMS = times.hours(hours).msecs; + Storages.localStorage.set('focusHours', hours); + refreshChart(); + } else { + /* eslint-disable-next-line security/detect-non-literal-fs-filename */ // verified false positive + viewDropdown.open(e); + } }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Client-side code to connect to server and handle incoming data //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - socket = io.connect(); + /* global io */ + client.socket = socket = io.connect({ transports: ["polling"] }); + client.alarmSocket = alarmSocket = io.connect("/alarm", { multiplex: true, transports: ["polling"] }); - function mergeDataUpdate(isDelta, cachedDataArray, receivedDataArray) { + socket.on('dataUpdate', dataUpdate); - function nsArrayDiff(oldArray, newArray) { - var seen = {}; - var l = oldArray.length; - for (var i = 0; i < l; i++) { seen[oldArray[i].mills] = true } - var result = []; - l = newArray.length; - for (var j = 0; j < l; j++) { if (!seen.hasOwnProperty(newArray[j].mills)) { result.push(newArray[j]); console.log('delta data found'); } } - return result; - } + function resetRetro () { + client.retro = { + loadedMills: 0 + , loadStartedMills: 0 + }; + } - // If there was no delta data, just return the original data - if (!receivedDataArray) { - return cachedDataArray || []; + client.resetRetroIfNeeded = function resetRetroIfNeeded () { + if (client.retro.loadedMills > 0 && Date.now() - client.retro.loadedMills > times.mins(5).msecs) { + resetRetro(); + console.info('Cleared retro data to free memory'); } + }; - // If this is not a delta update, replace all data - if (!isDelta) { - return receivedDataArray || []; + resetRetro(); + + client.loadRetroIfNeeded = function loadRetroIfNeeded () { + var now = Date.now(); + if (now - client.retro.loadStartedMills < times.secs(30).msecs) { + console.info('retro already loading, started', new Date(client.retro.loadStartedMills)); + return; } - // If this is delta, calculate the difference, merge and sort - var diff = nsArrayDiff(cachedDataArray, receivedDataArray); - return cachedDataArray.concat(diff).sort(function(a, b) { - return a.mills - b.mills; - }); - } + if (now - client.retro.loadedMills > times.mins(3).msecs) { + client.retro.loadStartedMills = now; + console.info('retro not fresh load started', new Date(client.retro.loadStartedMills)); + socket.emit('loadRetro', { + loadedMills: client.retro.loadedMills + }); + } + }; - socket.on('dataUpdate', dataUpdate); + socket.on('retroUpdate', function retroUpdate (retroData) { + console.info('got retroUpdate', retroData, new Date(client.now)); + client.retro = { + loadedMills: Date.now() + , loadStartedMills: 0 + , data: retroData + }; + }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Alarms and Text handling //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - socket.on('connect', function () { + + client.authorizeSocket = function authorizeSocket () { + + console.log('Authorizing socket'); + var auth_data = { + client: 'web' + , secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() + , token: client.authorized && client.authorized.token + , history: history + }; + + socket.emit( + 'authorize' + , auth_data + , function authCallback (data) { + if (!data) { + console.log('Crashed!'); + client.crashed(); + } + + if (!data.read || !hasRequiredPermission()) { + client.hashauth.requestAuthentication(function afterRequest () { + client.hashauth.updateSocketAuth(); + if (callback) { + callback(); + } + }); + } else if (callback) { + callback(); + } + } + ); + } + + socket.on('connect', function() { console.log('Client connected to server.'); + client.authorizeSocket(); + }); + + client.subscribeForAlarms = function subscribeForAlarms () { + + var auth_data = { + secret: client.authorized && client.authorized.token ? null : client.hashauth.hash() + , jwtToken: client.authorized && client.authorized.token + }; + + alarmSocket.emit( + 'subscribe' + , auth_data + , function subscribeCallback (data) { + if (!data) { + console.log('Crashed!'); + client.crashed(); + } + + console.log('Subscribed for alarms', data); + var shouldAuthenticationPromptOnLoad = client.settings.authenticationPromptOnLoad ; + if (!data.success) { + if (!data.read || !hasRequiredPermission() || shouldAuthenticationPromptOnLoad) { + return client.hashauth.requestAuthentication(function afterRequest () { + return client.hashauth.updateSocketAuth(); + }); + } + } + } + ); + } + + alarmSocket.on('connect', function() { + client.subscribeForAlarms(); }); + function hasRequiredPermission () { + if (client.requiredPermission) { + if (client.hashauth && client.hashauth.isAuthenticated()) { + return true; + } else { + return client.authorized && client.authorized.check(client.requiredPermission); + } + } else { + return true; + } + } //with predicted alarms, latestSGV may still be in target so to see if the alarm // is for a HIGH we can only check if it's >= the bottom of the target - function isAlarmForHigh() { - return client.latestSGV.mgdl >= client.settings.thresholds.bgTargetBottom; + function isAlarmForHigh () { + return client.latestSGV && client.latestSGV.mgdl >= client.settings.thresholds.bgTargetBottom; } //with predicted alarms, latestSGV may still be in target so to see if the alarm // is for a LOW we can only check if it's <= the top of the target - function isAlarmForLow() { - return client.latestSGV.mgdl <= client.settings.thresholds.bgTargetTop; + function isAlarmForLow () { + return client.latestSGV && client.latestSGV.mgdl <= client.settings.thresholds.bgTargetTop; } - socket.on('announcement', function (notify) { + alarmSocket.on('notification', function(notify) { + console.log('notification from server:', notify); + if (notify.timestamp && previousNotifyTimestamp !== notify.timestamp) { + previousNotifyTimestamp = notify.timestamp; + client.plugins.visualizeAlarm(client.sbx, notify, notify.title + ' ' + notify.message); + } else { + console.log('No timestamp found for notify, not passing to plugins'); + } + }); + + alarmSocket.on('announcement', function(notify) { console.info('announcement received from server'); - console.log('notify:',notify); currentAnnouncement = notify; currentAnnouncement.received = Date.now(); updateTitle(); }); - socket.on('alarm', function (notify) { + alarmSocket.on('alarm', function(notify) { console.info('alarm received from server'); - console.log('notify:',notify); var enabled = (isAlarmForHigh() && client.settings.alarmHigh) || (isAlarmForLow() && client.settings.alarmLow); if (enabled) { console.log('Alarm raised!'); - //TODO: Announcement hack a1/a2 - currentAlarmType = notify.isAnnouncement ? 'a' + notify.level : 'alarm'; - generateAlarm(alarmSound,notify); + generateAlarm(alarmSound, notify); } else { console.info('alarm was disabled locally', client.latestSGV.mgdl, client.settings); } chart.update(false); }); - socket.on('urgent_alarm', function (notify) { + alarmSocket.on('urgent_alarm', function(notify) { console.info('urgent alarm received from server'); - console.log('notify:',notify); - var enabled = (isAlarmForHigh() && client.settings.alarmUrgentHigh) || (isAlarmForLow() && client.settings.alarmUrgentLow); if (enabled) { console.log('Urgent alarm raised!'); - //TODO: Announcement hack a1/a2 - currentAlarmType = notify.isAnnouncement ? 'a' + notify.level : 'urgent_alarm'; - generateAlarm(urgentAlarmSound,notify); + generateAlarm(urgentAlarmSound, notify); } else { console.info('urgent alarm was disabled locally', client.latestSGV.mgdl, client.settings); } chart.update(false); }); - socket.on('clear_alarm', function () { + alarmSocket.on('clear_alarm', function(notify) { if (alarmInProgress) { console.log('clearing alarm'); - stopAlarm(); + stopAlarm(false, null, notify); } }); + /* + * + // TODO: When an unauthorized client attempts to silence an alarm, we should + // allow silencing locally, request for authorization, and if the + // authorization succeeds even republish the ACK notification. something like... + alarmSocket.on('authorization_needed', function(details) { + if (alarmInProgress) { + console.log('clearing alarm'); + stopAlarm(true, details.silenceTime, currentNotify); + } + client.hashauth.requestAuthentication(function afterRequest () { + console.log("SUCCESSFULLY AUTHORIZED, REPUBLISHED ACK?"); + // easiest way to update permission set on server side is to send another message. + alarmSocket.emit('resubscribe', currentNotify, details); + + if (isClient && currentNotify) { + alarmSocket.emit('ack', currentNotify.level, currentNotify.group, details.silenceTime); + } + }); + }); + + */ $('#testAlarms').click(function(event) { - d3.selectAll('.audio.alarms audio').each(function () { + + // Speech synthesis also requires on iOS that user triggers a speech event for it to speak anything + if (client.plugins('speech').isEnabled) { + var msg = new SpeechSynthesisUtterance('Ok ok.'); + msg.lang = 'en-US'; + window.speechSynthesis.speak(msg); + } + + d3.selectAll('.audio.alarms audio').each(function() { var audio = this; playAlarm(audio); setTimeout(function() { @@ -764,138 +1275,121 @@ client.init = function init(serverSettings, plugins) { event.preventDefault(); }); - $('.appName').text(serverSettings.name); - $('.version').text(serverSettings.version); - $('.head').text(serverSettings.head); - if (serverSettings.apiEnabled) { - $('.serverSettings').show(); - } - $('#treatmentDrawerToggle').toggle(client.settings.showPlugins.indexOf('careportal') > -1); - $('#boluscalcDrawerToggle').toggle(client.settings.showPlugins.indexOf('boluscalc') > -1); - container.toggleClass('has-minor-pills', plugins.hasShownType('pill-minor', client.settings)); - - function prepareData ( ) { - // Post processing after data is in - var temp1 = [ ]; - if (client.cal && client.rawbg.isEnabled(client.sbx)) { - temp1 = SGVdata.map(function (entry) { - var rawbgValue = client.rawbg.showRawBGs(entry.mgdl, entry.noise, client.cal, client.sbx) ? client.rawbg.calc(entry, client.cal, client.sbx) : 0; + if (serverSettings) { + $('.appName').text(serverSettings.name); + $('.version').text(serverSettings.version); + $('.head').text(serverSettings.head); + if (serverSettings.apiEnabled) { + $('.serverSettings').show(); + } + } + + client.updateAdminMenu = function updateAdminMenu() { + // hide food control if not enabled + $('.foodcontrol').toggle(client.settings.enable.indexOf('food') > -1); + // hide cob control if not enabled + $('.cobcontrol').toggle(client.settings.enable.indexOf('cob') > -1); +} + + client.updateAdminMenu(); + + container.toggleClass('has-minor-pills', client.plugins.hasShownType('pill-minor', client.settings)); + + function prepareEntries () { + // Post processing after data is in + var temp1 = []; + var sbx = client.sbx.withExtendedSettings(client.rawbg); + + if (client.ddata.cal && client.rawbg.isEnabled(sbx)) { + temp1 = client.ddata.sgvs.map(function(entry) { + var rawbgValue = client.rawbg.showRawBGs(entry.mgdl, entry.noise, client.ddata.cal, sbx) ? client.rawbg.calc(entry, client.ddata.cal, sbx) : 0; if (rawbgValue > 0) { return { mills: entry.mills - 2000, mgdl: rawbgValue, color: 'white', type: 'rawbg' }; } else { return null; } - }).filter(function (entry) { + }).filter(function(entry) { return entry !== null; }); } - var temp2 = SGVdata.map(function (obj) { - return { mills: obj.mills, mgdl: obj.mgdl, direction: obj.direction, color: sgvToColor(obj.mgdl), type: 'sgv', noise: obj.noise, filtered: obj.filtered, unfiltered: obj.unfiltered}; + var temp2 = client.ddata.sgvs.map(function(obj) { + return { mills: obj.mills, mgdl: obj.mgdl, direction: obj.direction, color: sgvToColor(obj.mgdl), type: 'sgv', noise: obj.noise, filtered: obj.filtered, unfiltered: obj.unfiltered }; }); - client.data = []; - client.data = client.data.concat(temp1, temp2); + client.entries = []; + client.entries = client.entries.concat(temp1, temp2); - client.data = client.data.concat(MBGdata.map(function (obj) { + client.entries = client.entries.concat(client.ddata.mbgs.map(function(obj) { return { mills: obj.mills, mgdl: obj.mgdl, color: 'red', type: 'mbg', device: obj.device }; })); - client.data.forEach(function (point) { + var tooOld = client.now - times.hours(48).msecs; + client.entries = _.filter(client.entries, function notTooOld (entry) { + return entry.mills > tooOld; + }); + + client.entries.forEach(function(point) { if (point.mgdl < 39) { point.color = 'transparent'; } }); + + client.entries.sort(function sorter (a, b) { + return a.mills - b.mills; + }); } - function dataUpdate (d) { + function dataUpdate (received, headless) { + console.info('got dataUpdate', new Date(client.now)); - if (!d) { - return; - } + var lastUpdated = Date.now(); + client.dataLastUpdated = lastUpdated; - // Calculate the diff to existing data and replace as needed - SGVdata = mergeDataUpdate(d.delta, SGVdata, d.sgvs); - MBGdata = mergeDataUpdate(d.delta,MBGdata, d.mbgs); - client.treatments = mergeDataUpdate(d.delta, client.treatments, d.treatments); - - // filter & prepare 'Sensor' events - client.sensortreatments = client.treatments.filter( function filterSensor(t) { - return t.eventType.indexOf('Sensor') > -1; - }).sort(function (a,b) { return a.mills > b.mills; }); - - // filter & prepare 'Profile Switch' events - client.profiletreatments = client.treatments.filter( function filterProfiles(t) { - return t.eventType === 'Profile Switch'; - }).sort(function (a,b) { return a.mills > b.mills; }); - - // filter & prepare temp basals - var tempbasaltreatments = client.treatments.filter( function filterBasals(t) { - return t.eventType && t.eventType.indexOf('Temp Basal') > -1; - }); - // cut temp basals by end events - // better to do it only on data update - var endevents = tempbasaltreatments.filter(function filterEnd(t) { - return ! t.duration; - }); - - function cutIfInInterval(base, end) { - if (base.mills < end.mills && base.mills + times.mins(base.duration).msecs > end.mills) { - base.duration = times.msecs(end.mills-base.mills).mins; - } - } - - // cut by end events - tempbasaltreatments.forEach(function allTreatments(t) { - endevents.forEach(function allEndevents(e) { - cutIfInInterval(t, e); - }); - }); + receiveDData(received, client.ddata, client.settings); - // cut by overlaping events - tempbasaltreatments.forEach(function allTreatments(t) { - tempbasaltreatments.forEach(function allEndevents(e) { - cutIfInInterval(t, e); - }); - }); - - // store prepared temp basal treatments - client.tempbasaltreatments = tempbasaltreatments.filter(function filterEnd(t) { - return t.duration; - }); - // Resend new treatments to profile - client.profilefunctions.updateTreatments(client.profiletreatments, client.tempbasaltreatments); - + client.profilefunctions.updateTreatments(client.ddata.profileTreatments, client.ddata.tempbasalTreatments, client.ddata.combobolusTreatments); - // Do some reporting on the console - console.log('Total SGV data size', SGVdata.length); - console.log('Total treatment data size', client.treatments.length); + if (received.profiles) { + profile.loadData(received.profiles); + } - if (d.profiles) { - profile.loadData(d.profiles); + if (client.ddata.sgvs) { + // TODO change the next line so that it uses the prediction if the signal gets lost (max 1/2 hr) + client.ctx.data.lastUpdated = lastUpdated; + client.latestSGV = client.ddata.sgvs[client.ddata.sgvs.length - 1]; } - if (d.cals) { client.cal = d.cals[d.cals.length-1]; } - if (d.devicestatus) { devicestatusData = d.devicestatus; } + client.ddata.inRetroMode = false; + client.ddata.profile = profile; - if (d.sgvs) { - // change the next line so that it uses the prediction if the signal gets lost (max 1/2 hr) - latestUpdateTime = Date.now(); - client.latestSGV = SGVdata[SGVdata.length - 1]; - prevSGV = SGVdata[SGVdata.length - 2]; - } + client.nowSBX = sandbox.clientInit( + client.ctx + , lastUpdated + , client.ddata + ); - prepareData(); + //all enabled plugins get a chance to set properties, even if they aren't shown + client.plugins.setProperties(client.nowSBX); + + prepareEntries(); updateTitle(); + // Don't invoke D3 in headless mode + + if (headless) return; + if (!isInitialData) { isInitialData = true; chart = client.chart = require('./chart')(client, d3, $); - brushed(); chart.update(true); - } else { + brushed(); chart.update(false); + } else if (!inRetroMode()) { + brushed(); + chart.update(false); + } else { + chart.updateContext(); } - } }; diff --git a/lib/client/receiveddata.js b/lib/client/receiveddata.js new file mode 100644 index 00000000000..e1d2cab88dd --- /dev/null +++ b/lib/client/receiveddata.js @@ -0,0 +1,172 @@ +'use strict'; + +var _ = require('lodash'); + +var TWO_DAYS = 172800000; + +function mergeDataUpdate (isDelta, cachedDataArray, receivedDataArray, maxAge) { + + function nsArrayDiff (oldArray, newArray) { + 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) { + knownMills.push(oldArray[i].mills); + } + /* eslint-enable security/detect-object-injection */ // verified false positive + } + + var result = { + updates: [], + new: [] + }; + + l = newArray.length; + for (var j = 0; j < l; j++) { + /* 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; + } + + // If there was no delta data, just return the original data + if (!receivedDataArray) { + return cachedDataArray || []; + } + + // If this is not a delta update, replace all data + if (!isDelta) { + return receivedDataArray || []; + } + + // purge old data from cache before updating + var mAge = (isNaN(maxAge) || maxAge == null) ? TWO_DAYS : maxAge; + var twoDaysAgo = new Date().getTime() - mAge; + + 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, 1); + } + } + + // If this is delta, calculate the difference, merge and sort + var diff = nsArrayDiff(cachedDataArray, receivedDataArray); + + // 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; + }); +} + +function mergeTreatmentUpdate (isDelta, cachedDataArray, receivedDataArray) { + + // If there was no delta data, just return the original data + if (!receivedDataArray) { + return cachedDataArray || []; + } + + // If this is not a delta update, replace all data + if (!isDelta) { + return receivedDataArray || []; + } + + // check for update, change, remove + 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); + break; + } + if (no.action === 'update') { + delete no.action; + cachedDataArray.splice(j, 1, no); + break; + } + } + } + } + + // If this is delta, calculate the difference, merge and sort + return cachedDataArray.sort(function(a, b) { + return a.mills - b.mills; + }); +} + +function receiveDData (received, ddata, settings) { + + if (!received) { + return; + } + + // Calculate the diff to existing data and replace as needed + ddata.sgvs = mergeDataUpdate(received.delta, ddata.sgvs, received.sgvs); + ddata.mbgs = mergeDataUpdate(received.delta, ddata.mbgs, received.mbgs); + ddata.treatments = mergeTreatmentUpdate(received.delta, ddata.treatments, received.treatments); + ddata.food = mergeTreatmentUpdate(received.delta, ddata.food, received.food); + + ddata.processTreatments(false); + + // Do some reporting on the console + // console.log('Total SGV data size', ddata.sgvs.length); + // console.log('Total treatment data size', ddata.treatments.length); + + if (received.cals) { + ddata.cals = received.cals; + ddata.cal = _.last(ddata.cals); + } + + if (received.devicestatus) { + if (settings.extendedSettings.devicestatus && settings.extendedSettings.devicestatus.advanced) { + //only use extra memory in advanced mode + ddata.devicestatus = mergeDataUpdate(received.delta, ddata.devicestatus, received.devicestatus); + } else { + ddata.devicestatus = received.devicestatus; + } + } + + if (received.dbstats && received.dbstats.dataSize) { + ddata.dbstats = received.dbstats; + } +} + +//expose for tests +receiveDData.mergeDataUpdate = mergeDataUpdate; +receiveDData.mergeTreatmentUpdate = mergeTreatmentUpdate; + +module.exports = receiveDData; diff --git a/lib/client/renderer.js b/lib/client/renderer.js index 11463076e45..ba848303a9f 100644 --- a/lib/client/renderer.js +++ b/lib/client/renderer.js @@ -2,27 +2,36 @@ var _ = require('lodash'); var times = require('../times'); +var consts = require('../constants'); var DEFAULT_FOCUS = times.hours(3).msecs , WIDTH_SMALL_DOTS = 420 , WIDTH_BIG_DOTS = 800 - , TOOLTIP_TRANS_MS = 200 // milliseconds - ; + , TOOLTIP_WIDTH = 150 //min-width + padding +; + +const zeroDate = new Date(0); function init (client, d3) { - var renderer = { }; + var renderer = {}; var utils = client.utils; var translate = client.translate; + function getOrAddDate(entry) { + if (entry.date) return entry.date; + entry.date = new Date(entry.mills); + return entry.date; + } + //chart isn't created till the client gets data, so can grab the var at init - function chart() { + function chart () { return client.chart; } - function focusRangeAdjustment ( ) { - return client.foucusRangeMS === DEFAULT_FOCUS ? 1 : 1 + ((client.foucusRangeMS - DEFAULT_FOCUS) / DEFAULT_FOCUS / 8); + function focusRangeAdjustment () { + return client.focusRangeMS === DEFAULT_FOCUS ? 1 : 1 + ((client.focusRangeMS - DEFAULT_FOCUS) / DEFAULT_FOCUS / 8); } var dotRadius = function(type) { @@ -38,56 +47,46 @@ function init (client, d3) { return radius / focusRangeAdjustment(); }; - function hideTooltip ( ) { - client.tooltip.transition() - .duration(TOOLTIP_TRANS_MS) - .style('opacity', 0); + function tooltipLeft () { + var windowWidth = $(client.tooltip.node()).parent().parent().width(); + var left = d3.event.pageX + TOOLTIP_WIDTH < windowWidth ? d3.event.pageX : windowWidth - TOOLTIP_WIDTH - 10; + return left + 'px'; + } + + function hideTooltip () { + client.tooltip.style('opacity', 0); } // get the desired opacity for context chart based on the brush extent - renderer.highlightBrushPoints = function highlightBrushPoints(data) { - if (data.mills >= chart().brush.extent()[0].getTime() && data.mills <= chart().brush.extent()[1].getTime()) { + renderer.highlightBrushPoints = function highlightBrushPoints (data, from, to) { + if (client.latestSGV && data.mills >= from && data.mills <= to) { return chart().futureOpacity(data.mills - client.latestSGV.mills); } else { return 0.5; } }; - renderer.bubbleScale = function bubbleScale ( ) { + renderer.bubbleScale = function bubbleScale () { // a higher bubbleScale will produce smaller bubbles (it's not a radius like focusDotRadius) return (chart().prevChartWidth < WIDTH_SMALL_DOTS ? 4 : (chart().prevChartWidth < WIDTH_BIG_DOTS ? 3 : 2)) * focusRangeAdjustment(); }; - function isDexcom(device) { - return device && device.toLowerCase().indexOf('dexcom') === 0; - } - - renderer.addFocusCircles = function addFocusCircles ( ) { - // get slice of data so that concatenation of predictions do not interfere with subsequent updates - var focusData = client.data.slice(); + renderer.addFocusCircles = function addFocusCircles () { - if (client.sbx.pluginBase.forecastPoints) { - focusData = focusData.concat(client.sbx.pluginBase.forecastPoints); - } - - // bind up the focus chart data to an array of circles - // selects all our data into data and uses date function to get current max date - var focusCircles = chart().focus.selectAll('circle').data(focusData, client.entryToDate); - - function prepareFocusCircles(sel) { + function updateFocusCircles (sel) { var badData = []; - sel.attr('cx', function (d) { - if (!d) { - console.error('Bad data', d); - return chart().xScale(new Date(0)); - } else if (!d.mills) { - console.error('Bad data, no mills', d); - return chart().xScale(new Date(0)); - } else { - return chart().xScale(new Date(d.mills)); - } - }) - .attr('cy', function (d) { + sel.attr('cx', function(d) { + if (!d) { + console.error('Bad data', d); + return chart().xScale(zeroDate); + } else if (!d.mills) { + console.error('Bad data, no mills', d); + return chart().xScale(zeroDate); + } else { + return chart().xScale(getOrAddDate(d)); + } + }) + .attr('cy', function(d) { var scaled = client.sbx.scaleEntry(d); if (isNaN(scaled)) { badData.push(d); @@ -96,19 +95,14 @@ function init (client, d3) { return chart().yScale(scaled); } }) - .attr('fill', function (d) { - return d.type === 'forecast' ? 'none' : d.color; - }) - .attr('opacity', function (d) { - return chart().futureOpacity(d.mills - client.latestSGV.mills); - }) - .attr('stroke-width', function (d) { - return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 1 : 0; - }) - .attr('stroke', function (d) { - return (isDexcom(d.device) ? 'white' : d.type === 'forecast' ? d.color : '#0099ff'); + .attr('opacity', function(d) { + if (d.noFade) { + return null; + } else { + return !client.latestSGV ? 1 : chart().futureOpacity(d.mills - client.latestSGV.mills); + } }) - .attr('r', function (d) { + .attr('r', function(d) { return dotRadius(d.type); }); @@ -119,21 +113,33 @@ function init (client, d3) { return sel; } + function prepareFocusCircles (sel) { + updateFocusCircles(sel) + .attr('fill', function(d) { + return d.type === 'forecast' ? 'none' : d.color; + }) + .attr('stroke-width', function(d) { + return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 2 : 0; + }) + .attr('stroke', function(d) { + return (d.type === 'mbg' ? 'white' : d.color); + }); + + return sel; + } + function focusCircleTooltip (d) { - if (d.type !== 'sgv' && d.type !== 'mbg') { + if (d.type !== 'sgv' && d.type !== 'mbg' && d.type !== 'forecast') { return; } - function bgType ( ) { - return d.type === 'sgv' ? 'CGM' : (isDexcom(d.device) ? 'Calibration' : 'Meter'); - } - - function getRawbgInfo ( ) { - var info = { }; + function getRawbgInfo () { + var info = {}; + var sbx = client.sbx.withExtendedSettings(client.rawbg); if (d.type === 'sgv') { info.noise = client.rawbg.noiseCodeToDisplay(d.mgdl, d.noise); - if (client.rawbg.showRawBGs(d.mgdl, d.noise, client.cal, client.sbx)) { - info.value = utils.scaleMgdl(client.rawbg.calc(d, client.cal, client.sbx)); + if (client.rawbg.showRawBGs(d.mgdl, d.noise, client.ddata.cal, sbx)) { + info.value = utils.scaleMgdl(client.rawbg.calc(d, client.ddata.cal, sbx)); } } return info; @@ -141,54 +147,163 @@ function init (client, d3) { var rawbgInfo = getRawbgInfo(); - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); - client.tooltip.html('' + bgType() + translate('BG')+ ': ' + client.sbx.scaleEntry(d) + - (d.type === 'mbg' ? '
    ' + translate('Device') + ': ' + d.device : '') + - (rawbgInfo.value ? '
    ' + translate('Raw BG') + ': ' + rawbgInfo.value : '') + - (rawbgInfo.noise ? '
    ' + translate('Noise') + ': ' + rawbgInfo.noise : '') + - '
    ' + translate('Time') + ': ' + client.formatTime(new Date(d.mills))) - .style('left', (d3.event.pageX) + 'px') + client.tooltip.style('opacity', .9); + client.tooltip.html('' + translate('BG') + ': ' + client.sbx.scaleEntry(d) + + (d.type === 'mbg' ? '
    ' + translate('Device') + ': ' + d.device : '') + + (d.type === 'forecast' && d.forecastType ? '
    ' + translate('Forecast Type') + ': ' + d.forecastType : '') + + (rawbgInfo.value ? '
    ' + translate('Raw BG') + ': ' + rawbgInfo.value : '') + + (rawbgInfo.noise ? '
    ' + translate('Noise') + ': ' + rawbgInfo.noise : '') + + '
    ' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d))) + .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); } + // CGM data + + var focusData = client.entries; + + // bind up the focus chart data to an array of circles + // selects all our data into data and uses date function to get current max date + var focusCircles = chart().focus.selectAll('circle.entry-dot').data(focusData, function genKey (d) { + return "cgmreading." + d.mills; + }); + // if already existing then transition each circle to its new position - prepareFocusCircles(focusCircles.transition()); + updateFocusCircles(focusCircles); // if new circle then just display prepareFocusCircles(focusCircles.enter().append('circle')) + .attr('class', 'entry-dot') .on('mouseover', focusCircleTooltip) .on('mouseout', hideTooltip); focusCircles.exit().remove(); - // add clipping path so that data stays within axis - focusCircles.attr('clip-path', 'url(#clip)'); + // Forecasts + + var shownForecastPoints = client.chart.getForecastData(); + + // bind up the focus chart data to an array of circles + // selects all our data into data and uses date function to get current max date + + var forecastCircles = chart().focus.selectAll('circle.forecast-dot').data(shownForecastPoints, function genKey (d) { + return d.forecastType + d.mills; + }); + + forecastCircles.exit().remove(); + + prepareFocusCircles(forecastCircles.enter().append('circle')) + .attr('class', 'forecast-dot') + .on('mouseover', focusCircleTooltip) + .on('mouseout', hideTooltip); + + updateFocusCircles(forecastCircles); + }; - renderer.addTreatmentCircles = function addTreatmentCircles ( ) { + renderer.addTreatmentCircles = function addTreatmentCircles (nowDate) { function treatmentTooltip (d) { - return ''+translate('Time')+': ' + client.formatTime(new Date(d.mills)) + '
    ' + - (d.eventType ? ''+translate('Treatment type')+': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
    ' : '') + - (d.glucose ? ''+translate('BG')+': ' + d.glucose + (d.glucoseType ? ' (' + translate(d.glucoseType) + ')': '') + '
    ' : '') + - (d.enteredBy ? ''+translate('Entered By')+': ' + d.enteredBy + '
    ' : '') + - (d.notes ? ''+translate('Notes')+': ' + d.notes : ''); + var targetBottom = d.targetBottom; + var targetTop = d.targetTop; + + if (client.settings.units === 'mmol') { + targetBottom = Math.round(targetBottom / consts.MMOL_TO_MGDL * 10) / 10; + targetTop = Math.round(targetTop / consts.MMOL_TO_MGDL * 10) / 10; + } + + var correctionRangeText; + if (d.correctionRange) { + var min = d.correctionRange[0]; + var max = d.correctionRange[1]; + + if (client.settings.units === 'mmol') { + max = client.sbx.roundBGToDisplayFormat(client.sbx.scaleMgdl(max)); + min = client.sbx.roundBGToDisplayFormat(client.sbx.scaleMgdl(min)); + } + + if (d.correctionRange[0] === d.correctionRange[1]) { + correctionRangeText = '' + min; + } else { + correctionRangeText = '' + min + ' - ' + max; + } + } + + var durationText; + if (d.durationType === "indefinite") { + durationText = translate("Indefinite"); + } else if (d.duration) { + var durationMinutes = Math.round(d.duration); + if (durationMinutes > 0 && durationMinutes % 60 == 0) { + var durationHours = durationMinutes / 60; + if (durationHours > 1) { + durationText = durationHours + ' hours'; + } else { + durationText = durationHours + ' hour'; + } + } else { + durationText = durationMinutes + ' min'; + } + } + + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
    ' + + (d.eventType ? '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
    ' : '') + + (d.reason ? '' + translate('Reason') + ': ' + translate(d.reason) + '
    ' : '') + + (d.glucose ? '' + translate('BG') + ': ' + d.glucose + (d.glucoseType ? ' (' + translate(d.glucoseType) + ')' : '') + '
    ' : '') + + (d.enteredBy ? '' + translate('Entered By') + ': ' + d.enteredBy + '
    ' : '') + + (d.targetTop ? '' + translate('Target Top') + ': ' + targetTop + '
    ' : '') + + (d.targetBottom ? '' + translate('Target Bottom') + ': ' + targetBottom + '
    ' : '') + + (durationText ? '' + translate('Duration') + ': ' + durationText + '
    ' : '') + + (d.insulinNeedsScaleFactor ? '' + translate('Insulin Scale Factor') + ': ' + d.insulinNeedsScaleFactor * 100 + '%
    ' : '') + + (correctionRangeText ? '' + translate('Correction Range') + ': ' + correctionRangeText + '
    ' : '') + + (d.transmitterId ? '' + translate('Transmitter ID') + ': ' + d.transmitterId + '
    ' : '') + + (d.sensorCode ? '' + translate('Sensor Code') + ': ' + d.sensorCode + '
    ' : '') + + (d.notes ? '' + translate('Notes') + ': ' + d.notes : ''); } function announcementTooltip (d) { - return ''+translate('Time')+': ' + client.formatTime(new Date(d.mills)) + '
    ' + - (d.eventType ? ''+translate('Announcement')+'
    ' : '') + - (d.notes && d.notes.length > 1 ? ''+translate('Message')+': ' + d.notes + '
    ' : '') + - (d.enteredBy ? ''+translate('Entered By')+': ' + d.enteredBy + '
    ' : ''); + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
    ' + + (d.eventType ? '' + translate('Announcement') + '
    ' : '') + + (d.notes && d.notes.length > 1 ? '' + translate('Message') + ': ' + d.notes + '
    ' : '') + + (d.enteredBy ? '' + translate('Entered By') + ': ' + d.enteredBy + '
    ' : ''); } + //TODO: filter in oref0 instead of here and after most people upgrade take this out + var openAPSSpam = ['BasalProfileStart', 'ResultDailyTotal', 'BGReceived']; + //NOTE: treatments with insulin or carbs are drawn by drawTreatment() // bind up the focus chart data to an array of circles - var treatCircles = chart().focus.selectAll('treatment-dot').data(client.treatments.filter(function(treatment) { - return !treatment.carbs && !treatment.insulin && !treatment.duration && treatment.eventType !== 'Temp Basal' && treatment.eventType !== 'Profile Switch'; - })); + var treatCircles = chart().focus.selectAll('.treatment-dot').data(client.ddata.treatments.filter(function(treatment) { + + var notCarbsOrInsulin = !treatment.carbs && !treatment.insulin; + var notTempOrProfile = !_.includes(['Temp Basal', 'Profile Switch', 'Combo Bolus', 'Temporary Target'], treatment.eventType); + + var notes = treatment.notes || ''; + var enteredBy = treatment.enteredBy || ''; - function prepareTreatCircles(sel) { - function strokeColor(d) { + var notOpenAPSSpam = enteredBy.indexOf('openaps://') === -1 || _.isUndefined(_.find(openAPSSpam, function startsWith (spam) { + return notes.indexOf(spam) === 0; + })); + + return notCarbsOrInsulin && !treatment.duration && treatment.durationType !== 'indefinite' && notTempOrProfile && notOpenAPSSpam; + }), function (d) { return d._id; }); + + function updateTreatCircles (sel) { + + sel.attr('cx', function(d) { + return chart().xScale(getOrAddDate(d)); + }) + .attr('cy', function(d) { + return chart().yScale(client.sbx.scaleEntry(d)); + }) + .attr('r', function() { + return dotRadius('mbg'); + }); + + return sel; + } + + function prepareTreatCircles (sel) { + function strokeColor (d) { var color = 'white'; if (d.isAnnouncement) { color = 'orange'; @@ -198,7 +313,7 @@ function init (client, d3) { return color; } - function fillColor(d) { + function fillColor (d) { var color = 'grey'; if (d.isAnnouncement) { color = 'orange'; @@ -208,15 +323,7 @@ function init (client, d3) { return color; } - sel.attr('cx', function (d) { - return chart().xScale(new Date(d.mills)); - }) - .attr('cy', function (d) { - return chart().yScale(client.sbx.scaleEntry(d)); - }) - .attr('r', function () { - return dotRadius('mbg'); - }) + updateTreatCircles(sel) .attr('stroke-width', 2) .attr('stroke', strokeColor) .attr('fill', fillColor); @@ -225,104 +332,148 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - prepareTreatCircles(treatCircles.transition()); + updateTreatCircles(treatCircles); // if new circle then just display prepareTreatCircles(treatCircles.enter().append('circle')) - .on('mouseover', function (d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + .attr('class', 'treatment-dot') + .on('mouseover', function(d) { + client.tooltip.style('opacity', .9); client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d)) - .style('left', (d3.event.pageX) + 'px') + .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); }) .on('mouseout', hideTooltip); - treatCircles.attr('clip-path', 'url(#clip)'); - + treatCircles.exit().remove(); + + var durationTreatments = client.ddata.treatments.filter(function(treatment) { + return !treatment.carbs && !treatment.insulin && (treatment.duration || treatment.durationType !== undefined) && + !_.includes(['Temp Basal', 'Profile Switch', 'Combo Bolus', 'Temporary Target'], treatment.eventType); + }); + + //use the processed temp target so there are no overlaps + durationTreatments = durationTreatments.concat(client.ddata.tempTargetTreatments); + // treatments with duration - var treatRects = chart().focus.selectAll('.g-duration').data(client.treatments.filter(function(treatment) { - return !treatment.carbs && !treatment.insulin && treatment.duration && treatment.eventType !== 'Temp Basal' && treatment.eventType !== 'Profile Switch'; - })); + var treatRects = chart().focus.selectAll('.g-duration').data(durationTreatments); - function fillColor(d) { + function fillColor (d) { // this is going to be updated by Event Type var color = 'grey'; if (d.eventType === 'Exercise') { color = 'Violet'; } else if (d.eventType === 'Note') { color = 'Salmon'; + } else if (d.eventType === 'Temporary Target') { + color = 'lightgray'; } return color; } - // if already existing then transition each rect to its new position - treatRects.transition() - .attr('transform', function (d) { - return 'translate(' + chart().xScale(new Date(d.mills)) + ',' + chart().yScale(utils.scaleMgdl(50)) + ')'; - }); + function rectHeight (d) { + var height = 20; + if (d.targetTop && d.targetTop > 0 && d.targetBottom && d.targetBottom > 0) { + height = Math.max(5, d.targetTop - d.targetBottom); + } + return height; + } - chart().focus.selectAll('.g-duration-rect').transition() - .attr('width', function (d) { - return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills)); - }); + function rectTranslate (d) { + var top = 50; + if (d.eventType === 'Temporary Target') { + top = d.targetTop === d.targetBottom ? d.targetTop + rectHeight(d) : d.targetTop; + } + return 'translate(' + chart().xScale(getOrAddDate(d)) + ',' + chart().yScale(utils.scaleMgdl(top)) + ')'; + } - chart().focus.selectAll('.g-duration-text').transition() - .attr('transform', function (d) { - return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills)))/2 + ',' + 10 + ')'; - }); + function treatmentRectWidth (d) { + if (d.durationType === "indefinite") { + return chart().xScale(chart().xScale.domain()[1].getTime()) - chart().xScale(getOrAddDate(d)); + } else { + return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(getOrAddDate(d)); + } + } - // if new rect then just display - var gs = treatRects.enter().append('g') - .attr('class','g-duration') - .attr('transform', function (d) { - return 'translate(' + chart().xScale(new Date(d.mills)) + ',' + chart().yScale(utils.scaleMgdl(50)) + ')'; - }) - .on('mouseover', function (d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + function treatmentTextTransform (d) { + if (d.durationType === "indefinite") { + var offset = 0; + if (chart().xScale(getOrAddDate(d)) < chart().xScale(chart().xScale.domain()[0].getTime())) { + offset = chart().xScale(nowDate) - chart().xScale(getOrAddDate(d)); + } + return 'translate(' + offset + ',' + 10 + ')'; + } else { + return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(getOrAddDate(d))) / 2 + ',' + 10 + ')'; + } + } + + function treatmentText (d) { + if (d.eventType === 'Temporary Target') { + return ''; + } + return d.notes || d.reason || d.eventType; + } + + function treatmentTextAnchor (d) { + return d.durationType === "indefinite" ? 'left' : 'middle'; + } + + // if transitioning, update rect text, position, and width + var rectUpdates = treatRects; + rectUpdates.attr('transform', rectTranslate); + + rectUpdates.select('text') + .text(treatmentText) + .attr('text-anchor', treatmentTextAnchor) + .attr('transform', treatmentTextTransform); + + rectUpdates.select('rect') + .attr('width', treatmentRectWidth) + + // if new rect then create new elements + var newRects = treatRects.enter().append('g') + .attr('class', 'g-duration') + .attr('transform', rectTranslate) + .on('mouseover', function(d) { + client.tooltip.style('opacity', .9); client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d)) - .style('left', (d3.event.pageX) + 'px') + .style('left', tooltipLeft()) .style('top', (d3.event.pageY + 15) + 'px'); }) .on('mouseout', hideTooltip); - gs.append('rect') + newRects.append('rect') .attr('class', 'g-duration-rect') - .attr('width', function (d) { - return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills)); - }) - .attr('height', 20) + .attr('width', treatmentRectWidth) + .attr('height', rectHeight) .attr('rx', 5) .attr('ry', 5) - //.attr('stroke-width', 2) .attr('opacity', .2) - //.attr('stroke', 'white') .attr('fill', fillColor); - gs.append('text') + newRects.append('text') .attr('class', 'g-duration-text') .style('font-size', 15) .attr('fill', 'white') - .attr('text-anchor', 'middle') + .attr('text-anchor', treatmentTextAnchor) .attr('dy', '.35em') - .attr('transform', function (d) { - return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills)))/2 + ',' + 10 + ')'; - }) - .text(function (d) { - return d.notes; - }); + .attr('transform', treatmentTextTransform) + .text(treatmentText); - - treatRects.attr('clip-path', 'url(#clip)'); + // Remove any rects no longer needed + treatRects.exit().remove(); }; - renderer.addContextCircles = function addContextCircles ( ) { + + + renderer.addContextCircles = function addContextCircles () { // bind up the context chart data to an array of circles - var contextCircles = chart().context.selectAll('circle').data(client.data); + var contextCircles = chart().context.selectAll('circle').data(client.entries); - function prepareContextCircles(sel) { + function prepareContextCircles (sel) { var badData = []; - sel.attr('cx', function (d) { return chart().xScale2(new Date(d.mills)); }) - .attr('cy', function (d) { + sel.attr('cx', function(d) { return chart().xScale2(getOrAddDate(d)); }) + .attr('cy', function(d) { var scaled = client.sbx.scaleEntry(d); if (isNaN(scaled)) { badData.push(d); @@ -331,11 +482,11 @@ function init (client, d3) { return chart().yScale2(scaled); } }) - .attr('fill', function (d) { return d.color; }) - .style('opacity', function (d) { return renderer.highlightBrushPoints(d) }) - .attr('stroke-width', function (d) { return d.type === 'mbg' ? 2 : 0; }) - .attr('stroke', function ( ) { return 'white'; }) - .attr('r', function (d) { return d.type === 'mbg' ? 4 : 2; }); + .attr('fill', function(d) { return d.color; }) + //.style('opacity', function(d) { return renderer.highlightBrushPoints(d) }) + .attr('stroke-width', function(d) { return d.type === 'mbg' ? 2 : 0; }) + .attr('stroke', function() { return 'white'; }) + .attr('r', function(d) { return d.type === 'mbg' ? 4 : 2; }); if (badData.length > 0) { console.warn('Bad Data: isNaN(sgv)', badData); @@ -345,7 +496,7 @@ function init (client, d3) { } // if already existing then transition each circle to its new position - prepareContextCircles(contextCircles.transition()); + prepareContextCircles(contextCircles); // if new circle then just display prepareContextCircles(contextCircles.enter().append('circle')); @@ -353,54 +504,95 @@ function init (client, d3) { contextCircles.exit().remove(); }; - function calcTreatmentRadius(treatment, opts) { - var CR = treatment.CR || 20; - var carbs = treatment.carbs || CR; - var insulin = treatment.insulin || 1; + function calcTreatmentRadius (treatment, opts, carbratio) { + var CR = treatment.CR || carbratio || 20; + var carbsOrInsulin = CR; + if (treatment.carbs) { + carbsOrInsulin = treatment.carbs; + } else if (treatment.insulin) { + carbsOrInsulin = treatment.insulin * CR; + } - var R1 = Math.sqrt(Math.min(carbs, insulin * CR)) / opts.scale - , R2 = Math.sqrt(Math.max(carbs, insulin * CR)) / opts.scale - , R3 = R2 + 8 / opts.scale - ; + // R1 determines the size of the treatment dot + var R1 = Math.sqrt(carbsOrInsulin) / opts.scale + , R2 = R1 + // R3/R4 determine how far from the treatment dot the labels are placed + , R3 = R1 + 8 / opts.scale + , R4 = R1 + 25 / opts.scale; return { R1: R1 , R2: R2 , R3: R3 + , R4: R4 , isNaN: isNaN(R1) || isNaN(R3) || isNaN(R3) }; } - function prepareArc(treatment, radius) { + function prepareArc (treatment, radius, bolusSettings) { var arc_data = [ - { 'element': '', 'color': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': radius.R1 }, - { 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': radius.R2, 'outer': radius.R3 }, + // white carb half-circle on top + { 'element': '', 'color': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': radius.R1 } + , { 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': radius.R2, 'outer': radius.R3 }, + // blue insulin half-circle on bottom { 'element': '', 'color': '#0099ff', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': radius.R1 }, - { 'element': '', 'color': 'transparent', 'start': 1.5708, 'end': 4.7124, 'inner': radius.R2, 'outer': radius.R3 } - ]; + // these form a very short transparent arc along the bottom of an insulin treatment to position the label + // these used to be semicircles from 1.5708 to 4.7124, but that made the tooltip target too big + { 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R3 } + , { 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R4 } + ] + , arc_data_1_elements = []; arc_data[0].outlineOnly = !treatment.carbs; arc_data[2].outlineOnly = !treatment.insulin; if (treatment.carbs > 0) { - arc_data[1].element = Math.round(treatment.carbs) + ' g'; + arc_data_1_elements.push(Math.round(treatment.carbs) + ' g'); + } + + if (treatment.protein > 0) { + arc_data_1_elements.push(Math.round(treatment.protein) + ' g'); + } + + if (treatment.fat > 0) { + arc_data_1_elements.push(Math.round(treatment.fat) + ' g'); + } + + arc_data[1].element = arc_data_1_elements.join(' / '); + + if (treatment.foodType) { + arc_data[1].element = arc_data[1].element + " " + treatment.foodType; } if (treatment.insulin > 0) { - arc_data[3].element = Math.round(treatment.insulin * 100) / 100 + ' U'; + var dosage_units = '' + Math.round(treatment.insulin * 100) / 100; + + var format = treatment.insulin < bolusSettings.renderOver ? bolusSettings.renderFormatSmall : bolusSettings.renderFormat; + + if (_.includes(['concise', 'minimal'], format)) { + dosage_units = (dosage_units + "").replace(/^0/, ""); + } + + 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; } - var arc = d3.svg.arc() - .innerRadius(function (d) { + if (treatment.status) { + arc_data[4].element = translate(treatment.status); + } + + var arc = d3.arc() + .innerRadius(function(d) { return 5 * d.inner; }) - .outerRadius(function (d) { + .outerRadius(function(d) { return 5 * d.outer; }) - .endAngle(function (d) { + .endAngle(function(d) { return d.start; }) - .startAngle(function (d) { + .startAngle(function(d) { return d.end; }); @@ -410,63 +602,358 @@ function init (client, d3) { }; } - function appendTreatments(treatment, arc) { + function isInRect (x, y, rect) { + return !(x < rect.x || x > rect.x + rect.width || y < rect.y || y > rect.y + rect.height); + } + + function appendTreatments (treatment, arc) { + function boluscalcTooltip (treatment) { if (!treatment.boluscalc) { return ''; } var html = '
    '; - html += (treatment.boluscalc.othercorrection ? ''+translate('Other correction')+': ' + parseFloat(treatment.boluscalc.othercorrection).toFixed(2) + 'U
    ' : ''); - html += (treatment.boluscalc.profile ? ''+translate('Profile used')+': ' + treatment.boluscalc.profile + '
    ' : ''); + html += (treatment.boluscalc.othercorrection ? '' + translate('Other correction') + ': ' + parseFloat(treatment.boluscalc.othercorrection).toFixed(2) + 'U
    ' : ''); + html += (treatment.boluscalc.profile ? '' + translate('Profile used') + ': ' + treatment.boluscalc.profile + '
    ' : ''); if (treatment.boluscalc.foods && treatment.boluscalc.foods.length) { html += ''; - for (var fi=0; fi'; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; html += ''; } html += '
    ' + translate('Food') + '
    '+ (f.portion*f.portions).toFixed(1) + ' ' + f.unit + '('+ (f.carbs*f.portions).toFixed(1) + ' g)' + f.name + '' + (f.portion * f.portions).toFixed(1) + ' ' + f.unit + '(' + (f.carbs * f.portions).toFixed(1) + ' g)
    '; } return html; } - - function treatmentTooltip() { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); - client.tooltip.html('' + translate('Time') + ': ' + client.formatTime(new Date(treatment.mills)) + '
    ' + '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '
    ' + - (treatment.carbs ? '' + translate('Carbs') + ': ' + treatment.carbs + '
    ' : '') + - (treatment.insulin ? '' + translate('Insulin') + ': ' + treatment.insulin + '
    ' : '') + - (treatment.glucose ? '' + translate('BG') + ': ' + treatment.glucose + (treatment.glucoseType ? ' (' + translate(treatment.glucoseType) + ')' : '') + '
    ' : '') + - (treatment.enteredBy ? '' + translate('Entered By') + ': ' + treatment.enteredBy + '
    ' : '') + - (treatment.notes ? '' + translate('Notes') + ': ' + treatment.notes : '') + - boluscalcTooltip(treatment) - ) - .style('left', (d3.event.pageX) + 'px') - .style('top', (d3.event.pageY + 15) + 'px'); + + function treatmentTooltip () { + 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); + const decimals = (client.settings.units === 'mmol' ? 10 : 1); + + glucose = Math.round(glucose * decimals) / decimals; + } + + client.tooltip.style('opacity', .9); + client.tooltip.html('' + translate('Time') + ': ' + client.formatTime(getOrAddDate(treatment)) + '
    ' + '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '
    ' + + (treatment.carbs ? '' + translate('Carbs') + ': ' + treatment.carbs + '
    ' : '') + + (treatment.protein ? '' + translate('Protein') + ': ' + treatment.protein + '
    ' : '') + + (treatment.fat ? '' + translate('Fat') + ': ' + treatment.fat + '
    ' : '') + + (treatment.absorptionTime > 0 ? '' + translate('Absorption Time') + ': ' + (Math.round(treatment.absorptionTime / 60.0 * 10) / 10) + 'h' + '
    ' : '') + + (treatment.insulin ? '' + translate('Insulin') + ': ' + utils.toRoundedStr(treatment.insulin, 2) + '
    ' : '') + + (treatment.enteredinsulin ? '' + translate('Combo Bolus') + ': ' + treatment.enteredinsulin + 'U, ' + treatment.splitNow + '% : ' + treatment.splitExt + '%, ' + translate('Duration') + ': ' + treatment.duration + '
    ' : '') + + (treatment.glucose ? '' + translate('BG') + ': ' + glucose + (treatment.glucoseType ? ' (' + translate(treatment.glucoseType) + ')' : '') + '
    ' : '') + + (treatment.enteredBy ? '' + translate('Entered By') + ': ' + treatment.enteredBy + '
    ' : '') + + (treatment.notes ? '' + translate('Notes') + ': ' + treatment.notes : '') + + boluscalcTooltip(treatment) + ) + .style('left', tooltipLeft()) + .style('top', (d3.event.pageY + 15) + 'px'); } + var newTime; + var deleteRect = { x: 0, y: 0, width: 0, height: 0 }; + var insulinRect = { x: 0, y: 0, width: 0, height: 0 }; + var carbsRect = { x: 0, y: 0, width: 0, height: 0 }; + var operation; + renderer.drag = d3.drag() + .on('start', function() { + //console.log(treatment); + var windowWidth = $(client.tooltip.node()).parent().parent().width(); + var left = d3.event.x + TOOLTIP_WIDTH < windowWidth ? d3.event.x : windowWidth - TOOLTIP_WIDTH - 10; + client.tooltip.style('opacity', .9) + .style('left', left + 'px') + .style('top', (d3.event.pageY ? d3.event.pageY + 15 : 40) + 'px'); + + deleteRect = { + x: 0 + , y: 0 + , width: 50 + , height: chart().yScale(chart().yScale.domain()[0]) + }; + chart().drag.append('rect') + .attr('class', 'drag-droparea') + .attr('x', deleteRect.x) + .attr('y', deleteRect.y) + .attr('width', deleteRect.width) + .attr('height', deleteRect.height) + .attr('fill', 'red') + .attr('opacity', 0.4) + .attr('rx', 10) + .attr('ry', 10); + chart().drag.append('text') + .attr('class', 'drag-droparea') + .attr('x', deleteRect.x + deleteRect.width / 2) + .attr('y', deleteRect.y + deleteRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', 'red') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .attr('transform', 'rotate(-90 ' + (deleteRect.x + deleteRect.width / 2) + ',' + (deleteRect.y + deleteRect.height / 2) + ')') + .text(translate('Remove')); + + if (treatment.insulin && treatment.carbs) { + carbsRect = { + x: 0 + , y: 0 + , width: chart().charts.attr('width') + , height: 50 + }; + insulinRect = { + x: 0 + , y: chart().yScale(chart().yScale.domain()[0]) - 50 + , width: chart().charts.attr('width') + , height: 50 + }; + chart().drag.append('rect') + .attr('class', 'drag-droparea') + .attr('x', carbsRect.x) + .attr('y', carbsRect.y) + .attr('width', carbsRect.width) + .attr('height', carbsRect.height) + .attr('fill', 'white') + .attr('opacitys', 0.4) + .attr('rx', 10) + .attr('ry', 10); + chart().drag.append('text') + .attr('class', 'drag-droparea') + .attr('x', carbsRect.x + carbsRect.width / 2) + .attr('y', carbsRect.y + carbsRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', 'white') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .text(translate('Move carbs')); + chart().drag.append('rect') + .attr('class', 'drag-droparea') + .attr('x', insulinRect.x) + .attr('y', insulinRect.y) + .attr('width', insulinRect.width) + .attr('height', insulinRect.height) + .attr('fill', '#0099ff') + .attr('opacity', 0.4) + .attr('rx', 10) + .attr('ry', 10); + chart().drag.append('text') + .attr('class', 'drag-droparea') + .attr('x', insulinRect.x + insulinRect.width / 2) + .attr('y', insulinRect.y + insulinRect.height / 2) + .attr('font-size', 15) + .attr('font-weight', 'bold') + .attr('fill', '#0099ff') + .attr('text-anchor', 'middle') + .attr('dy', '.35em') + .text(translate('Move insulin')); + } + + chart().basals.attr('display', 'none'); + + operation = 'Move'; + }) + .on('drag', function() { + //console.log(d3.event); + client.tooltip.style('opacity', .9); + var x = Math.min(Math.max(0, d3.event.x), chart().charts.attr('width')); + var y = Math.min(Math.max(0, d3.event.y), chart().focusHeight); + + operation = 'Move'; + if (isInRect(x, y, deleteRect) && isInRect(x, y, insulinRect)) { + operation = 'Remove insulin'; + } else if (isInRect(x, y, deleteRect) && isInRect(x, y, carbsRect)) { + operation = 'Remove carbs'; + } else if (isInRect(x, y, deleteRect)) { + operation = 'Remove'; + } else if (isInRect(x, y, insulinRect)) { + operation = 'Move insulin'; + } else if (isInRect(x, y, carbsRect)) { + operation = 'Move carbs'; + } + + newTime = new Date(chart().xScale.invert(x)); + var minDiff = times.msecs(newTime.getTime() - treatment.mills).mins.toFixed(0); + client.tooltip.html( + '' + translate('Operation') + ': ' + translate(operation) + '
    ' + + '' + translate('New time') + ': ' + newTime.toLocaleTimeString() + '
    ' + + '' + translate('Difference') + ': ' + (minDiff > 0 ? '+' : '') + minDiff + ' ' + translate('mins') + ); + + chart().drag.selectAll('.arrow').remove(); + chart().drag.append('line') + .attr('class', 'arrow') + .attr('marker-end', 'url(#arrow)') + .attr('x1', chart().xScale(getOrAddDate(treatment))) + .attr('y1', chart().yScale(client.sbx.scaleEntry(treatment))) + .attr('x2', x) + .attr('y2', y) + .attr('stroke-width', 2) + .attr('stroke', 'white'); + }) + .on('end', function() { + var newTreatment; + chart().drag.selectAll('.drag-droparea').remove(); + hideTooltip(); + switch (operation) { + case 'Move': + if (window.confirm(translate('Change treatment time to %1 ?', { params: [newTime.toLocaleTimeString()] }))) { + client.socket.emit( + 'dbUpdate', { + collection: 'treatments' + , _id: treatment._id + , data: { created_at: newTime.toISOString() } + } + , function callback (result) { + console.log(result); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); + } + ); + } else { + chart().drag.selectAll('.arrow').remove(); + } + break; + case 'Remove insulin': + if (window.confirm(translate('Remove insulin from treatment ?'))) { + client.socket.emit( + 'dbUpdateUnset', { + collection: 'treatments' + , _id: treatment._id + , data: { insulin: 1 } + } + , function callback (result) { + console.log(result); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); + } + ); + } else { + chart().drag.selectAll('.arrow').remove(); + } + break; + case 'Remove carbs': + if (window.confirm(translate('Remove carbs from treatment ?'))) { + client.socket.emit( + 'dbUpdateUnset', { + collection: 'treatments' + , _id: treatment._id + , data: { carbs: 1 } + } + , function callback (result) { + console.log(result); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); + } + ); + } else { + chart().drag.selectAll('.arrow').remove(); + } + break; + case 'Remove': + if (window.confirm(translate('Remove treatment ?'))) { + client.socket.emit( + 'dbRemove', { + collection: 'treatments' + , _id: treatment._id + } + , function callback (result) { + console.log(result); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); + } + ); + } else { + chart().drag.selectAll('.arrow').remove(); + } + break; + case 'Move insulin': + if (window.confirm(translate('Change insulin time to %1 ?', { params: [newTime.toLocaleTimeString()] }))) { + client.socket.emit( + 'dbUpdateUnset', { + collection: 'treatments' + , _id: treatment._id + , data: { insulin: 1 } + } + ); + newTreatment = _.cloneDeep(treatment); + delete newTreatment._id; + delete newTreatment.NSCLIENT_ID; + delete newTreatment.carbs; + newTreatment.created_at = newTime.toISOString(); + client.socket.emit( + 'dbAdd', { + collection: 'treatments' + , data: newTreatment + } + , function callback (result) { + console.log(result); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); + } + ); + } else { + chart().drag.selectAll('.arrow').remove(); + } + break; + case 'Move carbs': + if (window.confirm(translate('Change carbs time to %1 ?', { params: [newTime.toLocaleTimeString()] }))) { + client.socket.emit( + 'dbUpdateUnset', { + collection: 'treatments' + , _id: treatment._id + , data: { carbs: 1 } + } + ); + newTreatment = _.cloneDeep(treatment); + delete newTreatment._id; + delete newTreatment.NSCLIENT_ID; + delete newTreatment.insulin; + newTreatment.created_at = newTime.toISOString(); + client.socket.emit( + 'dbAdd', { + collection: 'treatments' + , data: newTreatment + } + , function callback (result) { + console.log(result); + chart().drag.selectAll('.arrow').style('opacity', 0).remove(); + } + ); + } else { + chart().drag.selectAll('.arrow').remove(); + } + break; + } + chart().basals.attr('display', ''); + }); + var treatmentDots = chart().focus.selectAll('treatment-insulincarbs') .data(arc.data) .enter() .append('g') - .attr('transform', 'translate(' + chart().xScale(new Date(treatment.mills)) + ', ' + chart().yScale(client.sbx.scaleEntry(treatment)) + ')') + .attr('class', 'draggable-treatment') + .attr('transform', 'translate(' + chart().xScale(getOrAddDate(treatment)) + ', ' + chart().yScale(client.sbx.scaleEntry(treatment)) + ')') .on('mouseover', treatmentTooltip) .on('mouseout', hideTooltip); + if (client.editMode) { + treatmentDots + .style('cursor', 'move') + .call(renderer.drag); + } treatmentDots.append('path') .attr('class', 'path') - .attr('fill', function (d) { + .attr('fill', function(d) { return d.outlineOnly ? 'transparent' : d.color; }) - .attr('stroke-width', function (d) { + .attr('stroke-width', function(d) { return d.outlineOnly ? 1 : 0; }) - .attr('stroke', function (d) { + .attr('stroke', function(d) { return d.color; }) - .attr('id', function (d, i) { + .attr('id', function(d, i) { return 's' + i; }) .attr('d', arc.svg); @@ -474,7 +961,7 @@ function init (client, d3) { return treatmentDots; } - function appendLabels(treatmentDots, arc, opts) { + function appendLabels (treatmentDots, arc, opts) { // labels for carbs and insulin if (opts.showLabels) { var label = treatmentDots.append('g') @@ -483,69 +970,124 @@ function init (client, d3) { .style('fill', 'white'); label.append('text') - .style('font-size', 40 / opts.scale) + .style('font-size', function(d) { + var fontSize = ( (opts.treatments >= 30) ? 40 : 50 - Math.floor((25 - opts.treatments) / 30 * 10) ) / opts.scale; + var elementValue = parseFloat(d.element); + if (!isNaN(elementValue) && elementValue < 1) { + fontSize = (25 + Math.floor(elementValue * 10)) / opts.scale; + } + return fontSize; + }) .style('text-shadow', '0px 0px 10px rgba(0, 0, 0, 1)') .attr('text-anchor', 'middle') .attr('dy', '.35em') - .attr('transform', function (d) { + .attr('transform', function(d) { d.outerRadius = d.outerRadius * 2.1; d.innerRadius = d.outerRadius * 2.1; return 'translate(' + arc.svg.centroid(d) + ')'; }) - .text(function (d) { + .text(function(d) { return d.element; }); } } - renderer.drawTreatment = function drawTreatment(treatment, opts) { - if (!treatment.carbs && !treatment.insulin) { + renderer.drawTreatments = function drawTreatments (client) { + + var treatmentCount = 0; + var bolusSettings = client.settings.extendedSettings.bolus || {}; + + chart().focus.selectAll('.draggable-treatment').remove(); + + _.forEach(client.ddata.treatments, function eachTreatment (d) { + if (Number(d.insulin) > 0 || Number(d.carbs) > 0) { treatmentCount += 1; } + }); + + // add treatment bubbles + _.forEach(client.ddata.treatments, function eachTreatment (d) { + 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()) + , bolusSettings); + }); + }; + + renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio, bolusSettings) { + if (!treatment.carbs && !treatment.protein && !treatment.fat && !treatment.insulin) { return; } //when the tests are run window isn't available var innerWidth = window && window.innerWidth || -1; // don't render the treatment if it's not visible - if (Math.abs(chart().xScale(new Date(treatment.mills))) > innerWidth) { + if (Math.abs(chart().xScale(getOrAddDate(treatment))) > innerWidth) { return; } - var radius = calcTreatmentRadius(treatment, opts); + var radius = calcTreatmentRadius(treatment, opts, carbratio); if (radius.isNaN) { console.warn('Bad Data: Found isNaN value in treatment', treatment); return; } - var arc = prepareArc(treatment, radius); + var arc = prepareArc(treatment, radius, bolusSettings); var treatmentDots = appendTreatments(treatment, arc); appendLabels(treatmentDots, arc, opts); }; renderer.addBasals = function addBasals (client) { + if (!client.settings.isEnabled('basal')) { + return; + } var mode = client.settings.extendedSettings.basal.render; var profile = client.sbx.data.profile; var linedata = []; var notemplinedata = []; var basalareadata = []; var tempbasalareadata = []; - var from = chart().brush.extent()[0].getTime(); - var to = Math.max(chart().brush.extent()[1].getTime(), client.sbx.time) + client.forecastTime; - + var comboareadata = []; + var selectedRange = chart().createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); + var date = from; var lastbasal = 0; + if (!profile.activeProfileToTime(from)) { + window.alert(translate('Redirecting you to the Profile Editor to create a new profile.')); + try { + window.location.href = '/profile'; + } catch (err) { + //doesn't work when running tests, so catch and ignore + } + return; + } + while (date <= to) { var basalvalue = profile.getTempBasal(date); if (!_.isEqual(lastbasal, basalvalue)) { - linedata.push( { d: date, b: basalvalue.tempbasal } ); - notemplinedata.push( { d: date, b: basalvalue.basal } ); - if (basalvalue.treatment) { - tempbasalareadata.push( { d: date, b: basalvalue.tempbasal } ); - basalareadata.push( { d: date, b: 0 } ); + linedata.push({ d: date, b: basalvalue.totalbasal }); + notemplinedata.push({ d: date, b: basalvalue.basal }); + if (basalvalue.combobolustreatment && basalvalue.combobolustreatment.relative) { + tempbasalareadata.push({ d: date, b: basalvalue.tempbasal }); + basalareadata.push({ d: date, b: 0 }); + comboareadata.push({ d: date, b: basalvalue.totalbasal }); + } else if (basalvalue.treatment) { + tempbasalareadata.push({ d: date, b: basalvalue.totalbasal }); + basalareadata.push({ d: date, b: 0 }); + comboareadata.push({ d: date, b: 0 }); } else { - basalareadata.push( { d: date, b: basalvalue.tempbasal } ); - tempbasalareadata.push( { d: date, b: 0 } ); + tempbasalareadata.push({ d: date, b: 0 }); + basalareadata.push({ d: date, b: basalvalue.totalbasal }); + comboareadata.push({ d: date, b: 0 }); } } lastbasal = basalvalue; @@ -554,12 +1096,15 @@ function init (client, d3) { var toTempBasal = profile.getTempBasal(to); - linedata.push( { d: to, b: toTempBasal.tempbasal } ); - notemplinedata.push( { d: to, b: toTempBasal.basal } ); - basalareadata.push( { d: to, b: toTempBasal.basal } ); - tempbasalareadata.push( { d: to, b: toTempBasal.tempbasal } ); + linedata.push({ d: to, b: toTempBasal.totalbasal }); + notemplinedata.push({ d: to, b: toTempBasal.basal }); + basalareadata.push({ d: to, b: toTempBasal.basal }); + tempbasalareadata.push({ d: to, b: toTempBasal.totalbasal }); + comboareadata.push({ d: to, b: toTempBasal.totalbasal }); - var max = d3.max(linedata, function (d) { return d.b; }); + var max_linedata = d3.max(linedata, function(d) { return d.b; }); + var max_notemplinedata = d3.max(notemplinedata, function(d) { return d.b; }); + var max = Math.max(max_linedata, max_notemplinedata) * ('icicle' === mode ? 1 : 1.1); chart().maxBasalValue = max; chart().yScaleBasals.domain('icicle' === mode ? [0, max] : [max, 0]); @@ -568,18 +1113,19 @@ function init (client, d3) { chart().basals.selectAll('.notempline').remove().data(notemplinedata); chart().basals.selectAll('.basalarea').remove().data(basalareadata); chart().basals.selectAll('.tempbasalarea').remove().data(tempbasalareadata); + chart().basals.selectAll('.comboarea').remove().data(comboareadata); - var valueline = d3.svg.line() - .interpolate('step-after') + var valueline = d3.line() .x(function(d) { return chart().xScaleBasals(d.d); }) - .y(function(d) { return chart().yScaleBasals(d.b); }); + .y(function(d) { return chart().yScaleBasals(d.b); }) + .curve(d3.curveStepAfter); - var area = d3.svg.area() - .interpolate('step-after') + var area = d3.area() .x(function(d) { return chart().xScaleBasals(d.d); }) .y0(chart().yScaleBasals(0)) - .y1(function(d) { return chart().yScaleBasals(d.b); }); - + .y1(function(d) { return chart().yScaleBasals(d.b); }) + .curve(d3.curveStepAfter); + var g = chart().basals.append('g'); g.append('path') @@ -587,8 +1133,7 @@ function init (client, d3) { .attr('stroke', '#0099ff') .attr('stroke-width', 1) .attr('fill', 'none') - .attr('d', valueline(linedata)) - .attr('clip-path', 'url(#clip)'); + .attr('d', valueline(linedata)); g.append('path') .attr('class', 'line notempline') @@ -596,8 +1141,7 @@ function init (client, d3) { .attr('stroke-width', 1) .attr('stroke-dasharray', ('3, 3')) .attr('fill', 'none') - .attr('d', valueline(notemplinedata)) - .attr('clip-path', 'url(#clip)'); + .attr('d', valueline(notemplinedata)); g.append('path') .attr('class', 'area basalarea') @@ -614,20 +1158,27 @@ function init (client, d3) { .attr('fill-opacity', .2) .attr('stroke-width', 1) .attr('d', area); - //console.log(tempbasals); - client.tempbasaltreatments.forEach(function (t) { + g.append('path') + .attr('class', 'area comboarea') + .datum(comboareadata) + .attr('fill', 'url(#hash)') + .attr('fill-opacity', .2) + .attr('stroke-width', 1) + .attr('d', area); + + _.forEach(client.ddata.tempbasalTreatments, function eachTemp (t) { // only if basal and focus interval overlap and there is a chance to fit - if (t.mills < to && t.mills + times.mins(t.duration).msecs > from) { + if (t.duration && t.mills < to && t.mills + times.mins(t.duration).msecs > from) { var text = g.append('text') .attr('class', 'tempbasaltext') .style('font-size', 15) .attr('fill', '#0099ff') .attr('text-anchor', 'middle') .attr('dy', '.35em') - .attr('x', chart().xScaleBasals((Math.max(t.mills, from) + Math.min(t.mills + times.mins(t.duration).msecs, to))/2)) + .attr('x', chart().xScaleBasals((Math.max(t.mills, from) + Math.min(t.mills + times.mins(t.duration).msecs, to)) / 2)) .attr('y', 10) - .text((t.percent ? (t.percent > 0 ? '+' : '') + t.percent + '%' : '') + (isNaN(t.absolute) ? '' : t.absolute + 'U')); + .text((t.percent ? (t.percent > 0 ? '+' : '') + t.percent + '%' : '') + (isNaN(t.absolute) ? '' : Number(t.absolute).toFixed(2) + 'U') + (t.relative ? 'C: +' + t.relative + 'U' : '')); // better hide if not fit if (text.node().getBBox().width > chart().xScaleBasals(t.mills + times.mins(t.duration).msecs) - chart().xScaleBasals(t.mills)) { text.attr('display', 'none'); @@ -642,43 +1193,65 @@ function init (client, d3) { if (client.profilefunctions.listBasalProfiles().length < 2) { return; // do not visualize profiles if there is only one } - + function profileTooltip (d) { - return ''+translate('Time')+': ' + client.formatTime(new Date(d.mills)) + '
    ' + - (d.eventType ? ''+translate('Treatment type')+': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
    ' : '') + - (d.profile ? ''+translate('Profile')+': ' + d.profile + '
    ' : '') + - (d.enteredBy ? ''+translate('Entered By')+': ' + d.enteredBy + '
    ' : '') + - (d.notes ? ''+translate('Notes')+': ' + d.notes : ''); + return '' + translate('Time') + ': ' + client.formatTime(getOrAddDate(d)) + '
    ' + + (d.eventType ? '' + translate('Treatment type') + ': ' + translate(client.careportal.resolveEventName(d.eventType)) + '
    ' : '') + + (d.endprofile ? '' + translate('End of profile') + ': ' + d.endprofile + '
    ' : '') + + (d.profile ? '' + translate('Profile') + ': ' + d.profile + '
    ' : '') + + (d.duration ? '' + translate('Duration') + ': ' + d.duration + translate('mins') + '
    ' : '') + + (d.enteredBy ? '' + translate('Entered By') + ': ' + d.enteredBy + '
    ' : '') + + (d.notes ? '' + translate('Notes') + ': ' + d.notes : ''); } // calculate position of profile on left side - var from = chart().brush.extent()[0].getTime(); - var to = chart().brush.extent()[1].getTime(); - var mult = (to-from) / times.hours(24).msecs; + var selectedRange = chart().createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); + var mult = (to - from) / times.hours(24).msecs; from += times.mins(20 * mult).msecs; - + var mode = client.settings.extendedSettings.basal.render; - var data = client.treatments.filter(function(treatment) { - return treatment.eventType === 'Profile Switch'; - }); + var data = client.ddata.profileTreatments.slice(); data.push({ //eventType: 'Profile Switch' profile: client.profilefunctions.activeProfileToTime(from) , mills: from , first: true }); + + _.forEach(client.ddata.profileTreatments, function eachTreatment (d) { + if (d.duration && !d.cuttedby) { + data.push({ + cutting: d.profile + , profile: client.profilefunctions.activeProfileToTime(times.mins(d.duration).msecs + d.mills + 1) + , mills: times.mins(d.duration).msecs + d.mills + , end: true + }); + } + }); + var treatProfiles = chart().basals.selectAll('.g-profile').data(data); var topOfText = ('icicle' === mode ? chart().maxBasalValue + 0.05 : -0.05); - treatProfiles.transition() - .attr('transform', function (t) { + + var generateText = function(t) { + var sign = t.first ? '▲▲▲' : '▬▬▬'; + var ret; + if (t.cutting) { + ret = sign + ' ' + t.cutting + ' ' + '►►►' + ' ' + t.profile + ' ' + sign; + } else { + ret = sign + ' ' + t.profile + ' ' + sign; + } + return ret; + }; + + treatProfiles.attr('transform', function(t) { // change text of record on left side - if (t.first) { - $(this).text('↑↑↑ ' + t.profile + ' ↑↑↑'); - } return 'rotate(-90,' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ') ' + - 'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')'; - }); + 'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')'; + }). + text(generateText); treatProfiles.enter().append('text') .attr('class', 'g-profile') @@ -687,22 +1260,20 @@ function init (client, d3) { .attr('fill', '#0099ff') .attr('text-anchor', 'end') .attr('dy', '.35em') - .attr('transform', function (t) { + .attr('transform', function(t) { return 'rotate(-90 ' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ') ' + - 'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')' - }) - .text(function (t) { - var sign = t.first ? '↑↑↑' : '---'; - return sign + ' ' + t.profile + ' ' + sign; + 'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')'; }) - .on('mouseover', function (d) { - client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9); + .text(generateText) + .on('mouseover', function(d) { + client.tooltip.style('opacity', .9); client.tooltip.html(profileTooltip(d)) .style('left', (d3.event.pageX) + 'px') .style('top', (d3.event.pageY + 15) + 'px'); }) .on('mouseout', hideTooltip); + treatProfiles.exit().remove(); }; return renderer; diff --git a/lib/client/ticks.js b/lib/client/ticks.js index 093a2775ebf..95be3e2cce3 100644 --- a/lib/client/ticks.js +++ b/lib/client/ticks.js @@ -4,7 +4,7 @@ function prepare (client, opts) { opts = checkOptions(client, opts); if (opts.scaleY === 'linear') { - return prepareLinear(client, opts); + return prepareLinear(client); } else { return prepareLog(client, opts); } @@ -47,7 +47,7 @@ function prepareLog (client, opts) { } } -function prepareLinear (client, opts) { +function prepareLinear (client) { if (client.settings.units === 'mmol') { return [ 2.0 diff --git a/lib/constants.json b/lib/constants.json index 31a4524b4d3..fea33d4bd58 100644 --- a/lib/constants.json +++ b/lib/constants.json @@ -4,5 +4,22 @@ "HTTP_UNAUTHORIZED" : 401, "HTTP_VALIDATION_ERROR" : 422, "HTTP_INTERNAL_ERROR" : 500, - "ENTRIES_DEFAULT_COUNT" : 10 + "HTTP_BAD_REQUEST": 400, + "ENTRIES_DEFAULT_COUNT" : 10, + "PROFILES_DEFAULT_COUNT" : 10, + "MMOL_TO_MGDL": 18.018018018, + "ONE_DAY" : 86400000, + "TWO_DAYS" : 172800000, + "FIFTEEN_MINUTES": 900000, + "THIRTY_MINUTES": 1800000, + "ONE_HOUR": 3600000, + "THREE_HOURS": 10800000, + "FOUR_HOURS": 14400000, + "SIX_HOURS": 21600000, + "LEVEL_URGENT": 2, + "LEVEL_WARN": 1, + "LEVEL_INFO": 0, + "LEVEL_LOW": -1, + "LEVEL_LOWEST": -2, + "LEVEL_NONE": -3 } diff --git a/lib/data.js b/lib/data.js deleted file mode 100644 index 93044f70d31..00000000000 --- a/lib/data.js +++ /dev/null @@ -1,376 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var async = require('async'); -var ObjectID = require('mongodb').ObjectID; -var rawbg = require('../lib/plugins/rawbg')(); - -var ONE_DAY = 86400000 - , TWO_DAYS = 172800000; - -function uniq(a) { - var seen = {}; - return a.filter(function (item) { - return seen.hasOwnProperty(item.mills) ? false : (seen[item.mills] = true); - }); -} - -function init(env, ctx) { - - var data = { - sgvs: [] - , treatments: [] - , mbgs: [] - , cals: [] - , profiles: [] - , devicestatus: {} - , lastUpdated: 0 - }; - - data.clone = function clone() { - return _.cloneDeep(data, function (value) { - //special handling of mongo ObjectID's - //see https://github.com/lodash/lodash/issues/602#issuecomment-47414964 - if (value instanceof ObjectID) { - return value.toString(); - } - }); - }; - - data.update = function update(done) { - - console.log('running data.update'); - data.lastUpdated = Date.now(); - - function loadComplete (err, result) { - data.treatments = _.uniq(data.treatments, false, function (item) { return item._id.toString(); }); - data.updateTreatmentDisplayBGs(); - if (err) { - console.error(err); - } - done(err, result); - } - - // clear treatments, we're going to merge from more queries - data.treatments = []; - - async.parallel([ - loadEntries.bind(null, data, ctx) - , loadTreatments.bind(null, data, ctx) - , loadProfileSwitchTreatments.bind(null, data, ctx) - , loadSensorTreatments.bind(null, data, ctx) - , loadProfile.bind(null, data, ctx) - , loadDeviceStatus.bind(null, data, ctx) - ], loadComplete); - - }; - - data.calculateDelta = function calculateDelta(lastData) { - return data.calculateDeltaBetweenDatasets(lastData,data); - }; - - data.calculateDeltaBetweenDatasets = function calculateDeltaBetweenDatasets(oldData,newData) { - - var delta = {'delta': true}; - var changesFound = false; - - // if there's no updates done so far, just return the full set - if (!oldData.sgvs) { return newData; } - - function nsArrayDiff(oldArray, newArray) { - var seen = {}; - var l = oldArray.length; - for (var i = 0; i < l; i++) { - seen[oldArray[i].mills] = true; - } - var result = []; - l = newArray.length; - for (var j = 0; j < l; j++) { - if (!seen.hasOwnProperty(newArray[j].mills)) { - result.push(newArray[j]); - } - } - return result; - } - - function sort(values) { - values.sort(function sorter(a, b) { - return a.mills - b.mills; - }); - } - - function compressArrays(delta, newData) { - // array compression - var compressibleArrays = ['sgvs', 'treatments', 'mbgs', 'cals']; - var changesFound = false; - - for (var array in compressibleArrays) { - if (compressibleArrays.hasOwnProperty(array)) { - var a = compressibleArrays[array]; - if (newData.hasOwnProperty(a)) { - - // if previous data doesn't have the property (first time delta?), just assign data over - if (!oldData.hasOwnProperty(a)) { - delta[a] = newData[a]; - changesFound = true; - continue; - } - - // Calculate delta and assign delta over if changes were found - var deltaData = nsArrayDiff(oldData[a], newData[a]); - if (deltaData.length > 0) { - console.log('delta changes found on', a); - changesFound = true; - sort(deltaData); - delta[a] = deltaData; - } - } - } - } - return {'delta': delta, 'changesFound': changesFound}; - } - - function deleteSkippables(delta,newData) { - // objects - var skippableObjects = ['profiles', 'devicestatus']; - var changesFound = false; - - for (var object in skippableObjects) { - if (skippableObjects.hasOwnProperty(object)) { - var o = skippableObjects[object]; - if (newData.hasOwnProperty(o)) { - if (JSON.stringify(newData[o]) !== JSON.stringify(oldData[o])) { - console.log('delta changes found on', o); - changesFound = true; - delta[o] = newData[o]; - } - } - } - } - return {'delta': delta, 'changesFound': changesFound}; - } - - delta.lastUpdated = newData.lastUpdated; - - var compressedDelta = compressArrays(delta, newData); - delta = compressedDelta.delta; - if (compressedDelta.changesFound) { changesFound = true; } - - var skippedDelta = deleteSkippables(delta, newData); - delta = skippedDelta.delta; - if (skippedDelta.changesFound) { changesFound = true; } - - if (changesFound) { return delta; } - return newData; - - }; - - data.updateTreatmentDisplayBGs = function updateTreatmentDisplayBGs () { - function updateTreatmentBG(treatment) { - - function mgdlByTime() { - - var withBGs = _.filter(data.sgvs, function(d) { - return d.mgdl > 39 || env.settings.isEnabled('rawbg'); - }); - - var beforeTreatment = _.findLast(withBGs, function (d) { - return d.mills <= treatment.mills; - }); - var afterTreatment = _.find(withBGs, function (d) { - return d.mills >= treatment.mills; - }); - - var mgdlBefore = mgdlValue(beforeTreatment) || calcRaw(beforeTreatment); - var mgdlAfter = mgdlValue(afterTreatment) || calcRaw(afterTreatment); - - var calcedBG = 0; - if (mgdlBefore && mgdlAfter) { - calcedBG = (mgdlBefore + mgdlAfter) / 2; - } else if (mgdlBefore) { - calcedBG = mgdlBefore; - } else if (mgdlAfter) { - calcedBG = mgdlAfter; - } - - return calcedBG || 400; - } - - function mgdlValue (entry) { - return entry && entry.mgdl >= 39 && Number(entry.mgdl); - } - - function calcRaw (entry) { - var raw; - if (entry && env.settings.isEnabled('rawbg')) { - var cal = _.last(data.cals); - if (cal) { - raw = rawbg.calc(entry, cal); - } - } - return raw; - } - - if (treatment.glucose && isNaN(treatment.glucose)) { - console.warn('found an invalid glucose value', treatment); - } else if (treatment.glucose && treatment.units) { - if (treatment.units === 'mmol') { - treatment.mmol = Number(treatment.glucose); - } else { - treatment.mgdl = Number(treatment.glucose); - } - } else if (treatment.glucose) { - //no units, assume everything is the same - console.warn('found a glucose value without any units, maybe from an old version?', treatment); - var units = env.DISPLAY_UNITS === 'mmol' ? 'mmol' : 'mgdl'; - treatment[units] = Number(treatment.glucose); - } else { - treatment.mgdl = mgdlByTime(); - } - } - - _.each(data.treatments, updateTreatmentBG); - - }; - - return data; - -} - -function loadEntries (data, ctx, callback) { - var q = { - find: { - date: { - $gte: data.lastUpdated - TWO_DAYS - } - } - , sort: {date: 1} - }; - - ctx.entries.list(q, function (err, results) { - if (!err && results) { - var mbgs = []; - var sgvs = []; - var cals = []; - results.forEach(function (element) { - if (element) { - if (element.mbg) { - mbgs.push({ - mgdl: Number(element.mbg), mills: element.date, device: element.device - }); - } else if (element.sgv) { - sgvs.push({ - mgdl: Number(element.sgv), mills: element.date, device: element.device, direction: element.direction, filtered: element.filtered, unfiltered: element.unfiltered, noise: element.noise, rssi: element.rssi - }); - } else if (element.type === 'cal') { - cals.push({ - mills: element.date, scale: element.scale, intercept: element.intercept, slope: element.slope - }); - } - } - }); - data.mbgs = uniq(mbgs); - data.sgvs = uniq(sgvs); - data.cals = uniq(cals); - } - callback(); - }); -} - -function mergeToTreatments (data, results) { - var treatments = results.map(function (treatment) { - treatment.mills = new Date(treatment.created_at).getTime(); - - return treatment; - }); - data.treatments = _.union(data.treatments, treatments); -} - -function loadTreatments (data, ctx, callback) { - var tq = { - find: { - created_at: { - $gte: new Date(data.lastUpdated - (ONE_DAY * 8)).toISOString() - } - } - , sort: {created_at: 1} - }; - - ctx.treatments.list(tq, function (err, results) { - if (!err && results) { - mergeToTreatments(data, results); - } - - callback(); - }); -} - -function loadProfileSwitchTreatments (data, ctx, callback) { - var tq = { - find: { - eventType: { - $eq: 'Profile Switch' - } - , created_at: { - $gte: new Date(data.lastUpdated - (ONE_DAY * 31 * 12)).toISOString() - } - } - , sort: {created_at: -1} - }; - - ctx.treatments.list(tq, function (err, results) { - if (!err && results) { - mergeToTreatments(data, results); - } - - callback(); - }); -} - -function loadSensorTreatments (data, ctx, callback) { - var tq = { - find: { - eventType: { - $in: [ 'Sensor Start', 'Sensor Change'] - } - , created_at: { - $gte: new Date(data.lastUpdated - (ONE_DAY * 32)).toISOString() - } - } - , sort: {created_at: -1} - }; - - ctx.treatments.list(tq, function (err, results) { - if (!err && results) { - mergeToTreatments(data, results); - } - - callback(); - }); -} - -function loadProfile (data, ctx, callback) { - ctx.profile.last(function (err, results) { - if (!err && results) { - var profiles = []; - results.forEach(function (element) { - if (element) { - profiles[0] = element; - } - }); - data.profiles = profiles; - } - callback(); - }); -} - -function loadDeviceStatus (data, ctx, callback) { - ctx.devicestatus.last(function (err, result) { - if (!err && result) { - data.devicestatus.uploaderBattery = result.uploaderBattery; - } - callback(); - }); -} - -module.exports = init; \ No newline at end of file diff --git a/lib/data/calcdelta.js b/lib/data/calcdelta.js new file mode 100644 index 00000000000..d58cca21c6f --- /dev/null +++ b/lib/data/calcdelta.js @@ -0,0 +1,162 @@ +'use strict'; + +var _ = require('lodash'); + +module.exports = function calcDelta (oldData, newData) { + + var delta = {'delta': true}; + var changesFound = false; + + // if there's no updates done so far, just return the full set + if (!oldData.sgvs) { return newData; } + + function nsArrayTreatments(oldArray, newArray) { + var result = []; + + // check for add, change + var l = newArray.length; + var m = oldArray.length; + var found, founddiff, no, oo, i, j; + for (i = 0; i < l; i++) { + no = newArray[i]; + no._id = no._id.toString(); + found = false; + founddiff = false; + for (j = 0; j < m; j++) { + oo = oldArray[j]; + oo._id = oo._id.toString(); + if (no._id === oo._id) { + found = true; + var oo_copy = _.clone(oo); + var no_copy = _.clone(no); + delete oo_copy.mgdl; + delete no_copy.mgdl; + if (!_.isEqual(oo_copy, no_copy)) { + founddiff = true; + } + break; + } + } + if (founddiff) { + var nno = _.clone(no); + nno.action = 'update'; + result.push(nno); + } + if (!found) { + result.push(no); + } + } + + //check for delete + for (j = 0; j < m; j++) { + oo = oldArray[j]; + found = false; + for (i = 0; i < l; i++) { + no = newArray[i]; + if (no._id === oo._id) { + found = true; + break; + } + } + if (!found) { + result.push({ _id: oo._id, mills: oo.mills, action: 'remove' }); + } + } + + 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[genKey(oldArray[i])] = true; + } + var result = []; + l = newArray.length; + for (var j = 0; j < l; j++) { + if (!Object.prototype.hasOwnProperty.call(seen, genKey(newArray[j]))) { + result.push(newArray[j]); + } + } + return result; + } + + function sort(values) { + values.sort(function sorter(a, b) { + return a.mills - b.mills; + }); + } + + function compressArrays(delta, newData) { + // array compression + var compressibleArrays = ['sgvs', 'treatments', 'mbgs', 'cals', 'devicestatus']; + var changesFound = false; + + for (var array in compressibleArrays) { + if (Object.prototype.hasOwnProperty.call(compressibleArrays, array)) { + var a = compressibleArrays[array]; + if (Object.prototype.hasOwnProperty.call(newData, a)) { + + // if previous data doesn't have the property (first time delta?), just assign data over + if (!Object.prototype.hasOwnProperty.call(oldData, a)) { + delta[a] = newData[a]; + changesFound = true; + continue; + } + + // Calculate delta and assign delta over if changes were found + var deltaData = (a === 'treatments' ? nsArrayTreatments(oldData[a], newData[a]) : nsArrayDiff(oldData[a], newData[a])); + if (deltaData.length > 0) { + //console.log('delta changes found on', a); + changesFound = true; + sort(deltaData); + delta[a] = deltaData; + } + } + } + } + return {'delta': delta, 'changesFound': changesFound}; + } + + function deleteSkippables(delta,newData) { + // objects + var skippableObjects = ['profiles']; + var changesFound = false; + + for (var object in skippableObjects) { + if (Object.prototype.hasOwnProperty.call(skippableObjects, object)) { + var o = skippableObjects[object]; + if (Object.prototype.hasOwnProperty.call(newData, o)) { + if (JSON.stringify(newData[o]) !== JSON.stringify(oldData[o])) { + //console.log('delta changes found on', o); + changesFound = true; + delta[o] = newData[o]; + } + } + } + } + return {'delta': delta, 'changesFound': changesFound}; + } + + delta.lastUpdated = newData.lastUpdated; + + var compressedDelta = compressArrays(delta, newData); + delta = compressedDelta.delta; + if (compressedDelta.changesFound) { changesFound = true; } + + var skippedDelta = deleteSkippables(delta, newData); + delta = skippedDelta.delta; + if (skippedDelta.changesFound) { changesFound = true; } + + if (changesFound) { return delta; } + return newData; + +}; diff --git a/lib/data/dataloader.js b/lib/data/dataloader.js new file mode 100644 index 00000000000..6361b37b333 --- /dev/null +++ b/lib/data/dataloader.js @@ -0,0 +1,511 @@ +'use strict'; + +const _ = require('lodash'); +const async = require('async'); +const fitTreatmentsToBGCurve = require('./treatmenttocurve'); +const constants = require('../constants'); + +function uniqBasedOnMills(a) { + var seen = {}; + return a.filter(function(item) { + return Object.prototype.hasOwnProperty.call(seen, item.mills) ? false : (seen[item.mills] = true); + }); +} + +const processForRuntime = (obj) => { + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'object' && obj[key]) { + if (obj[key].hasOwnProperty('_id')) { + obj[key]._id = obj[key]._id.toString(); + } + if (obj[key].hasOwnProperty('created_at') && !obj[key].hasOwnProperty('mills')) { + obj[key].mills = new Date(obj[key].created_at).getTime(); + } + } + }); +} + +const findLatestMills = (data) => { + if (!data) return; + let max = data[0].mills; + for (let i = 0, len = data.length; i < len; i++) { + let o = data[i]; + max = o.mills > max ? o.mills : max; + } + return max; +} + +function mergeProcessSort(oldData, newData, ageLimit) { + + processForRuntime(newData); + + var filtered = _.filter(newData, function hasId(object) { + const hasId = !_.isEmpty(object._id); + const isFresh = (ageLimit && object.mills >= ageLimit) || (!ageLimit); + return isFresh && hasId; + }); + + // Merge old and new data, preferring the new objects + + let merged = []; + if (oldData && filtered) { + merged = filtered; // Start with the new / updated data + for (let i = 0; i < oldData.length; i++) { + const oldElement = oldData[i]; + let found = false; + for (let j = 0; j < filtered.length; j++) { + if (oldElement._id == filtered[j]._id) { + found = true; + break; + } + } + if (!found) merged.push(oldElement); // Merge old object in, if it wasn't found in the new data + } + } else { + merged = filtered; + } + + return _.sortBy(merged, function(item) { + return item.mills; + }); + +} + +function init(env, ctx) { + + var dataloader = {}; + + dataloader.update = function update(ddata, opts, done) { + + if (opts && done == null && opts.call) { + done = opts; + opts = { + lastUpdated: Date.now(), + frame: false + }; + } + + if (opts.frame) { + ddata.page = { + frame: true, + after: opts.lastUpdated + // , before: opts. + }; + } + ddata.lastUpdated = opts.lastUpdated; + + const normalizeTreatments = (obj) => { + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'object' && obj[key]) { + const element = obj[key]; + if (element.hasOwnProperty('_id')) { + element._id = element._id.toString(); + } + if (element.hasOwnProperty('amount') && !element.hasOwnProperty('absolute')) { + element.absolute = Number(element.amount); + } + normalizeTreatments(obj[key]); + } + }); + } + + function loadComplete(err, result) { + + // convert all IDs to strings, as these are not used after load + + normalizeTreatments(ddata); + + ddata.treatments = _.uniq(ddata.treatments, false, function(item) { + return item._id; + }); + + //sort treatments so the last is the most recent + + ddata.treatments = _.sortBy(ddata.treatments, function(item) { + return item.mills; + }); + + fitTreatmentsToBGCurve(ddata, env, ctx); + if (err) { + console.error(err); + } + ddata.processTreatments(true); + + var counts = []; + _.forIn(ddata, function each(value, key) { + if (_.isArray(value) && value.length > 0) { + counts.push(key + ':' + value.length); + } + }); + + console.info('Load Complete:\n\t', counts.join(', ')); + done(err, result); + } + + // clear data we'll get from the cache + + ddata.treatments = []; + ddata.devicestatus = []; + ddata.entries = []; + + ddata.dbstats = {}; + + async.parallel([ + loadEntries.bind(null, ddata, ctx) + , loadTreatments.bind(null, ddata, ctx) + , loadProfileSwitchTreatments.bind(null, ddata, ctx) + , loadSensorAndInsulinTreatments.bind(null, ddata, ctx) + , loadProfile.bind(null, ddata, ctx) + , loadFood.bind(null, ddata, ctx) + , loadDeviceStatus.bind(null, ddata, env, ctx) + , loadActivity.bind(null, ddata, ctx) + , loadDatabaseStats.bind(null, ddata, ctx) + ], loadComplete); + + }; + + return dataloader; + +} + +function loadEntries(ddata, ctx, callback) { + + const withFrame = ddata.page && ddata.page.frame; + const longLoad = Math.round(constants.TWO_DAYS); + const loadTime = ctx.cache.isEmpty('entries') || withFrame ? longLoad : constants.FIFTEEN_MINUTES; + + var dateRange = { + $gte: ddata.lastUpdated - loadTime + }; + if (withFrame) { + dateRange['$lte'] = ddata.lastUpdated; + } + var q = { + find: { + date: dateRange + }, + sort: { + date: 1 + } + }; + + var obscureDeviceProvenance = ctx.settings.obscureDeviceProvenance; + ctx.entries.list(q, function(err, results) { + + if (err) { + console.log("Problem loading entries"); + } + + if (!err && results) { + + const r = ctx.ddata.processRawDataForRuntime(results); + const currentData = ctx.cache.insertData('entries', r).reverse(); + + const mbgs = []; + const sgvs = []; + const cals = []; + + currentData.forEach(function(element) { + if (element) { + if (!element.mills) element.mills = element.date; + if (element.mbg) { + mbgs.push({ + _id: element._id, + mgdl: Number(element.mbg), + mills: element.date, + device: obscureDeviceProvenance || element.device, + type: 'mbg' + }); + } else if (element.sgv) { + sgvs.push({ + _id: element._id, + mgdl: Number(element.sgv), + mills: element.date, + device: obscureDeviceProvenance || element.device, + direction: element.direction, + filtered: element.filtered, + unfiltered: element.unfiltered, + noise: element.noise, + rssi: element.rssi, + type: 'sgv' + }); + } else if (element.type === 'cal') { + cals.push({ + _id: element._id, + mills: element.date, + scale: element.scale, + intercept: element.intercept, + slope: element.slope, + type: 'cal' + }); + } + } + }); + + const ageLimit = ddata.lastUpdated - constants.TWO_DAYS; + ddata.sgvs = sgvs; + ddata.mbgs = mbgs; + ddata.cals = cals; + } + callback(); + }); +} + +function loadActivity(ddata, ctx, callback) { + var dateRange = { + $gte: new Date(ddata.lastUpdated - (constants.ONE_DAY * 2)).toISOString() + }; + if (ddata.page && ddata.page.frame) { + dateRange['$lte'] = new Date(ddata.lastUpdated).toISOString(); + } + + var q = { + find: { + created_at: dateRange + }, + sort: { + created_at: 1 + } + }; + + ctx.activity.list(q, function(err, results) { + + if (err) { + console.log("Problem loading activity data"); + } + + if (!err && results) { + var activity = []; + results.forEach(function(element) { + if (element) { + if (element.created_at) { + var d = new Date(element.created_at); + activity.push({ + mills: d, + heartrate: element.heartrate, + steps: element.steps, + activitylevel: element.activitylevel + }); + } + } + }); + + ddata.activity = uniqBasedOnMills(activity); + } + callback(); + }); +} + +function loadTreatments(ddata, ctx, callback) { + + const withFrame = ddata.page && ddata.page.frame; + const longLoad = Math.round(constants.ONE_DAY * 2.5); //ONE_DAY * 2.5; + + // Load 2.5 days to cover last 48 hours including overlapping temp boluses or temp targets for first load + // Subsequently load at least 15 minutes of data + + const loadTime = ctx.cache.isEmpty('treatments') || withFrame ? longLoad : constants.FIFTEEN_MINUTES; + + var dateRange = { + $gte: new Date(ddata.lastUpdated - loadTime).toISOString() + }; + if (withFrame) { + dateRange['$lte'] = new Date(ddata.lastUpdated).toISOString(); + } + var tq = { + find: { + created_at: dateRange + }, + sort: { + created_at: 1 + } + }; + + ctx.treatments.list(tq, function(err, results) { + if (!err && results) { + + // update cache and apply to runtime data + const r = ctx.ddata.processRawDataForRuntime(results); + const currentData = ctx.cache.insertData('treatments', r); + ddata.treatments = ctx.ddata.idMergePreferNew(ddata.treatments, currentData); + } + + callback(); + }); +} + +function loadProfileSwitchTreatments(ddata, ctx, callback) { + var dateRange = { + $gte: new Date(ddata.lastUpdated - (constants.ONE_DAY * 31 * 12)).toISOString() + }; + + if (ddata.page && ddata.page.frame) { + dateRange['$lte'] = new Date(ddata.lastUpdated).toISOString(); + } + + // Load the latest profile switch treatment + var tq = { + find: { + eventType: 'Profile Switch', + created_at: dateRange, + duration: 0 + }, + sort: { + created_at: -1 + }, + count: 1 + }; + + ctx.treatments.list(tq, function(err, results) { + if (!err && results) { + ddata.treatments = mergeProcessSort(ddata.treatments, results); + } + + // Store last profile switch + if (results) { + ddata.lastProfileFromSwitch = null; + var now = new Date().getTime(); + for (var p = 0; p < results.length; p++) { + var pdate = new Date(results[p].created_at).getTime(); + if (pdate < now) { + ddata.lastProfileFromSwitch = results[p].profile; + break; + } + } + } + + callback(); + }); +} + +function loadSensorAndInsulinTreatments(ddata, ctx, callback) { + async.parallel([ + loadLatestSingle.bind(null, ddata, ctx, 'Sensor Start') + ,loadLatestSingle.bind(null, ddata, ctx, 'Sensor Change') + ,loadLatestSingle.bind(null, ddata, ctx, 'Sensor Stop') + ,loadLatestSingle.bind(null, ddata, ctx, 'Site Change') + ,loadLatestSingle.bind(null, ddata, ctx, 'Insulin Change') + ,loadLatestSingle.bind(null, ddata, ctx, 'Pump Battery Change') + ], callback); +} + +function loadLatestSingle(ddata, ctx, dataType, callback) { + + var dateRange = { + $gte: new Date(ddata.lastUpdated - (constants.ONE_DAY * 62)).toISOString() + }; + + if (ddata.page && ddata.page.frame) { + dateRange['$lte'] = new Date(ddata.lastUpdated).toISOString(); + } + + var tq = { + find: { + eventType: { + $eq: dataType + }, + created_at: dateRange + }, + sort: { + created_at: -1 + }, + count: 1 + }; + + ctx.treatments.list(tq, function(err, results) { + if (!err && results) { + ddata.treatments = mergeProcessSort(ddata.treatments, results); + } + callback(); + }); +} + +function loadProfile(ddata, ctx, callback) { + ctx.profile.last(function(err, results) { + if (!err && results) { + var profiles = []; + results.forEach(function(element) { + if (element) { + profiles[0] = element; + } + }); + ddata.profiles = profiles; + } + callback(); + }); +} + +function loadFood(ddata, ctx, callback) { + ctx.food.list(function(err, results) { + if (!err && results) { + ddata.food = results; + } + callback(); + }); +} + +function loadDeviceStatus(ddata, env, ctx, callback) { + + const withFrame = ddata.page && ddata.page.frame; + const longLoad = env.extendedSettings.devicestatus && env.extendedSettings.devicestatus.days && env.extendedSettings.devicestatus.days == 2 ? constants.TWO_DAYS : constants.ONE_DAY; + const loadTime = ctx.cache.isEmpty('devicestatus') || withFrame ? longLoad : constants.FIFTEEN_MINUTES; + + var dateRange = { + $gte: new Date( ddata.lastUpdated - loadTime ).toISOString() + }; + + if (withFrame) { + dateRange['$lte'] = new Date(ddata.lastUpdated).toISOString(); + } + + var opts = { + find: { + created_at: dateRange + }, + sort: { + created_at: -1 + } + }; + + ctx.devicestatus.list(opts, function(err, results) { + if (!err && results) { + + // update cache and apply to runtime data + const r = ctx.ddata.processRawDataForRuntime(results); + const currentData = ctx.cache.insertData('devicestatus', r); + + const res2 = _.map(currentData, function eachStatus(result) { + if ('uploaderBattery' in result) { + result.uploader = { + battery: result.uploaderBattery + }; + delete result.uploaderBattery; + } + return result; + }); + + ddata.devicestatus = mergeProcessSort(ddata.devicestatus, res2); + } else { + ddata.devicestatus = []; + } + callback(); + }); +} + +function loadDatabaseStats(ddata, ctx, callback) { + ctx.store.db.stats(function mongoDone (err, result) { + if (err) { + console.log("Problem loading database stats"); + } + if (!err && result) { + ddata.dbstats = { + dataSize: result.dataSize + , indexSize: result.indexSize + }; + } + callback(); + }); +} + +module.exports = init; + diff --git a/lib/data/ddata.js b/lib/data/ddata.js new file mode 100644 index 00000000000..0ee1d9ebc7f --- /dev/null +++ b/lib/data/ddata.js @@ -0,0 +1,312 @@ +'use strict'; + +var _ = require('lodash'); +var times = require('../times'); +var consts = require('../constants'); + +var DEVICE_TYPE_FIELDS = ['uploader', 'pump', 'openaps', 'loop', 'xdripjs']; + +function init () { + + var ddata = { + sgvs: [] + , treatments: [] + , mbgs: [] + , cals: [] + , profiles: [] + , devicestatus: [] + , food: [] + , activity: [] + , dbstats: {} + , lastUpdated: 0 + }; + + /** + * Convert Mongo ids to strings and ensure all objects have the mills property for + * significantly faster processing than constant date parsing, plus simplified + * logic + */ + ddata.processRawDataForRuntime = (data) => { + + let obj = _.cloneDeep(data); + + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'object' && obj[key]) { + if (Object.prototype.hasOwnProperty.call(obj[key], '_id')) { + obj[key]._id = obj[key]._id.toString(); + } + if (Object.prototype.hasOwnProperty.call(obj[key], 'created_at') + && !Object.prototype.hasOwnProperty.call(obj[key], 'mills')) { + obj[key].mills = new Date(obj[key].created_at).getTime(); + } + if (Object.prototype.hasOwnProperty.call(obj[key], 'sysTime') + && !Object.prototype.hasOwnProperty.call(obj[key], 'mills')) { + obj[key].mills = new Date(obj[key].sysTime).getTime(); + } + } + }); + + return obj; + }; + + /** + * Merge two arrays based on _id string, preferring new objects when a collision is found + * @param {array} oldData + * @param {array} newData + */ + ddata.idMergePreferNew = (oldData, newData) => { + + if (!newData && oldData) return oldData; + if (!oldData && newData) return newData; + + const merged = _.cloneDeep(newData); + + for (let i = 0; i < oldData.length; i++) { + const oldElement = oldData[i]; + let found = false; + for (let j = 0; j < newData.length; j++) { + if (oldElement._id == newData[j]._id) { + found = true; + break; + } + } + if (!found) merged.push(oldElement); // Merge old object in, if it wasn't found in the new data + } + + return merged; + }; + + ddata.clone = function clone () { + return _.clone(ddata, function(value) { + //special handling of mongo ObjectID's + //see https://github.com/lodash/lodash/issues/602#issuecomment-47414964 + + //instead of requiring Mongo.ObjectID here and having it get pulled into the bundle + //we'll look for the toHexString function and then assume it's an ObjectID + if (value && value.toHexString && value.toHexString.call && value.toString && value.toString.call) { + return value.toString(); + } + }); + }; + + ddata.dataWithRecentStatuses = function dataWithRecentStatuses () { + var results = {}; + results.devicestatus = ddata.recentDeviceStatus(Date.now()); + results.sgvs = ddata.sgvs; + results.cals = ddata.cals; + + var profiles = _.cloneDeep(ddata.profiles); + if (profiles && profiles[0] && profiles[0].store) { + Object.keys(profiles[0].store).forEach(k => { + if (k.indexOf('@@@@@') > 0) { + delete profiles[0].store[k]; + } + }) + } + results.profiles = profiles; + results.mbgs = ddata.mbgs; + results.food = ddata.food; + results.treatments = ddata.treatments; + results.dbstats = ddata.dbstats; + + return results; + } + + ddata.recentDeviceStatus = function recentDeviceStatus (time) { + + var deviceAndTypes = + _.chain(ddata.devicestatus) + .map(function eachStatus (status) { + return _.chain(status) + .keys() + .filter(function isExcluded (key) { + return _.includes(DEVICE_TYPE_FIELDS, key); + }) + .map(function toDeviceTypeKey (key) { + return { + device: status.device + , type: key + }; + }) + .value(); + }) + .flatten() + .uniqWith(_.isEqual) + .value(); + + //console.info('>>>deviceAndTypes', deviceAndTypes); + + var rv = _.chain(deviceAndTypes) + .map(function findMostRecent (deviceAndType) { + return _.chain(ddata.devicestatus) + .filter(function isSameDeviceType (status) { + return status.device === deviceAndType.device && _.has(status, deviceAndType.type) + }) + .filter(function notInTheFuture (status) { + return status.mills <= time; + }) + .sortBy('mills') + .takeRight(10) + .value(); + }).value(); + + var merged = [].concat.apply([], rv); + + rv = _.chain(merged) + .filter(_.isObject) + .uniq('_id') + .sortBy('mills') + .value(); + + return rv; + + }; + + ddata.processDurations = function processDurations (treatments, keepzeroduration) { + + treatments = _.uniqBy(treatments, 'mills'); + + // cut temp basals by end events + // better to do it only on data update + var endevents = treatments.filter(function filterEnd (t) { + return !t.duration; + }); + + function cutIfInInterval (base, end) { + if (base.mills < end.mills && base.mills + times.mins(base.duration).msecs > end.mills) { + base.duration = times.msecs(end.mills - base.mills).mins; + if (end.profile) { + base.cuttedby = end.profile; + end.cutting = base.profile; + } + } + } + + // cut by end events + treatments.forEach(function allTreatments (t) { + if (t.duration) { + endevents.forEach(function allEndevents (e) { + cutIfInInterval(t, e); + }); + } + }); + + // cut by overlaping events + treatments.forEach(function allTreatments (t) { + if (t.duration) { + treatments.forEach(function allEndevents (e) { + cutIfInInterval(t, e); + }); + } + }); + + if (keepzeroduration) { + return treatments; + } else { + return treatments.filter(function filterEnd (t) { + return t.duration; + }); + } + }; + + ddata.processTreatments = function processTreatments (preserveOrignalTreatments) { + + // filter & prepare 'Site Change' events + ddata.sitechangeTreatments = ddata.treatments.filter(function filterSensor (t) { + return t.eventType.indexOf('Site Change') > -1; + }).sort(function(a, b) { + return a.mills > b.mills; + }); + + // filter & prepare 'Insulin Change' events + ddata.insulinchangeTreatments = ddata.treatments.filter(function filterInsulin (t) { + return t.eventType.indexOf('Insulin Change') > -1; + }).sort(function(a, b) { + return a.mills > b.mills; + }); + + // filter & prepare 'Pump Battery Change' events + ddata.batteryTreatments = ddata.treatments.filter(function filterSensor (t) { + return t.eventType.indexOf('Pump Battery Change') > -1; + }).sort(function(a, b) { + return a.mills > b.mills; + }); + + // filter & prepare 'Sensor' events + ddata.sensorTreatments = ddata.treatments.filter(function filterSensor (t) { + return t.eventType.indexOf('Sensor') > -1; + }).sort(function(a, b) { + return a.mills > b.mills; + }); + + // filter & prepare 'Profile Switch' events + var profileTreatments = ddata.treatments.filter(function filterProfiles (t) { + return t.eventType === 'Profile Switch'; + }).sort(function(a, b) { + return a.mills > b.mills; + }); + if (preserveOrignalTreatments) + profileTreatments = _.cloneDeep(profileTreatments); + ddata.profileTreatments = ddata.processDurations(profileTreatments, true); + + // filter & prepare 'Combo Bolus' events + ddata.combobolusTreatments = ddata.treatments.filter(function filterComboBoluses (t) { + return t.eventType === 'Combo Bolus'; + }).sort(function(a, b) { + return a.mills > b.mills; + }); + + // filter & prepare temp basals + var tempbasalTreatments = ddata.treatments.filter(function filterBasals (t) { + return t.eventType && t.eventType.indexOf('Temp Basal') > -1; + }); + if (preserveOrignalTreatments) + tempbasalTreatments = _.cloneDeep(tempbasalTreatments); + ddata.tempbasalTreatments = ddata.processDurations(tempbasalTreatments, false); + + // filter temp target + var tempTargetTreatments = ddata.treatments.filter(function filterTargets (t) { + return t.eventType && t.eventType.indexOf('Temporary Target') > -1; + }); + + function convertTempTargetTreatmentUnites (_treatments) { + + let treatments = _.cloneDeep(_treatments); + + for (let i = 0; i < treatments.length; i++) { + + let t = treatments[i]; + let converted = false; + + // if treatment is in mmol, convert to mg/dl + if (Object.prototype.hasOwnProperty.call(t,'units')) { + if (t.units == 'mmol') { + //convert to mgdl + t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; + t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; + t.units = 'mg/dl'; + converted = true; + } + } + + //if we have a temp target thats below 20, assume its mmol and convert to mgdl for safety. + if (!converted && (t.targetTop < 20 || t.targetBottom < 20)) { + t.targetTop = t.targetTop * consts.MMOL_TO_MGDL; + t.targetBottom = t.targetBottom * consts.MMOL_TO_MGDL; + t.units = 'mg/dl'; + } + } + return treatments; + } + + if (preserveOrignalTreatments) tempTargetTreatments = _.cloneDeep(tempTargetTreatments); + tempTargetTreatments = convertTempTargetTreatmentUnites(tempTargetTreatments); + ddata.tempTargetTreatments = ddata.processDurations(tempTargetTreatments, false); + + }; + + return ddata; + +} + +module.exports = init; diff --git a/lib/data/endpoints.js b/lib/data/endpoints.js new file mode 100644 index 00000000000..10d041e132a --- /dev/null +++ b/lib/data/endpoints.js @@ -0,0 +1,75 @@ +'use strict'; + +var moment = require('moment'); + +function get_time_spec (spec) { + return moment(spec).toDate(); +} + +function ddata_at (at, ctx, callback) { + var ddata = ctx.ddata.clone( ); + if (Math.abs(at - ddata.lastUpdated) < 1000 * 60 * 5) { + return callback(null, ctx.ddata); + } + ctx.dataloader.update(ddata, {lastUpdated: at, frame: true}, function (err) { + // console.log('results', err, result); + // console.log('ddata', ddata); + callback(err, ddata); + }); +} +function get_ddata (req, res, next) { + ddata_at(req.at.getTime( ), req.ctx, function (err, data) { + res.data = data; + // console.log('fetched results', err, data); + console.error(err); + next(err); + }); +} + +function ensure_at (req, res, next) { + if (!req.at) { + req.at = new Date( ); + } + next( ); +} + +function format_result (req, res, next) { + res.json(res.data); + next( ); +} + +/** + * @method configure + * Configure the ddata endpoints module, given an existing express app, common + * middlewares, and the global app's `ctx`. + * @param Express app The express app we'll mount onto. + * @param Object ctx The global ctx with all modules, storage, and event buses + * configured. + */ + +function configure (app, ctx) { + // default storage biased towards entries. + // var entries = ctx.entries; + var express = require('express'), + api = express.Router( ) + ; + + api.param('at', function (req, res, next, at) { + req.at = get_time_spec(at); + next( ); + }); + + api.use(function (req, res, next) { + req.ctx = ctx; + next( ); + }); + + api.use(ctx.authorization.isPermitted('api:entries:read'), + ctx.authorization.isPermitted('api:treatments:read')); + api.get('/at/:at?', ensure_at, get_ddata, format_result); + + return api; +} + +// expose module +module.exports = configure; diff --git a/lib/data/treatmenttocurve.js b/lib/data/treatmenttocurve.js new file mode 100644 index 00000000000..714fb9049e6 --- /dev/null +++ b/lib/data/treatmenttocurve.js @@ -0,0 +1,87 @@ +'use strict'; + +var _ = require('lodash'); +var consts = require('../constants'); + +const MAX_BG_MMOL = 22; +const MAX_BG_MGDL = MAX_BG_MMOL * consts.MMOL_TO_MGDL; + +module.exports = function fitTreatmentsToBGCurve (ddata, env, ctx) { + + var settings = env.settings; + var rawbg = require('../plugins/rawbg')({ + settings: settings + , language: ctx.language + }); + + function updateTreatmentBG(treatment) { + + function mgdlByTime() { + + var withBGs = _.filter(ddata.sgvs, function(d) { + return d.mgdl > 39 || settings.isEnabled('rawbg'); + }); + + var beforeTreatment = _.findLast(withBGs, function (d) { + return d.mills <= treatment.mills; + }); + var afterTreatment = _.find(withBGs, function (d) { + return d.mills >= treatment.mills; + }); + + var mgdlBefore = mgdlValue(beforeTreatment) || calcRaw(beforeTreatment); + var mgdlAfter = mgdlValue(afterTreatment) || calcRaw(afterTreatment); + + var calcedBG = 0; + if (mgdlBefore && mgdlAfter) { + calcedBG = (mgdlBefore + mgdlAfter) / 2; + } else if (mgdlBefore) { + calcedBG = mgdlBefore; + } else if (mgdlAfter) { + calcedBG = mgdlAfter; + } + + return Math.round(calcedBG) || 180; + } + + function mgdlValue (entry) { + return entry && entry.mgdl >= 39 && Number(entry.mgdl); + } + + function calcRaw (entry) { + var raw; + if (entry && settings.isEnabled('rawbg')) { + var cal = _.last(ddata.cals); + if (cal) { + raw = rawbg.calc(entry, cal); + } + } + return raw; + } + + //to avoid checking if eventType is null everywhere, just default it here + treatment.eventType = treatment.eventType || ''; + + if (treatment.glucose && isNaN(treatment.glucose)) { + console.warn('found an invalid glucose value', treatment); + } else if (treatment.glucose && treatment.units) { + if (treatment.units === 'mmol') { + treatment.mmol = Math.min(Number(treatment.glucose), MAX_BG_MMOL); + } else { + treatment.mgdl = Math.min(Number(treatment.glucose), MAX_BG_MGDL); + } + } else if (treatment.glucose) { + //no units, assume everything is the same + //console.warn('found a glucose value without any units, maybe from an old version?', _.pick(treatment, '_id', 'created_at', 'enteredBy')); + var units = settings.units === 'mmol' ? 'mmol' : 'mgdl'; + + treatment[units] = settings.units === 'mmol' ? Math.min(Number(treatment.glucose), MAX_BG_MMOL) : Math.min(Number(treatment.glucose), MAX_BG_MGDL); + } else { + treatment.mgdl = mgdlByTime(); + } + } + + _.each(ddata.treatments, updateTreatmentBG); + +}; + diff --git a/lib/devicestatus.js b/lib/devicestatus.js deleted file mode 100644 index 6dd5eccb9ac..00000000000 --- a/lib/devicestatus.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict'; - -var find_options = require('./query'); - -function storage (collection, ctx) { - var ObjectID = require('mongodb').ObjectID; - - function create(obj, fn) { - if (! obj.hasOwnProperty('created_at')){ - obj.created_at = (new Date()).toISOString(); - } - api().insert(obj, function (err, doc) { - fn(null, doc.ops); - }); - } - - function last(fn) { - return list({count: 1}, function (err, entries) { - if (entries && entries.length > 0) { - fn(err, entries[0]); - } else { - fn(err, null); - } - }); - } - - var with_collection = ctx.store.with_collection(collection); - - function query_for (opts) { - return find_options(opts, storage.queryOpts); - } - - function list(opts, fn) { - with_collection(function (err, collection) { - // these functions, find, sort, and limit, are used to - // dynamically configure the request, based on the options we've - // been given - - // determine sort options - function sort ( ) { - return opts && opts.sort || {created_at: -1}; - } - - // configure the limit portion of the current query - function limit ( ) { - if (opts && opts.count) { - return this.limit(parseInt(opts.count)); - } - return this; - } - - // handle all the results - function toArray (err, entries) { - fn(err, entries); - } - - // now just stitch them all together - limit.call(collection - .find(query_for(opts)) - .sort(sort( )) - ).toArray(toArray); - }); - } - - function remove (_id, fn) { - var filter; - if (_id === '*') { - filter = {}; - } else { - filter = { '_id': new ObjectID(_id) }; - } - return api( ).remove(filter, fn); - } - - function api() { - return ctx.store.db.collection(collection); - } - - api.list = list; - api.create = create; - api.last = last; - api.remove = remove; - api.indexedFields = ['created_at']; - return api; -} - -storage.queryOpts = { - dateField: 'created_at' -}; - -module.exports = storage; diff --git a/lib/entries.js b/lib/entries.js deleted file mode 100644 index 2d436252c5f..00000000000 --- a/lib/entries.js +++ /dev/null @@ -1,166 +0,0 @@ -'use strict'; - -var es = require('event-stream'); -var sgvdata = require('sgvdata'); -var find_options = require('./query'); -var ObjectID = require('mongodb').ObjectID; - -/**********\ - * Entries - * Encapsulate persistent storage of sgv entries. -\**********/ - -function storage(env, ctx) { - - // TODO: Code is a little redundant. - - var with_collection = ctx.store.with_collection(env.mongo_collection); - - // query for entries from storage - function list (opts, fn) { - with_collection(function (err, collection) { - // these functions, find, sort, and limit, are used to - // dynamically configure the request, based on the options we've - // been given - - // determine sort options - function sort ( ) { - return opts && opts.sort || {date: -1}; - } - - // configure the limit portion of the current query - function limit ( ) { - if (opts && opts.count) { - return this.limit(parseInt(opts.count)); - } - return this; - } - - // handle all the results - function toArray (err, entries) { - fn(err, entries); - } - - // now just stitch them all together - limit.call(collection - .find(query_for(opts)) - .sort(sort( )) - ).toArray(toArray); - }); - } - - function remove (opts, fn) { - with_collection(function (err, collection) { - collection.remove(query_for(opts), fn); - }); - } - - // return writable stream to lint each sgv record passing through it - // TODO: get rid of this? not doing anything now - function map ( ) { - return es.map(function iter (item, next) { - return next(null, item); - }); - } - - // writable stream that persists all records - // takes function to call when done - function persist (fn) { - // receives entire list at end of stream - function done (err, result) { - // report any errors - if (err) { return fn(err, result); } - // batch insert a list of records - create(result, fn); - } - // lint and store the entire list - return es.pipeline(map( ), es.writeArray(done)); - } - - //TODO: implement - //function update (fn) { - //} - // - //function remove (fn) { - //} - - // store new documents using the storage mechanism - function create (docs, fn) { - with_collection(function(err, collection) { - if (err) { fn(err); return; } - // potentially a batch insert - var firstErr = null, - numDocs = docs.length, - totalCreated = 0; - - docs.forEach(function(doc) { - var query = (doc.sysTime && doc.type) ? {sysTime: doc.sysTime, type: doc.type} : doc; - collection.update(query, doc, {upsert: true}, function (err) { - firstErr = firstErr || err; - if (++totalCreated === numDocs) { - //TODO: this is triggering a read from Mongo, we can do better - ctx.bus.emit('data-received'); - fn(firstErr, docs); - } - }); - }); - }); - } - - function getEntry(id, fn) { - with_collection(function(err, collection) { - if (err) { - fn(err); - } else { - collection.findOne({_id: ObjectID(id)}, function (err, entry) { - if (err) { - fn(err); - } else { - fn(null, entry); - } - }); - } - }); - } - - function query_for (opts) { - return find_options(opts, storage.queryOpts); - } - - // closure to represent the API - function api ( ) { - // obtain handle usable for querying the collection associated - // with these records - return ctx.store.db.collection(env.mongo_collection); - } - - // Expose all the useful functions - api.list = list; - api.echo = sgvdata.sync.json.echo; - api.map = map; - api.create = create; - api.remove = remove; - api.persist = persist; - api.query_for = query_for; - api.getEntry = getEntry; - api.indexedFields = [ 'date', 'type', 'sgv', 'mbg', 'sysTime', 'dateString' ]; - return api; -} - -storage.queryOpts = { - walker: { - date: parseInt - , sgv: parseInt - , filtered: parseInt - , unfiltered: parseInt - , rssi: parseInt - , noise: parseInt - , mbg: parseInt - } - , useEpoch: true -}; - -// expose module -storage.storage = storage; -module.exports = storage; - diff --git a/static/food/js/food.js b/lib/food/food.js similarity index 91% rename from static/food/js/food.js rename to lib/food/food.js index b7d2b7208e4..d4601cb73c6 100644 --- a/static/food/js/food.js +++ b/lib/food/food.js @@ -1,19 +1,14 @@ '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 () { - - if (serverSettings === undefined) { - console.error('server settings were not loaded, will not call init'); - } else { - client.init(serverSettings, Nightscout.plugins); - } - +client.init(function loaded () { var translate = client.translate; var foodrec_template = { @@ -24,6 +19,9 @@ var client = Nightscout.client; , name: '' , portion: 0 , carbs: 0 + , fat: 0 // in grams + , protein: 0 // in grams + , energy: 0 // in kJ , gi: 2 , unit: 'g' }; @@ -71,7 +69,8 @@ var client = Nightscout.client; // Fetch data from mongo $('#fe_status').hide().text('Loading food database ...').fadeIn('slow'); $.ajax('/api/v1/food.json', { - success: function ajaxSuccess(records) { + headers: client.headers() + , success: function ajaxSuccess(records) { records.forEach(function processRecords(r) { restoreBoolValue(r,'hidden'); restoreBoolValue(r,'hideafteruse'); @@ -92,8 +91,8 @@ var client = Nightscout.client; }); $('#fe_status').hide().text(translate('Database loaded')).fadeIn('slow'); foodquickpick.sort(function compare(a,b) { return cmp(parseInt(a.position),parseInt(b.position)) }); - }, - error: function ajaxError() { + } + , error: function ajaxError() { $('#fe_status').hide().text(translate('Error: Database failed to load')).fadeIn('slow'); } }).done(initeditor); @@ -147,7 +146,7 @@ var client = Nightscout.client; $('#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)); } } @@ -163,7 +162,7 @@ var client = Nightscout.client; $('#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)); } } @@ -199,7 +198,7 @@ var client = Nightscout.client; $('#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)); } @@ -226,7 +225,10 @@ var client = Nightscout.client; .append($('').attr('class','width100px').css('text-align','center').append(translate('Carbs'))) .append($('').attr('class','width100px').css('text-align','center').append(translate('GI')+' [1-3]')) .append($('').attr('class','width150px').append(translate('Category'))) - .append($('').attr('class','width150px').append(translate('Subcategory'))); + .append($('').attr('class','width150px').append(translate('Subcategory'))) + .append($('').attr('class','width100px').append(translate('Fat [g]'))) + .append($('').attr('class','width100px').append(translate('Protein [g]'))) + .append($('').attr('class','width100px').append(translate('Energy [kJ]'))); $('#fe_data').empty(); @@ -241,13 +243,16 @@ var client = Nightscout.client; .append($('').attr('title',translate('Edit record')).attr('src',icon_edit).attr('index',i).attr('class','fe_editimg')) .append($('').attr('title',translate('Delete record')).attr('src',icon_remove).attr('index',i).attr('class','fe_removeimg')) ) - .append($('').addClass('width200px').append(foodlist[i].name)) + .append($('').addClass('width200px').text(foodlist[i].name)) .append($('').addClass('width150px').css('text-align','center').append(foodlist[i].portion)) - .append($('').addClass('width50px').css('text-align','center').append(foodlist[i].unit)) + .append($('').addClass('width50px').css('text-align','center').text(foodlist[i].unit)) .append($('').addClass('width100px').css('text-align','center').append(foodlist[i].carbs)) .append($('').addClass('width100px').css('text-align','center').append(foodlist[i].gi)) - .append($('').addClass('width150px').append(foodlist[i].category)) - .append($('').addClass('width150px').append(foodlist[i].subcategory)) + .append($('').addClass('width150px').text(foodlist[i].category)) + .append($('').addClass('width150px').text(foodlist[i].subcategory)) + .append($('').addClass('width100px').append(foodlist[i].fat)) + .append($('').addClass('width100px').append(foodlist[i].protein)) + .append($('').addClass('width100px').append(foodlist[i].energy)) ); } @@ -393,7 +398,7 @@ var client = Nightscout.client; 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(); @@ -469,9 +474,12 @@ var client = Nightscout.client; $('#fe_name').val(foodrec.name); $('#fe_portion').val(foodrec.portion ? foodrec.portion : ''); $('#fe_unit').val(foodrec.unit); - $('#fe_carbs').val(foodrec.carbs ? foodrec.carbs : ''); - $('#fe_gi').val(foodrec.gi); - + $('#fe_carbs').val(foodrec.carbs ? foodrec.carbs : '') + $('#fe_gi').val(foodrec.gi) + $('#fe_fat').val(foodrec.fat ? foodrec.fat : '') + $('#fe_protein').val(foodrec.protein ? foodrec.protein : '') + $('#fe_energy').val(foodrec.energy ? foodrec.energy : ''); + $('#fe_quickpick_showhidden').prop('checked',showhidden); console.info(JSON.stringify(foodrec)); @@ -492,6 +500,12 @@ var client = Nightscout.client; foodrec.unit = $('#fe_unit').val(); foodrec.carbs = parseInt($('#fe_carbs').val()); foodrec.carbs = foodrec.carbs || 0; + foodrec.fat = parseInt($('#fe_fat').val()); + foodrec.fat = foodrec.fat || 0; + foodrec.protein = parseInt($('#fe_protein').val()); + foodrec.protein = foodrec.protein || 0; + foodrec.energy = parseInt($('#fe_energy').val()); + foodrec.energy = foodrec.energy || 0; foodrec.gi = parseInt($('#fe_gi').val()); showhidden = $('#fe_quickpick_showhidden').is(':checked'); @@ -523,9 +537,7 @@ var client = Nightscout.client; method: 'POST', url: '/api/v1/food/', data: foodrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success (response) { $('#fe_status').hide().text('OK').fadeIn('slow'); foodrec._id = response[0]._id; @@ -547,9 +559,7 @@ var client = Nightscout.client; method: 'PUT', url: '/api/v1/food/', data: foodrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success () { $('#fe_status').hide().text('OK').fadeIn('slow'); updateFoodArray(foodrec); @@ -574,9 +584,7 @@ var client = Nightscout.client; method: 'DELETE', url: '/api/v1/food/'+_id, data: foodrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success () { $('#fe_status').hide().text('OK').fadeIn('slow'); }).fail(function fail() { @@ -597,9 +605,7 @@ var client = Nightscout.client; method: 'PUT', url: '/api/v1/food/', data: foodrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success (response) { console.log('Updated record: ',response); }); @@ -621,9 +627,7 @@ var client = Nightscout.client; method: 'POST', url: '/api/v1/food/', data: newrec, - headers: { - 'api-secret': client.hashauth.hash() - } + headers: client.headers() }).done(function success (response) { $('#fe_status').hide().text('OK').fadeIn('slow'); newrec._id = response[0]._id; @@ -671,5 +675,8 @@ var client = Nightscout.client; if (after) { after(); } -} -})(); \ No newline at end of file + } +}); +}; + +module.exports = init; diff --git a/lib/hashauth.js b/lib/hashauth.js deleted file mode 100644 index 63a182be2a9..00000000000 --- a/lib/hashauth.js +++ /dev/null @@ -1,158 +0,0 @@ -'use strict'; - -var crypto = require('crypto'); - -var hashauth = { - apisecret: '' - , storeapisecret: false - , apisecrethash: null - , authenticated: false - , initialized: false -}; - -hashauth.init = function init(client, $) { - - if (hashauth.initialized) { - return hashauth; - } - - hashauth.verifyAuthentication = function verifyAuthentication(next) { - $.ajax({ - method: 'GET' - , url: '/api/v1/verifyauth?t=' + Date.now() //cache buster - , headers: { - 'api-secret': hashauth.apisecrethash - } - }).done(function verifysuccess (response) { - if (response.message === 'OK') { - hashauth.authenticated = true; - console.log('Authentication passed.'); - next(true); - } else { - console.log('Authentication failed.'); - hashauth.removeAuthentication(); - next(false); - } - }).fail(function verifyfail ( ) { - console.log('Authentication failed.'); - hashauth.removeAuthentication(); - next(false); - }); - }; - - hashauth.initAuthentication = function initAuthentication() { - hashauth.apisecrethash = $.localStorage.get('apisecrethash') || null; - hashauth.verifyAuthentication(function () { - $('#authentication_placeholder').html(hashauth.inlineCode()); - }); - return hashauth; - }; - - hashauth.removeAuthentication = function removeAuthentication(event) { - $.localStorage.remove('apisecrethash'); - hashauth.apisecret = null; - hashauth.apisecrethash = null; - hashauth.authenticated = false; - $('#authentication_placeholder').html(hashauth.inlineCode()); - if (event) { - event.preventDefault(); - } - return false; - }; - - hashauth.requestAuthentication = function requestAuthentication(event) { - var translate = client.translate; - - $( '#requestauthenticationdialog' ).dialog({ - width: 500 - , height: 240 - , buttons: [ - { - text: translate('Update') - , click: function() { - var dialog = this; - $( dialog ).dialog( 'close' ); - hashauth.processSecret($('#apisecret').val(),$('#storeapisecret').is(':checked')); - } - } - , { - text: translate('Remove') - , click: function () { - $( this ).dialog( 'close' ); - hashauth.removeAuthentication(); - } - } - ] - , open: function open ( ) { - $('#apisecret').val('').focus(); - } - - }); - if (event) { - event.preventDefault(); - } - return false; - }; - - hashauth.processSecret = function processSecret(apisecret, storeapisecret) { - var translate = client.translate; - - hashauth.apisecret = apisecret; - hashauth.storeapisecret = storeapisecret; - if (hashauth.apisecret.length < 12) { - window.alert(translate('Too short API secret')); - } else { - var shasum = crypto.createHash('sha1'); - shasum.update(hashauth.apisecret); - hashauth.apisecrethash = shasum.digest('hex'); - - hashauth.verifyAuthentication( function(isok) { - if (isok) { - if (hashauth.storeapisecret) { - $.localStorage.set('apisecrethash',hashauth.apisecrethash); - } - $('#authentication_placeholder').html(hashauth.inlineCode()); - } else { - alert(translate('Wrong API secret')); - } - }); - } -}; - - hashauth.inlineCode = function inlineCode() { - var translate = client.translate; - - var html = - ''+ - '
    '+ - (hashauth.isAuthenticated() ? - translate('Device authenticated') + ' (' + translate('Remove') + ')' - : - translate('Device not authenticated') + ' (' + translate('Authenticate') + ')' - )+ - '
    '; - - return html; - }; - - hashauth.hash = function hash() { - return hashauth.apisecrethash; - }; - - hashauth.isAuthenticated = function isAuthenticated() { - return hashauth.authenticated; - }; - - hashauth.initialized = true; - return hashauth; -}; - -module.exports = hashauth; diff --git a/lib/language.js b/lib/language.js index c70e8bc514c..17bf1e0170c 100644 --- a/lib/language.js +++ b/lib/language.js @@ -1,4797 +1,181 @@ 'use strict'; -function init() { - var lang; +var _ = require('lodash'); - function language() { +function init (fs) { + + function language () { return language; } + language.speechCode = 'en-US'; + language.lang = 'en'; + language.languages = [ - { code: 'bg', language: 'Български' } - , { code: 'cs', language: 'Čeština' } - , { code: 'dk', language: 'Dansk' } - , { code: 'de', language: 'Deutsch' } - , { code: 'en', language: 'English' } - , { code: 'es', language: 'Español' } - , { code: 'fr', language: 'Français' } - , { code: 'he', language: 'עברית' } - , { code: 'hr', language: 'Hrvatski' } - , { code: 'it', language: 'Italiano' } - , { code: 'nb', language: 'Norsk (Bokmål)' } - , { code: 'pl', language: 'Polski' } - , { code: 'pt', language: 'Português (Brasil)' } - , { code: 'ro', language: 'Română' } - , { code: 'sv', language: 'Svenska' } - , { code: 'fi', language: 'Suomi' } + { code: 'ar', file: 'ar_SA', language: 'اللغة العربية', speechCode: 'ar-SA' } + , { code: 'bg', file: 'bg_BG', language: 'Български', speechCode: 'bg-BG' } + , { 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: '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' } + , { code: 'fr', file: 'fr_FR', language: 'Français', speechCode: 'fr-FR' } + , { code: 'he', file: 'he_IL', language: 'עברית', speechCode: 'he-IL' } + , { code: 'hr', file: 'hr_HR', language: 'Hrvatski', speechCode: 'hr-HR' } + , { code: 'hu', file: 'hu_HU', language: 'Magyar', speechCode: 'hu-HU' } + , { code: 'it', file: 'it_IT', language: 'Italiano', speechCode: 'it-IT' } + , { code: 'ja', file: 'ja_JP', language: '日本語', speechCode: 'ja-JP' } + , { code: 'ko', file: 'ko_KR', language: '한국어', speechCode: 'ko-KR' } + , { 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_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: '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: 'uk', file: 'uk_UA', language: 'українська', speechCode: 'uk-UA' } + , { code: 'zh_cn', file: 'zh_CN', language: '中文(简体)', speechCode: 'cmn-Hans-CN' } +// , { code: 'zh_tw', file: 'zh_TW', language: '中文(繁體)', speechCode: 'cmn-Hant-TW' } ]; - var translations = { - // Server - 'Listening on port' : { - cs: 'Poslouchám na portu' - ,es: 'Escuchando en el puerto' - ,fr: 'Ecoute sur port' - ,pt: 'Escutando porta' - ,sv: 'Lyssnar på port' - ,ro: 'Activ pe portul' - ,bg: 'Активиране на порта' - ,hr: 'Slušanje na portu' - ,it: 'Porta in ascolto' - ,dk: 'Lytter på port' - ,fi: 'Kuuntelen porttia' - ,nb: 'Lytter på port' - ,he: 'מקשיב על פתחה' - ,pl: 'Słucham na porcie' - } - // Client - ,'Mo' : { - cs: 'Po' - ,de: 'Mo' - ,es: 'Lu' - ,fr: 'Lu' - ,pt: 'Seg' - ,sv: 'Mån' - ,ro: 'Lu' - ,bg: 'Пон' - ,hr: 'Pon' - ,it: 'Lun' - ,dk: 'Man' - ,fi: 'Ma' - ,nb: 'Man' - ,he: 'ב' - ,pl: 'Pn' - } - ,'Tu' : { - cs: 'Út' - ,de: 'Di' - ,es: 'Mar' - ,fr: 'Ma' - ,pt: 'Ter' - ,sv: 'Tis' - ,ro: 'Ma' - ,bg: 'Вт' - ,hr: 'Ut' - ,it: 'Mar' - ,dk: 'Tir' - ,fi: 'Ti' - ,nb: 'Tir' - ,he: 'ג' - ,pl: 'Wt' - } - ,'We' : { - cs: 'St' - ,de: 'Mi' - ,es: 'Mie' - ,fr: 'Me' - ,pt: 'Qua' - ,sv: 'Ons' - ,ro: 'Mie' - ,bg: 'Ср' - ,hr: 'Sri' - ,it: 'Mer' - ,dk: 'Ons' - ,fi: 'Ke' - ,nb: 'Ons' - ,he: 'ד' - ,pl: 'Śr' - } - ,'Th' : { - cs: 'Čt' - ,de: 'Do' - ,es: 'Jue' - ,fr: 'Je' - ,pt: 'Qui' - ,sv: 'Tor' - ,ro: 'Jo' - ,bg: 'Четв' - ,hr: 'Čet' - ,it: 'Gio' - ,dk: 'Tor' - ,fi: 'To' - ,nb: 'Tor' - ,he: 'ה' - ,pl: 'Cz' - } - ,'Fr' : { - cs: 'Pá' - ,de: 'Fr' - ,es: 'Vie' - ,fr: 'Ve' - ,pt: 'Sex' - ,sv: 'Fre' - ,ro: 'Vi' - ,bg: 'Пет' - ,hr: 'Pet' - ,it: 'Ven' - ,dk: 'Fre' - ,fi: 'Pe' - ,nb: 'Fre' - ,he: 'ו' - ,pl: 'Pt' - } - ,'Sa' : { - cs: 'So' - ,de: 'Sa' - ,es: 'Sab' - ,fr: 'Sa' - ,pt: 'Sab' - ,sv: 'Lör' - ,ro: 'Sa' - ,bg: 'Съб' - ,hr: 'Sub' - ,it: 'Sab' - ,dk: 'Lør' - ,fi: 'La' - ,nb: 'Lør' - ,he: 'ש' - ,pl: 'So' - } - ,'Su' : { - cs: 'Ne' - ,de: 'So' - ,es: 'Dom' - ,fr: 'Di' - ,pt: 'Dom' - ,sv: 'Sön' - ,ro: 'Du' - ,bg: 'Нед' - ,hr: 'Ned' - ,it: 'Dom' - ,dk: 'Søn' - ,fi: 'Su' - ,nb: 'Søn' - ,he: 'א' - ,pl: 'Nd' - } - ,'Monday' : { - cs: 'Pondělí' - ,de: 'Montag' - ,es: 'Lunes' - ,fr: 'Lundi' - ,pt: 'Segunda' - ,sv: 'Måndag' - ,ro: 'Luni' - ,bg: 'Понеделник' - ,hr: 'Ponedjeljak' - ,it: 'Lunedì' - ,dk: 'Mandag' - ,fi: 'Maanantai' - ,nb: 'Mandag' - ,he: 'שני' - ,pl: 'Poniedziałek' - } - ,'Tuesday' : { - cs: 'Úterý' - ,de: 'Dienstag' - ,es: 'Martes' - ,fr: 'Mardi' - ,pt: 'Terça' - ,ro: 'Marți' - ,bg: 'Вторник' - ,hr: 'Utorak' - ,sv: 'Tisdag' - ,it: 'Martedì' - ,dk: 'Tirsdag' - ,fi: 'Tiistai' - ,nb: 'Tirsdag' - ,he: 'שלישי' - ,pl: 'Wtorek' - } - ,'Wednesday' : { - cs: 'Středa' - ,de: 'Mittwoch' - ,es: 'Miércoles' - ,fr: 'Mercredi' - ,pt: 'Quarta' - ,sv: 'Onsdag' - ,ro: 'Miercuri' - ,bg: 'Сряда' - ,hr: 'Srijeda' - ,it: 'Mercoledì' - ,dk: 'Onsdag' - ,fi: 'Keskiviikko' - ,nb: 'Onsdag' - ,he: 'רביעי' - ,pl: 'Środa' - } - ,'Thursday' : { - cs: 'Čtvrtek' - ,de: 'Donnerstag' - ,es: 'Jueves' - ,fr: 'Jeudi' - ,pt: 'Quinta' - ,sv: 'Torsdag' - ,ro: 'Joi' - ,bg: 'Четвъртък' - ,hr: 'Četvrtak' - ,it: 'Giovedì' - ,dk: 'Torsdag' - ,fi: 'Torstai' - ,nb: 'Torsdag' - ,he: 'חמישי' - ,pl: 'Czwartek' - } - ,'Friday' : { - cs: 'Pátek' - ,de: 'Freitag' - ,fr: 'Vendredi' - ,pt: 'Sexta' - ,sv: 'Fredag' - ,ro: 'Vineri' - ,es: 'Viernes' - ,bg: 'Петък' - ,hr: 'Petak' - ,it: 'Venerdì' - ,dk: 'Fredag' - ,fi: 'Perjantai' - ,nb: 'Fredag' - ,he: 'שישי' - ,pl: 'Piątek' - } - ,'Saturday' : { - cs: 'Sobota' - ,de: 'Samstag' - ,es: 'Sábado' - ,fr: 'Samedi' - ,pt: 'Sábado' - ,ro: 'Sâmbătă' - ,bg: 'Събота' - ,hr: 'Subota' - ,sv: 'Lördag' - ,it: 'Sabato' - ,dk: 'Lørdag' - ,fi: 'Lauantai' - ,nb: 'Lørdag' - ,he: 'שבת' - ,pl: 'Sobota' - } - ,'Sunday' : { - cs: 'Neděle' - ,de: 'Sonntag' - ,es: 'Domingo' - ,fr: 'Dimanche' - ,pt: 'Domingo' - ,ro: 'Duminică' - ,bg: 'Неделя' - ,hr: 'Nedjelja' - ,sv: 'Söndag' - ,it: 'Domenica' - ,dk: 'Søndag' - ,fi: 'Sunnuntai' - ,nb: 'Søndag' - ,he: 'ראשון' - ,pl: 'Niedziela' - } - ,'Category' : { - cs: 'Kategorie' - ,de: 'Kategorie' - ,es: 'Categoría' - ,fr: 'Catégorie' - ,pt: 'Categoria' - ,sv: 'Kategori' - ,ro: 'Categorie' - ,bg: 'Категория' - ,hr: 'Kategorija' - ,it:'Categoria' - ,dk: 'Kategori' - ,fi: 'Luokka' - ,nb: 'Kategori' - ,he: 'קטגוריה' - ,pl: 'Kategoria' - } - ,'Subcategory' : { - cs: 'Podkategorie' - ,de: 'Unterkategorie' - ,es: 'Subcategoría' - ,fr: 'Sous-catégorie' - ,pt: 'Subcategoria' - ,sv: 'Underkategori' - ,ro: 'Subcategorie' - ,bg: 'Подкатегория' - ,hr: 'Podkategorija' - ,it: 'Sottocategoria' - ,dk: 'Underkategori' - ,fi: 'Alaluokka' - ,nb: 'Underkategori' - ,he: 'תת-קטגוריה' - ,pl: 'Pod kategoria' - } - ,'Name' : { - cs: 'Jméno' - ,de: 'Name' - ,es: 'Nombre' - ,fr: 'Nom' - ,pt: 'Nome' - ,sv: 'Namn' - ,ro: 'Nume' - ,bg: 'Име' - ,hr: 'Ime' - ,it: 'Nome' - ,dk: 'Navn' - ,fi: 'Nimi' - ,nb: 'Navn' - ,he: 'שם' - ,pl: 'Imie' - } - ,'Today' : { - cs: 'Dnes' - ,de: 'Heute' - ,es: 'Hoy' - ,fr: 'Aujourd\'hui' - ,pt: 'Hoje' - ,ro: 'Astăzi' - ,bg: 'Днес' - ,hr: 'Danas' - ,sv: 'Idag' - ,it: 'Oggi' - ,dk: 'Idag' - ,fi: 'Tänään' - ,nb: 'Idag' - ,he: 'היום' - ,pl: 'Dziś' - } - ,'Last 2 days' : { - cs: 'Poslední 2 dny' - ,de: 'letzten 2 Tage' - ,es: 'Últimos 2 días' - ,fr: '2 derniers jours' - ,pt: 'Últimos 2 dias' - ,ro: 'Ultimele 2 zile' - ,bg: 'Последните 2 дни' - ,hr: 'Posljednja 2 dana' - ,sv: 'Senaste 2 dagarna' - ,it: 'Ultimi 2 giorni' - ,dk: 'Sidste 2 dage' - ,fi: 'Edelliset 2 päivää' - ,nb: 'Siste 2 dager' - ,he: 'יומיים אחרונים' - ,pl: 'Ostatnie 2 dni' - } - ,'Last 3 days' : { - cs: 'Poslední 3 dny' - ,de: 'letzten 3 Tage' - ,es: 'Últimos 3 días' - ,fr: '3 derniers jours' - ,pt: 'Últimos 3 dias' - ,sv: 'Senaste 3 dagarna' - ,ro: 'Ultimele 3 zile' - ,bg: 'Последните 3 дни' - ,hr: 'Posljednja 3 dana' - ,it: 'Ultimi 3 giorni' - ,dk: 'Sidste 3 dage' - ,fi: 'Edelliset 3 päivää' - ,nb: 'Siste 3 dager' - ,he: '3 ימים אחרונים' - ,pl: 'Ostatnie 3 dni' - } - ,'Last week' : { - cs: 'Poslední týden' - ,de: 'letzte Woche' - ,es: 'Semana pasada' - ,fr: 'Semaine Dernière' - ,pt: 'Semana passada' - ,ro: 'Săptămâna trecută' - ,bg: 'Последната седмица' - ,hr: 'Protekli tjedan' - ,sv: 'Senaste veckan' - ,it: 'Settimana scorsa' - ,dk: 'Sidste uge' - ,fi: 'Viime viikko' - ,nb: 'Siste uke' - ,he: 'שבוע אחרון' - ,pl: 'Ostatni tydzeń' - } - ,'Last 2 weeks' : { - cs: 'Poslední 2 týdny' - ,de: 'letzten 2 Wochen' - ,es: 'Últimas 2 semanas' - ,fr: '2 dernières semaines' - ,pt: 'Últimas 2 semanas' - ,ro: 'Ultimele 2 săptămâni' - ,bg: 'Последните 2 седмици' - ,hr: 'Protekla 2 tjedna' - ,sv: 'Senaste 2 veckorna' - ,it: 'Ultime 2 settimane' - ,dk: 'Sidste 2 uger' - ,fi: 'Viimeiset 2 viikkoa' - ,nb: 'Siste 2 uker' - ,he: 'שבועיים אחרונים' - ,pl: 'Ostatnie 2 tygodnie' - } - ,'Last month' : { - cs: 'Poslední měsíc' - ,de: 'letzter Monat' - ,es: 'Mes pasado' - ,fr: 'Mois dernier' - ,pt: 'Mês passado' - ,ro: 'Ultima lună' - ,bg: 'Последният месец' - ,hr: 'Protekli mjesec' - ,sv: 'Senaste månaden' - ,it: 'Mese scorso' - ,dk: 'Sidste måned' - ,fi: 'Viime kuu' - ,nb: 'Siste måned' - ,he: 'חודש אחרון' - ,pl: 'Ostatni miesiąc' - } - ,'Last 3 months' : { - cs: 'Poslední 3 měsíce' - ,de: 'letzten 3 Monate' - ,es: 'Últimos 3 meses' - ,fr: '3 derniers mois' - ,pt: 'Últimos 3 meses' - ,ro: 'Ultimele 3 luni' - ,bg: 'Последните 3 месеца' - ,hr: 'Protekla 3 mjeseca' - ,sv: 'Senaste 3 månaderna' - ,it: 'Ultimi 3 mesi' - ,dk: 'Sidste 3 måneder' - ,fi: 'Viimeiset 3 kuukautta' - ,nb: 'Siste 3 måneder' - ,he: '3 חודשים אחרונים' - ,pl: 'Ostatnie 3 miesiące' - } - ,'From' : { - cs: 'Od' - ,de: 'Von' - ,es: 'Desde' - ,fr: 'De' - ,pt: 'De' - ,sv: 'Från' - ,ro: 'De la' - ,bg: 'От' - ,hr: 'Od' - ,it: 'Da' - ,dk: 'Fra' - ,fi: 'Alkaen' - ,nb: 'Fra' - ,he: 'מ' - ,pl: 'Od' - } - ,'To' : { - cs: 'Do' - ,de: 'Bis' - ,es: 'Hasta' - ,fr: 'À' - ,pt: 'a' - ,ro: 'La' - ,bg: 'До' - ,hr: 'Do' - ,sv: 'Till' - ,it: 'A' - ,dk: 'Til' - ,fi: 'Asti' - ,nb: 'Til' - ,he: 'עד' - ,pl: 'Do' - } - ,'Notes' : { - cs: 'Poznámky' - ,de: 'Notiz' - ,es: 'Notas' - ,fr: 'Notes' - ,pt: 'Notas' - ,sv: 'Notering' - ,ro: 'Note' - ,bg: 'Бележки' - ,hr: 'Bilješke' - ,it: 'Note' - ,dk: 'Noter' - ,fi: 'Merkinnät' - ,nb: 'Notater' - ,he: 'הערות' - ,pl: 'Uwagi' - } - ,'Food' : { - cs: 'Jídlo' - ,de: 'Essen' - ,es: 'Comida' - ,fr: 'Nourriture' - ,pt: 'Comida' - ,sv: 'Föda' - ,ro: 'Mâncare' - ,bg: 'Храна' - ,hr: 'Hrana' - ,it: 'Cibo' - ,dk: 'Mad' - ,fi: 'Ruoka' - ,nb: 'Mat' - ,he: 'אוכל' - ,pl: 'Jedzenie' - } - ,'Insulin' : { - cs: 'Inzulín' - ,de: 'Insulin' - ,es: 'Insulina' - ,fr: 'Insuline' - ,pt: 'Insulina' - ,ro: 'Insulină' - ,bg: 'Инсулин' - ,hr: 'Inzulin' - ,sv: 'Insulin' - ,it: 'Insulina' - ,dk: 'Insulin' - ,fi: 'Insuliini' - ,nb: 'Insulin' - ,he: 'אינסולין' - ,pl: 'Insulina' - } - ,'Carbs' : { - cs: 'Sacharidy' - ,de: 'Kohlenhydrate' - ,es: 'Hidratos de carbono' - ,fr: 'Glucides' - ,pt: 'Carboidrato' - ,ro: 'Carbohidrați' - ,bg: 'Въглехидрати' - ,hr: 'Ugljikohidrati' - ,sv: 'Kolhydrater' - ,it: 'Carboidrati' - ,dk: 'Kulhydrater' - ,fi: 'Hiilihydraatit' - ,nb: 'Karbohydrater' - ,he: 'פחמימות' - ,pl: 'Węglowodany' - } - ,'Notes contain' : { - cs: 'Poznámky obsahují' - ,de: 'Erläuterungen' - ,es: 'Contenido de las notas' - ,fr: 'Notes contiennent' - ,pt: 'Notas contém' - ,ro: 'Conținut note' - ,bg: 'бележките съдържат' - ,hr: 'Sadržaj bilješki' - ,sv: 'Notering innehåller' - ,it: 'Contiene note' - ,dk: 'Noter indeholder' - ,fi: 'Merkinnät sisältävät' - ,nb: 'Notater inneholder' - ,pl: 'Zawierają uwagi' - - } - ,'Target bg range bottom' : { - cs: 'Cílová glykémie spodní' - ,de: 'Untergrenze des Blutzuckerzielbereiches' - ,es: 'Objetivo inferior de glucemia' - ,fr: 'Limite inférieure glycémie' - ,pt: 'Limite inferior de glicemia' - ,ro: 'Limită de jos a glicemiei' - ,bg: 'Долна граница на КЗ' - ,hr: 'Ciljna donja granica GUK-a' - ,sv: 'Gräns för nedre blodsockervärde' - ,it: 'Limite inferiore della glicemia' - ,dk: 'Nedre grænse for blodsukkerværdier' - ,fi: 'Tavoitealueen alaraja' - ,nb: 'Nedre grense for blodsukkerverdier' - ,he: 'טווח מטרה סף תחתון' - ,pl: 'Docelowy zakres glikemii, dolny' - } - ,'top' : { - cs: 'horní' - ,de: 'oben' - ,es: 'Superior' - ,fr: 'Supérieur' - ,pt: 'Superior' - ,ro: 'Sus' - ,bg: 'горна' - ,hr: 'Gornja' - ,sv: 'Toppen' - ,it: 'Superiore' - ,dk: 'Top' - ,fi: 'yläraja' - ,nb: 'Topp' - ,he: 'למעלה' - ,pl: 'Górny' - } - ,'Show' : { - cs: 'Zobraz' - ,de: 'Zeigen' - ,es: 'Mostrar' - ,fr: 'Montrer' - ,pt: 'Mostrar' - ,ro: 'Arată' - ,sv: 'Visa' - ,bg: 'Покажи' - ,hr: 'Prikaži' - ,it: 'Mostra' - ,dk: 'Vis' - ,fi: 'Näytä' - ,nb: 'Vis' - ,he: 'הצג' - ,pl: 'Pokaż' - } - ,'Display' : { - cs: 'Zobraz' - ,de: 'Darstellen' - ,es: 'Visualizar' - ,fr: 'Afficher' - ,pt: 'Visualizar' - ,ro: 'Afișează' - ,bg: 'Покажи' - ,hr: 'Prikaži' - ,sv: 'Visa' - ,it: 'Schermo' - ,dk: 'Vis' - ,fi: 'Näyttö' - ,nb: 'Vis' - ,he: 'תצוגה' - ,pl: 'Wyświetl' - } - ,'Loading' : { - cs: 'Nahrávám' - ,de: 'Laden' - ,es: 'Cargando' - ,fr: 'Chargement' - ,pt: 'Carregando' - ,ro: 'Se încarcă' - ,bg: 'Зареждане' - ,hr: 'Učitavanje' - ,sv: 'Laddar' - ,it: 'Sto Caricando' - ,dk: 'Indlæser' - ,fi: 'Lataan' - ,nb: 'Laster' - ,he: 'טוען' - ,pl: 'Ładowanie' - } - ,'Loading profile' : { - cs: 'Nahrávám profil' - ,de: 'Lade Profil' - ,es: 'Cargando perfil' - ,fr: 'Chargement du profil' - ,pt: 'Carregando perfil' - ,sv: 'Laddar profil' - ,ro: 'Încarc profilul' - ,bg: 'Зареждане на профил' - ,hr: 'Učitavanje profila' - ,it: 'Sto Caricando il profilo' - ,dk: 'Indlæser profil' - ,fi: 'Lataan profiilia' - ,nb: 'Leser profil' - ,he: 'טוען פרופיל' - ,pl: 'Ładowanie profilu' - } - ,'Loading status' : { - cs: 'Nahrávám status' - ,de: 'Lade Status' - ,es: 'Cargando estado' - ,fr: 'Statut du chargement' - ,pt: 'Carregando status' - ,sv: 'Laddar status' - ,ro: 'Încarc statusul' - ,bg: 'Зареждане на статус' - ,hr: 'Učitavanje statusa' - ,it: 'Stato di caricamento' - ,dk: 'Indlæsnings status' - ,fi: 'Lataan tilaa' - ,nb: 'Leser status' - ,he: 'טוען סטטוס' - ,pl: 'Status załadowania' - } - ,'Loading food database' : { - cs: 'Nahrávám databázi jídel' - ,de: 'Lade Nahrungsmittel-Datenbank' - ,es: 'Cargando base de datos de alimentos' - ,fr: 'Chargement de la base de données alimentaire' - ,pt: 'Carregando dados de alimentos' - ,sv: 'Laddar födoämnesdatabas' - ,ro: 'Încarc baza de date de alimente' - ,bg: 'Зареждане на данни с храни' - ,hr: 'Učitavanje baze podataka o hrani' - ,it: 'Carico dati alimenti' - ,dk: 'Indlæser mad database' - ,fi: 'Lataan ruokatietokantaa' - ,nb: 'Leser matdatabase' - ,he: 'טוען נתוני אוכל' - ,pl: 'Ładowanie bazy posiłków' - } - ,'not displayed' : { - cs: 'není zobrazeno' - ,de: 'nicht angezeigt' - ,es: 'No mostrado' - ,fr: 'non affiché' - ,pt: 'não mostrado' - ,ro: 'neafișat' - ,bg: 'Не се показва' - ,hr: 'Ne prikazuje se' - ,sv: 'Visas ej' - ,it: 'Non visualizzato' - ,dk: 'Vises ikke' - ,fi: 'ei näytetä' - ,nb: 'Vises ikke' - ,he: 'לא מוצג' - ,pl: 'Nie jest wyświetlany' - } - ,'Loading CGM data of' : { - cs: 'Nahrávám CGM data' - ,de: 'Lade CGM-Daten von' - ,es: 'Cargando datos de CGM de' - ,fr: 'Chargement données CGM de' - ,pt: 'Carregando dados de CGM de' - ,sv: 'Laddar CGM-data för' - ,ro: 'Încarc datele CGM ale lui' - ,bg: 'Зареждане на CGM данни от' - ,hr: 'Učitavanja podataka CGM-a' - ,it: 'Carico dati CGM' - ,dk: 'Indlæser CGM-data for' - ,fi: 'Lataan sensoritietoja: ' - ,nb: 'Leser CGM-data for' - ,he: 'טוען נתוני חיישן סוכר של' - ,pl: 'Ładowanie danych z CGM' - } - ,'Loading treatments data of' : { - cs: 'Nahrávám data ošetření' - ,de: 'Lade Behandlungsdaten von' - ,es: 'Cargando datos de tratamientos de' - ,fr: 'Chargement données traitement de' - ,pt: 'Carregando dados de tratamento de' - ,sv: 'Laddar behandlingsdata för' - ,ro: 'Încarc datele despre tratament pentru' - ,bg: 'Зареждане на въведените лечения от' - ,hr: 'Učitavanje podataka o tretmanu' - ,it: 'Carico trattamenti dei dati di' - ,dk: 'Indlæser data for' - ,fi: 'Lataan toimenpidetietoja: ' - ,nb: 'Leser behandlingsdata for' - ,he: 'טוען נתוני טיפולים של' - ,pl: 'Ładowanie danych leczenia' - } - ,'Processing data of' : { - cs: 'Zpracovávám data' - ,de: 'Verarbeite Daten von' - ,es: 'Procesando datos de' - ,fr: 'Traitement des données de' - ,pt: 'Processando dados de' - ,sv: 'Behandlar data för' - ,ro: 'Procesez datele lui' - ,bg: 'Зареждане на данни от' - ,hr: 'Obrada podataka' - ,it: 'Elaborazione dei dati di' - ,dk: 'Behandler data for' - ,fi: 'Käsittelen tietoja: ' - ,nb: 'Behandler data for' - ,he: 'מעבד נתונים של' - ,pl: 'Przetważanie danych' - } - ,'Portion' : { - cs: 'Porce' - ,de: 'Portion' - ,es: 'Porción' - ,fr: 'Portion' - ,pt: 'Porção' - ,ro: 'Porție' - ,bg: 'Порция' - ,hr: 'Dio' - ,sv: 'Portion' - ,it: 'Porzione' - ,dk: 'Portion' - ,fi: 'Annos' - ,nb: 'Porsjon' - ,he: 'מנה' - ,pl: 'Część' - } - ,'Size' : { - cs: 'Rozměr' - ,de: 'Größe' - ,es: 'Tamaño' - ,fr: 'Taille' - ,pt: 'Tamanho' - ,ro: 'Mărime' - ,sv: 'Storlek' - ,bg: 'Големина' - ,hr: 'Veličina' - ,it: 'Formato' - ,dk: 'Størrelse' - ,fi: 'Koko' - ,nb: 'Størrelse' - ,he: 'גודל' - ,pl: 'Rozmiar' - } - ,'(none)' : { - cs: '(Žádný)' - ,de: '(nichts)' - ,es: '(ninguno)' - ,fr: '(aucun)' - ,pt: '(nenhum)' - ,sv: '(tom)' - ,ro: '(fără)' - ,bg: '(няма)' - ,hr: '(Prazno)' - ,it: '(Nessuno)' - ,dk: '(ingen)' - ,fi: '(tyhjä)' - ,nb: '(ingen)' - ,he: '(ללא)' - ,pl: '(brak)' - } - ,'None' : { - cs: 'Žádný' - ,de: 'Nichts' - ,es: 'Ninguno' - ,fr: 'aucun' - ,pt: 'nenhum' - ,sv: 'tom' - ,ro: 'fără' - ,bg: 'няма' - ,hr: 'Prazno' - ,it: 'Nessuno' - ,dk: 'ingen' - ,fi: 'tyhjä' - ,nb: 'ingen' - ,he: 'ללא' - ,pl: 'brak' - } - ,'' : { - cs: '<Žádný>' - ,de: '' - ,es: '' - ,fr: '' - ,pt: '' - ,sv: '' - ,ro: '' - ,bg: '<няма>' - ,hr: '' - ,it: '' - ,dk: '' - ,fi: '' - ,nb: '' - ,he: '<ללא>' - ,pl: '' - } - ,'Result is empty' : { - cs: 'Prázdný výsledek' - ,de: 'Leeres Ergebnis' - ,es: 'Resultado vacío' - ,fr: 'Pas de résultat' - ,pt: 'Resultado vazio' - ,sv: 'Resultat saknas' - ,ro: 'Fără rezultat' - ,bg: 'Няма резултат' - ,hr: 'Prazan rezultat' - ,it: 'Risultato vuoto' - ,dk: 'Tomt resultat' - ,fi: 'Ei tuloksia' - ,nb: 'Tomt resultat' - ,he: 'אין תוצאה' - ,pl: 'Brak wyniku' - } - ,'Day to day' : { - cs: 'Den po dni' - ,de: 'Von Tag zu Tag' - ,es: 'Día a día' - ,fr: 'jour par jour' - ,pt: 'Dia a dia' - ,sv: 'Dag för dag' - ,ro: 'Zi cu zi' - ,bg: 'Ден за ден' - ,hr: 'Svakodnevno' - ,it: 'Giorno per giorno' - ,dk: 'Dag til dag' - ,fi: 'Päivittäinen' - ,nb: 'Dag til dag' - ,he: 'יום-יום' - ,pl: 'Dzień po dniu' - } - ,'Daily Stats' : { - cs: 'Denní statistiky' - ,de: 'Tägliche Statistik' - ,es: 'Estadísticas diarias' - ,fr: 'Stats quotidiennes' - ,pt: 'Estatísticas diárias' - ,sv: 'Dygnsstatistik' - ,ro: 'Statistici zilnice' - ,bg: 'Дневна статистика' - ,hr: 'Dnevna statistika' - ,it: 'Statistiche giornaliere' - ,dk: 'Daglig statistik' - ,fi: 'Päivittäiset tilastot' - ,nb: 'Daglig statistikk' - ,he: 'סטטיסטיקה יומית' - ,pl: 'Statystyki dzienne' - } - ,'Percentile Chart' : { - cs: 'Percentil' - ,de: 'Durchschnittswert' - ,es: 'Percentiles' - ,fr: 'Percentiles' - ,pt: 'Percentis' - ,ro: 'Grafic percentile' - ,bg: 'Процентна графика' - ,hr: 'Tablica u postotcima' - ,sv: 'Procentgraf' - ,it: 'Grafico percentile' - ,dk: 'Procentgraf' - ,fi: 'Suhteellinen kuvaaja' - ,nb: 'Prosentgraf' - ,he: 'טבלת עשירונים' - ,pl: 'Wykres percentyl' - } - ,'Distribution' : { - cs: 'Rozložení' - ,de: 'Streuung' - ,es: 'Distribución' - ,fr: 'Distribution' - ,pt: 'Distribuição' - ,ro: 'Distribuție' - ,bg: 'Разпределение' - ,hr: 'Distribucija' - ,sv: 'Distribution' - ,it: 'Distribuzione' - ,dk: 'Distribution' - ,fi: 'Jakauma' - ,nb: 'Distribusjon' - ,he: 'פיזור' - ,pl: 'Dystrybucja' - } - ,'Hourly stats' : { - cs: 'Statistika po hodinách' - ,de: 'Stündliche Statistik' - ,es: 'Estadísticas por hora' - ,fr: 'Statistiques horaires' - ,pt: 'Estatísticas por hora' - ,sv: 'Timmstatistik' - ,ro: 'Statistici orare' - ,bg: 'Статистика по часове' - ,hr: 'Statistika po satu' - ,it: 'Statistiche per ore' - ,dk: 'Timestatistik' - ,fi: 'Tunneittainen tilasto' - ,nb: 'Timestatistikk' - ,he: 'סטטיסטיקה שעתית' - ,pl: 'Statystiki godzinowe' - } - ,'Weekly success' : { - cs: 'Statistika po týdnech' - ,de: 'Wöchentlicher Erfolg' - ,es: 'Resultados semanales' - ,fr: 'Résultat hebdomadaire' - ,pt: 'Resultados semanais' - ,ro: 'Rezultate săptămânale' - ,bg: 'Седмичен успех' - ,hr: 'Tjedni uspjeh' - ,sv: 'Veckoresultat' - ,it: 'Statistiche settimanali' - ,dk: 'Uge resultat' - ,fi: 'Viikottainen saavutus' - ,nb: 'Ukeresultat' - ,pl: 'Tygodniowe sukcesy' - } - ,'No data available' : { - cs: 'Žádná dostupná data' - ,de: 'Keine Daten verfügbar' - ,es: 'No hay datos disponibles' - ,fr: 'Pas de données disponibles' - ,pt: 'Não há dados' - ,ro: 'Fără date' - ,bg: 'Няма данни за показване' - ,hr: 'Nema raspoloživih podataka' - ,sv: 'Data saknas' - ,it: 'Dati non disponibili' - ,dk: 'Mangler data' - ,fi: 'Tietoja ei saatavilla' - ,nb: 'Mangler data' - ,he: 'אין מידע זמין' - ,pl: 'Brak danych' - } - ,'Low' : { - cs: 'Nízká' - ,de: 'Tief' - ,es: 'Bajo' - ,fr: 'Bas' - ,pt: 'Baixo' - ,sv: 'Låg' - ,ro: 'Prea jos' - ,bg: 'Ниска' - ,hr: 'Nizak' - ,it: 'Basso' - ,dk: 'Lav' - ,fi: 'Matala' - ,nb: 'Lav' - ,he: 'נמוך' - ,pl: 'Niski' - } - ,'In Range' : { - cs: 'V rozsahu' - ,de: 'Im Zielbereich' - ,es: 'En rango' - ,fr: 'dans la norme' - ,pt: 'Na meta' - ,sv: 'Inom intervallet' - ,ro: 'În interval' - ,bg: 'В граници' - ,hr: 'U rasponu' - ,it: 'In intervallo' - ,dk: 'Indenfor intervallet' - ,fi: 'Tavoitealueella' - ,nb: 'Innenfor intervallet' - ,he: 'בטווח' - ,pl: 'W zakresie' - } - ,'Period' : { - cs: 'Období' - ,de: 'Zeitraum' - ,es: 'Periodo' - ,fr: 'Période' - ,pt: 'Período' - ,sv: 'Period' - ,ro: 'Perioada' - ,bg: 'Период' - ,hr: 'Period' - ,it: 'Periodo' - ,dk: 'Period' - ,fi: 'Aikaväli' - ,nb: 'Periode' - ,he: 'תקופה' - ,pl: 'Okres' - } - ,'High' : { - cs: 'Vysoká' - ,de: 'Hoch' - ,es: 'Alto' - ,fr: 'Haut' - ,pt: 'Alto' - ,sv: 'Hög' - ,ro: 'Prea sus' - ,bg: 'Висока' - ,hr: 'Visok' - ,it: 'Alto' - ,dk: 'Høj' - ,fi: 'Korkea' - ,nb: 'Høy' - ,he: 'גבוה' - ,pl: 'Wysoki' - } - ,'Average' : { - cs: 'Průměrná' - ,de: 'Mittelwert' - ,es: 'Media' - ,fr: 'Moyenne' - ,pt: 'Média' - ,sv: 'Genomsnittligt' - ,ro: 'Media' - ,bg: 'Средна' - ,hr: 'Prosjek' - ,it: 'Media' - ,dk: 'Gennemsnit' - ,fi: 'Keskiarvo' - ,nb: 'Gjennomsnitt' - ,he: 'ממוצע' - ,pl: 'Średnia' - } - ,'Low Quartile' : { - cs: 'Nízký kvartil' - ,de: 'Unteres Quartil' - ,es: 'Cuartil inferior' - ,fr: 'Quartile inférieur' - ,pt: 'Quartil inferior' - ,ro: 'Pătrime inferioară' - ,bg: 'Ниска четвъртинка' - ,hr: 'Donji kvartil' - ,sv: 'Nedre kvadranten' - ,it: 'Quartile basso' - ,dk: 'Nedre kvartil' - ,fi: 'Alin neljäsosa' - ,nb: 'Nedre kvartil' - ,he: 'רבעון נמוך' - ,pl: 'Dolny kwartyl' - } - ,'Upper Quartile' : { - cs: 'Vysoký kvartil' - ,de: 'Oberes Quartil' - ,es: 'Cuartil superior' - ,fr: 'Quartile supérieur' - ,pt: 'Quartil superior' - ,ro: 'Pătrime superioară' - ,bg: 'Висока четвъртинка' - ,hr: 'Gornji kvartil' - ,sv: 'Övre kvadranten' - ,it: 'Quartile alto' - ,dk: 'Øvre kvartil' - ,fi: 'Ylin neljäsosa' - ,nb: 'Øvre kvartil' - ,he: 'רבעון גבוה' - ,pl: 'Górny kwartyl' - } - ,'Quartile' : { - cs: 'Kvartil' - ,de: 'Quartil' - ,es: 'Cuartil' - ,fr: 'Quartile' - ,pt: 'Quartil' - ,ro: 'Pătrime' - ,bg: 'Четвъртинка' - ,hr: 'Kvartil' - ,sv: 'Kvadrant' - ,it: 'Quartile' - ,dk: 'Kvartil' - ,fi: 'Neljäsosa' - ,nb: 'Kvartil' - ,he: 'רבעון' - ,pl: 'Kwartał' - } - ,'Date' : { - cs: 'Datum' - ,de: 'Datum' - ,es: 'Fecha' - ,fr: 'Date' - ,pt: 'Data' - ,sv: 'Datum' - ,ro: 'Data' - ,bg: 'Дата' - ,hr: 'Datum' - ,it: 'Data' - ,dk: 'Dato' - ,fi: 'Päivämäärä' - ,nb: 'Dato' - ,he: 'תאריך' - ,pl: 'Data' - } - ,'Normal' : { - cs: 'Normální' - ,de: 'Normal' - ,es: 'Normal' - ,fr: 'Normale' - ,pt: 'Normal' - ,sv: 'Normal' - ,ro: 'Normal' - ,bg: 'Нормалнa' - ,hr: 'Normalno' - ,it: 'Normale' - ,dk: 'Normal' - ,fi: 'Normaali' - ,nb: 'Normal' - ,pl: 'Norma' - } - ,'Median' : { - cs: 'Medián' - ,de: 'Median' - ,es: 'Mediana' - ,fr: 'Médiane' - ,pt: 'Mediana' - ,ro: 'Mediană' - ,bg: 'Средно' - ,hr: 'Srednje' - ,sv: 'Median' - ,it: 'Mediana' - ,dk: 'Median' - ,fi: 'Mediaani' - ,nb: 'Median' - ,he: 'חציון' - ,pl: 'Mediana' - } - ,'Readings' : { - cs: 'Záznamů' - ,de: 'Messwerte' - ,es: 'Valores' - ,fr: 'Valeurs' - ,pt: 'Valores' - ,sv: 'Avläsningar' - ,ro: 'Valori' - ,bg: 'Измервания' - ,hr: 'Vrijednosti' - ,it: 'Valori' - ,dk: 'Aflæsning' - ,fi: 'Lukemia' - ,nb: 'Avlesning' - ,he: 'קריאות' - ,pl: 'Odczyty' - } - ,'StDev' : { - cs: 'St. odchylka' - ,de: 'Standardabweichung' - ,es: 'Desviación estándar' - ,fr: 'Déviation St.' - ,pt: 'DesvPadr' - ,sv: 'StdDev' - ,ro: 'Standarddeviation' - ,bg: 'Стандартно отклонение' - ,hr: 'Standardna devijacija' - ,it: 'Deviazione standard' - ,dk: 'Standard afvigelse' - ,fi: 'Keskijakauma' - ,nb: 'Standardavvik' - ,he: 'סטיית תקן' - ,pl: 'Stand. odchyłka' - } - ,'Daily stats report' : { - cs: 'Denní statistiky' - ,de: 'Tagesstatistik Bericht' - ,es: 'Informe de estadísticas diarias' - ,fr: 'Rapport quotidien' - ,pt: 'Relatório diário' - ,ro: 'Raport statistică zilnică' - ,bg: 'Дневна статистика' - ,hr: 'Izvješće o dnevnim statistikama' - ,sv: 'Dygnsstatistik' - ,it: 'Statistiche giornaliere' - ,dk: 'Daglig statistik rapport' - ,fi: 'Päivittäinen tilasto' - ,nb: 'Daglig statistikkrapport' - ,he: 'דוח סטטיסטיקה יומית' - ,pl: 'Dzienne statystyki' - } - ,'Glucose Percentile report' : { - cs: 'Tabulka percentil glykémií' - ,de: 'Glukose-Prozent Bericht' - ,es: 'Informe de percetiles de glucemia' - ,fr: 'Rapport percentiles Glycémie' - ,pt: 'Relatório de Percentis de Glicemia' - ,sv: 'Glukosrapport i procent' - ,ro: 'Raport percentile glicemii' - ,bg: 'Графика на КЗ' - ,hr: 'Izvješće o postotku GUK-a' - ,it: 'Percentuale Glicemie' - ,dk: 'Glukoserapport i procent' - ,fi: 'Glukoosin suhteeellinen tilasto' - ,nb: 'Glukoserapport i prosent' - ,pl: 'Tabela centylowa glikemii' - } - ,'Glucose distribution' : { - cs: 'Rozložení glykémií' - ,de: 'Glukuse Verteilung' - ,es: 'Distribución de glucemias' - ,fr: 'Distribution glycémies' - ,pt: 'Distribuição de glicemias' - ,ro: 'Distribuție glicemie' - ,bg: 'Разпределение на КЗ' - ,hr: 'Distribucija GUK-a' - ,sv: 'Glukosdistribution' - ,it: 'Distribuzione glicemie' - ,dk: 'Glukosefordeling' - ,fi: 'Glukoosijakauma' - ,nb: 'Glukosefordeling' - ,he: 'פיזור סוכר' - ,pl: 'Rozkład glikemii' - } - ,'days total' : { - cs: 'dní celkem' - ,de: 'Gesamttage' - ,es: 'Total de días' - ,fr: 'jours totaux' - ,pt: 'dias no total' - ,sv: 'antal dagar' - ,ro: 'total zile' - ,bg: 'общо за деня' - ,hr: 'ukupno dana' - ,it: 'Giorni totali' - ,dk: 'antal dage' - ,fi: 'päivä yhteensä' - ,nb: 'antall dager' - ,he: 'מספר ימים' - ,pl: 'dni łącznie' - } - ,'Overall' : { - cs: 'Celkem' - ,de: 'Insgesamt' - ,es: 'General' - ,fr: 'En général' - ,pt: 'Geral' - ,sv: 'Genomsnitt' - ,ro: 'General' - ,bg: 'Общо' - ,hr: 'Ukupno' - ,it: 'Generale' - ,dk: 'Overall' - ,fi: 'Kaiken kaikkiaan' - ,nb: 'Generelt' - ,he: 'סך הכל' - ,pl: 'Ogółem' - } - ,'Range' : { - cs: 'Rozsah' - ,de: 'Bereich' - ,es: 'Intervalo' - ,fr: 'Intervalle' - ,pt: 'intervalo' - ,sv: 'Intervall' - ,ro: 'Interval' - ,bg: 'Диапазон' - ,hr: 'Raspon' - ,it: 'Intervallo' - ,dk: 'Interval' - ,fi: 'Alue' - ,nb: 'Intervall' - ,he: 'טווח' - ,pl: 'Zakres' - } - ,'% of Readings' : { - cs: '% záznamů' - ,de: '% der Messwerte' - ,es: '% de valores' - ,fr: '% de valeurs' - ,pt: '% de valores' - ,sv: '& av avläsningar' - ,ro: '% de valori' - ,bg: '% от измервания' - ,hr: '% očitanja' - ,it: '% dei valori' - ,dk: '% af aflæsningerne' - ,fi: '% lukemista' - ,nb: '% af avlesningene' - ,he: 'אחוז קריאות' - ,pl: '% Odczytów' - } - ,'# of Readings' : { - cs: 'počet záznamů' - ,de: '# der Messwerte' - ,es: 'N° de valores' - ,fr: 'nbr de valeurs' - ,pt: 'N° de valores' - ,sv: '# av avläsningar' - ,ro: 'nr. de valori' - ,bg: '№ от измервания' - ,hr: 'broj očitanja' - ,it: 'N° di valori' - ,dk: 'Antal af aflæsninger' - ,fi: 'Lukemien määrä' - ,nb: 'Antall avlesninger' - ,he: 'מספר קריאות' - ,pl: 'Ilość Odczytów' - } - ,'Mean' : { - cs: 'Střední hodnota' - ,de: 'Durchschnittlich' - ,es: 'Media' - ,fr: 'Moyenne' - ,pt: 'Média' - ,sv: 'Genomsnitt' - ,ro: 'Medie' - ,bg: 'Средна стойност' - ,hr: 'Prosjek' - ,it: 'Media' - ,dk: 'Gennemsnit' - ,fi: 'Keskiarvo' - ,nb: 'Gjennomsnitt' - ,he: 'ממוצע' - ,pl: 'Wartość średnia' - } - ,'Standard Deviation' : { - cs: 'Standardní odchylka' - ,de: 'Standardabweichung' - ,es: 'Desviación estándar' - ,fr: 'Déviation Standard' - ,pt: 'Desvio padrão' - ,ro: 'Deviație standard' - ,bg: 'Стандартно отклонение' - ,hr: 'Standardna devijacija' - ,sv: 'Standardavvikelse' - ,it: 'Deviazione Standard' - ,dk: 'Standardafgivelse' - ,fi: 'Keskijakauma' - ,nb: 'Standardavvik' - ,he: 'סטיית תקן' - ,pl: 'Standardowa odcyłka' - } - ,'Max' : { - cs: 'Max' - ,de: 'Max' - ,es: 'Max' - ,fr: 'Max' - ,pt: 'Máx' - ,sv: 'Max' - ,ro: 'Max' - ,bg: 'Макс.' - ,hr: 'Max' - ,it: 'Max' - ,dk: 'Max' - ,fi: 'Maks' - ,nb: 'Max' - ,he: 'מקסימאלי' - ,pl: 'max' - } - ,'Min' : { - cs: 'Min' - ,de: 'Min' - ,es: 'Min' - ,fr: 'Min' - ,pt: 'Mín' - ,sv: 'Min' - ,ro: 'Min' - ,bg: 'Мин.' - ,hr: 'Min' - ,it: 'Min' - ,dk: 'Min' - ,fi: 'Min' - ,nb: 'Min' - ,he: 'מינימאלי' - ,pl: 'Min' - } - ,'A1c estimation*' : { - cs: 'Předpokládané HBA1c*' - ,de: 'Prognose HbA1c*' - ,es: 'Estimación de HbA1c*' - ,fr: 'Estimation HbA1c*' - ,pt: 'HbA1c estimada*' - ,ro: 'HbA1C estimată' - ,bg: 'Очакван HbA1c' - ,hr: 'Procjena HbA1c-a' - ,sv: 'Beräknat A1c-värde ' - ,it: 'Stima A1c' - ,dk: 'Beregnet A1c-værdi ' - ,fi: 'A1c arvio*' - ,nb: 'Beregnet HbA1c' - ,he: 'משוער A1c' - ,pl: 'HbA1c oczekiwany' - } - ,'Weekly Success' : { - cs: 'Týdenní úspěšnost' - ,de: 'Wöchtlicher Erfolg' - ,es: 'Resultados semanales' - ,fr: 'Réussite hebdomadaire' - ,pt: 'Resultados semanais' - ,ro: 'Rezultate săptămânale' - ,bg: 'Седмичен успех' - ,hr: 'Tjedni uspjeh' - ,sv: 'Veckoresultat' - ,it: 'Risultati settimanali' - ,dk: 'Uge resultat' - ,fi: 'Viikottainen tulos' - ,nb: 'Ukeresultat' - ,pl: 'Tygodniowe sukcesy' - } - ,'There is not sufficient data to run this report. Select more days.' : { - cs: 'Není dostatek dat. Vyberte delší časové období.' - ,de: 'Für diesen Bericht sind nicht genug Daten verfügbar. Bitte weitere Tage auswählen' - ,es: 'No hay datos suficientes para generar este informe. Seleccione más días.' - ,fr: 'Pas assez de données pour un rapport. Sélectionnez plus de jours.' - ,pt: 'Não há dados suficientes. Selecione mais dias' - ,ro: 'Nu sunt sufieciente date pentru acest raport. Selectați mai multe zile.' - ,bg: 'Няма достатъчно данни за показване. Изберете повече дни.' - ,hr: 'Nema dovoljno podataka za izvođenje izvještaja. Odaberite još dana.' - ,sv: 'Data saknas för att köra rapport. Välj fler dagar.' - ,it: 'Non ci sono dati sufficienti per eseguire questo rapporto. Selezionare più giorni.' - ,dk: 'Der er utilstrækkeligt data til at generere rapporten. Vælg flere dage.' - ,fi: 'Raporttia ei voida luoda liian vähäisen tiedon vuoksi. Valitse useampia päiviä.' - ,nb: 'Der er ikke nok data til å lage rapporten. Velg flere dager.' - ,he: 'לא נמצא מספיק מידע ליצירת הדוח. בחר ימים נוספים.' - ,pl: 'Nie ma wystarczających danych dla tego raportu. Wybierz więcej dni. ' - } -// food editor - ,'Using stored API secret hash' : { - cs: 'Používám uložený hash API hesla' - ,de: 'Gespeicherte API-Prüfsumme verwenden' - ,es: 'Usando el hash del API pre-almacenado' - ,fr: 'Utilisation du hash API existant' - ,pt: 'Usando o hash de API existente' - ,ro: 'Utilizez cheie API secretă' - ,bg: 'Използване на запаметена API парола' - ,hr: 'Koristi se pohranjeni API tajni hash' - ,sv: 'Använd hemlig API-nyckel' - ,it: 'Stai utilizzando API hash segreta' - ,dk: 'Anvender gemt API-nøgle' - ,fi: 'Tallennettu salainen API-tarkiste käytössä' - ,nb: 'Bruker lagret API nøkkel' - ,pl: 'Kożystajac z zapisanego poufnego API' - } - ,'No API secret hash stored yet. You need to enter API secret.' : { - cs: 'Není uložený žádný hash API hesla. Musíte zadat API heslo.' - ,de: 'Keine API-Prüfsumme gespeichert. Bitte API-Prüfsumme eingeben.' - ,es: 'No se ha almacenado ningún hash todavía. Debe introducir su secreto API.' - ,fr: 'Pas de secret API existant. Vous devez l\'en entrer.' - ,pt: 'Hash de segredo de API inexistente. Insira um segredo de API.' - ,ro: 'Încă nu există cheie API secretă. Aceasta trebuie introdusă.' - ,bg: 'Няма запаметена API парола. Tрябва да въведете API парола' - ,hr: 'Nema pohranjenog API tajnog hasha. Unesite tajni API' - ,sv: 'Hemlig api-nyckel saknas. Du måste ange API hemlighet' - ,it: 'API hash segreto non è ancora memorizzato. È necessario inserire API segreto.' - ,dk: 'Mangler API-nøgle. Du skal indtaste API hemmelighed' - ,fi: 'Salainen API-tarkiste puuttuu. Syötä API tarkiste.' - ,nb: 'Mangler API nøkkel. Du må skrive inn API hemmelighet.' - ,pl: 'Nie ma żadnego poufnego API zapisanego. Należy wprowadzić poufne API.' - } - ,'Database loaded' : { - cs: 'Databáze načtena' - ,de: 'Datenbank geladen' - ,es: 'Base de datos cargada' - ,fr: 'Base de données chargée' - ,pt: 'Banco de dados carregado' - ,ro: 'Baza de date încărcată' - ,bg: 'База с данни заредена' - ,hr: 'Baza podataka je učitana' - ,sv: 'Databas laddad' - ,it: 'Database caricato' - ,dk: 'Database indlæst' - ,fi: 'Tietokanta ladattu' - ,nb: 'Database lest' - ,pl: 'Baza danych załadowana' - } - ,'Error: Database failed to load' : { - cs: 'Chyba při načítání databáze' - ,de: 'Fehler: Datenbank konnte nicht geladen werden' - ,es: 'Error: Carga de base de datos fallida' - ,fr: 'Erreur: le chargement de la base de données a échoué' - ,pt: 'Erro: Banco de dados não carregado' - ,ro: 'Eroare: Nu s-a încărcat baza de date' - ,bg: 'ГРЕШКА. Базата с данни не успя да се зареди' - ,hr: 'Greška: Baza podataka nije učitana' - ,sv: 'Error: Databas kan ej laddas' - ,it: 'Errore: database non è stato caricato' - ,dk: 'Fejl: Database kan ikke indlæses' - ,fi: 'Virhe: Tietokannan lataaminen epäonnistui' - ,nb: 'Feil: Database kan ikke leses' - ,he: 'שגיאה: לא ניתן לטעון בסיס נתונים' - ,pl: 'Błąd, baza danych nie może być załadowana' - } - ,'Create new record' : { - cs: 'Vytvořit nový záznam' - ,de: 'Erstelle neuen Datensatz' - ,es: 'Crear nuevo registro' - ,fr: 'Créer nouvel enregistrement' - ,pt: 'Criar novo registro' - ,ro: 'Crează înregistrare nouă' - ,bg: 'Създаване на нов запис' - ,hr: 'Kreiraj novi zapis' - ,sv: 'Skapa ny post' - ,it: 'Crea nuovo registro' - ,dk: 'Danner ny post' - ,fi: 'Luo uusi tallenne' - ,nb: 'Lager ny registrering' - ,he: 'צור רשומה חדשה' - ,pl: 'Tworzenie nowego rekordu' - } - ,'Save record' : { - cs: 'Uložit záznam' - ,de: 'Speichere Datensatz' - ,es: 'Guardar registro' - ,fr: 'Sauver enregistrement' - ,pt: 'Salvar registro' - ,ro: 'Salvează înregistrarea' - ,bg: 'Запази запис' - ,hr: 'Spremi zapis' - ,sv: 'Spara post' - ,it: 'Salva Registro' - ,dk: 'Gemmer post' - ,fi: 'Tallenna' - ,nb: 'Lagrer registrering' - ,he: 'שמור רשומה' - ,pl: 'Zapisz rekord' - } - ,'Portions' : { - cs: 'Porcí' - ,de: 'Portionen' - ,es: 'Porciones' - ,fr: 'Portions' - ,pt: 'Porções' - ,ro: 'Porții' - ,bg: 'Порции' - ,hr: 'Dijelovi' - ,sv: 'Portion' - ,it: 'Porzioni' - ,dk: 'Portioner' - ,fi: 'Annokset' - ,nb: 'Porsjoner' - ,he: 'מנות' - ,pl: 'Porcja' - } - ,'Unit' : { - cs: 'Jedn' - ,de: 'Einheit' - ,es: 'Unidades' - ,fr: 'Unités' - ,pt: 'Unidade' - ,ro: 'Unități' - ,bg: 'Единици' - ,hr: 'Jedinica' - ,sv: 'Enhet' - ,it: 'Unità' - ,dk: 'Enheder' - ,fi: 'Yksikkö' - ,nb: 'Enhet' - ,he: 'יחידות' - ,pl: 'Jednostka' - } - ,'GI' : { - cs: 'GI' - ,de: 'GI' - ,es: 'IG' - ,fr: 'IG' - ,pt: 'IG' - ,sv: 'GI' - ,ro: 'CI' - ,bg: 'ГИ' - ,hr: 'GI' - ,it: 'GI' - ,dk: 'GI' - ,fi: 'GI' - ,nb: 'GI' - ,pl: 'GI' - } - ,'Edit record' : { - cs: 'Upravit záznam' - ,de: 'Bearbeite Datensatz' - ,es: 'Editar registro' - ,fr: 'Modifier enregistrement' - ,pt: 'Editar registro' - ,ro: 'Editează înregistrarea' - ,bg: 'Редактирай запис' - ,hr: 'Uredi zapis' - ,sv: 'Editera post' - ,it: 'Modifica registro' - ,dk: 'Editere post' - ,fi: 'Muokkaa tallennetta' - ,nb: 'Editere registrering' - ,he: 'ערוך רשומה' - ,pl: 'Edytcja rekord' - } - ,'Delete record' : { - cs: 'Smazat záznam' - ,de: 'Lösche Datensatz' - ,es: 'Borrar registro' - ,fr: 'Effacer enregistrement' - ,pt: 'Apagar registro' - ,ro: 'Șterge înregistrarea' - ,bg: 'Изтрий запис' - ,hr: 'Izbriši zapis' - ,sv: 'Radera post' - ,it: 'Cancella registro' - ,dk: 'Slet post' - ,fi: 'Tuhoa tallenne' - ,nb: 'Slette registrering' - ,he: 'מחק רשומה' - ,pl: 'Usuń rekord' - } - ,'Move to the top' : { - cs: 'Přesuň na začátek' - ,de: 'Gehe zum Anfang' - ,es: 'Mover arriba' - ,fr: 'Déplacer au sommet' - ,pt: 'Mover para o topo' - ,sv: 'Gå till toppen' - ,ro: 'Mergi la început' - ,bg: 'Преместване в началото' - ,hr: 'Premjesti na vrh' - ,it: 'Spostare verso l\'alto' - ,dk: 'Gå til toppen' - ,fi: 'Siirrä ylimmäksi' - ,nb: 'Gå til toppen' - ,he: 'עבור למעלה' - ,pl: 'przejdź do góry' - } - ,'Hidden' : { - cs: 'Skrytý' - ,de: 'Versteckt' - ,es: 'Oculto' - ,fr: 'Caché' - ,pt: 'Oculto' - ,sv: 'Dold' - ,ro: 'Ascuns' - ,bg: 'Скрити' - ,hr: 'Skriveno' - ,it: 'Nascosto' - ,dk: 'Skjult' - ,fi: 'Piilotettu' - ,nb: 'Skjult' - ,he: 'מוסתר' - ,pl: 'Ukryte' - } - ,'Hide after use' : { - cs: 'Skryj po použití' - ,de: 'Verberge nach Gebrauch' - ,es: 'Ocultar después de utilizar' - ,fr: 'Cacher après utilisation' - ,pt: 'Ocultar após uso' - ,ro: 'Ascunde după folosireaa' - ,bg: 'Скрий след употреба' - ,hr: 'Sakrij nakon korištenja' - ,sv: 'Dölj efter användning' - ,it: 'Nascondi dopo l\'uso' - ,dk: 'Skjul efter brug' - ,fi: 'Piilota käytön jälkeen' - ,nb: 'Skjul etter bruk' - ,he: 'הסתר לאחר שימוש' - ,pl: 'Ukryj po użyciu' - } - ,'Your API secret must be at least 12 characters long' : { - cs: 'Vaše API heslo musí mít alespoň 12 znaků' - ,de: 'Deine API-Prüfsumme muss mindestens 12 Zeichen lang sein' - ,es: 'Su secreo API debe contener al menos 12 caracteres' - ,fr: 'Votre secret API doit contenir au moins 12 caractères' - ,pt: 'Seu segredo de API deve conter no mínimo 12 caracteres' - ,ro: 'Cheia API trebuie să aibă mai mult de 12 caractere' - ,bg: 'Вашата АPI парола трябва да е дълга поне 12 символа' - ,hr: 'Vaš tajni API mora sadržavati barem 12 znakova' - ,sv: 'Hemlig API-nyckel måsta innehålla 12 tecken' - ,it: 'il vostro API secreto deve essere lungo almeno 12 caratteri' - ,dk: 'Din API nøgle skal være mindst 12 tegn lang' - ,fi: 'API-avaimen tulee olla ainakin 12 merkin mittainen' - ,nb: 'Din API nøkkel må være minst 12 tegn lang' - ,pl: 'Twoje poufne API musi zawierać co majmniej 12 znaków' - } - ,'Bad API secret' : { - cs: 'Chybné API heslo' - ,de: 'Fehlerhafte API-Prüfsumme' - ,es: 'Secreto API incorrecto' - ,fr: 'Secret API erroné' - ,pt: 'Segredo de API incorreto' - ,ro: 'Cheie API greșită' - ,bg: 'Некоректна API парола' - ,hr: 'Neispravan tajni API' - ,sv: 'Felaktig API-nyckel' - ,it: 'API secreto non corretto' - ,dk: 'Forkert API-nøgle' - ,fi: 'Väärä API-avain' - ,nb: 'Ugyldig API nøkkel' - ,pl: 'Błędne poufne API' - } - ,'API secret hash stored' : { - cs: 'Hash API hesla uložen' - ,de: 'API-Prüfsumme gespeichert' - ,es: 'Hash de secreto API guardado' - ,fr: 'Hash API secret sauvegardé' - ,pt: 'Segredo de API guardado' - ,ro: 'Cheie API înregistrată' - ,bg: 'УРА! API парола запаметена' - ,hr: 'API tajni hash je pohranjen' - ,sv: 'Lagrad hemlig API-hash' - ,it: 'Hash API secreto memorizzato' - ,dk: 'Hemmelig API-hash gemt' - ,fi: '' - ,nb: 'API nøkkel lagret' - ,pl: 'Poufne API zapisane' - } - ,'Status' : { - cs: 'Status' - ,de: 'Status' - ,es: 'Estado' - ,fr: 'Statut' - ,pt: 'Status' - ,sv: 'Status' - ,ro: 'Status' - ,bg: 'Статус' - ,hr: 'Status' - ,it: 'Stato' - ,dk: 'Status' - ,fi: 'Tila' - ,nb: 'Status' - ,he: 'מצב מערכת' - ,pl: 'Status' - } - ,'Not loaded' : { - cs: 'Nenačtený' - ,de: 'Nicht geladen' - ,es: 'No cargado' - ,fr: 'Non chargé' - ,pt: 'Não carregado' - ,ro: 'Neîncărcat' - ,bg: 'Не е заредено' - ,hr: 'Nije učitano' - ,sv: 'Ej laddad' - ,it: 'Non caricato' - ,dk: 'Ikke indlæst' - ,fi: 'Ei ladattu' - ,nb: 'Ikke lest' - ,pl: 'Nie załadowany' - } - ,'Food Editor' : { - cs: 'Editor jídel' - ,de: 'Nahrungsmittel Editor' - ,es: 'Editor de alimentos' - ,fr: 'Editeur aliments' - ,pt: 'Editor de alimentos' - ,ro: 'Editor alimente' - ,bg: 'Редактор за храна' - ,hr: 'Editor hrane' - ,sv: 'Födoämneseditor' - ,it: 'Modifica alimenti' - ,dk: 'Mad editor' - ,fi: 'Muokkaa ruokia' - ,nb: 'Mat editor' - ,pl: 'Edytor posiłków' - } - ,'Your database' : { - cs: 'Vaše databáze' - ,de: 'Deine Datenbank' - ,es: 'Su base de datos' - ,fr: 'Votre base de données' - ,pt: 'Seu banco de dados' - ,sv: 'Din databas' - ,ro: 'Baza de date' - ,bg: 'Твоята база с данни' - ,hr: 'Vaša baza podataka' - ,it: 'Vostro database' - ,dk: 'Din database' - ,fi: 'Tietokantasi' - ,nb: 'Din database' - ,he: 'בסיס הנתונים שלך' - ,pl: 'Twoja baza danych' - } - ,'Filter' : { - cs: 'Filtr' - ,de: 'Filter' - ,es: 'Filtro' - ,fr: 'Filtre' - ,pt: 'Filtro' - ,sv: 'Filter' - ,ro: 'Filtru' - ,bg: 'Филтър' - ,hr: 'Filter' - ,it: 'Filtro' - ,dk: 'Filter' - ,fi: 'Suodatin' - ,nb: 'Filter' - ,he: 'סנן' - ,pl: 'Filtr' - } - ,'Save' : { - cs: 'Ulož' - ,de: 'Speichern' - ,es: 'Salvar' - ,fr: 'Sauver' - ,pt: 'Salvar' - ,ro: 'Salvează' - ,bg: 'Запази' - ,hr: 'Spremi' - ,sv: 'Spara' - ,it: 'Salva' - ,dk: 'Gem' - ,fi: 'Tallenna' - ,nb: 'Lagre' - ,he: 'שמור' - ,pl: 'Zapisz' - } - ,'Clear' : { - cs: 'Vymaž' - ,de: 'Löschen' - ,es: 'Limpiar' - ,fr: 'Effacer' - ,pt: 'Apagar' - ,ro: 'Inițializare' - ,bg: 'Изчисти' - ,hr: 'Očisti' - ,sv: 'Rensa' - ,it: 'Pulisci' - ,dk: 'Rense' - ,fi: 'Tyhjennä' - ,nb: 'Tøm' - ,he: 'נקה' - ,pl: 'Wyczyść' - } - ,'Record' : { - cs: 'Záznam' - ,de: 'Datensatz' - ,es: 'Guardar' - ,fr: 'Enregistrement' - ,pt: 'Gravar' - ,sv: 'Post' - ,ro: 'Înregistrare' - ,bg: 'Запиши' - ,hr: 'Zapis' - ,it: 'Registro' - ,dk: 'Post' - ,fi: 'Tietue' - ,nb: 'Registrering' - ,he: 'רשומה' - ,pl: 'Rekord' - } - ,'Quick picks' : { - cs: 'Rychlý výběr' - ,de: 'Schnellauswahl' - ,es: 'Selección rápida' - ,fr: 'Sélection rapide' - ,pt: 'Seleção rápida' - ,ro: 'Selecție rapidă' - ,bg: 'Бърз избор' - ,hr: 'Brzi izbor' - ,sv: 'Snabbval' - ,it: 'Scelta rapida' - ,dk: 'Hurtig valg' - ,fi: 'Nopeat valinnat' - ,nb: 'Hurtigvalg' - ,he: 'בחירה מהירה' - ,pl: 'Szybki wybór' - } - ,'Show hidden' : { - cs: 'Zobraz skryté' - ,de: 'Zeige verborgen' - ,es: 'Mostrar ocultos' - ,fr: 'Montrer cachés' - ,pt: 'Mostrar ocultos' - ,ro: 'Arată înregistrările ascunse' - ,bg: 'Покажи скритото' - ,hr: 'Prikaži skriveno' - ,sv: 'Visa dolda' - ,it: 'Mostra nascosto' - ,dk: 'Vis skjulte' - ,fi: 'Näytä piilotettu' - ,nb: 'Vis skjulte' - ,he: 'הצג נתונים מוסתרים' - ,pl: 'Pokaż ukryte' - } - ,'Your API secret' : { - cs: 'Vaše API heslo' - ,de: 'Deine API Prüfsumme' - ,es: 'Su secreto API' - ,fr: 'Votre secret API' - ,pt: 'Seu segredo de API' - ,sv: 'Din API-nyckel' - ,ro: 'Cheia API' - ,bg: 'Твоята API парола' - ,hr: 'Vaš tajni API' - ,it: 'Il tuo API secreto' - ,dk: 'Din API-nøgle' - ,fi: 'Sinun API-avaimesi' - ,nb: 'Din API nøkkel' - ,pl: 'Twoje poufne hasło API' - } - ,'Store hash on this computer (Use only on private computers)' : { - cs: 'Ulož hash na tomto počítači (používejte pouze na soukromých počítačích)' - ,de: 'Speichere Prüfsumme auf diesem Computer (nur auf privaten Computern anwenden)' - ,es: 'Guardar hash en este ordenador (Usar solo en ordenadores privados)' - ,fr: 'Sauver le hash sur cet ordinateur (privé uniquement)' - ,pt: 'Salvar hash nesse computador (Somente em computadores privados)' - ,ro: 'Salvează cheia pe acest PC (Folosiți doar PC de încredere)' - ,bg: 'Запамети данните на този компютър. ( Използвай само на собствен компютър)' - ,hr: 'Pohrani hash na ovom računalu (Koristiti samo na osobnom računalu)' - ,sv: 'Lagra hashvärde på denna dator (använd endast på privat dator)' - ,it: 'Conservare hash su questo computer (utilizzare solo su computer privati)' - ,dk: 'Gemme hash på denne computer (brug kun på privat computer)' - ,fi: 'Tallenna avain tälle tietokoneelle (käytä vain omalla tietokoneellasi)' - ,nb: 'Lagre hash på denne pc (bruk kun på privat pc)' - ,pl: 'Zapisane na tym komputerze (Kożystaj tylko na komputerach prywatnych)' - } - ,'Treatments' : { - cs: 'Ošetření' - ,de: 'Bearbeitung' - ,es: 'Tratamientos' - ,fr: 'Traitements' - ,pt: 'Procedimentos' - ,sv: 'Behandling' - ,ro: 'Tratamente' - ,bg: 'Събития' - ,hr: 'Tretmani' - ,it: 'Somministrazione' - ,dk: 'Behandling' - ,fi: 'Hoitotoimenpiteet' - ,nb: 'Behandlinger' - ,he: 'טיפולים' - ,pl: 'Zabiegi' - } - ,'Time' : { - cs: 'Čas' - ,de: 'Zeit' - ,es: 'Hora' - ,fr: 'Heure' - ,pt: 'Hora' - ,sv: 'Tid' - ,ro: 'Ora' - ,bg: 'Време' - ,hr: 'Vrijeme' - ,it: 'Tempo' - ,dk: 'Tid' - ,fi: 'Aika' - ,nb: 'Tid' - ,he: 'זמן' - ,pl: 'Czas' - } - ,'Event Type' : { - cs: 'Typ události' - ,de: 'Ereignis-Typ' - ,es: 'Tipo de evento' - ,fr: 'Type d\'événement' - ,pt: 'Tipo de evento' - ,sv: 'Händelsetyp' - ,ro: 'Tip eveniment' - ,bg: 'Вид събитие' - ,hr: 'Vrsta događaja' - ,it: 'Tipo di evento' - ,dk: 'Hændelsestype' - ,fi: 'Tapahtumatyyppi' - ,nb: 'Type' - ,he: 'סוג אירוע' - ,pl: 'Typ zdażenia' - } - ,'Blood Glucose' : { - cs: 'Glykémie' - ,de: 'Blutglukose' - ,es: 'Glucemia' - ,fr: 'Glycémie' - ,pt: 'Glicemia' - ,sv: 'Glukosvärde' - ,ro: 'Glicemie' - ,bg: 'Кръвна захар' - ,hr: 'GUK' - ,it: 'Glicemie' - ,dk: 'Glukoseværdi' - ,fi: 'Verensokeri' - ,nb: 'Blodsukker' - ,he: 'סוכר בדם' - ,pl: 'Glikemia z krwi' - } - ,'Entered By' : { - cs: 'Zadal' - ,de: 'Eingabe durch' - ,es: 'Introducido por' - ,fr: 'Entré par' - ,pt: 'Inserido por' - ,sv: 'Inlagt av' - ,ro: 'Introdus de' - ,bg: 'Въведено от' - ,hr: 'Unos izvršio' - ,it: 'inserito da' - ,dk: 'Indtastet af' - ,fi: 'Tiedot syötti' - ,nb: 'Lagt inn av' - ,he: 'הוזן על-ידי' - ,pl: 'Wprowadzono przez' - } - ,'Delete this treatment?' : { - cs: 'Vymazat toto ošetření?' - ,de: 'Bearbeitung löschen' - ,es: '¿Borrar este tratamiento?' - ,fr: 'Effacer ce traitement?' - ,pt: 'Apagar este procedimento?' - ,ro: 'Șterge acest eveniment?' - ,bg: 'Изтрий това събитие' - ,hr: 'Izbriši ovaj tretman?' - ,sv: 'Ta bort händelse?' - ,it: 'Eliminare questa somministrazione?' - ,dk: 'Slet denne hændelse?' - ,fi: 'Tuhoa tämä hoitotoimenpide?' - ,nb: 'Slett denne hendelsen?' - ,he: 'למחוק רשומה זו?' - ,pl: 'Usunąć ten zabieg?' - } - ,'Carbs Given' : { - cs: 'Sacharidů' - ,de: 'Kohlenhydratgabe' - ,es: 'Hidratos de carbono dados' - ,fr: 'Glucides donnés' - ,pt: 'Carboidratos' - ,ro: 'Carbohidrați' - ,bg: 'ВХ' - ,hr: 'Količina UH' - ,sv: 'Antal kolhydrater' - ,it: 'Carboidrati' - ,dk: 'Antal kulhydrater' - ,fi: 'Hiilihydraatit' - ,nb: 'Karbo' - ,he: 'פחמימות שנאכלו' - ,pl: 'Węglowodany spożyte' - } - ,'Inzulin Given' : { - cs: 'Inzulínu' - ,de: 'Insulingabe' - ,es: 'Insulina dada' - ,fr: 'Insuline donnée' - ,pt: 'Insulina' - ,ro: 'Insulină administrată' - ,bg: 'Инсулин' - ,hr: 'Količina inzulina' - ,sv: 'Insulin' - ,it: 'Insulina' - ,dk: 'Insulin' - ,fi: 'Insuliiniannos' - ,nb: 'Insulin' - ,he: 'אינסולין שניתן' - ,pl: 'Insulina podana' - } - ,'Event Time' : { - cs: 'Čas události' - ,de: 'Ereignis Zeit' - ,es: 'Hora del evento' - ,fr: 'Heure de l\'événement' - ,pt: 'Hora do evento' - ,sv: 'Klockslag' - ,ro: 'Ora evenimentului' - ,bg: 'Въвеждане' - ,hr: 'Vrijeme događaja' - ,it: 'Ora Evento' - ,dk: 'Tidspunkt for hændelsen' - ,fi: 'Aika' - ,nb: 'Tidspunkt for hendelsen' - ,he: 'זמן האירוע' - ,pl: 'Czas zdażenia' - } - ,'Please verify that the data entered is correct' : { - cs: 'Prosím zkontrolujte, zda jsou údaje zadány správně' - ,de: 'Bitte Daten auf Plausibilität prüfen' - ,es: 'Por favor, verifique que los datos introducidos son correctos' - ,fr: 'Merci de vérifier la correction des données entrées' - ,pt: 'Favor verificar se os dados estão corretos' - ,ro: 'Verificați conexiunea datelor introduse' - ,bg: 'Моля проверете, че датата е въведена правилно' - ,hr: 'Molim Vas provjerite jesu li uneseni podaci ispravni' - ,sv: 'Vänligen verifiera att inlagd data är korrekt' - ,it: 'Si prega di verificare che i dati inseriti siano corretti' - ,dk: 'Venligst verificer at indtastet data er korrekt' - ,fi: 'Varmista, että tiedot ovat oikein' - ,nb: 'Vennligst verifiser at inntastet data er korrekt' - ,he: 'נא לוודא שהמידע שהוזן הוא נכון ומדוייק' - ,pl: 'Proszę sprawdzić, czy wprowadzone dane są prawidłowe' - } - ,'BG' : { - cs: 'Glykémie' - ,de: 'Blutglukose' - ,es: 'Glucemia en sangre' - ,fr: 'Glycémie' - ,pt: 'Glicemia' - ,sv: 'BS' - ,ro: 'Glicemie' - ,bg: 'КЗ' - ,hr: 'GUK' - ,it: 'Glicemie' - ,dk: 'BS' - ,fi: 'VS' - ,nb: 'BS' - ,pl: 'BG' - } - ,'Use BG correction in calculation' : { - cs: 'Použij korekci na glykémii' - ,de: 'Verwende Blutglukose-Korrektur zur Kalkulation' - ,es: 'Usar la corrección de glucemia en los cálculos' - ,fr: 'Utiliser la correction de glycémie dans les calculs' - ,pt: 'Usar correção de glicemia nos cálculos' - ,ro: 'Folosește corecția de glicemie în calcule' - ,bg: 'Използвай корекцията за КЗ в изчислението' - ,hr: 'Koristi korekciju GUK-a u izračunu' - ,sv: 'Använd BS-korrektion för beräkning' - ,it: 'Utilizzare la correzione nei calcoli delle Glicemie' - ,dk: 'Anvend BS-korrektion før beregning' - ,fi: 'Käytä korjausannosta laskentaan' - ,nb: 'Bruk blodsukkerkorrigering i beregning' - ,pl: 'Użyj BG w obliczeniach korekty' - } - ,'BG from CGM (autoupdated)' : { - cs: 'Glykémie z CGM (automaticky aktualizovaná)' - ,de: 'Blutglukose vom CGM (Autoupdate)' - ,es: 'Glucemia del sensor (Actualizado automáticamente)' - ,fr: 'Glycémie CGM (automatique)' - ,pt: 'Glicemia do sensor (Automático)' - ,sv: 'BS från CGM (automatiskt)' - ,ro: 'Glicemie în senzor (automat)' - ,bg: 'КЗ от сензора (автоматично)' - ,hr: 'GUK sa CGM-a (ažuriran automatski)' - ,it: 'Glicemie da CGM (aggiornamento automatico)' - ,dk: 'BS fra CGM (automatisk)' - ,fi: 'VS sensorilta (päivitetty automaattisesti)' - ,nb: 'BS fra CGM (automatisk)' - ,pl: 'Wartość BG brana z CGM (automatycznie)' - } - ,'BG from meter' : { - cs: 'Glykémie z glukoměru' - ,de: 'Blutzucker vom Messgerät' - ,es: 'Glucemia del glucómetro' - ,fr: 'Glycémie de glucomètre' - ,pt: 'Glicemia do glicosímetro' - ,sv: 'BS från blodsockermätare' - ,ro: 'Glicemie în glucometru' - ,bg: 'КЗ от глюкомер' - ,hr: 'GUK s glukometra' - ,it: 'Glicemie da glucometro' - ,dk: 'BS fra blodsukkerapperat' - ,fi: 'VS mittarilta' - ,nb: 'BS fra blodsukkerapparat' - ,pl: 'Wartość BG z glukometru' - } - ,'Manual BG' : { - cs: 'Ručně zadaná glykémie' - ,de: 'Blutzucker händisch' - ,es: 'Glucemia manual' - ,fr: 'Glycémie manuelle' - ,pt: 'Glicemia Manual' - ,ro: 'Glicemie manuală' - ,bg: 'Ръчно въведена КЗ' - ,hr: 'Ručno unesen GUK' - ,sv: 'Manuellt BS' - ,it: 'Inserisci Glicemia' - ,dk: 'Manuelt BS' - ,fi: 'Käsin syötetty VS' - ,nb: 'Manuelt BS' - ,pl: 'Ręczne wprowadzenie BG' - } - ,'Quickpick' : { - cs: 'Rychlý výběr' - ,de: 'Schnellauswahl' - ,es: 'Selección rápida' - ,fr: 'Sélection rapide' - ,pt: 'Seleção rápida' - ,ro: 'Selecție rapidă' - ,bg: 'Бърз избор' - ,hr: 'Brzi izbor' - ,sv: 'Snabbval' - ,it: 'Scelta rapida' - ,dk: 'Hurtig snack' - ,fi: 'Pikavalinta' - ,nb: 'Hurtigvalg' - ,pl: 'Szybki wybór' - } - ,'or' : { - cs: 'nebo' - ,de: 'oder' - ,es: 'o' - ,fr: 'ou' - ,pt: 'or' - ,sv: 'Eller' - ,ro: 'sau' - ,bg: 'или' - ,hr: 'ili' - ,it: 'o' - ,dk: 'eller' - ,fi: 'tai' - ,nb: 'eller' - ,he: 'או' - ,pl: 'lub' - } - ,'Add from database' : { - cs: 'Přidat z databáze' - ,de: 'Ergänzt aus Datenbank' - ,es: 'Añadir desde la base de datos' - ,fr: 'Ajouter à partir de la base de données' - ,pt: 'Adicionar do banco de dados' - ,ro: 'Adaugă din baza de date' - ,bg: 'Добави от базата с данни' - ,hr: 'Dodaj iz baze podataka' - ,sv: 'Lägg till från databas' - ,it: 'Aggiungi dal database' - ,dk: 'Tilføj fra database' - ,fi: 'Lisää tietokannasta' - ,nb: 'Legg til fra database' - ,he: 'הוסף מבסיס נתונים' - ,pl: 'Dodaj z bazy danych' - } - ,'Use carbs correction in calculation' : { - cs: 'Použij korekci na sacharidy' - ,de: 'Verwende Kohlenhydrate-Korrektur zur Kalkulation' - ,es: 'Usar la corrección de hidratos de carbono en los cálculos' - ,fr: 'Utiliser la correction en glucides dans les calculs' - ,pt: 'Usar correção com carboidratos no cálculo' - ,ro: 'Folosește corecția de carbohidrați în calcule' - ,bg: 'Включи корекцията чрез ВХ в изчислението' - ,hr: 'Koristi korekciju za UH u izračunu' - ,sv: 'Använd kolhydratkorrektion för beräkning' - ,it: 'Utilizzare la correzione dei carboidrati nel calcolo' - ,dk: 'Benyt kulhydratkorrektion i beregning' - ,fi: 'Käytä hiilihydraattikorjausta laskennassa' - ,nb: 'Bruk karbohydratkorrigering i beregning' - ,pl: 'Użyj wartość węglowodanów w obliczeniach korekty' - } - ,'Use COB correction in calculation' : { - cs: 'Použij korekci na COB' - ,de: 'Verwende verzehrte Kohlenhydrate zur Kalkulation' - ,es: 'Usar la corrección de COB en los cálculos' - ,fr: 'Utiliser les COB dans les calculs' - ,pt: 'Usar correção de COB no cálculo' - ,ro: 'Folosește COB în calcule' - ,bg: 'Включи активните ВХ в изчислението' - ,hr: 'Koristi aktivne UH u izračunu' - ,sv: 'Använd aktiva kolhydrater för beräkning' - ,it: 'Utilizzare la correzione COB nel calcolo' - ,dk: 'Benyt aktive kulhydrater i beregning' - ,fi: 'Käytä aktiivisia hiilihydraatteja laskennassa' - ,nb: 'Benytt aktive karbohydrater i beregning' - ,pl: 'Użyj COB do obliczenia korekty' - } - ,'Use IOB in calculation' : { - cs: 'Použij IOB ve výpočtu' - ,de: 'Verwende gespritzes Insulin zur Kalkulation' - ,es: 'Usar la IOB en los cálculos' - ,fr: 'Utiliser l\'IOB dans les calculs' - ,pt: 'Usar IOB no cálculo' - ,ro: 'Folosește IOB în calcule' - ,bg: 'Включи активния инсулин в изчислението' - ,hr: 'Koristi aktivni inzulin u izračunu"' - ,sv: 'Använd aktivt insulin för beräkning' - ,it: 'Utilizzare la correzione IOB nel calcolo' - ,dk: 'Benyt aktivt insulin i beregningen' - ,fi: 'Käytä aktiviivista insuliinia laskennassa' - ,nb: 'Bruk aktivt insulin i beregningen' - ,pl: 'Użyj IOB w obliczeniach ' - } - ,'Other correction' : { - cs: 'Jiná korekce' - ,de: 'Weitere Korrektur' - ,es: 'Otra correción' - ,fr: 'Autre correction' - ,pt: 'Outra correção' - ,ro: 'Alte corecții' - ,bg: 'Друга корекция' - ,hr: 'Druga korekcija' - ,sv: 'Övrig korrektion' - ,it: 'Altre correzioni' - ,dk: 'Øvrig korrektion' - ,fi: 'Muu korjaus' - ,nb: 'Annen korrigering' - ,pl: 'Inna korekta' - } - ,'Rounding' : { - cs: 'Zaokrouhlení' - ,de: 'Gerundet' - ,es: 'Redondeo' - ,fr: 'Arrondi' - ,pt: 'Arredondamento' - ,sv: 'Avrundning' - ,ro: 'Rotunjire' - ,bg: 'Закръгляне' - ,hr: 'Zaokruživanje' - ,it: 'Arrotondamento' - ,dk: 'Afrunding' - ,fi: 'Pyöristys' - ,nb: 'Avrunding' - ,pl: 'Zaokrąglanie' + var translations = {}; + + language.translations = translations; + + language.offerTranslations = function offerTranslations (localization) { + translations = localization; + language.translations = translations; + } + + // case sensitive + language.translateCS = function translateCaseSensitive (text) { + if (translations[text]) { + return translations[text]; } - ,'Enter insulin correction in treatment' : { - cs: 'Zahrň inzulín do záznamu ošetření' - ,de: 'Insulin Korrektur zur Behandlung eingeben' - ,es: 'Introducir correción de insulina en tratamiento' - ,fr: 'Entrer correction insuline dans le traitement' - ,pt: 'Inserir correção de insulina no tratamento' - ,ro: 'Introdu corecția de insulină în tratament' - ,bg: 'Въведи корекция с инсулин като лечение' - ,hr: 'Unesi korekciju inzulinom u tretman' - ,sv: 'Ange insulinkorrektion för händelse' - ,it: 'Inserisci correzione insulina nella somministrazione' - ,dk: 'Indtast insulionkorrektion' - ,fi: 'Syötä insuliinikorjaus' - ,nb: 'Task inn insulinkorrigering' - ,pl: 'Wprowadź wartość korekty w zabiegach' - } - ,'Insulin needed' : { - cs: 'Potřebný inzulín' - ,de: 'Benötigtes Insulin' - ,es: 'Insulina necesaria' - ,fr: 'Insuline nécessaire' - ,pt: 'Insulina necessária' - ,ro: 'Necesar insulină' - ,bg: 'Необходим инсулин' - ,hr: 'Potrebno inzulina' - ,sv: 'Beräknad insulinmängd' - ,it: 'Insulina necessaria' - ,dk: 'Insulin påkrævet' - ,fi: 'Insuliinitarve' - ,nb: 'Insulin nødvendig' - ,he: 'אינסולין נדרש' - ,pl: 'Wymagana insulina' - } - ,'Carbs needed' : { - cs: 'Potřebné sach' - ,de: 'Benötigte Kohlenhydrate' - ,es: 'Hidratos de carbono necesarios' - ,fr: 'Glucides nécessaires' - ,pt: 'Carboidratos necessários' - ,ro: 'Necesar carbohidrați' - ,bg: 'Необходими въглехидрати' - ,hr: 'Potrebno UH' - ,sv: 'Beräknad kolhydratmängd' - ,it: 'Carboidrati necessari' - ,dk: 'Kulhydrater påkrævet' - ,fi: 'Hiilihydraattitarve' - ,nb: 'Karbohydrater nødvendig' - ,he: 'פחמימות נדרשות' - ,pl: 'Wymagane węglowodany' - } - ,'Carbs needed if Insulin total is negative value' : { - cs: 'Chybějící sacharidy v případě, že výsledek je záporný' - ,de: 'Benötigte Kohlenhydrate sofern Gesamtinsulin einen negativen Wert aufweist' - ,es: 'Hidratos de carbono necesarios si el total de insulina es un valor negativo' - ,fr: 'Glucides nécessaires si insuline totale est un valeur négative' - ,pt: 'Carboidratos necessários se Insulina total for negativa' - ,ro: 'Carbohidrați când necesarul de insulină este negativ' - ,bg: 'Необходими въглехидрати, ако няма инсулин' - ,hr: 'Potrebno UH ako je ukupna vrijednost inzulina negativna' - ,sv: 'Nödvändig kolhydratmängd för angiven insulinmängd' - ,it: 'Carboidrati necessari se l\'insulina totale è un valore negativo' - ,dk: 'Kulhydrater er nødvendige når total insulin mængde er negativ' - ,fi: 'Hiilihydraattitarve, jos yhteenlaskettu insuliini on negatiivinen' - ,nb: 'Karbohydrater er nødvendige når total insulinmengde er negativ' - ,pl: 'Wymagane węglowodany, jeśli łączna wartość Insuliny jest ujemna' - } - ,'Basal rate' : { - cs: 'Bazál' - ,de: 'Basalrate' - ,es: 'Tasa basal' - ,fr: 'Taux basal' - ,pt: 'Taxa basal' - ,ro: 'Rata bazală' - ,bg: 'Базален инсулин' - ,hr: 'Bazal' - ,sv: 'Basaldos' - ,it: 'Velocità basale' - ,dk: 'Basal rate' - ,fi: 'Perusannos' - ,nb: 'Basal' - ,pl: 'Bolus' - } - ,'60 minutes earlier' : { - cs: '60 min předem' - ,de: '60 Min. früher' - ,es: '60 min antes' - ,fr: '60 min avant' - ,pt: '60 min antes' - ,sv: '60 min tidigare' - ,ro: 'acum 60 min' - ,bg: 'Преди 60 минути' - ,hr: 'Prije 60 minuta' - ,it: '60 minuti prima' - ,dk: '60 min tidligere' - ,fi: '60 minuuttia aiemmin' - ,nb: '60 min tidligere' - ,pl: '60 minut wcześciej' - } - ,'45 minutes earlier' : { - cs: '45 min předem' - ,de: '45 Min. früher' - ,es: '45 min antes' - ,fr: '45 min avant' - ,pt: '45 min antes' - ,sv: '45 min tidigare' - ,ro: 'acum 45 min' - ,bg: 'Преди 45 минути' - ,hr: 'Prije 45 minuta' - ,it: '45 minuti prima' - ,dk: '45 min tidligere' - ,fi: '45 minuuttia aiemmin' - ,nb: '45 min tidligere' - ,pl: '45 minut wcześciej' - } - ,'30 minutes earlier' : { - cs: '30 min předem' - ,de: '30 Min früher' - ,es: '30 min antes' - ,fr: '30 min avant' - ,pt: '30 min antes' - ,sv: '30 min tidigare' - ,ro: 'acum 30 min' - ,bg: 'Преди 30 минути' - ,hr: 'Prije 30 minuta' - ,it: '30 minuti prima' - ,dk: '30 min tidigere' - ,fi: '30 minuuttia aiemmin' - ,nb: '30 min tidigere' - ,pl: '30 minut wcześciej' - } - ,'20 minutes earlier' : { - cs: '20 min předem' - ,de: '20 Min. früher' - ,es: '20 min antes' - ,fr: '20 min avant' - ,pt: '20 min antes' - ,sv: '20 min tidigare' - ,ro: 'acum 20 min' - ,bg: 'Преди 20 минути' - ,hr: 'Prije 20 minuta' - ,it: '20 minuti prima' - ,dk: '20 min tidligere' - ,fi: '20 minuuttia aiemmin' - ,nb: '20 min tidligere' - ,pl: '20 minut wcześciej' - } - ,'15 minutes earlier' : { - cs: '15 min předem' - ,de: '15 Min. früher' - ,es: '15 min antes' - ,fr: '15 min avant' - ,pt: '15 min antes' - ,sv: '15 min tidigare' - ,ro: 'acum 15 min' - ,bg: 'Преди 15 минути' - ,hr: 'Prije 15 minuta' - ,it: '15 minuti prima' - ,dk: '15 min tidligere' - ,fi: '15 minuuttia aiemmin' - ,nb: '15 min tidligere' - ,pl: '15 minut wcześciej' - } - ,'Time in minutes' : { - cs: 'Čas v minutách' - ,de: 'Zeit in Minuten' - ,es: 'Tiempo en minutos' - ,fr: 'Durée en minutes' - ,pt: 'Tempo em minutos' - ,sv: 'Tid i minuter' - ,ro: 'Timp în minute' - ,bg: 'Времето в минути' - ,hr: 'Vrijeme u minutama' - ,it: 'Tempo in minuti' - ,dk: 'Tid i minutter' - ,fi: 'Aika minuuteissa' - ,nb: 'Tid i minutter' - ,he: 'זמן בדקות' - ,pl: 'Czas w minutach' - } - ,'15 minutes later' : { - cs: '15 min po' - ,de: '15 Min. später' - ,es: '15 min más tarde' - ,fr: '15 min après' - ,pt: '15 min depois' - ,ro: 'după 15 min' - ,bg: 'След 15 минути' - ,hr: '15 minuta kasnije' - ,sv: '15 min senare' - ,it: '15 minuti più tardi' - ,dk: '15 min senere' - ,fi: '15 minuuttia myöhemmin' - ,nb: '15 min senere' - ,he: 'רבע שעה מאוחר יותר' - ,pl: '15 minut później' - } - ,'20 minutes later' : { - cs: '20 min po' - ,de: '20 Min. später' - ,es: '20 min más tarde' - ,fr: '20 min après' - ,pt: '20 min depois' - ,ro: 'după 20 min' - ,bg: 'След 20 минути' - ,hr: '20 minuta kasnije' - ,sv: '20 min senare' - ,it: '20 minuti più tardi' - ,dk: '20 min senere' - ,fi: '20 minuuttia myöhemmin' - ,nb: '20 min senere' - ,he: 'עשרים דקות מאוחר יותר' - ,pl: '20 minut później' - } - ,'30 minutes later' : { - cs: '30 min po' - ,de: '30 Min. später' - ,es: '30 min más tarde' - ,fr: '30 min après' - ,pt: '30 min depois' - ,ro: 'după 30 min' - ,bg: 'След 30 минути' - ,hr: '30 minuta kasnije' - ,sv: '30 min senare' - ,it: '30 minuti più tardi' - ,dk: '30 min senere' - ,fi: '30 minuuttia myöhemmin' - ,nb: '30 min senere' - ,he: 'חצי שעה מאוחר יותר' - ,pl: '30 minut później' - } - ,'45 minutes later' : { - cs: '45 min po' - ,de: '45 Min. später' - ,es: '45 min más tarde' - ,fr: '45 min après' - ,pt: '45 min depois' - ,ro: 'după 45 min' - ,bg: 'След 45 минути' - ,hr: '45 minuta kasnije' - ,sv: '45 min senare' - ,it: '45 minuti più tardi' - ,dk: '45 min senere' - ,fi: '45 minuuttia myöhemmin' - ,nb: '45 min senere' - ,he: 'שלושת רבעי שעה מאוחר יותר' - ,pl: '45 minut później' - } - ,'60 minutes later' : { - cs: '60 min po' - ,de: '60 Min. später' - ,es: '60 min más tarde' - ,fr: '60 min après' - ,pt: '60 min depois' - ,ro: 'după 60 min' - ,bg: 'След 60 минути' - ,hr: '60 minuta kasnije' - ,sv: '60 min senare' - ,it: '60 minuti più tardi' - ,dk: '60 min senere' - ,fi: '60 minuuttia myöhemmin' - ,nb: '60 min senere' - ,he: 'שעה מאוחר יותר' - ,pl: '60 minut później' - } - ,'Additional Notes, Comments' : { - cs: 'Dalši poznámky, komentáře' - ,de: 'Ergänzende Hinweise / Kommentare' - ,es: 'Notas adicionales, Comentarios' - ,fr: 'Notes additionnelles, commentaires' - ,pt: 'Notas adicionais e comentários' - ,ro: 'Note adiționale, comentarii' - ,bg: 'Допълнителни бележки, коментари' - ,hr: 'Dodatne bilješke, komentari' - ,sv: 'Notering, övrigt' - ,it: 'Note aggiuntive, commenti' - ,dk: 'Ekstra noter, kommentarer' - ,fi: 'Lisähuomiot, kommentit' - ,nb: 'Ekstra notater, kommentarer' - ,he: 'הערות נוספות' - ,pl: 'Uwagi dodatkowe' - } - ,'RETRO MODE' : { - cs: 'V MINULOSTI' - ,de: 'RETRO MODUS' - ,es: 'Modo Retrospectivo' - ,fr: 'MODE RETROSPECTIF' - ,pt: 'Modo Retrospectivo' - ,sv: 'Retroläge' - ,ro: 'MOD RETROSPECTIV' - ,bg: 'МИНАЛО ВРЕМЕ' - ,hr: 'Retrospektivni način' - ,it: 'Modalità retrospettiva' - ,dk: 'Retro mode' - ,fi: 'VANHENTUNEET TIEDOT' - ,nb: 'Retro mode' - ,pl: 'Tryb RETRO' - } - ,'Now' : { - cs: 'Nyní' - ,de: 'Jetzt' - ,es: 'Ahora' - ,fr: 'Maintenant' - ,pt: 'Agora' - ,sv: 'Nu' - ,ro: 'Acum' - ,bg: 'Сега' - ,hr: 'Sad' - ,it: 'Ora' - ,dk: 'Nu' - ,fi: 'Nyt' - ,nb: 'Nå' - ,he: 'עכשיו' - ,pl: 'Teraz' - } - ,'Other' : { - cs: 'Jiný' - ,de: 'Weitere' - ,es: 'Otro' - ,fr: 'Autre' - ,pt: 'Outro' - ,sv: 'Övrigt' - ,ro: 'Altul' - ,bg: 'Друго' - ,hr: 'Drugo' - ,it: 'Altro' - ,dk: 'Øvrige' - ,fi: 'Muu' - ,nb: 'Annet' - ,he: 'אחר' - ,pl: 'Inny' - } - ,'Submit Form' : { - cs: 'Odeslat formulář' - ,de: 'Eingabe senden' - ,es: 'Enviar formulario' - ,fr: 'Formulaire de soumission' - ,pt: 'Enviar formulário' - ,sv: 'Överför händelse' - ,ro: 'Trimite formularul' - ,bg: 'Въвеждане на данните' - ,hr: 'Predaj obrazac' - ,it: 'Invia il modulo' - ,dk: 'Gem hændelsen' - ,fi: 'Lähetä tiedot' - ,nb: 'Lagre' - ,he: 'שמור' - ,pl: 'Zatwierdz' - } - ,'Profile Editor' : { - cs: 'Editor profilu' - ,de: 'Profil-Einstellungen' - ,es: 'Editor de perfil' - ,fr: 'Editeur de profil' - ,pt: 'Editor de perfil' - ,sv: 'Editera profil' - ,ro: 'Editare profil' - ,bg: 'Редактор на профила' - ,hr: 'Editor profila' - ,it: 'NS - Dati personali' - ,dk: 'Profil editor' - ,fi: 'Profiilin muokkaus' - ,nb: 'Profileditor' - ,he: 'ערוך פרופיל' - ,pl: 'Edytor profilu' - } - ,'Reports' : { - cs: 'Výkazy' - ,de: 'Berichte' - ,es: 'Herramienta de informes' - ,fr: 'Outil de rapport' - ,pt: 'Relatórios' - ,sv: 'Rapportverktyg' - ,ro: 'Instrument de rapoarte' - ,bg: 'Статистика' - ,hr: 'Alat za prijavu' - ,it: 'NS - Statistiche' - ,dk: 'Rapporteringsværktøj' - ,fi: 'Raportointityökalu' - ,nb: 'Rapporteringsverktøy' - ,pl: 'Raporty' - } - ,'Add food from your database' : { - cs: 'Přidat jidlo z Vaší databáze' - ,de: 'Ergänzt durch deine Datenbank' - ,es: 'Añadir alimento a su base de datos' - ,fr: 'Ajouter aliment de votre base de données' - ,pt: 'Incluir alimento do seu banco de dados' - ,ro: 'Adaugă alimente din baza de date' - ,bg: 'Добави храна от твоята база с данни' - ,hr: 'Dodajte hranu iz svoje baze podataka' - ,sv: 'Lägg till livsmedel från databas' - ,it: 'Aggiungere cibo al database' - ,dk: 'Tilføj mad fra din database' - ,fi: 'Lisää ruoka tietokannasta' - ,nb: 'Legg til mat fra din database' - ,pl: 'Dodaj posiłek z twojej bazy danych' - } - ,'Reload database' : { - cs: 'Znovu nahraj databázi' - ,de: 'Datenbank nachladen' - ,es: 'Recargar base de datos' - ,fr: 'Recharger la base de données' - ,pt: 'Recarregar banco de dados' - ,ro: 'Reîncarcă baza de date' - ,bg: 'Презареди базата с данни' - ,hr: 'Ponovo učitajte bazu podataka' - ,sv: 'Ladda om databas' - ,it: 'Ricarica database' - ,dk: 'Genindlæs databasen' - ,fi: 'Lataa tietokanta uudelleen' - ,nb: 'Last inn databasen på nytt' - ,pl: 'Odśwież bazę danych' - } - ,'Add' : { - cs: 'Přidej' - ,de: 'Hinzufügen' - ,es: 'Añadir' - ,fr: 'Ajouter' - ,pt: 'Adicionar' - ,ro: 'Adaugă' - ,bg: 'Добави' - ,hr: 'Dodaj' - ,sv: 'Lägg till' - ,it: 'Aggiungere' - ,dk: 'Tilføj' - ,fi: 'Lisää' - ,nb: 'Legg til' - ,pl: 'Dodaj' - } - ,'Unauthorized' : { - cs: 'Neautorizováno' - ,de: 'Unbestätigt' - ,es: 'No autorizado' - ,fr: 'Non autorisé' - ,pt: 'Não autorizado' - ,sv: 'Ej behörig' - ,ro: 'Neautorizat' - ,bg: 'Неразрешен достъп' - ,hr: 'Neautorizirano' - ,it: 'Non Autorizzato' - ,dk: 'Uautoriseret' - ,fi: 'Kielletty' - ,nb: 'Uautorisert' - ,pl: 'Nieuwierzytelniono' - } - ,'Entering record failed' : { - cs: 'Vložení záznamu selhalo' - ,de: 'Eingabe Datensatz fehlerhaft' - ,es: 'Entrada de registro fallida' - ,fr: 'Entrée enregistrement a échoué' - ,pt: 'Entrada de registro falhou' - ,ro: 'Înregistrare eșuată' - ,bg: 'Въвеждане на записа не се осъществи' - ,hr: 'Neuspjeli unos podataka' - ,sv: 'Lägga till post nekas' - ,it: 'Voce del Registro fallita' - ,dk: 'Tilføjelse af post fejlede' - ,fi: 'Tiedon tallentaminen epäonnistui' - ,nb: 'Lagring feilet' - ,pl: 'Wprowadzenie rekordu nie udało się' - } - ,'Device authenticated' : { - cs: 'Zařízení ověřeno' - ,de: 'Gerät authentifiziert' - ,es: 'Dispositivo autenticado' - ,fr: 'Appareil authentifié' - ,pt: 'Dispositivo autenticado' - ,sv: 'Enhet autentiserad' - ,ro: 'Dispozitiv autentificat' - ,bg: 'Устройстово е разпознато' - ,hr: 'Uređaj autenticiran' - ,it: 'Dispositivo autenticato' - ,dk: 'Enhed godkendt' - ,fi: 'Laite autentikoitu' - ,nb: 'Enhet godkjent' - ,pl: 'Urządzenie uwierzytelnione' - } - ,'Device not authenticated' : { - cs: 'Zařízení není ověřeno' - ,de: 'Gerät nicht authentifiziert' - ,es: 'Dispositivo no autenticado' - ,fr: 'Appareil non authentifié' - ,pt: 'Dispositivo não autenticado' - ,sv: 'Enhet EJ autentiserad' - ,ro: 'Dispozitiv neautentificat' - ,bg: 'Устройсройството не е разпознато' - ,hr: 'Uređaj nije autenticiran' - ,it: 'Dispositivo non autenticato' - ,dk: 'Enhed ikke godkendt' - ,fi: 'Laite ei ole autentikoitu' - ,nb: 'Enhet ikke godkjent' - ,pl: 'Urządzenie nieuwierzytelnione' - } - ,'Authentication status' : { - cs: 'Stav ověření' - ,de: 'Authentifications Status' - ,es: 'Estado de autenticación' - ,fr: 'Status de l\'authentification' - ,pt: 'Status de autenticação' - ,ro: 'Starea autentificării' - ,bg: 'Статус на удостоверяване' - ,hr: 'Status autentikacije' - ,sv: 'Autentiseringsstatus' - ,it: 'Stato di autenticazione' - ,dk: 'Autentifikationsstatus' - ,fi: 'Autentikoinnin tila' - ,nb: 'Autentiseringsstatus' - ,pl: 'Stan uwierzytelnienia' - } - ,'Authenticate' : { - cs: 'Ověřit' - ,de: 'Authentifizieren' - ,es: 'Autenticar' - ,fr: 'Authentifier' - ,pt: 'Autenticar' - ,sv: 'Autentiserar' - ,ro: 'Autentificare' - ,bg: 'Удостоверяване' - ,hr: 'Autenticirati' - ,it: 'Autenticare' - ,dk: 'Godkende' - ,fi: 'Autentikoi' - ,nb: 'Autentiser' - ,pl: 'Uwierzytelnienia' - } - ,'Remove' : { - cs: 'Vymazat' - ,de: 'Entfernen' - ,es: 'Eliminar' - ,fr: 'Retirer' - ,pt: 'Remover' - ,ro: 'Șterge' - ,bg: 'Премахни' - ,hr: 'Ukloniti' - ,sv: 'Ta bort' - ,it: 'Rimuovere' - ,dk: 'Fjern' - ,fi: 'Poista' - ,nb: 'Slett' - ,pl: 'Usuń' - } - ,'Your device is not authenticated yet' : { - cs: 'Toto zařízení nebylo dosud ověřeno' - ,de: 'Dein Gerät ist derzeit nicht authentifiziert' - ,es: 'Su dispositivo no ha sido autenticado todavía' - ,fr: 'Votre appareil n\'est pas encore authentifié' - ,pt: 'Seu dispositivo ainda não foi autenticado' - ,ro: 'Dispozitivul nu este autentificat încă' - ,bg: 'Вашето устройство все още не е удостоверено' - ,hr: 'Vaš uređaj još nije autenticiran' - ,sv: 'Din enhet är ej autentiserad' - ,it: 'Il tuo dispositivo non è ancora stato autenticato' - ,dk: 'Din enhed er ikke godkendt endnu' - ,fi: 'Laitettasi ei ole vielä autentikoitu' - ,nb: 'Din enhet er ikke godkjent enda' - ,pl: 'Twoje urządzenie nie jest jeszcze uwierzytelnione' - } - ,'Sensor' : { - cs: 'Senzor' - ,de: 'Sensor' - ,es: 'Sensor' - ,fr: 'Senseur' - ,pt: 'Sensor' - ,sv: 'Sensor' - ,ro: 'Senzor' - ,bg: 'Сензор' - ,hr: 'Senzor' - ,it: 'Sensore' - ,dk: 'Sensor' - ,fi: 'Sensori' - ,nb: 'Sensor' - ,he: 'חיישן סוכר' - ,pl: 'Sensor' - } - ,'Finger' : { - cs: 'Glukoměr' - ,de: 'Finger' - ,es: 'Dedo' - ,fr: 'Doigt' - ,pt: 'Ponta de dedo' - ,sv: 'Finger' - ,ro: 'Deget' - ,bg: 'От пръстта' - ,hr: 'Prst' - ,it: 'Dito' - ,dk: 'Finger' - ,fi: 'Sormi' - ,nb: 'Finger' - ,he: 'אצבע' - ,pl: 'Glukometr' - } - ,'Manual' : { - cs: 'Ručně' - ,de: 'Händisch' - ,es: 'Manual' - ,fr: 'Manuel' - ,pt: 'Manual' - ,sv: 'Manuell' - ,ro: 'Manual' - ,bg: 'Ръчно' - ,hr: 'Ručno' - ,it: 'Manuale' - ,dk: 'Manuel' - ,fi: 'Manuaalinen' - ,nb: 'Manuell' - ,he: 'ידני' - ,pl: 'Ręcznie' - } - ,'Scale' : { - cs: 'Měřítko' - ,de: 'Messen' - ,es: 'Escala' - ,fr: 'Echelle' - ,pt: 'Escala' - ,ro: 'Scală' - ,bg: 'Скала' - ,hr: 'Skala' - ,sv: 'Skala' - ,it: 'Scala' - ,dk: 'Skala' - ,fi: 'Skaala' - ,nb: 'Skala' - ,pl: 'Skala' - - } - ,'Linear' : { - cs: 'Lineární' - ,de: 'Linear' - ,es: 'Lineal' - ,fr: 'Linéaire' - ,pt: 'Linear' - ,sv: 'Linjär' - ,ro: 'Liniar' - ,bg: 'Линеен' - ,hr: 'Linearno' - ,it: 'Lineare' - ,dk: 'Lineær' - ,fi: 'Lineaarinen' - ,nb: 'Lineær' - ,he: 'לינארי' - ,pl: 'Liniowa' - } - ,'Logarithmic' : { - cs: 'Logaritmické' - ,de: 'Logarithmisch' - ,es: 'Logarítmica' - ,fr: 'Logarithmique' - ,pt: 'Logarítmica' - ,sv: 'Logaritmisk' - ,ro: 'Logaritmic' - ,bg: 'Логоритмичен' - ,hr: 'Logaritamski' - ,it: 'Logaritmica' - ,dk: 'Logaritmisk' - ,fi: 'Logaritminen' - ,nb: 'Logaritmisk' - ,he: 'לוגריתמי' - ,pl: 'Logarytmiczna' - } - ,'Logarithmic (Dynamic)' : { - cs: 'Logaritmické (Dynamické)' - ,it: 'Logaritmica (Dinamica)' - } - ,'Silence for 30 minutes' : { - cs: 'Ztlumit na 30 minut' - ,de: 'Ausschalten für 30 Minuten' - ,es: 'Silenciar durante 30 minutos' - ,fr: 'Silence pendant 30 minutes' - ,pt: 'Silenciar por 30 minutos' - ,ro: 'Ignoră pentru 30 minute' - ,bg: 'Заглуши за 30 минути' - ,hr: 'Tišina 30 minuta' - ,sv: 'Tyst i 30 min' - ,it: 'Silenzio per 30 minuti' - ,dk: 'Stilhed i 30 min' - ,fi: 'Hiljennä 30 minuutiksi' - ,nb: 'Stille i 30 min' - ,pl: 'Wycisz na 30 minut' - } - ,'Silence for 60 minutes' : { - cs: 'Ztlumit na 60 minut' - ,de: 'Ausschalten für 60 Minuten' - ,es: 'Silenciar durante 60 minutos' - ,fr: 'Silence pendant 60 minutes' - ,pt: 'Silenciar por 60 minutos' - ,ro: 'Ignoră pentru 60 minute' - ,bg: 'Заглуши за 60 минути' - ,hr: 'Tišina 60 minuta' - ,sv: 'Tyst i 60 min' - ,it: 'Silenzio per 60 minuti' - ,dk: 'Stilhed i 60 min' - ,fi: 'Hiljennä tunniksi' - ,nb: 'Stille i 60 min' - ,pl: 'Wycisz na 60 minut' - } - ,'Silence for 90 minutes' : { - cs: 'Ztlumit na 90 minut' - ,de: 'Ausschalten für 90 Minuten' - ,es: 'Silenciar durante 90 minutos' - ,fr: 'Silence pendant 90 minutes' - ,pt: 'Silenciar por 90 minutos' - ,ro: 'Ignoră pentru 90 minure' - ,bg: 'Заглуши за 90 минути' - ,hr: 'Tišina 90 minuta' - ,sv: 'Tyst i 90 min' - ,it: 'Silenzio per 90 minuti' - ,dk: 'Stilhed i 90 min' - ,fi: 'Hiljennä 1.5 tunniksi' - ,nb: 'Stille i 90 min' - ,pl: 'Wycisz na 90 minut' - } - ,'Silence for 120 minutes' : { - cs: 'Ztlumit na 120 minut' - ,de: 'Ausschalten für 120 Minuten' - ,es: 'Silenciar durante 120 minutos' - ,fr: 'Silence pendant 120 minutes' - ,pt: 'Silenciar por 120 minutos' - ,ro: 'Ignoră pentru 120 minute' - ,bg: 'Заглуши за 120 минути' - ,hr: 'Tišina 120 minuta' - ,sv: 'Tyst i 120 min' - ,it: 'Silenzio per 120 minuti' - ,dk: 'Stilhed i 120 min' - ,fi: 'Hiljennä 2 tunniksi' - ,nb: 'Stile i 120 min' - ,pl: 'Wycisz na 120 minut' - } - ,'3HR' : { - cs: '3hod' - ,de: '3h' - ,es: '3h' - ,fr: '3hr' - ,pt: '3h' - ,sv: '3tim' - ,ro: '3h' - ,bg: '3часа' - ,hr: '3h' - ,it: '3ORE' - ,dk: '3tim' - ,fi: '3h' - ,nb: '3t' - ,pl: '3h' - } - ,'6HR' : { - cs: '6hod' - ,de: '6h' - ,es: '6h' - ,fr: '6hr' - ,pt: '6h' - ,sv: '6tim' - ,ro: '6h' - ,bg: '6часа' - ,hr: '6h' - ,it: '6ORE' - ,dk: '6tim' - ,fi: '6h' - ,nb: '6t' - ,pl: '6h' - } - ,'12HR' : { - cs: '12hod' - ,de: '12h' - ,es: '12h' - ,fr: '12hr' - ,pt: '12h' - ,sv: '12tim' - ,ro: '12h' - ,bg: '12часа' - ,hr: '12h' - ,it: '12ORE' - ,dk: '12tim' - ,fi: '12h' - ,nb: '12t' - ,pl: '12h' - } - ,'24HR' : { - cs: '24hod' - ,de: '24h' - ,es: '24h' - ,fr: '24hr' - ,pt: '24h' - ,sv: '24tim' - ,ro: '24h' - ,bg: '24часа' - ,hr: '24h' - ,it: '24ORE' - ,dk: '24tim' - ,fi: '24h' - ,nb: '24t' - ,pl: '24h' - } - ,'Settings' : { - cs: 'Nastavení' - ,de: 'Einstellungen' - ,es: 'Ajustes' - ,fr: 'Paramètres' - ,pt: 'Ajustes' - ,sv: 'Inställningar' - ,ro: 'Setări' - ,bg: 'Настройки' - ,hr: 'Postavke' - ,it: 'Impostazioni' - ,dk: 'Opsætning' - ,fi: 'Asetukset' - ,nb: 'Innstillinger' - ,pl: 'Ustawienia' - } - ,'Units' : { - cs: 'Jednotky' - ,de: 'Einheiten' - ,es: 'Unidades' - ,fr: 'Unités' - ,pt: 'Unidades' - ,ro: 'Unități' - ,bg: 'Единици' - ,hr: 'Jedinice' - ,sv: 'Enheter' - ,it: 'Unità' - ,dk: 'Enheder' - ,fi: 'Yksikköä' - ,nb: 'Enheter' - ,pl: 'Jednostki' - } - ,'Date format' : { - cs: 'Formát datumu' - ,de: 'Datum Format' - ,es: 'Formato de fecha' - ,fr: 'Format Date' - ,pt: 'Formato de data' - ,sv: 'Datumformat' - ,ro: 'Formatul datei' - ,bg: 'Формат на датата' - ,hr: 'Format datuma' - ,it: 'Formato data' - ,dk: 'dato format' - ,fi: 'Aikamuoto' - ,nb: 'Datoformat' - ,pl: 'Format daty' - } - ,'12 hours' : { - cs: '12 hodin' - ,de: '12 Stunden' - ,es: '12 horas' - ,fr: '12hr' - ,pt: '12 horas' - ,sv: '12-timmars' - ,ro: '12 ore' - ,bg: '12 часа' - ,hr: '12 sati' - ,it: '12 ore' - ,dk: '12 timer' - ,fi: '12 tuntia' - ,nb: '12 timer' - ,pl: '12 godzinny' - } - ,'24 hours' : { - cs: '24 hodin' - ,de: '24 Stunden' - ,es: '24 horas' - ,fr: '24hr' - ,pt: '24 horas' - ,sv: '24-timmars' - ,ro: '24 ore' - ,bg: '24 часа' - ,hr: '24 sata' - ,it: '24 ore' - ,dk: '24 timer' - ,fi: '24 tuntia' - ,nb: '24 timer' - ,pl: '24 godzinny' - } - ,'Log a Treatment' : { - cs: 'Záznam ošetření' - ,de: 'Dateneingabe' - ,es: 'Apuntar un tratamiento' - ,fr: 'Entrer un traitement' - ,pt: 'Entre um procedimento' - ,ro: 'Înregistrează un eveniment' - ,bg: 'Въвеждане на събитие' - ,hr: 'Evidencija tretmana' - ,sv: 'Ange händelse' - ,it: 'Somministrazione' - ,dk: 'Log en hændelse' - ,fi: 'Tallenna tapahtuma' - ,nb: 'Logg en hendelse' - ,he: 'הזן רשומה' - ,pl: 'Archiwizuj zabiegi' - } - ,'BG Check' : { - cs: 'Kontrola glykémie' - ,de: 'Blutglukose-Prüfung' - ,es: 'Control de glucemia' - ,fr: 'Contrôle glycémie' - ,pt: 'Medida de glicemia' - ,sv: 'Blodsockerkontroll' - ,ro: 'Verificare glicemie' - ,bg: 'Проверка на КЗ' - ,hr: 'Kontrola GUK-a' - ,it: 'Controllo Glicemia' - ,dk: 'BS kontrol' - ,fi: 'Verensokerin tarkistus' - ,nb: 'Blodsukkerkontroll' - ,he: 'בדיקת סוכר' - ,pl: 'Pomiar glikemii' - } - ,'Meal Bolus' : { - cs: 'Bolus na jídlo' - ,de: 'Mahlzeiten Bolus' - ,es: 'Bolo de comida' - ,fr: 'Bolus repas' - ,pt: 'Bolus de refeição' - ,ro: 'Bolus masă' - ,bg: 'Болус-основно хранене' - ,hr: 'Bolus za obrok' - ,sv: 'Måltidsbolus' - ,it: 'Bolo Pasto' - ,dk: 'Måltidsbolus' - ,fi: 'Ruokailubolus' - ,nb: 'Måltidsbolus' - ,he: 'בולוס ארוחה' - ,pl: 'Bolus posiłkowy' - } - ,'Snack Bolus' : { - cs: 'Bolus na svačinu' - ,de: 'Snack Bolus' - ,es: 'Bolo de aperitivo' - ,fr: 'Bolus friandise' - ,pt: 'Bolus de lanche' - ,sv: 'Mellanmålsbolus' - ,ro: 'Bolus gustare' - ,bg: 'Болус-лека закуска' - ,hr: 'Bolus za užinu' - ,it: 'Bolo Merenda' - ,dk: 'Mellemmåltidsbolus' - ,fi: 'Ruokakorjaus' - ,nb: 'Mellommåltidsbolus' - ,he: 'בולוס ארוחת ביניים' - ,pl: 'Bolus przekąskowy' - } - ,'Correction Bolus' : { - cs: 'Bolus na glykémii' - ,de: 'Korrektur Bolus' - ,es: 'Bolo corrector' - ,fr: 'Bolus de correction' - ,pt: 'Bolus de correção' - ,ro: 'Bolus corecție' - ,bg: 'Болус корекция' - ,hr: 'Korekcija' - ,sv: 'Korrektionsbolus' - ,it: 'Bolo Correttivo' - ,dk: 'Korrektionsbolus' - ,fi: 'Korjausbolus' - ,nb: 'Korreksjonsbolus' - ,he: 'בולוס תיקון' - ,pl: 'Bolus korekcyjny' - } - ,'Carb Correction' : { - cs: 'Přídavek sacharidů' - ,de: 'Kohlenhydrate Korrektur' - ,es: 'Hidratos de carbono de corrección' - ,fr: 'Correction glucide' - ,pt: 'Correção com carboidrato' - ,ro: 'Corecție de carbohidrați' - ,bg: 'Корекция чрез въглехидрати' - ,hr: 'Bolus za hranu' - ,sv: 'Kolhydratskorrektion' - ,it: 'Carboidrati Correttivi' - ,dk: 'Kulhydratskorrektion' - ,fi: 'Hiilihydraattikorjaus' - ,nb: 'Karbohydratkorrigering' - ,he: 'בולוס פחמימות' - ,pl: 'Węglowodany korekcyjne' - } - ,'Note' : { - cs: 'Poznámka' - ,de: 'Bemerkungen' - ,es: 'Nota' - ,fr: 'Note' - ,pt: 'Nota' - ,ro: 'Notă' - ,sv: 'Notering' - ,bg: 'Бележка' - ,hr: 'Bilješka' - ,it: 'Nota' - ,dk: 'Note' - ,fi: 'Merkintä' - ,nb: 'Notat' - ,he: 'הערה' - ,pl: 'Uwagi' - } - ,'Question' : { - cs: 'Otázka' - ,de: 'Frage' - ,es: 'Pregunta' - ,fr: 'Question' - ,pt: 'Pergunta' - ,ro: 'Întrebare' - ,sv: 'Fråga' - ,bg: 'Въпрос' - ,hr: 'Pitanje' - ,it: 'Domanda' - ,dk: 'Spørgsmål' - ,fi: 'Kysymys' - ,nb: 'Spørsmål' - ,he: 'שאלה' - ,pl: 'Pytanie' - } - ,'Exercise' : { - cs: 'Cvičení' - ,de: 'Bewegung' - ,es: 'Ejercicio' - ,fr: 'Exercice' - ,pt: 'Exercício' - ,sv: 'Aktivitet' - ,ro: 'Activitate fizică' - ,bg: 'Спорт' - ,hr: 'Aktivnost' - ,it: 'Esercizio Fisico' - ,dk: 'Træning' - ,fi: 'Fyysinen harjoitus' - ,nb: 'Trening' - ,he: 'פעילות גופנית' - ,pl: 'Ćwieczenia' - } - ,'Pump Site Change' : { - cs: 'Výměna setu' - ,de: 'Pumpen-Katheter Wechsel' - ,es: 'Cambio de catéter' - ,fr: 'Changement de site pompe' - ,pt: 'Troca de catéter' - ,sv: 'Pump/nålbyte' - ,ro: 'Schimbare loc pompă' - ,bg: 'Смяна на сет' - ,hr: 'Promjena seta' - ,it: '(CAGE) Cambio Ago' - ,dk: 'Skift insulin infusionssted' - ,fi: 'Pumpun kanyylin vaihto' - ,nb: 'Pumpebytte' - ,he: 'החלפת צינורית משאבה' - ,pl: 'Zmiana wkłucia pompy' - } - ,'Sensor Start' : { - cs: 'Spuštění sensoru' - ,de: 'Sensor Start' - ,es: 'Inicio de sensor' - ,fr: 'Démarrage senseur' - ,pt: 'Início de sensor' - ,sv: 'Sensorstart' - ,ro: 'Start senzor' - ,bg: 'Стартиране на сензор' - ,hr: 'Start senzora' - ,it: 'Avvio sensore' - ,dk: 'Sensorstart' - ,fi: 'Sensorin aloitus' - ,nb: 'Sensorstart' - ,he: 'אתחול חיישן סוכר' - ,pl: 'Start nowego sensora' - } - ,'Sensor Change' : { - cs: 'Výměna sensoru' - ,de: 'Sensor Wechsel' - ,es: 'Cambio de sensor' - ,fr: 'Changement senseur' - ,pt: 'Troca de sensor' - ,sv: 'Sensorbyte' - ,ro: 'Schimbare senzor' - ,bg: 'Смяна на сензор' - ,hr: 'Promjena senzora' - ,it: 'Cambio sensore' - ,dk: 'Sensor ombytning' - ,fi: 'Sensorin vaihto' - ,nb: 'Sensorbytte' - ,he: 'החלפת חיישן סוכר' - ,pl: 'Zmiana sensora' - } - ,'Dexcom Sensor Start' : { - cs: 'Spuštění sensoru' - ,de: 'Dexcom Sensor Start' - ,es: 'Inicio de sensor Dexcom' - ,fr: 'Démarrage senseur Dexcom' - ,pt: 'Início de sensor' - ,sv: 'Dexcom sensorstart' - ,ro: 'Pornire senzor Dexcom' - ,bg: 'Стартиране на Декском сензор' - ,hr: 'Start Dexcom senzora' - ,it: 'Avvio sensore Dexcom' - ,dk: 'Dexcom sensor start' - ,fi: 'Sensorin aloitus' - ,nb: 'Dexcom sensor start' - ,he: 'אתחול חיישן סוכר של דקסקום' - ,pl: 'Start sensora DEXCOM' - } - ,'Dexcom Sensor Change' : { - cs: 'Výměna sensoru' - ,de: 'Dexcom Sensor Wechsel' - ,es: 'Cambio de sensor Dexcom' - ,fr: 'Changement senseur Dexcom' - ,pt: 'Troca de sensor' - ,sv: 'Dexcom sensorbyte' - ,ro: 'Schimbare senzor Dexcom' - ,bg: 'Смяна на Декском сензор' - ,hr: 'Promjena Dexcom senzora' - ,it: 'Cambio sensore Dexcom' - ,dk: 'Dexcom sensor ombytning' - ,fi: 'Sensorin vaihto' - ,nb: 'Dexcom sensor bytte' - ,he: 'החלפת חיישן סוכר של דקסקום' - ,pl: 'Zmiana sensora DEXCOM' - } - ,'Insulin Cartridge Change' : { - cs: 'Výměna inzulínu' - ,de: 'Insulin Ampullen Wechsel' - ,es: 'Cambio de reservorio de insulina' - ,fr: 'Changement cartouche d\'insuline' - ,pt: 'Troca de reservatório de insulina' - ,sv: 'Insulinreservoarbyte' - ,ro: 'Schimbare cartuș insulină' - ,bg: 'Смяна на резервоар' - ,hr: 'Promjena spremnika inzulina' - ,it: 'Cambio cartuccia insulina' - ,dk: 'Skift insulin beholder' - ,fi: 'Insuliinisäiliön vaihto' - ,nb: 'Skifte insulin beholder' - ,he: 'החלפת מחסנית אינסולין' - ,pl: 'Zmiana zbiornika z insuliną' - } - ,'D.A.D. Alert' : { - cs: 'D.A.D. Alert' - ,de: 'Diabetes-Hund Alarm' - ,es: 'Alerta de perro de alerta diabética' - ,fr: 'Wouf! Wouf! Chien d\'alerte diabète' - ,pt: 'Alerta de cão sentinela de diabetes' - ,sv: 'Diabeteshundlarm (Duktig vovve!)' - ,ro: 'Alertă câine de serviciu' - ,bg: 'Сигнал от обучено куче' - ,hr: 'Obavijest dijabetičkog psa' - ,it: 'Allarme D.A.D.(Diabete Alert Dog)' - ,dk: 'Vuf Vuf! (Diabeteshundealarm!)' - ,fi: 'Diabeteskoirahälytys' - ,nb: 'Diabeteshundalarm' - ,he: 'ווף! ווף! התראת גשג' - ,pl: 'Psi Alarm cukrzycowy' - } - ,'Glucose Reading' : { - cs: 'Hodnota glykémie' - ,de: 'Glukose Messwert' - ,es: 'Valor de glucemia' - ,fr: 'Valeur de glycémie' - ,pt: 'Valor de glicemia' - ,sv: 'Glukosvärde' - ,ro: 'Valoare glicemie' - ,bg: 'Кръвна захар' - ,hr: 'Vrijednost GUK-a' - ,it: 'Lettura glicemie' - ,dk: 'Glukose aflæsning' - ,fi: 'Verensokeri' - ,nb: 'Blodsukkermåling' - ,he: 'מדידת סוכר' - ,pl: 'Odczyt glikemii' - } - ,'Measurement Method' : { - cs: 'Metoda měření' - ,de: 'Messmethode' - ,es: 'Método de medida' - ,fr: 'Méthode de mesure' - ,pt: 'Método de medida' - ,sv: 'Mätmetod' - ,ro: 'Metodă măsurare' - ,bg: 'Метод на измерване' - ,hr: 'Metoda mjerenja' - ,it: 'Metodo di misurazione' - ,dk: 'Målemetode' - ,fi: 'Mittaustapa' - ,nb: 'Målemetode' - ,he: 'אמצעי מדידה' - ,pl: 'Sposób pomiaru' - } - ,'Meter' : { - cs: 'Glukoměr' - ,de: 'BZ-Messgerät' - ,fr: 'Glucomètre' - ,pt: 'Glicosímetro' - ,sv: 'Mätare' - ,ro: 'Glucometru' - ,es: 'Glucómetro' - ,bg: 'Глюкомер' - ,hr: 'Glukometar' - ,it: 'Glucometro' - ,dk: 'Glukosemeter' - ,fi: 'Sokerimittari' - ,nb: 'Måleapparat' - ,he: 'מד סוכר' - ,pl: 'Glukometr' - } - ,'Insulin Given' : { - cs: 'Inzulín' - ,de: 'Insulingabe' - ,es: 'Insulina' - ,fr: 'Insuline donnée' - ,pt: 'Insulina' - ,sv: 'Insulindos' - ,ro: 'Insulină administrată' - ,bg: 'Инсулин' - ,hr: 'Količina iznulina' - ,it: 'Insulina' - ,dk: 'Insulin dosis' - ,fi: 'Insuliiniannos' - ,nb: 'Insulin' - ,he: 'אינסולין שניתן' - ,pl: 'Podana insulina' - } - ,'Amount in grams' : { - cs: 'Množství v gramech' - ,de: 'Menge in Gramm' - ,es: 'Cantidad en gramos' - ,fr: 'Quantité en grammes' - ,pt: 'Quantidade em gramas' - ,sv: 'Antal gram' - ,ro: 'Cantitate în grame' - ,bg: 'К-во в грамове' - ,hr: 'Količina u gramima' - ,it: 'Quantità in grammi' - ,dk: 'Antal gram' - ,fi: 'Määrä grammoissa' - ,nb: 'Antall gram' - ,he: 'כמות בגרמים' - ,pl: 'Wartość w gramach' - } - ,'Amount in units' : { - cs: 'Množství v jednotkách' - ,de: 'Anzahl in Einheiten' - ,es: 'Cantidad en unidades' - ,fr: 'Quantité en unités' - ,pt: 'Quantidade em unidades' - ,ro: 'Cantitate în unități' - ,bg: 'К-во в единици' - ,hr: 'Količina u jedinicama' - ,sv: 'Antal enheter' - ,it: 'Quantità in unità' - ,dk: 'Antal enheder' - ,fi: 'Annos yksiköissä' - ,nb: 'Antall enheter' - ,he: 'כמות ביחידות' - ,pl: 'Wartość w jednostkach' - } - ,'View all treatments' : { - cs: 'Zobraz všechny ošetření' - ,de: 'Zeige alle Eingaben' - ,es: 'Visualizar todos los tratamientos' - ,fr: 'Voir tous les traitements' - ,pt: 'Visualizar todos os procedimentos' - ,sv: 'Visa behandlingar' - ,ro: 'Vezi toate evenimentele' - ,bg: 'Преглед на всички събития' - ,hr: 'Prikaži sve tretmane' - ,it: 'Visualizza tutti le somministrazioni' - ,dk: 'Vis behandlinger' - ,fi: 'Katso kaikki hoitotoimenpiteet' - ,nb: 'Vis behandlinger' - ,he: 'הצג את כל הטיפולים' - ,pl: 'Pokaż wszytkie zabiegi ' - } - ,'Enable Alarms' : { - cs: 'Povolit alarmy' - ,de: 'Alarm einschalten' - ,es: 'Activar las alarmas' - ,fr: 'Activer les alarmes' - ,pt: 'Ativar alarmes' - ,sv: 'Aktivera larm' - ,ro: 'Activează alarmele' - ,bg: 'Активни аларми' - ,hr: 'Aktiviraj alarme' - ,it: 'Attiva Allarme' - ,dk: 'Aktivere alarmer' - ,fi: 'Aktivoi hälytykset' - ,nb: 'Aktiver alarmer' - ,he: 'הפעל התראות' - ,pl: 'Włącz Alarmy' - } - ,'When enabled an alarm may sound.' : { - cs: 'Při povoleném alarmu zní zvuk' - ,de: 'Sofern eingeschaltet ertönt ein Alarm' - ,es: 'Cuando estén activas, una alarma podrá sonar' - ,fr: 'Si activée, un alarme peut sonner.' - ,pt: 'Quando ativado, um alarme poderá soar' - ,ro: 'Când este activ, poate suna o alarmă.' - ,sv: 'När markerad är ljudlarm aktivt' - ,bg: 'Когато е активирано, алармата ще има звук' - ,hr: 'Kad je aktiviran, alarm se može oglasiti' - ,it: 'Quando si attiva un allarme acustico.' - ,dk: 'Når aktiveret kan alarm lyde' - ,fi: 'Aktivointi mahdollistaa äänihälytykset' - ,nb: 'Når aktivert er alarmer aktive' - ,he: 'כשמופעל התראות יכולות להישמע.' - ,pl: 'Sygnalizacja dzwiękowa przy włączonym alarmie' - } - ,'Urgent High Alarm' : { - cs: 'Urgentní vysoká glykémie' - ,de: 'Achtung Hoch Alarm' - ,es: 'Alarma de glucemia alta urgente' - ,fr: 'Alarme haute urgente' - ,pt: 'URGENTE: Alarme de glicemia alta' - ,sv: 'Brådskande högt larmvärde' - ,ro: 'Alarmă urgentă hiper' - ,bg: 'Много висока КЗ' - ,hr: 'Hitni alarm za hiper' - ,it: 'Urgente:Glicemia Alta' - ,dk: 'Høj grænse overskredet' - ,fi: 'Kriittinen korkea' - ,nb: 'Kritisk høy alarm' - ,he: 'התראת גבוה דחופה' - ,pl: 'Uwaga: Wysoki poziom glikemi' - } - ,'High Alarm' : { - cs: 'Vysoká glykémie' - ,de: 'Hoch Alarm' - ,es: 'Alarma de glucemia alta' - ,fr: 'Alarme haute' - ,pt: 'Alarme de glicemia alta' - ,sv: 'Högt larmvärde' - ,ro: 'Alarmă hiper' - ,bg: 'Висока КЗ' - ,hr: 'Alarm za hiper' - ,it: 'Glicemia Alta' - ,dk: 'Høj grænse overskredet' - ,fi: 'Korkea verensokeri' - ,nb: 'Høy alarm' - ,he: 'התראת גבוה' - ,pl: 'Wysoki poziom glikemi' - } - ,'Low Alarm' : { - cs: 'Nízká glykémie' - ,de: 'Tief Alarm' - ,es: 'Alarma de glucemia baja' - ,fr: 'Alarme basse' - ,pt: 'Alarme de glicemia baixa' - ,sv: 'Lågt larmvärde' - ,ro: 'Alarmă hipo' - ,bg: 'Ниска КЗ' - ,hr: 'Alarm za hipo' - ,it: 'Glicemia bassa' - ,dk: 'Lav grænse overskredet' - ,fi: 'Matala verensokeri' - ,nb: 'Lav alarm' - ,he: 'התראת נמוך' - ,pl: 'Niski poziom glikemi' - } - ,'Urgent Low Alarm' : { - cs: 'Urgentní nízká glykémie' - ,de: 'Achtung Tief Alarm' - ,es: 'Alarma de glucemia baja urgente' - ,fr: 'Alarme basse urgente' - ,pt: 'URGENTE: Alarme de glicemia baixa' - ,sv: 'Brådskande lågt larmvärde' - ,ro: 'Alarmă urgentă hipo' - ,bg: 'Много ниска КЗ' - ,hr: 'Hitni alarm za hipo' - ,it: 'Urgente:Glicemia Bassa' - ,dk: 'Advarsel: Lav' - ,fi: 'Kriittinen matala' - ,nb: 'Kritisk lav alarm' - ,he: 'התראת נמוך דחופה' - ,pl: 'Uwaga: Niski poziom glikemi' - } - ,'Stale Data: Warn' : { - cs: 'Zastaralá data' - ,de: 'Warnung: Daten nicht mehr gültig' - ,es: 'Datos obsoletos: aviso' - ,fr: 'Données dépassées: avis' - ,pt: 'Dados antigos: aviso' - ,sv: 'Förfluten data: Varning!' - ,ro: 'Date învechite: alertă' - ,bg: 'Стари данни' - ,hr: 'Pažnja: Stari podaci' - ,it: 'Notifica Dati' - ,dk: 'Advarsel: Gamle data' - ,fi: 'Vanhat tiedot: varoitus' - ,nb: 'Advarsel: Gamle data' - ,pl: 'Uwaga: dane nieaktualne' - } - ,'Stale Data: Urgent' : { - cs: 'Zastaralá data urgentní' - ,de: 'Achtung: Daten nicht mehr gültig' - ,es: 'Datos obsoletos: Urgente' - ,fr: 'Données dépassées urgentes' - ,pt: 'Dados antigos: Urgente' - ,sv: 'Brådskande varning, inaktuell data' - ,ro: 'Date învechite: urgent' - ,bg: 'Много стари данни' - ,hr: 'Hitno: Stari podaci' - ,it: 'Notifica:Urgente' - ,dk: 'Advarsel: Gamle data' - ,fi: 'Vanhat tiedot: hälytys' - ,nb: 'Advarsel: Veldig gamle data' - ,pl: 'Ostrzeżenie: dane nieaktualne' - } - ,'mins' : { - cs: 'min' - ,de: 'min' - ,es: 'min' - ,fr: 'mins' - ,pt: 'min' - ,sv: 'min' - ,ro: 'min' - ,bg: 'мин' - ,hr: 'min' - ,it: 'min' - ,dk: 'min' - ,fi: 'minuuttia' - ,nb: 'min' - ,pl: 'min' - } - ,'Night Mode' : { - cs: 'Noční mód' - ,de: 'Nacht Modus' - ,es: 'Modo nocturno' - ,fr: 'Mode nocturne' - ,pt: 'Modo noturno' - ,sv: 'Nattläge' - ,ro: 'Mod nocturn' - ,bg: 'Нощен режим' - ,hr: 'Noćni način' - ,it: 'Modalità Notte' - ,dk: 'Nat mode' - ,fi: 'Yömoodi' - ,nb: 'Nattmodus' - ,pl: 'Tryb nocny' - } - ,'When enabled the page will be dimmed from 10pm - 6am.' : { - cs: 'Když je povoleno, obrazovka je ztlumena 22:00 - 6:00' - ,de: 'Wenn aktiviert wird die Anzeige von 22 Uhr - 6 Uhr gedimmt' - ,es: 'Cuando esté activo, el brillo de la página bajará de 10pm a 6am.' - ,fr: 'Si activé, la page sera assombire de 22:00 à 6:00' - ,pt: 'Se ativado, a página será escurecida entre 22h e 6h' - ,sv: 'När aktiverad dimmas sidan mellan 22:00 - 06:00' - ,ro: 'La activare va scădea iluminarea între 22 și 6' - ,bg: 'Когато е активирано, страницата ще е затъмнена от 22-06ч' - ,hr: 'Kad je uključen, stranica će biti zatamnjena od 22-06' - ,it: 'Attivandola, la pagina sarà oscurata dalle 22:00-06:00.' - ,dk: 'Når aktiveret vil denne side nedtones fra 22:00-6:00' - ,fi: 'Aktivoituna sivu himmenee kello 22 ja 06 välillä' - ,nb: 'Når aktivert vil denne siden nedtones fra 22:00-06:00' - ,pl: 'Po włączeniu strona będzie przyciemniona od 22 wieczorem do 6 nad ranem. ' - } - ,'Enable' : { - cs: 'Povoleno' - ,de: 'Eingeschaltet' - ,es: 'Activar' - ,fr: 'Activer' - ,pt: 'Ativar' - ,sv: 'Aktivera' - ,ro: 'Activează' - ,bg: 'Активен' - ,hr: 'Aktiviraj' - ,it: 'Permettere' - ,dk: 'Aktivere' - ,fi: 'Aktivoi' - ,nb: 'Aktiver' - ,pl: 'Włącz' - } - ,'Show Raw BG Data' : { - cs: 'Zobraz RAW data' - ,de: 'Zeige Roh-Blutglukose Daten' - ,es: 'Mostrat datos en glucemia en crudo' - ,fr: 'Montrer les données BG brutes' - ,pt: 'Mostrar dados de glicemia não processados' - ,sv: 'Visa RAW-data' - ,ro: 'Afișează date primare glicemie' - ,bg: 'Показвай RAW данни' - ,hr: 'Prikazuj sirove podatke o GUK-u' - ,it: 'Mostra dati Raw BG' - ,dk: 'Vis rå data' - ,fi: 'Näytä raaka VS tieto' - ,nb: 'Vis rådata' - ,pl: 'Wyświetl surowe dane RAW' - } - ,'Never' : { - cs: 'Nikdy' - ,de: 'Nie' - ,es: 'Nunca' - ,fr: 'Jamais' - ,pt: 'Nunca' - ,sv: 'Aldrig' - ,ro: 'Niciodată' - ,bg: 'Никога' - ,hr: 'Nikad' - ,it: 'Mai' - ,dk: 'Aldrig' - ,fi: 'Ei koskaan' - ,nb: 'Aldri' - ,pl: 'Nigdy' - } - ,'Always' : { - cs: 'Vždy' - ,de: 'Immer' - ,es: 'Siempre' - ,fr: 'Toujours' - ,pt: 'Sempre' - ,sv: 'Alltid' - ,ro: 'Întotdeauna' - ,bg: 'Винаги' - ,hr: 'Uvijek' - ,it: 'Sempre' - ,dk: 'Altid' - ,fi: 'Aina' - ,nb: 'Alltid' - ,pl: 'Zawsze' - } - ,'When there is noise' : { - cs: 'Při šumu' - ,de: 'Sofern Störgeräusch vorhanden' - ,es: 'Cuando hay ruido' - ,fr: 'Quand il y a du bruit' - ,pt: 'Quando houver ruído' - ,sv: 'Endast vid brus' - ,ro: 'Atunci când este diferență' - ,bg: 'Когато има шум' - ,hr: 'Kad postoji šum' - ,it: 'Quando vi è rumore' - ,dk: 'Når der er støj' - ,fi: 'Signaalihäiriöiden yhteydessä' - ,nb: 'Når det er støy' - ,pl: 'Gdy sygnał jest zakłucony' - } - ,'When enabled small white dots will be displayed for raw BG data' : { - cs: 'Když je povoleno, malé tečky budou zobrazeny pro RAW data' - ,de: 'Bei Aktivierung erscheinen kleine weiße Punkte für Roh-Blutglukose Daten' - ,es: 'Cuando esté activo, pequeños puntos blancos mostrarán los datos en crudo' - ,fr: 'Si activé, des points blancs représenteront les données brutes' - ,pt: 'Se ativado, pontos brancos representarão os dados de glicemia não processados' - ,sv: 'När aktiverad visar de vita punkterna RAW-blodglukosevärden' - ,ro: 'La activare vor apărea puncte albe reprezentând citirea brută a glicemiei' - ,bg: 'Когато е активно, малки бели точки ще показват RAW данните' - ,hr: 'Kad je omogućeno, male bijele točkice će prikazivati sirove podatke o GUK-u.' - ,it: 'Quando lo abiliti, visualizzerai piccoli puntini bianchi (raw BG data)' - ,dk: 'Ved aktivering vil små hvide prikker blive vist for rå BG tal' - ,fi: 'Aktivoituna raaka VS tieto piirtyy aikajanalle valkoisina pisteinä' - ,nb: 'Ved aktivering vil små hvite prikker bli vist for rå BG tall' - ,pl: 'Gdy funkcja jest włączona, małe białe kropki pojawią się na surowych danych BG' - } - ,'Custom Title' : { - cs: 'Vlastní název stránky' - ,de: 'Benutzerdefiniert' - ,es: 'Título personalizado' - ,fr: 'Titre sur mesure' - ,pt: 'Customizar Título' - ,sv: 'Egen titel' - ,ro: 'Titlu particularizat' - ,bg: 'Име на страницата' - ,hr: 'Vlastiti naziv' - ,it: 'Titolo personalizzato' - ,dk: 'Egen titel' - ,fi: 'Omavalintainen otsikko' - ,nb: 'Egen tittel' - ,pl: 'Własny tytuł strony' - } - ,'Theme' : { - cs: 'Téma' - ,de: 'Thema' - ,es: 'Tema' - ,fr: 'Thème' - ,pt: 'tema' - ,ro: 'Temă' - ,bg: 'Тема' - ,hr: 'Tema' - ,sv: 'Tema' - ,it: 'Tema' - ,dk: 'Tema' - ,fi: 'Teema' - ,nb: 'Tema' - ,pl: 'Wygląd' - } - ,'Default' : { - cs: 'Výchozí' - ,de: 'Voreingestellt' - ,es: 'Por defecto' - ,fr: 'Par défaut' - ,pt: 'Padrão' - ,sv: 'Standard' - ,ro: 'Implicită' - ,bg: 'Черно-бяла' - ,hr: 'Default' - ,it: 'Predefinito' - ,dk: 'Standard' - ,fi: 'Oletus' - ,nb: 'Standard' - ,pl: 'Domyślny' - } - ,'Colors' : { - cs: 'Barevné' - ,de: 'Farben' - ,es: 'Colores' - ,fr: 'Couleurs' - ,pt: 'Colorido' - ,sv: 'Färg' - ,ro: 'Colorată' - ,bg: 'Цветна' - ,hr: 'Boje' - ,it: 'Colori' - ,dk: 'Farver' - ,fi: 'Värit' - ,nb: 'Farger' - ,pl: 'Kolorowy' - } - ,'Reset, and use defaults' : { - cs: 'Vymaž a nastav výchozí hodnoty' - ,de: 'Zurücksetzen und Voreinstellungen verwenden' - ,es: 'Inicializar y utilizar los valores por defecto' - ,fr: 'Remise à zéro et utiliser des valeurs par défaut' - ,pt: 'Zerar e usar padrões' - ,sv: 'Återställ standardvärden' - ,ro: 'Resetează și folosește setările implicite' - ,bg: 'Нулирай и използвай стандартните настройки' - ,hr: 'Resetiraj i koristi defaultne vrijednosti' - ,it: 'Resetta le impostazioni predefinite' - ,dk: 'Returner til standardopsætning' - ,fi: 'Palauta oletusasetukset' - ,nb: 'Gjenopprett standardinnstillinger' - ,pl: 'Reset i powrót do domyślnych ustawień' - } - ,'Calibrations' : { - cs: 'Kalibrace' - ,de: 'Kalibrierung' - ,es: 'Calibraciones' - ,fr: 'Calibration' - ,pt: 'Calibraçôes' - ,sv: 'Kalibreringar' - ,ro: 'Calibrări' - ,bg: 'Калибрации' - ,hr: 'Kalibriranje' - ,it: 'Calibrazioni' - ,dk: 'Kalibrering' - ,fi: 'Kalibraatiot' - ,nb: 'Kalibreringer' - ,pl: 'Kalibracja' - } - ,'Alarm Test / Smartphone Enable' : { - cs: 'Test alarmu' - ,de: 'Alarm Test / Smartphone aktivieren' - ,es: 'Test de Alarma / Activar teléfono' - ,fr: 'Test alarme / Activer Smartphone' - ,pt: 'Testar Alarme / Ativar Smartphone' - ,sv: 'Testa alarm / Aktivera Smatphone' - ,ro: 'Teste alarme / Activează pe smartphone' - ,bg: 'Тестване на алармата / Активно за мобилни телефони' - ,hr: 'Alarm test / Aktiviraj smartphone' - ,it: 'Test Allarme / Abilita Smartphone' - ,dk: 'Alarm test / Smartphone aktiveret' - ,fi: 'Hälytyksien testaus / Älypuhelimien äänet päälle' - ,nb: 'Alarmtest / Smartphone aktivering' - ,pl: 'Test alarmu' - } - ,'Bolus Wizard' : { - cs: 'Bolusový kalkulátor' - ,de: 'Bolus Kalkulator' - ,es: 'Bolus Wizard' - ,fr: 'Calculateur de bolus' - ,pt: 'Calculadora de bolus' - ,sv: 'Boluskalkylator' - ,ro: 'Calculator sugestie bolus' - ,bg: 'Болус съветник ' - ,hr: 'Bolus wizard' - ,it: 'Calcolatore di Bolo' - ,dk: 'Bolusberegner' - ,fi: 'Annosopas' - ,nb: 'Boluskalkulator' - ,pl: 'Bolus kreator' - } - ,'in the future' : { - cs: 'v budoucnosti' - ,de: 'in der Zuknft' - ,es: 'en el futuro' - ,fr: 'dans le futur' - ,pt: 'no futuro' - ,sv: 'framtida' - ,ro: 'în viitor' - ,bg: 'в бъдещето' - ,hr: 'U budućnosti' - ,it: 'nel futuro' - ,dk: 'fremtiden' - ,fi: 'tulevaisuudessa' - ,nb: 'fremtiden' - ,pl: 'w przyszłości' - } - ,'time ago' : { - cs: 'min zpět' - ,de: 'Aktualisiert' - ,es: 'tiempo atrás' - ,fr: 'temps avant' - ,pt: 'tempo atrás' - ,sv: 'förfluten tid' - ,ro: 'în trecut' - ,bg: 'преди време' - ,hr: 'prije' - ,it: 'tempo fa' - ,dk: 'tid siden' - ,fi: 'aikaa sitten' - ,nb: 'tid siden' - ,pl: 'czas temu' - } - ,'hr ago' : { - cs: 'hod zpět' - ,de: 'Std. vorher' - ,es: 'hr atrás' - ,fr: 'hr avant' - ,pt: 'h atrás' - ,sv: 'timmar sedan' - ,ro: 'oră în trecut' - ,bg: 'час по-рано' - ,hr: 'sat unazad' - ,it: 'ora fa' - ,dk: 'Time siden' - ,fi: 'tunti sitten' - ,nb: 'Time siden' - ,pl: 'godzina temu' - } - ,'hrs ago' : { - cs: 'hod zpět' - ,de: 'Std. vorher' - ,es: 'hr atrás' - ,fr: 'hrs avant' - ,pt: 'h atrás' - ,sv: 'Timmar sedan' - ,ro: 'h în trecut' - ,bg: 'часа по-рано' - ,hr: 'sati unazad' - ,it: 'ore fa' - ,dk: 'Timer siden' - ,fi: 'tuntia sitten' - ,nb: 'Timer siden' - ,pl: 'godzin temu' - } - ,'min ago' : { - cs: 'min zpět' - ,de: 'Min. vorher' - ,es: 'min atrás' - ,fr: 'min avant' - ,pt: 'min atrás' - ,sv: 'minut sedan' - ,ro: 'minut în trecut' - ,bg: 'мин. по-рано' - ,hr: 'minuta unazad' - ,it: 'minuto fa' - ,dk: 'minutter siden' - ,fi: 'minuuttia sitten' - ,nb: 'minutter siden' - ,pl: 'minuta temu' - } - ,'mins ago' : { - cs: 'min zpět' - ,de: 'Min. vorher' - ,es: 'min atrás' - ,fr: 'mins avant' - ,pt: 'min atrás' - ,sv: 'minuter sedan' - ,ro: 'minute în trecut' - ,bg: 'мин. по-рано' - ,hr: 'minuta unazad' - ,it: 'minuti fa' - ,dk: 'minutter siden' - ,fi: 'minuuttia sitten' - ,nb: 'minutter siden' - ,pl: 'minut temu' - } - ,'day ago' : { - cs: 'den zpět' - ,de: 'Tag vorher' - ,es: 'día atrás' - ,fr: 'jour avant' - ,pt: 'dia atrás' - ,sv: 'dag sedan' - ,ro: 'zi în trecut' - ,bg: 'ден по-рано' - ,hr: 'dan unazad' - ,it: 'Giorno fa' - ,dk: 'dag siden' - ,fi: 'päivä sitten' - ,nb: 'dag siden' - ,pl: 'dzień temu' - } - ,'days ago' : { - cs: 'dnů zpět' - ,de: 'Tage vorher' - ,es: 'días atrás' - ,fr: 'jours avant' - ,pt: 'dias atrás' - ,sv: 'dagar sedan' - ,ro: 'zile în trecut' - ,bg: 'дни по-рано' - ,hr: 'dana unazad' - ,it: 'giorni fa' - ,dk: 'dage siden' - ,fi: 'päivää sitten' - ,nb: 'dager siden' - ,pl: 'dni temu' - } - ,'long ago' : { - cs: 'dlouho zpět' - ,de: 'Lange her' - ,es: 'Hace mucho tiempo' - ,fr: 'il y a longtemps' - ,pt: 'muito tempo atrás' - ,sv: 'länge sedan' - ,ro: 'timp în trecut' - ,bg: 'преди много време' - ,hr: 'prije dosta vremena' - ,it: 'Molto tempo fa' - ,dk: 'længe siden' - ,fi: 'Pitkän aikaa sitten' - ,nb: 'lenge siden' - ,pl: 'minut temu' - } - ,'Clean' : { - cs: 'Čistý' - ,de: 'Komplett' - ,es: 'Limpio' - ,fr: 'Propre' - ,pt: 'Limpo' - ,sv: 'Rent' - ,ro: 'Curat' - ,bg: 'Чист' - ,hr: 'Čisto' - ,it: 'Pulito' - ,dk: 'Rent' - ,fi: 'Puhdas' - ,nb: 'Rent' - ,pl: 'Czysty' - } - ,'Light' : { - cs: 'Lehký' - ,de: 'Leicht' - ,es: 'Ligero' - ,fr: 'Léger' - ,pt: 'Leve' - ,sv: 'Lätt' - ,ro: 'Ușor' - ,bg: 'Лек' - ,hr: 'Lagano' - ,it: 'Leggero' - ,dk: 'Let' - ,fi: 'Kevyt' - ,nb: 'Lite' - ,pl: 'Niski' - } - ,'Medium' : { - cs: 'Střední' - ,de: 'Mittel' - ,es: 'Medio' - ,fr: 'Moyen' - ,pt: 'Médio' - ,sv: 'Måttligt' - ,ro: 'Mediu' - ,bg: 'Среден' - ,hr: 'Srednje' - ,it: 'Medio' - ,dk: 'Middel' - ,fi: 'Keskiverto' - ,nb: 'Middels' - ,pl: 'Średni' - } - ,'Heavy' : { - cs: 'Velký' - ,de: 'Schwer' - ,es: 'Fuerte' - ,fr: 'Important' - ,pt: 'Pesado' - ,sv: 'Rikligt' - ,ro: 'Puternic' - ,bg: 'Силен' - ,hr: 'Teško' - ,it: 'Pesante' - ,dk: 'Voldsom' - ,fi: 'Raskas' - ,nb: 'Mye' - ,pl: 'Wysoki' - } - ,'Treatment type' : { - cs: 'Typ ošetření' - ,de: 'Eingabe Typ' - ,es: 'Tipo de tratamiento' - ,fr: 'Type de traitement' - ,pt: 'Tipo de procedimento' - ,sv: 'Behandlingstyp' - ,ro: 'Tip tratament' - ,bg: 'Вид събитие' - ,hr: 'Vrsta tretmana' - ,it: 'Somministrazione' - ,dk: 'Behandlingstype' - ,fi: 'Hoidon tyyppi' - ,nb: 'Behandlingstype' - ,pl: 'Typ zabiegu' - } - ,'Raw BG' : { - cs: 'Glykémie z RAW dat' - ,de: 'Roh Blutglukose' - ,es: 'Glucemia en crudo' - ,fr: 'Glycémie brut' - ,pt: 'Glicemia sem processamento' - ,sv: 'RAW-BS' - ,ro: 'Citire brută a glicemiei' - ,bg: 'Непреработена КЗ' - ,hr: 'Sirovi podaci o GUK-u' - ,it: 'Raw BG' - ,dk: 'Råt BS' - ,fi: 'Raaka VS' - ,nb: 'RAW-BS' - ,pl: 'Raw BG' - } - ,'Device' : { - cs: 'Zařízení' - ,de: 'Gerät' - ,es: 'Dispositivo' - ,fr: 'Appareil' - ,pt: 'Dispositivo' - ,ro: 'Dispozitiv' - ,bg: 'Устройство' - ,hr: 'Uređaj' - ,sv: 'Enhet' - ,it: 'Dispositivo' - ,dk: 'Enhed' - ,fi: 'Laite' - ,nb: 'Enhet' - ,pl: 'Urządzenie' - } - ,'Noise' : { - cs: 'Šum' - ,de: 'Störgeräusch' - ,es: 'Ruido' - ,fr: 'Bruit' - ,pt: 'Ruído' - ,sv: 'Brus' - ,ro: 'Zgomot' - ,bg: 'Шум' - ,hr: 'Šum' - ,it: 'Rumore' - ,dk: 'Støj' - ,fi: 'Kohina' - ,nb: 'Støy' - ,pl: 'szum' - } - ,'Calibration' : { - cs: 'Kalibrace' - ,de: 'Kalibrierung' - ,es: 'Calibración' - ,fr: 'Calibration' - ,pt: 'Calibração' - ,sv: 'Kalibrering' - ,ro: 'Calibrare' - ,bg: 'Калибрация' - ,hr: 'Kalibriranje' - ,it: 'Calibratura' - ,dk: 'Kalibrering' - ,fi: 'Kalibraatio' - ,nb: 'Kalibrering' - ,pl: 'Kalibracja' - } - ,'Show Plugins' : { - cs: 'Zobrazuj pluginy' - ,de: 'Zeige Plugins' - ,es: 'Mostrar Plugins' - ,fr: 'Montrer Plugins' - ,pt: 'Mostrar Plugins' - ,ro: 'Arată plugin-urile' - ,bg: 'Покажи добавките' - ,hr: 'Prikaži plugine' - ,sv: 'Visa tillägg' - ,it: 'Mostra Plugin' - ,dk: 'Vis plugins' - ,fi: 'Näytä pluginit' - ,nb: 'Vis plugins' - ,pl: 'Pokaż rozszerzenia' - } - ,'About' : { - cs: 'O aplikaci' - ,de: 'Über' - ,es: 'Sobre' - ,fr: 'À propos de' - ,pt: 'Sobre' - ,ro: 'Despre' - ,bg: 'Относно' - ,hr: 'O aplikaciji' - ,sv: 'Om' - ,it: 'Informazioni' - ,dk: 'Om' - ,fi: 'Nightscoutista' - ,nb: 'Om' - ,pl: 'O aplikacji' - } - ,'Value in' : { - cs: 'Hodnota v' - ,de: 'Wert in' - ,es: 'Valor en' - ,fr: 'Valeur en' - ,pt: 'Valor em' - ,ro: 'Valoare în' - ,bg: 'Стойност в' - ,hr: 'Vrijednost u' - ,sv: 'Värde om' - ,it: 'Valore in' - ,dk: 'Værdi i' - ,fi: 'Arvo yksiköissä' - ,nb: 'Verdi i' - ,pl: 'Wartości w ' - } - ,'Carb Time' : { - cs: 'Čas jídla' - ,de: 'Kohlenhydrate Zeit' - ,es: 'Momento de la ingesta' - ,fr: 'Moment de Glucide' - ,pt: 'Hora do carboidrato' - ,ro: 'Ora carbohidrați' - ,bg: 'Ядене след' - ,hr: 'Vrijeme unosa UH' - ,sv: 'Kolhydratstid' - ,it: 'Tempo' - ,dk: 'Kulhydratstid' - ,fi: 'Syöntiaika' - ,nb: 'Karbohydrattid' - ,he: 'זמן פחמימה' - ,pl: 'Czas posiłku' - } - ,'Language' : { - cs: 'Jazyk' - ,sv: 'Språk' - ,nb: 'Språk' - ,fi: 'Kieli' - ,pt: 'Língua' - ,ro: 'Limba' - ,bg: 'Език' - ,pl: 'Język' - ,it: 'Lingua' - } - ,'Add new' : { - cs: 'Přidat nový' - } - ,'g' : { // grams shortcut - cs: 'g' - } - ,'ml' : { // milliliters shortcut - cs: 'ml' - } - ,'pcs' : { // pieces shortcut - cs: 'ks' - } - ,'Drag&drop food here' : { - cs: 'Sem táhni & pusť jídlo' - } - ,'Care Portal' : { - cs: 'Portál ošetření' - } - ,'Medium/Unknown' : { // GI of food - cs: 'Střední/Neznámá' - } - ,'IN THE FUTURE' : { - cs: 'V BUDOUCNOSTI' - } - ,'Update' : { // Update button - cs: 'Aktualizovat' - ,sv: 'Uppdatera' - ,nb: 'Oppdater' - ,pt: 'Atualizar' - ,ro: 'Actualizare' - ,bg: 'Актуализирай' - ,it: 'Aggiornamento' - ,pl: 'Aktualizacja' - } - ,'Order' : { - cs: 'Pořadí' - ,sv: 'Sortering' - ,nb: 'Sortering' - ,ro: 'Sortare' - ,bg: 'Ред' - ,it: 'Ordina' - ,pl: 'Zamówienie' - } - ,'oldest on top' : { - cs: 'nejstarší nahoře' - ,sv: 'Äldst först' - ,nb: 'Eldste først' - ,ro: 'mai vechi primele' - ,bg: 'Старите най-отгоре' - ,it: 'più vecchio in alto' - ,pl: 'Najstarszy na górze' - } - ,'newest on top' : { - cs: 'nejnovější nahoře' - ,sv: 'Nyast först' - ,nb: 'Nyeste først' - ,ro: 'mai noi primele' - ,bg: 'Новите най-отгоре' - ,it: 'più recente in alto' - ,pl: 'Najnowszy na górze' - } - ,'All sensor events' : { - cs: 'Všechny události sensoru' - ,nb: 'Alle sensorhendelser' - ,ro: 'Evenimente legate de senzor' - ,bg: 'Всички събития от сензора' - ,it: 'Tutti gli eventi del sensore' - } - ,'Remove future items from mongo database' : { - cs: 'Odebrání položek v budoucnosti z Mongo databáze' - ,nb: 'Fjern fremtidige elementer fra mongo database' - ,ro: 'Șterge date din viitor din baza de date mongo' - ,bg: 'Премахни бъдещите точки от Монго базата с данни' - ,it: 'Rimuovere gli oggetti dal database di mongo (prossimamente)' - } - ,'Find and remove treatments in the future' : { - cs: 'Najít a odstranit záznamy ošetření v budoucnosti' - ,nb: 'Finn og fjern fremtidige behandlinger' - ,ro: 'Caută și elimină tratamente din viiotr' - ,bg: 'Намери и премахни събития в бъдещето' - ,it: 'Individuare e rimuovere le somministrazioni (prossimamente)' - } - ,'This task find and remove treatments in the future.' : { - cs: 'Tento úkol najde a odstraní ošetření v budoucnosti.' - ,nb: 'Finn og fjern fremtidige behandlinger' - ,ro: 'Acest instrument curăță tratamentele din viitor.' - ,bg: 'Тази опция намира и премахва събития в бъдещето.' - ,it: 'Trovare e rimuovere le somministrazioni (prossimamente)' - } - ,'Remove treatments in the future' : { - cs: 'Odstraň ošetření v budoucnosti' - ,nb: 'Fjern fremtidige behandlinger' - ,ro: 'Șterge tratamentele din viitor' - ,bg: 'Премахни събитията в бъдешето' - ,it: 'Rimuovere somministrazioni (prossimamente)' - } - ,'Find and remove entries in the future' : { - cs: 'Najít a odstranit CGM data v budoucnosti' - ,nb: 'Finn og fjern fremtidige hendelser' - ,bg: 'Намери и премахни данни от сензора в бъдещето' - ,ro: 'Caută și elimină valorile din viitor' - ,it: 'Trovare e rimuovere le voci (prossimamente)' - } - ,'This task find and remove CGM data in the future created by uploader with wrong date/time.' : { - cs: 'Tento úkol najde a odstraní CGM data v budoucnosti vzniklé špatně nastaveným datem v uploaderu.' - ,nb: 'Finn og fjern fremtidige cgm data lastet opp med feil dato/tid' - ,bg: 'Тази опция ще намери и премахне данни от сензора в бъдещето, създадени поради грешна дата/време.' - ,ro: 'Instrument de căutare și eliminare a datelor din viitor, create de uploader cu ora setată greșit' - ,it: 'Trovare e rimuovere i dati CGM creato da uploader/xdrip con data / ora sbagliato (prossimamente)' - } - ,'Remove entries in the future' : { - cs: 'Odstraň CGM data v budoucnosti' - ,nb: 'Fjern fremtidige hendelser' - ,bg: 'Премахни данните от сензора в бъдещето' - ,ro: 'Elimină înregistrările din viitor' - ,it: 'Rimuovere le voci (prossimamente)' - } - ,'Loading database ...' : { - cs: 'Nahrávám databázi ...' - ,nb: 'Leser database ...' - ,bg: 'Зареждане на базата с данни ...' - ,ro: 'Încarc baza de date' - ,it: 'Caricamento Database ...' - } - ,'Database contains %1 future records' : { - cs: 'Databáze obsahuje %1 záznamů v budoucnosti' - ,nb: 'Databasen inneholder %1 fremtidige hendelser' - ,ro: 'Baza de date conține %1 înregistrări din viitor' - ,bg: 'Базата с дани съдържа %1 бъдещи записи' - ,it: 'Contiene Database %1 record (prossimamente)' - } - ,'Remove %1 selected records?' : { - cs: 'Odstranit %1 vybraných záznamů' - ,nb: 'Fjern %1 valgte elementer?' - ,ro: 'Șterg %1 înregistrări selectate?' - ,bg: 'Премахване на %1 от избраните записи?' - ,it: 'Rimuovere %1 record selezionati?' - } - ,'Error loading database' : { - cs: 'Chyba při nahrávání databáze' - ,nb: 'Feil udner lasting av database' - ,ro: 'Eroare la încărcarea bazei de date' - ,bg: 'Грешка при зареждане на базата с данни' - ,it: 'Errore di caricamento del database' - } - ,'Record %1 removed ...' : { - cs: 'Záznam %1 odstraněn ...' - ,nb: 'Element %1 fjernet' - ,ro: 'Înregistrarea %1 a fost ștearsă...' - ,bg: '%1 записи премахнати' - ,it: 'Record %1 rimosso ...' - } - ,'Error removing record %1' : { - cs: 'Chyba při odstaňování záznamu %1' - ,nb: 'Feil under fjerning av element %1' - ,ro: 'Eroare la ștergerea înregistrării %1' - ,bg: 'Грешка при премахването на %1 от записите' - ,it: 'Errore rimozione record %1' - } - ,'Deleting records ...' : { - cs: 'Odstraňování záznamů ...' - ,nb: 'Fjerner elementer...' - ,ro: 'Se șterg înregistrările...' - ,bg: 'Изтриване на записите...' - ,it: 'Eliminazione dei record ...' - } - ,'Clean Mongo status database' : { - cs: 'Vyčištění Mongo databáze statusů' - ,nb: 'Slett Mongo status database' - ,ro: 'Curăță tabela despre status din Mongo' - ,bg: 'Изчисти статуса на Монго базата с данни' - ,it: 'Pulire database di Mongo' - } - ,'Delete all documents from devicestatus collection' : { - cs: 'Odstranění všech záznamů z kolekce devicestatus' - ,nb: 'Fjern alle dokumenter fra device status tabell' - ,ro: 'Șterge toate documentele din colecția de status dispozitiv' - ,bg: 'Изтрий всички документи от папката статус-устройство' - ,it: 'Eliminare tutti i documenti dalla collezione "devicestatus"' - } - ,'This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.' : { - cs: 'Tento úkol odstraní všechny dokumenty z kolekce devicestatus. Je to vhodné udělat, pokud se ukazatel stavu baterie neobnovuje správně.' - ,nb: 'Denne funksjonen fjerner alle dokumenter fra device status tabellen. Nyttig når status for opplaster batteri ikke blir opppdatert' - ,ro: 'Acest instrument șterge toate documentele din colecția devicestatus. Se folosește când încărcarea bateriei nu se afișează corect.' - ,bg: 'Тази опция премахва всички документи от папката статус-устройство. Полезно е, когато статусът на батерията не се обновява.' - ,it: 'Questa attività elimina tutti i documenti dalla collezione "devicestatus". Utile quando lo stato della batteria uploader/xdrip non si aggiorna.' - } - ,'Delete all documents' : { - cs: 'Odstranit všechny dokumenty' - ,nb: 'Fjern alle dokumenter' - ,ro: 'Șterge toate documentele' - ,bg: 'Изтрий всички документи' - ,it: 'Eliminare tutti i documenti' - } - ,'Delete all documents from devicestatus collection?' : { - cs: 'Odstranit všechny dokumenty z kolekce devicestatus?' - ,nb: 'Fjern alle dokumenter fra device status tabellen?' - ,ro: 'Șterg toate documentele din colecția devicestatus?' - ,bg: 'Изтриване на всички документи от папката статус-устройство?' - ,it: 'Eliminare tutti i documenti dalla collezione devicestatus?' - } - ,'Database contains %1 records' : { - cs: 'Databáze obsahuje %1 záznamů' - ,nb: 'Databasen inneholder %1 elementer' - ,ro: 'Baza de date conține %1 înregistrări' - ,bg: 'Базата с данни съдържа %1 записи' - ,it: 'Contiene Database record%1' - } - ,'All records removed ...' : { - cs: 'Všechny záznamy odstraněny ...' - ,nb: 'Alle elementer fjernet ...' - ,ro: 'Toate înregistrările au fost șterse.' - ,bg: 'Всички записи премахнати ...' - ,it: 'Tutti i record rimossi ...' - } - ,'Admin Tools' : { - cs: 'Nástroje pro správu' - ,nb: 'Administrasjonsoppgaver' - ,ro: 'Instrumente de administrare' - ,bg: 'Настройки на администратора' - ,it: 'NS - Dati Mongo' - } - ,'Nightscout reporting' : { - cs: 'Nightscout - Výkazy' - ,nb: 'Nightscout - rapporter' - ,ro: 'Rapoarte Nightscout' - ,bg: 'Найтскаут статистика' - ,it: 'Nightscout - Statistiche' - } - ,'Cancel' : { - cs: 'Zrušit' - ,nb: 'Avbryt' - ,ro: 'Renunță' - ,bg: 'Откажи' - ,it: 'Cancellare' - } - ,'Edit treatment' : { - cs: 'Upravit ošetření' - ,nb: 'Editer behandling' - ,ro: 'Modifică înregistrarea' - ,bg: 'Редакция на събитие' - ,it: 'Modifica Somministrazione' - } - ,'Duration' : { - cs: 'Doba trvání' - ,ro: 'Durata' - ,bg: 'Времетраене' - ,it: 'Tempo' - } - ,'Duration in minutes' : { - cs: 'Doba trvání v minutách' - ,ro: 'Durata în minute' - ,bg: 'Времетраене в мин.' - ,it: 'Tempo in minuti' - } - ,'Temp Basal' : { - cs: 'Dočasný bazál' - ,ro: 'Bazală temporară' - ,bg: 'Временен базал' - ,it: 'Basale Temp' - } - ,'Temp Basal Start' : { - cs: 'Dočasný bazál začátek' - ,ro: 'Start bazală temporară' - ,bg: 'Начало на временен базал' - ,it: 'Inizio Basale Temp' - } - ,'Temp Basal End' : { - cs: 'Dočasný bazál konec' - ,ro: 'Sfârșit bazală temporară' - ,bg: 'Край на временен базал' - ,it: 'Fine Basale Temp' - } - ,'Percent' : { // value in % for temp basal - cs: 'Procenta' - ,ro: 'Procent' - ,bg: 'Процент' - ,it: 'Percentuale' - } - ,'Basal change in %' : { - cs: 'Změna bazálu v %' - ,ro: 'Bazală schimbată în %' - ,bg: 'Промяна на базала с %' - ,it: 'Variazione basale in %' - } - ,'Basal value' : { // absolute value for temp basal - cs: 'Hodnota bazálu' - ,ro: 'Valoare bazală' - ,bg: 'Временен базал' - ,it: 'Valore Basale temp' - } - ,'Absolute basal value' : { - cs: 'Hodnota bazálu' - ,bg: 'Базална стойност' - ,it: 'Valore Basale' - } - ,'Announcement' : { - cs: 'Oznámení' - ,bg: 'Известяване' - ,fi: 'Tiedoitus' - ,pt: 'Aviso' - ,ro: 'Anunț' - ,he: 'הודעה' - ,it: 'Annuncio' - ,pl: 'Powiadomienia' - } - ,'Loading temp basal data' : { - cs: 'Nahrávám dočasné bazály' - ,it: 'Caricamento basale temporanea' - } - ,'Save current record before changing to new?' : { - cs: 'Uložit současný záznam před změnou na nový?' - } - ,'Profile Switch' : { - cs: 'Přepnutí profilu' - } - ,'Profile' : { - cs: 'Profil' - } - ,'General profile settings' : { - cs: 'Obecná nastavení profilu' - } - ,'Title' : { - cs: 'Název' - } - ,'Database records' : { - cs: 'Záznamy v databázi' - } - ,'Add new record' : { - cs: 'Přidat nový záznam' - } - ,'Remove this record' : { - cs: 'Vymazat tento záznam' - } - ,'Clone this record to new' : { - cs: 'Zkopíruj tento záznam do nového' - } - ,'Record valid from' : { - cs: 'Záznam platný od' - } - ,'Stored profiles' : { - cs: 'Uložené profily' - } - ,'Timezone' : { - cs: 'Časová zóna' - } - ,'Duration of Insulin Activity (DIA)' : { - cs: 'Doba působnosti inzulínu (DIA)' - } - ,'Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.' : { - cs: 'Představuje typickou dobu, po kterou inzulín působí. Bývá různá podle pacienta a inzulínu. Typicky 3-4 hodiny pro pacienty s pumpou.' - } - ,'Insulin to carb ratio (I:C)' : { - cs: 'Inzulíno-sacharidový poměr (I:C).' - } - ,'hours' : { - cs: 'hodiny' - } - ,'g/hour' : { - cs: 'g/hod' - } - ,'g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.' : { - cs: 'gramy na jednotku inzulínu. Poměr, jaké množství sacharidů pokryje jednotku inzulínu.' - } - ,'Insulin Sensitivity Factor (ISF)' : { - cs: 'Citlivost inzulínu (ISF)' - } - ,'mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.' : { - cs: 'mg/dL nebo mmol/L na jednotku inzulínu. Poměr, jak se změní glykémie po podaní jednotky inzulínu' - } - ,'Carbs activity / absorption rate' : { - cs: 'Rychlost absorbce sacharidů' - } - ,'grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).' : { - cs: 'gramy za jednotku času. Reprezentuje jak změnu COB za jednoku času, tak množství sacharidů, které se za tu dobu projevily. Křivka absorbce sacharidů je mnohem méně pochopitelná než IOB, ale může být aproximována počáteční pauzou následovanou konstantní hodnotou absorbce (g/hod).' - } - ,'Basal rates [unit/hour]' : { - cs: 'Bazály [U/hod].' - } - ,'Target BG range [mg/dL,mmol/L]' : { - cs: 'Cílový rozsah glykémií [mg/dL,mmol/L]' - } - ,'Start of record validity' : { - cs: 'Začátek platnosti záznamu' - } - ,'Icicle' : { - cs: 'Rampouch' - } - ,'Render Basal' : { - cs: 'Zobrazení bazálu' - } - ,'Profile used' : { - cs: 'Použitý profil' - } + // console.log('localization:', text, 'not found'); + return text; + }; + // case insensitive + language.translateCI = function translateCaseInsensitive (text) { + var utext = text.toUpperCase(); + _.forEach(translations, function(ts, key) { + var ukey = key.toUpperCase(); + if (ukey === utext) { + text = ts; + } + }); + return text; }; - language.translate = function translate(text, options) { - var translated = text; - if (translations[text] && translations[text][lang]) { - translated = translations[text][lang]; + language.translate = function translate (text, options) { + var translated; + if (options && options.ci) { + translated = language.translateCI(text); + } else { + translated = language.translateCS(text); + } + + 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 (options && options.params) { - for (var i = 0; i < options.params.length; i++) { - var r = new RegExp('\%' + (i+1), 'g'); - translated = translated.replace(r, options.params[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.set = function set(newlang) { - lang = newlang; + language.getFilename = function getFilename (code) { + + if (code == 'en') { + return 'en/en.json'; + } + + let file; + 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) { + 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) { + if (!newlang) return; + language.lang = newlang; + + language.languages.forEach(function(l) { + if (l.code === language.lang && l.speechCode) language.speechCode = l.speechCode; + }); + return language(); }; + language.get = function get (lang) { + var r; + language.languages.forEach(function(l) { + if (l.code === lang) r = l; + }); + return r; + } + + // if run on server and we get a filesystem handle, load english by default + if (fs) { + language.set('en'); + language.loadLocalization(fs); + } + return language(); } diff --git a/lib/levels.js b/lib/levels.js index 14dce01d308..6602e64afea 100644 --- a/lib/levels.js +++ b/lib/levels.js @@ -1,15 +1,19 @@ 'use strict'; +var constants = require('./constants'); var levels = { - URGENT: 2 - , WARN: 1 - , INFO: 0 - , LOW: -1 - , LOWEST: -2 - , NONE: -3 + URGENT: constants.LEVEL_URGENT + , WARN: constants.LEVEL_WARN + , INFO: constants.LEVEL_INFO + , LOW: constants.LEVEL_LOW + , LOWEST: constants.LEVEL_LOWEST + , NONE: constants.LEVEL_NONE }; +levels.language = require('./language')(); +levels.translate = levels.language.translate; + var level2Display = { '2': 'Urgent' , '1':'Warning' @@ -25,12 +29,24 @@ levels.isAlarm = function isAlarm(level) { levels.toDisplay = function toDisplay(level) { var key = level !== undefined && level.toString(); - return key && level2Display[key] || 'Unknown'; + return key && levels.translate(level2Display[key]) || levels.translate('Unknown'); }; levels.toLowerCase = function toLowerCase(level) { return levels.toDisplay(level).toLowerCase(); }; +levels.toStatusClass = function toStatusClass(level) { + var cls = 'current'; + + if (level === levels.WARN) { + cls = 'warn'; + } else if (level === levels.URGENT) { + cls = 'urgent'; + } + + return cls; +}; + module.exports = levels; \ No newline at end of file diff --git a/lib/middleware/express-extension-to-accept.js b/lib/middleware/express-extension-to-accept.js new file mode 100644 index 00000000000..cdfe9a269fe --- /dev/null +++ b/lib/middleware/express-extension-to-accept.js @@ -0,0 +1,41 @@ +var mime = require('mime') +var url = require('url') + +module.exports = function (formats) { + if (!Array.isArray(formats)) + throw new TypeError('Formats must be an array.') + + var getType = Object.create(null) + + formats.forEach(function (format) { + if (!/^\w+$/.test(format)) + throw new TypeError('Invalid format - must be a word.') + + var type = getType[format] = mime.getType(format) + if (!type || type === 'application/octet-stream') + throw new Error('Invalid format.') + }) + + var regexp = new RegExp('\\.(' + formats.join('|') + ')$', 'i') + + return function (req, res, next) { + var match = req.path.match(regexp) + if (!match) + return next() + var type = getType[match[1]] + if (!type) + return next() + + req.extToAccept = { + url: req.url, + accept: req.headers.accept + } + + req.headers.accept = type + var parsed = url.parse(req.url) + parsed.pathname = req.path.replace(regexp, '') + req.url = url.format(parsed) + + next() + } +} \ No newline at end of file diff --git a/lib/middleware/index.js b/lib/middleware/index.js index 29255958a4c..7896d889a6d 100644 --- a/lib/middleware/index.js +++ b/lib/middleware/index.js @@ -1,21 +1,34 @@ 'use strict'; var wares = { - verifyAuthorization : require('./verify-token'), sendJSONStatus : require('./send-json-status'), - bodyParser : require('body-parser') + bodyParser : require('body-parser'), + compression : require('compression'), + obscureDeviceProvenance: require('./obscure-provenance') }; function extensions (list) { - return require('express-extension-to-accept')(list); + return require('./express-extension-to-accept')(list); } function configure (env) { return { - verifyAuthorization: wares.verifyAuthorization(env), sendJSONStatus: wares.sendJSONStatus( ), bodyParser: wares.bodyParser, - extensions: extensions + jsonParser: wares.bodyParser.json({ + limit: '1Mb', + }), + urlencodedParser: wares.bodyParser.urlencoded({ + limit: '1Mb', + extended: true, + parameterLimit: 50000 + }), + rawParser: wares.bodyParser.raw({ + limit: '1Mb' + }), + compression: wares.compression, + extensions: extensions, + obscure_device: wares.obscureDeviceProvenance(env) }; } diff --git a/lib/middleware/obscure-provenance.js b/lib/middleware/obscure-provenance.js new file mode 100644 index 00000000000..482cf20a8f0 --- /dev/null +++ b/lib/middleware/obscure-provenance.js @@ -0,0 +1,15 @@ +var _ = require('lodash'); + +module.exports = function create_device_obscurity (env) { + function obscure_device (req, res, next) { + if (res.entries && env.settings.obscureDeviceProvenance) { + var entries = _.cloneDeep(res.entries); + for (var i = 0; i < entries.length; i++) { + entries[i].device = env.settings.obscureDeviceProvenance; + } + res.entries = entries; + } + next( ); + } + return obscure_device; +} diff --git a/lib/middleware/verify-token.js b/lib/middleware/verify-token.js deleted file mode 100644 index 1f48d16053c..00000000000 --- a/lib/middleware/verify-token.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -var consts = require('../constants'); -function configure (env) { - function verifyAuthorization(req, res, next) { - // Retrieve the secret values to be compared. - var api_secret = env.api_secret; - var secret = req.params.secret ? req.params.secret : req.header('api-secret'); - - // Return an error message if the authorization fails. - var authorized = (api_secret && api_secret.length > 12) ? (secret === api_secret) : false; - if (!authorized) { - res.sendJSONStatus(res, consts.HTTP_UNAUTHORIZED, 'Unauthorized', 'api-secret Request Header is incorrect or missing.'); - } else { - next(); - } - - return authorized; - } - return verifyAuthorization; - -} -module.exports = configure; diff --git a/lib/mqtt.js b/lib/mqtt.js deleted file mode 100644 index 0a4058e1a8e..00000000000 --- a/lib/mqtt.js +++ /dev/null @@ -1,317 +0,0 @@ -'use strict'; - -var es = require('event-stream'); -var Long = require('long'); -var decoders = require('sgvdata/lib/protobuf'); -var direction = require('sgvdata/lib/utils').direction; -var moment = require('moment'); -var url = require('url'); - -function init (env, ctx) { - - function mqtt ( ) { - return mqtt; - } - var info = url.parse(env.MQTT_MONITOR); - var username = info.auth.split(':').slice(0, -1).join(''); - var shared_topic = '/downloads/' + username + '/#'; - var alias_topic = '/downloads/' + username + '/protobuf'; - var notification_topic = '/notifications/' + username + '/json'; - env.mqtt_shared_topic = shared_topic; - - mqtt.client = connect(env); - var downloads = mqtt.downloads = downloader(); - - if (mqtt.client) { - listenForMessages(ctx); - } - - mqtt.every = every; - mqtt.entries = process(); - - //expose for tests that don't need to connect - mqtt.sgvSensorMerge = sgvSensorMerge; - - function listenForMessages ( ) { - mqtt.client.on('message', function (topic, msg) { - console.log('topic', topic); - // XXX: ugly hack - if (topic === alias_topic) { - topic = '/downloads/protobuf'; - } - - console.log(topic, 'on message', 'msg', msg.length); - switch (topic) { - case '/uploader': - console.log({type: topic, msg: msg.toString()}); - break; - case '/downloads/protobuf': - downloadProtobuf(msg, topic, downloads, ctx); - break; - - default: - console.log(topic, 'on message', 'msg', msg); - // ctx.entries.write(msg); - break; - } - }); - } - - mqtt.emitNotification = function emitNotification(notify) { - console.info('Publishing notification to mqtt: ', notify); - [notification_topic, '/notifications/json'].forEach(function iter_notify (topic) { - mqtt.client.publish(topic, JSON.stringify(notify), function mqttCallback (err) { - if (err) { - console.error('Unable to publish notification to MQTT', err); - } - }); - }); - }; - - return mqtt(); -} - -function connect (env) { - var uri = env.MQTT_MONITOR; - var shared_topic = env.mqtt_shared_topic; - if (!uri) { - return null; - } - - var opts = { - encoding: 'binary', - clean: false, - clientId: env.mqtt_client_id - }; - var client = require('mqtt').connect(uri, opts); - - function granted () { console.log('granted', arguments); } - - client.subscribe('sgvs'); - client.subscribe('published'); - client.subscribe('/downloads/protobuf', {qos: 2}, granted); - client.subscribe(shared_topic, {qos: 2}, granted); - client.subscribe('/uploader', granted); - client.subscribe('/entries/sgv', granted); - - return client; -} - -function process ( ) { - var stream = es.through( - function _write(data) { - this.push(data); - } - ); - return stream; -} - -function every (storage) { - function iter(item, next) { - storage.create(item, next); - } - - return es.map(iter); -} - -function downloader ( ) { - var opts = { - model: decoders.models.G4Download - , json: function (o) { - return o; - } - , payload: function (o) { - return o; - } - }; - return decoders(opts); -} - -function downloadProtobuf (msg, topic, downloads, ctx) { - var b = new Buffer(msg, 'binary'); - console.log('BINARY', b.length, b.toString('hex')); - var packet; - try { - packet = downloads.parse(b); - if (!packet.type) { - packet.type = topic; - - } - console.log('DOWNLOAD msg', msg.length, packet); - console.log('download SGV', packet.sgv[0]); - console.log('download_timestamp', packet.download_timestamp, new Date(Date.parse(packet.download_timestamp))); - console.log('WRITE TO MONGO'); - var download_timestamp = moment(packet.download_timestamp); - if (packet.download_status === 0) { - es.readArray(sgvSensorMerge(packet)).pipe(ctx.entries.persist(function empty(err, result) { - console.log('DONE WRITING MERGED SGV TO MONGO', err, result); - })); - - iter_mqtt_record_stream(packet, 'cal', toCal) - .pipe(ctx.entries.persist(function empty(err, result) { - console.log('DONE WRITING Cal TO MONGO', err, result.length); - })); - iter_mqtt_record_stream(packet, 'meter', toMeter) - .pipe(ctx.entries.persist(function empty(err, result) { - console.log('DONE WRITING Meter TO MONGO', err, result.length); - })); - } - packet.type = 'download'; - ctx.devicestatus.create({ - uploaderBattery: packet.uploader_battery, - created_at: download_timestamp.toISOString() - }, function empty(err, result) { - console.log('DONE WRITING TO MONGO devicestatus ', result, err); - }); - - ctx.entries.create([ packet ], function empty(err) { - if (err) { - console.log('Error writting to mongo: ', err); - } else { - console.log('Download written to mongo: ', packet); - } - }); - } catch (e) { - console.log('DID NOT PARSE', e); - } -} - -function toSGV (proto, vars) { - vars.sgv = proto.sgv_mgdl; - vars.direction = direction(proto.trend); - vars.noise = proto.noise; - vars.type = 'sgv'; - return vars; -} - -function toCal (proto, vars) { - vars.slope = proto.slope; - vars.intercept = proto.intercept; - vars.scale = proto.scale; - vars.type = 'cal'; - return vars; -} - -function toSensor (proto, vars) { - vars.filtered = new Long(proto.filtered).toInt(); - vars.unfiltered = new Long(proto.unfiltered).toInt(); - vars.rssi = proto.rssi; - vars.type = 'sensor'; - return vars; -} - -function toMeter (proto, result) { - result.type = 'mbg'; - result.mbg = proto.mbg || proto.meter_bg_mgdl; - return result; -} - -function toTimestamp (proto, receiver_time, download_time) { - var record_offset = receiver_time - proto.sys_timestamp_sec; - var record_time = download_time.clone( ).subtract(record_offset, 'second'); - var obj = { - device: 'dexcom' - , date: record_time.unix() * 1000 - , dateString: record_time.format( ) - }; - return obj; -} - -function timestampFactory (packet) { - var receiver_time = packet.receiver_system_time_sec; - var download_time = moment(packet.download_timestamp); - function timestamp (item) { - return toTimestamp(item, receiver_time, download_time.clone( )); - } - return timestamp; -} - -function timeSort (a, b) { - return a.date - b.date; -} - -function sgvSensorMerge (packet) { - var timestamp = timestampFactory(packet); - var sgvs = (packet['sgv'] || []).map(function(sgv) { - var timestamped = timestamp(sgv); - return toSGV(sgv, timestamped); - }).sort(timeSort); - - var sensors = (packet['sensor'] || []).map(function(sensor) { - var timestamped = timestamp(sensor); - return toSensor(sensor, timestamped); - }).sort(timeSort); - - //based on com.nightscout.core.dexcom.Utils#mergeGlucoseDataRecords - var merged = [] - , sgvsLength = sgvs.length - , sensorsLength = sensors.length; - - if (sgvsLength >= 0 && sensorsLength === 0) { - merged = sgvs; - } else { - var smallerLength = Math.min(sgvsLength, sensorsLength); - for (var i = 1; i <= smallerLength; i++) { - var sgv = sgvs[sgvsLength - i]; - var sensor = sensors[sensorsLength - i]; - if (sgv && sensor && Math.abs(sgv.date - sensor.date) < 10000) { - //timestamps are close so merge - sgv.filtered = sensor.filtered; - sgv.unfiltered = sensor.unfiltered; - sgv.rssi = sensor.rssi; - merged.push(sgv); - } else { - console.info('mismatch or missing, sgv: ', sgv, ' sensor: ', sensor); - //timestamps aren't close enough so add both - if (sgv) { merged.push(sgv); } - //but the sensor will become and sgv now - if (sensor) { - sensor.type = 'sgv'; - merged.push(sensor); - } - } - } - - //any extra sgvs? - if (sgvsLength > smallerLength) { - for (var j = 0; j < sgvsLength - smallerLength; j++) { - var extraSGV = sgvs[j]; - merged.push(extraSGV); - } - } - - //any extra sensors? - if (sensorsLength > smallerLength) { - for (var k = 0; k < sensorsLength - smallerLength; k++) { - var extraSensor = sensors[k]; - //from now on we consider it a sgv - extraSensor.type = 'sgv'; - merged.push(extraSensor); - } - } - - } - - return merged; -} - -function iter_mqtt_record_stream (packet, prop, sync) { - var list = packet[prop]; - console.log('incoming', prop, (list || [ ]).length); - var stream = es.readArray(list || [ ]); - var receiver_time = packet.receiver_system_time_sec; - var download_time = moment(packet.download_timestamp); - function map(item, next) { - var timestamped = toTimestamp(item, receiver_time, download_time.clone( )); - var r = sync(item, timestamped); - if (!('type' in r)) { - r.type = prop; - } - console.log('ITEM', item, 'TO', prop, r); - next(null, r); - } - return stream.pipe(es.map(map)); -} - -init.downloadProtobuf = downloadProtobuf; -module.exports = init; diff --git a/lib/notifications.js b/lib/notifications.js index 236c4848499..4bd5481ffb3 100644 --- a/lib/notifications.js +++ b/lib/notifications.js @@ -1,12 +1,12 @@ 'use strict'; var _ = require('lodash'); -var levels = require('./levels'); - var THIRTY_MINUTES = 30 * 60 * 1000; +var DEFAULT_GROUPS = ['default']; -var Alarm = function(level, label) { +var Alarm = function(level, group, label) { this.level = level; + this.group = group; this.label = label; this.silenceTime = THIRTY_MINUTES; this.lastAckTime = 0; @@ -16,17 +16,18 @@ var Alarm = function(level, label) { var alarms = {}; function init (env, ctx) { + function notifications () { return notifications; } - function getAlarm (level) { - var alarm = alarms[level]; + function getAlarm (level, group) { + var key = level + '-' + group; + var alarm = alarms[key]; if (!alarm) { - //TODO: Announcement hack a1/a2 - var display = level.indexOf && level.indexOf('a') === 0 ? 'Announcement:' + level : levels.toDisplay(level); - alarm = new Alarm(level, display); - alarms[level] = alarm; + var display = group === 'default' ? ctx.levels.toDisplay(level) : group + ':' + level; + alarm = new Alarm(level, group, display); + alarms[key] = alarm; } return alarm; @@ -35,43 +36,41 @@ function init (env, ctx) { //should only be used when auto acking the alarms after going back in range or when an error corrects //setting the silence time to 1ms so the alarm will be re-triggered as soon as the condition changes //since this wasn't ack'd by a user action - function autoAckAlarms() { + function autoAckAlarms (group) { var sendClear = false; - for (var level = 1; level <=2; level++) { - var alarm = getAlarm(level); + for (var level = 1; level <= 2; level++) { + var alarm = getAlarm(level, group); if (alarm.lastEmitTime) { - console.info('auto acking ' + alarm.level); - notifications.ack(alarm.level, 1); + console.info('auto acking ' + alarm.level, ' - ', group); + notifications.ack(alarm.level, group, 1); sendClear = true; } } if (sendClear) { - var notify = {clear: true, title: 'All Clear', message: 'Auto ack\'d alarm(s)'}; + var notify = { clear: true, title: 'All Clear', message: 'Auto ack\'d alarm(s)', group: group }; ctx.bus.emit('notification', notify); logEmitEvent(notify); } } function emitNotification (notify) { - //TODO: Announcement hack a1/a2 - var level = notify.isAnnouncement ? 'a' + notify.level : notify.level; - var alarm = getAlarm(level); - if (ctx.data.lastUpdated > alarm.lastAckTime + alarm.silenceTime) { + var alarm = getAlarm(notify.level, notify.group); + if (ctx.ddata.lastUpdated > alarm.lastAckTime + alarm.silenceTime) { ctx.bus.emit('notification', notify); - alarm.lastEmitTime = ctx.data.lastUpdated; + alarm.lastEmitTime = ctx.ddata.lastUpdated; logEmitEvent(notify); } else { - console.log(alarm.label + ' alarm is silenced for ' + Math.floor((alarm.silenceTime - (ctx.data.lastUpdated - alarm.lastAckTime)) / 60000) + ' minutes more'); + console.log(alarm.label + ' alarm is silenced for ' + Math.floor((alarm.silenceTime - (ctx.ddata.lastUpdated - alarm.lastAckTime)) / 60000) + ' minutes more'); } } var requests = {}; - notifications.initRequests = function initRequests ( ) { - requests = { notifies: [] , snoozes: []}; + notifications.initRequests = function initRequests () { + requests = { notifies: [], snoozes: [] }; }; notifications.initRequests(); @@ -80,21 +79,26 @@ function init (env, ctx) { * Find the first URGENT or first WARN * @returns a notification or undefined */ - notifications.findHighestAlarm = function findHighestAlarm ( ) { - return _.find(requests.notifies, {level: levels.URGENT}) || _.find(requests.notifies, {level: levels.WARN}); + notifications.findHighestAlarm = function findHighestAlarm (group) { + group = group || 'default'; + var filtered = _.filter(requests.notifies, { group: group }); + return _.find(filtered, { level: ctx.levels.URGENT }) || _.find(filtered, { level: ctx.levels.WARN }); }; - notifications.findUnSnoozeable = function findUnSnoozeable ( ) { - return _.filter(requests.notifies, function (notify) { - return notify.level <= levels.INFO || notify.isAnnouncement; + notifications.findUnSnoozeable = function findUnSnoozeable () { + return _.filter(requests.notifies, function(notify) { + return notify.level <= ctx.levels.INFO || notify.isAnnouncement; }); }; notifications.snoozedBy = function snoozedBy (notify) { if (notify.isAnnouncement) { return false; } - if (_.isEmpty(requests.snoozes)) { return false; } - var byLevel = _.filter(requests.snoozes, function checkSnooze (snooze) { + var filtered = _.filter(requests.snoozes, { group: notify.group }); + + if (_.isEmpty(filtered)) { return false; } + + var byLevel = _.filter(filtered, function checkSnooze (snooze) { return snooze.level >= notify.level; }); var sorted = _.sortBy(byLevel, 'lengthMills'); @@ -103,10 +107,13 @@ function init (env, ctx) { }; notifications.requestNotify = function requestNotify (notify) { - if (!notify.hasOwnProperty('level') || !notify.title || !notify.message || !notify.plugin) { + if (!Object.prototype.hasOwnProperty.call(notify, 'level') || !notify.title || !notify.message || !notify.plugin) { console.error(new Error('Unable to request notification, since the notify isn\'t complete: ' + JSON.stringify(notify))); return; } + + notify.group = notify.group || 'default'; + requests.notifies.push(notify); }; @@ -115,38 +122,58 @@ function init (env, ctx) { console.error(new Error('Unable to request snooze, since the snooze isn\'t complete: ' + JSON.stringify(snooze))); return; } + + snooze.group = snooze.group || 'default'; + requests.snoozes.push(snooze); }; - notifications.process = function process ( ) { - var highestAlarm = notifications.findHighestAlarm(); + notifications.process = function process () { + + var notifyGroups = _.map(requests.notifies, function eachNotify (notify) { + return notify.group; + }); + + var alarmGroups = _.map(_.values(alarms), function eachAlarm (alarm) { + return alarm.group; + }); - if (highestAlarm) { - var snoozedBy = notifications.snoozedBy(highestAlarm); - if (snoozedBy) { - logSnoozingEvent(highestAlarm, snoozedBy); - notifications.ack(snoozedBy.level, snoozedBy.lengthMills, true); + var groups = _.uniq(notifyGroups.concat(alarmGroups)); + + if (_.isEmpty(groups)) { + groups = DEFAULT_GROUPS.slice(); + } + + _.each(groups, function eachGroup (group) { + var highestAlarm = notifications.findHighestAlarm(group); + + if (highestAlarm) { + var snoozedBy = notifications.snoozedBy(highestAlarm, group); + if (snoozedBy) { + logSnoozingEvent(highestAlarm, snoozedBy); + notifications.ack(snoozedBy.level, group, snoozedBy.lengthMills, true); + } else { + emitNotification(highestAlarm); + } } else { - emitNotification(highestAlarm); + autoAckAlarms(group); } - } else { - autoAckAlarms(); - } + }); notifications.findUnSnoozeable().forEach(function eachInfo (notify) { emitNotification(notify); }); }; - notifications.ack = function ack (level, time, sendClear) { - var alarm = getAlarm(level); + notifications.ack = function ack (level, group, time, sendClear) { + var alarm = getAlarm(level, group); if (!alarm) { - console.warn('Got an ack for an unknown alarm time'); + console.warn('Got an ack for an unknown alarm time, level:', level, ', group:', group); return; } if (Date.now() < alarm.lastAckTime + alarm.silenceTime) { - console.warn('Alarm has already been snoozed, don\'t snooze it again'); + console.warn('Alarm has already been snoozed, don\'t snooze it again, level:', level, ', group:', group); return; } @@ -155,20 +182,29 @@ function init (env, ctx) { delete alarm.lastEmitTime; if (level === 2) { - notifications.ack(1, time); + notifications.ack(1, group, time); } + /* + * TODO: modify with a local clear, this will clear all connected clients, + * globally + */ if (sendClear) { var notify = { - clear: true, title: 'All Clear', message: levels.toDisplay(level) + ' was ack\'d' + clear: true + , title: 'All Clear' + , message: group + ' - ' + ctx.levels.toDisplay(level) + ' was ack\'d' + , group: group }; + // When web client sends ack, this translates the websocket message into + // an event on our internal bus. ctx.bus.emit('notification', notify); logEmitEvent(notify); } }; - function ifTestModeThen(callback) { + function ifTestModeThen (callback) { if (env.testMode) { return callback(); } else { @@ -176,16 +212,17 @@ function init (env, ctx) { } } - notifications.resetStateForTests = function resetStateForTests ( ) { - ifTestModeThen(function doResetStateForTests ( ) { + notifications.resetStateForTests = function resetStateForTests () { + ifTestModeThen(function doResetStateForTests () { console.info('resetting notifications state for tests'); alarms = {}; }); }; - notifications.getAlarmForTests = function getAlarmForTests (level) { + notifications.getAlarmForTests = function getAlarmForTests (level, group) { return ifTestModeThen(function doResetStateForTests () { - var alarm = getAlarm(level); + group = group || 'default'; + var alarm = getAlarm(level, group); console.info('got alarm for tests: ', alarm); return alarm; }); @@ -193,9 +230,10 @@ function init (env, ctx) { function notifyToView (notify) { return { - level: levels.toDisplay(notify.level) + level: ctx.levels.toDisplay(notify.level) , title: notify.title , message: notify.message + , group: notify.group , plugin: notify.plugin ? notify.plugin.name : '' , debug: notify.debug }; @@ -203,21 +241,22 @@ function init (env, ctx) { function snoozeToView (snooze) { return { - level: levels.toDisplay(snooze.level) + level: ctx.levels.toDisplay(snooze.level) , title: snooze.title , message: snooze.message + , group: snooze.group }; } - function logEmitEvent(notify) { - var type = notify.level >= levels.WARN ? 'ALARM' : (notify.clear ? 'ALL CLEAR' : 'NOTIFICATION'); + function logEmitEvent (notify) { + var type = notify.level >= ctx.levels.WARN ? 'ALARM' : (notify.clear ? 'ALL CLEAR' : 'NOTIFICATION'); console.info([ logTimestamp() + '\tEMITTING ' + type + ':' , ' ' + JSON.stringify(notifyToView(notify)) ].join('\n')); } - function logSnoozingEvent(highestAlarm, snoozedBy) { + function logSnoozingEvent (highestAlarm, snoozedBy) { console.info([ logTimestamp() + '\tSNOOZING ALARM:' , ' ' + JSON.stringify(notifyToView(highestAlarm)) @@ -227,11 +266,11 @@ function init (env, ctx) { } //TODO: we need a common logger, but until then... - function logTimestamp ( ) { + function logTimestamp () { return (new Date).toISOString(); } return notifications(); } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/alexa.js b/lib/plugins/alexa.js new file mode 100644 index 00000000000..25b13d0596d --- /dev/null +++ b/lib/plugins/alexa.js @@ -0,0 +1,110 @@ +var _ = require('lodash'); +var async = require('async'); + +function init () { + console.log('Configuring Alexa...'); + function alexa() { + return alexa; + } + var intentHandlers = {}; + var rollup = {}; + + // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. + alexa.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { + if (!intentHandlers[intent]) { + intentHandlers[intent] = {}; + } + if (metrics) { + for (var i = 0, len = metrics.length; i < len; i++) { + if (!intentHandlers[intent][metrics[i]]) { + intentHandlers[intent][metrics[i]] = {}; + } + console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); + intentHandlers[intent][metrics[i]].handler = handler; + } + } else { + console.log('Storing handler for intent \'' + intent + '\''); + intentHandlers[intent].handler = handler; + } + }; + + // This function retrieves a handler based on the intent name and metric requested. + alexa.getIntentHandler = function getIntentHandler(intentName, metric) { + if (metric === undefined) { + console.log('Looking for handler for intent \'' + intentName + '\''); + if (intentName + && intentHandlers[intentName] + && intentHandlers[intentName].handler + ) { + console.log('Found!'); + return intentHandlers[intentName].handler; + } + } else { + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); + if (intentName + && intentHandlers[intentName] + && intentHandlers[intentName][metric] + && intentHandlers[intentName][metric].handler + ) { + console.log('Found!'); + return intentHandlers[intentName][metric].handler + } + } + + console.log('Not found!'); + return null; + }; + + alexa.addToRollup = function(rollupGroup, handler, rollupName) { + if (!rollup[rollupGroup]) { + console.log('Creating the rollup group: ', rollupGroup); + rollup[rollupGroup] = []; + } + rollup[rollupGroup].push({handler: handler, name: rollupName}); + }; + + alexa.getRollup = function(rollupGroup, sbx, slots, locale, callback) { + var handlers = _.map(rollup[rollupGroup], 'handler'); + console.log('Rollup array for ', rollupGroup); + console.log(rollup[rollupGroup]); + var nHandlers = []; + _.each(handlers, function (handler) { + nHandlers.push(handler.bind(null, slots, sbx)); + }); + async.parallelLimit(nHandlers, 10, function(err, results) { + if (err) { + console.error('Error: ', err); + } + callback(_.map(_.orderBy(results, ['priority'], ['asc']), 'results').join(' ')); + }); + }; + + // This creates the expected alexa response + alexa.buildSpeechletResponse = function buildSpeechletResponse(title, output, repromptText, shouldEndSession) { + return { + version: '1.0', + response: { + outputSpeech: { + type: 'PlainText', + text: output + }, + card: { + type: 'Simple', + title: title, + content: output + }, + reprompt: { + outputSpeech: { + type: 'PlainText', + text: repromptText + } + }, + shouldEndSession: shouldEndSession + } + }; + }; + + return alexa; +} + +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/ar2.js b/lib/plugins/ar2.js index 53a1f6f6361..31ab4766f50 100644 --- a/lib/plugins/ar2.js +++ b/lib/plugins/ar2.js @@ -1,10 +1,7 @@ 'use strict'; var _ = require('lodash'); -var levels = require('../levels'); var times = require('../times'); -var rawbg = require('./rawbg')(); -var delta = require('./delta')(); var BG_REF = 140; //Central tendency var BG_MIN = 36; //Not 39, but why? @@ -17,7 +14,9 @@ var AR = [-0.723, 1.716]; //TODO: move this to css var AR2_COLOR = 'cyan'; -function init() { +function init (ctx) { + var translate = ctx.language.translate; + var moment = ctx.moment; var ar2 = { name: 'ar2' @@ -25,88 +24,71 @@ function init() { , pluginType: 'forecast' }; + function buildTitle (prop, sbx) { + var rangeLabel = prop.eventName ? sbx.translate(prop.eventName, { ci: true }).toUpperCase() : sbx.translate('Check BG'); + var title = sbx.levels.toDisplay(prop.level) + ', ' + rangeLabel; + + var sgv = sbx.lastScaledSGV(); + if (sgv > sbx.scaleMgdl(sbx.settings.thresholds.bgTargetBottom) && sgv < sbx.scaleMgdl(sbx.settings.thresholds.bgTargetTop)) { + title += ' ' + sbx.translate('predicted'); + } + return title; + } + ar2.setProperties = function setProperties (sbx) { - sbx.offerProperty('ar2', function setAR2 ( ) { + sbx.offerProperty('ar2', function setAR2 () { var prop = { - forecast: ar2.forecast(sbx.data.sgvs, sbx) - , rawForecast: rawForecast(sbx) + forecast: ar2.forecast(sbx) }; - var result = checkForecast(prop.forecast, sbx) || checkForecast(prop.rawForecast, sbx, true); + var result = checkForecast(prop.forecast, sbx); if (result) { prop.level = result.level; prop.eventName = result.eventName; } - prop.usingRaw = result && result.usingRaw || false; - - var predicted = prop.usingRaw ? prop.rawForecast && prop.rawForecast.predicted : prop.forecast && prop.forecast.predicted; - var scaled = predicted && _.map(predicted, function(p) { return sbx.scaleEntry(p) } ); + var predicted = prop.forecast && prop.forecast.predicted; + var scaled = predicted && _.map(predicted, function(p) { return sbx.scaleEntry(p) }); if (scaled && scaled.length >= 3) { - prop.displayLine = (prop.usingRaw ? 'Raw BG' : 'BG') + ' 15m: ' + scaled[2] + ' ' + sbx.unitsLabel; + prop.displayLine = 'BG 15m: ' + scaled[2] + ' ' + sbx.unitsLabel; } - return prop; }); }; ar2.checkNotifications = function checkNotifications (sbx) { - if (Date.now() - sbx.lastSGVMills() > times.mins(10).msecs) { + if (sbx.time - sbx.lastSGVMills() > times.mins(10).msecs) { return; } var prop = sbx.properties.ar2; - if (prop) { - sbx.notifications.requestNotify({ + if (prop && prop.level) { + const notify = { level: prop.level , title: buildTitle(prop, sbx) , message: sbx.buildDefaultMessage() , eventName: prop.eventName - , pushoverSound: pushoverSound(prop) + , pushoverSound: pushoverSound(prop, sbx.levels) , plugin: ar2 , debug: buildDebug(prop, sbx) - }); + }; + sbx.notifications.requestNotify(notify); } }; - function rawForecast (sbx) { - var rawSGVs; - - if (useRaw(sbx)) { - //TODO:OnlyOneCal - currently we only load the last cal, so we can't ignore future data - var cal = _.last(sbx.data.cals); - if (cal) { - rawSGVs = _.map(_.takeRight(sbx.data.sgvs, 2), function eachSGV(sgv) { - var rawResult = rawbg.calc(sgv, cal); - - //ignore when raw is 0, and use BG_MIN if raw is lower - var rawY = rawResult === 0 ? 0 : Math.max(rawResult, BG_MIN); - - return { mills: sgv.mills, mgdl: rawY }; - }); - } - } - - return ar2.forecast(rawSGVs, sbx); - } - - function useRaw (sbx) { - return sbx.properties.rawbg && sbx.extendedSettings.useRaw !== undefined && (sbx.extendedSettings.useRaw === true || sbx.extendedSettings.useRaw.toLowerCase() === 'true'); - } - - ar2.forecast = function forecast (sgvs, sbx) { + ar2.forecast = function forecast (sbx) { var result = { predicted: [] , avgLoss: 0 }; - if (!okToForecast(sgvs)) { + if (!okToForecast(sbx)) { return result; } @@ -114,7 +96,7 @@ function init() { result.predicted = _.reduce( new Array(6) //only 6 points are used for calculating avgLoss , pushPoint - , initAR2(sgvs, sbx) + , initAR2(sbx) ).points; // compute current loss @@ -126,28 +108,19 @@ function init() { return result; }; - ar2.updateVisualisation = function updateVisualisation(sbx) { - sbx.pluginBase.addForecastPoints(ar2.forecastCone(sbx)); + ar2.updateVisualisation = function updateVisualisation (sbx) { + sbx.pluginBase.addForecastPoints(ar2.forecastCone(sbx), { type: 'ar2', label: 'AR2 Forecast' }); }; ar2.forecastCone = function forecastCone (sbx) { - var sgvs = sbx.data.sgvs; - - if (!okToForecast(sgvs)) { - return []; - } - - var current = sgvs[sgvs.length - 1]; - var prev = sgvs[sgvs.length - 2]; - - if (!current || current.mgdl < BG_MIN || !prev || prev.mgdl < BG_MIN) { + if (!okToForecast(sbx)) { return []; } var coneFactor = getConeFactor(sbx); - function pushConePoints(result, step) { + function pushConePoints (result, step) { var next = incrementAR2(result); //offset from points so they are at a unique time @@ -168,32 +141,75 @@ function init() { var result = _.reduce( [0.020, 0.041, 0.061, 0.081, 0.099, 0.116, 0.132, 0.146, 0.159, 0.171, 0.182, 0.192, 0.201] , pushConePoints - , initAR2(sgvs, sbx) + , initAR2(sbx) ); return result.points; }; + function virtAsstAr2Handler (next, slots, sbx) { + var predicted = _.get(sbx, 'properties.ar2.forecast.predicted'); + if (predicted) { + var forecast = predicted; + var max = forecast[0].mgdl; + var min = forecast[0].mgdl; + var maxForecastMills = forecast[0].mills; + for (var i = 1, len = forecast.length; i < len; i++) { + if (forecast[i].mgdl > max) { + max = forecast[i].mgdl; + } + if (forecast[i].mgdl < min) { + min = forecast[i].mgdl; + } + if (forecast[i].mills > maxForecastMills) { + maxForecastMills = forecast[i].mills; + } + } + var response = ''; + if (min === max) { + response = translate('virtAsstAR2ForecastAround', { + params: [ + max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); + } else { + response = translate('virtAsstAR2ForecastBetween', { + params: [ + min + , max + , moment(maxForecastMills).from(moment(sbx.time)) + ] + }); + } + next(translate('virtAsstTitleAR2Forecast'), response); + } else { + next(translate('virtAsstTitleAR2Forecast'), translate('virtAsstUnknown')); + } + } + + ar2.virtAsst = { + intentHandlers: [{ + intent: 'MetricNow' + , metrics: ['ar2 forecast', 'forecast'] + , intentHandler: virtAsstAr2Handler + }] + }; + return ar2; } -function checkForecast(forecast, sbx, usingRaw) { +function checkForecast (forecast, sbx) { var result = undefined; - if (forecast && forecast.avgLoss > URGENT_THRESHOLD && usingRaw) { - //only send warnings for raw, and only when urgent is predicted - result = { level: levels.WARN }; - } else { - if (forecast && forecast.avgLoss > URGENT_THRESHOLD) { - result = { level: levels.URGENT }; - } else if (forecast && forecast.avgLoss > WARN_THRESHOLD) { - result = { level: levels.WARN }; - } + if (forecast && forecast.avgLoss > URGENT_THRESHOLD) { + result = { level: sbx.levels.URGENT }; + } else if (forecast && forecast.avgLoss > WARN_THRESHOLD) { + result = { level: sbx.levels.WARN }; } if (result) { result.forecast = forecast; - result.usingRaw = usingRaw; result.eventName = selectEventType(result, sbx); } @@ -201,7 +217,7 @@ function checkForecast(forecast, sbx, usingRaw) { } function selectEventType (prop, sbx) { - var predicted = prop.forecast && _.map(prop.forecast.predicted, function(p) { return sbx.scaleEntry(p) } ); + var predicted = prop.forecast && _.map(prop.forecast.predicted, function(p) { return sbx.scaleEntry(p) }); var in20mins = predicted && predicted.length >= 4 ? predicted[3] : undefined; @@ -209,9 +225,9 @@ function selectEventType (prop, sbx) { var eventName = ''; if (in20mins !== undefined) { - if (in20mins > sbx.scaleMgdl(sbx.settings.thresholds.bgTargetTop)) { + if (sbx.settings.alarmHigh && in20mins > sbx.scaleMgdl(sbx.settings.thresholds.bgTargetTop)) { eventName = 'high'; - } else if (in20mins < sbx.scaleMgdl(sbx.settings.thresholds.bgTargetBottom)) { + } else if (sbx.settings.alarmLow && in20mins < sbx.scaleMgdl(sbx.settings.thresholds.bgTargetBottom)) { eventName = 'low'; } } @@ -219,21 +235,7 @@ function selectEventType (prop, sbx) { return eventName; } -function buildTitle(prop, sbx) { - var rangeLabel = prop.eventName ? prop.eventName.toUpperCase() : 'Check BG'; - var title = levels.toDisplay(prop.level) + ', ' + rangeLabel; - - var sgv = sbx.lastScaledSGV(); - if (sgv > sbx.scaleMgdl(sbx.settings.thresholds.bgTargetBottom) && sgv < sbx.scaleMgdl(sbx.settings.thresholds.bgTargetTop)) { - title += ' predicted'; - } - if (prop.usingRaw) { - title += ' w/raw'; - } - return title; -} - -function pushoverSound (prop) { +function pushoverSound (prop, levels) { var sound; if (prop.level === levels.URGENT) { @@ -255,27 +257,28 @@ function getConeFactor (sbx) { return value; } -function okToForecast(sgvs) { - if (!sgvs || sgvs.length < 2) { +function okToForecast (sbx) { + + var bgnow = sbx.properties.bgnow; + var delta = sbx.properties.delta; + + if (!bgnow || !delta) { return false; } - var current = sgvs[sgvs.length - 1]; - var prev = sgvs[sgvs.length - 2]; - - return current && current.mgdl >= BG_MIN && prev && prev.mgdl >= BG_MIN; + return bgnow.mean >= BG_MIN && delta.mean5MinsAgo && _.isNumber(delta.mean5MinsAgo); } -function initAR2 (sgvs, sbx) { - var current = sgvs[sgvs.length - 1]; - var prev = sgvs[sgvs.length - 2]; - var mgdl5MinsAgo = delta.calc(prev, current, sbx).mgdl5MinsAgo; +function initAR2 (sbx) { + var bgnow = sbx.properties.bgnow; + var delta = sbx.properties.delta; + var mean5MinsAgo = delta.mean5MinsAgo; return { - forecastTime: current.mills || Date.now() + forecastTime: bgnow.mills || sbx.time , points: [] - , prev: Math.log(mgdl5MinsAgo / BG_REF) - , curr: Math.log(current.mgdl / BG_REF) + , prev: Math.log(mean5MinsAgo / BG_REF) + , curr: Math.log(bgnow.mean / BG_REF) }; } @@ -288,7 +291,7 @@ function incrementAR2 (result) { }; } -function pushPoint(result) { +function pushPoint (result) { var next = incrementAR2(result); next.points.push(ar2Point( @@ -299,8 +302,7 @@ function pushPoint(result) { return next; } - -function ar2Point(next, options) { +function ar2Point (next, options) { var step = options.step || 0; var coneFactor = options.coneFactor || 0; var offset = options.offset || 0; @@ -316,21 +318,15 @@ function ar2Point(next, options) { }; } - function buildDebug (prop, sbx) { return prop.forecast && { - usingRaw: prop.usingRaw - , forecast: { + forecast: { avgLoss: prop.forecast.avgLoss - , predicted: _.map(prop.forecast.predicted, function(p) { return sbx.scaleEntry(p) }).join(', ') - } - , rawForecast: prop.rawForecast && { - avgLoss: prop.rawForecast.avgLoss - , predicted: _.map(prop.rawForecast.predicted, function(p) { return sbx.scaleEntry(p) }).join(', ') + , predicted: _.map(prop.forecast.predicted, function(p) { return sbx.scaleEntry(p) }).join(', ') } }; } -function log10(val) { return Math.log(val) / Math.LN10; } +function log10 (val) { return Math.log(val) / Math.LN10; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/basalprofile.js b/lib/plugins/basalprofile.js index a948934e990..69da48c98d6 100644 --- a/lib/plugins/basalprofile.js +++ b/lib/plugins/basalprofile.js @@ -1,7 +1,12 @@ 'use strict'; var times = require('../times'); +var consts = require('../constants'); +var _ = require('lodash'); -function init() { +function init (ctx) { + var moment = ctx.moment; + + var translate = ctx.language.translate; var basal = { name: 'basal' @@ -9,6 +14,26 @@ function init() { , pluginType: 'pill-minor' }; + basal.setProperties = function setProperties (sbx) { + if (hasRequiredInfo(sbx)) { + var profile = sbx.data.profile; + var current = profile.getTempBasal(sbx.time); + + var tempMark = ''; + tempMark += current.treatment ? 'T' : ''; + tempMark += current.combobolustreatment ? 'C' : ''; + tempMark += tempMark ? ': ' : ''; + + sbx.offerProperty('basal', function setBasal() { + return { + display: tempMark + current.totalbasal.toFixed(3) + 'U' + , current: current + }; + }); + } + }; + + function hasRequiredInfo (sbx) { if (!sbx.data.profile) { return false; } @@ -33,40 +58,111 @@ function init() { } var profile = sbx.data.profile; - - var basalValue = profile.getTempBasal(sbx.time); - var tempMark = basalValue.treatment ? 'T: ' : ''; + var prop = sbx.properties.basal; + var basalValue = prop && prop.current; var tzMessage = profile.getTimezone() ? profile.getTimezone() : 'Timezone not set in profile'; - var info = [{label: 'Current basal', value: tempMark + basalValue.tempbasal + ' U'} - , {label: 'Current sensitivity', value: profile.getSensitivity(sbx.time) + ' ' + sbx.settings.units + '/ U'} - , {label: 'Current carb ratio', value: '1 U /' + profile.getCarbRatio(sbx.time) + 'g'} - , {label: 'Basal timezone', value: tzMessage} + var sensitivity = profile.getSensitivity(sbx.time); + var units = profile.getUnits(); + + if (sbx.settings.units != units) { + sensitivity *= (sbx.settings.units === 'mmol' ? (1 / consts.MMOL_TO_MGDL) : consts.MMOL_TO_MGDL); + var decimals = (sbx.settings.units === 'mmol' ? 10 : 1); + + sensitivity = Math.round(sensitivity * decimals) / decimals; + } + + var info = [{label: translate('Current basal'), value: prop.display} + , {label: translate('Sensitivity'), value: sensitivity + ' ' + sbx.settings.units + ' / U'} + , {label: translate('Current Carb Ratio'), value: '1 U / ' + profile.getCarbRatio(sbx.time) + 'g'} + , {label: translate('Basal timezone'), value: tzMessage} , {label: '------------', value: ''} - , {label: 'Active profile', value: profile.activeProfileToTime(sbx.time)} + , {label: translate('Active profile'), value: profile.activeProfileToTime(sbx.time)} ]; + var tempText, remaining; if (basalValue.treatment) { - var tempText = (basalValue.treatment.percent ? (basalValue.treatment.percent > 0 ? '+' : '') + basalValue.treatment.percent + '%' : '') + (basalValue.treatment.absolute ? basalValue.treatment.absolute + 'U' : ''); - var remaining = parseInt(basalValue.treatment.duration - times.msecs(sbx.time - basalValue.treatment.mills).mins); + tempText = basalValue.treatment.percent ? (basalValue.treatment.percent > 0 ? '+' : '') + basalValue.treatment.percent + '%' : + !isNaN(basalValue.treatment.absolute) ? basalValue.treatment.absolute + 'U/h' : ''; + remaining = parseInt(basalValue.treatment.duration - times.msecs(sbx.time - basalValue.treatment.mills).mins); + info.push({label: '------------', value: ''}); + info.push({label: translate('Active temp basal'), value: tempText}); + info.push({label: translate('Active temp basal start'), value: new Date(basalValue.treatment.mills).toLocaleString()}); + info.push({label: translate('Active temp basal duration'), value: parseInt(basalValue.treatment.duration) + ' ' + translate('mins')}); + info.push({label: translate('Active temp basal remaining'), value: remaining + ' ' + translate('mins')}); + info.push({label: translate('Basal profile value'), value: basalValue.basal.toFixed(3) + ' U'}); + } + + if (basalValue.combobolustreatment) { + tempText = (basalValue.combobolustreatment.relative ? '+' + basalValue.combobolustreatment.relative + 'U/h' : ''); + remaining = parseInt(basalValue.combobolustreatment.duration - times.msecs(sbx.time - basalValue.combobolustreatment.mills).mins); info.push({label: '------------', value: ''}); - info.push({label: 'Active temp basal', value: tempText}); - info.push({label: 'Active temp basal start', value: new Date(basalValue.treatment.mills).toLocaleString()}); - info.push({label: 'Active temp basal duration', value: parseInt(basalValue.treatment.duration) + ' min'}); - info.push({label: 'Active temp basal remaining', value: remaining + ' min'}); + info.push({label: translate('Active combo bolus'), value: tempText}); + info.push({label: translate('Active combo bolus start'), value: new Date(basalValue.combobolustreatment.mills).toLocaleString()}); + info.push({label: translate('Active combo bolus duration'), value: parseInt(basalValue.combobolustreatment.duration) + ' ' + translate('mins')}); + info.push({label: translate('Active combo bolus remaining'), value: remaining + ' ' + translate('mins')}); } sbx.pluginBase.updatePillText(basal, { - value: tempMark + basalValue.tempbasal + 'U' - , label: 'BASAL' + value: prop.display + , label: translate('BASAL') , info: info }); }; + function basalMessage(slots, sbx) { + var basalValue = sbx.data.profile.getTempBasal(sbx.time); + var response = translate('virtAsstUnknown'); + var pwd = _.get(slots, 'pwd.value'); + var preamble = pwd ? translate('virtAsstPreamble3person', { + params: [ + pwd + ] + }) : translate('virtAsstPreamble'); + if (basalValue.treatment) { + var minutesLeft = moment(basalValue.treatment.endmills).from(moment(sbx.time)); + response = translate('virtAsstBasalTemp', { + params: [ + preamble, + basalValue.totalbasal, + minutesLeft + ] + }); + } else { + response = translate('virtAsstBasal', { + params: [ + preamble, + basalValue.totalbasal + ] + }); + } + return response; + } + + function virtAsstRollupCurrentBasalHandler (slots, sbx, callback) { + callback(null, {results: basalMessage(slots, sbx), priority: 1}); + } + + function virtAsstCurrentBasalhandler (next, slots, sbx) { + next(translate('virtAsstTitleCurrentBasal'), basalMessage(slots, sbx)); + } + + basal.virtAsst = { + rollupHandlers: [{ + rollupGroup: 'Status' + , rollupName: 'current basal' + , rollupHandler: virtAsstRollupCurrentBasalHandler + }], + intentHandlers: [{ + intent: 'MetricNow' + , metrics: ['basal', 'current basal'] + , intentHandler: virtAsstCurrentBasalhandler + }] + }; + return basal; } - -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/batteryage.js b/lib/plugins/batteryage.js new file mode 100644 index 00000000000..941cb3199d7 --- /dev/null +++ b/lib/plugins/batteryage.js @@ -0,0 +1,156 @@ +'use strict'; + +var _ = require('lodash'); + +function init(ctx) { + var moment = ctx.moment; + var translate = ctx.language.translate; + var levels = ctx.levels; + + var bage = { + name: 'bage' + , label: 'Pump Battery Age' + , pluginType: 'pill-minor' + }; + + bage.getPrefs = function getPrefs(sbx) { + return { + info: sbx.extendedSettings.info || 312 + , warn: sbx.extendedSettings.warn || 336 + , urgent: sbx.extendedSettings.urgent || 360 + , display: sbx.extendedSettings.display ? sbx.extendedSettings.display : 'days' + , enableAlerts: sbx.extendedSettings.enableAlerts || false + }; + }; + + bage.setProperties = function setProperties (sbx) { + sbx.offerProperty('bage', function setProp ( ) { + return bage.findLatestTimeChange(sbx); + }); + }; + + bage.checkNotifications = function checkNotifications(sbx) { + var batteryInfo = sbx.properties.bage; + + if (batteryInfo.notification) { + var notification = _.extend({}, batteryInfo.notification, { + plugin: bage + , debug: { + age: batteryInfo.age + } + }); + sbx.notifications.requestNotify(notification); + } + }; + + bage.findLatestTimeChange = function findLatestTimeChange(sbx) { + + var prefs = bage.getPrefs(sbx); + + var batteryInfo = { + found: false + , age: 0 + , treatmentDate: null + , checkForAlert: false + }; + + var prevDate = 0; + + _.each(sbx.data.batteryTreatments, function eachTreatment (treatment) { + var treatmentDate = treatment.mills; + if (treatmentDate > prevDate && treatmentDate <= sbx.time) { + + prevDate = treatmentDate; + batteryInfo.treatmentDate = treatmentDate; + + var a = moment(sbx.time); + var b = moment(batteryInfo.treatmentDate); + var days = a.diff(b,'days'); + var hours = a.diff(b,'hours') - days * 24; + var age = a.diff(b,'hours'); + + if (!batteryInfo.found || (age >= 0 && age < batteryInfo.age)) { + batteryInfo.found = true; + batteryInfo.age = age; + batteryInfo.days = days; + batteryInfo.hours = hours; + batteryInfo.notes = treatment.notes; + batteryInfo.minFractions = a.diff(b,'minutes') - age * 60; + } + } + }); + + + batteryInfo.level = levels.NONE; + + var sound = 'incoming'; + var message; + var sendNotification = false; + + if (batteryInfo.age >= prefs.urgent) { + sendNotification = batteryInfo.age === prefs.urgent; + message = translate('Pump Battery change overdue!'); + sound = 'persistent'; + batteryInfo.level = levels.URGENT; + } else if (batteryInfo.age >= prefs.warn) { + sendNotification = batteryInfo.age === prefs.warn; + message = translate('Time to change pump battery'); + batteryInfo.level = levels.WARN; + } else if (batteryInfo.age >= prefs.info) { + sendNotification = batteryInfo.age === prefs.info; + message = 'Change pump battery soon'; + batteryInfo.level = levels.INFO; + } + + if (prefs.display === 'days' && batteryInfo.found) { + batteryInfo.display = ''; + if (batteryInfo.age >= 24) { + batteryInfo.display += batteryInfo.days + 'd'; + } + batteryInfo.display += batteryInfo.hours + 'h'; + } else { + batteryInfo.display = batteryInfo.found ? batteryInfo.age + 'h' : 'n/a '; + } + + //allow for 20 minute period after a full hour during which we'll alert the user + if (prefs.enableAlerts && sendNotification && batteryInfo.minFractions <= 20) { + batteryInfo.notification = { + title: translate('Pump battery age %1 hours', { params: [batteryInfo.age] }) + , message: message + , pushoverSound: sound + , level: batteryInfo.level + , group: 'BAGE' + }; + } + + return batteryInfo; + }; + + bage.updateVisualisation = function updateVisualisation (sbx) { + + var batteryInfo = sbx.properties.bage; + + var info = [{ label: translate('Inserted'), value: new Date(batteryInfo.treatmentDate).toLocaleString() }]; + + if (!_.isEmpty(batteryInfo.notes)) { + info.push({label: translate('Notes') + ':', value: batteryInfo.notes}); + } + + var statusClass = null; + if (batteryInfo.level === levels.URGENT) { + statusClass = 'urgent'; + } else if (batteryInfo.level === levels.WARN) { + statusClass = 'warn'; + } + + sbx.pluginBase.updatePillText(bage, { + value: batteryInfo.display + , label: translate('BAGE') + , info: info + , pillClass: statusClass + }); + }; + return bage; +} + +module.exports = init; diff --git a/lib/plugins/bgnow.js b/lib/plugins/bgnow.js new file mode 100644 index 00000000000..43120eeb1e1 --- /dev/null +++ b/lib/plugins/bgnow.js @@ -0,0 +1,291 @@ +'use strict'; + +var _ = require('lodash'); +var times = require('../times'); + +var offset = times.mins(2.5).msecs; +var bucketFields = ['index', 'fromMills', 'toMills']; + +function init (ctx) { + + var moment = ctx.moment; + var translate = ctx.language.translate; + var utils = require('../utils')(ctx); + + var bgnow = { + name: 'bgnow' + , label: 'BG Now' + , pluginType: 'pill-primary' + }; + + bgnow.mostRecentBucket = function mostRecentBucket (buckets) { + return _.find(buckets, function notEmpty(bucket) { + return bucket && !bucket.isEmpty; + }); + }; + + bgnow.previousBucket = function previousBucket(recent, buckets) { + var previous = null; + + if (_.isObject(recent)) { + previous = _.chain(buckets).find(function afterFirstNotEmpty(bucket) { + return bucket.mills < recent.mills && !bucket.isEmpty; + }).value(); + } + + return previous; + }; + + bgnow.setProperties = function setProperties (sbx) { + var buckets = bgnow.fillBuckets(sbx); + var recent = bgnow.mostRecentBucket(buckets); + var previous = bgnow.previousBucket(recent, buckets); + var delta = bgnow.calcDelta(recent, previous, sbx); + + sbx.offerProperty('bgnow', function setBGNow ( ) { + return _.omit(recent, bucketFields); + }); + + sbx.offerProperty('delta', function setBGNow ( ) { + return delta; + }); + + sbx.offerProperty('buckets', function setBGNow ( ) { + return buckets; + }); + }; + + bgnow.fillBuckets = function fillBuckets (sbx, opts) { + + var bucketCount = (opts && opts.bucketCount) || 4; + var bucketMins = (opts && opts.bucketMins) || 5; + var bucketMsecs = times.mins(bucketMins).msecs; + + var lastSGVMills = sbx.lastSGVMills(); + + var buckets = _.times(bucketCount, function createBucket (index) { + var fromMills = lastSGVMills - offset - (index * bucketMsecs); + return { + index: index + , fromMills: fromMills + , toMills: fromMills + bucketMsecs + , sgvs: [ ] + }; + }); + + _.takeRightWhile(sbx.data.sgvs, function addToBucket (sgv) { + + //if in the future, return true and keep taking right + if (sgv.mills > sbx.time) { + return true; + } + + var bucket = _.find(buckets, function containsSGV (bucket) { + return sgv.mills >= bucket.fromMills && sgv.mills <= bucket.toMills; + }); + + if (bucket) { + sbx.scaleEntry(sgv); + bucket.sgvs.push(sgv); + } + + return bucket; + }); + + return _.map(buckets, bgnow.analyzeBucket); + }; + + function notError (entry) { + return entry && entry.mgdl > 39; //TODO maybe lower instead of expecting dexcom? + } + + function isError (entry) { + return !entry || !entry.mgdl || entry.mgdl < 39; + } + + bgnow.analyzeBucket = function analyzeBucket (bucket) { + + if (_.isEmpty(bucket.sgvs)) { + bucket.isEmpty = true; + return bucket; + } + + var details = { }; + + var sgvs = _.filter(bucket.sgvs, notError); + + function calcMean ( ) { + var sum = 0; + _.forEach(sgvs, function eachSGV (sgv) { + sum += Number(sgv.mgdl); + }); + + return sum / sgvs.length; + } + + var mean = calcMean(sgvs); + + if (mean && _.isNumber(mean)) { + details.mean = mean; + } + + var mostRecent = _.maxBy(sgvs, 'mills'); + + if (mostRecent) { + details.last = mostRecent.mgdl; + details.mills = mostRecent.mills; + } + + var errors = _.filter(bucket.sgvs, isError); + if (!_.isEmpty(errors)) { + details.errors = errors; + } + + return _.merge(details, bucket); + }; + + bgnow.calcDelta = function calcDelta (recent, previous, sbx) { + + if (_.isEmpty(recent)) { + //console.info('No recent CGM data is available'); + return null; + } + + if (_.isEmpty(previous)) { + //console.info('previous bucket not found, not calculating delta'); + return null; + } + + var delta = { + absolute: recent.mean - previous.mean + , elapsedMins: (recent.mills - previous.mills) / times.min().msecs + }; + + delta.interpolated = delta.elapsedMins > 9; + + delta.mean5MinsAgo = delta.interpolated ? + recent.mean - delta.absolute / delta.elapsedMins * 5 : recent.mean - delta.absolute; + + delta.times = { + recent: recent.mills + , previous: previous.mills + }; + + delta.mgdl = Math.round(recent.mean - delta.mean5MinsAgo); + + delta.scaled = sbx.settings.units === 'mmol' ? + sbx.roundBGToDisplayFormat(sbx.scaleMgdl(recent.mean) - sbx.scaleMgdl(delta.mean5MinsAgo)) : delta.mgdl; + + delta.display = (delta.scaled >= 0 ? '+' : '') + delta.scaled; + + delta.previous = _.omit(previous, bucketFields); + + return delta; + + }; + + bgnow.updateVisualisation = function updateVisualisation (sbx) { + var prop = sbx.properties.bgnow; + var delta = sbx.properties.delta; + + var info = []; + var display = delta && delta.display; + + if (delta && delta.interpolated) { + display += ' *'; + info.push({label: translate('Elapsed Time'), value: Math.round(delta.elapsedMins) + ' ' + translate('mins')}); + info.push({label: translate('Absolute Delta'), value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(delta.absolute)) + ' ' + sbx.unitsLabel}); + info.push({label: translate('Interpolated'), value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(delta.mean5MinsAgo)) + ' ' + sbx.unitsLabel}); + } + + var deviceInfos = { }; + + if (prop.sgvs) { + _.forEach(prop.sgvs, function deviceAndValue(entry) { + var device = utils.deviceName(entry.device); + deviceInfos[device] = { + time: utils.timeFormat(moment(entry.mills), sbx) + , value: sbx.scaleEntry(entry) + , recent: entry + }; + }); + } + + if (delta && delta.previous && delta.previous.sgvs) { + _.forEach(delta.previous.sgvs, function deviceAndValue(entry) { + var device = utils.deviceName(entry.device); + var deviceInfo = deviceInfos[device]; + if (deviceInfo && deviceInfo.recent) { + var deviceDelta = bgnow.calcDelta( + { mills: deviceInfo.recent.mills , mean: deviceInfo.recent.mgdl} + , { mills: entry.mills, mean: entry.mgdl} + , sbx + ); + + if (deviceDelta) { + deviceInfo.delta = deviceDelta.display + } + } else { + deviceInfos[device] = { + time: utils.timeFormat(moment(entry.mills), sbx) + , value: sbx.scaleEntry(entry) + }; + } + }); + + if (_.keys(deviceInfos).length > 1) { + _.forIn(deviceInfos, function addInfo(deviceInfo, name) { + var display = deviceInfo.value; + if (deviceInfo.delta) { + display += ' ' + deviceInfo.delta; + } + + display += ' (' + deviceInfo.time + ')'; + + info.push({label: name, value: display}); + }); + } + } + + sbx.pluginBase.updatePillText({ + name: 'delta' + , label: translate('BG Delta') + , pluginType: 'pill-major' + , pillFlip: true + }, { + value: display + , label: sbx.unitsLabel + , info: _.isEmpty(info) ? null : info + }); + }; + + function virtAsstDelta(next, slots, sbx) { + var delta = sbx.properties.delta; + + next( + translate('virtAsstTitleDelta') + , translate(delta.interpolated ? 'virtAsstDeltaEstimated' : 'virtAsstDelta' + , { + params: [ + delta.display == '+0' ? '0' : delta.display + , moment(delta.times.recent).from(moment(sbx.time)) + , moment(delta.times.previous).from(moment(sbx.time)) + ] + } + ) + ); + } + + bgnow.virtAsst = { + intentHandlers: [{ + intent: "MetricNow" + , metrics: ["delta"] + , intentHandler: virtAsstDelta + }] + }; + + return bgnow; + +} + +module.exports = init; diff --git a/lib/plugins/bolus.js b/lib/plugins/bolus.js new file mode 100644 index 00000000000..bc2f9a322b9 --- /dev/null +++ b/lib/plugins/bolus.js @@ -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; \ No newline at end of file diff --git a/lib/plugins/boluswizardpreview.js b/lib/plugins/boluswizardpreview.js index fd7d012d2d1..3559d35760c 100644 --- a/lib/plugins/boluswizardpreview.js +++ b/lib/plugins/boluswizardpreview.js @@ -1,10 +1,12 @@ 'use strict'; var _ = require('lodash'); -var levels = require('../levels'); var times = require('../times'); -function init() { +function init (ctx) { + + var translate = ctx.language.translate; + var levels = ctx.levels; var bwp = { name: 'bwp' @@ -48,7 +50,7 @@ function init() { if (bwp.highSnoozedByIOB(prop, settings, sbx)) { sbx.notifications.requestSnooze({ level: levels.URGENT - , title: 'Snoozing high alarm since there is enough IOB' + , title: translate('Snoozing high alarm since there is enough IOB') , message: [sbx.propertyLine('bwp'), sbx.propertyLine('iob')].join('\n') , lengthMills: settings.snoozeLength , debug: prop @@ -60,7 +62,7 @@ function init() { sbx.notifications.requestNotify({ level: level - , title: levelLabel + ', Check BG, time to bolus?' + , title: levelLabel + ', ' + translate('Check BG, time to bolus?') , message: sbx.buildDefaultMessage() , eventName: 'bwp' , pushoverSound: sound @@ -94,7 +96,7 @@ function init() { sbx.pluginBase.updatePillText(bwp, { value: value - , label: 'BWP' + , label: translate('BWP') , info: info }); }; @@ -119,7 +121,7 @@ function init() { } var profile = sbx.data.profile; - var iob = results.iob = sbx.properties.iob.iob; + var iob = results.iob = sbx.properties.iob.iob || 0; results.effect = iob * profile.getSensitivity(sbx.time); results.outcome = scaled - results.effect; @@ -173,7 +175,7 @@ function init() { results.outcomeDisplay = sbx.roundBGToDisplayFormat(results.outcome); results.displayIOB = sbx.roundInsulinForDisplayFormat(results.iob); results.effectDisplay = sbx.roundBGToDisplayFormat(results.effect); - results.displayLine = 'BWP: ' + results.bolusEstimateDisplay + 'U'; + results.displayLine = translate('BWP') + ': ' + results.bolusEstimateDisplay + 'U'; return results; }; @@ -187,105 +189,106 @@ function init() { }; } - return bwp; - -} - -function isSGVOk (sbx) { - var lastSGVEntry = sbx.lastSGVEntry(); - return lastSGVEntry && lastSGVEntry.mgdl >= 39 && sbx.isCurrent(lastSGVEntry); -} - -function profileFieldsMissing (sbx) { - return !sbx.data.profile.getSensitivity(sbx.time) - || !sbx.data.profile.getHighBGTarget(sbx.time) - || !sbx.data.profile.getLowBGTarget(sbx.time); -} + function isSGVOk (sbx) { + var lastSGVEntry = sbx.lastSGVEntry(); + return lastSGVEntry && lastSGVEntry.mgdl >= 39 && sbx.isCurrent(lastSGVEntry); + } -function pushInfo(prop, info, sbx) { - if (prop && prop.errors) { - info.push({label: 'Notice', value: 'required info missing'}); - _.each(prop.errors, function pushError (error) { - info.push({label: ' • ', value: error}); - }); - } else if (prop) { - info.push({label: 'Insulin on Board', value: prop.displayIOB + 'U'}); - info.push({label: 'Current target', value: 'Low: '+sbx.data.profile.getLowBGTarget(sbx.time) + ' High: ' + sbx.data.profile.getHighBGTarget(sbx.time)}); - info.push({label: 'Sensitivity', value: '-' + sbx.data.profile.getSensitivity(sbx.time) + ' ' + sbx.settings.units + '/U'}); - info.push({label: 'Expected effect', value: prop.displayIOB + ' x -' + sbx.data.profile.getSensitivity(sbx.time) + ' = -' + prop.effectDisplay + ' ' + sbx.settings.units}); - info.push({label: 'Expected outcome', value: sbx.lastScaledSGV() + '-' + prop.effectDisplay + ' = ' + prop.outcomeDisplay + ' ' + sbx.settings.units}); - if (prop.bolusEstimateDisplay < 0) { - info.unshift({label: '---------', value: ''}); - var carbEquivalent = Math.ceil(Math.abs(sbx.data.profile.getCarbRatio() * prop.bolusEstimateDisplay)); - info.unshift({label: 'Carb Equivalent', value: prop.bolusEstimateDisplay + 'U * ' + sbx.data.profile.getCarbRatio() + ' = ' + carbEquivalent + 'g'}); - info.unshift({label: 'Current Carb Ratio', value: '1U / ' + sbx.data.profile.getCarbRatio() + ' g'}); - - if (prop.recentCarbs) { - info.unshift({ - label: 'Recent carbs' - , value: prop.recentCarbs.carbs + 'g @ ' + moment(prop.recentCarbs.mills).format('LT')}); - } - - if (!prop.belowLowTarget) { - info.unshift({ - label: '-BWP' - , value: 'Excess insulin equivalent ' + prop.bolusEstimateDisplay + 'U more than needed to reach low target, not accounting for carbs'}); - } + function profileFieldsMissing (sbx) { + return !sbx.data.profile.getSensitivity(sbx.time) + || !sbx.data.profile.getHighBGTarget(sbx.time) + || !sbx.data.profile.getLowBGTarget(sbx.time); + } - if (prop.belowLowTarget) { - if (prop.iob > 0) { + function pushInfo(prop, info, sbx) { + if (prop && prop.errors) { + info.push({label: translate('Notice'), value: translate('required info missing')}); + _.each(prop.errors, function pushError (error) { + info.push({label: ' • ', value: error}); + }); + } else if (prop) { + info.push({label: translate('Insulin on Board'), value: prop.displayIOB + 'U'}); + info.push({label: translate('Current target'), value: translate('Low') +': ' + sbx.data.profile.getLowBGTarget(sbx.time) + ' ' + translate('High') + ': ' + sbx.data.profile.getHighBGTarget(sbx.time)}); + info.push({label: translate('Sensitivity'), value: '-' + sbx.data.profile.getSensitivity(sbx.time) + ' ' + sbx.settings.units + '/U'}); + info.push({label: translate('Expected effect'), value: prop.displayIOB + ' x -' + sbx.data.profile.getSensitivity(sbx.time) + ' = -' + prop.effectDisplay + ' ' + sbx.settings.units}); + info.push({label: translate('Expected outcome'), value: sbx.lastScaledSGV() + '-' + prop.effectDisplay + ' = ' + prop.outcomeDisplay + ' ' + sbx.settings.units}); + if (prop.bolusEstimateDisplay < 0) { + info.unshift({label: '---------', value: ''}); + var carbEquivalent = Math.ceil(Math.abs(sbx.data.profile.getCarbRatio() * prop.bolusEstimateDisplay)); + info.unshift({label: translate('Carb Equivalent'), value: prop.bolusEstimateDisplay + 'U * ' + sbx.data.profile.getCarbRatio() + ' = ' + carbEquivalent + 'g'}); + info.unshift({label: translate('Current Carb Ratio'), value: '1U / ' + sbx.data.profile.getCarbRatio() + ' g'}); + + if (prop.recentCarbs) { info.unshift({ - label: '-BWP' - , value: 'Excess insulin equivalent ' + prop.bolusEstimateDisplay + 'U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS'}); - } else { + label: translate('Last Carbs') + , value: prop.recentCarbs.carbs + 'g @ ' + new Date(prop.recentCarbs.mills).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}); + } + + if (!prop.belowLowTarget) { info.unshift({ - label: '-BWP' - , value: prop.bolusEstimateDisplay + 'U reduction needed in active insulin to reach low target, too much basal?'}); + label: '-' + translate('BWP') + , value: translate('Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs', { params: [prop.bolusEstimateDisplay] })}); + } + + if (prop.belowLowTarget) { + if (prop.iob > 0) { + info.unshift({ + label: '-' + translate('BWP') + , value: translate('Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS', { params: [prop.bolusEstimateDisplay] })}); + } else { + info.unshift({ + label: '-' + translate('BWP') + , value: translate('%1U reduction needed in active insulin to reach low target, too much basal?', { params: [prop.bolusEstimateDisplay] })}); + } } } + } else { + info.push({label: translate('Notice'), value: translate('required info missing')}); } - } else { - info.push({label: 'Notice', value: 'required info missing'}); - } - pushTempBasalAdjustments(prop, info, sbx); -} + pushTempBasalAdjustments(prop, info, sbx); + } -function pushTempBasalAdjustments(prop, info, sbx) { - if (prop && prop.tempBasalAdjustment) { - var carbsOrBolusMessage = 'basal adjustment out of range, give carbs?'; - var sign = ''; - if (prop.tempBasalAdjustment.thirtymin > 100) { - carbsOrBolusMessage = 'basal adjustment out of range, give bolus?'; - sign = '+'; - } + function pushTempBasalAdjustments(prop, info, sbx) { + if (prop && prop.tempBasalAdjustment) { + var carbsOrBolusMessage = translate('basal adjustment out of range, give carbs?'); + var sign = ''; + if (prop.tempBasalAdjustment.thirtymin > 100) { + carbsOrBolusMessage = translate('basal adjustment out of range, give bolus?'); + sign = '+'; + } - info.push({label: '---------', value: ''}); - if (prop.aimTarget) { - info.push({label: 'Projected BG ' + prop.aimTargetString +' target', value: 'aiming at ' + prop.aimTarget + ' ' + sbx.settings.units}); - } - - if (prop.bolusEstimate > 0) { - info.push({label: 'Bolus ' + prop.bolusEstimateDisplay + ' units', value: 'or adjust basal'}); - info.push({label: 'Check BG using glucometer before correcting!', value: ''}); info.push({label: '---------', value: ''}); - } else { - info.push({label: 'Basal reduction to account ' + prop.bolusEstimateDisplay + ' units:', value: ''}); - } + if (prop.aimTarget) { + info.push({label: translate('Projected BG %1 target', { params: [translate(prop.aimTargetString)] }), value: translate('aiming at') + ' ' + prop.aimTarget + ' ' + sbx.settings.units}); + } - info.push({label: 'Current basal', value: sbx.data.profile.getBasal(sbx.time)}); - - if (prop.tempBasalAdjustment.thirtymin >= 0 && prop.tempBasalAdjustment.thirtymin <= 200) { - info.push({label: '30m temp basal', value: '' + prop.tempBasalAdjustment.thirtymin + '% (' + sign + (prop.tempBasalAdjustment.thirtymin - 100) + '%)'}); - } else { - info.push({label: '30m temp basal', value: carbsOrBolusMessage}); - } - if (prop.tempBasalAdjustment.onehour >= 0 && prop.tempBasalAdjustment.onehour <= 200) { - info.push({label: '1h temp basal', value: '' + prop.tempBasalAdjustment.onehour + '% (' + sign + (prop.tempBasalAdjustment.onehour - 100) + '%)'}); - } else { - info.push({label: '1h temp basal', value: carbsOrBolusMessage}); + if (prop.bolusEstimate > 0) { + info.push({label: translate('Bolus %1 units', { params: [prop.bolusEstimateDisplay] }), value: translate('or adjust basal')}); + info.push({label: translate('Check BG using glucometer before correcting!'), value: ''}); + info.push({label: '---------', value: ''}); + } else { + info.push({label: translate('Basal reduction to account %1 units:', { params: [prop.bolusEstimateDisplay] }), value: ''}); + } + + info.push({label: translate('Current basal'), value: sbx.data.profile.getBasal(sbx.time)}); + + if (prop.tempBasalAdjustment.thirtymin >= 0 && prop.tempBasalAdjustment.thirtymin <= 200) { + info.push({label: translate('30m temp basal'), value: '' + prop.tempBasalAdjustment.thirtymin + '% (' + sign + (prop.tempBasalAdjustment.thirtymin - 100) + '%)'}); + } else { + info.push({label: translate('30m temp basal'), value: carbsOrBolusMessage}); + } + if (prop.tempBasalAdjustment.onehour >= 0 && prop.tempBasalAdjustment.onehour <= 200) { + info.push({label: translate('1h temp basal'), value: '' + prop.tempBasalAdjustment.onehour + '% (' + sign + (prop.tempBasalAdjustment.onehour - 100) + '%)'}); + } else { + info.push({label: translate('1h temp basal'), value: carbsOrBolusMessage}); + } } } + + return bwp; + } -module.exports = init; \ No newline at end of file + +module.exports = init; diff --git a/lib/plugins/bridge.js b/lib/plugins/bridge.js index 9b8fb76fcf3..5241e9734dc 100644 --- a/lib/plugins/bridge.js +++ b/lib/plugins/bridge.js @@ -2,9 +2,12 @@ var engine = require('share2nightscout-bridge'); -function init (env) { +// Track the most recently seen record +var mostRecentRecord; + +function init (env, bus) { if (env.extendedSettings.bridge && env.extendedSettings.bridge.userName && env.extendedSettings.bridge.password) { - return create(env); + return create(env, bus); } else { console.info('Dexcom bridge not enabled'); } @@ -15,6 +18,14 @@ function bridged (entries) { if (err) { console.error('Bridge error: ', err); } else { + if (glucose) { + for (var i = 0; i < glucose.length; i++) { + if (glucose[i].date > mostRecentRecord) { + mostRecentRecord = glucose[i].date; + } + } + //console.log("DEXCOM: Most recent entry received; "+new Date(mostRecentRecord).toString()); + } entries.create(glucose, function stored (err) { if (err) { console.error('Bridge storage error: ', err); @@ -36,9 +47,17 @@ function options (env) { , minutes: env.extendedSettings.bridge.minutes || 1440 }; + var interval = env.extendedSettings.bridge.interval || 60000 * 2.6; // Default: 2.6 minutes + + if (interval < 1000 || interval > 300000) { + // Invalid interval range. Revert to default + console.error("Invalid interval set: [" + interval + "ms]. Defaulting to 2.6 minutes.") + interval = 60000 * 2.6 // 2.6 minutes + } + return { login: config - , interval: env.extendedSettings.bridge.interval || 60000 * 2.5 + , interval: interval , fetch: fetch_config , nightscout: { } , maxFailures: env.extendedSettings.bridge.maxFailures || 3 @@ -46,18 +65,70 @@ function options (env) { }; } -function create (env) { +function create (env, bus) { var bridge = { }; var opts = options(env); var interval = opts.interval; + mostRecentRecord = new Date().getTime() - opts.fetch.minutes * 60000; + bridge.startEngine = function startEngine (entries) { + opts.callback = bridged(entries); - setInterval(engine(opts), interval); + let last_run = new Date(0).getTime(); + let last_ondemand = new Date(0).getTime(); + + function should_run() { + // Time we expect to have to collect again + const msRUN_AFTER = (300+20) * 1000; + const msNow = new Date().getTime(); + + const next_entry_expected = mostRecentRecord + msRUN_AFTER; + + if (next_entry_expected > msNow) { + // we're not due to collect a new slot yet. Use interval + const ms_since_last_run = msNow - last_run; + if (ms_since_last_run < interval) { + return false; + } + + last_run = msNow; + last_ondemand = new Date(0).getTime(); + console.log("DEXCOM: Running poll"); + return true; + } + + const ms_since_last_run = msNow - last_ondemand; + + if (ms_since_last_run < interval) { + return false; + } + last_run = msNow; + last_ondemand = msNow; + console.log("DEXCOM: Data due, running extra poll"); + return true; + } + + let timer = setInterval(function () { + if (!should_run()) return; + + + opts.fetch.minutes = parseInt((new Date() - mostRecentRecord) / 60000); + opts.fetch.maxCount = parseInt((opts.fetch.minutes / 5) + 1); + opts.firstFetchCount = opts.fetch.maxCount; + console.log("Fetching Share Data: ", 'minutes', opts.fetch.minutes, 'maxCount', opts.fetch.maxCount); + engine(opts); + }, 1000 /*interval*/); + + if (bus) { + bus.on('teardown', function serverTeardown () { + clearInterval(timer); + }); + } }; return bridge; diff --git a/lib/plugins/cannulaage.js b/lib/plugins/cannulaage.js index 400e7e6794b..3b3f52eca49 100644 --- a/lib/plugins/cannulaage.js +++ b/lib/plugins/cannulaage.js @@ -1,120 +1,157 @@ 'use strict'; var _ = require('lodash'); -var moment = require('moment'); -var times = require('../times'); -var cage = { - name: 'cage' - , label: 'Cannula Age' - , pluginType: 'pill-minor' -}; +function init(ctx) { + var moment = ctx.moment; + var translate = ctx.language.translate; + var levels = ctx.levels; -function init() { - return cage; -} + var cage = { + name: 'cage' + , label: 'Cannula Age' + , pluginType: 'pill-minor' + }; -module.exports = init; + cage.getPrefs = function getPrefs (sbx) { + // CAGE_INFO = 44 CAGE_WARN=48 CAGE_URGENT=70 + return { + info: sbx.extendedSettings.info || 44 + , warn: sbx.extendedSettings.warn || 48 + , urgent: sbx.extendedSettings.urgent || 72 + , display: sbx.extendedSettings.display ? sbx.extendedSettings.display : 'hours' + , enableAlerts: sbx.extendedSettings.enableAlerts || false + }; + }; -cage.getPrefs = function getPrefs(sbx) { - // CAGE_INFO = 44 CAGE_WARN=48 CAGE_URGENT=70 - return { - 'info' : sbx.extendedSettings.info ? sbx.extendedSettings.info : 44, - 'warn' : sbx.extendedSettings.warn ? sbx.extendedSettings.warn : 48, - 'urgent' : sbx.extendedSettings.urgent ? sbx.extendedSettings.urgent : 72, - 'display' : sbx.extendedSettings.display ? sbx.extendedSettings.display : 'hours', - 'enableAlerts' : sbx.extendedSettings.enableAlerts + cage.setProperties = function setProperties (sbx) { + sbx.offerProperty('cage', function setProp ( ) { + return cage.findLatestTimeChange(sbx); + }); + }; + + cage.checkNotifications = function checkNotifications (sbx) { + var cannulaInfo = sbx.properties.cage; + + if (cannulaInfo.notification) { + var notification = _.extend({}, cannulaInfo.notification, { + plugin: cage + , debug: { + age: cannulaInfo.age + } + }); + sbx.notifications.requestNotify(notification); + } }; -}; - -cage.checkNotifications = function checkNotifications(sbx) { - - var cannulaInfo = cage.findLatestTimeChange(sbx); - var prefs = cage.getPrefs(sbx); - - if (!prefs.enableAlerts || !cannulaInfo.checkForAlert) { return; } - - var sendNotification = false; - var title = 'Cannula age ' + cannulaInfo.age +' hours'; - var sound = 'incoming'; - var msg = ', change due soon'; - var level = 1; - - if (cannulaInfo.age === Number(prefs.info)) { sendNotification = true; } - if (cannulaInfo.age === Number(prefs.warn)) { sendNotification = true; msg = ', time to change'; } - if (cannulaInfo.age === Number(prefs.urgent)) { sendNotification = true; msg = ', change overdue!'; sound = 'persistent'; level = 2;} - - if (sendNotification) { - sbx.notifications.requestNotify({ - level: level - , title: title - , message: title + msg - , pushoverSound: sound - , plugin: cage - , debug: { - cannulaInfo: cannulaInfo + + cage.findLatestTimeChange = function findLatestTimeChange (sbx) { + + var prefs = cage.getPrefs(sbx); + + var cannulaInfo = { + found: false + , age: 0 + , treatmentDate: null + , checkForAlert: false + }; + + var prevDate = 0; + + _.each(sbx.data.sitechangeTreatments, function eachTreatment (treatment) { + var treatmentDate = treatment.mills; + if (treatmentDate > prevDate && treatmentDate <= sbx.time) { + + prevDate = treatmentDate; + cannulaInfo.treatmentDate = treatmentDate; + + var a = moment(sbx.time); + var b = moment(cannulaInfo.treatmentDate); + var days = a.diff(b,'days'); + var hours = a.diff(b,'hours') - days * 24; + var age = a.diff(b,'hours'); + + if (!cannulaInfo.found || (age >= 0 && age < cannulaInfo.age)) { + cannulaInfo.found = true; + cannulaInfo.age = age; + cannulaInfo.days = days; + cannulaInfo.hours = hours; + cannulaInfo.notes = treatment.notes; + cannulaInfo.minFractions = a.diff(b,'minutes') - age * 60; + } } }); - } -}; - -cage.findLatestTimeChange = function findLatestTimeChange(sbx) { - - var returnValue = {'message':'', 'found': false, 'age': 0, 'treatmentDate': null, 'checkForAlert': false}; - - var prevDate = 0; - - _.each(sbx.data.treatments, function eachTreatment (treatment) { - var treatmentDate = treatment.mills; - if (treatment.eventType === 'Site Change' && treatmentDate > prevDate && treatmentDate <= sbx.time) { - - prevDate = treatmentDate; - returnValue.treatmentDate = treatmentDate; - - //allow for 10 minute period after a full hour during which we'll alert the user - var a = moment(sbx.time); - var b = moment(returnValue.treatmentDate); - var hours = a.diff(b,'hours'); - var minFractions = a.diff(b,'minutes') - hours * 60; - - returnValue.checkForAlert = minFractions <= 10; - - if (!returnValue.found) { - returnValue.found = true; - returnValue.age = hours; - } else { - if (hours >= 0 && hours < returnValue.age) { - returnValue.age = hours; - if (treatment.notes) { - returnValue.message = treatment.notes; - } else { - returnValue.message = ''; - } - } + + cannulaInfo.level = levels.NONE; + + var sound = 'incoming'; + var message; + var sendNotification = false; + + if (cannulaInfo.age >= prefs.urgent) { + sendNotification = cannulaInfo.age === prefs.urgent; + message = translate('Cannula change overdue!'); + sound = 'persistent'; + cannulaInfo.level = levels.URGENT; + } else if (cannulaInfo.age >= prefs.warn) { + sendNotification = cannulaInfo.age === prefs.warn; + message = translate('Time to change cannula'); + cannulaInfo.level = levels.WARN; + } else if (cannulaInfo.age >= prefs.info) { + sendNotification = cannulaInfo.age === prefs.info; + message = 'Change cannula soon'; + cannulaInfo.level = levels.INFO; + } + + if (prefs.display === 'days' && cannulaInfo.found) { + cannulaInfo.display = ''; + if (cannulaInfo.age >= 24) { + cannulaInfo.display += cannulaInfo.days + 'd'; } + cannulaInfo.display += cannulaInfo.hours + 'h'; + } else { + cannulaInfo.display = cannulaInfo.found ? cannulaInfo.age + 'h' : 'n/a '; + } + + //allow for 20 minute period after a full hour during which we'll alert the user + if (prefs.enableAlerts && sendNotification && cannulaInfo.minFractions <= 20) { + cannulaInfo.notification = { + title: translate('Cannula age %1 hours', { params: [cannulaInfo.age] }) + , message: message + , pushoverSound: sound + , level: cannulaInfo.level + , group: 'CAGE' + }; + } + + return cannulaInfo; + }; + + cage.updateVisualisation = function updateVisualisation (sbx) { + + var cannulaInfo = sbx.properties.cage; + + var info = [{ label: translate('Inserted'), value: new Date(cannulaInfo.treatmentDate).toLocaleString() }]; + + if (!_.isEmpty(cannulaInfo.notes)) { + info.push({label: translate('Notes') + ':', value: cannulaInfo.notes}); } - }); - - return returnValue; -}; - -cage.updateVisualisation = function updateVisualisation (sbx) { - - var cannulaInfo = cage.findLatestTimeChange(sbx); - var prefs = cage.getPrefs(sbx); - - var info = [{ label: 'Inserted', value: new Date(cannulaInfo.treatmentDate).toLocaleString() }]; - if (cannulaInfo.message !== '') { info.push({label: 'Notes:', value: cannulaInfo.message}); } - var shownAge; - if (prefs.display === 'days' && cannulaInfo.found && cannulaInfo.age > 24) { - shownAge = times.hours(cannulaInfo.age).days.toFixed(1) + 'd'; - } else { - shownAge = cannulaInfo.found ? cannulaInfo.age + 'h' : 'n/a '; - } - - sbx.pluginBase.updatePillText(cage, { - value: shownAge - , label: 'CAGE' - , info: info - }); -}; \ No newline at end of file + + var statusClass = null; + if (cannulaInfo.level === levels.URGENT) { + statusClass = 'urgent'; + } else if (cannulaInfo.level === levels.WARN) { + statusClass = 'warn'; + } + + sbx.pluginBase.updatePillText(cage, { + value: cannulaInfo.display + , label: translate('CAGE') + , info: info + , pillClass: statusClass + }); + }; + return cage; +} + +module.exports = init; + diff --git a/lib/plugins/careportal.js b/lib/plugins/careportal.js index f4018cb26f2..2dc992341c6 100644 --- a/lib/plugins/careportal.js +++ b/lib/plugins/careportal.js @@ -1,7 +1,5 @@ 'use strict'; -// this is just a fake plugin to enable hiding from settings drawer - function init() { var careportal = { @@ -10,7 +8,100 @@ function init() { , pluginType: 'drawer' }; + careportal.getEventTypes = function getEventTypes () { + + //TODO: use sbx and new CAREPORTAL_EVENTTYPE_GROUPS="core temps combo dad sensor site etc" + + return [ + { val: '' + , name: '' + , 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, sensor: false + } + ]; + + }; + return careportal; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/cob.js b/lib/plugins/cob.js index 14c80008b17..a75de32f9f8 100644 --- a/lib/plugins/cob.js +++ b/lib/plugins/cob.js @@ -1,10 +1,12 @@ 'use strict'; var _ = require('lodash') - , iob = require('./iob')() - , moment = require('moment'); + , times = require('../times'); -function init() { +function init (ctx) { + var moment = ctx.moment; + var translate = ctx.language.translate; + var iob = require('./iob')(ctx); var cob = { name: 'cob' @@ -12,52 +14,170 @@ function init() { , pluginType: 'pill-minor' }; - cob.setProperties = function setProperties(sbx) { - sbx.offerProperty('cob', function setCOB ( ) { - return cob.cobTotal(sbx.data.treatments, sbx.data.profile, sbx.time); + cob.RECENCY_THRESHOLD = times.mins(30).msecs; + + cob.setProperties = function setProperties (sbx) { + sbx.offerProperty('cob', function setCOB () { + return cob.cobTotal(sbx.data.treatments, sbx.data.devicestatus, sbx.data.profile, sbx.time); }); }; - cob.cobTotal = function cobTotal(treatments, profile, time, spec_profile) { + cob.cobTotal = function cobTotal (treatments, devicestatus, profile, time, spec_profile) { - if (!profile || !profile.hasData()) { + if (!profile || !profile.hasData()) { console.warn('For the COB plugin to function you need a treatment profile'); return {}; } if (!profile.getSensitivity(time, spec_profile) || !profile.getCarbRatio(time, spec_profile)) { - console.warn('For the CPB plugin to function your treatment profile must have both sens and carbratio fields'); + console.warn('For the COB plugin to function your treatment profile must have both sens and carbratio fields'); return {}; } - // TODO: figure out the liverSensRatio that gives the most accurate purple line predictions - var liverSensRatio = 8; - var totalCOB = 0; - var lastCarbs = null; + if (typeof time === 'undefined') { + time = Date.now(); + } else if (time && time.getTime) { + time = time.getTime(); + } + + var devicestatusCOB = cob.lastCOBDeviceStatus(devicestatus, time); + var result = devicestatusCOB; + + const TEN_MINUTES = 10 * 60 * 1000; + + if (_.isEmpty(result) || _.isNil(result.cob) || (Date.now() - result.mills) > TEN_MINUTES) { + + var treatmentCOB = (treatments !== undefined && treatments.length) ? cob.fromTreatments(treatments, devicestatus, profile, time, spec_profile) : {}; + + result = _.cloneDeep(treatmentCOB); + result.source = 'Care Portal'; + result.treatmentCOB = _.cloneDeep(treatmentCOB); + } + + return addDisplay(result); + }; - if (!treatments) { + function addDisplay (cob) { + if (_.isEmpty(cob) || cob.cob === undefined) { return {}; } - if (typeof time === 'undefined') { - time = Date.now(); + var display = Math.round(cob.cob * 10) / 10; + return _.merge(cob, { + display: display + , displayLine: 'COB: ' + display + 'g' + }); + } + + cob.isDeviceStatusAvailable = function isDeviceStatusAvailable (devicestatus) { + + return _.chain(devicestatus) + .map(cob.fromDeviceStatus) + .reject(_.isEmpty) + .value() + .length > 0; + }; + + cob.lastCOBDeviceStatus = function lastCOBDeviceStatus (devicestatus, time) { + + var futureMills = time + times.mins(5).msecs; //allow for clocks to be a little off + var recentMills = time - cob.RECENCY_THRESHOLD; + + return _.chain(devicestatus) + .filter(function(cobStatus) { + return cobStatus.mills <= futureMills && cobStatus.mills >= recentMills; + }) + .map(cob.fromDeviceStatus) + .reject(_.isEmpty) + .sortBy('mills') + .last() + .value(); + }; + + cob.COBDeviceStatusesInTimeRange = function COBDeviceStatusesInTimeRange (devicestatus, from, to) { + + return _.chain(devicestatus) + .filter(function(cobStatus) { + return cobStatus.mills > from && cobStatus.mills < to; + }) + .map(cob.fromDeviceStatus) + .reject(_.isEmpty) + .sortBy('mills') + .value(); + }; + + cob.fromDeviceStatus = function fromDeviceStatus (devicestatusEntry) { + + var cobObj; + if (_.get(devicestatusEntry, 'openaps') !== undefined) { + var suggested = devicestatusEntry.openaps.suggested; + var enacted = devicestatusEntry.openaps.enacted; + + var lastCOB = null; + var lastMoment = null; + + if (suggested && enacted) { + var suggestedMoment = moment(suggested.timestamp); + var enactedMoment = moment(enacted.timestamp); + if (enactedMoment.isAfter(suggestedMoment)) { + lastCOB = enacted.COB; + lastMoment = enactedMoment; + } else { + lastCOB = suggested.COB; + lastMoment = suggestedMoment; + } + } else if (enacted) { + lastCOB = enacted.COB; + lastMoment = moment(enacted.timestamp); + } else if (suggested) { + lastCOB = suggested.COB; + lastMoment = moment(suggested.timestamp); + } + + if (lastCOB === null || !lastMoment) { + return {}; + } + + return { + cob: lastCOB + , source: 'OpenAPS' + , device: devicestatusEntry.device + , mills: lastMoment.valueOf() + }; + } else if (_.get(devicestatusEntry, 'loop.cob') !== undefined) { + cobObj = devicestatusEntry.loop.cob; + return { + cob: cobObj.cob + , source: 'Loop' + , device: devicestatusEntry.device + , mills: moment(cobObj.timestamp).valueOf() + }; + } else { + return {}; } + }; + + cob.fromTreatments = function fromTreatments (treatments, devicestatus, profile, time, spec_profile) { + // TODO: figure out the liverSensRatio that gives the most accurate purple line predictions + var liverSensRatio = 8; + var totalCOB = 0; + var lastCarbs = null; var isDecaying = 0; var lastDecayedBy = 0; - _.each(treatments, function eachTreatment(treatment) { + _.each(treatments, function eachTreatment (treatment) { if (treatment.carbs && treatment.mills < time) { lastCarbs = treatment; var cCalc = cob.cobCalc(treatment, profile, lastDecayedBy, time, spec_profile); var decaysin_hr = (cCalc.decayedBy - time) / 1000 / 60 / 60; if (decaysin_hr > -10) { // units: BG - var actStart = iob.calcTotal(treatments, profile, lastDecayedBy, spec_profile).activity; - var actEnd = iob.calcTotal(treatments, profile, cCalc.decayedBy, spec_profile).activity; + var actStart = iob.calcTotal(treatments, devicestatus, profile, lastDecayedBy, spec_profile).activity; + var actEnd = iob.calcTotal(treatments, devicestatus, profile, cCalc.decayedBy, spec_profile).activity; var avgActivity = (actStart + actEnd) / 2; // units: g = BG * scalar / BG / U * g / U - var delayedCarbs = ( avgActivity * liverSensRatio / profile.getSensitivity(treatment.mills, spec_profile) ) * profile.getCarbRatio(treatment.mills, spec_profile); + var delayedCarbs = (avgActivity * liverSensRatio / profile.getSensitivity(treatment.mills, spec_profile)) * profile.getCarbRatio(treatment.mills, spec_profile); var delayMinutes = Math.round(delayedCarbs / profile.getCarbAbsorptionRate(treatment.mills, spec_profile) * 60); if (delayMinutes > 0) { cCalc.decayedBy.setMinutes(cCalc.decayedBy.getMinutes() + delayMinutes); @@ -72,7 +192,7 @@ function init() { if (decaysin_hr > 0) { //console.info('Adding ' + delayMinutes + ' minutes to decay of ' + treatment.carbs + 'g bolus at ' + treatment.mills); totalCOB += Math.min(Number(treatment.carbs), decaysin_hr * profile.getCarbAbsorptionRate(treatment.mills, spec_profile)); - //console.log("cob:", Math.min(cCalc.initialCarbs, decaysin_hr * profile.getCarbAbsorptionRate(treatment.mills)),cCalc.initialCarbs,decaysin_hr,profile.getCarbAbsorptionRate(treatment.mills)); + //console.log('cob:', Math.min(cCalc.initialCarbs, decaysin_hr * profile.getCarbAbsorptionRate(treatment.mills)),cCalc.initialCarbs,decaysin_hr,profile.getCarbAbsorptionRate(treatment.mills)); isDecaying = cCalc.isDecaying; } else { totalCOB = 0; @@ -82,20 +202,18 @@ function init() { }); var rawCarbImpact = isDecaying * profile.getSensitivity(time, spec_profile) / profile.getCarbRatio(time, spec_profile) * profile.getCarbAbsorptionRate(time, spec_profile) / 60; - var display = Math.round(totalCOB * 10) / 10; + return { decayedBy: lastDecayedBy , isDecaying: isDecaying , carbs_hr: profile.getCarbAbsorptionRate(time, spec_profile) , rawCarbImpact: rawCarbImpact , cob: totalCOB - , display: display - , displayLine: 'COB: ' + display + 'g' , lastCarbs: lastCarbs }; }; - cob.carbImpact = function carbImpact(rawCarbImpact, insulinImpact) { + cob.carbImpact = function carbImpact (rawCarbImpact, insulinImpact) { var liverSensRatio = 1.0; var liverCarbImpactMax = 0.7; var liverCarbImpact = Math.min(liverCarbImpactMax, liverSensRatio * insulinImpact); @@ -103,12 +221,12 @@ function init() { var netCarbImpact = Math.max(0, rawCarbImpact - liverCarbImpact); var totalImpact = netCarbImpact - insulinImpact; return { - netCarbImpact: netCarbImpact, - totalImpact: totalImpact + netCarbImpact: netCarbImpact + , totalImpact: totalImpact }; }; - cob.cobCalc = function cobCalc(treatment, profile, lastDecayedBy, time, spec_profile) { + cob.cobCalc = function cobCalc (treatment, profile, lastDecayedBy, time, spec_profile) { var delay = 20; var isDecaying = 0; @@ -116,7 +234,7 @@ function init() { if (treatment.carbs) { var carbTime = new Date(treatment.mills); - + var carbs_hr = profile.getCarbAbsorptionRate(treatment.mills, spec_profile); var carbs_min = carbs_hr / 60; @@ -125,31 +243,28 @@ function init() { decayedBy.setMinutes(decayedBy.getMinutes() + Math.max(delay, minutesleft) + treatment.carbs / carbs_min); if (delay > minutesleft) { initialCarbs = parseInt(treatment.carbs); - } - else { + } else { initialCarbs = parseInt(treatment.carbs) + minutesleft * carbs_min; } var startDecay = new Date(carbTime); startDecay.setMinutes(carbTime.getMinutes() + delay); if (time < lastDecayedBy || time > startDecay) { isDecaying = 1; - } - else { + } else { isDecaying = 0; } return { - initialCarbs: initialCarbs, - decayedBy: decayedBy, - isDecaying: isDecaying, - carbTime: carbTime + initialCarbs: initialCarbs + , decayedBy: decayedBy + , isDecaying: isDecaying + , carbTime: carbTime }; - } - else { + } else { return ''; } }; - cob.updateVisualisation = function updateVisualisation(sbx) { + cob.updateVisualisation = function updateVisualisation (sbx) { var prop = sbx.properties.cob; @@ -157,20 +272,55 @@ function init() { var displayCob = Math.round(prop.cob * 10) / 10; - var info = null; - if (prop.lastCarbs) { - var when = moment(prop.lastCarbs.mills).format('lll'); - var amount = prop.lastCarbs.carbs + 'g'; - info = [{label: 'Last Carbs', value: amount + ' @ ' + when }]; + var info = []; + if (prop.treatmentCOB !== undefined && prop.treatmentCOB.cob) { + info.push({ label: translate('Careportal COB'), value: Math.round(prop.treatmentCOB.cob * 10) / 10 }); + } + + var lastCarbs = prop.lastCarbs || (prop.treatmentCOB && prop.treatmentCOB.lastCarbs); + if (lastCarbs) { + var when = new Date(lastCarbs.mills).toLocaleString(); + var amount = lastCarbs.carbs + 'g'; + info.push({ label: translate('Last Carbs'), value: amount + ' @ ' + when }); } sbx.pluginBase.updatePillText(sbx, { value: displayCob + 'g' - , label: 'COB' + , label: translate('COB') , info: info }); }; + function virtAsstCOBHandler (next, slots, sbx) { + var response = ''; + var cob = _.get(sbx, 'properties.cob.cob'); + var pwd = _.get(slots, 'pwd.value'); + var value = cob ? cob : 0; + if (pwd) { + response = translate('virtAsstCob3person', { + params: [ + pwd.replace('\'s', '') + , value + ] + }); + } else { + response = translate('virtAsstCob', { + params: [ + value + ] + }); + } + next(translate('virtAsstTitleCurrentCOB'), response); + } + + cob.virtAsst = { + intentHandlers: [{ + intent: 'MetricNow' + , metrics: ['cob', 'carbs on board', 'carbohydrates on board'] + , intentHandler: virtAsstCOBHandler + }] + }; + return cob; } diff --git a/lib/plugins/dbsize.js b/lib/plugins/dbsize.js new file mode 100644 index 00000000000..9c3e0c50c35 --- /dev/null +++ b/lib/plugins/dbsize.js @@ -0,0 +1,150 @@ +'use strict'; + +var _ = require('lodash'); + +function init (ctx) { + var translate = ctx.language.translate; + + var dbsize = { + name: 'dbsize' + , label: translate('Database Size') + , pluginType: 'pill-status' + , pillFlip: true + }; + + dbsize.getPrefs = function getPrefs (sbx) { + return { + warnPercentage: sbx.extendedSettings.warnPercentage ? sbx.extendedSettings.warnPercentage : 60 + , urgentPercentage: sbx.extendedSettings.urgentPercentage ? sbx.extendedSettings.urgentPercentage : 75 + , max: sbx.extendedSettings.max ? sbx.extendedSettings.max : 496 + , enableAlerts: sbx.extendedSettings.enableAlerts + , inMib: sbx.extendedSettings.inMib + }; + }; + + dbsize.setProperties = function setProperties (sbx) { + sbx.offerProperty('dbsize', function setDbsize () { + return dbsize.analyzeData(sbx); + }); + }; + + dbsize.analyzeData = function analyzeData (sbx) { + + var prefs = dbsize.getPrefs(sbx); + + var recentData = sbx.data.dbstats; + + var result = { + level: undefined + , display: prefs.inMib ? '?MiB' : '?%' + , status: undefined + }; + + var maxSize = (prefs.max > 0) ? prefs.max : 100 * 1024; + var totalDataSize = (recentData && recentData.dataSize) ? recentData.dataSize : 0; + totalDataSize += (recentData && recentData.indexSize) ? recentData.indexSize : 0; + totalDataSize /= 1024 * 1024; + + var dataPercentage = Math.floor((totalDataSize * 100.0) / maxSize); + + result.totalDataSize = totalDataSize; + result.dataPercentage = dataPercentage; + result.notificationLevel = ctx.levels.INFO; + result.details = { + maxSize: parseFloat(maxSize.toFixed(2)) + , dataSize: parseFloat(totalDataSize.toFixed(2)) + }; + + // failsafe to have percentage in 0..100 range + var boundWarnPercentage = Math.max(0, Math.min(100, parseInt(prefs.warnPercentage))); + var boundUrgentPercentage = Math.max(0, Math.min(100, parseInt(prefs.urgentPercentage))); + + var warnSize = Math.floor((boundWarnPercentage/100) * maxSize); + var urgentSize = Math.floor((boundUrgentPercentage/100) * maxSize); + + if ((totalDataSize >= urgentSize)&&(boundUrgentPercentage > 0)) { + result.notificationLevel = ctx.levels.URGENT; + } else if ((totalDataSize >= warnSize)&&(boundWarnPercentage > 0)) { + result.notificationLevel = ctx.levels.WARN; + } + + result.display = prefs.inMib ? parseFloat(totalDataSize.toFixed(0)) + 'MiB' : dataPercentage + '%'; + result.status = ctx.levels.toStatusClass(result.notificationLevel); + + return result; + }; + + dbsize.checkNotifications = function checkNotifications (sbx) { + var prefs = dbsize.getPrefs(sbx); + + if (!prefs.enableAlerts) { return; } + + var prop = sbx.properties.dbsize; + + if (prop.dataPercentage && prop.notificationLevel && prop.notificationLevel >= ctx.levels.WARN) { + sbx.notifications.requestNotify({ + level: prop.notificationLevel + , title: ctx.levels.toDisplay(prop.notificationLevel) + ' ' + translate('Database Size near its limits!') + , message: translate('Database size is %1 MiB out of %2 MiB. Please backup and clean up database!', { + params: [prop.details.dataSize, prop.details.maxSize] + }) + , pushoverSound: 'echo' + , group: 'Database Size' + , plugin: dbsize + , debug: prop + }); + } + }; + + dbsize.updateVisualisation = function updateVisualisation (sbx) { + var prop = sbx.properties.dbsize; + + var infos = [{ + label: translate('Data size') + , value: translate('%1 MiB of %2 MiB (%3%)', { + params: [prop.details.dataSize, prop.details.maxSize, prop.dataPercentage] + }) + } + ]; + + sbx.pluginBase.updatePillText(dbsize, { + value: prop && prop.display + , labelClass: 'plugicon-database' + , pillClass: prop && prop.status + , info: infos + , hide: !(prop && prop.totalDataSize && prop.totalDataSize >= 0) + }); + }; + + function virtAsstDatabaseSizeHandler (next, slots, sbx) { + var display = _.get(sbx, 'properties.dbsize.display'); + if (display) { + var dataSize = _.get(sbx, 'properties.dbsize.details.dataSize'); + var dataPercentage = _.get(sbx, 'properties.dbsize.dataPercentage'); + var response = translate('virtAsstDatabaseSize', { + params: [ + dataSize + , dataPercentage + ] + }); + next(translate('virtAsstTitleDatabaseSize'), response); + } else { + next(translate('virtAsstTitleDatabaseSize'), translate('virtAsstUnknown')); + } + } + + dbsize.virtAsst = { + intentHandlers: [ + { + intent: 'MetricNow' + , metrics: ['db size'] + , intentHandler: virtAsstDatabaseSizeHandler + } + ] + }; + + return dbsize; + +} + +module.exports = init; diff --git a/lib/plugins/delta.js b/lib/plugins/delta.js deleted file mode 100644 index aeff1885a8b..00000000000 --- a/lib/plugins/delta.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -var ONE_MINUTE = 60000; - -function init() { - - var delta = { - name: 'delta' - , label: 'BG Delta' - , pluginType: 'pill-major' - , pillFlip: true - }; - - delta.setProperties = function setProperties (sbx) { - sbx.offerProperty('delta', function setDelta ( ) { - var prev = sbx.data.sgvs.length >= 2 ? sbx.data.sgvs[sbx.data.sgvs.length - 2] : null; - var current = sbx.data.sgvs.length >= 1 ? sbx.data.sgvs[sbx.data.sgvs.length - 1] : null; - if (sbx.data.inRetroMode && !sbx.isCurrent(current)) { - return undefined; - } else { - return delta.calc( - prev - , current - , sbx - ); - } - }); - }; - - delta.updateVisualisation = function updateVisualisation (sbx) { - var prop = sbx.properties.delta; - - var info = []; - var display = prop && prop.display; - - if (prop && prop.interpolated) { - display += ' *'; - info.push({label: 'Elapsed Time', value: Math.round(prop.elapsedMins) + ' mins'}); - info.push({label: 'Absolute Delta', value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(prop.absMgdl)) + ' ' + sbx.unitsLabel}); - info.push({label: 'Interpolated', value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(prop.mgdl5MinsAgo)) + ' ' + sbx.unitsLabel}); - } - - sbx.pluginBase.updatePillText(delta, { - value: display - , label: sbx.unitsLabel - , info: info - }); - }; - - delta.calc = function calc(prev, current, sbx) { - var result = { display: null }; - - if (!isSGVOk(prev) || !isSGVOk(current)) { return result; } - - result.absMgdl = current.mgdl - prev.mgdl; - result.elapsedMins = (current.mills - prev.mills) / ONE_MINUTE; - - updateWithInterpolation(prev, current, result); - - result.mgdl = Math.round(current.mgdl - result.mgdl5MinsAgo); - result.scaled = sbx.settings.units === 'mmol' ? - sbx.roundBGToDisplayFormat(sbx.scaleMgdl(current.mgdl) - sbx.scaleMgdl(result.mgdl5MinsAgo)) : result.mgdl; - - result.display = (result.scaled >= 0 ? '+' : '') + result.scaled; - - return result; - }; - - return delta; - -} - -function updateWithInterpolation (prev, current, result) { - result.interpolated = result.elapsedMins > 9; - result.mgdl5MinsAgo = result.interpolated - ? current.mgdl - result.absMgdl / result.elapsedMins * 5 - : result.mgdl5MinsAgo = prev.mgdl; -} - -function isSGVOk (entry) { - return entry && entry.mgdl >= 13; -} - -module.exports = init; diff --git a/lib/plugins/direction.js b/lib/plugins/direction.js index 07f9f6e1071..6274fc9e537 100644 --- a/lib/plugins/direction.js +++ b/lib/plugins/direction.js @@ -1,7 +1,5 @@ 'use strict'; -var _ = require('lodash'); - function init() { var direction = { @@ -12,10 +10,10 @@ function init() { direction.setProperties = function setProperties (sbx) { sbx.offerProperty('direction', function setDirection ( ) { - if (sbx.data.inRetroMode && !sbx.isCurrent(sbx.lastSGVEntry())) { + if (!sbx.isCurrent(sbx.lastSGVEntry())) { return undefined; } else { - return direction.info(_.last(sbx.data.sgvs)); + return direction.info(sbx.lastSGVEntry()); } }); }; @@ -23,7 +21,7 @@ function init() { direction.updateVisualisation = function updateVisualisation (sbx) { var prop = sbx.properties.direction; - if (!prop) { + if (!prop || !prop.value) { sbx.pluginBase.updatePillText(direction, { hide: true }); @@ -54,6 +52,7 @@ function init() { var dir2Char = { NONE: '⇼' + , TripleUp: '⤊' , DoubleUp: '⇈' , SingleUp: '↑' , FortyFiveUp: '↗' @@ -61,6 +60,7 @@ function init() { , FortyFiveDown: '↘' , SingleDown: '↓' , DoubleDown: '⇊' + , TripleDown: '⤋' , 'NOT COMPUTABLE': '-' , 'RATE OUT OF RANGE': '⇕' }; @@ -77,4 +77,4 @@ function init() { } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/errorcodes.js b/lib/plugins/errorcodes.js index 1b1504cd754..bd01a189aeb 100644 --- a/lib/plugins/errorcodes.js +++ b/lib/plugins/errorcodes.js @@ -1,10 +1,9 @@ 'use strict'; var _ = require('lodash'); -var levels = require('../levels'); var times = require('../times'); -function init() { +function init(ctx) { var errorcodes = { name: 'errorcodes' @@ -36,7 +35,7 @@ function init() { errorcodes.toDisplay = toDisplay; errorcodes.checkNotifications = function checkNotifications (sbx) { - var now = Date.now(); + var now = sbx.time; var lastSGV = sbx.lastSGVEntry(); var code2Level = buildMappingFromSettings(sbx.extendedSettings); @@ -53,6 +52,7 @@ function init() { , message: errorDisplay , plugin: errorcodes , pushoverSound: pushoverSound + , group: 'CGM Error Code' , debug: { lastSGV: lastSGV } @@ -78,9 +78,9 @@ function init() { }); } - addValuesToMapping(extendedSettings.info || '1 2 3 4 5 6 7 8', levels.INFO); - addValuesToMapping(extendedSettings.warn || 'off', levels.WARN); - addValuesToMapping(extendedSettings.urgent || '9 10', levels.URGENT); + addValuesToMapping(extendedSettings.info || '1 2 3 4 5 6 7 8', ctx.levels.INFO); + addValuesToMapping(extendedSettings.warn || false, ctx.levels.WARN); + addValuesToMapping(extendedSettings.urgent || '9 10', ctx.levels.URGENT); return mapping; } diff --git a/lib/plugins/googlehome.js b/lib/plugins/googlehome.js new file mode 100644 index 00000000000..6b6d7b098f2 --- /dev/null +++ b/lib/plugins/googlehome.js @@ -0,0 +1,97 @@ +var _ = require('lodash'); +var async = require('async'); + +function init () { + console.log('Configuring Google Home...'); + function googleHome() { + return googleHome; + } + var intentHandlers = {}; + var rollup = {}; + + // There is no protection for a previously handled metric - one plugin can overwrite the handler of another plugin. + googleHome.configureIntentHandler = function configureIntentHandler(intent, handler, metrics) { + if (!intentHandlers[intent]) { + intentHandlers[intent] = {}; + } + if (metrics) { + for (var i = 0, len = metrics.length; i < len; i++) { + if (!intentHandlers[intent][metrics[i]]) { + intentHandlers[intent][metrics[i]] = {}; + } + console.log('Storing handler for intent \'' + intent + '\' for metric \'' + metrics[i] + '\''); + intentHandlers[intent][metrics[i]].handler = handler; + } + } else { + console.log('Storing handler for intent \'' + intent + '\''); + intentHandlers[intent].handler = handler; + } + }; + + // This function retrieves a handler based on the intent name and metric requested. + googleHome.getIntentHandler = function getIntentHandler(intentName, metric) { + console.log('Looking for handler for intent \'' + intentName + '\' for metric \'' + metric + '\''); + if (intentName && intentHandlers[intentName]) { + if (intentHandlers[intentName][metric] && intentHandlers[intentName][metric].handler) { + console.log('Found!'); + return intentHandlers[intentName][metric].handler + } else if (intentHandlers[intentName].handler) { + console.log('Found!'); + return intentHandlers[intentName].handler; + } + console.log('Not found!'); + return null; + } else { + console.log('Not found!'); + return null; + } + }; + + googleHome.addToRollup = function(rollupGroup, handler, rollupName) { + if (!rollup[rollupGroup]) { + console.log('Creating the rollup group: ', rollupGroup); + rollup[rollupGroup] = []; + } + rollup[rollupGroup].push({handler: handler, name: rollupName}); + }; + + googleHome.getRollup = function(rollupGroup, sbx, slots, locale, callback) { + var handlers = _.map(rollup[rollupGroup], 'handler'); + console.log('Rollup array for ', rollupGroup); + console.log(rollup[rollupGroup]); + var nHandlers = []; + _.each(handlers, function (handler) { + nHandlers.push(handler.bind(null, slots, sbx)); + }); + async.parallelLimit(nHandlers, 10, function(err, results) { + if (err) { + console.error('Error: ', err); + } + callback(_.map(_.orderBy(results, ['priority'], ['asc']), 'results').join(' ')); + }); + }; + + // This creates the expected Google Home response + googleHome.buildSpeechletResponse = function buildSpeechletResponse(output, expectUserResponse) { + return { + payload: { + google: { + expectUserResponse: expectUserResponse, + richResponse: { + items: [ + { + simpleResponse: { + textToSpeech: output + } + } + ] + } + } + } + }; + }; + + return googleHome; +} + +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 6e4bba3823b..fefba1406b9 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -1,15 +1,22 @@ 'use strict'; -var _ = require('lodash'); +var _find = require('lodash/find'); +var _each = require('lodash/each'); +var _filter = require('lodash/filter'); +var _get = require('lodash/get'); +var _isArray = require('lodash/isArray'); +var _map = require('lodash/map'); -function init() { +function init (ctx) { var allPlugins = [] , enabledPlugins = []; - function plugins(name) { + function plugins (name) { if (name) { - return _.find(allPlugins, {name: name}); + return _find(allPlugins, { + name: name + }); } else { return plugins; } @@ -18,129 +25,205 @@ function init() { plugins.base = require('./pluginbase'); var clientDefaultPlugins = [ - require('./rawbg')() - , require('./delta')() - , require('./direction')() - , require('./ar2')() - , require('./errorcodes')() - , require('./iob')() - , require('./cob')() - , require('./boluswizardpreview')() - , require('./cannulaage')() - , require('./basalprofile')() - , require('./upbat')() - , require('./careportal')() - , require('./boluscalc')() + require('./bgnow')(ctx) + , require('./rawbg')(ctx) + , require('./direction')(ctx) + , require('./timeago')(ctx) + , require('./upbat')(ctx) + , require('./ar2')(ctx) + , require('./errorcodes')(ctx) + , require('./iob')(ctx) + , require('./cob')(ctx) + , require('./careportal')(ctx) + , require('./pump')(ctx) + , require('./openaps')(ctx) + , require('./xdripjs')(ctx) + , require('./loop')(ctx) + , require('./override')(ctx) + , require('./boluswizardpreview')(ctx) + , require('./cannulaage')(ctx) + , require('./sensorage')(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) + , require('./dbsize')(ctx) ]; var serverDefaultPlugins = [ - require('./rawbg')() - , require('./delta')() - , require('./direction')() - , require('./ar2')() - , require('./simplealarms')() - , require('./errorcodes')() - , require('./iob')() - , require('./cob')() - , require('./boluswizardpreview')() - , require('./cannulaage')() - , require('./treatmentnotify')() + require('./bgnow')(ctx) + , require('./rawbg')(ctx) + , require('./direction')(ctx) + , require('./upbat')(ctx) + , require('./ar2')(ctx) + , require('./simplealarms')(ctx) + , require('./errorcodes')(ctx) + , require('./iob')(ctx) + , require('./cob')(ctx) + , require('./pump')(ctx) + , require('./openaps')(ctx) + , require('./xdripjs')(ctx) + , require('./loop')(ctx) + , require('./boluswizardpreview')(ctx) + , require('./cannulaage')(ctx) + , require('./sensorage')(ctx) + , require('./insulinage')(ctx) + , require('./batteryage')(ctx) + , require('./treatmentnotify')(ctx) + , require('./timeago')(ctx) + , require('./basalprofile')(ctx) + , require('./dbsize')(ctx) + , require('./runtimestate')(ctx) ]; - plugins.registerServerDefaults = function registerServerDefaults() { + plugins.registerServerDefaults = function registerServerDefaults () { plugins.register(serverDefaultPlugins); return plugins; }; - plugins.registerClientDefaults = function registerClientDefaults() { + plugins.registerClientDefaults = function registerClientDefaults () { plugins.register(clientDefaultPlugins); return plugins; }; - plugins.register = function register(all) { - _.each(all, function eachPlugin(plugin) { + plugins.register = function register (all) { + _each(all, function eachPlugin (plugin) { allPlugins.push(plugin); }); - }; - plugins.init = function initPlugins (settings) { enabledPlugins = []; - function isEnabled(plugin) { + + var enable = _get(ctx, 'settings.enable'); + + function isEnabled (plugin) { //TODO: unify client/server env/app - return settings.enable.indexOf(plugin.name) > -1; + return enable && enable.indexOf(plugin.name) > -1; } - _.each(allPlugins, function eachPlugin(plugin) { + _each(allPlugins, function eachPlugin (plugin) { plugin.enabled = isEnabled(plugin); if (plugin.enabled) { enabledPlugins.push(plugin); } }); - return plugins; }; - plugins.eachPlugin = function eachPlugin(f) { - _.each(allPlugins, f); + plugins.isPluginEnabled = function isPluginEnabled (pluginName) { + var p = _find(enabledPlugins, 'name', pluginName); + return (p !== null); + } + + plugins.getPlugin = function getPlugin (pluginName) { + return _find(enabledPlugins, 'name', pluginName); + } + + plugins.eachPlugin = function eachPlugin (f) { + _each(allPlugins, f); }; - plugins.eachEnabledPlugin = function eachEnabledPlugin(f) { - _.each(enabledPlugins, f); + plugins.eachEnabledPlugin = function eachEnabledPlugin (f) { + _each(enabledPlugins, f); }; //these plugins are either always on or have custom settings - plugins.specialPlugins = 'ar2 delta direction upbat rawbg errorcodes'; + plugins.specialPlugins = 'ar2 bgnow delta direction timeago upbat rawbg errorcodes profile bolus'; plugins.shownPlugins = function(sbx) { - return _.filter(enabledPlugins, function filterPlugins(plugin) { + return _filter(enabledPlugins, function filterPlugins (plugin) { return plugins.specialPlugins.indexOf(plugin.name) > -1 || (sbx && sbx.showPlugins && sbx.showPlugins.indexOf(plugin.name) > -1); }); }; - plugins.eachShownPlugins = function eachShownPlugins(sbx, f) { - _.each(plugins.shownPlugins(sbx), f); + plugins.eachShownPlugins = function eachShownPlugins (sbx, f) { + _each(plugins.shownPlugins(sbx), f); }; - plugins.hasShownType = function hasShownType(pluginType, sbx) { - return _.find(plugins.shownPlugins(sbx), function findWithType(plugin) { + plugins.hasShownType = function hasShownType (pluginType, sbx) { + return _find(plugins.shownPlugins(sbx), function findWithType (plugin) { return plugin.pluginType === pluginType; }) !== undefined; }; - plugins.setProperties = function setProperties(sbx) { - plugins.eachEnabledPlugin( function eachPlugin (plugin) { + plugins.setProperties = function setProperties (sbx) { + plugins.eachEnabledPlugin(function eachPlugin (plugin) { if (plugin.setProperties) { - plugin.setProperties(sbx.withExtendedSettings(plugin)); + try { + plugin.setProperties(sbx.withExtendedSettings(plugin)); + } catch (error) { + console.error('Plugin error on setProperties(): ', plugin.name, error); + } } }); }; - plugins.checkNotifications = function checkNotifications(sbx) { - plugins.eachEnabledPlugin( function eachPlugin (plugin) { + plugins.checkNotifications = function checkNotifications (sbx) { + plugins.eachEnabledPlugin(function eachPlugin (plugin) { if (plugin.checkNotifications) { - plugin.checkNotifications(sbx.withExtendedSettings(plugin)); + try { + plugin.checkNotifications(sbx.withExtendedSettings(plugin)); + } catch (error) { + console.error('Plugin error on checkNotifications(): ', plugin.name, error); + } } }); }; - plugins.updateVisualisations = function updateVisualisations(sbx) { - plugins.eachShownPlugins(sbx, function eachPlugin(plugin) { + plugins.visualizeAlarm = function visualizeAlarm (sbx, alarm, alarmMessage) { + plugins.eachShownPlugins(sbx, function eachPlugin (plugin) { + if (plugin.visualizeAlarm) { + try { + plugin.visualizeAlarm(sbx.withExtendedSettings(plugin), alarm, alarmMessage); + } catch (error) { + console.error('Plugin error on visualizeAlarm(): ', plugin.name, error); + } + } + }); + }; + + plugins.updateVisualisations = function updateVisualisations (sbx) { + plugins.eachShownPlugins(sbx, function eachPlugin (plugin) { if (plugin.updateVisualisation) { - plugin.updateVisualisation(sbx.withExtendedSettings(plugin)); + try { + plugin.updateVisualisation(sbx.withExtendedSettings(plugin)); + } catch (error) { + console.error('Plugin error on visualizeAlarm(): ', plugin.name, error); + } } }); }; - plugins.enabledPluginNames = function enabledPluginNames() { - return _.map(enabledPlugins, function mapped(plugin) { + plugins.getAllEventTypes = function getAllEventTypes (sbx) { + var all = []; + plugins.eachEnabledPlugin(function eachPlugin (plugin) { + if (plugin.getEventTypes) { + var eventTypes = plugin.getEventTypes(sbx.withExtendedSettings(plugin)); + if (_isArray(eventTypes)) { + all = all.concat(eventTypes); + } + } + }); + + return all; + }; + + plugins.enabledPluginNames = function enabledPluginNames () { + return _map(enabledPlugins, function mapped (plugin) { return plugin.name; }).join(' '); }; plugins.extendedClientSettings = function extendedClientSettings (allExtendedSettings) { var clientSettings = {}; - _.each(clientDefaultPlugins, function eachClientPlugin (plugin) { + _each(clientDefaultPlugins, function eachClientPlugin (plugin) { clientSettings[plugin.name] = allExtendedSettings[plugin.name]; }); + + //HACK: include devicestatus + clientSettings.devicestatus = allExtendedSettings.devicestatus; + return clientSettings; }; @@ -148,4 +231,4 @@ function init() { } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/insulinage.js b/lib/plugins/insulinage.js new file mode 100644 index 00000000000..53da82c1ee8 --- /dev/null +++ b/lib/plugins/insulinage.js @@ -0,0 +1,151 @@ +'use strict'; + +var _ = require('lodash'); + +function init(ctx) { + var moment = ctx.moment; + var translate = ctx.language.translate; + var levels = ctx.levels; + + var iage = { + name: 'iage' + , label: 'Insulin Age' + , pluginType: 'pill-minor' + }; + + iage.getPrefs = function getPrefs(sbx) { + // IAGE_INFO=44 IAGE_WARN=48 IAGE_URGENT=70 + return { + info: sbx.extendedSettings.info || 44 + , warn: sbx.extendedSettings.warn || 48 + , urgent: sbx.extendedSettings.urgent || 72 + , enableAlerts: sbx.extendedSettings.enableAlerts || false + }; + }; + + iage.setProperties = function setProperties (sbx) { + sbx.offerProperty('iage', function setProp ( ) { + return iage.findLatestTimeChange(sbx); + }); + }; + + iage.checkNotifications = function checkNotifications(sbx) { + var insulinInfo = sbx.properties.iage; + + if (insulinInfo.notification) { + var notification = _.extend({}, insulinInfo.notification, { + plugin: iage + , debug: { + age: insulinInfo.age + } + }); + + sbx.notifications.requestNotify(notification); + } + }; + + iage.findLatestTimeChange = function findLatestTimeChange(sbx) { + + var insulinInfo = { + found: false + , age: 0 + , treatmentDate: null + }; + + var prevDate = 0; + + _.each(sbx.data.insulinchangeTreatments, function eachTreatment (treatment) { + var treatmentDate = treatment.mills; + if (treatmentDate > prevDate && treatmentDate <= sbx.time) { + + prevDate = treatmentDate; + insulinInfo.treatmentDate = treatmentDate; + + var a = moment(sbx.time); + var b = moment(insulinInfo.treatmentDate); + var days = a.diff(b,'days'); + var hours = a.diff(b,'hours') - days * 24; + var age = a.diff(b,'hours'); + + if (!insulinInfo.found || (age >= 0 && age < insulinInfo.age)) { + insulinInfo.found = true; + insulinInfo.age = age; + insulinInfo.days = days; + insulinInfo.hours = hours; + insulinInfo.notes = treatment.notes; + insulinInfo.minFractions = a.diff(b,'minutes') - age * 60; + + insulinInfo.display = ''; + if (insulinInfo.age >= 24) { + insulinInfo.display += insulinInfo.days + 'd'; + } + insulinInfo.display += insulinInfo.hours + 'h'; + } + } + }); + + var prefs = iage.getPrefs(sbx); + + insulinInfo.level = levels.NONE; + + var sound = 'incoming'; + var message; + var sendNotification = false; + + if (insulinInfo.age >= insulinInfo.urgent) { + sendNotification = insulinInfo.age === prefs.urgent; + message = translate('Insulin reservoir change overdue!'); + sound = 'persistent'; + insulinInfo.level = levels.URGENT; + } else if (insulinInfo.age >= prefs.warn) { + sendNotification = insulinInfo.age === prefs.warn; + message = translate('Time to change insulin reservoir'); + insulinInfo.level = levels.WARN; + } else if (insulinInfo.age >= prefs.info) { + sendNotification = insulinInfo.age === prefs.info; + message = translate('Change insulin reservoir soon'); + insulinInfo.level = levels.INFO; + } + + //allow for 20 minute period after a full hour during which we'll alert the user + if (prefs.enableAlerts && sendNotification && insulinInfo.minFractions <= 20) { + insulinInfo.notification = { + title: translate('Insulin reservoir age %1 hours', { params: [insulinInfo.age] }) + , message: message + , pushoverSound: sound + , level: insulinInfo.level + , group: 'IAGE' + }; + } + + return insulinInfo; + }; + + iage.updateVisualisation = function updateVisualisation (sbx) { + + var insulinInfo = sbx.properties.iage; + + var info = [{ label: translate('Changed'), value: new Date(insulinInfo.treatmentDate).toLocaleString() }]; + if (!_.isEmpty(insulinInfo.notes)) { + info.push({label: translate('Notes:'), value: insulinInfo.notes}); + } + + var statusClass = null; + if (insulinInfo.level === levels.URGENT) { + statusClass = 'urgent'; + } else if (insulinInfo.level === levels.WARN) { + statusClass = 'warn'; + } + sbx.pluginBase.updatePillText(iage, { + value: insulinInfo.display + , label: translate('IAGE') + , info: info + , pillClass: statusClass + }); + }; + + return iage; +} + +module.exports = init; + diff --git a/lib/plugins/iob.js b/lib/plugins/iob.js index 554973c8808..7ea38c168ae 100644 --- a/lib/plugins/iob.js +++ b/lib/plugins/iob.js @@ -1,33 +1,151 @@ 'use strict'; -var _ = require('lodash') - , moment = require('moment') - , utils = require('../utils')(); - -function init() { +const _ = require('lodash') +const times = require('../times'); +function init(ctx) { + var moment = ctx.moment; + var translate = ctx.language.translate; + var utils = require('../utils')(ctx); + var iob = { name: 'iob' , label: 'Insulin-on-Board' , pluginType: 'pill-major' }; + iob.RECENCY_THRESHOLD = times.mins(30).msecs; + iob.setProperties = function setProperties(sbx) { sbx.offerProperty('iob', function setIOB ( ) { - return iob.calcTotal(sbx.data.treatments, sbx.data.profile, sbx.time); + return iob.calcTotal(sbx.data.treatments, sbx.data.devicestatus, sbx.data.profile, sbx.time); }); }; - iob.calcTotal = function calcTotal(treatments, profile, time, spec_profile) { + iob.calcTotal = function calcTotal(treatments, devicestatus, profile, time, spec_profile) { + if (time === undefined) { + time = Date.now(); + } - var totalIOB = 0 - , totalActivity = 0; + var result = iob.lastIOBDeviceStatus(devicestatus, time); + + var treatmentResult = (treatments !== undefined && treatments.length) ? iob.fromTreatments(treatments, profile, time, spec_profile) : {}; - if (!treatments) { return {}; } + if (_.isEmpty(result)) { + result = treatmentResult; + } else if (treatmentResult.iob) { + result.treatmentIob = +(Math.round(treatmentResult.iob + "e+3") + "e-3"); + } + if (result.iob) result.iob = +(Math.round(result.iob + "e+3") + "e-3"); + return addDisplay(result); + }; - if (time === undefined) { - time = Date.now(); + function addDisplay(iob) { + if (_.isEmpty(iob) || iob.iob === undefined) { + return {}; + } + var display = utils.toFixed(iob.iob); + return _.merge(iob, { + display: display + , displayLine: 'IOB: ' + display + 'U' + }); + } + + iob.isDeviceStatusAvailable = function isDeviceStatusAvailable (devicestatus) { + + return _.chain(devicestatus) + .map(iob.fromDeviceStatus) + .reject(_.isEmpty) + .value() + .length > 0; + }; + + iob.lastIOBDeviceStatus = function lastIOBDeviceStatus(devicestatus, time) { + if (time && time.getTime) { + time = time.getTime(); } + var futureMills = time + times.mins(5).msecs; //allow for clocks to be a little off + var recentMills = time - iob.RECENCY_THRESHOLD; + + // All IOBs + var iobs = _.chain(devicestatus) + .filter(function (iobStatus) { + return iobStatus.mills <= futureMills && iobStatus.mills >= recentMills; + }) + .map(iob.fromDeviceStatus) + .reject(_.isEmpty) + .sortBy('mills'); + + // Loop IOBs + var loopIOBs = iobs.filter(function (iobStatus) { + return iobStatus.source === 'Loop'; + }); + + // Loop uploads both Loop IOB and pump-reported IOB, prioritize Loop IOB if available + return loopIOBs.last().value() || iobs.last().value(); + }; + + iob.IOBDeviceStatusesInTimeRange = function IOBDeviceStatusesInTimeRange (devicestatus, from, to) { + + return _.chain(devicestatus) + .filter(function (iobStatus) { + return iobStatus.mills > from && iobStatus.mills < to; + }) + .map(iob.fromDeviceStatus) + .reject(_.isEmpty) + .sortBy('mills') + .value(); + }; + + iob.fromDeviceStatus = function fromDeviceStatus(devicestatusEntry) { + var iobOpenAPS = _.get(devicestatusEntry, 'openaps.iob'); + var iobLoop = _.get(devicestatusEntry, 'loop.iob'); + var iobPump = _.get(devicestatusEntry, 'pump.iob'); + + if (_.isObject(iobOpenAPS)) { + + //hacks to support AMA iob array with time fields instead of timestamp fields + iobOpenAPS = _.isArray(iobOpenAPS) ? iobOpenAPS[0] : iobOpenAPS; + + // array could still be empty, handle as null + if (_.isEmpty(iobOpenAPS)) { + return {}; + } + + if (iobOpenAPS.time) { + iobOpenAPS.timestamp = iobOpenAPS.time; + } + + return { + iob: iobOpenAPS.iob + , basaliob: iobOpenAPS.basaliob + , activity: iobOpenAPS.activity + , source: 'OpenAPS' + , device: devicestatusEntry.device + , mills: moment(iobOpenAPS.timestamp).valueOf( ) + }; + } else if (_.isObject(iobLoop)) { + return { + iob: iobLoop.iob + , source: 'Loop' + , device: devicestatusEntry.device + , mills: moment(iobLoop.timestamp).valueOf( ) + }; + } else if (_.isObject(iobPump)) { + return { + iob: iobPump.iob || iobPump.bolusiob + , source: devicestatusEntry.connect !== undefined ? 'MM Connect' : undefined + , device: devicestatusEntry.device + , mills: devicestatusEntry.mills + }; + } else { + return {}; + } + }; + + iob.fromTreatments = function fromTreatments(treatments, profile, time, spec_profile) { + var totalIOB = 0 + , totalActivity = 0; var lastBolus = null; @@ -43,13 +161,11 @@ function init() { } }); - var display = utils.toFixed(totalIOB); return { - iob: totalIOB - , display: display - , displayLine: 'IOB: ' + display + 'U' + iob: +(Math.round(totalIOB + "e+3") + "e-3") , activity: totalActivity , lastBolus: lastBolus + , source: translate('Care Portal') }; }; @@ -82,8 +198,8 @@ function init() { } else if (minAgo < 180) { var x2 = (minAgo - 75) / 5; - result.iobContrib = treatment.insulin * (0.001323 * x2 * x2 - .054233 * x2 + .55556); - result.activityContrib = sens * treatment.insulin * (2 / dia / 60 - (minAgo - peak) * 2 / dia / 60 / (60 * dia - peak)); + result.iobContrib = treatment.insulin * (0.001323 * x2 * x2 - 0.054233 * x2 + 0.55556); + result.activityContrib = sens * treatment.insulin * (2 / dia / 60 - (minAgo - peak) * 2 / dia / 60 / (60 * 3 - peak)); } } @@ -93,24 +209,83 @@ function init() { }; iob.updateVisualisation = function updateVisualisation(sbx) { - var info = null; + var info = []; var prop = sbx.properties.iob; - if (prop && prop.lastBolus) { - var when = moment(prop.lastBolus.mills).format('lll'); + if (prop.lastBolus) { + var when = new Date(prop.lastBolus.mills).toLocaleTimeString(); var amount = sbx.roundInsulinForDisplayFormat(Number(prop.lastBolus.insulin)) + 'U'; - info = [{label: 'Last Bolus', value: amount + ' @ ' + when }]; + info.push({ label: translate('Last Bolus'), value: amount + ' @ ' + when }); + } + if (prop.basaliob !== undefined) { + info.push({ label: translate('Basal IOB'), value: prop.basaliob.toFixed(2) }); + } + if (prop.source !== undefined) { + info.push({ label: translate('Source'), value: prop.source }); } + if (prop.device !== undefined) { + info.push({ label: translate('Device'), value: prop.device }); + } + + if (prop.treatmentIob !== undefined) { + info.push({label: '------------', value: ''}); + info.push({ label: translate('Careportal IOB'), value: prop.treatmentIob.toFixed(2) }); + } + + var value = (prop.display !== undefined ? sbx.roundInsulinForDisplayFormat(prop.display) : '---') + 'U'; sbx.pluginBase.updatePillText(iob, { - value: sbx.roundInsulinForDisplayFormat(prop.display) + 'U' - , label: 'IOB' + value: value + , label: translate('IOB') , info: info }); }; + function virtAsstIOBIntentHandler (callback, slots, sbx) { + + var message = translate('virtAsstIobIntent', { + params: [ + getIob(sbx) + ] + }); + callback(translate('virtAsstTitleCurrentIOB'), message); + } + + function virtAsstIOBRollupHandler (slots, sbx, callback) { + var iob = getIob(sbx); + var message = translate('virtAsstIob', { + params: [iob] + }); + callback(null, {results: message, priority: 2}); + } + + function getIob(sbx) { + var iob = _.get(sbx, 'properties.iob.iob'); + if (iob !== 0) { + return translate('virtAsstIobUnits', { + params: [ + utils.toFixed(iob) + ] + }); + } + return translate('virtAsstNoInsulin'); + } + + iob.virtAsst = { + rollupHandlers: [{ + rollupGroup: 'Status' + , rollupName: 'current iob' + , rollupHandler: virtAsstIOBRollupHandler + }] + , intentHandlers: [{ + intent: 'MetricNow' + , metrics: ['iob', 'insulin on board'] + , intentHandler: virtAsstIOBIntentHandler + }] + }; + return iob; } diff --git a/lib/plugins/loop.js b/lib/plugins/loop.js new file mode 100644 index 00000000000..a08edb0a479 --- /dev/null +++ b/lib/plugins/loop.js @@ -0,0 +1,645 @@ +'use strict'; + +var _ = require('lodash'); +var times = require('../times'); + +// var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'freq', 'rssi']; Unused variable + +function init (ctx) { + var moment = ctx.moment; + + var utils = require('../utils')(ctx); + var translate = ctx.language.translate; + var levels = ctx.levels; + + var loop = { + name: 'loop' + , label: 'Loop' + , pluginType: 'pill-status' + }; + + var firstPrefs = true; + + loop.getPrefs = function getPrefs (sbx) { + + var prefs = { + warn: sbx.extendedSettings.warn ? sbx.extendedSettings.warn : 30 + , urgent: sbx.extendedSettings.urgent ? sbx.extendedSettings.urgent : 60 + , enableAlerts: sbx.extendedSettings.enableAlerts + }; + + if (firstPrefs) { + firstPrefs = false; + console.info(' Prefs:', prefs); + } + + return prefs; + }; + + loop.setProperties = function setProperties (sbx) { + sbx.offerProperty('loop', function setLoop () { + return loop.analyzeData(sbx); + }); + }; + + loop.analyzeData = function analyzeData (sbx) { + var recentHours = 6; + var recentMills = sbx.time - times.hours(recentHours).msecs; + + var recentData = _.chain(sbx.data.devicestatus) + .filter(function(status) { + return ('loop' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills; + }).value(); + + var prefs = loop.getPrefs(sbx); + var recent = moment(sbx.time).subtract(prefs.warn / 2, 'minutes'); + + function getDisplayForStatus (status) { + + var desc = { + symbol: '⚠' + , code: 'warning' + , label: 'Warning' + }; + + if (!status) { + return desc; + } + + if (status.failureReason || (status.enacted && !status.enacted.received)) { + desc.symbol = 'x'; + desc.code = 'error'; + desc.label = 'Error'; + } else if (status.enacted && moment(status.timestamp).isAfter(recent)) { + desc.symbol = '⌁'; + desc.code = 'enacted'; + desc.label = 'Enacted'; + } else if (status.recommendedTempBasal && moment(status.recommendedTempBasal.timestamp).isAfter(recent)) { + desc.symbol = '⏀'; + desc.code = 'recommendation'; + desc.label = 'Recomendation'; + } else if (status.moment && status.moment.isAfter(recent)) { + desc.symbol = '↻'; + desc.code = 'looping'; + desc.label = 'Looping'; + } + return desc; + } + + var result = { + lastLoop: null + , lastEnacted: null + , lastPredicted: null + , lastOkMoment: null + }; + + function assignLastEnacted (loopStatus) { + var enacted = loopStatus.enacted; + if (enacted && enacted.timestamp) { + enacted.moment = moment(enacted.timestamp); + if (!result.lastEnacted || enacted.moment.isAfter(result.lastEnacted.moment)) { + result.lastEnacted = enacted; + } + } + } + + function assignLastPredicted (loopStatus) { + if (loopStatus.predicted && loopStatus.predicted.startDate) { + result.lastPredicted = loopStatus.predicted; + } + } + + function assignLastLoop (loopStatus) { + if (!result.lastLoop || loopStatus.moment.isAfter(result.lastLoop.moment)) { + result.lastLoop = loopStatus; + } + } + + function assignLastOverride (status) { + var override = status.override; + if (override && override.timestamp) { + override.moment = moment(override.timestamp); + if (!result.lastOverride || override.moment.isAfter(result.lastOverride.moment)) { + result.lastOverride = override; + } + } + } + + function assignLastOkMoment (loopStatus) { + if (!loopStatus.failureReason && (!result.lastOkMoment || loopStatus.moment.isAfter(result.lastOkMoment))) { + result.lastOkMoment = loopStatus.moment; + } + } + + _.forEach(recentData, function eachStatus (status) { + if (status && status.loop && status.loop.timestamp) { + var loopStatus = status.loop; + loopStatus.moment = moment(loopStatus.timestamp); + assignLastEnacted(loopStatus); + assignLastLoop(loopStatus); + assignLastPredicted(loopStatus); + assignLastOverride(status); + assignLastOkMoment(loopStatus); + } + }); + + result.display = getDisplayForStatus(result.lastLoop); + + return result; + }; + + loop.checkNotifications = function checkNotifications (sbx) { + var prefs = loop.getPrefs(sbx); + + if (!prefs.enableAlerts) { return; } + + var prop = sbx.properties.loop; + + if (!prop.lastLoop) { + console.info('Loop hasn\'t reported a loop yet'); + return; + } + + var now = moment(); + var level = statusLevel(prop, prefs, sbx); + if (level >= levels.WARN) { + sbx.notifications.requestNotify({ + level: level + , title: 'Loop isn\'t looping' + , message: 'Last Loop: ' + utils.formatAgo(prop.lastOkMoment, now.valueOf()) + , pushoverSound: 'echo' + , group: 'Loop' + , plugin: loop + , debug: prop + }); + } + }; + + loop.getEventTypes = function getEventTypes (sbx) { + + var units = sbx.settings.units; + console.log('units', units); + + var reasonconf = []; + + if (sbx.data === undefined || sbx.data.profile === undefined || sbx.data.profile.data.length == 0) { + return []; + } + + let profile = sbx.data.profile.data[0]; + + if (profile.loopSettings === undefined || profile.loopSettings.overridePresets == undefined) { + return []; + } + + let presets = profile.loopSettings.overridePresets; + + for (var i = 0; i < presets.length; i++) { + let preset = presets[i] + reasonconf.push({ name: preset.name, displayName: preset.symbol + " " + preset.name, duration: preset.duration / 60}); + } + + var postLoopNotification = function (client, data, callback) { + + $.ajax({ + method: "POST" + , headers: client.headers() + , url: '/api/v2/notifications/loop' + , data: data + }) + .done(function () { + callback(); + }) + .fail(function (jqXHR) { + callback(jqXHR.responseText); + }); + } + + // TODO: add OTP entry + + return [ + { + val: 'Temporary Override' + , name: 'Temporary Override' + , bg: false + , insulin: false + , carbs: false + , prebolus: false + , duration: true + , percent: false + , absolute: false + , profile: false + , split: false + , targets: false + , reasons: reasonconf + , submitHook: postLoopNotification + }, + { + val: 'Temporary Override Cancel' + , name: 'Temporary Override Cancel' + , bg: false + , insulin: false + , carbs: false + , prebolus: false + , duration: false + , percent: false + , absolute: false + , profile: false + , split: false + , targets: false + , submitHook: postLoopNotification + }, + { + val: 'Remote Carbs Entry' + , name: 'Remote Carbs Entry' + , remoteCarbs: true + , remoteAbsorption: true + , otp: true + , submitHook: postLoopNotification + }, + { + val: 'Remote Bolus Entry' + , name: 'Remote Bolus Entry' + , remoteBolus: true + , otp: true + , submitHook: postLoopNotification + } + ]; + }; + + // TODO: Add event listener to customize labels + + + loop.updateVisualisation = function updateVisualisation (sbx) { + var prop = sbx.properties.loop; + + var prefs = loop.getPrefs(sbx); + + function valueString (prefix, value) { + return (value != null) ? prefix + value : ''; + } + + var events = []; + + function addRecommendedTempBasal () { + if (prop.lastLoop && prop.lastLoop.recommendedTempBasal) { + + var recommendedTempBasal = prop.lastLoop.recommendedTempBasal; + + var valueParts = [ + 'Suggested Temp: ' + recommendedTempBasal.rate + 'U/hour for ' + + recommendedTempBasal.duration + 'm' + ]; + + valueParts = concatIOB(valueParts); + valueParts = concatCOB(valueParts); + valueParts = concatEventualBG(valueParts); + valueParts = concatRecommendedBolus(valueParts); + + events.push({ + time: moment(recommendedTempBasal.timestamp) + , value: valueParts.join('') + }); + } + } + + function addRSSI () { + + var mostRecent = ""; + var pumpRSSI = ""; + var bleRSSI = ""; + var reportRSSI = ""; + + _.forEach(sbx.data.devicestatus, function(entry) { + + if (entry.radioAdapter) { + var entryMoment = moment(entry.created_at); + + if (mostRecent == "") { + mostRecent = entryMoment; + if (entry.radioAdapter.pumpRSSI) { + pumpRSSI = entry.radioAdapter.pumpRSSI; + } + if (entry.radioAdapter.RSSI) { + bleRSSI = entry.radioAdapter.RSSI; + } + } + + if (mostRecent < entryMoment) { + mostRecent = entryMoment; + if (entry.radioAdapter.pumpRSSI) { + pumpRSSI = entry.radioAdapter.pumpRSSI; + } + if (entry.radioAdapter.RSSI) { + bleRSSI = entry.radioAdapter.RSSI; + } + } + } + }); + + if (bleRSSI != "") { + reportRSSI = "BLE RSSI: " + bleRSSI + " "; + } + + if (pumpRSSI != "") { + reportRSSI = reportRSSI + "Pump RSSI: " + pumpRSSI; + } + + if (reportRSSI != "") { + events.push({ + time: mostRecent + , value: reportRSSI + }); + } + + } + + function addLastEnacted () { + if (prop.lastEnacted) { + var valueParts = [] + + if (prop.lastEnacted.bolusVolume) { + valueParts.push('Automatic Bolus') + valueParts.push(' ' + prop.lastEnacted.bolusVolume + 'U') + if (prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0) { + valueParts.push(' (Temp Basal Canceled)') + } + } else if (prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0) { + valueParts.push('Temp Basal Canceled') + } else if (prop.lastEnacted.rate != null) { + valueParts.push('Temp Basal Started') + valueParts.push(' ' + prop.lastEnacted.rate.toFixed(2) + 'U/hour for ' + prop.lastEnacted.duration + 'm') + } + valueParts.push(valueString(', ', prop.lastEnacted.reason)) + + valueParts = concatIOB(valueParts); + valueParts = concatCOB(valueParts); + valueParts = concatEventualBG(valueParts); + valueParts = concatRecommendedBolus(valueParts); + + events.push({ + time: prop.lastEnacted.moment + , value: valueParts.join('') + }); + } + } + + function concatIOB (valueParts) { + if (prop.lastLoop && prop.lastLoop.iob) { + var iob = prop.lastLoop.iob; + valueParts = valueParts.concat([ + ', IOB: ' + + , sbx.roundInsulinForDisplayFormat(iob.iob) + 'U' + + , iob.basaliob ? ', Basal IOB ' + sbx.roundInsulinForDisplayFormat(iob.basaliob) + 'U' : '' + ]); + } + + return valueParts; + } + + function concatCOB (valueParts) { + if (prop.lastLoop && prop.lastLoop.cob) { + var cob = prop.lastLoop.cob.cob; + cob = Math.round(cob); + valueParts = valueParts.concat([ + ', COB: ' + , cob + 'g' + ]); + } + + return valueParts; + } + + function concatEventualBG (valueParts) { + if (prop.lastLoop && prop.lastLoop.predicted) { + var predictedBGvalues = prop.lastLoop.predicted.values; + var eventualBG = predictedBGvalues[predictedBGvalues.length - 1]; + var maxBG = Math.max.apply(null, predictedBGvalues); + var minBG = Math.min.apply(null, predictedBGvalues); + var eventualBGscaled = sbx.settings.units === 'mmol' ? + sbx.roundBGToDisplayFormat(sbx.scaleMgdl(eventualBG)) : eventualBG; + var maxBGscaled = sbx.settings.units === 'mmol' ? + sbx.roundBGToDisplayFormat(sbx.scaleMgdl(maxBG)) : maxBG; + var minBGscaled = sbx.settings.units === 'mmol' ? + sbx.roundBGToDisplayFormat(sbx.scaleMgdl(minBG)) : minBG; + + valueParts = valueParts.concat([ + ', Predicted Min-Max BG: ' + , minBGscaled + , '-' + , maxBGscaled + , ', Eventual BG: ' + , eventualBGscaled + ]); + } + + return valueParts; + } + + function concatRecommendedBolus (valueParts) { + if (prop.lastLoop && prop.lastLoop.recommendedBolus) { + var recommendedBolus = prop.lastLoop.recommendedBolus; + valueParts = valueParts.concat([ + ', Recommended Bolus: ' + , recommendedBolus + 'U' + ]); + } + + return valueParts; + } + + function getForecastPoints () { + var points = []; + + function toPoints (startTime, offset) { + return function toPoint (value, index) { + return { + mgdl: value + , color: '#ff00ff' + , mills: startTime.valueOf() + times.mins(5 * index).msecs + offset + , noFade: true + }; + }; + } + + if (prop.lastPredicted) { + var predicted = prop.lastPredicted; + var startTime = moment(predicted.startDate); + if (predicted.values) { + points = points.concat(_.map(predicted.values, toPoints(startTime, 0))); + } + } + + return points; + } + + if ('error' === prop.display.code) { + events.push({ + time: prop.lastLoop.moment + , value: valueString('Error: ', prop.lastLoop.failureReason) + }); + addRecommendedTempBasal(); + } else if ('enacted' === prop.display.code) { + addLastEnacted(); + } else if ('looping' === prop.display.code) { + addLastEnacted(); + } else { + addRecommendedTempBasal(); + } + + addRSSI(); + + var sorted = _.sortBy(events, function toMill (event) { + return event.time.valueOf(); + }).reverse(); + + var info = _.map(sorted, function eventToInfo (event) { + return { + label: utils.timeAt(false, sbx) + utils.timeFormat(event.time, sbx) + , value: event.value + }; + }); + + var loopName = 'Loop'; + + if (prop.lastLoop && prop.lastLoop.name) { + loopName = prop.lastLoop.name; + } + + var eventualBGValue = ''; + if (prop.lastLoop && prop.lastLoop.predicted) { + var predictedBGvalues = prop.lastLoop.predicted.values; + var eventualBG = predictedBGvalues[predictedBGvalues.length - 1]; + if (sbx.settings.units === 'mmol') { + eventualBG = sbx.roundBGToDisplayFormat(sbx.scaleMgdl(eventualBG)); + } + eventualBGValue = ' ↝ ' + eventualBG; + } + + var label = loopName + ' ' + prop.display.symbol; + + var lastLoopValue = prop.lastLoop ? + utils.timeFormat(prop.lastLoop.moment, sbx) + eventualBGValue : null; + + sbx.pluginBase.updatePillText(loop, { + value: lastLoopValue + , label: label + , info: info + , pillClass: statusClass(prop, prefs, sbx) + }); + + var forecastPoints = getForecastPoints(); + if (forecastPoints && forecastPoints.length > 0) { + sbx.pluginBase.addForecastPoints(forecastPoints, { type: 'loop', label: 'Loop Forecasts' }); + } + }; + + function virtAsstForecastHandler (next, slots, sbx) { + var predicted = _.get(sbx, 'properties.loop.lastLoop.predicted'); + if (predicted) { + var forecast = predicted.values; + var max = forecast[0]; + var min = forecast[0]; + var maxForecastIndex = Math.min(6, forecast.length); + + var startPrediction = moment(predicted.startDate); + var endPrediction = startPrediction.clone().add(maxForecastIndex * 5, 'minutes'); + if (endPrediction.valueOf() < sbx.time) { + next(translate('virtAsstTitleLoopForecast'), translate('virtAsstForecastUnavailable')); + } else { + for (var i = 1, len = forecast.slice(0, maxForecastIndex).length; i < len; i++) { + if (forecast[i] > max) { + max = forecast[i]; + } + if (forecast[i] < min) { + min = forecast[i]; + } + } + var response = ''; + if (min === max) { + response = translate('virtAsstLoopForecastAround', { + params: [ + max + , moment(endPrediction).from(moment(sbx.time)) + ] + }); + } else { + response = translate('virtAsstLoopForecastBetween', { + params: [ + min + , max + , moment(endPrediction).from(moment(sbx.time)) + ] + }); + } + next(translate('virtAsstTitleLoopForecast'), response); + } + } else { + next(translate('virtAsstTitleLoopForecast'), translate('virtAsstUnknown')); + } + } + + function virtAsstLastLoopHandler (next, slots, sbx) { + var lastLoop = _.get(sbx, 'properties.loop.lastLoop') + if (lastLoop) { + console.log(JSON.stringify(lastLoop)); + var response = translate('virtAsstLastLoop', { + params: [ + moment(sbx.properties.loop.lastOkMoment).from(moment(sbx.time)) + ] + }); + next(translate('virtAsstTitleLastLoop'), response); + } else { + next(translate('virtAsstTitleLastLoop'), translate('virtAsstUnknown')); + } + } + + loop.virtAsst = { + intentHandlers: [{ + intent: 'MetricNow' + , metrics: ['loop forecast', 'forecast'] + , intentHandler: virtAsstForecastHandler + }, { + intent: 'LastLoop' + , intentHandler: virtAsstLastLoopHandler + }] + }; + + function statusClass (prop, prefs, sbx) { + var level = statusLevel(prop, prefs, sbx); + var cls = 'current'; + + if (level === levels.WARN) { + cls = 'warn'; + } else if (level === levels.URGENT) { + cls = 'urgent'; + } + + return cls; + } + + function statusLevel (prop, prefs, sbx) { + var level = levels.NONE; + var now = moment(sbx.time); + + if (prop.lastOkMoment) { + var urgentTime = prop.lastOkMoment.clone().add(prefs.urgent, 'minutes'); + var warningTime = prop.lastOkMoment.clone().add(prefs.warn, 'minutes'); + + if (urgentTime.isBefore(now)) { + level = levels.URGENT; + } else if (warningTime.isBefore(now)) { + level = levels.WARN; + } + } + + return level; + } + + return loop; + +} + +module.exports = init; diff --git a/lib/plugins/maker-setup.md b/lib/plugins/maker-setup.md deleted file mode 100644 index a7d89084376..00000000000 --- a/lib/plugins/maker-setup.md +++ /dev/null @@ -1,88 +0,0 @@ - - -**Table of Contents** - -- [Nightscout/IFTTT Maker](#nightscoutifttt-maker) - - [Overview](#overview) - - [Events](#events) - - [Configuration](#configuration) - - [Create a recipe](#create-a-recipe) - - [Start [creating a recipe](https://ifttt.com/myrecipes/personal/new)](#start-creating-a-recipehttpsiftttcommyrecipespersonalnew) - - [1. Choose a Trigger Channel](#1-choose-a-trigger-channel) - - [2. Choose a Trigger](#2-choose-a-trigger) - - [3. Complete Trigger Fields](#3-complete-trigger-fields) - - [4. That](#4-that) - - [5. Choose an Action](#5-choose-an-action) - - [6. Complete Action Fields](#6-complete-action-fields) - - [7. Create and Connect](#7-create-and-connect) - - [Result](#result) - - - -Nightscout/IFTTT Maker -====================================== - -## Overview - - In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Maker](https://ifttt.com/maker). - - With Maker you are able to integrate with all the other [IFTTT Channels](https://ifttt.com/channels). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more. - -## Events - - Plugins can create custom events, but all events sent to maker will be prefixed with `ns-`. The core events are: - - * `ns-event` - This event is sent to the maker service for all alarms and notifications. This is good catch all event for general logging. - * `ns-allclear` - This event is sent to the maker service when an alarm has been ack'd or when the server starts up without triggering any alarms. For example, you could use this event to turn a light to green. - * `ns-info` - Plugins that generate notifications at the info level will cause this event to also be triggered. It will be sent in addition to `ns-event`. - * `ns-warning` - Alarms at the warning level with cause this event to also be triggered. It will be sent in addition to `ns-event`. - * `ns-urgent` - Alarms at the urgent level with cause this event to also be triggered. It will be sent in addition to `ns-event`. - * `ns-warning-high` - Alarms at the warning level with cause this event to also be triggered. It will be sent in addition to `ns-event` and `ns-warning`. - * `ns-urgent-high` - Alarms at the urgent level with cause this event to also be triggered. It will be sent in addition to `ns-event` and `ns-urgent`. - * `ns-warning-low` - Alarms at the warning level with cause this event to also be triggered. It will be sent in addition to `ns-event` and `ns-warning`. - * `ns-urgent-low` - Alarms at the urgent level with cause this event to also be triggered. It will be sent in addition to `ns-event` and `ns-urgent`. - * `ns-info-treatmentnotify` - When a treatment is entered into the care portal this event is triggered. It will be sent in addition to `ns-event` and `ns-info`. - * `ns-warning-bwp` - When the BWP plugin generates a warning alarm. It will be sent in addition to `ns-event` and `ns-warning`. - * `ns-urgent-bwp` - When the BWP plugin generates an urgent alarm. It will be sent in addition to `ns-event` and `ns-urget`. - -## Configuration - - 1. Setup IFTTT account: [login](https://ifttt.com/login) or [create an account](https://ifttt.com/join) - 2. Find your secret key on the [maker page](https://ifttt.com/maker) - 3. Configure Nightscout by setting these environment variables: - * `ENABLE` - `maker` should be added to the list of plugin, for example: `ENABLE="maker"`. - * `MAKER_KEY` - Set this to your secret key that you located in step 2, for example: `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` - -## Create a recipe - -### Start [creating a recipe](https://ifttt.com/myrecipes/personal/new) -![screen shot 2015-06-29 at 10 58 48 pm](https://cloud.githubusercontent.com/assets/751143/8425240/bab51986-1eb8-11e5-88fb-5aed311896be.png) - -### 1. Choose a Trigger Channel - ![screen shot 2015-06-29 at 10 59 01 pm](https://cloud.githubusercontent.com/assets/751143/8425243/c007ace6-1eb8-11e5-96d1-b13f9c3d071f.png) - -### 2. Choose a Trigger - ![screen shot 2015-06-29 at 10 59 18 pm](https://cloud.githubusercontent.com/assets/751143/8425246/c77c5a4e-1eb8-11e5-9084-32ae40518ee0.png) - -### 3. Complete Trigger Fields - ![screen shot 2015-06-29 at 10 59 33 pm](https://cloud.githubusercontent.com/assets/751143/8425249/ced7b450-1eb8-11e5-95a3-730f6b9b2925.png) - -### 4. That - ![screen shot 2015-06-29 at 10 59 46 pm](https://cloud.githubusercontent.com/assets/751143/8425251/d46e1dc8-1eb8-11e5-91be-8dc731e308b2.png) - -### 5. Choose an Action - ![screen shot 2015-06-29 at 11 00 12 pm](https://cloud.githubusercontent.com/assets/751143/8425254/de634844-1eb8-11e5-8f09-cd43c41ccf3f.png) - -### 6. Complete Action Fields - **Example:** `Nightscout: {{Value1}} {{Value2}} {{Value3}}` - - ![screen shot 2015-06-29 at 11 02 14 pm](https://cloud.githubusercontent.com/assets/751143/8425267/f2da6dd4-1eb8-11e5-8e4d-cad2590d111f.png) - ![screen shot 2015-06-29 at 11 02 21 pm](https://cloud.githubusercontent.com/assets/751143/8425272/f83ceb62-1eb8-11e5-8ea2-afd4dcbd391f.png) - -### 7. Create and Connect - ![screen shot 2015-06-29 at 11 02 43 pm](https://cloud.githubusercontent.com/assets/751143/8425277/fe52f618-1eb8-11e5-8d7f-e0b34eebe29a.png) - -### Result - ![cinpiqkumaa33u7](https://cloud.githubusercontent.com/assets/751143/8425925/e7d08d2c-1ebf-11e5-853c-cdc5381c4186.png) - - diff --git a/lib/plugins/mmconnect.js b/lib/plugins/mmconnect.js index 01d430f9537..c6321e9f7f3 100644 --- a/lib/plugins/mmconnect.js +++ b/lib/plugins/mmconnect.js @@ -4,27 +4,33 @@ var _ = require('lodash'), connect = require('minimed-connect-to-nightscout'); -function init (env, entries) { +function init (env, entries, devicestatus, bus) { if (env.extendedSettings.mmconnect && env.extendedSettings.mmconnect.userName && env.extendedSettings.mmconnect.password) { - return {run: makeRunner(env, entries)}; + return {run: makeRunner(env, entries, devicestatus, bus)}; } else { console.info('MiniMed Connect not enabled'); return null; } } -function makeRunner (env, entries) { +function makeRunner (env, entries, devicestatus, bus) { var options = getOptions(env); var client = connect.carelink.Client(options); connect.logger.setVerbose(options.verbose); - var handleData = makeHandler_(entries, options.sgvLimit, makeRecentSgvFilter(), options.storeRawData); + var handleData = makeHandler_(entries, devicestatus, options.sgvLimit, options.storeRawData); return function run () { - setInterval(function() { + let timer = setInterval(function() { client.fetch(handleData); }, options.interval); + + if (bus) { + bus.on('teardown', function serverTeardown () { + clearInterval(timer); + }); + } }; } @@ -40,50 +46,52 @@ function getOptions (env) { }; } -function makeHandler_ (entries, sgvLimit, filter, storeRawData) { +function makeHandler_ (entries, devicestatus, sgvLimit, storeRawData) { + var filterSgvs = connect.filter.makeRecencyFilter(function(item) { + return item['date']; + }); + var filterDevicestatus = connect.filter.makeRecencyFilter(function(item) { + return new Date(item['created_at']).getTime(); + }); + return function handleCarelinkData (err, data) { if (err) { console.error('MiniMed Connect error: ' + err); } else { var transformed = connect.transform(data, sgvLimit); - if (storeRawData && transformed.length) { - transformed.push(rawDataEntry(data)); + if (storeRawData && (transformed.entries.length || transformed.devicestatus.length)) { + transformed.entries.push(rawDataEntry(data)); } // If we blindly upsert the SGV entries, we will lose trend data for // entries we've already stored, since all SGVs from CareLink except // the most recent are missing trend data. - var filtered = filter(transformed); + var filteredSgvs = filterSgvs(transformed.entries); - entries.create(filtered, function afterCreate (err) { - if (err) { - console.error('MiniMed Connect storage error: ' + err); - } + // The devicestatus collection doesn't upsert, so we need to avoid + // duplicates here + var filteredStatus = filterDevicestatus(transformed.devicestatus); + + // Can't do "bulk" insert, must be done serially + createMaybe_(entries, filteredSgvs, function() { + createMaybe_(devicestatus, filteredStatus, _.noop); }); } }; } -function makeRecentSgvFilter () { - var lastSgvDate = 0; - - return function filter (entries) { - var out = []; - - entries.forEach(function(entry) { - if (entry['type'] !== 'sgv' || entry['date'] > lastSgvDate) { - out.push(entry); +function createMaybe_ (collection, items, callback) { + if (items.length === 0) { + callback(); + } else { + collection.create(items, function afterCreate (err) { + if (err) { + console.error('MiniMed Connect storage error: ' + err); } + callback(); }); - - out.filter(function(e) { return e['type'] === 'sgv'; }) - .forEach(function(e) { - lastSgvDate = Math.max(lastSgvDate, e['date']); - }); - - return out; - }; + } } function rawDataEntry (data) { @@ -110,6 +118,5 @@ module.exports = { init: init // exposed for testing , getOptions: getOptions - , makeRecentSgvFilter: makeRecentSgvFilter , rawDataEntry: rawDataEntry }; diff --git a/lib/plugins/openaps.js b/lib/plugins/openaps.js new file mode 100644 index 00000000000..81e7bc25a05 --- /dev/null +++ b/lib/plugins/openaps.js @@ -0,0 +1,630 @@ +'use strict'; + +var _ = require('lodash'); +var times = require('../times'); +var consts = require('../constants'); + +// var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'meal-assist', 'freq', 'rssi']; Unused variable + +function init (ctx) { + var moment = ctx.moment; + var utils = require('../utils')(ctx); + var openaps = { + name: 'openaps' + , label: 'OpenAPS' + , pluginType: 'pill-status' + }; + var translate = ctx.language.translate; + var firstPrefs = true; + var levels = ctx.levels; + + openaps.getClientPrefs = function getClientPrefs() { + return ([{ + label: "Color prediction lines", + id: "colorPredictionLines", + type: "boolean" + }]); + } + + openaps.getPrefs = function getPrefs (sbx) { + + function cleanList (value) { + return decodeURIComponent(value || '').toLowerCase().split(' '); + } + + function isEmpty (list) { + return _.isEmpty(list) || _.isEmpty(list[0]); + } + + const settings = sbx.extendedSettings || {}; + + var fields = cleanList(settings.fields); + fields = isEmpty(fields) ? ['status-symbol', 'status-label', 'iob', 'meal-assist', 'rssi'] : fields; + + var retroFields = cleanList(settings.retroFields); + retroFields = isEmpty(retroFields) ? ['status-symbol', 'status-label', 'iob', 'meal-assist', 'rssi'] : retroFields; + + if (typeof settings.colorPredictionLines == 'undefined') { + settings.colorPredictionLines = true; + } + + var prefs = { + fields: fields + , retroFields: retroFields + , warn: settings.warn ? settings.warn : 30 + , urgent: settings.urgent ? settings.urgent : 60 + , enableAlerts: settings.enableAlerts + , predIOBColor: settings.predIobColor ? settings.predIobColor : '#1e88e5' + , predCOBColor: settings.predCobColor ? settings.predCobColor : '#FB8C00' + , predACOBColor: settings.predAcobColor ? settings.predAcobColor : '#FB8C00' + , predZTColor: settings.predZtColor ? settings.predZtColor : '#00d2d2' + , predUAMColor: settings.predUamColor ? settings.predUamColor : '#c9bd60' + , colorPredictionLines: settings.colorPredictionLines + }; + + if (firstPrefs) { + firstPrefs = false; + } + + return prefs; + }; + + openaps.setProperties = function setProperties (sbx) { + sbx.offerProperty('openaps', function setOpenAPS () { + return openaps.analyzeData(sbx); + }); + }; + + openaps.analyzeData = function analyzeData (sbx) { + var recentHours = 6; //TODO dia*2 + var recentMills = sbx.time - times.hours(recentHours).msecs; + + var recentData = _.chain(sbx.data.devicestatus) + .filter(function(status) { + return ('openaps' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills; + }) + .map(function(status) { + if (status.openaps && _.isArray(status.openaps.iob) && status.openaps.iob.length > 0) { + status.openaps.iob = status.openaps.iob[0]; + if (status.openaps.iob.time) { + status.openaps.iob.timestamp = status.openaps.iob.time; + } + } + return status; + }) + .value(); + + var prefs = openaps.getPrefs(sbx); + var recent = moment(sbx.time).subtract(prefs.warn / 2, 'minutes'); + + var result = { + seenDevices: {} + , lastEnacted: null + , lastNotEnacted: null + , lastSuggested: null + , lastIOB: null + , lastMMTune: null + , lastPredBGs: null + }; + + function getDevice (status) { + var uri = status.device || 'device'; + var device = result.seenDevices[uri]; + + if (!device) { + device = { + name: utils.deviceName(uri) + , uri: uri + }; + + result.seenDevices[uri] = device; + } + return device; + } + + function toMoments (status) { + var enacted = false; + var notEnacted = false; + if (status.openaps.enacted && status.openaps.enacted.timestamp && (status.openaps.enacted.recieved || status.openaps.enacted.received)) { + if (status.openaps.enacted.mills) { + enacted = moment(status.openaps.enacted.mills); + } else { + enacted = moment(status.openaps.enacted.timestamp); + } + } else if (status.openaps.enacted && status.openaps.enacted.timestamp && !(status.openaps.enacted.recieved || status.openaps.enacted.received)) { + if (status.openaps.enacted.mills) { + notEnacted = moment(status.openaps.enacted.mills) + } else { + notEnacted = moment(status.openaps.enacted.timestamp) + } + } + + var suggested = false; + if (status.openaps.suggested && status.openaps.suggested.mills) { + suggested = moment(status.openaps.suggested.mills); + } else if (status.openaps.suggested && status.openaps.suggested.timestamp) { + suggested = moment(status.openaps.suggested.timestamp); + } + + var iob = false; + if (status.openaps.iob && status.openaps.iob.mills) { + iob = moment(status.openaps.iob.mills); + } else if (status.openaps.iob && status.openaps.iob.timestamp) { + iob = moment(status.openaps.iob.timestamp); + } + + return { + when: moment(status.mills) + , enacted + , notEnacted + , suggested + , iob + }; + } + + function momentsToLoopStatus (moments, noWarning) { + + var status = { + symbol: '⚠' + , code: 'warning' + , label: 'Warning' + }; + + if (moments.notEnacted && ( + (moments.enacted && moments.notEnacted.isAfter(moments.enacted)) || (!moments.enacted && moments.notEnacted.isAfter(recent)))) { + status.symbol = 'x'; + status.code = 'notenacted'; + status.label = 'Not Enacted'; + } else if (moments.enacted && moments.enacted.isAfter(recent)) { + status.symbol = '⌁'; + status.code = 'enacted'; + status.label = 'Enacted'; + } else if (moments.suggested && moments.suggested.isAfter(recent)) { + status.symbol = '↻'; + status.code = 'looping'; + status.label = 'Looping'; + } else if (moments.when && (noWarning || moments.when.isAfter(recent))) { + status.symbol = '◉'; + status.code = 'waiting'; + status.label = 'Waiting'; + } + + return status; + } + + _.forEach(recentData, function eachStatus (status) { + var device = getDevice(status); + + var moments = toMoments(status); + var loopStatus = momentsToLoopStatus(moments, true); + + if (!device.status || moments.when.isAfter(device.status.when)) { + device.status = loopStatus; + device.status.when = moments.when; + } + + var enacted = status.openaps && status.openaps.enacted; + if (enacted && moments.enacted && (!result.lastEnacted || moments.enacted.isAfter(result.lastEnacted.moment))) { + if (enacted.mills) { + enacted.moment = moment(enacted.mills); + } else { + enacted.moment = moment(enacted.timestamp); + } + result.lastEnacted = enacted; + if (enacted.predBGs && (!result.lastPredBGs || enacted.moment.isAfter(result.lastPredBGs.moment))) { + result.lastPredBGs = _.isArray(enacted.predBGs) ? { values: enacted.predBGs } : enacted.predBGs; + result.lastPredBGs.moment = enacted.moment; + } + } + + if (enacted && moments.notEnacted && (!result.lastNotEnacted || moments.notEnacted.isAfter(result.lastNotEnacted.moment))) { + if (enacted.mills) { + enacted.moment = moment(enacted.mills); + } else { + enacted.moment = moment(enacted.timestamp); + } + result.lastNotEnacted = enacted; + } + + var suggested = status.openaps && status.openaps.suggested; + if (suggested && moments.suggested && (!result.lastSuggested || moments.suggested.isAfter(result.lastSuggested.moment))) { + if (suggested.mills) { + suggested.moment = moment(suggested.mills); + } else { + suggested.moment = moment(suggested.timestamp); + } + result.lastSuggested = suggested; + if (suggested.predBGs && (!result.lastPredBGs || suggested.moment.isAfter(result.lastPredBGs.moment))) { + result.lastPredBGs = _.isArray(suggested.predBGs) ? { values: suggested.predBGs } : suggested.predBGs; + result.lastPredBGs.moment = suggested.moment; + } + } + + var iob = status.openaps && status.openaps.iob; + if (moments.iob && (!result.lastIOB || moment(iob.timestamp).isAfter(result.lastIOB.moment))) { + iob.moment = moments.iob; + result.lastIOB = iob; + } + + if (status.mmtune && status.mmtune.timestamp) { + status.mmtune.moment = moment(status.mmtune.timestamp); + if (!device.mmtune || moments.when.isAfter(device.mmtune.moment)) { + device.mmtune = status.mmtune; + } + } + }); + + if (result.lastEnacted && result.lastSuggested) { + if (result.lastEnacted.moment.isAfter(result.lastSuggested.moment)) { + result.lastLoopMoment = result.lastEnacted.moment; + result.lastEventualBG = result.lastEnacted.eventualBG; + } else { + result.lastLoopMoment = result.lastSuggested.moment; + result.lastEventualBG = result.lastSuggested.eventualBG; + } + } else if (result.lastEnacted && result.lastEnacted.moment) { + result.lastLoopMoment = result.lastEnacted.moment; + result.lastEventualBG = result.lastEnacted.eventualBG; + } else if (result.lastSuggested && result.lastSuggested.moment) { + result.lastLoopMoment = result.lastSuggested.moment; + result.lastEventualBG = result.lastSuggested.eventualBG; + } + + result.status = momentsToLoopStatus({ + enacted: result.lastEnacted && result.lastEnacted.moment + , notEnacted: result.lastNotEnacted && result.lastNotEnacted.moment + , suggested: result.lastSuggested && result.lastSuggested.moment + }, false, recent); + + return result; + }; + + openaps.getEventTypes = function getEventTypes (sbx) { + + var units = sbx.settings.units; + console.log('units', units); + + var reasonconf = []; + + if (units == 'mmol') { + reasonconf.push({ name: translate('Eating Soon'), targetTop: 4.5, targetBottom: 4.5, duration: 60 }); + reasonconf.push({ name: translate('Activity'), targetTop: 8, targetBottom: 6.5, duration: 120 }); + } else { + reasonconf.push({ name: translate('Eating Soon'), targetTop: 80, targetBottom: 80, duration: 60 }); + reasonconf.push({ name: translate('Activity'), targetTop: 140, targetBottom: 120, duration: 120 }); + } + + reasonconf.push({ name: 'Manual' }); + + return [ + { + val: 'Temporary Target' + , name: 'Temporary Target' + , bg: false + , insulin: false + , carbs: false + , prebolus: false + , duration: true + , percent: false + , absolute: false + , profile: false + , split: false + , targets: true + , reasons: reasonconf + } + , { + val: 'Temporary Target Cancel' + , name: 'Temporary Target Cancel' + , bg: false + , insulin: false + , carbs: false + , prebolus: false + , duration: false + , percent: false + , absolute: false + , profile: false + , split: false + } + , { + val: 'OpenAPS Offline' + , name: 'OpenAPS Offline' + , bg: false + , insulin: false + , carbs: false + , prebolus: false + , duration: true + , percent: false + , absolute: false + , profile: false + , split: false + } + ]; + }; + + openaps.checkNotifications = function checkNotifications (sbx) { + var prefs = openaps.getPrefs(sbx); + + if (!prefs.enableAlerts) { return; } + + var prop = sbx.properties.openaps; + + if (!prop.lastLoopMoment) { + console.info('OpenAPS hasn\'t reported a loop yet'); + return; + } + + var now = moment(); + var level = statusLevel(prop, prefs, sbx); + if (level >= levels.WARN) { + sbx.notifications.requestNotify({ + level: level + , title: 'OpenAPS isn\'t looping' + , message: 'Last Loop: ' + utils.formatAgo(prop.lastLoopMoment, now.valueOf()) + , pushoverSound: 'echo' + , group: 'OpenAPS' + , plugin: openaps + , debug: prop + }); + } + }; + + openaps.findOfflineMarker = function findOfflineMarker (sbx) { + return _.findLast(sbx.data.treatments, function match (treatment) { + var eventTime = sbx.entryMills(treatment); + var eventEnd = treatment.duration ? eventTime + times.mins(treatment.duration).msecs : eventTime; + return eventTime <= sbx.time && treatment.eventType === 'OpenAPS Offline' && eventEnd >= sbx.time; + }); + }; + + openaps.updateVisualisation = function updateVisualisation (sbx) { + var prop = sbx.properties.openaps; + + var prefs = openaps.getPrefs(sbx); + + var selectedFields = sbx.data.inRetroMode ? prefs.retroFields : prefs.fields; + + function valueString (prefix, value) { + return value ? prefix + value : ''; + } + + var events = []; + + function addSuggestion () { + if (prop.lastSuggested) { + var bg = prop.lastSuggested.bg; + var units = sbx.data.profile.getUnits(); + + if (units === 'mmol') { + bg = Math.round(bg / consts.MMOL_TO_MGDL * 10) / 10; + } + + var valueParts = [ + valueString('BG: ', bg) + , valueString(', ', prop.lastSuggested.reason) + , prop.lastSuggested.sensitivityRatio ? ', Sensitivity Ratio: ' + prop.lastSuggested.sensitivityRatio : '' + ]; + + if (_.includes(selectedFields, 'iob')) { + valueParts = concatIOB(valueParts); + } + + events.push({ + time: prop.lastSuggested.moment + , value: valueParts.join('') + }); + } + } + + function concatIOB (valueParts) { + if (prop.lastIOB) { + valueParts = valueParts.concat([ + ', IOB: ' + , sbx.roundInsulinForDisplayFormat(prop.lastIOB.iob) + 'U' + , prop.lastIOB.basaliob ? ', Basal IOB ' + sbx.roundInsulinForDisplayFormat(prop.lastIOB.basaliob) + 'U' : '' + , prop.lastIOB.bolusiob ? ', Bolus IOB ' + sbx.roundInsulinForDisplayFormat(prop.lastIOB.bolusiob) + 'U' : '' + ]); + } + + return valueParts; + } + + function getForecastPoints () { + var points = []; + + function toPoints (offset, forecastType) { + return function toPoint (value, index) { + var colors = { + 'Values': '#ff00ff' + , 'IOB': prefs.predIOBColor + , 'Zero-Temp': prefs.predZTColor + , 'COB': prefs.predCOBColor + , 'Accel-COB': prefs.predACOBColor + , 'UAM': prefs.predUAMColor + } + + return { + mgdl: value + , color: prefs.colorPredictionLines ? colors[forecastType] : '#ff00ff' + , mills: prop.lastPredBGs.moment.valueOf() + times.mins(5 * index).msecs + offset + , noFade: true + , forecastType: forecastType + }; + }; + } + + if (prop.lastPredBGs) { + if (prop.lastPredBGs.values) { + points = points.concat(_.map(prop.lastPredBGs.values, toPoints(0, "Values"))); + } + if (prop.lastPredBGs.IOB) { + points = points.concat(_.map(prop.lastPredBGs.IOB, toPoints(3333, "IOB"))); + } + if (prop.lastPredBGs.ZT) { + points = points.concat(_.map(prop.lastPredBGs.ZT, toPoints(4444, "Zero-Temp"))); + } + if (prop.lastPredBGs.aCOB) { + points = points.concat(_.map(prop.lastPredBGs.aCOB, toPoints(5555, "Accel-COB"))); + } + if (prop.lastPredBGs.COB) { + points = points.concat(_.map(prop.lastPredBGs.COB, toPoints(7777, "COB"))); + } + if (prop.lastPredBGs.UAM) { + points = points.concat(_.map(prop.lastPredBGs.UAM, toPoints(9999, "UAM"))); + } + } + + return points; + } + + if ('enacted' === prop.status.code) { + var canceled = prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0; + + var valueParts = [ + valueString('BG: ', prop.lastEnacted.bg) + , ', Temp Basal' + (canceled ? ' Canceled' : ' Started') + '' + , canceled ? '' : ' ' + prop.lastEnacted.rate.toFixed(2) + ' for ' + prop.lastEnacted.duration + 'm' + , valueString(', ', prop.lastEnacted.reason) + , prop.lastEnacted.mealAssist && _.includes(selectedFields, 'meal-assist') ? ' Meal Assist: ' + prop.lastEnacted.mealAssist : '' + ]; + + if (prop.lastSuggested && prop.lastSuggested.moment.isAfter(prop.lastEnacted.moment)) { + addSuggestion(); + } else { + valueParts = concatIOB(valueParts); + } + + events.push({ + time: prop.lastEnacted.moment + , value: valueParts.join('') + }); + } else { + addSuggestion(); + } + + _.forIn(prop.seenDevices, function seenDevice (device) { + var deviceInfo = [device.name]; + + if (_.includes(selectedFields, 'status-symbol')) { + deviceInfo.push(device.status.symbol); + } + + if (_.includes(selectedFields, 'status-label')) { + deviceInfo.push(device.status.label); + } + + if (device.mmtune) { + var best = _.maxBy(device.mmtune.scanDetails, function(d) { + return d[2]; + }); + + if (_.includes(selectedFields, 'freq')) { + deviceInfo.push(device.mmtune.setFreq + 'MHz'); + } + if (best && best.length > 2 && _.includes(selectedFields, 'rssi')) { + deviceInfo.push('@ ' + best[2] + 'dB'); + } + } + events.push({ + time: device.status.when + , value: deviceInfo.join(' ') + }); + }); + + var sorted = _.sortBy(events, function toMill (event) { + return event.time.valueOf(); + }).reverse(); + + var info = _.map(sorted, function eventToInfo (event) { + return { + label: utils.timeAt(false, sbx) + utils.timeFormat(event.time, sbx) + , value: event.value + }; + }); + + var label = 'OpenAPS'; + if (_.includes(selectedFields, 'status-symbol')) { + label += ' ' + prop.status.symbol; + } + + sbx.pluginBase.updatePillText(openaps, { + value: utils.timeFormat(prop.lastLoopMoment, sbx) + , label: label + , info: info + , pillClass: statusClass(prop, prefs, sbx) + }); + + var forecastPoints = getForecastPoints(); + if (forecastPoints && forecastPoints.length > 0) { + sbx.pluginBase.addForecastPoints(forecastPoints, { type: 'openaps', label: 'OpenAPS Forecasts' }); + } + }; + + function virtAsstForecastHandler (next, slots, sbx) { + var lastEventualBG = _.get(sbx, 'properties.openaps.lastEventualBG'); + if (lastEventualBG) { + var response = translate('virtAsstOpenAPSForecast', { + params: [ + lastEventualBG + ] + }); + next(translate('virtAsstTitleOpenAPSForecast'), response); + } else { + next(translate('virtAsstTitleOpenAPSForecast'), translate('virtAsstUnknown')); + } + } + + function virtAsstLastLoopHandler (next, slots, sbx) { + var lastLoopMoment = _.get(sbx, 'properties.openaps.lastLoopMoment'); + if (lastLoopMoment) { + var response = translate('virtAsstLastLoop', { + params: [ + moment(lastLoopMoment).from(moment(sbx.time)) + ] + }); + next(translate('virtAsstTitleLastLoop'), response); + } else { + next(translate('virtAsstTitleLastLoop'), translate('virtAsstUnknown')); + } + } + + openaps.virtAsst = { + intentHandlers: [{ + intent: 'MetricNow' + , metrics: ['openaps forecast', 'forecast'] + , intentHandler: virtAsstForecastHandler + }, { + intent: 'LastLoop' + , intentHandler: virtAsstLastLoopHandler + }] + }; + + function statusClass (prop, prefs, sbx) { + var level = statusLevel(prop, prefs, sbx); + return levels.toStatusClass(level); + } + + function statusLevel (prop, prefs, sbx) { + var level = levels.NONE; + var now = moment(sbx.time); + + if (openaps.findOfflineMarker(sbx)) { + console.info('OpenAPS known offline, not checking for alerts'); + } else if (prop.lastLoopMoment) { + var urgentTime = prop.lastLoopMoment.clone().add(prefs.urgent, 'minutes'); + var warningTime = prop.lastLoopMoment.clone().add(prefs.warn, 'minutes'); + + if (urgentTime.isBefore(now)) { + level = levels.URGENT; + } else if (warningTime.isBefore(now)) { + level = levels.WARN; + } + } + + return level; + } + + return openaps; + +} + +module.exports = init; diff --git a/lib/plugins/override.js b/lib/plugins/override.js new file mode 100644 index 00000000000..ba57b11761f --- /dev/null +++ b/lib/plugins/override.js @@ -0,0 +1,67 @@ +'use strict'; + +function init() { + var override = { + name: 'override' + , label: 'Override' + , pluginType: 'pill-status' + }; + + override.isActive = function isActive(overrideStatus, sbx) { + + if (!overrideStatus) { + return false; + } else { + var endMoment = overrideStatus.duration ? overrideStatus.moment.clone().add(overrideStatus.duration, 'seconds') : null; + overrideStatus.endMoment = endMoment; + return overrideStatus.active && (!endMoment || endMoment.isAfter(sbx.time)); + } + + }; + + override.updateVisualisation = function updateVisualisation (sbx) { + var lastOverride = sbx.properties.loop.lastOverride; + var info = [ ]; + var label = ''; + var isActive = override.isActive(lastOverride, sbx); + + if (isActive) { + if (lastOverride.currentCorrectionRange) { + var max = lastOverride.currentCorrectionRange.maxValue; + var min = lastOverride.currentCorrectionRange.minValue; + + if (sbx.settings.units === 'mmol') { + max = sbx.roundBGToDisplayFormat(sbx.scaleMgdl(max)); + min = sbx.roundBGToDisplayFormat(sbx.scaleMgdl(min)); + } + + if (lastOverride.currentCorrectionRange.minValue === lastOverride.currentCorrectionRange.maxValue) { + label += 'BG Target: ' + min; + } else { + label += 'BG Targets: ' + min + ':' + max; + } + } + if ((lastOverride.multiplier || lastOverride.multiplier === 0) && lastOverride.multiplier !== 1) { + var multiplier = (lastOverride.multiplier * 100).toFixed(0); + label += ' | O: ' + multiplier + '%'; + } + } + + var endOverrideValue = lastOverride && lastOverride.endMoment ? + '⇥ ' + lastOverride.endMoment.format('LT') : (lastOverride ? '∞' : ''); + + sbx.pluginBase.updatePillText(override, { + value: endOverrideValue + , label: label + , info: info + , hide: !isActive + }); + + }; + + return override; + +} + + +module.exports = init; diff --git a/lib/plugins/pluginbase.js b/lib/plugins/pluginbase.js index eed09d50f32..0e2adad3586 100644 --- a/lib/plugins/pluginbase.js +++ b/lib/plugins/pluginbase.js @@ -1,14 +1,16 @@ 'use strict'; -var _ = require('lodash'); +var _map = require('lodash/map'); +var _each = require('lodash/each'); -var TOOLTIP_WIDTH = 250; //min-width + padding +var TOOLTIP_WIDTH = 275; //min-width + padding function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { var pluginBase = { }; - pluginBase.forecastPoints = []; + pluginBase.forecastInfos = []; + pluginBase.forecastPoints = {}; function findOrCreatePill (plugin) { var container = null; @@ -77,37 +79,39 @@ function init (majorPills, minorPills, statusPills, bgStatus, tooltip) { } if (options.info && options.info.length) { - var html = _.map(options.info, function mapInfo (i) { + var html = _map(options.info, function mapInfo (i) { return '' + i.label + ' ' + i.value; }).join('
    \n'); pill.mouseover(function pillMouseover (event) { - tooltip.transition().duration(200).style('opacity', .9); + tooltip.style('opacity', .9); - var windowWidth = $(tooltip).parent().parent().width(); - var left = event.pageX + TOOLTIP_WIDTH < windowWidth ? event.pageX : windowWidth - TOOLTIP_WIDTH; + var windowWidth = $(tooltip.node()).parent().parent().width(); + var left = event.pageX + TOOLTIP_WIDTH < windowWidth ? event.pageX : windowWidth - TOOLTIP_WIDTH - 10; tooltip.html(html) .style('left', left + 'px') .style('top', (event.pageY + 15) + 'px'); }); pill.mouseout(function pillMouseout ( ) { - tooltip.transition() - .duration(200) - .style('opacity', 0); + tooltip.style('opacity', 0); }); + } else { + pill.off('mouseover'); } }; - pluginBase.addForecastPoints = function addForecastPoints (points) { - _.each(points, function eachPoint (point) { + pluginBase.addForecastPoints = function addForecastPoints (points, info) { + _each(points, function eachPoint (point) { point.type = 'forecast'; + point.info = info; if (point.mgdl < 13) { point.color = 'transparent'; } }); - pluginBase.forecastPoints = pluginBase.forecastPoints.concat(points); + pluginBase.forecastInfos.push(info); + pluginBase.forecastPoints[info.type] = points; }; return pluginBase; diff --git a/lib/plugins/profile.js b/lib/plugins/profile.js new file mode 100644 index 00000000000..e35caec0e1c --- /dev/null +++ b/lib/plugins/profile.js @@ -0,0 +1,16 @@ +'use strict'; + +// this is just a fake plugin to hold extended settings + +function init() { + + var profile = { + name: 'profile' + , label: 'Profile' + , pluginType: 'fake' + }; + + return profile; +} + +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/pump.js b/lib/plugins/pump.js new file mode 100644 index 00000000000..f055a4b1299 --- /dev/null +++ b/lib/plugins/pump.js @@ -0,0 +1,387 @@ +'use strict'; + +var _ = require('lodash'); +var times = require('../times'); + +var ALL_STATUS_FIELDS = ['reservoir', 'battery', 'clock', 'status', 'device']; + +function init (ctx) { + var moment = ctx.moment; + var translate = ctx.language.translate; + var timeago = require('./timeago')(ctx); + var openaps = require('./openaps')(ctx); + var levels = ctx.levels; + + var pump = { + name: 'pump' + , label: 'Pump' + , pluginType: 'pill-status' + }; + + pump.getPrefs = function getPrefs (sbx) { + + function cleanList (value) { + return decodeURIComponent(value || '').toLowerCase().split(' '); + } + + function isEmpty (list) { + return _.isEmpty(list) || _.isEmpty(list[0]); + } + + var fields = cleanList(sbx.extendedSettings.fields); + fields = isEmpty(fields) ? ['reservoir'] : fields; + + 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 + , warnClock: sbx.extendedSettings.warnClock || 30 + , urgentClock: sbx.extendedSettings.urgentClock || 60 + , warnRes: sbx.extendedSettings.warnRes || 10 + , urgentRes: sbx.extendedSettings.urgentRes || 5 + , warnBattV: sbx.extendedSettings.warnBattV || 1.35 + , urgentBattV: sbx.extendedSettings.urgentBattV || 1.3 + , warnBattP: sbx.extendedSettings.warnBattP || 30 + , 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 + }; + }; + + pump.setProperties = function setProperties (sbx) { + sbx.offerProperty('pump', function setPump ( ) { + + var prefs = pump.getPrefs(sbx); + var recentMills = sbx.time - times.mins(prefs.urgentClock * 2).msecs; + + var filtered = _.filter(sbx.data.devicestatus, function (status) { + return ('pump' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills; + }); + + var pumpStatus = null; + + _.forEach(filtered, function each (status) { + status.clockMills = status.pump && status.pump.clock ? moment(status.pump.clock).valueOf() : status.mills; + if (!pumpStatus || status.clockMills > pumpStatus.clockMills) { + pumpStatus = status; + } + }); + + pumpStatus = pumpStatus || { }; + pumpStatus.data = prepareData(pumpStatus, prefs, sbx); + + return pumpStatus; + }); + }; + + pump.checkNotifications = function checkNotifications (sbx) { + var prefs = pump.getPrefs(sbx); + + if (!prefs.enableAlerts) { return; } + + pump.warnOnSuspend = prefs.warnOnSuspend; + + var data = prepareData(sbx.properties.pump, prefs, sbx); + + if (data.level >= levels.WARN) { + sbx.notifications.requestNotify({ + level: data.level + , title: data.title + , message: data.message + , pushoverSound: 'echo' + , group: 'Pump' + , plugin: pump + }); + } + }; + + pump.updateVisualisation = function updateVisualisation (sbx) { + var prop = sbx.properties.pump; + + var prefs = pump.getPrefs(sbx); + var result = prepareData(prop, prefs, sbx); + + var values = [ ]; + var info = [ ]; + + var selectedFields = sbx.data.inRetroMode ? prefs.retroFields : prefs.fields; + + _.forEach(ALL_STATUS_FIELDS, function eachField (fieldName) { + var field = result[fieldName]; + if (field) { + var selected = _.indexOf(selectedFields, fieldName) > -1; + if (selected) { + values.push(field.display); + } else { + info.push({label: field.label, value: field.display}); + } + } + }); + + if (result.extended) { + info.push({label: '------------', value: ''}); + _.forOwn(result.extended, function(value, key) { + info.push({ label: key, value: value }); + }); + } + + sbx.pluginBase.updatePillText(pump, { + value: values.join(' ') + , info: info + , label: translate('Pump') + , pillClass: statusClass(result.level) + }); + }; + + function virtAsstReservoirHandler (next, slots, sbx) { + var reservoir = _.get(sbx, 'properties.pump.pump.reservoir'); + if (reservoir || reservoir === 0) { + var response = translate('virtAsstReservoir', { + params: [ + reservoir + ] + }); + next(translate('virtAsstTitlePumpReservoir'), response); + } else { + next(translate('virtAsstTitlePumpReservoir'), translate('virtAsstUnknown')); + } + } + + function virtAsstBatteryHandler (next, slots, sbx) { + var battery = _.get(sbx, 'properties.pump.data.battery'); + if (battery) { + var response = translate('virtAsstPumpBattery', { + params: [ + battery.value, + battery.unit + ] + }); + next(translate('virtAsstTitlePumpBattery'), response); + } else { + next(translate('virtAsstTitlePumpBattery'), translate('virtAsstUnknown')); + } + } + + pump.virtAsst = { + intentHandlers:[ + { + // backwards compatibility + intent: 'InsulinRemaining', + intentHandler: virtAsstReservoirHandler + } + , { + // backwards compatibility + intent: 'PumpBattery', + intentHandler: virtAsstBatteryHandler + } + , { + intent: 'MetricNow' + , metrics: ['pump reservoir'] + , intentHandler: virtAsstReservoirHandler + } + , { + intent: 'MetricNow' + , metrics: ['pump battery'] + , intentHandler: virtAsstBatteryHandler + } + ] + }; + + function statusClass (level) { + var cls = 'current'; + + if (level === levels.WARN) { + cls = 'warn'; + } else if (level === levels.URGENT) { + cls = 'urgent'; + } + + return cls; + } + + + function updateClock (prefs, result, sbx) { + if (result.clock) { + result.clock.label = 'Last Clock'; + result.clock.display = timeFormat(result.clock.value, sbx); + + var urgent = moment(sbx.time).subtract(prefs.urgentClock, 'minutes'); + var warn = moment(sbx.time).subtract(prefs.warnClock, 'minutes'); + + if (urgent.isAfter(result.clock.value)) { + result.clock.level = levels.URGENT; + result.clock.message = 'URGENT: Pump data stale'; + } else if (warn.isAfter(result.clock.value)) { + result.clock.level = levels.WARN; + result.clock.message = 'Warning, Pump data stale'; + } else { + result.clock.level = levels.NONE; + } + } + } + + function updateReservoir (prefs, result) { + if (result.reservoir) { + result.reservoir.label = 'Reservoir'; + 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'; + } else if (result.reservoir.value < prefs.warnRes) { + result.reservoir.level = levels.WARN; + result.reservoir.message = 'Warning, Pump Reservoir Low'; + } else { + result.reservoir.level = levels.NONE; + } + } else if (result.manufacturer === 'Insulet') { + result.reservoir = { + label: 'Reservoir', display: '50+ U' + } + } + if (result.reservoir_display_override) { + result.reservoir.display = result.reservoir_display_override; + } + if (result.reservoir_level_override) { + result.reservoir.level = result.reservoir_level_override; + } + } + + 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 && batteryWarn) { + result.battery.level = levels.URGENT; + result.battery.message = 'URGENT: Pump Battery Low'; + } else if (result.battery.value < warn && batteryWarn) { + result.battery.level = levels.WARN; + result.battery.message = 'Warning, Pump Battery Low'; + } else { + result.battery.level = levels.NONE; + } + } + } + + + function buildMessage (result) { + if (result.level > levels.NONE) { + var message = []; + + if (result.battery) { + message.push('Pump Battery: ' + result.battery.display); + } + + if (result.reservoir) { + message.push('Pump Reservoir: ' + result.reservoir.display); + } + + result.message = message.join('\n'); + } + } + + function updateStatus(pump, result) { + if (pump.status) { + var status = pump.status.status || 'normal'; + if (pump.status.bolusing) { + status = 'bolusing'; + } else if (pump.status.suspended) { + status = 'suspended'; + if (pump.warnOnSuspend && pump.status.suspended) { + result.status.level = levels.WARN; + result.status.message = 'Pump Suspended'; + } + } + result.status = { value: status, display: status, label: translate('Status') }; + } + } + + 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 + , reservoir_level_override: pump.reservoir_level_override || null + , manufacturer: pump.manufacturer + , model: pump.model + , extended: pump.extended || null + }; + + updateClock(prefs, result, sbx); + updateReservoir(prefs, result); + updateStatus(pump, result); + + if (pump.battery && pump.battery.percent) { + result.battery = { value: pump.battery.percent, unit: 'percent' }; + updateBattery('%', prefs, result, batteryWarn); + } else if (pump.battery && pump.battery.voltage) { + result.battery = { value: pump.battery.voltage, unit: 'volts'}; + updateBattery('v', prefs, result, batteryWarn); + } + + result.device = { label: translate('Device'), display: prop.device }; + + result.title = 'Pump Status'; + result.level = levels.NONE; + + //TODO: A new Pump Offline marker? Something generic? Use something new instead of a treatment? + if (openaps.findOfflineMarker(sbx)) { + console.info('OpenAPS known offline, not checking for alerts'); + } else { + _.forEach(ALL_STATUS_FIELDS, function eachField(fieldName) { + var field = result[fieldName]; + if (field && field.level > result.level) { + result.level = field.level; + result.title = field.message; + } + }); + } + + buildMessage(result); + + return result; + } + + function timeFormat (m, sbx) { + + var when; + if (m && sbx.data.inRetroMode) { + when = m.format('LT'); + } else if (m) { + when = formatAgo(m, sbx.time); + } else { + when = 'unknown'; + } + + return when; + } + + function formatAgo (m, nowMills) { + var ago = timeago.calcDisplay({mills: m.valueOf()}, nowMills); + return translate('%1' + ago.shortLabel + (ago.shortLabel.length === 1 ? ' ago' : ''), { params: [(ago.value ? ago.value : '')]}); + } + + return pump; +} + +module.exports = init; diff --git a/lib/plugins/pushover.js b/lib/plugins/pushover.js index 3e04564a9e0..7bb1274593e 100644 --- a/lib/plugins/pushover.js +++ b/lib/plugins/pushover.js @@ -3,10 +3,9 @@ var _ = require('lodash'); var Pushover = require('pushover-notifications'); var request = require('request'); -var levels = require('../levels'); var times = require('../times'); -function init (env) { +function init (env, ctx) { var pushover = { PRIORITY_NORMAL: 0 , PRIORITY_EMERGENCY: 2 @@ -19,7 +18,7 @@ function init (env) { if (notify.isAnnouncement) { keys = pushoverAPI.announcementKeys; - } else if (levels.isAlarm(notify.level)) { + } else if (ctx.levels.isAlarm(notify.level)) { keys = pushoverAPI.alarmKeys; } else { keys = pushoverAPI.userKeys; @@ -39,12 +38,12 @@ function init (env) { , sound: notify.pushoverSound || 'gamelan' , timestamp: new Date() //USE PUSHOVER_EMERGENCY for WARN and URGENT so we get the acks - , priority: notify.level >= levels.WARN ? pushover.PRIORITY_EMERGENCY : pushover.PRIORITY_NORMAL + , priority: notify.level >= ctx.levels.WARN ? pushover.PRIORITY_EMERGENCY : pushover.PRIORITY_NORMAL }; - if (levels.isAlarm(notify.level)) { + if (ctx.levels.isAlarm(notify.level)) { //ADJUST RETRY TIME based on WARN or URGENT - msg.retry = notify.level === levels.URGENT ? times.mins(2).secs : times.mins(15).secs; + msg.retry = notify.level === ctx.levels.URGENT ? times.mins(2).secs : times.mins(15).secs; if (env.settings && env.settings.baseURL) { msg.callback = env.settings.baseURL + '/api/v1/notifications/pushovercallback'; } @@ -70,7 +69,7 @@ function init (env) { pushover.sendAPIRequest = function sendAPIRequest (msg, callback) { pushoverAPI.send(msg, function response (err, result) { if (err) { - console.error('unable to send pushover notification', err); + console.error('unable to send pushover notification', msg, err); } else { console.info('sent pushover notification: ', msg, 'result: ', result); } @@ -106,7 +105,7 @@ function setupPushover (env) { var key = env.extendedSettings && env.extendedSettings.pushover && env.extendedSettings.pushover[type]; - if (key === 'off') { + if (key === false) { return []; //don't consider fallback, this type has been disabled } else if (key && key.split) { return key.split(' ') || fallback; @@ -127,7 +126,10 @@ function setupPushover (env) { if (apiToken && (userKeys.length > 0 || alarmKeys.length > 0 || announcementKeys.length > 0)) { var pushoverAPI = new Pushover({ - token: apiToken + token: apiToken, + onerror: function(error) { + console.error('Pushover error', error); + } }); pushoverAPI.apiToken = apiToken; diff --git a/lib/plugins/rawbg.js b/lib/plugins/rawbg.js index 346cdf491e0..a784f49363f 100644 --- a/lib/plugins/rawbg.js +++ b/lib/plugins/rawbg.js @@ -2,7 +2,9 @@ var _ = require('lodash'); -function init() { +function init (ctx) { + + var translate = ctx.language.translate; var rawbg = { name: 'rawbg' @@ -11,6 +13,12 @@ function init() { , pillFlip: true }; + rawbg.getPrefs = function getPrefs (sbx) { + return { + display: (sbx && sbx.extendedSettings.display) ? sbx.extendedSettings.display : 'unsmoothed' + }; + }; + rawbg.setProperties = function setProperties (sbx) { sbx.offerProperty('rawbg', function setRawBG ( ) { var result = { }; @@ -47,14 +55,18 @@ function init() { sbx.pluginBase.updatePillText(rawbg, options); }; - rawbg.calc = function calc(sgv, cal) { + rawbg.calc = function calc(sgv, cal, sbx) { var raw = 0; var cleaned = cleanValues(sgv, cal); + var prefs = rawbg.getPrefs(sbx); + if (cleaned.slope === 0 || cleaned.unfiltered === 0 || cleaned.scale === 0) { raw = 0; - } else if (cleaned.filtered === 0 || sgv.mgdl < 40) { + } else if (cleaned.filtered === 0 || sgv.mgdl < 40 || prefs.display === 'unfiltered') { raw = cleaned.scale * (cleaned.unfiltered - cleaned.intercept) / cleaned.slope; + } else if (prefs.display === 'filtered') { + raw = cleaned.scale * (cleaned.filtered - cleaned.intercept) / cleaned.slope; } else { var ratio = cleaned.scale * (cleaned.filtered - cleaned.intercept) / cleaned.slope / sgv.mgdl; raw = cleaned.scale * (cleaned.unfiltered - cleaned.intercept) / cleaned.slope / ratio; @@ -79,13 +91,13 @@ function init() { var display; switch (parseInt(noise)) { case 0: display = '---'; break; - case 1: display = 'Clean'; break; - case 2: display = 'Light'; break; - case 3: display = 'Medium'; break; - case 4: display = 'Heavy'; break; + case 1: display = translate('Clean'); break; + case 2: display = translate('Light'); break; + case 3: display = translate('Medium'); break; + case 4: display = translate('Heavy'); break; default: if (mgdl < 40) { - display = 'Heavy'; + display = translate('Heavy'); } else { display = '~~~'; } @@ -94,6 +106,28 @@ function init() { return display; }; + function virtAsstRawBGHandler (next, slots, sbx) { + var rawBg = _.get(sbx, 'properties.rawbg.mgdl'); + if (rawBg) { + var response = translate('virtAsstRawBG', { + params: [ + rawBg + ] + }); + next(translate('virtAsstTitleRawBG'), response); + } else { + next(translate('virtAsstTitleRawBG'), translate('virtAsstUnknown')); + } + } + + rawbg.virtAsst = { + intentHandlers: [{ + intent: 'MetricNow' + , metrics:['raw bg', 'raw blood glucose'] + , intentHandler: virtAsstRawBGHandler + }] + }; + return rawbg; } @@ -108,4 +142,4 @@ function cleanValues (sgv, cal) { }; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/runtimestate.js b/lib/plugins/runtimestate.js new file mode 100644 index 00000000000..0447da0cc00 --- /dev/null +++ b/lib/plugins/runtimestate.js @@ -0,0 +1,23 @@ +'use strict'; + +function init() { + + var runtime = { + name: 'runtimestate' + , label: 'Runtime state' + , pluginType: 'fake' + }; + + runtime.setProperties = function setProperties(sbx) { + sbx.offerProperty('runtimestate', function setProp ( ) { + return { + state: sbx.runtimeState + }; + }); + }; + + return runtime; + +} + +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/sensorage.js b/lib/plugins/sensorage.js new file mode 100644 index 00000000000..a14b4f7c213 --- /dev/null +++ b/lib/plugins/sensorage.js @@ -0,0 +1,215 @@ +'use strict'; + +var _ = require('lodash'); +var times = require('../times'); + +function init(ctx) { + var moment = ctx.moment; + var translate = ctx.language.translate; + var levels = ctx.levels; + + var sage = { + name: 'sage' + , label: 'Sensor Age' + , pluginType: 'pill-minor' + }; + + sage.getPrefs = function getPrefs(sbx) { + return { + info: sbx.extendedSettings.info || times.days(6).hours + , warn: sbx.extendedSettings.warn || (times.days(7).hours - 4) + , urgent: sbx.extendedSettings.urgent || (times.days(7).hours - 2) + , enableAlerts: sbx.extendedSettings.enableAlerts || false + }; + }; + + sage.setProperties = function setProperties (sbx) { + sbx.offerProperty('sage', function setProp ( ) { + return sage.findLatestTimeChange(sbx); + }); + }; + + sage.checkNotifications = function checkNotifications(sbx) { + + var info = sbx.properties.sage; + var sensorInfo = info[info.min]; + + if (sensorInfo.notification) { + var notification = _.extend({}, sensorInfo.notification, { + plugin: sage + , debug: { + age: sensorInfo.age + } + }); + + sbx.notifications.requestNotify(notification); + } + + }; + + function minButValid(record) { + var events = [ ]; + + var start = record['Sensor Start']; + if (start && start.found) { + events.push({eventType: 'Sensor Start', treatmentDate: start.treatmentDate}); + } + + var change = record['Sensor Change']; + if (change && change.found) { + events.push({eventType: 'Sensor Change', treatmentDate: change.treatmentDate}); + } + + var sorted = _.sortBy(events, 'treatmentDate'); + + var mostRecent = _.last(sorted); + + return (mostRecent && mostRecent.eventType) || 'Sensor Start'; + } + + sage.findLatestTimeChange = function findLatestTimeChange(sbx) { + + var returnValue = { + 'Sensor Start': { + found: false + } + , 'Sensor Change': { + found: false + } + }; + + var prevDate = { + 'Sensor Start': 0 + , 'Sensor Change': 0 + }; + + _.each(sbx.data.sensorTreatments, function eachTreatment (treatment) { + ['Sensor Start', 'Sensor Change'].forEach( function eachEvent(event) { + var treatmentDate = treatment.mills; + if (treatment.eventType === event && treatmentDate > prevDate[event] && treatmentDate <= sbx.time) { + + prevDate[event] = treatmentDate; + + var a = moment(sbx.time); + var b = moment(treatmentDate); + var days = a.diff(b,'days'); + var hours = a.diff(b,'hours') - days * 24; + var age = a.diff(b,'hours'); + + var eventValue = returnValue[event]; + if (!eventValue.found || (age >= 0 && age < eventValue.age)) { + eventValue.found = true; + eventValue.treatmentDate = treatmentDate; + eventValue.age = age; + eventValue.days = days; + eventValue.hours = hours; + eventValue.notes = treatment.notes; + eventValue.minFractions = a.diff(b,'minutes') - age * 60; + + eventValue.display = ''; + if (eventValue.age >= 24) { + eventValue.display += eventValue.days + 'd'; + } + eventValue.display += eventValue.hours + 'h'; + + eventValue.displayLong = ''; + if (eventValue.age >= 24) { + eventValue.displayLong += eventValue.days + ' ' + translate('days'); + } + if (eventValue.displayLong.length > 0) { + eventValue.displayLong += ' '; + } + eventValue.displayLong += eventValue.hours + ' ' + translate('hours'); + } + } + }); + }); + + if (returnValue['Sensor Change'].found && returnValue['Sensor Start'].found && + returnValue['Sensor Change'].treatmentDate >= returnValue['Sensor Start'].treatmentDate) { + returnValue['Sensor Start'].found = false; + } + + returnValue.min = minButValid(returnValue); + + var sensorInfo = returnValue[returnValue.min]; + var prefs = sage.getPrefs(sbx); + + var sendNotification = false; + var sound = 'incoming'; + var message; + + sensorInfo.level = levels.NONE; + + if (sensorInfo.age >= prefs.urgent) { + sendNotification = sensorInfo.age === prefs.urgent; + message = translate('Sensor change/restart overdue!'); + sound = 'persistent'; + sensorInfo.level = levels.URGENT; + } else if (sensorInfo.age >= prefs.warn) { + sendNotification = sensorInfo.age === prefs.warn; + message = translate('Time to change/restart sensor'); + sensorInfo.level = levels.WARN; + } else if (sensorInfo.age >= prefs.info) { + sendNotification = sensorInfo.age === prefs.info; + message = translate('Change/restart sensor soon'); + sensorInfo.level = levels.INFO; + } + + //allow for 20 minute period after a full hour during which we'll alert the user + if (prefs.enableAlerts && sendNotification && sensorInfo.minFractions <= 20) { + sensorInfo.notification = { + title: translate('Sensor age %1 days %2 hours', { params: [sensorInfo.days, sensorInfo.hours] }) + , message: message + , pushoverSound: sound + , level: sensorInfo.level + , group: 'SAGE' + }; + } + + return returnValue; + }; + + sage.updateVisualisation = function updateVisualisation (sbx) { + + var latest = sbx.properties.sage; + var sensorInfo = latest[latest.min]; + var info = []; + + ['Sensor Change', 'Sensor Start'].forEach( function eachEvent(event) { + if (latest[event].found) { + var label = event === 'Sensor Change' ? 'Sensor Insert' : event; + info.push( { label: translate(label), value: new Date(latest[event].treatmentDate).toLocaleString() } ); + info.push( { label: translate('Duration'), value: latest[event].displayLong } ); + 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}); + } + } + }); + + var statusClass = null; + if (sensorInfo.level === levels.URGENT) { + statusClass = 'urgent'; + } else if (sensorInfo.level === levels.WARN) { + statusClass = 'warn'; + } + + sbx.pluginBase.updatePillText(sage, { + value: sensorInfo.display + , label: translate('SAGE') + , info: info + , pillClass: statusClass + }); + }; + + return sage; +} + +module.exports = init; + diff --git a/lib/plugins/simplealarms.js b/lib/plugins/simplealarms.js index 9c2ae9ed405..6a99060fe1b 100644 --- a/lib/plugins/simplealarms.js +++ b/lib/plugins/simplealarms.js @@ -1,22 +1,24 @@ 'use strict'; -var levels = require('../levels'); var times = require('../times'); -function init() { +function init(ctx) { var simplealarms = { name: 'simplealarms' , label: 'Simple Alarms' , pluginType: 'notification' }; + + var levels = ctx.levels; simplealarms.checkNotifications = function checkNotifications(sbx) { + var lastSGVEntry = sbx.lastSGVEntry() , scaledSGV = sbx.scaleEntry(lastSGVEntry) ; - if (scaledSGV && lastSGVEntry && lastSGVEntry.mgdl > 39 && Date.now() - lastSGVEntry.mills < times.mins(10).msecs) { + if (scaledSGV && lastSGVEntry && lastSGVEntry.mgdl > 39 && sbx.time - lastSGVEntry.mills < times.mins(10).msecs) { var result = simplealarms.compareBGToTresholds(scaledSGV, sbx); if (levels.isAlarm(result.level)) { sbx.notifications.requestNotify({ @@ -37,27 +39,31 @@ function init() { simplealarms.compareBGToTresholds = function compareBGToTresholds(scaledSGV, sbx) { var result = { level: levels.INFO }; - if (scaledSGV > sbx.scaleMgdl(sbx.settings.thresholds.bgHigh)) { + if (sbx.settings.alarmUrgentHigh && scaledSGV > sbx.scaleMgdl(sbx.settings.thresholds.bgHigh)) { result.level = levels.URGENT; result.title = levels.toDisplay(levels.URGENT) + ' HIGH'; result.pushoverSound = 'persistent'; result.eventName = 'high'; - } else if (scaledSGV > sbx.scaleMgdl(sbx.settings.thresholds.bgTargetTop)) { - result.level = levels.WARN; - result.title = levels.toDisplay(levels.WARN) + ' HIGH'; - result.pushoverSound = 'climb'; - result.eventName = 'high'; - } else if (scaledSGV < sbx.scaleMgdl(sbx.settings.thresholds.bgLow)) { + } else + if (sbx.settings.alarmHigh && scaledSGV > sbx.scaleMgdl(sbx.settings.thresholds.bgTargetTop)) { + result.level = levels.WARN; + result.title = levels.toDisplay(levels.WARN) + ' HIGH'; + result.pushoverSound = 'climb'; + result.eventName = 'high'; + } + + if (sbx.settings.alarmUrgentLow && scaledSGV < sbx.scaleMgdl(sbx.settings.thresholds.bgLow)) { result.level = levels.URGENT; result.title = levels.toDisplay(levels.URGENT) + ' LOW'; result.pushoverSound = 'persistent'; result.eventName = 'low'; - } else if (scaledSGV < sbx.scaleMgdl(sbx.settings.thresholds.bgTargetBottom)) { + } else if (sbx.settings.alarmLow && scaledSGV < sbx.scaleMgdl(sbx.settings.thresholds.bgTargetBottom)) { result.level = levels.WARN; result.title = levels.toDisplay(levels.WARN) + ' LOW'; result.pushoverSound = 'falling'; result.eventName = 'low'; } + return result; }; diff --git a/lib/plugins/speech.js b/lib/plugins/speech.js new file mode 100644 index 00000000000..498071c2a5a --- /dev/null +++ b/lib/plugins/speech.js @@ -0,0 +1,88 @@ +'use strict'; + +var lastEntryValue; +var lastMinutes; +var lastEntryTime; + +function init(ctx) { + var translate = ctx.language.translate; + var speechLangCode = ctx.language.speechCode; + + var speech = { + name: 'speech', + label: 'Speech', + pluginType: 'pill-status', + pillFlip: true + }; + + + speech.say = function say(sayIt) { + console.log('saying', sayIt, 'using lang code', speechLangCode); + + var msg = new SpeechSynthesisUtterance(sayIt.toLowerCase()); + if (speechLangCode) msg.lang = speechLangCode; + window.speechSynthesis.speak(msg); + } + + speech.visualizeAlarm = function visualizeAlarm(sbx, alarm, alarmMessage) { + console.log('Speech got an Alarm Message:',alarmMessage); + speech.say(alarmMessage); + } + + speech.updateVisualisation = function updateVisualisation(sbx) { + + if (sbx.data.inRetroMode) return; + + var timeNow = sbx.time; + var entry = sbx.lastSGVEntry(); + + if (timeNow && entry && entry.mills) { + + var timeSince = timeNow - entry.mills; + var timeMinutes = Math.round(timeSince / 60000); + + if (lastEntryTime != entry.mills) { + + var lE = sbx.scaleMgdl(lastEntryValue); + var cE = sbx.scaleMgdl(entry.mgdl); + + var delta = ((cE - lE) %1 === 0) ? cE - lE : Math.round( (cE - lE) * 10) / 10; + + lastEntryValue = entry.mgdl; + lastEntryTime = entry.mills; + + var sayIt = sbx.roundBGToDisplayFormat(sbx.scaleMgdl(entry.mgdl)); + + if (!isNaN(delta)) { + + sayIt += ', ' + translate('change') + ' ' + delta + } + + var iob = sbx.properties.iob; + if (iob) { + var iobString = sbx.roundInsulinForDisplayFormat(iob.display); + + if (iobString) { + sayIt += ", IOB " + iobString; + } + } + speech.say(sayIt); + + } else { + + if (timeMinutes > 5 && timeMinutes != lastMinutes && timeMinutes % 5 == 0) { + lastMinutes = timeMinutes; + + var lastEntryString = translate('Last entry {0} minutes ago'); + sayIt = lastEntryString.replace('{0}', timeMinutes); + speech.say(sayIt); + } + } + } + }; + + return speech; + +} + +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/timeago.js b/lib/plugins/timeago.js new file mode 100644 index 00000000000..592fa2943b7 --- /dev/null +++ b/lib/plugins/timeago.js @@ -0,0 +1,200 @@ +'use strict'; + +var times = require('../times'); +var lastChecked = new Date(); +var lastRecoveryTimeFromSuspend = new Date("1900-01-01"); + +function init(ctx) { + var translate = ctx.language.translate; + var levels = ctx.levels; + + var timeago = { + name: 'timeago', + label: 'Timeago', + pluginType: 'pill-status', + pillFlip: true + }; + + timeago.checkNotifications = function checkNotifications(sbx) { + + if (!sbx.extendedSettings.enableAlerts) { + return; + } + + var lastSGVEntry = sbx.lastSGVEntry(); + + if (!lastSGVEntry || lastSGVEntry.mills >= sbx.time) { + return; + } + + function buildMessage(agoDisplay) { + var lines = sbx.prepareDefaultLines(); + lines.unshift(translate('Last received:') + ' ' + [agoDisplay.value, agoDisplay.label].join(' ')); + return lines.join('\n'); + } + + function sendAlarm(opts) { + var agoDisplay = timeago.calcDisplay(lastSGVEntry, sbx.time); + + sbx.notifications.requestNotify({ + level: opts.level, + title: translate('Stale data, check rig?'), + message: buildMessage(agoDisplay), + eventName: timeago.name, + plugin: timeago, + group: 'Time Ago', + pushoverSound: opts.pushoverSound, + debug: agoDisplay + }); + } + + var status = timeago.checkStatus(sbx); + if (status === 'urgent') { + sendAlarm({ + level: levels.URGENT, + pushoverSound: 'echo' + }); + } else if (status === 'warn') { + sendAlarm({ + level: levels.WARN, + pushoverSound: 'echo' + }); + } + + }; + + timeago.checkStatus = function checkStatus(sbx) { + // Check if the app has been suspended; if yes, snooze data missing alarmn for 15 seconds + var now = new Date(); + var delta = now.getTime() - lastChecked.getTime(); + lastChecked = now; + + function isHibernationDetected() { + if (sbx.runtimeEnvironment === 'client') { + if (delta > 20 * 1000) { // Looks like we've been hibernating + lastRecoveryTimeFromSuspend = now; + } + var timeSinceLastRecovered = now.getTime() - lastRecoveryTimeFromSuspend.getTime(); + return timeSinceLastRecovered < (10 * 1000); + } + + // Assume server never hibernates, or if it does, it's alarm-worthy + return false; + + } + + if (isHibernationDetected()) { + console.log('Hibernation detected, suspending timeago alarm'); + return 'current'; + } + + var lastSGVEntry = sbx.lastSGVEntry(), + warn = sbx.settings.alarmTimeagoWarn, + warnMins = sbx.settings.alarmTimeagoWarnMins || 15, + urgent = sbx.settings.alarmTimeagoUrgent, + urgentMins = sbx.settings.alarmTimeagoUrgentMins || 30; + + function isStale(mins) { + return sbx.time - lastSGVEntry.mills > times.mins(mins).msecs; + } + + var status = 'current'; + + if (!lastSGVEntry) { + //assume current + } else if (urgent && isStale(urgentMins)) { + status = 'urgent'; + } else if (warn && isStale(warnMins)) { + status = 'warn'; + } + + return status; + + }; + + timeago.isMissing = function isMissing(opts) { + if (!opts || !opts.entry || isNaN(opts.entry.mills) || isNaN(opts.time) || isNaN(opts.timeSince)) { + return { + label: translate('time ago'), + shortLabel: translate('ago') + }; + } + }; + + timeago.inTheFuture = function inTheFuture(opts) { + if (opts.entry.mills - times.mins(5).msecs > opts.time) { + return { + label: translate('in the future'), + shortLabel: translate('future') + }; + } + }; + + timeago.almostInTheFuture = function almostInTheFuture(opts) { + if (opts.entry.mills > opts.time) { + return { + value: 1, + label: translate('min ago'), + shortLabel: 'm' + }; + } + }; + + timeago.isLessThan = function isLessThan(limit, divisor, label, shortLabel) { + return function checkIsLessThan(opts) { + if (opts.timeSince < limit) { + return { + value: Math.max(1, Math.round(opts.timeSince / divisor)), + label: label, + shortLabel: shortLabel + }; + } + }; + }; + + timeago.resolvers = [ + timeago.isMissing, timeago.inTheFuture, timeago.almostInTheFuture, timeago.isLessThan(times.mins(2).msecs, times.min().msecs, 'min ago', 'm'), timeago.isLessThan(times.hour().msecs, times.min().msecs, 'mins ago', 'm'), timeago.isLessThan(times.hours(2).msecs, times.hour().msecs, 'hour ago', 'h'), timeago.isLessThan(times.day().msecs, times.hour().msecs, 'hours ago', 'h'), timeago.isLessThan(times.days(2).msecs, times.day().msecs, 'day ago', 'd'), timeago.isLessThan(times.week().msecs, times.day().msecs, 'days ago', 'd'), + function () { + return { + label: 'long ago', + shortLabel: 'ago' + } + } + ]; + + timeago.calcDisplay = function calcDisplay(entry, time) { + var opts = { + time: time, + entry: entry + }; + + if (time && entry && entry.mills) { + opts.timeSince = time - entry.mills; + } + + for (var i = 0; i < timeago.resolvers.length; i++) { + var value = timeago.resolvers[i](opts); + if (value) { + return value; + } + } + }; + + timeago.updateVisualisation = function updateVisualisation(sbx) { + var agoDisplay = timeago.calcDisplay(sbx.lastSGVEntry(), sbx.time); + var inRetroMode = sbx.data.inRetroMode; + + sbx.pluginBase.updatePillText(timeago, { + value: inRetroMode ? null : agoDisplay.value, + label: inRetroMode ? translate('RETRO') : translate(agoDisplay.label) + //no warning/urgent class when in retro mode + , + pillClass: inRetroMode ? 'current' : timeago.checkStatus(sbx) + }); + }; + + return timeago; + +} + +module.exports = init; \ No newline at end of file diff --git a/lib/plugins/treatmentnotify.js b/lib/plugins/treatmentnotify.js index 7efe2a037ce..be76b8cac5f 100644 --- a/lib/plugins/treatmentnotify.js +++ b/lib/plugins/treatmentnotify.js @@ -1,28 +1,56 @@ 'use strict'; -var _ = require('lodash'); -var levels = require('../levels'); -var times = require('../times'); -var simplealarms = require('./simplealarms')(); +const _ = require('lodash'); +const times = require('../times'); +const crypto = require('crypto'); -function init() { +const MANUAL_TREATMENTS = ['BG Check', 'Meal Bolus', 'Carb Correction', 'Correction Bolus']; - var treatmentnotify = { +function init(ctx) { + + const treatmentnotify = { name: 'treatmentnotify' , label: 'Treatment Notifications' , pluginType: 'notification' }; + const simplealarms = require('./simplealarms')(ctx); + + //automated treatments from OpenAPS or Loop shouldn't trigger notifications or snooze alarms + 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; + }); + + return treatments; + } + treatmentnotify.checkNotifications = function checkNotifications (sbx) { + + var treatments = filterTreatments(sbx); var lastMBG = sbx.lastEntry(sbx.data.mbgs); - var lastTreatment = sbx.lastEntry(sbx.data.treatments); + var lastTreatment = sbx.lastEntry(treatments); var mbgCurrent = isCurrent(lastMBG); var treatmentCurrent = isCurrent(lastTreatment); + + var translate = sbx.language.translate; if (mbgCurrent || treatmentCurrent) { - var mbgMessage = mbgCurrent ? 'Meter BG ' + sbx.scaleEntry(lastMBG) + ' ' + sbx.unitsLabel : ''; - var treatmentMessage = treatmentCurrent ? 'Treatment: ' + lastTreatment.eventType : ''; + var mbgMessage = mbgCurrent ? translate('Meter BG') +' ' + sbx.scaleEntry(lastMBG) + ' ' + sbx.unitsLabel : ''; + var treatmentMessage = treatmentCurrent ? translate('Treatment') + ': ' + lastTreatment.eventType : ''; autoSnoozeAlarms(mbgMessage, treatmentMessage, lastTreatment, sbx); @@ -40,7 +68,7 @@ function init() { if (lastTreatment && !lastTreatment.isAnnouncement) { var snoozeLength = sbx.extendedSettings.snoozeMins && times.mins(sbx.extendedSettings.snoozeMins).msecs || times.mins(10).msecs; sbx.notifications.requestSnooze({ - level: levels.URGENT + level: sbx.levels.URGENT , title: 'Snoozing alarms since there was a recent treatment' , message: _.trim([mbgMessage, treatmentMessage].join('\n')) , lengthMills: snoozeLength @@ -50,10 +78,12 @@ function init() { function requestMBGNotify (lastMBG, sbx) { console.info('requestMBGNotify for', lastMBG); + var translate = sbx.language.translate; + sbx.notifications.requestNotify({ - level: levels.INFO + level: sbx.levels.INFO , title: 'Calibration' //assume all MGBs are calibrations for now - , message: 'Meter BG: ' + sbx.scaleEntry(lastMBG) + ' ' + sbx.unitsLabel + , message: translate('Meter BG') + ': ' + sbx.scaleEntry(lastMBG) + ' ' + sbx.unitsLabel , plugin: treatmentnotify , pushoverSound: 'magic' }); @@ -64,37 +94,72 @@ function init() { sbx.notifications.requestNotify({ level: result.level - , title: (result.level === levels.URGENT ? levels.toDisplay(levels.URGENT) + ' ' : '') + lastTreatment.eventType + , title: (result.level === sbx.levels.URGENT ? sbx.levels.toDisplay(sbx.levels.URGENT) + ' ' : '') + lastTreatment.eventType , message: lastTreatment.notes || '.' //some message is required , plugin: treatmentnotify - , pushoverSound: levels.isAlarm(result.level) ? result.pushoverSound : undefined + , group: 'Announcement' + , pushoverSound: sbx.levels.isAlarm(result.level) ? result.pushoverSound : undefined , isAnnouncement: true }); } function requestTreatmentNotify (lastTreatment, sbx) { + var translate = sbx.language.translate; + if (lastTreatment.isAnnouncement) { requestAnnouncementNotify(lastTreatment, sbx); } else { - var message = buildTreatmentMessage(lastTreatment, sbx); + let message = buildTreatmentMessage(lastTreatment, sbx); + + let eventType = lastTreatment.eventType; + if (lastTreatment.duration === 0 && eventType === 'Temporary Target') { + eventType += ' Cancel'; + message = translate('Canceled'); + } + + const timestamp = lastTreatment.timestamp; + if (!message) { + message = '...'; + } + + if (!eventType && lastTreatment.carbs && lastTreatment.insulin) eventType = "Meal Bolus"; + if (!eventType && lastTreatment.carbs) eventType = "Carb Correction"; + if (!eventType && lastTreatment.insulin) eventType = "Correcton Bolus"; + if (!eventType) eventType = "Note"; + + const hash = crypto.createHash('sha1'); + const info = JSON.stringify({ eventType, timestamp}); + hash.update(info); + const notifyhash = hash.digest('hex'); + sbx.notifications.requestNotify({ - level: levels.INFO - , title: lastTreatment.eventType - , message: message + level: sbx.levels.INFO + , title: translate(eventType) + , message + , timestamp , plugin: treatmentnotify + , notifyhash }); } } function buildTreatmentMessage(lastTreatment, sbx) { - return (lastTreatment.glucose ? 'BG: ' + lastTreatment.glucose + ' (' + lastTreatment.glucoseType + ')' : '') + - (lastTreatment.carbs ? '\nCarbs: ' + lastTreatment.carbs + 'g' : '') + + var translate = sbx.language.translate; + + return (lastTreatment.glucose ? translate('BG') + ': ' + lastTreatment.glucose + ' (' + lastTreatment.glucoseType + ')' : '') + + (lastTreatment.reason ? '\n' + translate('Reason') + ': ' + lastTreatment.reason : '') + + (lastTreatment.targetTop ? '\n' + translate('Target Top') + ': ' + lastTreatment.targetTop : '') + + (lastTreatment.targetBottom ? '\n' + translate('Target Bottom') + ': ' + lastTreatment.targetBottom : '') + + (lastTreatment.carbs ? '\n' + translate('Carbs') + ': ' + lastTreatment.carbs + 'g' : '') + - (lastTreatment.insulin ? '\nInsulin: ' + sbx.roundInsulinForDisplayFormat(lastTreatment.insulin) + 'U' : '')+ - (lastTreatment.enteredBy ? '\nEntered By: ' + lastTreatment.enteredBy : '') + + (lastTreatment.insulin ? '\n' + translate('Insulin') + ': ' + sbx.roundInsulinForDisplayFormat(lastTreatment.insulin) + 'U' : '')+ + (lastTreatment.duration ? '\n' + translate('Duration') + ': ' + lastTreatment.duration + ' min' : '')+ + (lastTreatment.percent ? '\n' + translate('Percent') + ': ' + (lastTreatment.percent > 0 ? '+' : '') + lastTreatment.percent + '%' : '')+ + (!isNaN(lastTreatment.absolute) ? '\n' + translate('Value') + ': ' + lastTreatment.absolute + 'U' : '')+ + (lastTreatment.enteredBy ? '\n' + translate('Entered By') + ': ' + lastTreatment.enteredBy : '') + - (lastTreatment.notes ? '\nNotes: ' + lastTreatment.notes : ''); + (lastTreatment.notes ? '\n' + translate('Notes') + ': ' + lastTreatment.notes : ''); } return treatmentnotify; @@ -111,4 +176,4 @@ function isCurrent(last) { return ago !== -1 && ago < times.mins(10).msecs; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/upbat.js b/lib/plugins/upbat.js index 8bf0127528c..fb870cdf343 100644 --- a/lib/plugins/upbat.js +++ b/lib/plugins/upbat.js @@ -1,6 +1,10 @@ 'use strict'; -function init() { +var _ = require('lodash'); +var times = require('../times'); + +function init(ctx) { + var translate = ctx.language.translate; var upbat = { name: 'upbat' @@ -9,53 +13,246 @@ function init() { , pillFlip: true }; + upbat.getPrefs = function getPrefs(sbx) { + return { + warn: sbx.extendedSettings.warn ? sbx.extendedSettings.warn : 30 + , urgent: sbx.extendedSettings.urgent ? sbx.extendedSettings.urgent : 20 + , enableAlerts: sbx.extendedSettings.enableAlerts + }; + }; + upbat.setProperties = function setProperties (sbx) { - sbx.offerProperty('upbat', function setUpbat ( ) { + sbx.offerProperty('upbat', function setUpbat2 ( ) { + return upbat.analyzeData(sbx); + }); + }; + + function byBattery (status) { + return status.uploader.battery; + } + + upbat.analyzeData = function analyzeData (sbx) { + + var prefs = upbat.getPrefs(sbx); + + var recentMins = 30; + var recentMills = sbx.time - times.mins(recentMins).msecs; + + var recentData = _.filter(sbx.data.devicestatus, function eachStatus (status) { + return ('uploader' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills; + }); + + var result = { + level: undefined + , display: '?%' + , status: undefined + , devices: {} + }; + + function getDevice (status) { + var uri = status.device || 'uploader'; + var device = result.devices[uri]; + + if (!device) { + device = { + //TODO: regex to look for any uri schemes, such as: xdrip://phone + name: uri.indexOf('openaps://') === 0 ? uri.substring('openaps://'.length) : uri + , uri: uri + , statuses: [ ] + }; - var result = { display: null }; + result.devices[uri] = device; + } + return device; + } + + function analyzeStatus (status) { + + var uploaderStatus = status.uploader; + + var battery = uploaderStatus.battery; + var voltage = uploaderStatus.batteryVoltage; + var charging = status.isCharging ? status.isCharging : false; + var voltageDisplay; + + if (voltage) { + if (voltage > 1000) { + voltage = voltage / 1000; + } + voltageDisplay = voltage.toFixed(3) + 'v'; + } - var battery = sbx.data.uploaderBattery; + if (battery || voltage) { + uploaderStatus.value = battery || voltage; - if (battery) { - result.value = battery; - result.display = battery + '%'; + if (battery) { + uploaderStatus.battery = battery; + } + + if (voltage) { + uploaderStatus.voltage = voltage; + uploaderStatus.voltageDisplay = voltageDisplay; + } + + uploaderStatus.display = (battery ? battery + '%' : voltageDisplay) + (charging ? "⚡" : ""); if (battery >= 95) { - result.level = 100; + uploaderStatus.level = 100; } else if (battery < 95 && battery >= 55) { - result.level = 75; + uploaderStatus.level = 75; } else if (battery < 55 && battery >= 30) { - result.level = 50; + uploaderStatus.level = 50; } else { - result.level = 25; + uploaderStatus.level = 25; } - if (battery <= 30 && battery > 20) { - result.status = 'warn'; - } else if (battery <= 20) { - result.status = 'urgent'; - } else { - result.status = null; + if (battery <= prefs.warn && battery > prefs.urgent) { + uploaderStatus.notification = ctx.levels.WARN; + } else if (battery <= prefs.urgent) { + uploaderStatus.notification = ctx.levels.URGENT; } + } + } - return result; + _.forEach(recentData, function eachRecentStatus (status) { + analyzeStatus(status); + var device = getDevice(status); + device.statuses.push(_.pick(status, ['uploader', 'created_at', 'mills', '_id'])); }); + + var recentLowests = [ ]; + _.forEach(result.devices, function eachDevice (device) { + device.statuses = _.sortBy(device.statuses, function (status) { + return sbx.entryMills(status); + }).reverse(); + var first = _.first(device.statuses); + var recent = sbx.entryMills(first) - times.mins(10).msecs; + var recentLowest = _.chain(device.statuses) + .filter(function isRecent (status) { + return sbx.entryMills(status) > recent; + }) + .minBy(byBattery) + .value(); + + device.min = recentLowest.uploader; + recentLowests.push(recentLowest); + }); + + var min = _.minBy(recentLowests, byBattery); + + if (min && min.uploader) { + result.level = min.uploader.level; + result.display = min.uploader.display; + result.status = ctx.levels.toStatusClass(min.uploader.notification); + result.min = min.uploader; + } + + return result; + }; + + upbat.checkNotifications = function checkNotifications(sbx) { + var prefs = upbat.getPrefs(sbx); + + var prop = sbx.properties.upbat; + if (!prop || !prefs.enableAlerts) { return; } + + if (prop.min && prop.min.notification && prop.min.notification >= ctx.levels.WARN) { + var message = _.map(_.values(prop.devices), function toMessage (device) { + var info = [ + device.name + , device.min.display + ]; + + if (device.min && device.min.battery && device.min.voltageDisplay) { + info.push('(' + device.min.voltageDisplay + ')'); + } + + return info.join(' '); + }).join('; '); + + sbx.notifications.requestNotify({ + level: prop.min.notification + , title: ctx.levels.toDisplay(prop.min.notification) + ' Uploader Battery is Low' + , message: message + , pushoverSound: 'echo' + , group: 'Uploader Battery' + , plugin: upbat + , debug: prop + }); + } }; upbat.updateVisualisation = function updateVisualisation (sbx) { var prop = sbx.properties.upbat; + var infos = null; + + if (_.values(prop.devices).length > 1) { + infos = _.map(_.values(prop.devices), function toInfo (device) { + var info = { + label: device.name + , value: device.min.display + }; + + if (device.min && device.min.battery && device.min.voltageDisplay) { + info.value += ' (' + device.min.voltageDisplay + ')'; + } + + if (device.min && device.min.temperature) { + info.value += ' ' + device.min.temperature; + } + return info; + }); + } else { + if (prop.min && prop.min.battery && prop.min.voltageDisplay) { + infos = [{label: 'Voltage', value: prop.min.voltageDisplay}]; + } + if (prop.min && prop.min.temperature) { + infos.push({label: 'Temp', value : prop.min.temperature}); + } + } + sbx.pluginBase.updatePillText(upbat, { value: prop && prop.display , labelClass: prop && prop.level && 'icon-battery-' + prop.level , pillClass: prop && prop.status - , hide: !(prop && prop.value && prop.value >= 0) + , info: infos + , hide: !(prop && prop.min && prop.min.value && prop.min.value >= 0) }); }; + function virtAsstUploaderBatteryHandler (next, slots, sbx) { + var upBat = _.get(sbx, 'properties.upbat.display'); + if (upBat) { + var response = translate('virtAsstUploaderBattery', { + params: [ + upBat + ] + }); + next(translate('virtAsstTitleUploaderBattery'), response); + } else { + next(translate('virtAsstTitleUploaderBattery'), translate('virtAsstUnknown')); + } + } + + upbat.virtAsst = { + intentHandlers: [ + { + // for backwards compatibility + intent: 'UploaderBattery' + , intentHandler: virtAsstUploaderBatteryHandler + } + , { + intent: 'MetricNow' + , metrics: ['uploader battery'] + , intentHandler: virtAsstUploaderBatteryHandler + } + ] + }; + return upbat; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/plugins/virtAsstBase.js b/lib/plugins/virtAsstBase.js new file mode 100644 index 00000000000..e7cf2c731a3 --- /dev/null +++ b/lib/plugins/virtAsstBase.js @@ -0,0 +1,92 @@ +'use strict'; + +var _each = require('lodash/each'); + +function init(env, ctx) { + var moment = ctx.moment; + + function virtAsstBase() { + return virtAsstBase; + } + + var entries = ctx.entries; + var translate = ctx.language.translate; + + virtAsstBase.setupMutualIntents = function (configuredPlugin) { + // full status + configuredPlugin.addToRollup('Status', function (slots, sbx, callback) { + entries.list({count: 1}, function (err, records) { + var direction; + if (translate(records[0].direction)) { + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time)) + ] + }); + + callback(null, {results: status, priority: -1}); + }); + }, 'BG Status'); + + configuredPlugin.configureIntentHandler('NSStatus', function (callback, slots, sbx, locale) { + configuredPlugin.getRollup('Status', sbx, slots, locale, function (status) { + callback(translate('virtAsstTitleFullStatus'), status); + }); + }); + + // blood sugar and direction + configuredPlugin.configureIntentHandler('MetricNow', function (callback, slots, sbx) { + entries.list({count: 1}, function(err, records) { + var direction; + if(translate(records[0].direction)){ + direction = translate(records[0].direction); + } else { + direction = records[0].direction; + } + var status = translate('virtAsstStatus', { + params: [ + sbx.scaleMgdl(records[0].sgv), + direction, + moment(records[0].date).from(moment(sbx.time))] + }); + + callback(translate('virtAsstTitleCurrentBG'), status); + }); + }, ['bg', 'blood glucose', 'number']); + }; + + virtAsstBase.setupVirtAsstHandlers = function (configuredPlugin) { + ctx.plugins.eachEnabledPlugin(function (plugin) { + if (plugin.virtAsst) { + if (plugin.virtAsst.intentHandlers) { + console.log('Plugin "' + plugin.name + '" supports Virtual Assistants'); + _each(plugin.virtAsst.intentHandlers, function (route) { + if (route) { + configuredPlugin.configureIntentHandler(route.intent, route.intentHandler, route.metrics); + } + }); + } + if (plugin.virtAsst.rollupHandlers) { + console.log('Plugin "' + plugin.name + '" supports rollups for Virtual Assistants'); + _each(plugin.virtAsst.rollupHandlers, function (route) { + if (route) { + configuredPlugin.addToRollup(route.rollupGroup, route.rollupHandler, route.rollupName); + } + }); + } + } else { + console.log('Plugin "' + plugin.name + '" does not support Virtual Assistants'); + } + }); + }; + + return virtAsstBase; +} + +module.exports = init; diff --git a/lib/plugins/xdripjs.js b/lib/plugins/xdripjs.js new file mode 100644 index 00000000000..e1e64b89528 --- /dev/null +++ b/lib/plugins/xdripjs.js @@ -0,0 +1,443 @@ +'use strict'; + +var _ = require('lodash'); +var times = require('../times'); + +function init(ctx) { + var moment = ctx.moment; + var levels = ctx.levels; + var utils = require('../utils')(ctx); + var firstPrefs = true; + var lastStateNotification = null; + var translate = ctx.language.translate; + + var sensorState = { + name: 'xdripjs' + , label: 'CGM Status' + , pluginType: 'pill-status' + }; + + sensorState.getPrefs = function getPrefs(sbx) { + var prefs = { + enableAlerts: sbx.extendedSettings.enableAlerts || false + , warnBatV: sbx.extendedSettings.warnBatV || 300 + , stateNotifyIntrvl: sbx.extendedSettings.stateNotifyIntrvl || 0.5 + }; + + if (firstPrefs) { + firstPrefs = false; + console.info('xdripjs Prefs:', prefs); + } + + return prefs; + }; + + sensorState.setProperties = function setProperties (sbx) { + sbx.offerProperty('sensorState', function setProp ( ) { + return sensorState.getStateString(sbx); + }); + }; + + sensorState.checkNotifications = function checkNotifications(sbx) { + + var info = sbx.properties.sensorState; + + if (info && info.notification) { + var notification = _.extend({}, info.notification, { + plugin: sensorState + , debug: { + stateString: info.lastStateString + } + }); + + sbx.notifications.requestNotify(notification); + } + + }; + + sensorState.getStateString = function findLatestState(sbx) { + var prefs = sensorState.getPrefs(sbx); + + var recentHours = 24; + var recentMills = sbx.time - times.hours(recentHours).msecs; + + var result = { + seenDevices: { } + , latest: null + , lastDevice: null + , lastState: null + , lastStateString: null + , lastStateStringShort: null + , lastSessionStart: null + , lastStateTime: null + , lastTxId: null + , lastTxStatus: null + , lastTxStatusString: null + , lastTxStatusStringShort: null + , lastTxActivation: null + , lastMode: null + , lastRssi: null + , lastUnfiltered: null + , lastFiltered: null + , lastNoise: null + , lastNoiseString: null + , lastSlope: null + , lastIntercept: null + , lastCalType: null + , lastCalibrationDate: null + , lastBatteryTimestamp: null + , lastVoltageA: null + , lastVoltageB: null + , lastTemperature: null + , lastResistance: null + }; + + function toMoments (status) { + return { + when: moment(status.mills) + , timestamp: status.xdripjs && status.xdripjs.timestamp && moment(status.xdripjs.timestamp) + }; + } + + function getDevice(status) { + var uri = status.device || 'device'; + var device = result.seenDevices[uri]; + + if (!device) { + device = { + name: utils.deviceName(uri) + , uri: uri + }; + + result.seenDevices[uri] = device; + } + return device; + } + + var recentData = _.chain(sbx.data.devicestatus) + .filter(function (status) { + return ('xdripjs' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills; + }) + .value( ); + + recentData = _.sortBy(recentData, 'xdripjs.timestamp'); + + _.forEach(recentData, function eachStatus (status) { + getDevice(status); + + var moments = toMoments(status); + + if (status.xdripjs && (!result.latest || moments.timestamp && moments.timestamp.isAfter(result.lastStateTime))) { + result.latest = status; + result.lastStateTime = moment(status.xdripjs.timestamp); + } + }); + + var sendNotification = false; + var sound = 'incoming'; + var message; + var title; + + var sensorInfo = result.latest; + + result.level = levels.NONE; + + if (sensorInfo && sensorInfo.xdripjs) { + + if (sensorInfo.xdripjs.state != 0x6) { + // Send warning notification for all states that are not 'OK' + // but only send state notifications at interval preference + if (!lastStateNotification || (lastStateNotification.state != sensorInfo.xdripjs.state) || !prefs.stateNotifyIntrvl || (moment().diff(lastStateNotification.timestamp, 'minutes') > (prefs.stateNotifyIntrvl*60))) { + sendNotification = true; + lastStateNotification = { + timestamp: moment() + , state: sensorInfo.xdripjs.state + }; + } + + message = 'CGM Transmitter state: ' + sensorInfo.xdripjs.stateString; + title = 'CGM Transmitter state: ' + sensorInfo.xdripjs.stateString; + + if (sensorInfo.xdripjs.state == 0x7) { + // If it is a calibration request, only use INFO + result.level = levels.INFO; + } else { + result.level = levels.WARN; + } + } + + if (sensorInfo.xdripjs.voltagea && (sensorInfo.xdripjs.voltagea < prefs.warnBatV)) { + sendNotification = true; + message = 'CGM Transmitter Battery A Low Voltage: ' + sensorInfo.xdripjs.voltagea; + title = 'CGM Transmitter Battery Low'; + result.level = levels.WARN; + } + + if (sensorInfo.xdripjs.voltageb && (sensorInfo.xdripjs.voltageb < (prefs.warnBatV - 10))) { + sendNotification = true; + message = 'CGM Transmitter Battery B Low Voltage: ' + sensorInfo.xdripjs.voltageb; + title = 'CGM Transmitter Battery Low'; + result.level = levels.WARN; + } + + if (prefs.enableAlerts && sendNotification) { + result.notification = { + title: title + , message: message + , pushoverSound: sound + , level: result.level + , group: 'xDrip-js' + }; + } + + result.lastState = sensorInfo.xdripjs.state; + result.lastStateString = sensorInfo.xdripjs.stateString; + result.lastStateStringShort = sensorInfo.xdripjs.stateStringShort; + result.lastSessionStart = sensorInfo.xdripjs.sessionStart; + result.lastTxId = sensorInfo.xdripjs.txId; + result.lastTxStatus = sensorInfo.xdripjs.txStatus; + result.lastTxStatusString = sensorInfo.xdripjs.txStatusString; + result.lastTxStatusStringShort = sensorInfo.xdripjs.txStatusStringShort; + result.lastTxActivation = sensorInfo.xdripjs.txActivation; + result.lastMode = sensorInfo.xdripjs.mode; + result.lastRssi = sensorInfo.xdripjs.rssi; + result.lastUnfiltered = sensorInfo.xdripjs.unfiltered; + result.lastFiltered = sensorInfo.xdripjs.filtered; + result.lastNoise = sensorInfo.xdripjs.noise; + result.lastNoiseString = sensorInfo.xdripjs.noiseString; + result.lastSlope = Math.round(sensorInfo.xdripjs.slope * 100) / 100.0; + result.lastIntercept = Math.round(sensorInfo.xdripjs.intercept * 100) / 100.0; + result.lastCalType = sensorInfo.xdripjs.calType; + result.lastCalibrationDate = sensorInfo.xdripjs.lastCalibrationDate; + result.lastBatteryTimestamp = sensorInfo.xdripjs.batteryTimestamp; + result.lastVoltageA = sensorInfo.xdripjs.voltagea; + result.lastVoltageB = sensorInfo.xdripjs.voltageb; + result.lastTemperature = sensorInfo.xdripjs.temperature; + result.lastResistance = sensorInfo.xdripjs.resistance; + } + + return result; + }; + + sensorState.updateVisualisation = function updateVisualisation (sbx) { + + var sensor = sbx.properties.sensorState; + var sessionDuration = 'Unknown'; + var info = []; + + _.forIn(sensor.seenDevices, function seenDevice (device) { + info.push( { label: 'Seen: ', value: device.name } ); + }); + + info.push( { label: 'State Time: ', value: (sensor && sensor.lastStateTime && moment().diff(sensor.lastStateTime, 'minutes') + ' minutes ago') || 'Unknown' } ); + info.push( { label: 'Mode: ', value: (sensor && sensor.lastMode) || 'Unknown' } ); + info.push( { label: 'Status: ', value: (sensor && sensor.lastStateString) || 'Unknown' } ); + + // session start is only valid if in a session + if (sensor && sensor.lastSessionStart && (sensor.lastState != 0x1)) { + var diffTime = moment().diff(moment(sensor.lastSessionStart)); + var duration = moment.duration(diffTime); + + sessionDuration = duration.days() + ' days ' + duration.hours() + ' hours'; + + info.push( { label: 'Session Age: ', value: sessionDuration } ); + } + + info.push( { label: 'Tx ID: ', value: (sensor && sensor.lastTxId) || 'Unknown' } ); + info.push( { label: 'Tx Status: ', value: (sensor && sensor.lastTxStatusString) || 'Unknown' } ); + + if (sensor) { + if (sensor.lastTxActivation) { + info.push( { label: 'Tx Age: ', value: moment().diff(moment(sensor.lastTxActivation), 'days') + ' days' } ); + } + + if (sensor.lastRssi) { + info.push( { label: 'RSSI: ', value: sensor.lastRssi } ); + } + + if (sensor.lastUnfiltered) { + info.push( { label: 'Unfiltered: ', value: sensor.lastUnfiltered } ); + } + + if (sensor.lastFiltered) { + info.push( { label: 'Filtered: ', value: sensor.lastFiltered } ); + } + + if (sensor.lastNoiseString) { + info.push( { label: 'Noise: ', value: sensor.lastNoiseString } ); + } + + if (sensor.lastSlope) { + info.push( { label: 'Slope: ', value: sensor.lastSlope } ); + } + + if (sensor.lastIntercept) { + info.push( { label: 'Intercept: ', value: sensor.lastIntercept } ); + } + + if (sensor.lastCalType) { + info.push( { label: 'CalType: ', value: sensor.lastCalType } ); + } + + if (sensor.lastCalibrationDate) { + info.push( { label: 'Calibration: ', value: moment().diff(moment(sensor.lastCalibrationDate), 'hours') + ' hours ago' } ); + } + + if (sensor.lastBatteryTimestamp) { + info.push( { label: 'Battery: ', value: moment().diff(moment(sensor.lastBatteryTimestamp), 'minutes') + ' minutes ago' } ); + } + + if (sensor.lastVoltageA) { + info.push( { label: 'VoltageA: ', value: sensor.lastVoltageA } ); + } + + if (sensor.lastVoltageB) { + info.push( { label: 'VoltageB: ', value: sensor.lastVoltageB } ); + } + + if (sensor.lastTemperature) { + info.push( { label: 'Temperature: ', value: sensor.lastTemperature } ); + } + + if (sensor.lastResistance) { + info.push( { label: 'Resistance: ', value: sensor.lastResistance } ); + } + + var statusClass = null; + if (sensor.level === levels.URGENT) { + statusClass = 'urgent'; + } else if (sensor.level === levels.WARN) { + statusClass = 'warn'; + } else if (sensor.level === levels.INFO) { + // Still highlight even the 'INFO' events for now + statusClass = 'warn'; + } + + sbx.pluginBase.updatePillText(sensorState, { + value: (sensor && sensor.lastStateStringShort) || (sensor && sensor.lastStateString) || 'Unknown' + , label: 'CGM' + , info: info + , pillClass: statusClass + }); + } + }; + + function virtAsstGenericCGMHandler(translateItem, field, next, sbx) { + var response; + var state = _.get(sbx, 'properties.sensorState.'+field); + if (state) { + response = translate('virtAsstCGM'+translateItem, { + params:[ + state + , moment(sbx.properties.sensorState.lastStateTime).from(moment(sbx.time)) + ] + }); + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGM'+translateItem), response); + } + + sensorState.virtAsst = { + intentHandlers: [ + { + intent: 'MetricNow' + , metrics: ['cgm mode'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Mode', 'lastMode', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm status'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Status', 'lastStateString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm session age'] + , intentHandler: function(next, slots, sbx){ + var response; + var lastSessionStart = _.get(sbx, 'properties.sensorState.lastSessionStart'); + // session start is only valid if in a session + if (lastSessionStart) { + if (_.get(sbx, 'properties.sensorState.lastState') != 0x1) { + var duration = moment.duration(moment().diff(moment(lastSessionStart))); + response = translate('virtAsstCGMSessAge', { + params: [ + duration.days(), + duration.hours() + ] + }); + } else { + response = translate('virtAsstCGMSessNotStarted'); + } + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGMSessAge'), response); + } + } + , { + intent: 'MetricNow' + , metrics: ['cgm tx status'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('TxStatus', 'lastTxStatusString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm tx age'] + , intentHandler: function(next, slots, sbx){ + var lastTxActivation = _.get(sbx, 'properties.sensorState.lastTxActivation'); + next( + translate('virtAsstTitleCGMTxAge'), + lastTxActivation + ? translate('virtAsstCGMTxAge', {params:[moment().diff(moment(lastTxActivation), 'days')]}) + : translate('virtAsstUnknown') + ); + } + } + , { + intent: 'MetricNow' + , metrics: ['cgm noise'] + , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Noise', 'lastNoiseString', next, sbx);} + } + , { + intent: 'MetricNow' + , metrics: ['cgm battery'] + , intentHandler: function(next, slots, sbx){ + var response; + var lastVoltageA = _.get(sbx, 'properties.sensorState.lastVoltageA'); + var lastVoltageB = _.get(sbx, 'properties.sensorState.lastVoltageB'); + var lastBatteryTimestamp = _.get(sbx, 'properties.sensorState.lastBatteryTimestamp'); + if (lastVoltageA || lastVoltageB) { + if (lastVoltageA && lastVoltageB) { + response = translate('virtAsstCGMBattTwo', { + params:[ + (lastVoltageA / 100) + , (lastVoltageB / 100) + , moment(lastBatteryTimestamp).from(moment(sbx.time)) + ] + }); + } else { + var finalValue = lastVoltageA ? lastVoltageA : lastVoltageB; + response = translate('virtAsstCGMBattOne', { + params:[ + (finalValue / 100) + , moment(lastBatteryTimestamp).from(moment(sbx.time)) + ] + }); + } + } else { + response = translate('virtAsstUnknown'); + } + + next(translate('virtAsstTitleCGMBatt'), response); + } + } + ] + }; + + return sensorState; +} + +module.exports = init; + diff --git a/lib/profile.js b/lib/profile.js deleted file mode 100644 index 99c60322e88..00000000000 --- a/lib/profile.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -function storage (collection, ctx) { - var ObjectID = require('mongodb').ObjectID; - - function create (obj, fn) { - obj.created_at = (new Date( )).toISOString( ); - api().insert(obj, function (err, doc) { - fn(null, doc); - }); - ctx.bus.emit('data-received'); - } - - function save (obj, fn) { - obj._id = new ObjectID(obj._id); - if (!obj.created_at) { - obj.created_at = (new Date( )).toISOString( ); - } - api().save(obj, function (err) { - //id should be added for new docs - fn(err, obj); - }); - ctx.bus.emit('data-received'); - } - - function list (fn) { - return api( ).find({ }).sort({startDate: -1}).toArray(fn); - } - - function last (fn) { - return api().find().sort({startDate: -1}).limit(1).toArray(fn); - } - - function remove (_id, fn) { - api( ).remove({ '_id': new ObjectID(_id) }, fn); - - ctx.bus.emit('data-received'); - } - - function api () { - return ctx.store.db.collection(collection); - } - - api.list = list; - api.create = create; - api.save = save; - api.remove = remove; - api.last = last; - api.indexedFields = ['startDate']; - return api; -} - -module.exports = storage; diff --git a/static/profile/js/profileeditor.js b/lib/profile/profileeditor.js similarity index 76% rename from static/profile/js/profileeditor.js rename to lib/profile/profileeditor.js index 43c5af2478d..71355dd194a 100644 --- a/static/profile/js/profileeditor.js +++ b/lib/profile/profileeditor.js @@ -1,9 +1,9 @@ -(function () { - 'use strict'; +'use strict'; + +var init = function init () { //for the tests window isn't the global object var $ = window.$; var _ = window._; - var moment = window.moment; var Nightscout = window.Nightscout; var client = Nightscout.client; @@ -16,10 +16,11 @@ var timeInput = $('#pe_time'); var dateInput = $('#pe_date'); - if (serverSettings === undefined) { - console.error('server settings were not loaded, will not call init'); - } else { - client.init(serverSettings, Nightscout.plugins); + client.init(function loaded () { + + console.log("LOADING CLIENT INIT"); + if (c_profile !== null) { + return; // already loaded so don't load again } var translate = client.translate; @@ -28,7 +29,6 @@ //General values 'dia':3, - // Simple style values, 'from' are in minutes from midnight 'carbratio': [ { 'time': '00:00', @@ -67,6 +67,7 @@ 'time': '00:00', 'value': 0 }] + ,startDate: new Date(0).toISOString() }; // , 'startDate': new Date() @@ -77,38 +78,42 @@ var icon_remove = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACrElEQVQ4T42Ty2sTQRzHv5tmk2yyjRNtpfZhL8V6s2KoUNC2XqwgaCsVQcGiFqpHi0c9iRdR/ANE9KR40FIQX4cueKoPaKFoLdSYNtE0abKT1+5s9iW7aUMiHtzTzO7v85md+c6PA4DrHbsPCKIgOWO1pA7dT6YXnXH949SE/F63pqwZtRrO+SCKgjQ5NUV+azpmHj2krMwaJC4c8Erj+/eRyloMMwWFKgbn1nC3ervlK1evkXBLGBZT8SOewotnTylTNLdgeg/pDgZDC2cPHSR8bB22DVC9hFe0SG/H0xFXcHlykjRHRDBWgJcZSCY38Xx2lhqMnRYE34Px/sN9vlQWeoHBAx2yXsRruVAVuFsIBaSJ8+eJGPaBqQV4NROJjTzez89jLBoFn6FgybQL54wS3uTyVDFQ3cL2IYpBv3RhdJSIIQ80tQyv7gEqJvS8AmUlBs7UXPhtjtZgh3UFNYngk86NHCfNAg9dMwHVBPu+CpsVkTXKeJeVG+AGgTOZ3tt6MSKKjy+NjEBjFrR4ElZmA4pdxstMFsyyJu6tZZ7Ux9vwB6EAL50ZGiRECEPPUOixVTRxHlicgSVWxEdZpuZWfNuS2hk48NjwMIkIYZglBnV5Cbqtws/5IaAJmsfCglrEl2y2QeKmEBJ80tixKmxrFpSVr0gV0viQoxho2YUuPohmeFD22PiklLC4ma5JuBvdrfLJI0dJd0s7bM0ES8aR/BXDXGaTskqlL+D3Lwy0tZEePoAd4EA5YF4tYymdonfjmQh3s6dTPjU4SHYGwjAKecSXFyGlM1TdytntE56T+ts7SC/vhw3gm6njc2Kd3vm5Ub1IwQAvnYhGiZpYw1wiWYPrIw7wnBTt7CLOOwdmut14kQQvqt24tfK/utGR6LaF+iRqMf4N/O/8D28HiiCRYqzAAAAAAElFTkSuQmCC'; //var icon_clone = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACSklEQVQ4T5VTYU9SYRR+zn0vXhDEFyRBXcpmbZnZsOXsI2xpVnNcv/ilufET8h/oL8h/gN/aWhs2yjKt26rPSvUh5yQodQlqgKQCd5fbLg2jxlycT+/OOc85z/OcvYSa8MoRLqimUEuzOSgwBl0nFFUtW1K1J/Ho6Hxtr/H2yQqnarJ3fFHucNnC57tdXIUJhydl6CAwIhwfFXCwf5hUC6XpjYXAQgUcUngRzkxlgHc0Kl+57I443S7s5VQwxqCVCYVSGfkTFSQQmsQmHGVzKJ78nJMs2izBohRg8ZFB293iSPQPdPOiqiOdyiGdysdKmp4t64LXxlu9otUKTVdhkSSUjgooHOez7R1uvr2bA10cfx66cb0n3GS14tPHrWz6R34i+fT2m6q0rruv7tvs9gdOtxNk0mA2SRB0gqVZwGb8AOSbXIkMDV+Qt7cy2IinJ+LRWxWNf5ur+Kx2vub0OABRg2QSIYqEr/EMaHjqrdJ/tce/upqIxR76B/8FVzySFc7MzWvtnW6vyjSAAFFgyBkSrt17n7jU1+ldjcVn1x+PzNTZzkWLTXF5zvmYmaEslCEIxnUE7H07AA1NvVPaO9r8X5Kpic+Pfp+oNvpCH/R6rKo5GphUIq1Oh7y++T2wv/LHvLNAtTXqHX8Zkqyt4XQ6M73/emzuf4GnDLg/wu0tbYliqTifWhqZbniAAfCMLs/oRMHU0s26VzjTA6PI/QqXJC1BxAZ3XwSSjbA4/UyesWU/Y2Jw51mgIRmnA4ytXXcU385iINYIg1+OJdcoyf/hkgAAAABJRU5ErkJggg=='; //var icon_apply = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACh0lEQVQ4T5XSXUhTYRgH8P85O+fMOZ3H2WButc7MD3AQWyaYIRZEqANpt9HHCiwvIhWim8gPiqArJailVmdhJGEI3USk4rAQc1MnTdMitZhtfm86ncvcYhMls2i9l+/Hj//zvA+B/1kmcKSArGIohmNIZoIMkRVE1O95sOQaOXCCK+KUYgWcy99gne2zRA1Q9VR1/u7DVcVcIdyrLshFCtQPNyIqgG6gtWImduB69lVM+r9EQqvEajQM8dEBokcxnSWas0fSElMw5B1EdlIuLM43GJxz1P0zgfhhjDFDmsFf1pVixOeASqTGgt+Llk+tEwCt2wDuQgsSWghgxwXYNxvL8iwbIoLjN3KvsQQVRCAUgIyWg3c0wRdYPmoz2C0E7qE2NUFdnpa4D73TfZgLLNThIirCCGuW8MWpRcYC9TF8DYwhPU6DtvEO2Kb666zF9sidMBC6lVsJES1EiCDwZPQZHHMfzBJB7ONdcbLOm3mVcP9wQi5UwO2bQdP7pxOi9aDOYrB7NgATQnfyb2Mq4AJN0eDiU9A82oL+6UFcyS7DXqkSfixDKpDB1P8Ai98Xdd1661aZBO6jNif5YPm5zFOYXZsCQzHYE8dhZP4jspK1mAm6oYpR49Xn1+iZtNW81fdU/zp8kSaSDSSfpzxkPK85jfn1WawSK9BI9mMJXkiFSXAvzYAfMNu79O90v0/u1jdKzPER5IzmJLyhefiJFchFyWApKUzWRnj8Hp1F370VfRPaNgfKZgWfIlEZL2WVQsAASUIZ2sY60Ou01bQXdm2L/kcgvJn5PJ3nElTG0qwSuHwutAy32tsLunZE/ysQPsh5caCaoYRlQgHjoUnC8PK4ZUf0TeAnGJ/iJEGClrwAAAAASUVORK5CYII='; - + var mongorecords = []; var currentrecord = 0; var currentprofile = null; var dirty = false; // Fetch data from mongo - peStatus.hide().text('Loading profile records ...').fadeIn('slow'); - $.ajax('/api/v1/profile.json', { - success: function (records) { + peStatus.hide().text(translate('Loading profile records ...')).fadeIn('slow'); + $.ajax('/api/v1/profile.json?count=20', { + headers: client.headers() + , success: function (records) { + if (!records.length) { + records.push(defaultprofile); + } client.profilefunctions.loadData(records); // do a conversion if needed - mongorecords = client.profilefunctions.data; + mongorecords = _.cloneDeep(client.profilefunctions.data); // create new profile to be edited from last record if (mongorecords.length) { _.each(mongorecords, function eachMongoProfile (mongoprofile) { _.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]; } } convertToRanges(p); }); }); - - peStatus.hide().text('Values loaded.').fadeIn('slow'); + + peStatus.hide().text(translate('Values loaded.')).fadeIn('slow'); } else { mongorecords.push({ defaultProfile: 'Default' @@ -117,10 +122,10 @@ } , startDate: new Date().toISOString() }); - peStatus.hide().text('Default values used.').fadeIn('slow'); + peStatus.hide().text(translate('Default values used.')).fadeIn('slow'); } - }, - error: function () { + } + , error: function () { mongorecords.push({ defaultProfile: 'Default' , store : { @@ -128,10 +133,10 @@ } , startDate: new Date().toISOString() }); - peStatus.hide().text('Error. Default values used.').fadeIn('slow'); + peStatus.hide().text(translate('Error. Default values used.')).fadeIn('slow'); } }).done(initeditor); - + // convert simple values to ranges if needed function convertToRanges(profile) { if (typeof profile.carbratio !== 'object') { profile.carbratio = [{ 'time': '00:00', 'value': profile.carbratio }]; } @@ -140,21 +145,24 @@ if (typeof profile.target_high !== 'object') { profile.target_high = [{ 'time': '00:00', 'value': profile.target_high }]; } if (typeof profile.basal !== 'object') { profile.basal = [{ 'time': '00:00', 'value': profile.basal }]; } if (profile.target_high.length !== profile.target_low.length) { - alert('Time ranges of target_low and target_high don\'t match. Values are restored to defaults.'); + window.alert(translate('Time ranges of target_low and target_high don\'t match. Values are restored to defaults.')); profile.target_low = _.cloneDeep(defaultprofile.target_low); profile.target_high = _.cloneDeep(defaultprofile.target_high); } } - + function initeditor() { + $('#pe_history').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.history); + $('#pe_multiple').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.multiple); + // Load timezones timezoneInput.empty(); - moment.tz.names().forEach(function addTz(tz) { + client.ctx.timezones.forEach(function addTz(tz) { timezoneInput.append(''); }); $('#pe_form').find('button').click(profileSubmit); - + $('#pe_profiles').unbind().bind('change', profileChange); $('#pe_profile_add').unbind().bind('click', profileAdd); $('#pe_profile_remove').unbind().bind('click', profileRemove); @@ -174,39 +182,61 @@ $('#pe_title').text(client.settings.customTitle); currentprofile = mongorecords[currentrecord].defaultProfile; - + // prepare basal profiles initRecord(); // hide unused style of ratios switchStyle(); - + console.log('Done initeditor()'); } - + function initRecord() { databaseRecords.empty(); for (var r = 0; r < mongorecords.length; r++ ) { - databaseRecords.append(''); + databaseRecords.append(''); } databaseRecords.val(currentrecord); - - timeInput.val(moment(mongorecords[currentrecord].startDate).format('HH:mm')); - dateInput.val(moment(mongorecords[currentrecord].startDate).format('YYYY-MM-DD')); - + + timeInput.val(client.ctx.moment(mongorecords[currentrecord].startDate).format('HH:mm')); + dateInput.val(client.ctx.moment(mongorecords[currentrecord].startDate).format('YYYY-MM-DD')); + initProfile(); } - + + function timeDiffMinutes(time1, time2) + { + var minutes1 = toMinutesFromMidnight(time1); + var minutes2 = toMinutesFromMidnight(time2); + if (minutes2 <= minutes1) { + minutes2 += 24*60; + } + return minutes2-minutes1; + } + function refreshTotalBasal() + { + GUIToObject(); + var total = 0; + for (var i=0, len=c_profile['basal'].length; i' + key + ''); } } - + $('#pe_profiles').val(currentprofile); $('#pe_profile_name').val(currentprofile); @@ -214,8 +244,9 @@ mongorecords[currentrecord].defaultProfile = currentprofile; // Set values from profile to html fillTimeRanges(); + refreshTotalBasal(); } - + // Handling of record list box change function recordChange (event) { if (dirty && window.confirm(translate('Save current record before switching to new?'))) { @@ -252,9 +283,7 @@ $.ajax({ method: 'DELETE' , url: '/api/v1/profile/'+mongorecords[currentrecord]._id - , headers: { - 'api-secret': client.hashauth.hash() - } + , headers: client.headers() }).done(function postSuccess () { console.info('profile deleted'); peStatus.hide().text(status).fadeIn('slow'); @@ -284,14 +313,13 @@ profileSubmit(); } GUIToObject(); - mongorecords.push(_.cloneDeep(mongorecords[currentrecord])); + mongorecords.push(_.omit(mongorecords[currentrecord], ['_id', 'srvModified', 'srvCreated', 'identifier', 'mills'])); currentrecord = mongorecords.length - 1; mongorecords[currentrecord].startDate = new Date().toISOString(); currentprofile = mongorecords[currentrecord].defaultProfile; - delete mongorecords[currentrecord]._id; initRecord(); dirty = true; - + maybePreventDefault(event); } @@ -301,8 +329,10 @@ var newpr = $('#pe_profiles').val(); // copy values from html to c_profile GUIToObject(); - + var newname = $('#pe_profile_name').val(); + if (!isNaN(newname)) newname = 'Profile' + newname; + if (currentprofile !== newname) { // rename if already exists while (record.store[newname]) { @@ -321,12 +351,12 @@ maybePreventDefault(event); return false; } - + function profileAdd (event) { var record = mongorecords[currentrecord]; var newname = 'New profile'; while (record.store[newname]) { - newname += '1' + newname += '1'; } record.store[newname] = _.cloneDeep(defaultprofile); currentprofile = newname; @@ -336,7 +366,7 @@ maybePreventDefault(event); return false; } - + function profileRemove (event) { var record = mongorecords[currentrecord]; var availableProfile = getFirstAvailableProfile(record); @@ -350,15 +380,16 @@ maybePreventDefault(event); return false; } - + function profileClone (event) { GUIToObject(); var record = mongorecords[currentrecord]; var newname = $('#pe_profile_name').val() + ' (copy)'; while (record.store[newname]) { - newname += '1' + newname += '1'; } record.store[newname] = _.cloneDeep(record.store[currentprofile]); + currentprofile = newname; dirty = true; @@ -366,7 +397,7 @@ maybePreventDefault(event); return false; } - + // Handling html events and setting/getting values function switchStyle(event) { if (!$('#pe_perGIvalues').is(':checked')) { @@ -378,12 +409,12 @@ } maybePreventDefault(event); } - + function fillTimeRanges(event) { if (event) { GUIToObject(); } - + function shouldAddTime(i, time, array) { if (i === 0 && time === 0) { return true; @@ -394,7 +425,7 @@ return !isNaN(minutesFromMidnight) && minutesFromMidnight < time * 30; } } - + function addSingleLine(e,i) { var tr = $('
    ').append('From: ').append(select)); - tr.append($('').append(e.label).append($('').attr('id',e.prefix+'_val_'+i).attr('value',c_profile[e.array][i].value))); - var icons_td = $('').append($('').attr('class','addsingle').attr('style','cursor:pointer').attr('title','Add new interval before').attr('src',icon_add).attr('array',e.array).attr('pos',i)); + tr.append($('').append(translate('From') + ': ').append(select)); + tr.append($('').append(e.label).append($('').attr('id',e.prefix+'_val_'+i).attr('value',c_profile[e.array][i].value).attr('class', e.prefix + '_value'))); + var icons_td = $('').append($('').attr('class','addsingle').attr('style','cursor:pointer').attr('title',translate('Add new interval before')).attr('src',icon_add).attr('array',e.array).attr('pos',i)); if (c_profile[e.array].length>1) { - icons_td.append($('').attr('class','delsingle').attr('style','cursor:pointer').attr('title','Delete interval').attr('src',icon_remove).attr('array',e.array).attr('pos',i)); + icons_td.append($('').attr('class','delsingle').attr('style','cursor:pointer').attr('title',translate('Delete interval')).attr('src',icon_remove).attr('array',e.array).attr('pos',i)); } tr.append(icons_td); - + if (lowest>toMinutesFromMidnight(c_profile[e.array][i].time)) { c_profile[e.array][i].time = toTimeString(lowest); } return tr[0].outerHTML; } - + // Fill dropdown boxes - _.each([{prefix:'pe_basal', array:'basal', label:'Rate: '}, - {prefix:'pe_ic', array:'carbratio', label:'IC: '}, - {prefix:'pe_isf', array:'sens', label:'ISF: '} + _.each([{prefix:'pe_basal', array:'basal', label: translate('Basal rate') + ' : '}, + {prefix:'pe_ic', array:'carbratio', label: translate('I:C') + ' : '}, + {prefix:'pe_isf', array:'sens', label: translate('ISF') + ' : '} ], function (e) { var html = ''; for (var i=0; i'; + html += ''; html += '
    '; $('#'+e.prefix+'_placeholder').html(html); }); - + $('.pe_basal_value').on('change keyup paste', refreshTotalBasal); $('.addsingle').click(function addsingle_click() { var array = $(this).attr('array'); var pos = $(this).attr('pos'); - GUIToObject(); + GUIToObject(); c_profile[array].splice(pos,0,{time:'00:00',value:0}); - return fillTimeRanges(); + var retVal = fillTimeRanges(); + refreshTotalBasal(); + return retVal; }); - + $('.delsingle').click(function delsingle_click() { var array = $(this).attr('array'); var pos = $(this).attr('pos'); - GUIToObject(); + GUIToObject(); c_profile[array].splice(pos,1); c_profile[array][0].time = '00:00'; - return fillTimeRanges(); + var retVal = fillTimeRanges(); + refreshTotalBasal(); + return retVal; }); function addBGLine(i) { @@ -465,35 +500,35 @@ } var selectedValue = toMinutesFromMidnight(c_profile.target_low[i].time) * 30; select.val(selectedValue); - tr.append($('
    ').append('From: ').append(select)); - tr.append($('').append('Low : ').append($('').attr('id','pe_targetbg_low_'+i).attr('value',c_profile.target_low[i].value))); - tr.append($('').append('High : ').append($('').attr('id','pe_targetbg_high_'+i).attr('value',c_profile.target_high[i].value))); - var icons_td = $('').append($('').attr('class','addtargetbg').attr('style','cursor:pointer').attr('title','Add new interval before').attr('src',icon_add).attr('pos',i)); + tr.append($('').append(translate('From') + ': ').append(select)); + tr.append($('').append(translate('Low') + ' : ').append($('').attr('id','pe_targetbg_low_'+i).attr('value',c_profile.target_low[i].value))); + tr.append($('').append(translate('High') + ' : ').append($('').attr('id','pe_targetbg_high_'+i).attr('value',c_profile.target_high[i].value))); + var icons_td = $('').append($('').attr('class','addtargetbg').attr('style','cursor:pointer').attr('title',translate('Add new interval before')).attr('src',icon_add).attr('pos',i)); if (c_profile.target_low.length>1) { - icons_td.append($('').attr('class','deltargetbg').attr('style','cursor:pointer').attr('title','Delete interval').attr('src',icon_remove).attr('pos',i)); + icons_td.append($('').attr('class','deltargetbg').attr('style','cursor:pointer').attr('title',translate('Delete interval')).attr('src',icon_remove).attr('pos',i)); } tr.append(icons_td); - + // Fix time to correct value after add or change if (lowesttime>toMinutesFromMidnight(c_profile.target_low[i].time)) { c_profile.target_low[i].time = toTimeString(lowesttime); } return tr[0].outerHTML; } - - + + // target BG var html = ''; for (var i=0; i'; + html += ''; html += '
    '; $('#pe_targetbg_placeholder').html(html); - + $('.addtargetbg').click(function addtargetbg_click() { var pos = $(this).attr('pos'); - GUIToObject(); + GUIToObject(); c_profile.target_low.splice(pos,0,{time:'00:00',value:0}); c_profile.target_high.splice(pos,0,{time:'00:00',value:0}); dirty = true; @@ -502,7 +537,7 @@ $('.deltargetbg').click(function deltargetbg_click() { var pos = $(this).attr('pos'); - GUIToObject(); + GUIToObject(); c_profile.target_low.splice(pos,1); c_profile.target_high.splice(pos,1); c_profile.target_low[0].time = '00:00'; @@ -511,13 +546,13 @@ return fillTimeRanges(); }); - $('.pe_selectabletime').unbind().on('change', fillTimeRanges); + $('.pe_selectabletime').unbind().on('change', fillTimeRanges).on('change', refreshTotalBasal); objectToGUI(); maybePreventDefault(event); return false; } - + // fill GUI with values from c_profile object function objectToGUI() { @@ -530,8 +565,21 @@ $('#pe_delay_high').val(c_profile.delay_high); $('#pe_delay_medium').val(c_profile.delay_medium); $('#pe_delay_low').val(c_profile.delay_low); - timezoneInput.val(c_profile.timezone); - + + // find the right zone regardless of string case + + var foundCase = c_profile.timezone; + + if (foundCase != "") { + var lcZone = c_profile.timezone.toLowerCase(); + + client.ctx.timezones.forEach(function testCase(tz) { + if (tz.toLowerCase() == lcZone) foundCase = tz; + }); + } + + timezoneInput.val(foundCase); + var index; [ { prefix:'pe_basal', array:'basal' }, { prefix:'pe_ic', array:'carbratio' }, @@ -542,7 +590,7 @@ $('#'+e.prefix+'_val_'+index).val(c_profile[e.array][index].value); } }); - + for (index=0; index= value.timeAsSeconds) { returnValue = value.value; @@ -100,115 +127,241 @@ function init(profileData) { }); } - if (returnValue) { returnValue = parseFloat(returnValue); } + if (returnValue) { + returnValue = parseFloat(returnValue); + if (isCcpProfile) { + switch (valueType) { + case "sens": + case "carbratio": + returnValue = returnValue * 100 / percentage; + break; + case "basal": + returnValue = returnValue * percentage / 100; + break; + } + } + } - profile.timeValueCache[cacheKey] = returnValue; + cache.put(cacheKey, returnValue, cacheTTL); return returnValue; }; - profile.getCurrentProfile = function getCurrentProfile(time, spec_profile) { - time = time || new Date().getTime(); - var data = profile.hasData() ? profile.data[0] : null; - var timeprofile = spec_profile || profile.activeProfileToTime(time); - return data && data.store[timeprofile] ? data.store[timeprofile] : {}; + profile.getCurrentProfile = function getCurrentProfile (time, spec_profile) { + + time = time || Date.now(); + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = ("profile" + minuteTime + spec_profile); + var returnValue = cache.get(cacheKey); + + if (returnValue) { + return returnValue; + } + + var pdataActive = profile.profileFromTime(time); + var data = profile.hasData() ? pdataActive : null; + var timeprofile = profile.activeProfileToTime(time); + returnValue = data && data.store[timeprofile] ? data.store[timeprofile] : {}; + + cache.put(cacheKey, returnValue, cacheTTL); + return returnValue; + }; - profile.getUnits = function getUnits(spec_profile) { - return profile.getCurrentProfile(spec_profile)['units']; + profile.getUnits = function getUnits (spec_profile) { + var pu = profile.getCurrentProfile(null, spec_profile)['units'] + ' '; + if (pu.toLowerCase().includes('mmol')) return 'mmol'; + return 'mg/dl'; }; - profile.getTimezone = function getTimezone(spec_profile) { - return profile.getCurrentProfile(spec_profile)['timezone']; + profile.getTimezone = function getTimezone (spec_profile) { + let rVal = profile.getCurrentProfile(null, spec_profile)['timezone']; + // Work around Loop uploading non-ISO compliant time zone string + if (rVal) rVal.replace('ETC','Etc'); + return rVal; }; - profile.hasData = function hasData() { + profile.hasData = function hasData () { return profile.data ? true : false; }; - profile.getDIA = function getDIA(time, spec_profile) { - return profile.getValueByTime(time, 'dia', spec_profile); + profile.getDIA = function getDIA (time, spec_profile) { + return profile.getValueByTime(Number(time), 'dia', spec_profile); }; - profile.getSensitivity = function getSensitivity(time, spec_profile) { - return profile.getValueByTime(time, 'sens', spec_profile); + profile.getSensitivity = function getSensitivity (time, spec_profile) { + return profile.getValueByTime(Number(time), 'sens', spec_profile); }; - profile.getCarbRatio = function getCarbRatio(time, spec_profile) { - return profile.getValueByTime(time, 'carbratio', spec_profile); + profile.getCarbRatio = function getCarbRatio (time, spec_profile) { + return profile.getValueByTime(Number(time), 'carbratio', spec_profile); }; - profile.getCarbAbsorptionRate = function getCarbAbsorptionRate(time, spec_profile) { - return profile.getValueByTime(time, 'carbs_hr', spec_profile); + profile.getCarbAbsorptionRate = function getCarbAbsorptionRate (time, spec_profile) { + return profile.getValueByTime(Number(time), 'carbs_hr', spec_profile); }; - profile.getLowBGTarget = function getLowBGTarget(time, spec_profile) { - return profile.getValueByTime(time, 'target_low', spec_profile); + profile.getLowBGTarget = function getLowBGTarget (time, spec_profile) { + return profile.getValueByTime(Number(time), 'target_low', spec_profile); }; - profile.getHighBGTarget = function getHighBGTarget(time, spec_profile) { - return profile.getValueByTime(time, 'target_high', spec_profile); + profile.getHighBGTarget = function getHighBGTarget (time, spec_profile) { + return profile.getValueByTime(Number(time), 'target_high', spec_profile); }; - profile.getBasal = function getBasal(time, spec_profile) { - return profile.getValueByTime(time, 'basal', spec_profile); + profile.getBasal = function getBasal (time, spec_profile) { + return profile.getValueByTime(Number(time), 'basal', spec_profile); }; - profile.updateTreatments = function updateTreatments (profiletreatments, tempbasaltreatments) { - profile.profiletreatments = profiletreatments; - profile.tempbasaltreatments = tempbasaltreatments; - profile.profiletreatments_hash = crypto.createHash('sha1').update(JSON.stringify(profile.profiletreatments)).digest('hex'); - profile.tempbasaltreatments_hash = crypto.createHash('sha1').update(JSON.stringify(profile.tempbasaltreatments)).digest('hex'); + profile.updateTreatments = function updateTreatments (profiletreatments, tempbasaltreatments, combobolustreatments) { + + profile.profiletreatments = profiletreatments || []; + profile.tempbasaltreatments = tempbasaltreatments || []; + + // dedupe temp basal events + profile.tempbasaltreatments = _.uniqBy(profile.tempbasaltreatments, 'mills'); + + _.each(profile.tempbasaltreatments, function addDuration (t) { + t.endmills = t.mills + times.mins(t.duration || 0).msecs; + }); + + profile.tempbasaltreatments.sort(function compareTreatmentMills (a, b) { + return a.mills - b.mills; + }); + + profile.combobolustreatments = combobolustreatments || []; + + cache.clear(); }; - + profile.activeProfileToTime = function activeProfileToTime (time) { if (profile.hasData()) { - var timeprofile = profile.data[0].defaultProfile; - time = time || new Date().getTime(); + time = Number(time) || new Date().getTime(); + + var pdataActive = profile.profileFromTime(time); + var timeprofile = pdataActive.defaultProfile; var treatment = profile.activeProfileTreatmentToTime(time); - if (treatment) { + + if (treatment && pdataActive.store && pdataActive.store[treatment.profile]) { timeprofile = treatment.profile; } return timeprofile; } return null; }; - - profile.activeProfileTreatmentToTime = function activeProfileTreatmentToTime(time) { - var cacheKey = 'profile' + time + profile.profiletreatments_hash; - var returnValue = profile.timeValueCache[cacheKey]; + + profile.activeProfileTreatmentToTime = function activeProfileTreatmentToTime (time) { + + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = 'profileCache' + minuteTime; + var returnValue = cache.get(cacheKey); if (returnValue) { return returnValue; } var treatment = null; - profile.profiletreatments.forEach( function eachTreatment (t) { - if (time > t.mills) { - treatment = t; - } - }); - + if (profile.hasData()) { + var pdataActive = profile.profileFromTime(time); + profile.profiletreatments.forEach(function eachTreatment(t) { + if (time >= t.mills && t.mills >= pdataActive.mills) { + var duration = times.mins(t.duration || 0).msecs; + if (duration != 0 && time < t.mills + duration) { + treatment = t; + // if profile switch contains json of profile inject it in to store to be findable by profile name + if (treatment.profileJson && !pdataActive.store[treatment.profile]) { + if (treatment.profile.indexOf("@@@@@") < 0) + treatment.profile += "@@@@@" + treatment.mills; + let json = JSON.parse(treatment.profileJson); + pdataActive.store[treatment.profile] = json; + } + } + if (duration == 0) { + treatment = t; + // if profile switch contains json of profile inject it in to store to be findable by profile name + if (treatment.profileJson && !pdataActive.store[treatment.profile]) { + if (treatment.profile.indexOf("@@@@@") < 0) + treatment.profile += "@@@@@" + treatment.mills; + let json = JSON.parse(treatment.profileJson); + pdataActive.store[treatment.profile] = json; + } + } + } + }); + } + returnValue = treatment; - profile.timeValueCache[cacheKey] = returnValue; + cache.put(cacheKey, returnValue, cacheTTL); return returnValue; }; - profile.tempBasalTreatment = function tempBasalTreatment(time) { + profile.profileSwitchName = function profileSwitchName (name) { + var index = name.indexOf("@@@@@"); + if (index < 0) return name; + else return name.substring(0, index); + } + + profile.profileFromTime = function profileFromTime (time) { + var profileData = null; + + if (profile.hasData()) { + profileData = profile.data[0]; + for (var i = 0; i < profile.data.length; i++) + { + if (Number(time) >= Number(profile.data[i].mills)) { + profileData = profile.data[i]; + break; + } + } + } + + return profileData; + } + + profile.tempBasalTreatment = function tempBasalTreatment (time) { + + // Most queries for the data in reporting will match the latest found value, caching that hugely improves performance + if (prevBasalTreatment && time >= prevBasalTreatment.mills && time <= prevBasalTreatment.endmills) { + return prevBasalTreatment; + } + + // Binary search for events for O(log n) performance + var first = 0 + , last = profile.tempbasaltreatments.length - 1; + + while (first <= last) { + var i = first + Math.floor((last - first) / 2); + var t = profile.tempbasaltreatments[i]; + if (time >= t.mills && time <= t.endmills) { + prevBasalTreatment = t; + return t; + } + if (time < t.mills) { + last = i - 1; + } else { + first = i + 1; + } + } + + return null; + }; + + profile.comboBolusTreatment = function comboBolusTreatment (time) { var treatment = null; - profile.tempbasaltreatments.forEach( function eachTreatment (t) { - var duration = times.mins(t.duration || 0).msecs; - if (time < t.mills + duration && time > t.mills) { - treatment = t; - } + profile.combobolustreatments.forEach(function eachTreatment (t) { + var duration = times.mins(t.duration || 0).msecs; + if (time < t.mills + duration && time > t.mills) { + treatment = t; + } }); return treatment; }; - profile.getTempBasal = function getTempBasal(time, spec_profile) { + profile.getTempBasal = function getTempBasal (time, spec_profile) { - var cacheKey = 'basal' + time + profile.tempbasaltreatments_hash + spec_profile; - var returnValue = profile.timeValueCache[cacheKey]; + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = 'basalCache' + minuteTime + spec_profile; + var returnValue = cache.get(cacheKey); if (returnValue) { return returnValue; @@ -216,20 +369,28 @@ function init(profileData) { var basal = profile.getBasal(time, spec_profile); var tempbasal = basal; + var combobolusbasal = 0; var treatment = profile.tempBasalTreatment(time); + var combobolustreatment = profile.comboBolusTreatment(time); //special handling for absolute to support temp to 0 if (treatment && !isNaN(treatment.absolute) && treatment.duration > 0) { - tempbasal = treatment.absolute; + tempbasal = Number(treatment.absolute); } else if (treatment && treatment.percent) { tempbasal = basal * (100 + treatment.percent) / 100; } + if (combobolustreatment && combobolustreatment.relative) { + combobolusbasal = combobolustreatment.relative; + } returnValue = { basal: basal , treatment: treatment + , combobolustreatment: combobolustreatment , tempbasal: tempbasal + , combobolusbasal: combobolusbasal + , totalbasal: tempbasal + combobolusbasal }; - profile.timeValueCache[cacheKey] = returnValue; + cache.put(cacheKey, returnValue, cacheTTL); return returnValue; }; @@ -238,17 +399,14 @@ function init(profileData) { if (profile.hasData()) { var current = profile.activeProfileToTime(); profiles.push(current); - - for (var key in profile.data[0].store) { - if (profile.data[0].store.hasOwnProperty(key) && key !== current) { - profiles.push(key); - } - } + + Object.keys(profile.data[0].store).forEach(key => { + if (key !== current && key.indexOf('@@@@@') < 0) profiles.push(key); + }) } return profiles; }; - - + if (profileData) { profile.loadData(profileData); } // init treatments array profile.updateTreatments([], []); @@ -256,4 +414,4 @@ function init(profileData) { return profile; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/report/predictions.js b/lib/report/predictions.js new file mode 100644 index 00000000000..8e341d0b666 --- /dev/null +++ b/lib/report/predictions.js @@ -0,0 +1,33 @@ +var predictions = { + offset: 0, + backward: function () { + this.offset -= 5; + this.updateOffsetHtml(); + }, + forward: function () { + this.offset += 5; + this.updateOffsetHtml(); + }, + moreBackward: function () { + this.offset -= 30; + this.updateOffsetHtml(); + }, + moreForward: function () { + this.offset += 30; + this.updateOffsetHtml(); + }, + reset: function () { + this.offset = 0; + this.updateOffsetHtml(); + }, + updateOffsetHtml: function () { + $('#rp_predictedOffset').html(this.offset); + } +}; + +$(document).on('change', '#rp_optionspredicted', function() { + $('#rp_predictedSettings').toggle(this.checked); + predictions.reset(); +}); + +module.exports = predictions; diff --git a/lib/report/reportclient.js b/lib/report/reportclient.js new file mode 100644 index 00000000000..bf806591436 --- /dev/null +++ b/lib/report/reportclient.js @@ -0,0 +1,916 @@ + +var init = function init () { + 'use strict'; + //for the tests window isn't the global object + var $ = window.$; + var _ = window._; + var Nightscout = window.Nightscout; + var client = Nightscout.client; + var report_plugins_preinit = Nightscout.report_plugins_preinit; + var report_plugins; + + client.init(function loaded () { + + var moment = client.ctx.moment; + + report_plugins = report_plugins_preinit(client.ctx); + Nightscout.report_plugins = report_plugins; + + // init HTML code + report_plugins.addHtmlFromPlugins(client); + // make show() accessible outside for treatments.js + report_plugins.show = show; + + var translate = client.translate; + + var maxInsulinValue = 0 + , maxCarbsValue = 0 + , maxDailyCarbsValue = 0; + var maxdays = 6 * 31; + var datastorage = {}; + var daystoshow = {}; + var sorteddaystoshow = []; + + var targetBGdefault = { + 'mg/dl': { + low: client.settings.thresholds.bgTargetBottom + , high: client.settings.thresholds.bgTargetTop + } + , 'mmol': { + low: client.utils.scaleMgdl(client.settings.thresholds.bgTargetBottom) + , high: client.utils.scaleMgdl(client.settings.thresholds.bgTargetTop) + } + }; + + var ONE_MIN_IN_MS = 60000; + + prepareGUI(); + + // ****** FOOD CODE START ****** + var food_categories = []; + var food_list = []; + + var filter = { + category: '' + , subcategory: '' + , name: '' + }; + + function fillFoodForm (event) { + $('#rp_category').empty().append(''); + Object.keys(food_categories).forEach(function eachCategory (s) { + $('#rp_category').append(''); + }); + filter.category = ''; + fillFoodSubcategories(); + + $('#rp_category').change(fillFoodSubcategories); + $('#rp_subcategory').change(doFoodFilter); + $('#rp_name').on('input', doFoodFilter); + + return maybePrevent(event); + } + + function fillFoodSubcategories (event) { + filter.category = $('#rp_category').val(); + filter.subcategory = ''; + $('#rp_subcategory').empty().append(''); + if (filter.category !== '') { + Object.keys(food_categories[filter.category] || {}).forEach(function eachSubCategory (s) { + $('#rp_subcategory').append(''); + }); + } + doFoodFilter(); + return maybePrevent(event); + } + + function doFoodFilter (event) { + if (event) { + filter.category = $('#rp_category').val(); + filter.subcategory = $('#rp_subcategory').val(); + filter.name = $('#rp_name').val(); + } + $('#rp_food').empty(); + for (var i = 0; i < food_list.length; i++) { + if (filter.category !== '' && food_list[i].category !== filter.category) { continue; } + if (filter.subcategory !== '' && food_list[i].subcategory !== filter.subcategory) { continue; } + if (filter.name !== '' && food_list[i].name.toLowerCase().indexOf(filter.name.toLowerCase()) < 0) { continue; } + var o = ''; + o += food_list[i].name + ' | '; + o += translate('Portion') + ': ' + food_list[i].portion + ' '; + o += food_list[i].unit + ' | '; + o += translate('Carbs') + ': ' + food_list[i].carbs + ' g'; + $('#rp_food').append(''); + } + + return maybePrevent(event); + } + + $('#info').html('' + translate('Loading food database') + ' ...'); + $.ajax('/api/v1/food/regular.json', { + headers: client.headers() + , success: function foodLoadSuccess (records) { + records.forEach(function(r) { + food_list.push(r); + if (r.category && !food_categories[r.category]) { food_categories[r.category] = {}; } + if (r.category && r.subcategory) { food_categories[r.category][r.subcategory] = true; } + }); + fillFoodForm(); + } + }).done(function() { + if (food_list.length) { + enableFoodGUI(); + } else { + disableFoodGUI(); + } + }).fail(function() { + disableFoodGUI(); + }); + + function enableFoodGUI () { + $('#info').html(''); + + $('.rp_foodgui').css('display', ''); + $('#rp_food').change(function(event) { + $('#rp_enablefood').prop('checked', true); + return maybePrevent(event); + }); + } + + function disableFoodGUI () { + $('#info').html(''); + $('.rp_foodgui').css('display', 'none'); + } + + // ****** FOOD CODE END ****** + + function prepareGUI () { + $('.presetdates').click(function(event) { + var days = $(this).attr('days'); + $('#rp_enabledate').prop('checked', true); + return setDataRange(event, days); + }); + $('#rp_show').click(show); + $('#rp_notes').bind('input', function(event) { + $('#rp_enablenotes').prop('checked', true); + return maybePrevent(event); + }); + $('#rp_eventtype').bind('input', function(event) { + $('#rp_enableeventtype').prop('checked', true); + return maybePrevent(event); + }); + + // fill careportal events + $('#rp_eventtype').empty(); + _.each(client.careportal.events, function eachEvent (event) { + $('#rp_eventtype').append(''); + }); + $('#rp_eventtype').append(''); + + $('#rp_targetlow').val(targetBGdefault[client.settings.units.toLowerCase()].low); + $('#rp_targethigh').val(targetBGdefault[client.settings.units.toLowerCase()].high); + + if (client.settings.scaleY === 'linear') { + $('#rp_linear').prop('checked', true); + $('#wrp_linear').prop('checked', true); + } else { + $('#rp_log').prop('checked', true); + $('#wrp_log').prop('checked', true); + } + + $('.menutab').click(switchreport_handler); + + setDataRange(null, 7); + } + + function sgvToColor (sgv, options) { + var color = 'darkgreen'; + + if (sgv > options.targetHigh) { + color = 'red'; + } else if (sgv < options.targetLow) { + color = 'red'; + } + + return color; + } + + function show (event) { + var options = { + width: 1000 + , height: 300 + , weekwidth: 1000 + , weekheight: 300 + , targetLow: 3.5 + , targetHigh: 10 + , raw: true + , notes: true + , food: true + , insulin: true + , carbs: true + , iob: true + , cob: true + , basal: true + , scale: report_plugins.consts.scaleYFromSettings(client) + , weekscale: report_plugins.consts.scaleYFromSettings(client) + , units: client.settings.units + }; + + // default time range if no time range specified in GUI + var zone = client.sbx.data.profile.getTimezone(); + var timerange = '&find[created_at][$gte]=' + moment.tz('2000-01-01', zone).toISOString(); + //console.log(timerange,zone); + options.targetLow = parseFloat($('#rp_targetlow').val().replace(',', '.')); + options.targetHigh = parseFloat($('#rp_targethigh').val().replace(',', '.')); + options.raw = $('#rp_optionsraw').is(':checked'); + options.iob = $('#rp_optionsiob').is(':checked'); + options.cob = $('#rp_optionscob').is(':checked'); + options.openAps = $('#rp_optionsopenaps').is(':checked'); + options.predicted = $('#rp_optionspredicted').is(':checked'); + options.predictedTruncate = $('#rp_optionsPredictedTruncate').is(':checked'); + options.basal = $('#rp_optionsbasal').is(':checked'); + options.notes = $('#rp_optionsnotes').is(':checked'); + options.food = $('#rp_optionsfood').is(':checked'); + options.insulin = $('#rp_optionsinsulin').is(':checked'); + options.insulindistribution = $('#rp_optionsdistribution').is(':checked'); + options.carbs = $('#rp_optionscarbs').is(':checked'); + options.scale = ($('#rp_linear').is(':checked') ? report_plugins.consts.SCALE_LINEAR : report_plugins.consts.SCALE_LOG); + options.weekscale = ($('#wrp_linear').is(':checked') ? report_plugins.consts.SCALE_LINEAR : report_plugins.consts.SCALE_LOG); + options.order = ($('#rp_oldestontop').is(':checked') ? report_plugins.consts.ORDER_OLDESTONTOP : report_plugins.consts.ORDER_NEWESTONTOP); + options.width = parseInt($('#rp_size :selected').attr('x')); + options.weekwidth = parseInt($('#wrp_size :selected').attr('x')); + options.height = parseInt($('#rp_size :selected').attr('y')); + options.weekheight = parseInt($('#wrp_size :selected').attr('y')); + options.loopalyzer = $("#loopalyzer").hasClass("selected"); // We only want to run through Loopalyzer if that tab is selected + if (options.loopalyzer) { + options.iob = true; + 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 + function datefilter () { + if ($('#rp_enabledate').is(':checked')) { + matchesneeded++; + var from = moment.tz(moment($('#rp_from').val()).startOf('day'), zone).startOf('day'); + var to = moment.tz(moment($('#rp_to').val()).endOf('day'), zone).endOf('day'); + timerange = '&find[created_at][$gte]=' + from.toISOString() + '&find[created_at][$lt]=' + to.toISOString(); + + console.log("FROM", from.format( ), "TO", to.format( ), 'timerange', timerange); + //console.log($('#rp_from').val(),$('#rp_to').val(),zone,timerange); + while (from <= to) { + if (daystoshow[from.format('YYYY-MM-DD')]) { + daystoshow[from.format('YYYY-MM-DD')]++; + } else { + daystoshow[from.format('YYYY-MM-DD')] = 1; + } + from.add(1, 'days'); + } + } + //console.log('Dayfilter: ',daystoshow); + foodfilter(); + } + + //food filter + function foodfilter () { + if ($('#rp_enablefood').is(':checked')) { + matchesneeded++; + var _id = $('#rp_food').val(); + if (_id) { + var treatmentData; + var tquery = '?find[boluscalc.foods._id]=' + _id + timerange; + $.ajax('/api/v1/treatments.json' + tquery, { + headers: client.headers() + , success: function(xhr) { + treatmentData = xhr.map(function(treatment) { + return moment.tz(treatment.created_at, zone).format('YYYY-MM-DD'); + }); + // unique it + treatmentData = $.grep(treatmentData, function(v, k) { + return $.inArray(v, treatmentData) === k; + }); + treatmentData.sort(function(a, b) { return a > b; }); + } + }).done(function() { + //console.log('Foodfilter: ',treatmentData); + for (var d = 0; d < treatmentData.length; d++) { + if (daystoshow[treatmentData[d]]) { + daystoshow[treatmentData[d]]++; + } else { + daystoshow[treatmentData[d]] = 1; + } + } + notesfilter(); + }); + } + } else { + notesfilter(); + } + } + + //notes filter + function notesfilter () { + if ($('#rp_enablenotes').is(':checked')) { + matchesneeded++; + var notes = $('#rp_notes').val(); + if (notes) { + var treatmentData; + var tquery = '?find[notes]=/' + notes + '/i'; + $.ajax('/api/v1/treatments.json' + tquery + timerange, { + headers: client.headers() + , success: function(xhr) { + treatmentData = xhr.map(function(treatment) { + return moment.tz(treatment.created_at, zone).format('YYYY-MM-DD'); + }); + // unique it + treatmentData = $.grep(treatmentData, function(v, k) { + return $.inArray(v, treatmentData) === k; + }); + treatmentData.sort(function(a, b) { return a > b; }); + } + }).done(function() { + //console.log('Notesfilter: ',treatmentData); + for (var d = 0; d < treatmentData.length; d++) { + if (daystoshow[treatmentData[d]]) { + daystoshow[treatmentData[d]]++; + } else { + daystoshow[treatmentData[d]] = 1; + } + } + eventtypefilter(); + }); + } + } else { + eventtypefilter(); + } + } + + //event type filter + function eventtypefilter () { + if ($('#rp_enableeventtype').is(':checked')) { + matchesneeded++; + var eventtype = $('#rp_eventtype').val(); + if (eventtype) { + var treatmentData; + var tquery = '?find[eventType]=/' + eventtype + '/i'; + $.ajax('/api/v1/treatments.json' + tquery + timerange, { + headers: client.headers() + , success: function(xhr) { + treatmentData = xhr.map(function(treatment) { + return moment.tz(treatment.created_at, zone).format('YYYY-MM-DD'); + }); + // unique it + treatmentData = $.grep(treatmentData, function(v, k) { + return $.inArray(v, treatmentData) === k; + }); + treatmentData.sort(function(a, b) { return a > b; }); + } + }).done(function() { + //console.log('Eventtypefilter: ',treatmentData); + for (var d = 0; d < treatmentData.length; d++) { + if (daystoshow[treatmentData[d]]) { + daystoshow[treatmentData[d]]++; + } else { + daystoshow[treatmentData[d]] = 1; + } + } + daysfilter(); + }); + } + } else { + daysfilter(); + } + } + + function daysfilter () { + matchesneeded++; + Object.keys(daystoshow).forEach(function eachDay (d) { + var day = moment.tz(d, zone).day(); + if (day === 0 && $('#rp_su').is(':checked')) { daystoshow[d]++; } + if (day === 1 && $('#rp_mo').is(':checked')) { daystoshow[d]++; } + if (day === 2 && $('#rp_tu').is(':checked')) { daystoshow[d]++; } + if (day === 3 && $('#rp_we').is(':checked')) { daystoshow[d]++; } + if (day === 4 && $('#rp_th').is(':checked')) { daystoshow[d]++; } + if (day === 5 && $('#rp_fr').is(':checked')) { daystoshow[d]++; } + if (day === 6 && $('#rp_sa').is(':checked')) { daystoshow[d]++; } + }); + countDays(); + addPreviousDayTreatments(); + display(); + } + + function display () { + var count = 0; + sorteddaystoshow = []; + $('#info').html('' + translate('Loading') + ' ...'); + for (var d in daystoshow) { + if (count < maxdays) { + $('#info').append($('
    ')); + count++; + loadData(d, options, dataLoadedCallback); + } else { + $('#info').append($('
    ' + d + ' ' + translate('not displayed') + '.
    ')); + delete daystoshow[d]; + } + } + if (count === 0) { + $('#info').html('' + translate('Result is empty') + ''); + $('#rp_show').css('display', ''); + } + } + + var dayscount = 0; + var loadeddays = 0; + + function countDays () { + for (var d in daystoshow) { + if (Object.prototype.hasOwnProperty.call(daystoshow, d)) { + if (daystoshow[d] === matchesneeded) { + if (dayscount < maxdays) { + dayscount++; + } + } else { + delete daystoshow[d]; + } + } + } + //console.log('Total: ', daystoshow, 'Matches needed: ', matchesneeded, 'Will be loaded: ', dayscount); + } + + function addPreviousDayTreatments () { + for (var d in daystoshow) { + if (Object.prototype.hasOwnProperty.call(daystoshow, d)) { + var day = moment.tz(d, zone); + var previous = day.subtract(1, 'days'); + var formated = previous.format('YYYY-MM-DD'); + if (!daystoshow[formated]) { + daystoshow[formated] = { treatmentsonly: true }; + console.log('Adding ' + formated + ' for loading treatments'); + dayscount++; + } + } + } + //console.log('Total: ', daystoshow, 'Matches needed: ', matchesneeded, 'Will be loaded: ', dayscount); + } + + function dataLoadedCallback (day) { + loadeddays++; + if (!daystoshow[day].treatmentsonly) { + sorteddaystoshow.push(day); + } + if (loadeddays === dayscount) { + sorteddaystoshow.sort(); + var dFrom = sorteddaystoshow[0]; + var dTo = sorteddaystoshow[(sorteddaystoshow.length - 1)]; + + if (options.order === report_plugins.consts.ORDER_NEWESTONTOP) { + sorteddaystoshow.reverse(); + } + loadProfileSwitch(dFrom, function loadProfileSwitchCallback () { + loadProfilesRange(dFrom, dTo, sorteddaystoshow.length, function loadProfilesCallback () { + $('#info > b').html('' + translate('Rendering') + ' ...'); + window.setTimeout(function() { + showreports(options); + }, 0); + }); + }); + } + } + + $('#rp_show').css('display', 'none'); + daystoshow = {}; + + datefilter(); + return maybePrevent(event); + } + + function showreports (options) { + // prepare some data used in more reports + datastorage.allstatsrecords = []; + datastorage.alldays = 0; + sorteddaystoshow.forEach(function eachDay (day) { + if (!daystoshow[day].treatmentsonly) { + datastorage.allstatsrecords = datastorage.allstatsrecords.concat(datastorage[day].statsrecords); + datastorage.alldays++; + } + }); + options.maxInsulinValue = maxInsulinValue; + options.maxCarbsValue = maxCarbsValue; + options.maxDailyCarbsValue = maxDailyCarbsValue; + + datastorage.treatments = []; + datastorage.devicestatus = []; + datastorage.combobolusTreatments = []; + datastorage.tempbasalTreatments = []; + Object.keys(daystoshow).forEach(function eachDay (day) { + datastorage.treatments = datastorage.treatments.concat(datastorage[day].treatments); + datastorage.devicestatus = datastorage.devicestatus.concat(datastorage[day].devicestatus); + datastorage.combobolusTreatments = datastorage.combobolusTreatments.concat(datastorage[day].combobolusTreatments); + datastorage.tempbasalTreatments = datastorage.tempbasalTreatments.concat(datastorage[day].tempbasalTreatments); + }); + datastorage.tempbasalTreatments = Nightscout.client.ddata.processDurations(datastorage.tempbasalTreatments); + datastorage.treatments.sort(function sort (a, b) { return a.mills - b.mills; }); + + for (var d in daystoshow) { + if (Object.prototype.hasOwnProperty.call(daystoshow, d)) { + if (daystoshow[d].treatmentsonly) { + delete daystoshow[d]; + delete datastorage[d]; + } + } + } + + report_plugins.eachPlugin(function(plugin) { + // jquery plot doesn't draw to hidden div + $('#' + plugin.name + '-placeholder').css('display', ''); + + console.log('Drawing ', plugin.name); + + var skipRender = false; + + if (plugin.name == 'daytoday' && !$('#daytoday').hasClass('selected')) skipRender = true; + if (plugin.name == 'treatments' && !$('#treatments').hasClass('selected')) skipRender = true; + if (plugin.name == 'weektoweek' && !$('#weektoweek').hasClass('selected')) skipRender = true; + if (plugin.name == 'loopalyzer' && !$('#loopalyzer').hasClass('selected')) skipRender = true; + + if (skipRender) { + console.log('Skipping ', plugin.name); + } else { + plugin.report(datastorage, sorteddaystoshow, options); + } + + if (!$('#' + plugin.name).hasClass('selected')) { + $('#' + plugin.name + '-placeholder').css('display', 'none'); + } + }); + + $('#info').html(''); + $('#rp_show').css('display', ''); + } + + function setDataRange (event, days) { + $('#rp_to').val(moment().format('YYYY-MM-DD')); + $('#rp_from').val(moment().add(-days + 1, 'days').format('YYYY-MM-DD')); + return maybePrevent(event); + } + + function switchreport_handler (event) { + var id = $(this).attr('id'); + + $('.menutab').removeClass('selected'); + $('#' + id).addClass('selected'); + + $('.tabplaceholder').css('display', 'none'); + $('#' + id + '-placeholder').css('display', ''); + return maybePrevent(event); + } + + function loadData (day, options, callback) { + // check for loaded data + if ((options.openAps || options.predicted || options.iob || options.cob) && datastorage[day] && !datastorage[day].devicestatus.length) { + // OpenAPS requested but data not loaded. Load anyway ... + } else if (datastorage[day] && day !== moment().format('YYYY-MM-DD')) { + callback(day); + return; + } + // patientData = [actual, predicted, mbg, treatment, cal, devicestatusData]; + var data = {}; + var cgmData = [] + , mbgData = [] + , treatmentData = [] + , calData = []; + var from; + if (client.sbx.data.profile.getTimezone()) { + from = moment(day).tz(client.sbx.data.profile.getTimezone()).startOf('day').format('x'); + } else { + from = moment(day).startOf('day').format('x'); + } + from = parseInt(from); + var to = from + 1000 * 60 * 60 * 24; + + function loadCGMData () { + if (daystoshow[day].treatmentsonly) { + data.sgv = []; + data.mbg = []; + data.cal = []; + return $.Deferred().resolve(); + } + $('#info-' + day).html('' + translate('Loading CGM data of') + ' ' + day + ' ...'); + var query = '?find[date][$gte]=' + from + '&find[date][$lt]=' + to + '&count=10000'; + return $.ajax('/api/v1/entries.json' + query, { + headers: client.headers() + , success: function(xhr) { + xhr.forEach(function(element) { + if (element) { + if (element.mbg) { + mbgData.push({ + y: element.mbg + , mills: element.date + , d: element.dateString + , device: element.device + }); + } else if (element.sgv) { + cgmData.push({ + y: element.sgv + , mills: element.date + , d: element.dateString + , device: element.device + , filtered: element.filtered + , unfiltered: element.unfiltered + , noise: element.noise + , rssi: element.rssi + , sgv: element.sgv + }); + } else if (element.type === 'cal') { + calData.push({ + mills: element.date + 1 + , d: element.dateString + , scale: element.scale + , intercept: element.intercept + , slope: element.slope + }); + } + } + }); + // sometimes cgm contains duplicates. uniq it. + data.sgv = cgmData.slice(); + data.sgv.sort(function(a, b) { return a.mills - b.mills; }); + var lastDate = 0; + data.sgv = data.sgv.filter(function(d) { + var ok = (lastDate + ONE_MIN_IN_MS) <= d.mills; + lastDate = d.mills; + if (!ok) { console.log("itm", JSON.stringify(d)); } + return ok; + }); + data.mbg = mbgData.slice(); + data.mbg.sort(function(a, b) { return a.mills - b.mills; }); + data.cal = calData.slice(); + data.cal.sort(function(a, b) { return a.mills - b.mills; }); + } + }); + } + + function loadTreatmentData () { + if (!datastorage.profileSwitchTreatments) + datastorage.profileSwitchTreatments = []; + $('#info-' + day).html('' + translate('Loading treatments data of') + ' ' + day + ' ...'); + var tquery = '?find[created_at][$gte]=' + new Date(from).toISOString() + '&find[created_at][$lt]=' + new Date(to).toISOString() + '&count=1000'; + return $.ajax('/api/v1/treatments.json' + tquery, { + headers: client.headers() + , cache: false + , success: function(xhr) { + treatmentData = xhr.map(function(treatment) { + var timestamp = new Date(treatment.timestamp || treatment.created_at); + treatment.mills = timestamp.getTime(); + return treatment; + }); + data.treatments = treatmentData.slice(); + data.treatments.sort(function(a, b) { return a.mills - b.mills; }); + // filter 'Combo Bolus' events + data.combobolusTreatments = data.treatments.filter(function filterComboBoluses (t) { + return t.eventType === 'Combo Bolus'; + }); + // filter temp basal treatments + data.tempbasalTreatments = data.treatments.filter(function filterTempBasals (t) { + return t.eventType === 'Temp Basal'; + }); + // filter profile switch treatments + var profileSwitch = data.treatments.filter(function filterProfileSwitch (t) { + return t.eventType === 'Profile Switch'; + }); + datastorage.profileSwitchTreatments = datastorage.profileSwitchTreatments.concat(profileSwitch); + } + }); + } + + function loadDevicestatusData () { + if (daystoshow[day].treatmentsonly) { + data.devicestatus = []; + return $.Deferred().resolve(); + } + if (options.iob || options.cob || options.openAps || options.predicted) { + $('#info-' + day).html('' + translate('Loading device status data of') + ' ' + day + ' ...'); + var tquery = '?find[created_at][$gte]=' + new Date(from).toISOString() + '&find[created_at][$lt]=' + new Date(to).toISOString() + '&count=10000'; + return $.ajax('/api/v1/devicestatus.json' + tquery, { + headers: client.headers() + , success: function(xhr) { + data.devicestatus = xhr.map(function(devicestatus) { + devicestatus.mills = new Date(devicestatus.timestamp || devicestatus.created_at).getTime(); + return devicestatus; + }); + } + }); + } else { + data.devicestatus = []; + return $.Deferred().resolve(); + } + } + + $.when(loadCGMData(), loadTreatmentData(), loadDevicestatusData()).done(function() { + $('#info-' + day).html('' + translate('Processing data of') + ' ' + day + ' ...'); + processData(data, day, options, callback); + }); + } + + function loadProfileSwitch (from, callback) { + $('#info > b').html('' + translate('Loading profile switch data') + ' ...'); + var tquery = '?find[eventType]=Profile Switch' + '&find[created_at][$lte]=' + new Date(from).toISOString() + '&count=1'; + $.ajax('/api/v1/treatments.json' + tquery, { + headers: client.headers() + , success: function(xhr) { + var treatmentData = xhr.map(function(treatment) { + var timestamp = new Date(treatment.timestamp || treatment.created_at); + treatment.mills = timestamp.getTime(); + return treatment; + }); + if (!datastorage.profileSwitchTreatments) + datastorage.profileSwitchTreatments = []; + datastorage.profileSwitchTreatments = datastorage.profileSwitchTreatments.concat(treatmentData); + datastorage.profileSwitchTreatments.sort(function(a, b) { return a.mills - b.mills; }); + } + }).done(function() { + callback(); + }); + } + + function loadProfilesRange (dateFrom, dateTo, dayCount, callback) { + $('#info > b').html('' + translate('Loading profile range') + ' ...'); + + $.when( + loadProfilesRangeCore(dateFrom, dateTo, dayCount) + , loadProfilesRangePrevious(dateFrom) + , loadProfilesRangeNext(dateTo) + ) + .done(callback) + .fail(function() { + datastorage.profiles = []; + }); + } + + function loadProfilesRangeCore (dateFrom, dateTo) { + $('#info > b').html('' + translate('Loading core profiles') + ' ...'); + + //The results must be returned in descending order to work with key logic in routines such as getCurrentProfile + var tquery = '?find[startDate][$gte]=' + new Date(dateFrom).toISOString() + '&find[startDate][$lte]=' + new Date(dateTo).toISOString() + '&sort[startDate]=-1&count=1000'; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers() + , async: false + , success: function(records) { + datastorage.profiles = records; + } + }); + } + + function loadProfilesRangePrevious (dateFrom) { + $('#info > b').html('' + translate('Loading previous profile') + ' ...'); + + //Find first one before the start date and add to datastorage.profiles + var tquery = '?find[startDate][$lt]=' + new Date(dateFrom).toISOString() + '&sort[startDate]=-1&count=1'; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers() + , async: false + , success: function(records) { + records.forEach(function(r) { + datastorage.profiles.push(r); + }); + } + }); + } + + function loadProfilesRangeNext (dateTo) { + $('#info > b').html('' + translate('Loading next profile') + ' ...'); + + //Find first one after the end date and add to datastorage.profiles + var tquery = '?find[startDate][$gt]=' + new Date(dateTo).toISOString() + '&sort[startDate]=1&count=1'; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers() + , async: false + , success: function(records) { + records.forEach(function(r) { + //must be inserted as top to maintain profiles being sorted by date in descending order + datastorage.profiles.unshift(r); + }); + } + }); + } + + function processData (data, day, options, callback) { + if (daystoshow[day].treatmentsonly) { + datastorage[day] = data; + $('#info-' + day).html(''); + callback(day); + return; + } + // treatments + data.dailyCarbs = 0; + data.dailyProtein = 0; + data.dailyFat = 0; + + data.treatments.forEach(function(d) { + if (parseFloat(d.insulin) > maxInsulinValue) { + maxInsulinValue = parseFloat(d.insulin); + } + if (parseFloat(d.carbs) > maxCarbsValue) { + maxCarbsValue = parseFloat(d.carbs); + } + if (d.carbs) { + data.dailyCarbs += Number(d.carbs); + } + if (d.protein) { + data.dailyProtein += Number(d.protein); + } + if (d.fat) { + data.dailyFat += Number(d.fat); + } + }); + if (data.dailyCarbs > maxDailyCarbsValue) { + maxDailyCarbsValue = data.dailyCarbs; + } + + var cal = data.cal[data.cal.length - 1]; + var temp1 = []; + var rawbg = client.rawbg; + if (cal) { + temp1 = data.sgv.map(function(entry) { + entry.mgdl = entry.y; // value names changed from enchilada + var rawBg = rawbg.calc(entry, cal); + return { mills: entry.mills, date: new Date(entry.mills - 2 * 1000), y: rawBg, sgv: client.utils.scaleMgdl(rawBg), color: 'gray', type: 'rawbg', filtered: entry.filtered, unfiltered: entry.unfiltered }; + }).filter(function(entry) { return entry.y > 0 }); + } + var temp2 = data.sgv.map(function(obj) { + return { mills: obj.mills, date: new Date(obj.mills), y: obj.y, sgv: client.utils.scaleMgdl(obj.y), color: sgvToColor(client.utils.scaleMgdl(obj.y), options), type: 'sgv', noise: obj.noise, filtered: obj.filtered, unfiltered: obj.unfiltered }; + }); + data.sgv = [].concat(temp1, temp2); + + //Add MBG's also, pretend they are SGV's + data.sgv = data.sgv.concat(data.mbg.map(function(obj) { return { date: new Date(obj.mills), y: obj.y, sgv: client.utils.scaleMgdl(obj.y), color: 'red', type: 'mbg', device: obj.device } })); + + // make sure data range will be exactly 24h + var from; + if (client.sbx.data.profile.getTimezone()) { + from = moment(day).tz(client.sbx.data.profile.getTimezone()).startOf('day').toDate(); + } else { + from = moment(day).startOf('day').toDate(); + } + var to = new Date(from.getTime() + 1000 * 60 * 60 * 24); + data.sgv.push({ date: from, y: 40, sgv: 40, color: 'transparent', type: 'rawbg' }); + data.sgv.push({ date: to, y: 40, sgv: 40, color: 'transparent', type: 'rawbg' }); + + // clear error data. we don't need it to display them + data.sgv = data.sgv.filter(function(d) { + if (d.y < 39) { + return false; + } + return true; + }); + + data.sgv = data.sgv.map(function eachSgv (sgv) { + var status = _.find(data.devicestatus, function(d) { + return d.mills >= sgv.mills && d.mills < sgv.mills + 5 * 60 * 1000; + }); + + if (status && status.openaps) { + sgv.openaps = status.openaps; + } + return sgv; + }); + + // for other reports + data.statsrecords = data.sgv.filter(function(r) { + if (r.type) { + return r.type === 'sgv'; + } else { + return true; + } + }).map(function(r) { + var ret = {}; + ret.sgv = parseFloat(r.sgv); + ret.bgValue = parseInt(r.y); + ret.displayTime = r.date; + return ret; + }); + + datastorage[day] = data; + $('#info-' + day).html(''); + callback(day); + } + + function maybePrevent (event) { + if (event) { + event.preventDefault(); + } + return false; + } + }); +}; + +module.exports = init; diff --git a/lib/report/reportstorage.js b/lib/report/reportstorage.js new file mode 100644 index 00000000000..5997261d7eb --- /dev/null +++ b/lib/report/reportstorage.js @@ -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}; diff --git a/lib/report_plugins/calibrations.js b/lib/report_plugins/calibrations.js index 72d6f5e3239..baf16a47a27 100644 --- a/lib/report_plugins/calibrations.js +++ b/lib/report_plugins/calibrations.js @@ -1,5 +1,6 @@ 'use strict'; var translate = require('../language')().translate; +var d3 = (global && global.d3) || require('d3'); var calibrations = { name: 'calibrations' @@ -7,30 +8,29 @@ var calibrations = { , pluginType: 'report' }; -function init() { +function init () { return calibrations; } module.exports = init; -calibrations.html = function html(client) { +calibrations.html = function html (client) { var translate = client.translate; var ret = - '

    ' + translate('Calibrations') + '

    ' - + '
    ' - + '
    ' - ; + '

    ' + translate('Calibrations') + '

    ' + + '
    ' + + '
    '; return ret; }; -calibrations.report = function report_calibrations(datastorage,sorteddaystoshow) { +calibrations.report = function report_calibrations (datastorage, sorteddaystoshow) { var Nightscout = window.Nightscout; var report_plugins = Nightscout.report_plugins; var padding = { top: 15, right: 15, bottom: 30, left: 70 }; var treatments = []; - sorteddaystoshow.forEach(function (day) { - treatments = treatments.concat(datastorage[day].treatments.filter(function (t) { + sorteddaystoshow.forEach(function(day) { + treatments = treatments.concat(datastorage[day].treatments.filter(function(t) { if (t.eventType === 'Sensor Start') { return true; } @@ -42,59 +42,58 @@ calibrations.report = function report_calibrations(datastorage,sorteddaystoshow) }); var cals = []; - sorteddaystoshow.forEach(function (day) { + sorteddaystoshow.forEach(function(day) { cals = cals.concat(datastorage[day].cal); }); var sgvs = []; - sorteddaystoshow.forEach(function (day) { + sorteddaystoshow.forEach(function(day) { sgvs = sgvs.concat(datastorage[day].sgv); }); - + var mbgs = []; - sorteddaystoshow.forEach(function (day) { + sorteddaystoshow.forEach(function(day) { mbgs = mbgs.concat(datastorage[day].mbg); }); - mbgs.forEach(function (mbg) { calcmbg(mbg); }); - + mbgs.forEach(function(mbg) { calcmbg(mbg); }); var events = treatments.concat(cals).concat(mbgs).sort(function(a, b) { return a.mills - b.mills; }); - - var colors = ['Aqua','Blue','Brown','Chartreuse','Coral','CornflowerBlue','DarkCyan','DarkMagenta','DarkOrange','Fuchsia','Green','Yellow']; + + var colors = ['Aqua', 'Blue', 'Brown', 'Chartreuse', 'Coral', 'CornflowerBlue', 'DarkCyan', 'DarkMagenta', 'DarkOrange', 'Fuchsia', 'Green', 'Yellow']; var colorindex = 0; var html = ''; var lastmbg = null; - for (var i=0; i'; - }; - + } + html += '
    '; + html += '' + report_plugins.utils.localeDateTime(new Date(e.mills)) + ''; e.bgcolor = colors[colorindex]; if (e.eventType) { - html += ''+translate(e.eventType)+':
    '; + html += '' + translate(e.eventType) + ':
    '; } else if (typeof e.device !== 'undefined') { - html += ' '; - html += 'MBG: ' + e.y + ' Raw: '+e.raw+'
    '; + html += ' '; + html += 'MBG: ' + e.y + ' Raw: ' + e.raw + '
    '; lastmbg = e; e.cals = []; e.checked = false; } else if (typeof e.scale !== 'undefined') { html += 'CAL: ' + ' Scale: ' + e.scale.toFixed(2) + ' Intercept: ' + e.intercept.toFixed(0) + ' Slope: ' + e.slope.toFixed(2) + '
    '; - if (lastmbg) { + if (lastmbg) { lastmbg.cals.push(e); } } else { html += JSON.stringify(e); } html += '
    '; $('#calibrations-list').html(html); - + // select last 3 mbgs checkLastCheckboxes(3); drawelements(); @@ -102,27 +101,27 @@ calibrations.report = function report_calibrations(datastorage,sorteddaystoshow) $('.calibrations-checkbox').change(checkboxevent); function checkLastCheckboxes (maxcals) { - for (i=events.length-1; i>0; i--) { + for (i = events.length - 1; i > 0; i--) { if (typeof events[i].device !== 'undefined') { events[i].checked = true; - $('#calibrations-'+i).prop('checked',true); - if (--maxcals<1) { + $('#calibrations-' + i).prop('checked', true); + if (--maxcals < 1) { break; } } } } - - function checkboxevent(event) { + + function checkboxevent (event) { var index = $(this).attr('index'); events[index].checked = $(this).is(':checked'); drawelements(); event.preventDefault(); } - function drawelements() { + function drawelements () { drawChart(); - for (var i=0; i5*60*1000) { - console.log('Last SGV too old for MBG. Time diff: '+((mbg.mills-lastsgv.mills)/1000/60).toFixed(1)+' min',mbg); + if (mbg.mills - lastsgv.mills > 5 * 60 * 1000) { + console.log('Last SGV too old for MBG. Time diff: ' + ((mbg.mills - lastsgv.mills) / 1000 / 60).toFixed(1) + ' min', mbg); } else { mbg.raw = lastsgv.filtered || lastsgv.unfiltered; } } else { - console.log('Last entry not found for MBG ',mbg); + console.log('Last entry not found for MBG ', mbg); } } - - function drawmbg(mbg) { + + function drawmbg (mbg) { var color = mbg.bgcolor; if (mbg.raw) { calibration_context.append('circle') @@ -266,11 +261,11 @@ calibrations.report = function report_calibrations(datastorage,sorteddaystoshow) .attr('r', 5); } } - - function findlatest(date,storage) { + + function findlatest (date, storage) { var last = null; var time = date.getTime(); - for (var i=0; i time) { return last; } diff --git a/lib/report_plugins/dailystats.js b/lib/report_plugins/dailystats.js index 7fcbd3a15f8..e1e2a4a3f26 100644 --- a/lib/report_plugins/dailystats.js +++ b/lib/report_plugins/dailystats.js @@ -6,80 +6,81 @@ var dailystats = { , pluginType: 'report' }; -function init() { +function init () { return dailystats; } module.exports = init; -dailystats.html = function html(client) { +dailystats.html = function html (client) { var translate = client.translate; var ret = - '

    ' + translate('Daily stats report') + '

    ' - + '
    ' - ; + '

    ' + translate('Daily stats report') + '

    ' + + '
    '; return ret; }; dailystats.css = - '#dailystats-placeholder .tdborder {' - + ' width:80px;' - + ' border: 1px #ccc solid;' - + ' margin: 0;' - + ' padding: 1px;' - + ' text-align:center;' - + '}' - + '#dailystats-placeholder .inlinepiechart {' - + ' width: 2.0in;' - + ' height: 0.9in;' - + '}' - ; - -dailystats.report = function report_dailystats(datastorage,sorteddaystoshow,options) { + '#dailystats-placeholder .tdborder {' + + ' width:80px;' + + ' border: 1px #ccc solid;' + + ' margin: 0;' + + ' padding: 1px;' + + ' text-align:center;' + + '}' + + '#dailystats-placeholder .inlinepiechart {' + + ' width: 2.2in;' + + ' height: 0.9in;' + + '}'; + +dailystats.report = function report_dailystats (datastorage, sorteddaystoshow, options) { var Nightscout = window.Nightscout; var client = Nightscout.client; var translate = client.translate; var report_plugins = Nightscout.report_plugins; - + var ss = require('simple-statistics'); var todo = []; var report = $('#dailystats-report'); - var minForDay, maxForDay; + var minForDay, maxForDay, sum; report.empty(); var table = $(''); report.append(table); var thead = $(''); $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); thead.appendTo(table); - sorteddaystoshow.forEach(function (day) { + sorteddaystoshow.forEach(function(day) { var tr = $(''); var daysRecords = datastorage[day].statsrecords; - + if (daysRecords.length === 0) { $('').appendTo(tr); - $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); table.append(tr); - return;; + return; } minForDay = daysRecords[0].sgv; maxForDay = daysRecords[0].sgv; + sum = 0; + var stats = daysRecords.reduce(function(out, record) { record.sgv = parseFloat(record.sgv); if (record.sgv < options.targetLow) { @@ -95,52 +96,55 @@ dailystats.report = function report_dailystats(datastorage,sorteddaystoshow,opti if (maxForDay < record.sgv) { maxForDay = record.sgv; } + sum += record.sgv; return out; }, { - lows: 0, - normal: 0, - highs: 0 + lows: 0 + , normal: 0 + , highs: 0 }); + var average = sum / daysRecords.length; + var bgValues = daysRecords.map(function(r) { return r.sgv; }); - $('').appendTo(tr); - - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); + $('').appendTo(tr); + + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); table.append(tr); var inrange = [ { - label: translate('Low'), - data: Math.round(stats.lows * 1000 / daysRecords.length) / 10 - }, - { - label: translate('In Range'), - data: Math.round(stats.normal * 1000 / daysRecords.length) / 10 - }, - { - label: translate('High'), - data: Math.round(stats.highs * 1000 / daysRecords.length) / 10 + label: translate('Low') + , data: Math.round(stats.lows * 1000 / daysRecords.length) / 10 + } + , { + label: translate('In Range') + , data: Math.round(stats.normal * 1000 / daysRecords.length) / 10 + } + , { + label: translate('High') + , data: Math.round(stats.highs * 1000 / daysRecords.length) / 10 } ]; $.plot( - '#dailystat-chart-' + day.toString(), - inrange, - { + '#dailystat-chart-' + day.toString() + , inrange, { series: { pie: { show: true } - }, - colors: ['#f88', '#8f8', '#ff8'] + } + , colors: ['#f88', '#8f8', '#ff8'] } ); }); diff --git a/lib/report_plugins/daytoday.js b/lib/report_plugins/daytoday.js index 5f35e3bf9a7..c44cdf0386f 100644 --- a/lib/report_plugins/daytoday.js +++ b/lib/report_plugins/daytoday.js @@ -3,6 +3,7 @@ var _ = require('lodash'); var moment = window.moment; var times = require('../times'); +var d3 = (global && global.d3) || require('d3'); var daytoday = { name: 'daytoday' @@ -10,66 +11,116 @@ var daytoday = { , pluginType: 'report' }; -function init() { +function init () { return daytoday; } module.exports = init; -daytoday.html = function html(client) { +daytoday.html = function html (client) { + const reportStorage = require('../report/reportstorage'); var translate = client.translate; var ret = - translate('Display') + ': ' - + ''+translate('Insulin')+'' - + ''+translate('Carbs')+'' - + ''+translate('Basal rate')+'' - + ''+translate('Notes') - + ''+translate('Food') - + ''+translate('Raw')+'' - + ''+translate('IOB')+'' - + ''+translate('COB')+'' - + ' '+translate('Size') - + ' ' - + '
    ' - + translate('Scale') + ': ' - + '' - + translate('Linear') - + '' - + translate('Logarithmic') - + '
    ' - + '
    ' - + '
    ' - ; - return ret; + '

    ' + translate('Day to day') + '

    ' + + '' + translate('To see this report, press SHOW while in this view') + '
    ' + + translate('Display') + ': ' + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + ' ' + translate('Size') + + ' ' + + '
    ' + + translate('Scale') + ': ' + + '' + + '' + + `
    ` + + translate('Truncate predictions: ') + + `` + + '
    ' + + translate('Predictions offset') + ': ' + + ' minutes' + + '    ' + + '' + + '' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + return ret; }; -daytoday.prepareHtml = function daytodayPrepareHtml(sorteddaystoshow) { +daytoday.prepareHtml = function daytodayPrepareHtml (sorteddaystoshow) { $('#daytodaycharts').html(''); - sorteddaystoshow.forEach(function eachDay(d) { - $('#daytodaycharts').append($('
    ')); + sorteddaystoshow.forEach(function eachDay (d) { + $('#daytodaycharts').append($('
    '+translate('Date')+''+translate('Low')+''+translate('Normal')+''+translate('High')+''+translate('Readings')+''+translate('Min')+''+translate('Max')+''+translate('StDev')+''+translate('25%')+''+translate('Median')+''+translate('75%')+'' + translate('Date') + '' + translate('Low') + '' + translate('Normal') + '' + translate('High') + '' + translate('Readings') + '' + translate('Min') + '' + translate('Max') + '' + translate('Average') + '' + translate('StDev') + '' + translate('25%') + '' + translate('Median') + '' + translate('75%') + '
    ').appendTo(tr); - $('' + report_plugins.utils.localeDate(day) + ''+translate('No data available')+'' + report_plugins.utils.localeDate(day) + '' + translate('No data available') + '
    ' + report_plugins.utils.localeDate(day) + '' + Math.round((100 * stats.lows) / daysRecords.length) + '%' + Math.round((100 * stats.normal) / daysRecords.length) + '%' + Math.round((100 * stats.highs) / daysRecords.length) + '%' + daysRecords.length +'' + minForDay +'' + maxForDay +'' + Math.round(ss.standard_deviation(bgValues)) + '' + ss.quantile(bgValues, 0.25).toFixed(1) + '' + ss.quantile(bgValues, 0.5).toFixed(1) + '' + ss.quantile(bgValues, 0.75).toFixed(1) + '
    ' + report_plugins.utils.localeDate(day) + '' + Math.round((100 * stats.lows) / daysRecords.length) + '%' + Math.round((100 * stats.normal) / daysRecords.length) + '%' + Math.round((100 * stats.highs) / daysRecords.length) + '%' + daysRecords.length + '' + minForDay + '' + maxForDay + '' + average.toFixed(1) + '' + ss.standard_deviation(bgValues).toFixed(1) + '' + ss.quantile(bgValues, 0.25).toFixed(1) + '' + ss.quantile(bgValues, 0.5).toFixed(1) + '' + ss.quantile(bgValues, 0.75).toFixed(1) + '
    ')); }); }; -daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options) { +daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, options) { var Nightscout = window.Nightscout; var client = Nightscout.client; var translate = client.translate; var profile = client.sbx.data.profile; var report_plugins = Nightscout.report_plugins; var scaledTreatmentBG = report_plugins.utils.scaledTreatmentBG; - + var padding = { top: 15, right: 22, bottom: 30, left: 35 }; - - daytoday.prepareHtml(sorteddaystoshow) ; - sorteddaystoshow.forEach( function eachDay(day) { - drawChart(day,datastorage[day],options); + + var tddSum = 0; + var basalSum = 0; + var baseBasalSum = 0; + var bolusSum = 0; + var carbsSum = 0; + var proteinSum = 0; + var fatSum = 0; + + daytoday.prepareHtml(sorteddaystoshow); + console.log('DAY2DAY', 'sorteddaystoshow', sorteddaystoshow); + sorteddaystoshow.forEach(function eachDay (day) { + + drawChart(day, datastorage[day], options); }); - + + var tddAverage = tddSum / datastorage.alldays; + var basalAveragePercent = Math.round( (basalSum / datastorage.alldays) / tddAverage * 100); + var baseBasalAveragePercent = Math.round( (baseBasalSum / datastorage.alldays) / tddAverage * 100); + var bolusAveragePercent = Math.round( (bolusSum / datastorage.alldays) / tddAverage * 100); + var carbsAverage = carbsSum / datastorage.alldays; + var proteinAverage = proteinSum / datastorage.alldays; + var fatAverage = fatSum / datastorage.alldays; + + if (options.insulindistribution) { + var html = '

    ' + translate('TDD average') + ': ' + tddAverage.toFixed(1) + 'U  '; + html += '' + translate('Bolus average') + ': ' + bolusAveragePercent + '%  '; + html += '' + translate('Basal average') + ': ' + basalAveragePercent + '%  '; + html += '(' + translate('Base basal average:') + ' ' + baseBasalAveragePercent + '%)  '; + html += '' + translate('Carbs average') + ': ' + carbsAverage.toFixed(0) + 'g'; + html += '' + translate('Protein average') + ': ' + proteinAverage.toFixed(0) + 'g'; + html += '' + translate('Fat average') + ': ' + fatAverage.toFixed(0) + 'g'; + $('#daytodaycharts').append(html); + } + function timeTicks(n,i) { var t12 = [ '12am', '', '2am', '', '4am', '', '6am', '', '8am', '', '10am', '', @@ -81,68 +132,83 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options) return t12[i]; } } - - function drawChart(day,data,options) { + + function drawChart (day, data, options) { var tickValues , charts , context , xScale2, yScale2 , yInsulinScale, yCarbsScale, yScaleBasals , xAxis2, yAxis2 - , dateFn = function (d) { return new Date(d.date) } + , dateFn = function(d) { return new Date(d.date); } , foodtexts = 0; - tickValues = client.ticks(client, { - scaleY: options.scale === report_plugins.consts.SCALE_LOG ? 'log' : 'linear' - , targetTop: options.targetHigh - , targetBottom: options.targetLow - }); + tickValues = client.ticks(client, { + scaleY: options.scale === report_plugins.consts.SCALE_LOG ? 'log' : 'linear' + , targetTop: options.targetHigh + , targetBottom: options.targetLow + }); + + // add defs for combo boluses + var dashWidth = 5; + d3.select('body').append('svg') + .append('defs') + .append('pattern') + .attr('id', 'hash') + .attr('patternUnits', 'userSpaceOnUse') + .attr('width', 6) + .attr('height', 6) + .attr('x', 0) + .attr('y', 0) + .append('g') + .style('fill', 'none') + .style('stroke', '#0099ff') + .style('stroke-width', 2) + .append('path').attr('d', 'M0,0 l' + dashWidth + ',' + dashWidth) + .append('path').attr('d', 'M' + dashWidth + ',0 l-' + dashWidth + ',' + dashWidth); - // create svg and g to contain the chart contents + // create svg and g to contain the chart contents charts = d3.select('#daytodaychart-' + day).html( - ''+ - report_plugins.utils.localeDate(day)+ + '' + + report_plugins.utils.localeDate(moment(day)) + '
    ' - ).append('svg'); + ).append('svg'); charts.append('rect') .attr('width', '100%') .attr('height', '100%') .attr('fill', 'WhiteSmoke'); - + context = charts.append('g'); // define the parts of the axis that aren't dependent on width or height - xScale2 = d3.time.scale() + xScale2 = d3.scaleTime() .domain(d3.extent(data.sgv, dateFn)); if (options.scale === report_plugins.consts.SCALE_LOG) { - yScale2 = d3.scale.log() - .domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]); + yScale2 = d3.scaleLog() + .domain([client.utils.scaleMgdl(options.basal ? 30 : 36), client.utils.scaleMgdl(420)]); } else { - yScale2 = d3.scale.linear() - .domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]); + yScale2 = d3.scaleLinear() + .domain([client.utils.scaleMgdl(options.basal ? -40 : 36), client.utils.scaleMgdl(420)]); } - yInsulinScale = d3.scale.linear() - .domain([0, options.maxInsulinValue*2]); + // allow insulin to be negative (when plotting negative IOB) + yInsulinScale = d3.scaleLinear() + .domain([-2 * options.maxInsulinValue, 2 * options.maxInsulinValue]); + + yCarbsScale = d3.scaleLinear() + .domain([0, options.maxCarbsValue * 1.25]); - yCarbsScale = d3.scale.linear() - .domain([0, options.maxCarbsValue*1.25]); + yScaleBasals = d3.scaleLinear(); - yScaleBasals = d3.scale.linear(); - - xAxis2 = d3.svg.axis() - .scale(xScale2) + xAxis2 = d3.axisBottom(xScale2) .tickFormat(timeTicks) - .ticks(24) - .orient('bottom'); + .ticks(24); - yAxis2 = d3.svg.axis() - .scale(yScale2) + yAxis2 = d3.axisLeft(yScale2) .tickFormat(d3.format('d')) - .tickValues(tickValues) - .orient('left'); + .tickValues(tickValues); // get current data range var dataRange = d3.extent(data.sgv, dateFn); @@ -154,20 +220,20 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options) //set the width and height of the SVG element charts.attr('width', options.width) .attr('height', options.height); - + // ranges are based on the width and height available so reset xScale2.range([0, chartWidth]); - yScale2.range([chartHeight,0]); - yInsulinScale.range([chartHeight,0]); - yCarbsScale.range([chartHeight,0]); - yScaleBasals.range([chartHeight * 3 / 4, chartHeight]); + yScale2.range([chartHeight, 0]); + yInsulinScale.range([chartHeight * 2, 0]); + yCarbsScale.range([chartHeight, 0]); + yScaleBasals.range([yScale2(client.utils.scaleMgdl(72)), chartHeight]); // add target BG rect context.append('rect') - .attr('x', xScale2(dataRange[0])+padding.left) - .attr('y', yScale2(options.targetHigh)+padding.top) - .attr('width', xScale2(dataRange[1]- xScale2(dataRange[0]))) - .attr('height', yScale2(options.targetLow)-yScale2(options.targetHigh)) + .attr('x', xScale2(dataRange[0]) + padding.left) + .attr('y', yScale2(options.targetHigh) + padding.top) + .attr('width', xScale2(dataRange[1] - xScale2(dataRange[0]))) + .attr('height', yScale2(options.targetLow) - yScale2(options.targetHigh)) .style('fill', '#D6FFD6') .attr('stroke', 'grey'); @@ -194,50 +260,173 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options) .style('fill', 'none') .call(xAxis2); - _.each(tickValues, function (n, li) { + _.each(tickValues, function(n, li) { context.append('line') .attr('class', 'high-line') - .attr('x1', xScale2(dataRange[0])+padding.left) - .attr('y1', yScale2(tickValues[li])+padding.top) - .attr('x2', xScale2(dataRange[1])+padding.left) - .attr('y2', yScale2(tickValues[li])+padding.top) - .style('stroke-dasharray', ('3, 3')) + .attr('x1', xScale2(dataRange[0]) + padding.left) + .attr('y1', yScale2(tickValues[li]) + padding.top) + .attr('x2', xScale2(dataRange[1]) + padding.left) + .attr('y2', yScale2(tickValues[li]) + padding.top) + .style('stroke-dasharray', ('1, 5')) .attr('stroke', 'grey'); }); - // bind up the context chart data to an array of circles - var contextCircles = context.selectAll('circle') - .data(data.sgv); - - function prepareContextCircles(sel) { + function prepareContextCircles (sel) { var badData = []; - sel.attr('cx', function (d) { - return xScale2(d.date) + padding.left; + sel.attr('cx', function(d) { + return xScale2(d.date) + padding.left; + }) + .attr('cy', function(d) { + if (isNaN(d.sgv)) { + badData.push(d); + return yScale2(client.utils.scaleMgdl(450) + padding.top); + } else { + return yScale2(d.sgv) + padding.top; + } + }) + .attr('fill', function(d) { + if (d.color === 'gray' && !options.raw) { + return 'transparent'; + } + return d.color; }) - .attr('cy', function (d) { - if (isNaN(d.sgv)) { - badData.push(d); - return yScale2(client.utils.scaleMgdl(450) + padding.top); - } else { - return yScale2(d.sgv) + padding.top; + .style('opacity', function() { return 0.5 }) + .attr('stroke-width', function(d) { if (d.type === 'mbg') { return 2; } else if (options.openAps && d.openaps) { return 1; } else { return 0; } }) + .attr('stroke', function() { return 'black'; }) + .attr('r', function(d) { + if (d.type === 'mbg') { + return 4; + } else { + return 2 + (options.width - 800) / 400; + } + }) + .on('mouseover', function(d) { + if (options.openAps && d.openaps) { + client.tooltip.style('opacity', .9); + var text = 'BG: ' + d.openaps.suggested.bg + + ', ' + d.openaps.suggested.reason + + (d.openaps.suggested.mealAssist ? ' Meal Assist: ' + d.openaps.suggested.mealAssist : ''); + client.tooltip.html(text) + .style('left', (d3.event.pageX) + 'px') + .style('top', (d3.event.pageY + 15) + 'px'); + } + }) + .on('mouseout', hideTooltip); + + if (badData.length > 0) { + console.warn('Bad Data: isNaN(sgv)', badData); + } + return sel; + } + + // PREDICTIONS START + // + function preparePredictedData () { + + var treatmentsTimestamps = []; // Only timestamps for (carbs and bolus insulin) treatments will be captured in this array + treatmentsTimestamps.push(dataRange[0]); // Create a fake timestamp at midnight so we can show predictions during night + for (var i in data.treatments) { + var treatment = data.treatments[i]; + if (undefined != treatment.carbs && null != treatment.carbs && treatment.carbs > 0) { + if (treatment.timestamp) + treatmentsTimestamps.push(treatment.timestamp); + else if (treatment.created_at) + treatmentsTimestamps.push(treatment.created_at); + } + if (undefined != treatment.insulin && null != treatment.insulin && treatment.insulin > 0) { + if (treatment.timestamp) + treatmentsTimestamps.push(treatment.timestamp); + else if (treatment.created_at) + treatmentsTimestamps.push(treatment.created_at); + } + } + + var predictions = []; + if (data && data.devicestatus) { + for (i = data.devicestatus.length - 1; i >= 0; i--) { + if (data.devicestatus[i].loop && data.devicestatus[i].loop.predicted) { + predictions.push(data.devicestatus[i].loop.predicted); + } else if (data.devicestatus[i].openaps && data.devicestatus[i].openaps.suggested && data.devicestatus[i].openaps.suggested.predBGs) { + var entry = {}; + entry.startDate = data.devicestatus[i].openaps.suggested.timestamp; + // For OpenAPS/AndroidAPS we fall back from COB if present, to UAM, then IOB + if (data.devicestatus[i].openaps.suggested.predBGs.COB) { + entry.values = data.devicestatus[i].openaps.suggested.predBGs.COB; + } else if (data.devicestatus[i].openaps.suggested.predBGs.UAM) { + entry.values = data.devicestatus[i].openaps.suggested.predBGs.UAM; + } else entry.values = data.devicestatus[i].openaps.suggested.predBGs.IOB; + predictions.push(entry); + } + } + } + + var p = []; + if (predictions.length > 0 && treatmentsTimestamps.length > 0) { + + // Iterate over all treatments, find the predictions for each and add them to the predicted array p + for (var treatmentsIndex = 0; treatmentsIndex < treatmentsTimestamps.length; treatmentsIndex++) { + var timestamp = treatmentsTimestamps[treatmentsIndex]; + // TODO refactor code so this is set here - now set as global in file loaded by the browser + var predictedIndex = findPredicted(predictions, timestamp, Nightscout.predictions.offset); // Find predictions offset before or after timestamp + + if (predictedIndex != null) { + entry = predictions[predictedIndex]; // Start entry + var d = moment(entry.startDate); + var end = moment().endOf('day'); + if (options.predictedTruncate) { + if (Nightscout.predictions.offset >= 0) { + // If we are looking forward we want to stop at the next treatment + if (treatmentsIndex < treatmentsTimestamps.length - 1) { + end = moment(treatmentsTimestamps[treatmentsIndex + 1]); + } + } else { + // If we are looking back, then we want to stop at "this" treatment + end = moment(treatmentsTimestamps[treatmentsIndex]); + } } - }) - .attr('fill', function (d) { - if (d.color === 'gray' && !options.raw) { - return 'transparent'; + for (var entryIndex in entry.values) { + if (!d.isAfter(end)) { + var value = {}; + value.sgv = client.utils.scaleMgdl(entry.values[entryIndex]); + value.date = d.toDate(); + value.color = 'purple'; + p.push(value); + d.add(5, 'minutes'); + } } - return d.color; - }) - .style('opacity', function () { return 0.5 }) - .attr('stroke-width', function (d) {if (d.type === 'mbg') { return 2; } else { return 0; }}) - .attr('stroke', function () { return 'black'; }) - .attr('r', function(d) { if (d.type === 'mbg') { return 4; } else { return 2; }}); - - if (badData.length > 0) { - console.warn('Bad Data: isNaN(sgv)', badData); } - return sel; + } + } + return p; + } + + /* Find the earliest new predicted instance that has a timestamp equal to or larger than timestamp */ + /* (so if we have bolused or eaten we want to find the prediction that Loop has estimated just after that) */ + /* Returns the index into the predictions array that is the predicted we are looking for */ + function findPredicted (predictions, timestamp, offset) { + var ts = moment(timestamp).add(offset, 'minutes'); + var predicted = null; + if (offset && offset < 0) { // If offset is negative, start searching from first prediction going forward + for (var i = 0; i < predictions.length; i++) { + if (predictions[i] && predictions[i].startDate && moment(predictions[i].startDate) <= ts) { + predicted = i; + } + } + } else { // If offset is positive or zero, start searching from last prediction going backward + for (i = predictions.length - 1; i > 0; i--) { + if (predictions[i] && predictions[i].startDate && moment(predictions[i].startDate) >= ts) { + predicted = i; + } + } } + return predicted; + } + // + // PREDICTIONS ENDS + + // bind up the context chart data to an array of circles + var contextData = (options.predicted ? data.sgv.concat(preparePredictedData()) : data.sgv); + var contextCircles = context.selectAll('circle').data(contextData); // if new circle then just display prepareContextCircles(contextCircles.enter().append('circle')); @@ -245,88 +434,193 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options) contextCircles.exit() .remove(); - var to = moment(day).add(1, 'days'); - var from = moment(day); - var iobpolyline = '', cobpolyline = ''; - + var from = moment.tz(moment(day), profile.getTimezone( )).startOf('day'); + var to = moment(from.clone( )).add(1, 'days'); + var iobpolyline = '' + , cobpolyline = ''; + // basals data var linedata = []; var notemplinedata = []; var basalareadata = []; var tempbasalareadata = []; + var comboareadata = []; var lastbasal = 0; + var lastDt = from; + var lastIOB; var basals = context.append('g'); - profile.updateTreatments([], datastorage.tempbasaltreatments); - - for (var dt=moment(from); dt < to; dt.add(5, 'minutes')) { - if (options.iob) { - var iob = Nightscout.plugins('iob').calcTotal(data.treatments,profile,dt.toDate()).iob; - if (dt!==from) { - iobpolyline += ', '; + data.netBasalPositive = []; + data.netBasalNegative = []; + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23].forEach(function(hour) { + data.netBasalPositive[hour] = 0; + data.netBasalNegative[hour] = 0; + }); + + profile.loadData(datastorage.profiles); + + profile.updateTreatments(datastorage.profileSwitchTreatments, datastorage.tempbasalTreatments, datastorage.combobolusTreatments); + + var bolusInsulin = 0; + var baseBasalInsulin = 0; + var positiveTemps = 0; + var negativeTemps = 0; + + iobpolyline += (xScale2(moment(from)) + padding.left) + ',' + (yInsulinScale(0) + padding.top) + ' '; + cobpolyline += (xScale2(moment(from)) + padding.left) + ',' + (yCarbsScale(0) + padding.top) + ' '; + var timestart = new Date(); + var cobStatusAvailable = client.plugins('cob').isDeviceStatusAvailable(datastorage.devicestatus); + var iobStatusAvailable = client.plugins('iob').isDeviceStatusAvailable(datastorage.devicestatus); + + console.log("Device COB status available: ", cobStatusAvailable); + console.log("Device IOB status available: ", iobStatusAvailable); + + for (var dt = moment(from); dt < to; dt.add(5, 'minutes')) { + if (options.iob && !iobStatusAvailable) { + var iob = client.plugins('iob').calcTotal(datastorage.treatments, datastorage.devicestatus, profile, dt.toDate()).iob; + // make the graph discontinuous when data is missing + if (iob === undefined) { + iobpolyline += ', ' + (xScale2(lastDt) + padding.left) + ',' + (yInsulinScale(0) + padding.top); + iobpolyline += ', ' + (xScale2(dt) + padding.left) + ',' + (yInsulinScale(0) + padding.top); + } else { + if (lastIOB === undefined) { + iobpolyline += ', ' + (xScale2(dt) + padding.left) + ',' + (yInsulinScale(0) + padding.top); + } + iobpolyline += ', ' + (xScale2(dt) + padding.left) + ',' + (yInsulinScale(iob) + padding.top); } - iobpolyline += (xScale2(dt) + padding.left) + ',' + (yInsulinScale(iob) + padding.top) + ' '; + lastDt = dt.clone(); + lastIOB = iob; } - if (options.cob) { - var cob = Nightscout.plugins('cob').cobTotal(data.treatments,profile,dt.toDate()).cob; - if (dt!==from) { + if (options.cob && !cobStatusAvailable) { + var cob = client.plugins('cob').cobTotal(datastorage.treatments, datastorage.devicestatus, profile, dt.toDate()).cob; + if (!dt.isSame(from)) { cobpolyline += ', '; } cobpolyline += (xScale2(dt.toDate()) + padding.left) + ',' + (yCarbsScale(cob) + padding.top) + ' '; } - if (options.basal) { + if (options.basal) { var date = dt.format('x'); + var hournow = dt.hour(); var basalvalue = profile.getTempBasal(date); + // Calculate basal stats + baseBasalInsulin += basalvalue.basal * 5 / 60; // 5 minutes part + var tempPart = (basalvalue.tempbasal - basalvalue.basal) * 5 / 60; + if (tempPart > 0) { + positiveTemps += tempPart; + data.netBasalPositive[hournow] += tempPart; + } + if (tempPart < 0) { + negativeTemps += tempPart; + data.netBasalNegative[hournow] += tempPart; + } + if (!_.isEqual(lastbasal, basalvalue)) { - linedata.push( { d: date, b: basalvalue.tempbasal } ); - notemplinedata.push( { d: date, b: basalvalue.basal } ); - if (basalvalue.treatment) { - tempbasalareadata.push( { d: date, b: basalvalue.tempbasal } ); - basalareadata.push( { d: date, b: 0 } ); + linedata.push({ d: date, b: basalvalue.totalbasal }); + notemplinedata.push({ d: date, b: basalvalue.basal }); + if (basalvalue.combobolustreatment && basalvalue.combobolustreatment.relative) { + tempbasalareadata.push({ d: date, b: basalvalue.tempbasal }); + basalareadata.push({ d: date, b: 0 }); + comboareadata.push({ d: date, b: basalvalue.totalbasal }); + } else if (basalvalue.treatment) { + tempbasalareadata.push({ d: date, b: basalvalue.totalbasal }); + basalareadata.push({ d: date, b: 0 }); + comboareadata.push({ d: date, b: 0 }); } else { - basalareadata.push( { d: date, b: basalvalue.tempbasal } ); - tempbasalareadata.push( { d: date, b: 0 } ); + tempbasalareadata.push({ d: date, b: 0 }); + basalareadata.push({ d: date, b: basalvalue.totalbasal }); + comboareadata.push({ d: date, b: 0 }); } } lastbasal = basalvalue; } } + + // Draw COB from devicestatuses if available + if (cobStatusAvailable) { + var lastdate = 0; + var previousdate = 0; + var cobArray = client.plugins('cob').COBDeviceStatusesInTimeRange(datastorage.devicestatus, from.valueOf(), to.valueOf()); + _.each(cobArray, function drawCob (point) { + if (previousdate !== 0 && point.mills - previousdate > times.mins(15).msecs) { + cobpolyline += ', '; + cobpolyline += (xScale2(previousdate) + padding.left) + ',' + (yCarbsScale(0) + padding.top) + ' '; + cobpolyline += ', '; + cobpolyline += (xScale2(point.mills) + padding.left) + ',' + (yCarbsScale(0) + padding.top) + ' '; + } + cobpolyline += ', '; + cobpolyline += (xScale2(point.mills) + padding.left) + ',' + (yCarbsScale(point.cob) + padding.top) + ' '; + if (point.mills > lastdate) lastdate = point.mills; + previousdate = point.mills; + }); + cobpolyline += ', ' + (xScale2(lastdate) + padding.left) + ',' + (yCarbsScale(0) + padding.top); + } else { + cobpolyline += ', ' + (xScale2(to) + padding.left) + ',' + (yCarbsScale(0) + padding.top); + } + + // Draw IOB from devicestatuses if available + if (iobStatusAvailable) { + lastdate = 0; + previousdate = 0; + var iobArray = client.plugins('iob').IOBDeviceStatusesInTimeRange(datastorage.devicestatus, from.valueOf(), to.valueOf()); + _.each(iobArray, function drawCob (point) { + if (previousdate !== 0 && point.mills - previousdate > times.mins(15).msecs) { + iobpolyline += ', '; + iobpolyline += (xScale2(previousdate) + padding.left) + ',' + (yInsulinScale(0) + padding.top) + ' '; + iobpolyline += ', '; + iobpolyline += (xScale2(point.mills) + padding.left) + ',' + (yInsulinScale(0) + padding.top) + ' '; + } + iobpolyline += ', '; + iobpolyline += (xScale2(point.mills) + padding.left) + ',' + (yInsulinScale(point.iob) + padding.top) + ' '; + if (point.mills > lastdate) lastdate = point.mills; + previousdate = point.mills; + }); + iobpolyline += ', ' + (xScale2(lastdate) + padding.left) + ',' + (yInsulinScale(0) + padding.top); + } else { + iobpolyline += ', ' + (xScale2(to) + padding.left) + ',' + (yInsulinScale(0) + padding.top); + } + if (options.iob) { context.append('polyline') .attr('stroke', 'blue') .attr('opacity', '0.5') .attr('fill-opacity', '0.1') - .attr('points',iobpolyline); - } + .attr('points', iobpolyline); + } if (options.cob) { context.append('polyline') .attr('stroke', 'red') .attr('opacity', '0.5') .attr('fill-opacity', '0.1') - .attr('points',cobpolyline); - } + .attr('points', cobpolyline); + } if (options.basal) { var toTempBasal = profile.getTempBasal(to.format('x')); - linedata.push( { d: to.format('x'), b: toTempBasal.tempbasal } ); - notemplinedata.push( { d: to.format('x'), b: toTempBasal.basal } ); - basalareadata.push( { d: to.format('x'), b: toTempBasal.basal } ); - tempbasalareadata.push( { d: to.format('x'), b: toTempBasal.tempbasal } ); - - yScaleBasals.domain([d3.max(linedata, function(d) { return d.b; }), 0]); + linedata.push({ d: to.format('x'), b: toTempBasal.totalbasal }); + notemplinedata.push({ d: to.format('x'), b: toTempBasal.basal }); + basalareadata.push({ d: to.format('x'), b: toTempBasal.basal }); + tempbasalareadata.push({ d: to.format('x'), b: toTempBasal.totalbasal }); + comboareadata.push({ d: to.format('x'), b: toTempBasal.totalbasal }); - var valueline = d3.svg.line() - .interpolate('step-after') + var basalMax = d3.max(linedata, function(d) { return d.b; }); + basalMax = Math.max(basalMax, d3.max(basalareadata, function(d) { return d.b; })); + basalMax = Math.max(basalMax, d3.max(tempbasalareadata, function(d) { return d.b; })); + basalMax = Math.max(basalMax, d3.max(comboareadata, function(d) { return d.b; })); + + yScaleBasals.domain([basalMax, 0]); + + var valueline = d3.line() + .curve(d3.curveStepAfter) .x(function(d) { return xScale2(d.d) + padding.left; }) .y(function(d) { return yScaleBasals(d.b) + padding.top; }); - var area = d3.svg.area() - .interpolate('step-after') + var area = d3.area() + .curve(d3.curveStepAfter) .x(function(d) { return xScale2(d.d) + padding.left; }) .y0(yScaleBasals(0) + padding.top) .y1(function(d) { return yScaleBasals(d.b) + padding.top; }); - + var g = basals.append('g'); g.append('path') @@ -362,7 +656,15 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options) .attr('stroke-width', 1) .attr('d', area); - datastorage.tempbasaltreatments.forEach(function (t) { + g.append('path') + .attr('class', 'area comboarea') + .datum(comboareadata) + .attr('fill', 'url(#hash)') + .attr('fill-opacity', .3) + .attr('stroke-width', 1) + .attr('d', area); + + datastorage.tempbasalTreatments.forEach(function(t) { // only if basal and focus interval overlap and there is a chance to fit if (t.mills < to.format('x') && t.mills + times.mins(t.duration).msecs > from.format('x')) { var text = g.append('text') @@ -372,9 +674,9 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options) .attr('fill', '#0099ff') .attr('text-anchor', 'middle') .attr('dy', '.35em') - .attr('x', xScale2((Math.max(t.mills, from.format('x')) + Math.min(t.mills + times.mins(t.duration).msecs, to.format('x')))/2) + padding.left) + .attr('x', xScale2((Math.max(t.mills, from.format('x')) + Math.min(t.mills + times.mins(t.duration).msecs, to.format('x'))) / 2) + padding.left) .attr('y', yScaleBasals(0) - 10 + padding.top) - .text((t.percent ? (t.percent > 0 ? '+' : '') + t.percent + '%' : '') + (t.absolute ? t.absolute + 'U' : '')); + // .text((t.percent ? (t.percent > 0 ? '+' : '') + t.percent + '%' : '') + (t.absolute ? Number(t.absolute).toFixed(2) + 'U' : '')); // better hide if not fit if (text.node().getBBox().width > xScale2(t.mills + times.mins(t.duration).msecs) - xScale2(t.mills)) { text.attr('display', 'none'); @@ -382,17 +684,30 @@ daytoday.report = function report_daytoday(datastorage,sorteddaystoshow,options) } }); } - - data.treatments.forEach(function (treatment) { - if (treatment.boluscalc && treatment.boluscalc.foods && treatment.boluscalc.foods.length > 0 || treatment.notes) { + + data.treatments.forEach(function(treatment) { + // Calculate bolus stats + if (treatment.insulin) { + bolusInsulin += treatment.insulin; + } + // Combo bolus part + if (treatment.relative) { + bolusInsulin += treatment.relative; + } + + var renderNotes = treatment.notes && options.notes; + + if (treatment.eventType === 'Note' && !options.notes) return; + + if (treatment.boluscalc && treatment.boluscalc.foods && treatment.boluscalc.foods.length > 0 || renderNotes) { var lastfoodtext = foodtexts; var drawpointer = false; if (treatment.boluscalc && treatment.boluscalc.foods && treatment.boluscalc.foods.length > 0 && options.food) { var foods = treatment.boluscalc.foods; - for (var fi=0; fi'); + $('
    ' + translate('Bolus insulin:') + '' + bolusInsulin.toFixed(1) + 'U
    ' + translate('Base basal insulin:') + '' + baseBasalInsulin.toFixed(1) + 'U
    ' + translate('Positive temp basal insulin:') + '' + positiveTemps.toFixed(1) + 'U
    ' + translate('Negative temp basal insulin:') + '' + negativeTemps.toFixed(1) + 'U
    ' + translate('Total basal insulin:') + '' + totalBasalInsulin.toFixed(1) + 'U
    ' + translate('Total daily insulin:') + '' + totalDailyInsulin.toFixed(1) + 'U
    ' + translate('Total carbs') + ':' + data.dailyCarbs + ' g
    ' + translate('Total protein') + ':' + data.dailyProtein + ' g
    ' + translate('Total fat') + ':' + data.dailyFat + ' g
    ' + + '' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '* ' + translate('This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:') + + 'Nathan, David M., et al. "Translating the A1C assay into estimated average glucose values." Diabetes care 31.8 (2008): 1473-1478.' + '

    ' + + translate('Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.') + '

    ' + + translate('Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.') + '

    ' + + translate('Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.') + '

    ' + + translate('Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.') + '

    ' + + translate('GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.') + + '


    ' + + translate('Filter by hours') + ':' + + '
    ' + + '0' + + '1' + + '2' + + '3' + + '4' + + '5' + + '6' + + '7' + + '8' + + '9' + + '10' + + '11' + + '12' + + '13' + + '14' + + '15' + + '16' + + '17' + + '18' + + '19' + + '20' + + '21' + + '22' + + '23'; return ret; }; glucosedistribution.css = - '#glucosedistribution-overviewchart {' - + ' width: 2.4in;' - + ' height: 2.4in;' - + ' float:left;' - + '}' - + '#glucosedistribution-placeholder .tdborder {' - + ' width:80px;' - + ' border: 1px #ccc solid;' - + ' margin: 0;' - + ' padding: 1px;' - + ' text-align:center;' - + '}' - ; - - - -glucosedistribution.report = function report_glucosedistribution(datastorage,sorteddaystoshow,options) { + '#glucosedistribution-overviewchart {' + + ' width: 2.4in;' + + ' height: 2.4in;' + + '}' + + '#glucosedistribution-placeholder .tdborder {' + + ' width:80px;' + + ' border: 1px #ccc solid;' + + ' margin: 0;' + + ' padding: 1px;' + + ' text-align:center;' + + '}'; + +glucosedistribution.report = function report_glucosedistribution (datastorage, sorteddaystoshow, options) { var Nightscout = window.Nightscout; var client = Nightscout.client; var translate = client.translate; + var displayUnits = Nightscout.client.settings.units; var ss = require('simple-statistics'); var colors = ['#f88', '#8f8', '#ff8']; - var tablecolors = { Low:'#f88', Normal: '#8f8', High: '#ff8' }; - + var tablecolors = { + Low: '#f88' + , Normal: '#8f8' + , High: '#ff8' + }; + + var enabledHours = [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true]; + var report = $('#glucosedistribution-report'); report.empty(); + + var stability = $('#glucosedistribution-stability'); + stability.empty(); + var stats = []; var table = $(''); var thead = $(''); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); thead.appendTo(table); var data = datastorage.allstatsrecords; var days = datastorage.alldays; - - $('#glucosedistribution-days').text(days+' '+translate('days total')); - - var result = { }; - + + var reportPlugins = Nightscout.report_plugins; + var firstDay = reportPlugins.utils.localeDate(sorteddaystoshow[sorteddaystoshow.length - 1]); + var lastDay = reportPlugins.utils.localeDate(sorteddaystoshow[0]); + + $('#glucosedistribution-days').text(days + ' ' + translate('days total') + ', ' + firstDay + ' - ' + lastDay); + + for (var i = 0; i < 24; i++) { + $('#glucosedistribution-' + i).unbind('click').click(onClick); + enabledHours[i] = $('#glucosedistribution-' + i).is(':checked'); + } + + var result = {}; + + // Filter data for noise + // data cleaning pass 0 - remove duplicates and non-sgv entries, sort + var seen = []; + data = data.filter(function(item) { + if (!item.sgv || !item.bgValue || !item.displayTime || item.bgValue < 39) { + console.log(item); + return false; + } + return seen.includes(item.displayTime) ? false : (seen[item.displayTime] = true); + }); + + data.sort(function(a, b) { + return a.displayTime.getTime() - b.displayTime.getTime(); + }); + + var glucose_data = [data[0]]; + + if (data.length === 0) { + $('#glucosedistribution-days').text(translate('Result is empty')); + return; + } + + // data cleaning pass 1 - add interpolated missing points + for (i = 0; i <= data.length - 2; i++) { + var entry = data[i]; + var nextEntry = data[i + 1]; + + var timeDelta = nextEntry.displayTime.getTime() - entry.displayTime.getTime(); + + if (timeDelta < 9 * 60 * 1000 || timeDelta > 25 * 60 * 1000) { + glucose_data.push(entry); + continue; + } + + var missingRecords = Math.floor(timeDelta / (5 * 60 * 990)) - 1; + + var timePatch = Math.floor(timeDelta / (missingRecords + 1)); + var bgDelta = (nextEntry.bgValue - entry.bgValue) / (missingRecords + 1); + + glucose_data.push(entry); + + for (var j = 1; j <= missingRecords; j++) { + var bg = Math.floor(entry.bgValue + bgDelta * j); + var t = new Date(entry.displayTime.getTime() + j * timePatch); + var newEntry = { + sgv: displayUnits === 'mmol' ? bg / consts.MMOL_TO_MGDL : bg + , bgValue: bg + , displayTime: t + }; + glucose_data.push(newEntry); + } + } + // Need to add the last record, after interpolating between points + glucose_data.push(data[data.length - 1]); + + // data cleaning pass 2 - replace single jumpy measures with interpolated values + var glucose_data2 = [glucose_data[0]]; + var prevEntry = glucose_data[0]; + + const maxGap = (5 * 60 * 1000) + 10000; + + for (i = 1; i <= glucose_data.length - 2; i++) { + let entry = glucose_data[i]; + let nextEntry = glucose_data[i + 1]; + + let timeDelta = nextEntry.displayTime.getTime() - entry.displayTime.getTime(); + let timeDelta2 = entry.displayTime.getTime() - prevEntry.displayTime.getTime(); + + if (timeDelta > maxGap || timeDelta2 > maxGap) { + glucose_data2.push(entry); + prevEntry = entry; + continue; + } + + var delta1 = entry.bgValue - prevEntry.bgValue; + var delta2 = nextEntry.bgValue - entry.bgValue; + + if (delta1 <= 8 && delta2 <= 8) { + glucose_data2.push(entry); + prevEntry = entry; + continue; + } + + if ((delta1 > 0 && delta2 < 0) || (delta1 < 0 && delta2 > 0)) { + const d = (nextEntry.bgValue - prevEntry.bgValue) / 2; + const interpolatedValue = prevEntry.bgValue + d; + + let newEntry = { + sgv: displayUnits === 'mmol' ? interpolatedValue / consts.MMOL_TO_MGDL : interpolatedValue + , bgValue: interpolatedValue + , displayTime: entry.displayTime + }; + glucose_data2.push(newEntry); + prevEntry = newEntry; + continue; + } + + glucose_data2.push(entry); + prevEntry = entry; + } + // Need to add the last record, after interpolating between points + glucose_data2.push(glucose_data[glucose_data.length - 1]); + + glucose_data = data = glucose_data2.filter(function(r) { + return enabledHours[new Date(r.displayTime).getHours()] + }); + + glucose_data.sort(function(a, b) { + return a.displayTime.getTime() - b.displayTime.getTime(); + }); + + var timeTotal = 0; + for (i = 1; i <= glucose_data.length - 2; i++) { + let entry = glucose_data[i]; + let nextEntry = glucose_data[i + 1]; + let timeDelta = nextEntry.displayTime.getTime() - entry.displayTime.getTime(); + if (timeDelta < maxGap) { + timeTotal += timeDelta; + } + } + + var daysTotal = timeTotal / (1000 * 60 * 60 * 24); + ['Low', 'Normal', 'High'].forEach(function(range) { - result[range] = { }; + result[range] = {}; var r = result[range]; - r.rangeRecords = data.filter(function(r) { + r.rangeRecords = glucose_data.filter(function(r) { if (range === 'Low') { return r.sgv > 0 && r.sgv < options.targetLow; } else if (range === 'Normal') { @@ -91,27 +271,42 @@ glucosedistribution.report = function report_glucosedistribution(datastorage,sor } }); stats.push(r.rangeRecords.length); - r.rangeRecords.sort(function(a,b) { + r.rangeRecords.sort(function(a, b) { return a.sgv - b.sgv; }); - r.localBgs = r.rangeRecords.map(function(r) { return r.sgv; }).filter(function(bg) { return !!bg; }); + r.localBgs = r.rangeRecords.map(function(r) { + return r.sgv; + }).filter(function(bg) { + return !!bg; + }); r.midpoint = Math.floor(r.rangeRecords.length / 2); - r.readingspct = Math.floor(100 * r.rangeRecords.length / data.length); + r.readingspct = (100 * r.rangeRecords.length / data.length).toFixed(1); if (r.rangeRecords.length > 0) { - r.mean = Math.floor(10*ss.mean(r.localBgs))/10; + r.mean = Math.floor(10 * ss.mean(r.localBgs)) / 10; r.median = r.rangeRecords[r.midpoint].sgv; - r.stddev = Math.floor(ss.standard_deviation(r.localBgs)*10)/10; + r.stddev = Math.floor(ss.standard_deviation(r.localBgs) * 10) / 10; } }); - + // make sure we have total 100% - result.Normal.readingspct = 100 - result.Low.readingspct - result.High.readingspct; - + result.Normal.readingspct = (100 - result.Low.readingspct - result.High.readingspct).toFixed(1); + ['Low', 'Normal', 'High'].forEach(function(range) { var tr = $(''); var r = result[range]; - $('').appendTo(tr); + var rangeExp = ''; + if (range == 'Low') { + rangeExp = ' (<' + options.targetLow + ')'; + } + if (range == 'High') { + rangeExp = ' (>=' + options.targetHigh + ')'; + } + + var rangeLabel = range; + if (rangeLabel == 'Normal') rangeLabel = 'In Range'; + + $('').appendTo(tr); $('').appendTo(tr); $('').appendTo(tr); if (r.rangeRecords.length > 0) { @@ -128,18 +323,26 @@ glucosedistribution.report = function report_glucosedistribution(datastorage,sor table.append(tr); }); - + var tr = $(''); - $('').appendTo(tr); + $('').appendTo(tr); $('').appendTo(tr); - $('').appendTo(tr); - if (data.length > 0) { - var localBgs = data.map(function(r) { return r.sgv; }).filter(function(bg) { return !!bg; }); - var mgDlBgs = data.map(function(r) { return r.bgValue; }).filter(function(bg) { return !!bg; }); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); + $('').appendTo(tr); + if (glucose_data.length > 0) { + var localBgs = glucose_data.map(function(r) { + return r.sgv; + }).filter(function(bg) { + return !!bg; + }); + var mgDlBgs = glucose_data.map(function(r) { + return r.bgValue; + }).filter(function(bg) { + return !!bg; + }); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); } else { $('').appendTo(tr); $('').appendTo(tr); @@ -149,18 +352,137 @@ glucosedistribution.report = function report_glucosedistribution(datastorage,sor table.append(tr); report.append(table); + // Stability + var t1 = 6; + var t2 = 11; + var t1count = 0; + var t2count = 0; + + var events = 0; + + var GVITotal = 0; + var GVIIdeal = 0; + var GVIIdeal_Time = 0; + + var RMSTotal = 0; + + var usedRecords = 0; + var glucoseTotal = 0; + var deltaTotal = 0; + + for (i = 0; i <= glucose_data.length - 2; i++) { + const entry = glucose_data[i]; + const nextEntry = glucose_data[i + 1]; + const timeDelta = nextEntry.displayTime.getTime() - entry.displayTime.getTime(); + + // Use maxGap constant + if (timeDelta == 0 || timeDelta > maxGap) { // 6 * 60 * 1000) { + // console.log("Record skipped"); + continue; + } + + usedRecords += 1; + events += 1; + + var delta = Math.abs(nextEntry.bgValue - entry.bgValue); + deltaTotal += delta; + + if (delta > 0) { // avoid divide by 0 error + // Are we rising at faster than 5mg/DL/5minutes + if ((delta / timeDelta) >= (t1 / (1000 * 60 * 5))) { + t1count += 1; + } + // Are we rising at faster than 10mg/DL/5minutes + if ((delta / timeDelta) >= (t2 / (1000 * 60 * 5))) { + t2count += 1; + } + } + + // Calculate the distance travelled for this time step + GVITotal += Math.sqrt(Math.pow(timeDelta / (1000 * 60), 2) + Math.pow(delta, 2)); + + // Keep track of the number of minutes in this timestep + GVIIdeal_Time += timeDelta / (1000 * 60); + glucoseTotal += entry.bgValue; + + if (entry.bgValue < options.targetLow) { + RMSTotal += Math.pow(options.targetLow - entry.bgValue, 2); + } + if (entry.bgValue > options.targetHigh) { + RMSTotal += Math.pow(entry.bgValue - options.targetHigh, 2); + } + } + + // Difference between first and last reading + var GVIDelta = Math.floor(glucose_data[0].bgValue - glucose_data[glucose_data.length - 1].bgValue); + + // Delta for total time considered against total period rise + GVIIdeal = Math.sqrt(Math.pow(GVIIdeal_Time, 2) + Math.pow(GVIDelta, 2)); + + var GVI = Math.round(GVITotal / GVIIdeal * 100) / 100; + console.log('GVI', GVI, 'GVIIdeal', GVIIdeal, 'GVITotal', GVITotal, 'GVIIdeal_Time', GVIIdeal_Time); + + var glucoseMean = Math.floor(glucoseTotal / usedRecords); + var tirMultiplier = result.Normal.readingspct / 100.0; + var PGS = Math.round(GVI * glucoseMean * (1 - tirMultiplier) * 100) / 100; + console.log('glucoseMean', glucoseMean, 'tirMultiplier', tirMultiplier, 'PGS', PGS); + + var TDC = deltaTotal / daysTotal; + var TDCHourly = TDC / 24.0; + + var RMS = Math.sqrt(RMSTotal / events); + + // console.log('TADC',TDC,'days',days); + + var timeInT1 = Math.round(100 * t1count / events).toFixed(1); + var timeInT2 = Math.round(100 * t2count / events).toFixed(1); + + var unitString = ' mg/dl'; + if (displayUnits == 'mmol') { + TDC = TDC / consts.MMOL_TO_MGDL; + TDCHourly = TDCHourly / consts.MMOL_TO_MGDL; + unitString = ' mmol/L'; + + RMS = Math.sqrt(RMSTotal / events) / consts.MMOL_TO_MGDL; + } + + TDC = Math.round(TDC * 100) / 100; + TDCHourly = Math.round(TDCHourly * 100) / 100; + + var stabilitytable = $('
    '+translate('Range')+''+translate('% of Readings')+''+translate('# of Readings')+''+translate('Mean')+''+translate('Median')+''+translate('Standard Deviation')+''+translate('A1c estimation*')+'' + translate('Range') + '' + translate('% of Readings') + '' + translate('# of Readings') + '' + translate('Average') + '' + translate('Median') + '' + translate('Standard Deviation') + '' + translate('A1c estimation*') + '
    ' + translate(range) + ': ' + translate(rangeLabel) + rangeExp + ': ' + r.readingspct + '%' + r.rangeRecords.length + '
    '+translate('Overall')+': ' + translate('Overall') + ': ' + data.length + '' + (Math.round(10*ss.mean(localBgs))/10).toFixed(1) + '' + (Math.round(10*ss.quantile(localBgs, 0.5))/10).toFixed(1) + '' + (Math.round(ss.standard_deviation(localBgs)*10)/10).toFixed(1) + '
    ' + (Math.round(10*(ss.mean(mgDlBgs)+46.7)/28.7)/10).toFixed(1) + '%DCCT | ' + Math.round(((ss.mean(mgDlBgs)+46.7)/28.7 - 2.15)*10.929) + 'IFCC
    ' + glucose_data.length + '' + (Math.round(10 * ss.mean(localBgs)) / 10).toFixed(1) + '' + (Math.round(10 * ss.quantile(localBgs, 0.5)) / 10).toFixed(1) + '' + (Math.round(ss.standard_deviation(localBgs) * 10) / 10).toFixed(1) + '
    ' + (Math.round(10 * (ss.mean(mgDlBgs) + 46.7) / 28.7) / 10).toFixed(1) + '%DCCT | ' + Math.round(((ss.mean(mgDlBgs) + 46.7) / 28.7 - 2.15) * 10.929) + 'IFCC
    N/AN/A
    '); + + var t1exp = '>5 mg/dl/5m'; + var t2exp = '>10 mg/dl/5m'; + if (displayUnits == 'mmol') { + t1exp = '>0.27 mmol/l/5m'; + t2exp = '>0.55 mmol/l/5m'; + } + + $('').appendTo(stabilitytable); + $('').appendTo(stabilitytable); + + $('').appendTo(stabilitytable); + $('').appendTo(stabilitytable); + + $('').appendTo(stabilitytable); + $('').appendTo(stabilitytable); + stabilitytable.appendTo(stability); + setTimeout(function() { $.plot( - '#glucosedistribution-overviewchart', - stats, - { + '#glucosedistribution-overviewchart' + , stats, { series: { pie: { show: true } - }, - colors: colors + } + , colors: colors } ); }); + + function onClick () { + report_glucosedistribution(datastorage, sorteddaystoshow, options); + } }; diff --git a/lib/report_plugins/hourlystats.js b/lib/report_plugins/hourlystats.js index 59f86be5209..84114209c96 100644 --- a/lib/report_plugins/hourlystats.js +++ b/lib/report_plugins/hourlystats.js @@ -8,90 +8,119 @@ var hourlystats = { , pluginType: 'report' }; -function init() { +function init () { return hourlystats; } module.exports = init; -hourlystats.html = function html(client) { +hourlystats.html = function html (client) { var translate = client.translate; var ret = - '

    ' + translate('Hourly stats') + '

    ' - + '
    ' - + '
    ' - ; + '

    ' + translate('Hourly stats') + '

    ' + + '
    ' + + '
    '; return ret; }; hourlystats.css = - '#hourlystats-overviewchart {' - + ' width: 100%;' - + ' min-width: 6.5in;' - + ' height: 5in;' - + '}' - + '#hourlystats-placeholder td {' - + ' text-align:center;' - + '}' ; - -hourlystats.report = function report_hourlystats(datastorage, sorteddaystoshow, options) { -//console.log(window); + '#hourlystats-overviewchart {' + + ' width: 100%;' + + ' min-width: 6.5in;' + + ' height: 5in;' + + '}' + + '#hourlystats-placeholder td {' + + ' text-align:center;' + + '}'; + +hourlystats.report = function report_hourlystats (datastorage, sorteddaystoshow, options) { + //console.log(window); var ss = require('simple-statistics'); var Nightscout = window.Nightscout; var client = Nightscout.client; var translate = client.translate; + var report_plugins = Nightscout.report_plugins; var report = $('#hourlystats-report'); var stats = []; var pivotedByHour = {}; var data = datastorage.allstatsrecords; - + for (var i = 0; i < 24; i++) { pivotedByHour[i] = []; } + + data = data.filter(function(o) { return !isNaN(o.sgv); }); + data.forEach(function(record) { + var d = new Date(record.displayTime); + record.sgv = Number(record.sgv); pivotedByHour[d.getHours()].push(record); }); + var table = $('
    ' + translate('Mean Total Daily Change') + '' + translate('Time in fluctuation') + '
    (' + t1exp + ')
    ' + translate('Time in rapid fluctuation') + '
    (' + t2exp + ')
    ' + TDC + unitString + '' + timeInT1 + '%' + timeInT2 + '%
    ' + translate('Mean Hourly Change') + 'GVIPGS
    ' + TDCHourly + unitString + '' + GVI + '' + PGS + '
    Out of Range RMS
    ' + Math.round(RMS * 100) / 100 + unitString + '
    '); var thead = $(''); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); - $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); + $('').appendTo(thead); thead.appendTo(table); - [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23].forEach(function(hour) { + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23].forEach(function(hour) { var tr = $(''); - var display = new Date(0, 0 , 1, hour, 0, 0, 0).toLocaleTimeString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'); + var display = new Date(0, 0, 1, hour, 0, 0, 0).toLocaleTimeString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'); - var avg = Math.floor(pivotedByHour[hour].map(function(r) { return r.sgv; }).reduce(function(o,v){ return o+v; }, 0) / pivotedByHour[hour].length); + var avg = Math.floor(pivotedByHour[hour].map(function(r) { + return r.sgv; + }).reduce(function(o, v) { + return o + v; + }, 0) / pivotedByHour[hour].length); var d = new Date(times.hours(hour).msecs); - var dev = ss.standard_deviation(pivotedByHour[hour].map(function(r) { return r.sgv; })); + var dev = ss.standard_deviation(pivotedByHour[hour].map(function(r) { + return r.sgv; + })); stats.push([ - new Date(d), - ss.quantile(pivotedByHour[hour].map(function(r) { return r.sgv; }), 0.25), - ss.quantile(pivotedByHour[hour].map(function(r) { return r.sgv; }), 0.75), - avg - dev, - avg + dev + new Date(d) + , ss.quantile(pivotedByHour[hour].map(function(r) { + return r.sgv; + }), 0.25) + , ss.quantile(pivotedByHour[hour].map(function(r) { + return r.sgv; + }), 0.75) + , avg - dev + , avg + dev ]); var tmp; $('').appendTo(tr); $('').appendTo(tr); $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); - $('').appendTo(tr); + $('').appendTo(tr); + // eslint-disable-next-line no-cond-assign + $('').appendTo(tr); + // eslint-disable-next-line no-cond-assign + $('').appendTo(tr); + // eslint-disable-next-line no-cond-assign + $('').appendTo(tr); + $('').appendTo(tr); + $('').appendTo(tr); table.append(tr); }); @@ -99,30 +128,91 @@ hourlystats.report = function report_hourlystats(datastorage, sorteddaystoshow, report.append(table); $.plot( - '#hourlystats-overviewchart', - [{ - data:stats, - candle:true - }], - { + '#hourlystats-overviewchart' + , [{ + data: stats + , candle: true + }], { series: { - candle: true, - lines: false //Somehow it draws lines if you dont disable this. Should investigate and fix this ;) - }, - xaxis: { - mode: 'time', - timeFormat: '%h:00', - min: 0, - max: times.hours(24).msecs - times.secs(1).msecs - }, - yaxis: { - min: 0, - max: options.units === 'mmol' ? 22: 400, - show: true - }, - grid: { + candle: true + , lines: false //Somehow it draws lines if you dont disable this. Should investigate and fix this ;) + } + , xaxis: { + mode: 'time' + , timeFormat: '%h:00' + , min: 0 + , max: times.hours(24).msecs - times.secs(1).msecs + } + , yaxis: { + min: 0 + , max: options.units === 'mmol' ? 22 : 400 + , show: true + } + , grid: { show: true } } ); -}; \ No newline at end of file + + var totalPositive = []; + var totalNegative = []; + var positivesCount = []; + var negativesCount = []; + var totalNet = []; + var days = 0; + table = $('
    '+translate('Time')+''+translate('Readings')+''+translate('Average')+''+translate('Min')+''+translate('Quartile')+' 25'+translate('Median')+''+translate('Quartile')+' 75'+translate('Max')+''+translate('Standard Deviation')+'' + translate('Time') + '' + translate('Readings') + '' + translate('Average') + '' + translate('Min') + '' + translate('Quartile') + ' 25' + translate('Median') + '' + translate('Quartile') + ' 75' + translate('Max') + '' + translate('Standard Deviation') + '
    ' + display + '' + pivotedByHour[hour].length + ' (' + Math.floor(100 * pivotedByHour[hour].length / data.length) + '%)' + avg + '' + Math.min.apply(Math, pivotedByHour[hour].map(function(r) { return r.sgv; })) + '' + ((tmp=ss.quantile(pivotedByHour[hour].map(function(r) { return r.sgv; }), 0.25)) ? tmp.toFixed(1) : 0 ) + '' + ((tmp=ss.quantile(pivotedByHour[hour].map(function(r) { return r.sgv; }), 0.5)) ? tmp.toFixed(1) : 0 ) + '' + ((tmp=ss.quantile(pivotedByHour[hour].map(function(r) { return r.sgv; }), 0.75)) ? tmp.toFixed(1) : 0 ) + '' + Math.max.apply(Math, pivotedByHour[hour].map(function(r) { return r.sgv; })) + '' + Math.floor(dev*10)/10 + '' + Math.min.apply(Math, pivotedByHour[hour].map(function(r) { + return r.sgv; + })) + '' + ((tmp = ss.quantile(pivotedByHour[hour].map(function(r) { + return r.sgv; + }), 0.25)) ? tmp.toFixed(1) : 0) + '' + ((tmp = ss.quantile(pivotedByHour[hour].map(function(r) { + return r.sgv; + }), 0.5)) ? tmp.toFixed(1) : 0) + '' + ((tmp = ss.quantile(pivotedByHour[hour].map(function(r) { + return r.sgv; + }), 0.75)) ? tmp.toFixed(1) : 0) + '' + Math.max.apply(Math, pivotedByHour[hour].map(function(r) { + return r.sgv; + })) + '' + Math.floor(dev * 10) / 10 + '
    '); + thead = $(''); + ["", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23].forEach(function(hour) { + $('').appendTo(thead); + totalPositive[hour] = 0; + totalNegative[hour] = 0; + positivesCount[hour] = 0; + negativesCount[hour] = 0; + totalNet[hour] = 0; + }); + thead.appendTo(table); + + sorteddaystoshow.forEach(function(day) { + if (datastorage[day].netBasalPositive) { + days++; + var tr = $(''); + $('').appendTo(tr); + for (var h = 0; h < 24; h++) { + var positive = datastorage[day].netBasalPositive[h]; + var negative = datastorage[day].netBasalNegative[h]; + var net = positive + negative; + totalPositive[h] += positive; + totalNegative[h] += negative; + if (positive + negative > 0) positivesCount[h] += 1; + else if (positive + negative < 0) negativesCount[h] += 1; + totalNet[h] += net; + var color = Math.abs(net) < 0.019 ? "black" : (net < 0 ? "red" : "lightgreen"); + $('').appendTo(tr); + } + table.append(tr); + } + }); + if (days > 0) { + var tr = $(''); + $('').appendTo(tr); + for (var h = 0; h < 24; h++) { + var color = Math.abs(totalNet[h]) < 0.01 ? "white" : (totalNet[h] < 0 ? "red" : "lightgreen"); + $('').appendTo(tr); + } + table.append(tr); + } + + report.append('
    '); + report.append('

    ' + translate('netIOB stats') + '

    '); + report.append(translate('(temp basals must be rendered to display this report)')); + report.append('

    '); + report.append(table); +}; diff --git a/lib/report_plugins/index.js b/lib/report_plugins/index.js index 5c8c0356444..bc945d04f36 100644 --- a/lib/report_plugins/index.js +++ b/lib/report_plugins/index.js @@ -2,7 +2,7 @@ var _ = require('lodash'); -function init() { +function init(ctx) { var consts = { SCALE_LINEAR: 0 , SCALE_LOG: 1 @@ -10,14 +10,17 @@ function init() { , ORDER_NEWESTONTOP: 1 } , allPlugins = [ - require('./daytoday')() - , require('./dailystats')() - , require('./glucosedistribution')() - , require('./hourlystats')() - , require('./percentile')() - , require('./success')() - , require('./calibrations')() - , require('./treatments')() + require('./daytoday')(ctx) + , require('./weektoweek')(ctx) + , require('./dailystats')(ctx) + , require('./glucosedistribution')(ctx) + , require('./hourlystats')(ctx) + , require('./percentile')(ctx) + , require('./success')(ctx) + , require('./calibrations')(ctx) + , require('./treatments')(ctx) + , require('./profiles')(ctx) + , require('./loopalyzer')(ctx) ]; consts.scaleYFromSettings = function scaleYFromSettings (client) { @@ -38,8 +41,13 @@ function init() { plugins.consts = consts; - plugins.utils = require('./utils')(); - + plugins.utils = require('./utils')({ + //TODO: refactor so all this happens after init somehow + // until then use some defaults so things don't blow up + language: ctx.language + , settings: {} + }); + plugins.addHtmlFromPlugins = function addHtmlFromPlugins(client) { plugins.eachPlugin(function addHtml(p) { // add main plugin html @@ -68,4 +76,4 @@ function init() { } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/report_plugins/loopalyzer.js b/lib/report_plugins/loopalyzer.js new file mode 100644 index 00000000000..f954eb574fc --- /dev/null +++ b/lib/report_plugins/loopalyzer.js @@ -0,0 +1,1270 @@ +'use strict'; + +//var _ = require('lodash'); +var moment = window.moment; +//var times = require('../times'); +//var d3 = (global && global.d3) || require('d3'); + +var loopalyzer = { + name: 'loopalyzer' + , label: 'Loopalyzer' + , pluginType: 'report' +}; + +function init () { + return loopalyzer; +} + +module.exports = init; + +var laDebug = false; // If we should print console.logs +var laVersion = '2019-02-02 v6'; +var risingInterpolationGap = 6; // How large a gap in COB/IOB graph is allowed to be to be interpolated if end value is larger than start +var fallingInterpolationGap = 24; // And if less than start +var interpolationRatio = 1.25; // But do allow rising interpolation if gap larger than interpolationGap and end value is less than 10% larger than start + +loopalyzer.html = function html (client) { + var translate = client.translate; + var ret = ''; + ret += '

    Loopalyzer  

    '; + ret += '' + translate('The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.'); + ret += '

    ' + translate('Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.'); + ret += '
    ' + translate('In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.'); + ret += '

    ' + translate('Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.'); + ret += '

    ' + translate('Note that time shift is available only when viewing multiple days.'); + ret += '

    '; + ret += translate('To see this report, press SHOW while in this view'); + ret += '
    '; + ret += ''; + ret += ''; /* loopalyzer-button */ + ret += '
    '; + ret += '
    '; + ret += '
    '; + ret += '
    '; + ret += '
    '; + ret += '
    '; + ret += '
    '; + ret += '
    '; + ret += '
    '; + return ret; +}; + +loopalyzer.css = + '#loopalyzer-charts, #loopalyzer-profiles { padding: 20px; } ' + + '#loopalyzer-basal, #loopalyzer-bg, #loopalyzer-tempbasal, #loopalyzer-iob, #loopalyzer-cob, #loopalyzer-profiles {' + + ' width: 100%;' + + ' height: 100%;' + + '}' + + '#loopalyzer-profiles-table table { margin: 0 10px; border-collapse: collapse; border: 0px; }' + + '#loopalyzer-profiles-table td { vertical-align: top; }' + + '#loopalyzer-profiles-table td table { margin: 0 10px; border-collapse: collapse; border: 0px; }' + + '#loopalyzer-profiles-table td caption { text-align: left; font-weight: bold; }' + + '#loopalyzer-profiles-table td th { background-color: #4CAF50; color: white; }' + + '#loopalyzer-profiles-table td td { text-align: right; vertical-align: top; padding: 0 1px; }' + + '#loopalyzer-profiles-table td td td { padding: 1px 8px; }'; + +loopalyzer.prepareHtml = function loopalyzerPrepareHtml () { + // $('#loopalyzer-charts').append($('
    ' + hour + '
    ' + report_plugins.utils.localeDate(day) + '' + + '' + negative.toFixed(2) + '' + '
    ' + + '' + positive.toFixed(2) + '' + '
    ' + + '' + net.toFixed(2) + '' + + '
    ' + '' + translate('Average') + " " + days + " " + translate('days') + '' + '' + + '' + (totalNegative[h] / days).toFixed(2) + ' (' + negativesCount[h] + ')' + '' + '
    ' + + '' + (totalPositive[h] / days).toFixed(2) + ' (' + positivesCount[h] + ')' + '' + '
    ' + + '' + (totalNet[h] / days).toFixed(2) + '' + + '
    ')); +}; + +// loopalyzer.ss = require('simple-statistics'); + +// +// Functions to pull data from datastorage and prepare in bins +// +loopalyzer.getSGVs = function(datastorage, daysToShow) { + var data = datastorage.allstatsrecords; + var bins = loopalyzer.getEmptyBins(); + + // Loop thru the days to show, for each day find the matching SGVs and insert into the bins entry array + daysToShow.forEach(function(day) { + var entries = []; // Array with all SGVs for this day, we'll fill this and then insert into the bins later + for (let i = 0; i < 288; i++) entries.push(NaN); // Fill the array with NaNs so we have something in case we don't find an SGV + var fromDate = moment(day); + var toDate = moment(day); + fromDate.set({ 'hours': 0, 'minutes': 0, 'seconds': 0, 'milliseconds': 0 }); + toDate.set({ 'hours': 0, 'minutes': 5, 'seconds': 0, 'milliseconds': 0 }); // toDate is 5 mins ahead + for (let i = 0; i < 288; i++) { + var found = false; + data.some(function(record) { + var recDate = moment(record.displayTime); + if (!found && recDate.isAfter(fromDate) && recDate.isBefore(toDate)) { + entries[i] = record.sgv; + found = true; + } + return found; // Breaks .some loop if found is true + }) + fromDate.add(5, 'minutes'); + toDate.add(5, 'minutes'); + } + loopalyzer.addArrayToBins(bins, entries); + }); + return bins; +} + +loopalyzer.getBasals = function(datastorage, daysToShow, profile) { + var bins = loopalyzer.getEmptyBins(); + + daysToShow.forEach(function(day) { + var dayStart = moment(day).startOf('day'); + var dayEnd = moment(day).endOf('day'); + var basals = []; + for (var i = 0; i < 288; i++) basals.push(NaN); // Clear the basals by filling with NaNs + + var index = 0; + for (var dt = dayStart; dt < dayEnd; dt.add(5, 'minutes')) { + var basal = profile.getTempBasal(dt.toDate()); + if (basal) + basals[index++] = basal.basal; + } + if (laDebug) console.log('getBasals ' + day, basals); + loopalyzer.addArrayToBins(bins, basals); + }); + return bins; +} + +loopalyzer.getTempBasalDeltas = function(datastorage, daysToShow, profile) { + var bins = loopalyzer.getEmptyBins(); + + daysToShow.forEach(function(day) { + var dayStart = moment(day).startOf('day'); + var dayEnd = moment(day).endOf('day'); + var temps = []; + for (var i = 0; i < 288; i++) temps.push(NaN); // Clear the basals by filling with NaNs + + var index = 0; + for (var dt = dayStart; dt < dayEnd; dt.add(5, 'minutes')) { + var basal = profile.getTempBasal(dt.toDate()); + if (basal) + temps[index++] = basal.tempbasal - basal.basal; + } + if (laDebug) console.log('getTempBasalDeltas ' + day, temps); + loopalyzer.addArrayToBins(bins, temps); + }); + return bins; +} + +loopalyzer.getIOBs = function(datastorage, daysToShow, profile, client, treatments) { + var iobStatusAvailable = client.plugins('iob').isDeviceStatusAvailable(datastorage.devicestatus); + if (laDebug) console.log('getIOBs iobStatusAvailable=' + iobStatusAvailable); + + var bins = loopalyzer.getEmptyBins(); + + daysToShow.forEach(function(day) { + var dayStart = moment(day).startOf('day'); + var dayEnd = moment(day).endOf('day'); + var iobs = []; + if (iobStatusAvailable) { + // var dayStartMills = dayStart.milliseconds(); + for (var i = 0; i < 288; i++) iobs.push(NaN); // Clear the IOBs by filling with NaNs + var iobArray = client.plugins('iob').IOBDeviceStatusesInTimeRange(datastorage.devicestatus, dayStart.valueOf(), dayEnd.valueOf()); + if (laDebug) console.log('getIOBs iobArray', iobArray); + iobArray.forEach(function(entry) { + var index = Math.floor(moment(entry.mills).diff(dayStart, 'minutes') / 5); + iobs[index] = entry.iob; + }); + + if (daysToShow.length === 1) loopalyzer.fillNanWithTreatments(iobs, treatments); + + // Loop thru these entries and where no IOB has been found, interpolate between nearby to get a continuous array + var startIndex = 0 + , stopIndex = 0; + while (startIndex < iobs.length && isNaN(iobs[startIndex])) { + startIndex++; // Advance start to the first real number + } + if (startIndex < iobs.length) { + stopIndex = startIndex + 1; + while (stopIndex < iobs.length) { + while (stopIndex < iobs.length && isNaN(iobs[stopIndex])) { + stopIndex++; // Advance stop to the first real number after start + } + if (stopIndex < iobs.length) { + // Now we have real numbers at start and stop and NaNs in between + // Compute the y=k*x+m = (y2-y1)/(x2-x1)*x+y1 + // Only interpolate on decreasing or steady, or on increasing if the gap is less than interpolationGap + // if (stopIndex-startIndex= 0 && isNaN(array[start])) { + start--; + } + while (stop < array.length && isNaN(array[stop])) { + stop++; + } + // var gap = stop - start; + // if (isNaN(array[start]) || isNaN(array[stop]) || gap > interpolationGap || (gap < interpolationGap && array[start]= interpolationGap || array[start]==0)) ) { + var interpolate = (isNaN(array[start]) || isNaN(array[stop]) ? true : loopalyzer.canInterpolate(array, start, stop)); + if (!interpolate) { + array[index] = treatment.amount; + } + } + }) +} + +/* Returns true if we can interpolate between this start and end */ +loopalyzer.canInterpolate = function(array, start, stop) { + var interpolate = false; + if (array[stop] <= array[start] * interpolationRatio) { + // Falling + if (stop - start < fallingInterpolationGap) interpolate = true; + } else { + // Rising + if (stop - start < risingInterpolationGap && array[start] !== 0) interpolate = true; + } + return interpolate; +} + +/* Returns the carbs treatments array as [date, amount] */ +loopalyzer.getCarbTreatments = function(datastorage, daysToShow) { + var treatments = []; // Holds the treatments [date, amount] + var startDate = moment(daysToShow[0]); + var endDate = moment(daysToShow[daysToShow.length - 1]).add(1, 'days'); + + datastorage.treatments.filter(function(treatment) { return treatment.carbs && treatment.carbs > 0 }).forEach(function(treatment) { + if (moment(treatment.created_at).isBetween(startDate, endDate)) { + treatments.push({ date: treatment.created_at, amount: treatment.carbs }); + } + }) + if (laDebug) console.log('Carb treatments', treatments); + return treatments; +} + +/* Returns the insulin treatments array as [date, amount] */ +loopalyzer.getInsulinTreatments = function(datastorage, daysToShow) { + var treatments = []; // Holds the treatments [date, amount] + var startDate = moment(daysToShow[0]); + var endDate = moment(daysToShow[daysToShow.length - 1]).add(1, 'days'); + + datastorage.treatments.filter(function(treatment) { return treatment.insulin && treatment.insulin > 0 }).forEach(function(treatment) { + if (moment(treatment.created_at).isBetween(startDate, endDate)) { + treatments.push({ date: treatment.created_at, amount: treatment.insulin }); + } + }) + if (laDebug) console.log('Insulin treatments', treatments); + return treatments; +} + +// PREDICTIONS START +// +loopalyzer.getAllTreatmentTimestampsForADay = function(datastorage, day) { + var timestamps = []; + var dayStart = moment(day).startOf('day'); + var carbTreatments = loopalyzer.getCarbTreatments(datastorage, [day]); + var insulinTreatments = loopalyzer.getInsulinTreatments(datastorage, [day]); + carbTreatments.forEach(function(entry) { timestamps.push(entry.date) }); + insulinTreatments.forEach(function(entry) { timestamps.push(entry.date) }); + timestamps.sort(function(a, b) { return (a < b ? -1 : 1) }); + timestamps.splice(0, 0, dayStart.toDate()); // Insert a fake timestamp at midnight so we can show predictions during night + return timestamps; +} + +loopalyzer.getAllPredictionsForADay = function(datastorage, day) { + var predictions = []; + var dayStart = moment(day).startOf('day'); + for (var i = datastorage.devicestatus.length - 1; i >= 0; i--) { + if (datastorage.devicestatus[i].loop && datastorage.devicestatus[i].loop.predicted) { + var predicted = datastorage.devicestatus[i].loop.predicted; + if (moment(predicted.startDate).isSame(dayStart, 'day')) + predictions.push(datastorage.devicestatus[i].loop.predicted); + } else if (datastorage.devicestatus[i].openaps && datastorage.devicestatus[i].openaps.suggested && datastorage.devicestatus[i].openaps.suggested.predBGs) { + var entry = {}; + entry.startDate = datastorage.devicestatus[i].openaps.suggested.timestamp; + // For OpenAPS/AndroidAPS we fall back from COB if present, to UAM, then IOB + if (datastorage.devicestatus[i].openaps.suggested.predBGs.COB) { + entry.values = datastorage.devicestatus[i].openaps.suggested.predBGs.COB; + } else if (datastorage.devicestatus[i].openaps.suggested.predBGs.UAM) { + entry.values = datastorage.devicestatus[i].openaps.suggested.predBGs.UAM; + } else entry.values = datastorage.devicestatus[i].openaps.suggested.predBGs.IOB; + predictions.push(entry); + } + } + // Remove duplicates before we're done + var p = []; + predictions.forEach(function(prediction) { + if (p.length === 0 || prediction.startDate !== p[p.length - 1].startDate) + p.push(prediction); + }) + return p; +} + +/* Find the earliest new predicted instance that has a timestamp equal to or larger than timestamp */ +/* (so if we have bolused or eaten we want to find the prediction that Loop has estimated just after that) */ +/* Returns the index into the predictions array that is the predicted we are looking for */ +loopalyzer.findPredicted = function(predictions, timestamp, offset) { + var ts = moment(timestamp).add(offset, 'minutes'); + var predicted = null; + if (offset && offset < 0) { // If offset is negative, start searching from first prediction going forward + for (let i = 0; i < predictions.length; i++) { + if (predictions[i] && predictions[i].startDate && moment(predictions[i].startDate) <= ts) { + predicted = i; + } + } + } else { // If offset is positive or zero, start searching from last prediction going backward + for (let i = predictions.length - 1; i > 0; i--) { + if (predictions[i] && predictions[i].startDate && moment(predictions[i].startDate) >= ts) { + predicted = i; + } + } + } + return predicted; +} + +loopalyzer.getPredictions = function(datastorage, daysToShow, client) { + + if (!datastorage.devicestatus) + return []; + + var predictedOffset = 0; + var truncatePredictions = true; + + // Fill the bins array with the timestamp, one per 5 minutes + var bins = []; + var date = moment(); + date.set({ 'hours': 0, 'minutes': 0, 'seconds': 0, 'milliseconds': 0 }); + for (var i = 0; i < 288; i++) { + bins.push([date.toDate(), []]); + date.add(5, 'minutes'); + } + + daysToShow.forEach(function(day) { + var p = []; // Array with all prediction SGVs for this day, we'll fill this and then insert into the bins later + for (var i = 0; i < 288; i++) p.push(NaN); + var treatmentTimestamps = loopalyzer.getAllTreatmentTimestampsForADay(datastorage, day); + var predictions = loopalyzer.getAllPredictionsForADay(datastorage, day); + + if (predictions.length > 0 && treatmentTimestamps.length > 0) { + + // Iterate over all treatments, find the predictions for each and add them to the entries array p, aligned on timestamp + for (var treatmentsIndex = 0; treatmentsIndex < treatmentTimestamps.length; treatmentsIndex++) { + var timestamp = treatmentTimestamps[treatmentsIndex]; + var predictedIndex = loopalyzer.findPredicted(predictions, timestamp, predictedOffset); // Find predictions offset before or after timestamp + + if (predictedIndex != null) { + var entry = predictions[predictedIndex]; // Start entry + var d = moment(entry.startDate); + var end = moment(day).endOf('day'); // Default to stop and end of the day + if (truncatePredictions) { + if (predictedOffset >= 0) { + // But if we are looking forward we want to stop at the next treatment + if (treatmentsIndex < treatmentTimestamps.length - 1) { + end = moment(treatmentTimestamps[treatmentsIndex + 1]); + } + } else { + // And if we are looking backward then we want to stop at "this" treatment + end = moment(treatmentTimestamps[treatmentsIndex]); + } + } + for (var entryIndex in entry.values) { + if (!d.isAfter(end)) { + var dayStart = moment(d).startOf('day'); + var minutesAfterMidnight = moment(d).diff(dayStart, 'minutes'); + var index = Math.floor(minutesAfterMidnight / 5); + p[index] = client.utils.scaleMgdl(entry.values[entryIndex]); + d.add(5, 'minutes'); + } + } + } + } + } + for (let i = 0; i < 288; i++) { + bins[i][1].push(p[i]); + } + }) + return bins; +} +// +// PREDICTIONS ENDS + +// VARIOUS UTILITY FUNCTIONS // + +/* Create an empty bins array with date stamps for today */ +loopalyzer.getEmptyBins = function() { + var bins = []; + var todayStart = moment().startOf('day'); + var todayEnd = moment().endOf('day'); + for (var dt = todayStart; dt < todayEnd; dt.add(5, 'minutes')) { + bins.push([dt.toDate(), []]); + } + return bins; +} + +/* Takes an array of 288 values and adds to the bins */ +loopalyzer.addArrayToBins = function(bins, values) { + if (bins && bins.length === 288 && values && values.length === 288) { + values.forEach(function(value, index) { + bins[index][1].push(value); + }); + } else + console.log('addArrayToBins - array must have 288 items', values); +} + +/* Fill all NaNs in an array by interpolating between adjacent values */ +loopalyzer.interpolateArray = function(values, allowNegative) { + var startIndex = 0 + , stopIndex = 0 + , k = 0 + , m = 0; + + while (isNaN(values[startIndex])) { + startIndex++; // Advance start to the first real number + } + stopIndex = startIndex + 1; + while (stopIndex < values.length) { + while (stopIndex < values.length && isNaN(values[stopIndex])) { + stopIndex++; // Advance stop to the first real number after start + } + if (stopIndex < values.length) { + // Now we have real numbers at start and stop and NaNs in between + // Compute the y=k*x+m = (y2-y1)/(x2-x1)*x+y1 + // Only interpolate if decreasing or steady, newer on increasing + if (values[stopIndex] <= values[startIndex]) { + k = (values[stopIndex] - values[startIndex]) / (stopIndex - startIndex); + m = values[startIndex]; + } + for (var x = 0; x < (stopIndex - startIndex); x++) { + values[x + startIndex] = k * x + m; + if (!allowNegative && values[x + startIndex] < 0) { + values[x + startIndex] = 0; + } + } + startIndex = stopIndex; + stopIndex++; + } + } +} + +/* Compute min value in bins */ +loopalyzer.min = function(xBins) { + var min = xBins[0][1]; + for (var i = 0; i < xBins.length; i++) { + if (isNaN(min) || min === null) min = xBins[i][1]; + if (!isNaN(xBins[i][1]) && xBins[i][1] < min) min = xBins[i][1]; + } + return min; +} + +/* Compute max value in bins */ +loopalyzer.max = function(xBins) { + var max = xBins[0][1]; + for (var i = 0; i < xBins.length; i++) { + if (isNaN(max) || max === null) max = xBins[i][1]; + if (!isNaN(xBins[i][1]) && xBins[i][1] > max) max = xBins[i][1]; + } + return max; +} + +/* Compute avg value in bins */ +loopalyzer.avg = function(xBins) { + var out = []; + xBins.forEach(function(entry) { + var sum = 0; + var count = 0; + entry[1].forEach(function(value) { + if (value && !isNaN(value)) { + sum += value; + count++; + } + }) + var avg = sum / count; + out.push([entry[0], avg]); + }) + return out; +} + +// Timeshifts a bins array with subarrays for multiple days +loopalyzer.timeShiftBins = function(bins, timeShift) { + if (bins && bins.length > 0) { + timeShift.forEach(function(minutes, dayIndex) { + if (minutes !== 0) { + var tempBin = []; + bins.forEach(function() { + tempBin.push(NaN); // Fill tempBin with NaNs + }) + var minutesBy5 = Math.floor(minutes / 5); + if (minutesBy5 > 0) { + let count = 288 - minutesBy5; + // If minutes>0 it means we should shift forward in time + // Example: Shift by 15 mins = 3 buckets + // bin : 0 1 2 3 4 5 6 7 8 9 10 + // tempBin: NaN NaN NaN 0 1 2 3 4 5 6 7 + for (let i = 0; i < count; i++) { + tempBin[i + minutesBy5] = bins[i][1][dayIndex]; + } + } + if (minutesBy5 < 0) { + let count = 288 + minutesBy5; + // If minutes<0 it means we should shift backward in time + // Example: Shift by 15 mins = 3 buckets + // bin : 0 1 2 3 4 5 6 7 8 9 10 + // tempBin: 3 4 5 6 7 8 9 10 NaN NaN NaN + for (var i = 0; i < count; i++) { + tempBin[i] = bins[i - minutesBy5][1][dayIndex]; + } + } + // Put the shifted data back into original bins variable (pass by pointer) + for (let i = 0; i < 288; i++) { + bins[i][1][dayIndex] = tempBin[i]; + } + } + }); + } +} + +// Modifies the timestamp in the bin by timeShift minutes, for each day +loopalyzer.timeShiftSingleBin = function(bin, daysToShow, timeShift) { + if (bin && bin.length > 0) { + daysToShow.forEach(function(day, dayIndex) { + var minutesToAdd = timeShift[dayIndex]; + var date = moment(day); + bin.forEach(function(entry, entryIndex) { + var entryDate = moment(entry.date); + if (entryDate.isSame(date, 'day')) { + entryDate.add(minutesToAdd, 'minutes'); + bin[entryIndex].date = entryDate.toDate(); + } + }) + }) + } +} + +/* Returns true if the profile values in a is identical to values in b, false otherwise */ +loopalyzer.isSameProfileValues = function(a, b) { + // Because the order of the keys are random when stringifying we do our own custom stringify ourselves + var aString = ''; + var bString = ''; + if (a.basal) { + aString += 'basal:'; + a.basal.forEach(function(entry) { + aString += 's' + entry.timeAsSeconds + 't' + entry.time + 'v' + entry.value; + }) + } + if (a.carbratio) { + aString += 'carbratio:'; + a.carbratio.forEach(function(entry) { + aString += 's' + entry.timeAsSeconds + 't' + entry.time + 'v' + entry.value; + }) + } + if (a.sens) { + aString += 'sens:'; + a.sens.forEach(function(entry) { + aString += 's' + entry.timeAsSeconds + 't' + entry.time + 'v' + entry.value; + }) + } + if (b.basal) { + bString += 'basal:'; + b.basal.forEach(function(entry) { + bString += 's' + entry.timeAsSeconds + 't' + entry.time + 'v' + entry.value; + }) + } + if (b.carbratio) { + bString += 'carbratio:'; + b.carbratio.forEach(function(entry) { + bString += 's' + entry.timeAsSeconds + 't' + entry.time + 'v' + entry.value; + }) + } + if (b.sens) { + bString += 'sens:'; + b.sens.forEach(function(entry) { + bString += 's' + entry.timeAsSeconds + 't' + entry.time + 'v' + entry.value; + }) + } + return (aString == bString); +} + +loopalyzer.renderProfilesTable = function(datastoreProfiles, daysToShow, client) { + + // Loop thru the daysToShow and get the timestamp of the first day displayed + var beginningOfFirstDay = null; + var endOfLastDay = null; + daysToShow.forEach(function(day) { + var dayStart = moment(day).startOf('day'); + var dayEnd = moment(day).endOf('day'); + if (!beginningOfFirstDay || dayStart < beginningOfFirstDay) + beginningOfFirstDay = dayStart; + if (!endOfLastDay || dayEnd > endOfLastDay) + endOfLastDay = dayEnd; + }); + + // Now some profile juggling... We want to display only the profiles relevant to the days we are showing. + // This includes the last profile created before the first display date, and the profiles created on the display dates. + // However we don't want to show duplicate profiles and we also don't want to show more than just a few if there are many. + + // First, extract the profiles that have a startDate less than the endOfLastDay as only these are relevant, and sort + // these on ascending startDate (create a clone array so we don't modify the Store array). And only save the profiles + // that have basal, carbratio, or sens. + var profilesArray1 = []; + datastoreProfiles.forEach(function(entry) { + var newEntry = {}; + newEntry.startDate = entry.startDate; + var store = entry.store; + if (store) { + for (var key in store) { + if (laDebug) console.log('profile ' + key); + if (Object.prototype.hasOwnProperty.call(store, key)) { + var defaultProfile = store[key]; + newEntry.profileName = key; + if (defaultProfile.basal) newEntry.basal = defaultProfile.basal; + if (defaultProfile.carbratio) newEntry.carbratio = defaultProfile.carbratio; + if (defaultProfile.sens) newEntry.sens = defaultProfile.sens; + if ((newEntry.basal || newEntry.carbratio || newEntry.sens) && moment(entry.startDate).isBefore(endOfLastDay)) + profilesArray1.push(newEntry); + } + } + } + }) + profilesArray1.sort(function(a, b) { return (a.startDate > b.startDate ? 1 : -1) }); // Ascending + if (laDebug) { + profilesArray1.forEach(function(entry) { + console.log('profilesArray1 - ' + entry.startDate); + }) + } + if (laDebug) console.log('profilesArray1 has ' + profilesArray1.length + ' profiles'); + + // Second, the deduplication - remove all duplicates which have a later startDate but identical data + var profilesArray2 = []; + var profileToCompareWith = profilesArray1[0]; + profilesArray2.push(profileToCompareWith); // Push the first profile, which should always be included. + profilesArray1.forEach(function(entry) { + if (laDebug) { + console.log('Comparing ' + JSON.stringify(profileToCompareWith.startDate) + ' to ' + JSON.stringify(entry.startDate)); + console.log(profileToCompareWith, entry); + } + if (!loopalyzer.isSameProfileValues(profileToCompareWith, entry)) { + profilesArray2.push(entry); + profileToCompareWith = entry; + if (laDebug) + console.log('ADDING IT'); + } else { + // Do NOT push the entry to profilesArray2, and keep comparing with the same (olders unique) profile + if (laDebug) + console.log('SKIPPING IT'); + } + }) + if (laDebug) console.log('profilesArray2 has ' + profilesArray2.length + ' profiles'); + + // Sort the newest Profile first + profilesArray2.sort(function(a, b) { return (a.startDate > b.startDate ? 1 : -1) }); // Ascending + + // Third, find the latest profile with a startDate before beginningOfFirstDay + var latestProfile = profilesArray2[0]; // This is the oldest one + profilesArray2.forEach(function(entry) { + if (laDebug) + console.log(entry.startDate + ' isBefore ' + beginningOfFirstDay + ' = ' + moment(entry.startDate).isBefore(beginningOfFirstDay)); + if (moment(entry.startDate).isBefore(beginningOfFirstDay)) + latestProfile = entry; + }); + if (laDebug) console.log('latest profile is ' + latestProfile.startDate); + + // Now create a final array with the latest profile found above and add all + // the other profiles with a startDate between beginningOfFirstDay and endOfLastDay + var profiles = []; + profiles.push(latestProfile); // Add the latest one + profilesArray2.forEach(function(entry) { + if (laDebug) + console.log(entry.startDate + ' isAfter ' + beginningOfFirstDay + ' = ' + moment(entry.startDate).isAfter(beginningOfFirstDay)); + if (moment(entry.startDate).isAfter(beginningOfFirstDay)) + profiles.push(entry); // Add the profile if it's between beginning and end of show dates + }); + + // Now we have an array of all the profiles that are relevant for the days we are displaying. + if (laDebug) { + profiles.forEach(function(entry) { + console.log('profiles - ' + entry.startDate); + }) + } + if (laDebug) console.log('profiles has ' + profiles.length + ' profiles'); + + var translate = client.translate; + var tableHtml = ''; + + profiles.forEach(function(theProfile, index) { + + if (index < 3) { + tableHtml += ''; + + } else + if (index == 3) { + // Add ellipsis if too many profiles to display, but only one ellipsis even if there are more profiles + tableHtml += ''; + } + }); + + // Close the entire table + tableHtml += '
    '; + tableHtml += ''; + tableHtml += ''; + tableHtml += ''; + + // Add Basal as a table in the first td + tableHtml += ''; + + // Add Carb Ratio as a table in the second td + tableHtml += ''; + + // Add Sensitivity as a table in the third td + tableHtml += ''; + + // Close theProfile table + tableHtml += '
    ' + theProfile.profileName + ' (' + new Date(theProfile.startDate).toLocaleString() + ')
    ' + translate('Basal') + '' + translate('Carb ratio') + '' + translate('Sensitivity') + '
    '; + if (theProfile.basal) { + theProfile.basal.forEach(function(entry) { + tableHtml += '' + }); + } + tableHtml += '
    ' + entry.time + '' + parseFloat(entry.value).toFixed(3) + '
    '; + if (theProfile.carbratio) { + theProfile.carbratio.forEach(function(entry) { + tableHtml += '' + }); + } + tableHtml += '
    ' + entry.time + '' + parseFloat(entry.value).toFixed(1) + '
    '; + if (theProfile.sens) { + theProfile.sens.forEach(function(entry) { + tableHtml += '' + }); + } + tableHtml += '
    ' + entry.time + '' + parseFloat(entry.value).toFixed(1) + '
    .....
    '; + + // And add our HTML to the view + $("#loopalyzer-profiles").html(tableHtml); + +}; + +// Main method +loopalyzer.report = function(datastorage, sorteddaystoshow, options) { + if (laDebug) console.log('Loopalyzer ' + laVersion); + + // Copy the sorteddaystoshow into new array (clone) and re-sort ascending (so we don't mess with original array) + var daysToShow = []; + sorteddaystoshow.forEach(function(day) { daysToShow.push(day) }); + daysToShow.sort(function(a, b) { return (a < b ? -1 : 1) }); // We always want them chronological order + + var firstDay = moment(daysToShow[0]); + var lastDay = moment(daysToShow[daysToShow.length - 1]); + var days = lastDay.diff(firstDay, 'day') + 1; + if (laDebug) console.log('Loopalyzer ' + firstDay.format() + ' - ' + lastDay.format() + ' is ' + days + ' days'); + if (days <= 14) { + $("#loopalyzer-notenoughdata").hide(); + $("#loopalyzer-dateinfo").show(); + $("#loopalyzer-buttons").show(); + $("#loopalyzer-charts").show(); + $("#loopalyzer-profiles-table").show(); + $("#loopalyzer-help").hide(); + loopalyzer.generateReport(datastorage, daysToShow, options); + } else { + $("#loopalyzer-notenoughdata").show(); + $("#loopalyzer-dateinfo").hide(); + $("#loopalyzer-buttons").hide(); + $("#loopalyzer-charts").hide(); + $("#loopalyzer-profiles-table").hide(); + $("#loopalyzer-help").hide(); + } +} + +loopalyzer.generateReport = function(datastorage, daysToShow, options) { + var Nightscout = window.Nightscout; + var client = Nightscout.client; + var translate = client.translate; + var profile = client.sbx.data.profile; + // var report_plugins = Nightscout.report_plugins; + // var scaledTreatmentBG = report_plugins.utils.scaledTreatmentBG; + + var today = new Date(); + var todayJSON = { 'year': today.getFullYear(), 'month': today.getMonth(), 'date': today.getDate() }; + + var dateInfo = moment(daysToShow[0]).format('ddd MMM D'); // .split(',')[0]; + if (daysToShow.length > 1) dateInfo += ' - ' + moment(daysToShow[daysToShow.length - 1]).format('ddd MMM D'); // .split(',')[0]; + $("#loopalyzer-dateinfo").html(dateInfo); + + loopalyzer.prepareHtml(); + $("#loopalyzer-buttons").show(); + if (daysToShow.length == 1) { + // Disable and gray out timeShift if only a single day + $("#rp_loopalyzertimeshift").prop('checked', false); + $("#rp_loopalyzertimeshift").attr("disabled", true); + $("#rp_loopalyzermincarbs").attr("disabled", true); + $("#rp_loopalyzert1").attr("disabled", true); + $("#rp_loopalyzert2").attr("disabled", true); + $("#rp_loopalyzertimeshiftinput").css('color', 'gray'); + } else { + // Enable and turn the timeShift black if multiple days + $("#rp_loopalyzertimeshift").removeAttr("disabled"); + $("#rp_loopalyzermincarbs").removeAttr("disabled"); + $("#rp_loopalyzert1").removeAttr("disabled"); + $("#rp_loopalyzert2").removeAttr("disabled"); + $("#rp_loopalyzertimeshiftinput").css('color', 'black'); + } + // Check if there is data in the profiles and render the profiles table if there is + if ($("#rp_loopalyzerprofiles").is(":checked") && (datastorage.profiles && datastorage.profiles.length > 0)) { + $("#loopalyzer-profiles-table").show(); + loopalyzer.renderProfilesTable(datastorage.profiles, daysToShow, client); + } else + $("#loopalyzer-profiles-table").hide(); + + // Pull all necessary treatment information + profile.updateTreatments(datastorage.profileSwitchTreatments, datastorage.tempbasalTreatments, datastorage.combobolusTreatments); + + var carbTreatments = loopalyzer.getCarbTreatments(datastorage, daysToShow); + var insulinTreatments = loopalyzer.getInsulinTreatments(datastorage, daysToShow); + var sgvBin = loopalyzer.getSGVs(datastorage, daysToShow); + var basalsBin = loopalyzer.getBasals(datastorage, daysToShow, profile); + var tempBasalsBin = loopalyzer.getTempBasalDeltas(datastorage, daysToShow, profile); + var iobBin = loopalyzer.getIOBs(datastorage, daysToShow, profile, client, insulinTreatments); + var cobBin = loopalyzer.getCOBs(datastorage, daysToShow, profile, client, carbTreatments); + var predictionsBin = []; + + if ($("#rp_loopalyzerpredictions").is(":checked")) { + predictionsBin = loopalyzer.getPredictions(datastorage, daysToShow, client); + } + + // Prepare an array with the minutes to timeShift each day (0 as default since timeShift is off by default) + var timeShifts = []; + var firstCarbs = []; + var timeShiftStartTime = null; // If timeShifting this is the average time the meals were eaten + var timeShiftStopTime = null; // and this is the start + DIA according to profile + var doTimeShift = false; + daysToShow.forEach(function() { timeShifts.push(0); + firstCarbs.push(NaN) }); + + // Check to see if we are doing timeShift or not + if ($("#rp_loopalyzertimeshift").is(":checked") && daysToShow.length > 1) { + var mealMinCarbs = $("#rp_loopalyzermincarbs").val(); + var t1 = $("#rp_loopalyzert1").val(); + var t2 = $("#rp_loopalyzert2").val(); + + if (t2 > t1) { + var h1 = t1.split(':')[0]; + var m1 = t1.split(':')[1]; + var h2 = t2.split(':')[0]; + var m2 = t2.split(':')[1]; + + var timeShiftBegin = moment(); + timeShiftBegin.set({ 'hours': h1, 'minutes': m1, 'seconds': 0 }); + + var timeShiftEnd = moment(); + timeShiftEnd.set({ 'hours': h2, 'minutes': m2, 'seconds': 0 }); + + //Loop through the carb treatments and find the first meal each day + daysToShow.forEach(function(day, dayIndex) { + var timeShiftBegin = moment(day); + var timeShiftEnd = moment(day); + timeShiftBegin.set({ 'hours': h1, 'minutes': m1, 'seconds': 0 }); + timeShiftEnd.set({ 'hours': h2, 'minutes': m2, 'seconds': 0 }); + + var found = false; + carbTreatments.forEach(function(entry) { + if (!found && entry.amount >= mealMinCarbs) { + var date = moment(entry.date); + if ((date.isSame(timeShiftBegin, 'minute') || date.isAfter(timeShiftBegin, 'minute')) && + (date.isSame(timeShiftEnd, 'minute') || date.isBefore(timeShiftEnd, 'minute'))) { + var startOfDay = moment(entry.date); + startOfDay.set({ 'hours': 0, 'minutes': 0, 'seconds': 0 }); + var minutesAfterMidnight = date.diff(startOfDay, 'minutes'); + firstCarbs[dayIndex] = minutesAfterMidnight; + found = true; + doTimeShift = true; + } + } + }) + }) + + // Calculate the average starting time, in minutes after midnight + var sum = 0 + , count = 0; + + firstCarbs.forEach(function(minutesAfterMidnight) { + if (minutesAfterMidnight) { // Avoid NaN + sum += minutesAfterMidnight; + count++; + } + }); + + var averageMinutesAfterMidnight = Math.round(sum / count); + + var dia = profile.getDIA(); + if (!dia || dia <= 0) + dia = 6; // Default to 6h if DIA not set in profile + timeShiftStartTime = moment(todayJSON); + timeShiftStartTime.minutes(averageMinutesAfterMidnight); + timeShiftStopTime = moment(todayJSON); + if (averageMinutesAfterMidnight + dia * 60 < 24 * 60) + timeShiftStopTime.minutes(averageMinutesAfterMidnight + dia * 60); // If not beyond midnight, stop at end of DIA + else + timeShiftStopTime.minutes(24 * 60 - 1); // If beyond midnight, stop at midnight + + // Compute the timeShift (+ / -) that we should add to each entry (sgv, iob, carbs, etc) for each day + firstCarbs.forEach(function(minutesAfterMidnight, index) { + if (minutesAfterMidnight) { // Avoid NaN + var delta = Math.round(averageMinutesAfterMidnight - minutesAfterMidnight); + timeShifts[index] = delta; + } + }); + + if (doTimeShift) { + loopalyzer.timeShiftBins(sgvBin, timeShifts); + loopalyzer.timeShiftBins(basalsBin, timeShifts); + loopalyzer.timeShiftBins(tempBasalsBin, timeShifts); + loopalyzer.timeShiftBins(iobBin, timeShifts); + loopalyzer.timeShiftBins(cobBin, timeShifts); + loopalyzer.timeShiftBins(predictionsBin, timeShifts); + loopalyzer.timeShiftSingleBin(carbTreatments, daysToShow, timeShifts); + loopalyzer.timeShiftSingleBin(insulinTreatments, daysToShow, timeShifts); + } + } else { + console.log('Loopalyzer - Timeshift end must be later than beginning.'); + } + } + + // After timeShift code block, get the average values + var sgvAvg = loopalyzer.avg(sgvBin); + var basalsAvg = loopalyzer.avg(basalsBin); + var tempBasalsAvg = loopalyzer.avg(tempBasalsBin); + var iobAvg = loopalyzer.avg(iobBin); + var cobAvg = loopalyzer.avg(cobBin); + var predictionsAvg = loopalyzer.avg(predictionsBin); + + var high = options.targetHigh; + var low = options.targetLow; + + // Set up the charts basics + function tickFormatter (val, axis) { + if (val <= axis.min) { return ''; } + if (val >= axis.max) { return ''; } + return val + ''; + } + + var tickColor = '#DDDDDD'; + var basalColor = '#33A0FF'; + var glucoseColor = '#33AA33'; + var predictionsColor = '#8E1578'; + var glucoseRangeColor = '#D6FFD6'; + var insulinColor = '#FF7000'; + var carbColor = '#23D820'; + var timeShiftBackgroundColor = "#F3F3F3"; + var barWidth = (24 * 60 * 60 * 1000 / 288); + var borderWidth = 1; + var labelWidth = 25; + var xaxisCfg = { + mode: 'time' + , timezone: 'browser' + , timeformat: '%H:%M' + , tickColor: tickColor + , tickSize: [1, "hour"] + , font: { size: 0 } + }; + + var hiddenAxis = { + position: "right" + , show: true + , labelWidth: 10 + , tickColor: "#FFFFFF" + , font: { size: 0 } + } + + // For drawing the carbs and insulin treatments + var markings = []; + var markingColor = "#000000"; + + // Chart 1: Basal + markings = []; + if (doTimeShift) + markings.push({ xaxis: { from: timeShiftStartTime.toDate(), to: timeShiftStopTime.toDate() }, color: timeShiftBackgroundColor }); + var chartBasalData = [{ + data: basalsAvg + , label: translate('Basal profile') + , id: 'basals' + , color: basalColor + , points: { show: false } + , bars: { show: true, fill: true, barWidth: barWidth } + , yaxis: 1 + }]; + var chartBasalOptions = { + xaxis: xaxisCfg + , yaxes: [{ + tickColor: tickColor + , labelWidth: labelWidth + , tickFormatter: function(val, axis) { return tickFormatter(val, axis); } + } + , hiddenAxis] + , grid: { + borderWidth: borderWidth + , markings: markings + } + }; + $.plot('#loopalyzer-basal', chartBasalData, chartBasalOptions); + + // Chart 2: Blood glucose + markings = []; + if (doTimeShift) + markings.push({ xaxis: { from: timeShiftStartTime.toDate(), to: timeShiftStopTime.toDate() }, color: timeShiftBackgroundColor }); + markings.push({ yaxis: { from: low, to: high }, color: glucoseRangeColor }); + + var chartBGData = [{ + label: translate('Blood glucose') + , data: sgvAvg + , id: 'glucose' + , color: glucoseColor + , points: { show: false } + , lines: { show: true } + }]; + if (predictionsAvg && predictionsAvg.length > 0) { + chartBGData.push({ + label: translate('Predictions') + , data: predictionsAvg + , id: 'predictions' + , color: predictionsColor + , points: { show: true, fill: true, radius: 0.75, fillColor: predictionsColor } + , lines: { show: false } + }); + } + var chartBGOptions = { + xaxis: xaxisCfg + , yaxes: [{ + min: 0 + , max: options.units === 'mmol' ? 20 : 400 + , tickColor: tickColor + , labelWidth: labelWidth + , tickFormatter: function(val, axis) { return tickFormatter(val, axis); } + } + , hiddenAxis] + , grid: { + borderWidth: borderWidth + , markings: markings + } + }; + $.plot('#loopalyzer-bg', chartBGData, chartBGOptions); + + // Chart 3: Delta temp basals + markings = []; + if (doTimeShift) + markings.push({ xaxis: { from: timeShiftStartTime.toDate(), to: timeShiftStopTime.toDate() }, color: timeShiftBackgroundColor }); + markings.push({ yaxis: { from: 0, to: 0 }, color: insulinColor, lineWidth: 2 }); + + var chartTempBasalData = [{ + data: tempBasalsAvg + , label: translate('Temp basal delta') + , id: 'tempBasals' + , color: insulinColor + , points: { show: false } + , bars: { show: true, barWidth: barWidth } + }]; + var chartTempBasalOptions = { + xaxis: xaxisCfg + , yaxes: [{ + tickColor: tickColor + , labelWidth: labelWidth + , tickFormatter: function(val, axis) { return tickFormatter(val, axis); } + } + , hiddenAxis] + , grid: { + borderWidth: borderWidth + , markings: markings + } + }; + $.plot('#loopalyzer-tempbasal', chartTempBasalData, chartTempBasalOptions); + + // Chart 4: IOB + markings = []; + if (doTimeShift) + markings.push({ xaxis: { from: timeShiftStartTime.toDate(), to: timeShiftStopTime.toDate() }, color: timeShiftBackgroundColor }); + insulinTreatments.forEach(function(treatment) { + var startDate = moment(treatment.date); + var endDate = moment(treatment.date); + startDate.set(todayJSON); + endDate.set(todayJSON); + endDate.add(5, 'minutes'); + markings.push({ xaxis: { from: startDate.toDate(), to: endDate.toDate() }, yaxis: { from: 0, to: treatment.amount }, color: markingColor }); + }) + + var chartIOBData = [{ + data: iobAvg + , label: translate('IOB') + , id: 'iobs' + , color: insulinColor + , points: { show: false } + , bars: { show: true, fill: true, barWidth: barWidth } + }]; + var chartIOBOptions = { + xaxis: xaxisCfg + , yaxes: [{ + tickColor: tickColor + , labelWidth: labelWidth + , tickFormatter: function(val, axis) { return tickFormatter(val, axis); } + } + , hiddenAxis] + , grid: { + borderWidth: borderWidth + , markings: markings + } + }; + $.plot('#loopalyzer-iob', chartIOBData, chartIOBOptions); + + // Chart 5: COB + markings = []; + if (doTimeShift) + markings.push({ xaxis: { from: timeShiftStartTime.toDate(), to: timeShiftStopTime.toDate() }, color: timeShiftBackgroundColor }); + carbTreatments.forEach(function(treatment) { + var startDate = moment(treatment.date); + var endDate = moment(treatment.date); + startDate.set(todayJSON); + endDate.set(todayJSON); + endDate.add(5, 'minutes'); + markings.push({ xaxis: { from: startDate.toDate(), to: endDate.toDate() }, yaxis: { from: 0, to: treatment.amount }, color: markingColor }); + }) + delete xaxisCfg.font; // Remove the font config so HH:MM is shown on the last chart + + var chartCOBData = [{ + data: cobAvg + , label: translate('COB') + , id: 'cobs' + , color: carbColor + , points: { show: false } + , bars: { show: true, fil: true, barWidth: barWidth } + }]; + var chartCOBOptions = { + xaxis: xaxisCfg + , yaxes: [{ + tickColor: tickColor + , labelWidth: labelWidth + , tickFormatter: function(val, axis) { return tickFormatter(val, axis); } + } + , hiddenAxis] + , grid: { + borderWidth: borderWidth + , markings: markings + } + }; + $.plot('#loopalyzer-cob', chartCOBData, chartCOBOptions); + +}; diff --git a/lib/report_plugins/percentile.js b/lib/report_plugins/percentile.js index bc8d67ad8fa..9694fd51faf 100644 --- a/lib/report_plugins/percentile.js +++ b/lib/report_plugins/percentile.js @@ -15,11 +15,17 @@ module.exports = init; percentile.html = function html(client) { var translate = client.translate; var ret = - '

    ' + translate('Glucose Percentile report') + '

    ' - + '
    ' - + '
    ' - + '
    ' - ; + '

    ' + + translate('Glucose Percentile report') + + ' (' + + '' + + ')' + + '

    ' + + '
    ' + + '
    ' + + '
    ' + ; + return ret; }; @@ -36,16 +42,23 @@ percentile.report = function report_percentile(datastorage, sorteddaystoshow, op var translate = client.translate; var ss = require('simple-statistics'); - var minutewindow = 30; //minute-window should be a divisor of 60 - + var minutewindow = 30; //minute-window should be a divisor of 60 + var data = datastorage.allstatsrecords; - + var bins = []; var filterFunc = function withinWindow(record) { var recdate = new Date(record.displayTime); return recdate.getHours() === hour && recdate.getMinutes() >= minute && recdate.getMinutes() < minute + minutewindow; }; - + + var reportPlugins = Nightscout.report_plugins; + var firstDay = reportPlugins.utils.localeDate(sorteddaystoshow[sorteddaystoshow.length - 1]); + var lastDay = reportPlugins.utils.localeDate(sorteddaystoshow[0]); + var countDays = sorteddaystoshow.length; + + $('#percentile-days').text(countDays + ' ' + translate('days total') + ', ' + firstDay + ' - ' + lastDay); + for (var hour = 0; hour < 24; hour++) { for (var minute = 0; minute < 60; minute = minute + minutewindow) { var date = new Date(); diff --git a/lib/report_plugins/profiles.js b/lib/report_plugins/profiles.js new file mode 100644 index 00000000000..49c41d30aa9 --- /dev/null +++ b/lib/report_plugins/profiles.js @@ -0,0 +1,105 @@ +'use strict'; + +var profiles = { + name: 'profiles' + , label: 'Profiles' + , pluginType: 'report' +}; + +function init () { + return profiles; +} + +module.exports = init; + +profiles.html = function html (client) { + var translate = client.translate; + var ret = + '

    ' + translate('Profiles') + '

    ' + + '
    ' + translate('Database records') + ' ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + return ret; +}; + +profiles.css = + '#profiles-chart {' + + ' width: 100%;' + + ' height: 100%;' + + '}'; + +profiles.report = function report_profiles (datastorage) { + var Nightscout = window.Nightscout; + var client = Nightscout.client; + var translate = client.translate; + + var profileRecords = datastorage.profiles; + var databaseRecords = $('#profiles-databaserecords'); + + databaseRecords.empty(); + for (var r = 0; r < profileRecords.length; r++) { + databaseRecords.append(''); + } + databaseRecords.unbind().bind('change', recordChange); + + recordChange(); + + function recordChange (event) { + if ($('#profiles-databaserecords option').length < 1) + return; + var currentindex = databaseRecords.val(); + var currentrecord = profileRecords[currentindex]; + + var table = $(''); + var tr = $(''); + + $('#profiles-default').val(currentrecord.defaultProfile); + + Object.keys(currentrecord.store).forEach(key => { + tr.append(displayRecord(currentrecord.store[key], key)); + }); + + table.append(tr); + + $('#profiles-chart').empty().append(table); + + if (event) { + event.preventDefault(); + } + } + + function displayRecord (record, name) { + var td = $(' + + +
    '); + var table = $(''); + + table.append($('').append($('').append($('').append($('').append($('').append($('').append($('').append($('').append($('').append($(''); + table.append(''); table.append('' + quarters.filter(function(quarter) { return quarter.records.length > 0; }).map(function(quarter) { var INVERT = true; return '' + [ - quarter.starting.toLocaleDateString() + ' - ' + quarter.ending.toLocaleDateString(), - { - klass: lowComparison(quarter, averages, 'percentLow'), - text: Math.round(quarter.percentLow) + '%' - }, - { - klass: lowComparison(quarter, averages, 'percentInRange', INVERT), - text: Math.round(quarter.percentInRange) + '%' - }, - { - klass: lowComparison(quarter, averages, 'percentHigh'), - text: Math.round(quarter.percentHigh) + '%' - }, - { - klass: lowComparison(quarter, averages, 'standardDeviation'), - text: (quarter.standardDeviation > 10? Math.round(quarter.standardDeviation): quarter.standardDeviation.toFixed(1)) - }, - { - klass: lowQuartileEvaluation(quarter, averages), - text: quarter.lowerQuartile - }, - { - klass: lowComparison(quarter, averages, 'average'), - text: quarter.average.toFixed(1) - }, - { - klass: upperQuartileEvaluation(quarter, averages), - text: quarter.upperQuartile + quarter.starting.toLocaleDateString() + ' - ' + quarter.ending.toLocaleDateString() + , { + klass: lowComparison(quarter, averages, 'percentLow') + , text: Math.round(quarter.percentLow) + '%' + } + , { + klass: lowComparison(quarter, averages, 'percentInRange', INVERT) + , text: Math.round(quarter.percentInRange) + '%' + } + , { + klass: lowComparison(quarter, averages, 'percentHigh') + , text: Math.round(quarter.percentHigh) + '%' + } + , { + klass: lowComparison(quarter, averages, 'standardDeviation') + , text: (quarter.standardDeviation > 10 ? Math.round(quarter.standardDeviation) : quarter.standardDeviation.toFixed(1)) + } + , { + klass: lowQuartileEvaluation(quarter, averages) + , text: quarter.lowerQuartile + } + , { + klass: lowComparison(quarter, averages, 'average') + , text: quarter.average.toFixed(1) + } + , { + klass: upperQuartileEvaluation(quarter, averages) + , text: quarter.upperQuartile } ].map(function(v) { if (typeof v === 'object') { - return ''; + return ''; } else { return ''; } @@ -217,4 +211,4 @@ success.report = function report_success(datastorage, sorteddaystoshow, options) }).join('') + ''); table.appendTo(grid); -}; \ No newline at end of file +}; diff --git a/lib/report_plugins/treatments.js b/lib/report_plugins/treatments.js index c37768d0a8a..f07c84ad322 100644 --- a/lib/report_plugins/treatments.js +++ b/lib/report_plugins/treatments.js @@ -18,7 +18,8 @@ module.exports = init; treatments.html = function html(client) { var translate = client.translate; var ret = - '

    ' + translate('Treatments') + '

    ' + '

    ' + translate('Treatments') + '

    ' + + '' + translate('To see this report, press SHOW while in this view') + '' + '
    ' ; ret += @@ -43,6 +44,16 @@ treatments.html = function html(client) { + ' ' + ' ' + '
    ' + + ' ' + + '
    ' + + ' ' + + '
    ' + ' ').should.be.greaterThan(-1); //dailystats + result.indexOf('').should.be.greaterThan(-1); // distribution + result.indexOf('').should.be.greaterThan(-1); // hourlystats + result.indexOf('
    ').should.be.greaterThan(-1); //success + result.indexOf('CAL: Scale: 1.10 Intercept: 31102 Slope: 776.91').should.be.greaterThan(-1); //calibrations + result.indexOf('
    ').should.be.greaterThan(-1); //treatments done(); }); }); - after(function (done) { - benv.teardown(true); - done(); - }); + it ('should produce week to week report', function (done) { + var client = window.Nightscout.client; - it ('should produce some html', function (done) { - var plugins = require('../lib/plugins/')().registerClientDefaults(); - var client = require('../lib/client'); - - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; @@ -258,53 +454,44 @@ describe('reports', function ( ) { window.alert = function mockAlert () { return true; }; + + window.setTimeout = function mockSetTimeout (call, timer) { + if (timer == 60000) return; + call(); + }; - client.init(serverSettings, plugins); - client.dataUpdate(nowData); - - // Load profile, we need to operate in UTC - client.sbx.data.profile.loadData(exampleProfile); - - $('a.presetdates :first').click(); - $('#rp_notes').val('something'); - $('#rp_eventtype').val('BG Check'); - $('#rp_from').val('2015/08/08'); - $('#rp_to').val('2015/09/07'); - $('#rp_optionsraw').prop('checked',true); - $('#rp_optionsiob').prop('checked',true); - $('#rp_optionscob').prop('checked',true); - $('#rp_enableeventtype').click(); - $('#rp_enablenotes').click(); - $('#rp_enablefood').click(); - $('#rp_enablefood').click(); - $('#rp_log').prop('checked',true); - $('#rp_show').click(); - - $('#rp_linear').prop('checked',true); - $('#rp_show').click(); - $('#dailystats').click(); - - $('img.deleteTreatment:first').click(); - $('img.editTreatment:first').click(); - $('.ui-button:contains("Save")').click(); - - var result = $('body').html(); - //var filesys = require('fs'); - //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) - //logfile.write($('body').html()); - - //console.log(result); - - result.indexOf('Milk now').should.be.greaterThan(-1); // daytoday - result.indexOf('50 g (1.67U)').should.be.greaterThan(-1); // daytoday - result.indexOf('').should.be.greaterThan(-1); //dailystats - result.indexOf('td class="tdborder" style="background-color:#8f8">Normal: ').should.be.greaterThan(-1); // distribution - result.indexOf('').should.be.greaterThan(-1); // hourlystats - result.indexOf('
    ').should.be.greaterThan(-1); //success - result.indexOf('CAL: Scale: 1.10 Intercept: 31102 Slope: 776.91').should.be.greaterThan(-1); //calibrations - result.indexOf('
    ').should.be.greaterThan(-1); //treatments + client.init(function afterInit ( ) { + client.dataUpdate(nowData); + + console.log('Sending profile to client'); + + // Load profile, we need to operate in UTC + client.sbx.data.profile.loadData(exampleProfile); + + $('#weektoweek').addClass('selected'); + $('a.presetdates :first').click(); + $('#rp_from').val('2015-08-08'); + $('#rp_to').val('2015-09-07'); + $('#wrp_log').prop('checked', true); + $('#rp_show').click(); + + $('#wrp_linear').prop('checked', true); + $('#rp_show').click(); + $('.ui-button:contains("Save")').click(); + + var result = $('body').html(); + + result.indexOf(' { + let reportstorage, storage, mockStorage; + + beforeEach(() => { + reportstorage = require('../lib/report/reportstorage'); + storage = require('js-storage').localStorage; + mockStorage = require('./fixtures/localstorage'); + storage.get = mockStorage.get; + storage.set = mockStorage.set; + }); + + afterEach(() => { + delete require.cache[require.resolve('js-storage')]; + delete require.cache[require.resolve('./fixtures/localstorage')]; + delete require.cache[require.resolve('../lib/report/reportstorage')]; + }); + + it('reportstorage definition - returns saveProps and getValue function', () => { + reportstorage.should.not.be.undefined(); + (reportstorage.getValue instanceof Function).should.be.true(); + (reportstorage.saveProps instanceof Function).should.be.true(); + }); + + it('reportstorage.getValue returns default properties', () => { + let keyCount = 0; + for (const v in defaultValues) { + reportstorage.getValue(v).should.equal(defaultValues[v]); + keyCount++; + } + keyCount.should.equal(Object.keys(defaultValues).length); + }); + + it('reportstorage.saveProps sets property in localstorage', () => { + reportstorage.saveProps({insulin: false}); + should.exist(mockStorage.get('reportProperties')); + mockStorage.get('reportProperties').insulin.should.be.false(); + }); + + it('reportstorage.saveProps ignores property not tracked', () => { + reportstorage.saveProps({foo: 'bar'}); + should.exist(mockStorage.get('reportProperties')); + should.not.exist(mockStorage.get('reportProperties').foo); + }); +}); diff --git a/tests/sandbox.test.js b/tests/sandbox.test.js index b6c491d85a6..e6422843c02 100644 --- a/tests/sandbox.test.js +++ b/tests/sandbox.test.js @@ -6,22 +6,26 @@ describe('sandbox', function ( ) { var now = Date.now(); it('init on client', function (done) { - var clientSettings = { - units: 'mg/dl' - , thresholds:{ - bgHigh: 260 - , bgTargetTop: 180 - , bgTargetBottom: 80 - , bgLow: 55 + var ctx = { + settings: { + units: 'mg/dl' + , thresholds:{ + bgHigh: 260 + , bgTargetTop: 180 + , bgTargetBottom: 80 + , bgLow: 55 + } } + , pluginBase: {} }; + + ctx.language = require('../lib/language')(); - var pluginBase = {}; var data = {sgvs: [{mgdl: 100, mills: now}]}; - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + var sbx = sandbox.clientInit(ctx, Date.now(), data); - sbx.pluginBase.should.equal(pluginBase); + sbx.pluginBase.should.equal(ctx.pluginBase); sbx.data.should.equal(data); sbx.lastSGVMgdl().should.equal(100); @@ -29,10 +33,11 @@ describe('sandbox', function ( ) { }); function createServerSandbox() { - var env = require('../env')(); + var env = require('../lib/server/env')(); var ctx = {}; - ctx.data = require('../lib/data')(env, ctx); + ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); + ctx.language = require('../lib/language')(); return sandbox.serverInit(env, ctx); } diff --git a/tests/security.test.js b/tests/security.test.js index 7f692b6a31f..98088a04c18 100644 --- a/tests/security.test.js +++ b/tests/security.test.js @@ -1,61 +1,74 @@ 'use strict'; -var request = require('supertest'); -var should = require('should'); -var load = require('./fixtures/load'); - -describe('API_SECRET', function ( ) { - var api = require('../lib/api/'); +const request = require('supertest'); +const should = require('should'); +const language = require('../lib/language')(); +//const io = require('socket.io-client'); +describe('API_SECRET', function() { + var api; var scope = this; + var websocket; + var app; + var server; + var listener; + + this.timeout(7000); + + afterEach(function(done) { + if (listener) { + listener.close(done); + } + done(); + }); + + after(function(done) { + if (listener) { + listener.close(done); + } + done(); + }); + function setup_app (env, fn) { - require('../lib/bootevent')(env).boot(function booted (ctx) { - var wares = require('../lib/middleware/')(env); - ctx.app = api(env, wares, ctx); + api = require('../lib/api/'); + require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { + ctx.app = api(env, ctx); scope.app = ctx.app; scope.entries = ctx.entries; - ctx.entries.create(load('json'), function () { - fn(ctx); - }); + fn(ctx); }); } - /* - before(function (done) { - }); - */ - after(function (done) { - scope.entries( ).remove({ }, done); - }); + function setup_big_app (env, fn) { + api = require('../lib/api/'); + require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { + ctx.app = api(env, ctx); + scope.app = ctx.app; + scope.entries = ctx.entries; - it('should work fine absent', function (done) { - delete process.env.API_SECRET; - var env = require('../env')( ); - should.not.exist(env.api_secret); - setup_app(env, function (ctx) { + app = require('../lib/server/app')(env, ctx); + server = require('http').createServer(app); + listener = server.listen(1337, 'localhost'); + websocket = require('../lib/server/websocket')(env, ctx, server); - ctx.app.enabled('api').should.equal(false); - ping_status(ctx.app, again); - function again ( ) { - ping_authorized_endpoint(ctx.app, 404, done); - } + fn(ctx); }); - }); - + } - it('should work fail set unauthorized', function (done) { + it('should fail when unauthorized', function(done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; process.env.API_SECRET = 'this is my long pass phrase'; - var env = require('../env')( ); - env.api_secret.should.equal(known); - setup_app(env, function (ctx) { - // console.log(this.app.enabled('api')); + var env = require('../lib/server/env')(); + + env.enclave.isApiKey(known).should.equal(true); + + setup_app(env, function(ctx) { ctx.app.enabled('api').should.equal(true); - // ping_status(ctx.app, done); - // ping_authorized_endpoint(ctx.app, 200, done); ping_status(ctx.app, again); - function again ( ) { + + function again () { ctx.app.api_secret = ''; ping_authorized_endpoint(ctx.app, 401, done); } @@ -63,62 +76,103 @@ describe('API_SECRET', function ( ) { }); - - it('should work fine set', function (done) { + it('should work fine set', function(done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; process.env.API_SECRET = 'this is my long pass phrase'; - var env = require('../env')( ); - env.api_secret.should.equal(known); - setup_app(env, function (ctx) { - // console.log(this.app.enabled('api')); + var env = require('../lib/server/env')(); + env.enclave.isApiKey(known).should.equal(true); + setup_app(env, function(ctx) { ctx.app.enabled('api').should.equal(true); - // ping_status(ctx.app, done); - // ping_authorized_endpoint(ctx.app, 200, done); ping_status(ctx.app, again); - function again ( ) { - ctx.app.api_secret = env.api_secret; + + function again () { + ctx.app.api_secret = known; ping_authorized_endpoint(ctx.app, 200, done); } }); }); - it('should not work short', function ( ) { + it('should not work short', function() { delete process.env.API_SECRET; process.env.API_SECRET = 'tooshort'; - var env; - (function ( ) { - env = require('../env')( ); - }).should.throw( ); - should.not.exist(env); + var env = require('../lib/server/env')(); + should.not.exist(env.api_secret); + env.err[0].desc.should.startWith('API_SECRET should be at least'); }); function ping_status (app, fn) { - request(app) - .get('/status.json') - .expect(200) - .end(function (err, res) { - // console.log(res.body); - res.body.status.should.equal('ok'); - fn( ); - // console.log('err', err, 'res', res); - }); + request(app) + .get('/status.json') + .expect(200) + .end(function(err, res) { + res.body.status.should.equal('ok'); + fn(); + }); } function ping_authorized_endpoint (app, fails, fn) { - request(app) - .get('/experiments/test') - .set('api-secret', app.api_secret || '') - .expect(fails) - .end(function (err, res) { - if (fails < 400) { - res.body.status.should.equal('ok'); - } - fn( ); - // console.log('err', err, 'res', res); - }); + request(app) + .get('/experiments/test') + .set('api-secret', app.api_secret || '') + .expect(fails) + .end(function(err, res) { + if (fails < 400) { + res.body.status.should.equal('ok'); + } + fn(); + }); } -}); + /* + it('socket IO should connect', function(done) { + + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../lib/server/env')(); + + setup_big_app(env, function(ctx) { + + const socket2 = io.connect('ws://localhost:1337/'); + + socket2.on('connect', function() { + console.log('Socket 2 authorizing'); + socket2.emit("authorize", { + secret: known + }); + }); + + socket2.on('disconnect', function() { + //socket.emit("authorize"); + console.log('Client 2 disconnected'); + done(); + }); + + socket2.on('connected', function(msg) { + console.log('Connected'); + // Disconnect both client connections + socket2.disconnect(); + + const socket = io.connect('ws://localhost:1337/'); + + socket.on('connect', function() { + console.log('Socket 1 authorizing'); + socket.emit("authorize"); + }); + + socket.on('disconnect', function() { + //socket.emit("authorize"); + console.log('Client 1 disconnected'); + done(); + }); + + }); + + }); + + }); + */ + +}); diff --git a/tests/sensorage.test.js b/tests/sensorage.test.js new file mode 100644 index 00000000000..7b102827e3e --- /dev/null +++ b/tests/sensorage.test.js @@ -0,0 +1,174 @@ +'use strict'; + +var should = require('should'); +var times = require('../lib/times'); +const helper = require('./inithelper')(); + +describe('sage', function ( ) { + var env = require('../lib/server/env')(); + var ctx = helper.getctx(); + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + var sage = require('../lib/plugins/sensorage')(ctx); + var sandbox = require('../lib/sandbox')(); + + function prepareSandbox ( ) { + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + return sbx; + } + + it('set a pill to the current age since start with change', function (done) { + + var data = { + sensorTreatments: [ + {eventType: 'Sensor Change', notes: 'Foo', mills: Date.now() - times.days(2).msecs} + , {eventType: 'Sensor Start', notes: 'Bar', mills: Date.now() - times.days(1).msecs} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + + console.log(JSON.stringify(options)); + options.value.should.equal('1d0h'); + options.info[0].label.should.equal('Sensor Insert'); + options.info[1].should.match({ label: 'Duration', value: '2 days 0 hours' }); + options.info[2].should.match({ label: 'Notes', value: 'Foo' }); + options.info[3].label.should.equal('Sensor Start'); + options.info[4].should.match({ label: 'Duration', value: '1 days 0 hours' }); + options.info[5].should.match({ label: 'Notes', value: 'Bar' }); + done(); + } + } + }; + ctx.language = require('../lib/language')(); + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sage.setProperties(sbx); + sage.updateVisualisation(sbx); + + }); + + it('set a pill to the current age since start without change', function (done) { + + var data = { + sensorTreatments: [ + {eventType: 'Sensor Start', notes: 'Bar', mills: Date.now() - times.days(3).msecs} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('3d0h'); + options.info[0].label.should.equal('Sensor Start'); + options.info[1].should.match({ label: 'Duration', value: '3 days 0 hours' }); + options.info[2].should.match({ label: 'Notes', value: 'Bar' }); + done(); + } + } + }; + ctx.language = require('../lib/language')(); + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sage.setProperties(sbx); + sage.updateVisualisation(sbx); + + }); + + it('set a pill to the current age since change without start', function (done) { + + var data = { + sensorTreatments: [ + {eventType: 'Sensor Change', notes: 'Foo', mills: Date.now() - times.days(3).msecs} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('3d0h'); + options.info[0].label.should.equal('Sensor Insert'); + options.info[1].should.match({ label: 'Duration', value: '3 days 0 hours' }); + options.info[2].should.match({ label: 'Notes', value: 'Foo' }); + done(); + } + } + }; + ctx.language = require('../lib/language')(); + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sage.setProperties(sbx); + sage.updateVisualisation(sbx); + + }); + + it('set a pill to the current age since change after start', function (done) { + + var data = { + sensorTreatments: [ + {eventType: 'Sensor Start', notes: 'Bar', mills: Date.now() - times.days(10).msecs} + , {eventType: 'Sensor Change', notes: 'Foo', mills: Date.now() - times.days(3).msecs} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('3d0h'); + options.info.length.should.equal(3); + options.info[0].label.should.equal('Sensor Insert'); + options.info[1].should.match({ label: 'Duration', value: '3 days 0 hours' }); + options.info[2].should.match({ label: 'Notes', value: 'Foo' }); + done(); + } + } + }; + ctx.language = require('../lib/language')(); + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sage.setProperties(sbx); + sage.updateVisualisation(sbx); + + }); + + it('trigger an alarm when sensor is 6 days and 22 hours old', function (done) { + ctx.notifications.initRequests(); + + var before = Date.now() - times.days(6).msecs - times.hours(22).msecs; + + ctx.ddata.sensorTreatments = [{eventType: 'Sensor Start', mills: before}]; + + var sbx = prepareSandbox(); + sbx.extendedSettings = { 'enableAlerts': true }; + sage.setProperties(sbx); + sage.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('SAGE'); + highest.level.should.equal(ctx.levels.URGENT); + highest.title.should.equal('Sensor age 6 days 22 hours'); + done(); + }); + + it('not trigger an alarm when sensor is 6 days and 23 hours old', function (done) { + ctx.notifications.initRequests(); + + var before = Date.now() - times.days(6).msecs - times.hours(23).msecs; + + ctx.ddata.sensorTreatments = [{eventType: 'Sensor Start', mills: before}]; + + var sbx = prepareSandbox(); + sbx.extendedSettings = { 'enableAlerts': true }; + sage.setProperties(sbx); + sage.checkNotifications(sbx); + + should.not.exist(ctx.notifications.findHighestAlarm('SAGE')); + done(); + }); + +}); diff --git a/tests/settings.test.js b/tests/settings.test.js index 531e6944825..9c12b5bf483 100644 --- a/tests/settings.test.js +++ b/tests/settings.test.js @@ -28,7 +28,10 @@ describe('settings', function ( ) { settings.alarmTimeagoUrgent.should.equal(true); settings.alarmTimeagoUrgentMins.should.equal(30); settings.language.should.equal('en'); - settings.showPlugins.should.equal(''); + settings.showPlugins.should.equal('dbsize'); + settings.insecureUseHttp.should.equal(false); + settings.secureHstsHeader.should.equal(true); + settings.secureCsp.should.equal(false); }); it('support setting from env vars', function () { diff --git a/tests/simplealarms.test.js b/tests/simplealarms.test.js index 845bb1bd73a..eb72210da4d 100644 --- a/tests/simplealarms.test.js +++ b/tests/simplealarms.test.js @@ -2,14 +2,18 @@ var should = require('should'); var levels = require('../lib/levels'); describe('simplealarms', function ( ) { + var env = require('../lib/server/env')(); + var ctx = { + settings: {} + , language: require('../lib/language')() + , levels: levels + }; - var simplealarms = require('../lib/plugins/simplealarms')(); - var delta = require('../lib/plugins/delta')(); + var simplealarms = require('../lib/plugins/simplealarms')(ctx); - var env = require('../env')(); - var ctx = {}; - ctx.data = require('../lib/data')(env, ctx); + ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); + var bgnow = require('../lib/plugins/bgnow')(ctx); var now = Date.now(); var before = now - (5 * 60 * 1000); @@ -17,7 +21,7 @@ describe('simplealarms', function ( ) { it('Not trigger an alarm when in range', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); simplealarms.checkNotifications(sbx); @@ -28,10 +32,10 @@ describe('simplealarms', function ( ) { it('should trigger a warning when above target', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: before, mgdl: 171}, {mills: now, mgdl: 181}]; + ctx.ddata.sgvs = [{mills: before, mgdl: 171}, {mills: now, mgdl: 181}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); - delta.setProperties(sbx); + bgnow.setProperties(sbx); simplealarms.checkNotifications(sbx); var highest = ctx.notifications.findHighestAlarm(); highest.level.should.equal(levels.WARN); @@ -41,7 +45,7 @@ describe('simplealarms', function ( ) { it('should trigger a urgent alarm when really high', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: now, mgdl: 400}]; + ctx.ddata.sgvs = [{mills: now, mgdl: 400}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); simplealarms.checkNotifications(sbx); @@ -52,7 +56,7 @@ describe('simplealarms', function ( ) { it('should trigger a warning when below target', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: now, mgdl: 70}]; + ctx.ddata.sgvs = [{mills: now, mgdl: 70}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); simplealarms.checkNotifications(sbx); @@ -63,7 +67,7 @@ describe('simplealarms', function ( ) { it('should trigger a urgent alarm when really low', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: now, mgdl: 40}]; + ctx.ddata.sgvs = [{mills: now, mgdl: 40}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); simplealarms.checkNotifications(sbx); diff --git a/tests/storage.test.js b/tests/storage.test.js deleted file mode 100644 index 6f399746bdc..00000000000 --- a/tests/storage.test.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -var should = require('should'); -var assert = require('assert'); - -describe('STORAGE', function () { - var env = require('../env')(); - - before(function (done) { - delete env.api_secret; - done(); - }); - - it('The storage class should be OK.', function (done) { - should.exist(require('../lib/storage')); - done(); - }); - - it('After initializing the storage class it should re-use the open connection', function (done) { - var store = require('../lib/storage'); - store(env, function (err1, db1) { - should.not.exist(err1); - - store(env, function (err2, db2) { - should.not.exist(err2); - assert(db1.db, db2.db, 'Check if the handlers are the same.'); - - done(); - }); - }); - }); - - it('When no connection-string is given the storage-class should throw an error.', function (done) { - delete env.mongo; - should.not.exist(env.mongo); - - (function () { - return require('../lib/storage')(env, false, true); - }).should.throw('MongoDB connection string is missing'); - - done(); - }); - - it('An invalid connection-string should throw an error.', function (done) { - env.mongo = 'This is not a MongoDB connection-string'; - - (function () { - return require('../lib/storage')(env, false, true); - }).should.throw(Error); - - done(); - }); - -}); - diff --git a/tests/timeago.test.js b/tests/timeago.test.js new file mode 100644 index 00000000000..e7fc56904ec --- /dev/null +++ b/tests/timeago.test.js @@ -0,0 +1,131 @@ +var should = require('should'); +var levels = require('../lib/levels'); +var times = require('../lib/times'); + +describe('timeago', function() { + var ctx = {}; + ctx.levels = levels; + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + ctx.language = require('../lib/language')(); + ctx.settings = require('../lib/settings')(); + ctx.settings.heartbeat = 0.5; // short heartbeat to speedup tests + + var timeago = require('../lib/plugins/timeago')(ctx); + + var env = require('../lib/server/env')(); + + function freshSBX () { + //set extendedSettings right before calling withExtendedSettings, there's some strange test interference here + env.extendedSettings = { timeago: { enableAlerts: true } }; + var sbx = require('../lib/sandbox')().serverInit(env, ctx).withExtendedSettings(timeago); + return sbx; + } + + it('Not trigger an alarm when data is current', function(done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{ mills: Date.now(), mgdl: 100, type: 'sgv' }]; + + var sbx = freshSBX(); + timeago.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm('Time Ago')); + + done(); + }); + + it('Not trigger an alarm with future data', function(done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{ mills: Date.now() + times.mins(15).msecs, mgdl: 100, type: 'sgv' }]; + + var sbx = freshSBX(); + timeago.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm('Time Ago')); + + done(); + }); + + + it('should trigger a warning when data older than 15m', function(done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{ mills: Date.now() - times.mins(16).msecs, mgdl: 100, type: 'sgv' }]; + + var sbx = freshSBX(); + timeago.checkNotifications(sbx); + + var currentTime = new Date().getTime(); + + var highest = ctx.notifications.findHighestAlarm('Time Ago'); + highest.level.should.equal(levels.WARN); + highest.message.should.equal('Last received: 16 mins ago\nBG Now: 100 mg/dl'); + done(); + }); + + it('should trigger an urgent alarm when data older than 30m', function(done) { + ctx.notifications.initRequests(); + ctx.ddata.sgvs = [{ mills: Date.now() - times.mins(31).msecs, mgdl: 100, type: 'sgv' }]; + + var sbx = freshSBX(); + timeago.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm('Time Ago'); + highest.level.should.equal(levels.URGENT); + highest.message.should.equal('Last received: 31 mins ago\nBG Now: 100 mg/dl'); + done(); + }); + + it('calc timeago displays', function() { + var now = Date.now(); + + should.deepEqual( + timeago.calcDisplay({ mills: now + times.mins(15).msecs }, now) + , { label: 'in the future', shortLabel: 'future' } + ); + + //TODO: current behavior, we can do better + //just a little in the future, pretend it's ok + should.deepEqual( + timeago.calcDisplay({ mills: now + times.mins(4).msecs }, now) + , { value: 1, label: 'min ago', shortLabel: 'm' } + ); + + should.deepEqual( + timeago.calcDisplay(null, now) + , { label: 'time ago', shortLabel: 'ago' } + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now }, now) + , { value: 1, label: 'min ago', shortLabel: 'm' } + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - 1 }, now) + , { value: 1, label: 'min ago', shortLabel: 'm' } + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.sec(30).msecs }, now) + , { value: 1, label: 'min ago', shortLabel: 'm' } + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.mins(30).msecs }, now) + , { value: 30, label: 'mins ago', shortLabel: 'm' } + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.hours(5).msecs }, now) + , { value: 5, label: 'hours ago', shortLabel: 'h' } + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.days(5).msecs }, now) + , { value: 5, label: 'days ago', shortLabel: 'd' } + ); + + should.deepEqual( + timeago.calcDisplay({ mills: now - times.days(10).msecs }, now) + , { label: 'long ago', shortLabel: 'ago' } + ); + }); + +}); diff --git a/tests/treatmentnotify.test.js b/tests/treatmentnotify.test.js index 8803d87abd4..7507410aaeb 100644 --- a/tests/treatmentnotify.test.js +++ b/tests/treatmentnotify.test.js @@ -4,19 +4,21 @@ var levels = require('../lib/levels'); describe('treatmentnotify', function ( ) { - var treatmentnotify = require('../lib/plugins/treatmentnotify')(); - - var env = require('../env')(); + var env = require('../lib/server/env')(); var ctx = {}; - ctx.data = require('../lib/data')(env, ctx); + ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); + ctx.levels = levels; + ctx.language = require('../lib/language')().set('en'); + + var treatmentnotify = require('../lib/plugins/treatmentnotify')(ctx); var now = Date.now(); it('Request a snooze for a recent treatment and request an info notify', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: now, mgdl: 100}]; - ctx.data.treatments = [{eventType: 'BG Check', glucose: '100', mills: now}]; + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.treatments = [{eventType: 'BG Check', glucose: '100', mills: now}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); treatmentnotify.checkNotifications(sbx); @@ -30,8 +32,8 @@ describe('treatmentnotify', function ( ) { it('Not Request a snooze for an older treatment and not request an info notification', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: now, mgdl: 100}]; - ctx.data.treatments = [{mills: now - (15 * 60 * 1000)}]; + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.treatments = [{mills: now - (15 * 60 * 1000)}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); treatmentnotify.checkNotifications(sbx); @@ -45,8 +47,8 @@ describe('treatmentnotify', function ( ) { it('Request a snooze for a recent calibration and request an info notify', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: now, mgdl: 100}]; - ctx.data.mbgs = [{mgdl: '100', mills: now}]; + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.mbgs = [{mgdl: '100', mills: now}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); treatmentnotify.checkNotifications(sbx); @@ -60,8 +62,8 @@ describe('treatmentnotify', function ( ) { it('Not Request a snooze for an older calibration treatment and not request an info notification', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: now, mgdl: 100}]; - ctx.data.mbgs = [{mgdl: '100', mills: now - (15 * 60 * 1000)}]; + ctx.ddata.sgvs = [{mills: now, mgdl: 100}]; + ctx.ddata.mbgs = [{mgdl: '100', mills: now - (15 * 60 * 1000)}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); treatmentnotify.checkNotifications(sbx); @@ -75,7 +77,7 @@ describe('treatmentnotify', function ( ) { it('Request a notification for an announcement even there is an active snooze', function (done) { ctx.notifications.initRequests(); - ctx.data.treatments = [{mills: now, mgdl: 40, eventType: 'Announcement', isAnnouncement: true, notes: 'This not an alarm'}]; + ctx.ddata.treatments = [{mills: now, mgdl: 40, eventType: 'Announcement', isAnnouncement: true, notes: 'This not an alarm'}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); @@ -96,7 +98,7 @@ describe('treatmentnotify', function ( ) { announcement.title.should.equal('Urgent Announcement'); announcement.level.should.equal(levels.URGENT); announcement.pushoverSound.should.equal('persistent'); - should.deepEqual(ctx.notifications.findHighestAlarm(), announcement); + should.deepEqual(ctx.notifications.findHighestAlarm('Announcement'), announcement); ctx.notifications.snoozedBy(announcement).should.equal(false); @@ -105,7 +107,7 @@ describe('treatmentnotify', function ( ) { it('Request a notification for a non-error announcement', function (done) { ctx.notifications.initRequests(); - ctx.data.treatments = [{mills: now, mgdl: 100, eventType: 'Announcement', isAnnouncement: true, notes: 'This not an alarm'}]; + ctx.ddata.treatments = [{mills: now, mgdl: 100, eventType: 'Announcement', isAnnouncement: true, notes: 'This not an alarm'}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); diff --git a/tests/units.test.js b/tests/units.test.js index b6e8a9faa8f..2fbef0c4d3e 100644 --- a/tests/units.test.js +++ b/tests/units.test.js @@ -13,4 +13,20 @@ describe('units', function ( ) { units.mgdlToMMOL(180).should.equal('10.0'); }); + it('should convert 5.5 to 99', function () { + units.mmolToMgdl(5.5).should.equal(99); + }); + + it('should convert 10.0 to 180', function () { + units.mmolToMgdl(10.0).should.equal(180); + }); + + it('should convert 5.5 mmol and then convert back to 5.5 mmol', function () { + units.mgdlToMMOL(units.mmolToMgdl(5.5)).should.equal('5.5'); + }); + + it('should convert 99 mgdl and then convert back to 99 mgdl', function () { + units.mmolToMgdl(units.mgdlToMMOL(99)).should.equal(99); + }); + }); diff --git a/tests/upbat.test.js b/tests/upbat.test.js index 27f227f7f19..e81d11fa5ca 100644 --- a/tests/upbat.test.js +++ b/tests/upbat.test.js @@ -1,78 +1,133 @@ 'use strict'; require('should'); +const fs = require('fs'); + +var levels = require('../lib/levels'); describe('Uploader Battery', function ( ) { - var data = {uploaderBattery: 20}; - var clientSettings = {}; + var data = {devicestatus: [{mills: Date.now(), uploader: {battery: 20}}]}; it('display uploader battery status', function (done) { - var sandbox = require('../lib/sandbox')(); - var sbx = sandbox.clientInit(clientSettings, Date.now(), {}, data); + var ctx = { + settings: {} + , language: require('../lib/language')(fs) + }; + ctx.language.set('en'); + ctx.levels = levels; + var sandbox = require('../lib/sandbox')(ctx); + + var sbx = sandbox.clientInit(ctx, Date.now(), data); sbx.offerProperty = function mockedOfferProperty (name, setter) { name.should.equal('upbat'); var result = setter(); - result.value.should.equal(20); result.display.should.equal('20%'); result.status.should.equal('urgent'); - result.level.should.equal(25); + result.min.value.should.equal(20); + result.min.level.should.equal(25); done(); }; - var upbat = require('../lib/plugins/upbat')(); + var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); }); it('set a pill to the uploader battery status', function (done) { - var pluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.value.should.equal('20%'); - options.labelClass.should.equal('icon-battery-25'); - options.pillClass.should.equal('urgent'); - done(); + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('20%'); + options.labelClass.should.equal('icon-battery-25'); + options.pillClass.should.equal('urgent'); + done(); + } } + , language: require('../lib/language')(fs) + , levels: levels }; + ctx.language.set('en'); var sandbox = require('../lib/sandbox')(); - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); - var upbat = require('../lib/plugins/upbat')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); upbat.updateVisualisation(sbx); }); it('hide the pill if there is no uploader battery status', function (done) { - var pluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.hide.should.equal(true); - done(); + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.hide.should.equal(true); + done(); + } } + , language: require('../lib/language')(fs) + , levels: levels }; + ctx.language.set('en'); var sandbox = require('../lib/sandbox')(); - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, {}); - var upbat = require('../lib/plugins/upbat')(); + var sbx = sandbox.clientInit(ctx, Date.now(), {}); + var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); upbat.updateVisualisation(sbx); }); it('hide the pill if there is uploader battery status is -1', function (done) { - var pluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.hide.should.equal(true); - done(); - } + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.hide.should.equal(true); + done(); + } + }, language: require('../lib/language')(fs) + , levels: levels }; + ctx.language.set('en'); var sandbox = require('../lib/sandbox')(); - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, {uploaderBattery: -1}); - var upbat = require('../lib/plugins/upbat')(); + var sbx = sandbox.clientInit(ctx, Date.now(), {devicestatus: [{uploader: {battery: -1}}]}); + var upbat = require('../lib/plugins/upbat')(ctx); upbat.setProperties(sbx); upbat.updateVisualisation(sbx); }); + it('should handle virtAsst requests', function (done) { + + var ctx = { + settings: {} + , language: require('../lib/language')(fs) + , levels: levels + }; + ctx.language.set('en'); + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + var upbat = require('../lib/plugins/upbat')(ctx); + upbat.setProperties(sbx); + + upbat.virtAsst.intentHandlers.length.should.equal(2); + + upbat.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Uploader Battery'); + response.should.equal('Your uploader battery is at 20%'); + + upbat.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Uploader Battery'); + response.should.equal('Your uploader battery is at 20%'); + + done(); + }, [], sbx); + + }, [], sbx); + + }); }); diff --git a/tests/update-throttle.test.js b/tests/update-throttle.test.js deleted file mode 100644 index 384af352be6..00000000000 --- a/tests/update-throttle.test.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var request = require('supertest'); -require('should'); - -describe('Throttle', function ( ) { - var self = this; - - var api = require('../lib/api/'); - before(function (done) { - delete process.env.API_SECRET; - process.env.API_SECRET = 'this is my long pass phrase'; - self.env = require('../env')(); - this.wares = require('../lib/middleware/')(self.env); - self.app = require('express')(); - self.app.enable('api'); - require('../lib/bootevent')(self.env).boot(function booted(ctx) { - self.ctx = ctx; - self.app.use('/api', api(self.env, ctx)); - done(); - }); - }); - - after(function () { - delete process.env.API_SECRET; - }); - - it('only update once when there are multiple posts', function (done) { - - //if the data-loaded event is triggered more than once the test will fail - self.ctx.bus.on('data-loaded', function dataWasLoaded ( ) { - done(); - }); - - function post () { - request(self.app) - .post('/api/entries/') - .set('api-secret', self.env.api_secret || '') - .send({type: 'sgv', sgv: 100, date: Date.now()}) - .expect(200) - .end(function(err) { - if (err) { - done(err); - } - }); - } - - _.times(10, post); - }); - -}); diff --git a/tests/utils.test.js b/tests/utils.test.js index d107622070b..068e1ab2a45 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -2,23 +2,37 @@ require('should'); +const helper = require('./inithelper')(); + describe('utils', function ( ) { - var settings = { + + const ctx = helper.getctx(); + + ctx.settings = { alarmTimeagoUrgentMins: 30 , alarmTimeagoWarnMins: 15 }; - var utils = require('../lib/utils')(settings); + var utils = require('../lib/utils')(ctx); it('format numbers', function () { utils.toFixed(5.499999999).should.equal('5.50'); }); - it('show format recent times to 1 minute', function () { - var result = utils.timeAgo(Date.now() - 30000); - result.value.should.equal(1); - result.label.should.equal('min ago'); - result.status.should.equal('current'); + it('format numbers short', function () { + var undef; + utils.toRoundedStr(3.345, 2).should.equal('3.35'); + utils.toRoundedStr(5.499999999, 0).should.equal('5'); + utils.toRoundedStr(5.499999999, 1).should.equal('5.5'); + utils.toRoundedStr(5.499999999, 3).should.equal('5.5'); + utils.toRoundedStr(123.45, -2).should.equal('100'); + utils.toRoundedStr(-0.001, 2).should.equal('0'); + utils.toRoundedStr(-2.47, 1).should.equal('-2.5'); + utils.toRoundedStr(-2.44, 1).should.equal('-2.4'); + + utils.toRoundedStr(undef, 2).should.equal('0'); + utils.toRoundedStr(null, 2).should.equal('0'); + utils.toRoundedStr('text', 2).should.equal('0'); }); it('merge date and time', function () { diff --git a/tests/verifyauth.test.js b/tests/verifyauth.test.js index 0e635ec304c..7e97b11d61c 100644 --- a/tests/verifyauth.test.js +++ b/tests/verifyauth.test.js @@ -1,15 +1,18 @@ 'use strict'; var request = require('supertest'); +var language = require('../lib/language')(); +require('should'); describe('verifyauth', function ( ) { var api = require('../lib/api/'); + this.timeout(25000); + var scope = this; function setup_app (env, fn) { - require('../lib/bootevent')(env).boot(function booted (ctx) { - var wares = require('../lib/middleware/')(env); - ctx.app = api(env, wares, ctx); + require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { + ctx.app = api(env, ctx); scope.app = ctx.app; fn(ctx); }); @@ -19,27 +22,76 @@ describe('verifyauth', function ( ) { done(); }); - it('should fail unauthorized', function (done) { + it('should return defaults when called without secret', function (done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + var known512 = '8c8743d38cbe00debe4b3ba8d0ffbb85e4716c982a61bb9e57bab203178e3718b2965831c1a5e42b9da16f082fdf8a6cecf993b49ed67e3a8b1cd475885d8070'; delete process.env.API_SECRET; process.env.API_SECRET = 'this is my long pass phrase'; - var env = require('../env')( ); - env.api_secret.should.equal(known); + var env = require('../lib/server/env')( ); + env.enclave.isApiKey(known).should.equal(true); + env.enclave.isApiKey(known512).should.equal(true); setup_app(env, function (ctx) { ctx.app.enabled('api').should.equal(true); ctx.app.api_secret = ''; - ping_authorized_endpoint(ctx.app, 401, done); + ping_authorized_endpoint(ctx.app, 200, done); + }); + }); + + it('should fail when calling with wrong secret', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../lib/server/env')( ); + env.enclave.isApiKey(known).should.equal(true); + setup_app(env, function (ctx) { + ctx.app.enabled('api').should.equal(true); + ctx.app.api_secret = 'wrong secret'; + + function check(res) { + res.body.message.message.should.equal('UNAUTHORIZED'); + done(); + } + + ping_authorized_endpoint(ctx.app, 200, check, true); }); + }); + + + it('should fail unauthorized and delay subsequent attempts', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../lib/server/env')( ); + env.enclave.isApiKey(known).should.equal(true); + setup_app(env, function (ctx) { + ctx.app.enabled('api').should.equal(true); + ctx.app.api_secret = 'wrong secret'; + const time = Date.now(); + + function checkTimer(res) { + res.body.message.message.should.equal('UNAUTHORIZED'); + const delta = Date.now() - time; + delta.should.be.greaterThan(49); + done(); + } + function pingAgain (res) { + res.body.message.message.should.equal('UNAUTHORIZED'); + ping_authorized_endpoint(ctx.app, 200, checkTimer, true); + } + + ping_authorized_endpoint(ctx.app, 200, pingAgain, true); + }); }); + it('should work fine authorized', function (done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; process.env.API_SECRET = 'this is my long pass phrase'; - var env = require('../env')( ); - env.api_secret.should.equal(known); + var env = require('../lib/server/env')( ); + env.enclave.isApiKey(known).should.equal(true); setup_app(env, function (ctx) { ctx.app.enabled('api').should.equal(true); ctx.app.api_secret = env.api_secret; @@ -49,17 +101,14 @@ describe('verifyauth', function ( ) { }); - function ping_authorized_endpoint (app, fails, fn) { + function ping_authorized_endpoint (app, httpResponse, fn, passres) { request(app) .get('/verifyauth') .set('api-secret', app.api_secret || '') - .expect(fails) + .expect(httpResponse) .end(function (err, res) { - //console.log(res.body); - if (fails < 400) { - res.body.status.should.equal(200); - } - fn( ); + res.body.status.should.equal(httpResponse); + if (passres) { fn(res); } else { fn(); } // console.log('err', err, 'res', res); }); } diff --git a/translations/ar_SA.json b/translations/ar_SA.json new file mode 100644 index 00000000000..9f6a5432cb1 --- /dev/null +++ b/translations/ar_SA.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "يتم الاستماع على المَنفَذ", + "Mo": "اث", + "Tu": "ث", + "We": "أر", + "Th": "خ", + "Fr": "ج", + "Sa": "س", + "Su": "أح", + "Monday": "الأثنين", + "Tuesday": "الثلاثاء", + "Wednesday": "الأربعاء", + "Thursday": "الخميس", + "Friday": "الجمعة", + "Saturday": "السبت", + "Sunday": "الأحد", + "Category": "التصنيف", + "Subcategory": "التصنيفات الفرعية", + "Name": "الاسم", + "Today": "اليوم", + "Last 2 days": "آخر يومين", + "Last 3 days": "آخر 3 أيام", + "Last week": "الأسبوع الماضي", + "Last 2 weeks": "آخر أسبوعين", + "Last month": "الشهر الماضي", + "Last 3 months": "آخر ٣ أشهر", + "From": "مِن", + "To": "إلى", + "Notes": "الملاحظات", + "Food": "طعام", + "Insulin": "إنسولين", + "Carbs": "كربوهيدرات", + "Notes contain": "الملاحظات تحتوي على", + "Target BG range bottom": "الحد الأدنى للنطاق المستهدف لسكر الدم", + "top": "الأعلى", + "Show": "إظهار", + "Display": "عرض", + "Loading": "جاري التحميل", + "Loading profile": "تحميل الملف الشخصي", + "Loading status": "جارِ تحميل الحالة", + "Loading food database": "تحميل قاعدة بيانات الطعام", + "not displayed": "غير معروضة", + "Loading CGM data of": "تحميل بيانات CGM من", + "Loading treatments data of": "تحميل بيانات العلاج من", + "Processing data of": "جارِ معالجة بيانات", + "Portion": "حصة", + "Size": "الحجم", + "(none)": "(لا شيء)", + "None": "لا شيء", + "": "<لا شيء>", + "Result is empty": "النتيجة فارغة", + "Day to day": "يوما بعد يوم", + "Week to week": "أسبوع بعد أسبوع", + "Daily Stats": "الإحصائيات اليومية", + "Percentile Chart": "مخطط النسبة المئوية", + "Distribution": "التوزيع", + "Hourly stats": "إحصائيات الساعة", + "netIOB stats": "إحصائيات netIOB", + "temp basals must be rendered to display this report": "يجب توفير أساسيات الدرجة لعرض هذا التقرير", + "No data available": "لا يوجد بيانات متاحة", + "Low": "الحد الادني", + "In Range": "في النطاق", + "Period": "مدة", + "High": "مرتفع", + "Average": "المتوسط", + "Low Quartile": "الحد الأدنى", + "Upper Quartile": "الحد الأقصى", + "Quartile": "الربعي", + "Date": "التاريخ", + "Normal": "طبيعي", + "Median": "متوسط", + "Readings": "القرائات", + "StDev": "الانحراف المعياري", + "Daily stats report": "تقرير الحالة اليومي", + "Glucose Percentile report": "تقرير نسب السكر", + "Glucose distribution": "توزيع السكر", + "days total": "إجمالي أيام", + "Total per day": "المجموع اليومي", + "Overall": "الأداء العام", + "Range": "المدى", + "% of Readings": "% من القراءات", + "# of Readings": "# من القراءات", + "Mean": "‮المتوسط", + "Standard Deviation": "انحراف معياري", + "Max": "الحد الأقصى", + "Min": "الحد الأدنى", + "A1c estimation*": "تقدير الهيموجلوبين السكري A1c", + "Weekly Success": "التتابع الأسبوعي", + "There is not sufficient data to run this report. Select more days.": "لا توجد بيانات كافية لإدارة هذا التقرير، يرجى اختيار أيامًا إضافية", + "Using stored API secret hash": "استخدام تجزئة كلمة المرور لواجهة برمجة التطبيقات المخزنة", + "No API secret hash stored yet. You need to enter API secret.": "لم يتم تخزين تجزئة كلمة المرور لواجهة برمجة التطبيقات حتى الآن، تحتاج إلى إدخال كلمة مرور واجهة برمجة التطبيقات.", + "Database loaded": "قاعدة البيانات المحملة", + "Error: Database failed to load": "خطأ: فشل تحميل قاعدة البيانات", + "Error": "خطأ", + "Create new record": "إنشاء سجل حديد", + "Save record": "حفظ السجل", + "Portions": "قطاعات", + "Unit": "وحدة", + "GI": "مؤشر نسبة السكر في الدم", + "Edit record": "تحرير تسجيل", + "Delete record": "حذف السجل", + "Move to the top": "انتقل للأعلى", + "Hidden": "مخفي", + "Hide after use": "إخفاء بعد الاستخدام", + "Your API secret must be at least 12 characters long": "يجب ألا يقل طول كلمة مرور واجهة برمجة التطبيقات عن 12 حرفًا", + "Bad API secret": "كلمة مرور واجهة برمجة تطبيقات تالفة", + "API secret hash stored": "تم تخزين كلمة مرور واجهة برمجة التطبيقات مكونة من مزيج", + "Status": "الحالة", + "Not loaded": "لم يتم التحميل", + "Food Editor": "محرر الغذاء", + "Your database": "قاعدة البيانات الخاصة بك", + "Filter": "منقي", + "Save": "حفظ", + "Clear": "مسح", + "Record": "تسجيل", + "Quick picks": "اختيارات سريعة", + "Show hidden": "إظهار المخفي", + "Your API secret or token": "كلمة السر", + "Remember this device. (Do not enable this on public computers.)": "تذكر هذا الجهاز. (لا تقم بتمكين هذا على أجهزة الكمبيوتر العامة.)", + "Treatments": "العلاجات", + "Time": "الوقت", + "Event Type": "نوع الحدث", + "Blood Glucose": "مستوى السكر بالدم", + "Entered By": "تم إدخاله بواسطة", + "Delete this treatment?": "حذف هذه المعالجة؟", + "Carbs Given": "الكاربوهيدرات المعطاه", + "Insulin Given": "الإنسولين المُعطى", + "Event Time": "وقت الحدث", + "Please verify that the data entered is correct": "الرجاء التحقق من صحة البيانات التي تم إدخالها", + "BG": "سكر الدم", + "Use BG correction in calculation": "استخدم تصحيح سكر الدم في الحساب", + "BG from CGM (autoupdated)": "نسبة السكر في الدم من المراقبة المستمرة للسكر (التحديث التلقائي)", + "BG from meter": "نسبة سكر الدم من العداد", + "Manual BG": "سكر الدم يدويًا", + "Quickpick": "اختيار سريع", + "or": "أو", + "Add from database": "أضف من قاعدة البيانات", + "Use carbs correction in calculation": "استخدم تصحيح الكربوهيدرات في الحساب", + "Use COB correction in calculation": "استخدم تصحيح الكربوهيدرات النشط في الحساب", + "Use IOB in calculation": "استخدم الإنسولين النشط في الحساب", + "Other correction": "تصحيحات أخرى", + "Rounding": "تقريبًا", + "Enter insulin correction in treatment": "ادخل تصحيح الإنسولين في المعالجة", + "Insulin needed": "الإنسولين المطلوب", + "Carbs needed": "كمية الكاربوهيدرات المطلوبة", + "Carbs needed if Insulin total is negative value": "الكربوهيدرات اللازمة إذا كان إجمالي الأنسولين قيمة سالبة", + "Basal rate": "المعدل الأساسي", + "60 minutes earlier": "منذ 60 دقيقة", + "45 minutes earlier": "منذ 45 دقيقة", + "30 minutes earlier": "منذ 30 دقيقة", + "20 minutes earlier": "منذ 20 دقيقة", + "15 minutes earlier": "منذ 15 دقيقة", + "Time in minutes": "الوقت بالدقائق", + "15 minutes later": "بعد 15 دقيقة", + "20 minutes later": "بعد 20 دقيقة", + "30 minutes later": "بعد 30 دقيقة", + "45 minutes later": "بعد 45 دقيقة", + "60 minutes later": "بعد 60 دقيقة", + "Additional Notes, Comments": "ملاحظات وتعليقات إضافية", + "RETRO MODE": "وضع العودة", + "Now": "الآن", + "Other": "أخرى", + "Submit Form": "تقديم النموذج", + "Profile Editor": "محرر الملف الشخصي", + "Reports": "التقارير", + "Add food from your database": "أضف الطعام من قاعدة بياناتك", + "Reload database": "إعادة تحميل قاعدة البيانات", + "Add": "إضافة", + "Unauthorized": "غير مصرح به", + "Entering record failed": "فشل دخول السجل", + "Device authenticated": "مصادقة الجهاز", + "Device not authenticated": "لم تتم مصادقة الجهاز", + "Authentication status": "حالة المصادقة", + "Authenticate": "المصادقة", + "Remove": "إزالة", + "Your device is not authenticated yet": "لم تتم مصادقة جهازك بعد", + "Sensor": "مستشعر", + "Finger": "إصبع", + "Manual": "دليل المستخدم", + "Scale": "مقياس", + "Linear": "خطي", + "Logarithmic": "لوغاريتمي", + "Logarithmic (Dynamic)": "لوغاريتمي (ديناميكي)", + "Insulin-on-Board": "الإنسولين النشط", + "Carbs-on-Board": "الكربوهيدرات النشط", + "Bolus Wizard Preview": "عرض الجرعة المعالجة", + "Value Loaded": "تم تحميل القيمة", + "Cannula Age": "عمر الكانيولا", + "Basal Profile": "الملف الشخصي الأساسي", + "Silence for 30 minutes": "صمت لمدة 30 دقيقة", + "Silence for 60 minutes": "صمت لمدة 60 دقيقة", + "Silence for 90 minutes": "صمت لمدة 90 دقيقة", + "Silence for 120 minutes": "صمت لمدة 120 دقيقة", + "Settings": "إعدادات", + "Units": "الوحدات", + "Date format": "صيغة التاريخ", + "12 hours": "12 ساعة", + "24 hours": "24 ساعة", + "Log a Treatment": "سجل العلاج", + "BG Check": "فحص نسبة السكر في الدم", + "Meal Bolus": "جرعة الوجبة", + "Snack Bolus": "جرعة وجبة خفيفة", + "Correction Bolus": "جرعة تصحيحية", + "Carb Correction": "تصحيح الكربوهيدرات", + "Note": "ملاحظة", + "Question": "سؤال", + "Exercise": "تمرين", + "Pump Site Change": "تغيير موقع المضخة", + "CGM Sensor Start": "بدء تشغيل حساس المراقبة المستمرة للسكر", + "CGM Sensor Stop": "إيقاف تشغيل حساس المراقبة المستمرة للسكر", + "CGM Sensor Insert": "إدخال حساس المراقبة المستمرة للسكر", + "Sensor Code": "رمز الحساس", + "Transmitter ID": "معرف المرسل", + "Dexcom Sensor Start": "تشغيل مستشعر ديكسكوم", + "Dexcom Sensor Change": "تغيير مستشعر ديكسكوم", + "Insulin Cartridge Change": "تغيير خرطوشة الأنسولين", + "D.A.D. Alert": "تنبيه الكلب المنبه لمرض السكري", + "Glucose Reading": "قراءة السكر", + "Measurement Method": "طريقة القياس", + "Meter": "متر", + "Amount in grams": "الكمية بالجرامات", + "Amount in units": "الكمية بالوحدات", + "View all treatments": "عرض جميع العلاجات", + "Enable Alarms": "تمكين التنبيهات", + "Pump Battery Change": "تغيير بطارية المضخة", + "Pump Battery Age": "عمر بطارية المضخة", + "Pump Battery Low Alarm": "تنبيه انخفاض بطارية المضخة", + "Pump Battery change overdue!": "تأخر تغيير بطارية المضخة!", + "When enabled an alarm may sound.": "عند التفعيل قد ينطلق صوت المنبه.", + "Urgent High Alarm": "تنبيه عالي عاجل", + "High Alarm": "تنبيه ارتفاع", + "Low Alarm": "تنبيه انخفاض", + "Urgent Low Alarm": "تنبيه منخفض عاجل", + "Stale Data: Warn": "بيانات تالفة: تحذير", + "Stale Data: Urgent": "بيانات تالفة: عاجل", + "mins": "دقائق", + "Night Mode": "الوضع الليلي", + "When enabled the page will be dimmed from 10pm - 6am.": "أثناء التفعيل تكون الشاشة خافتة من 10 مساءًا الي 6 صباحًا.", + "Enable": "تمكين", + "Show Raw BG Data": "إظهار البيانات الخام لسكر الدم", + "Never": "أبدا", + "Always": "دائمًا", + "When there is noise": "عند وجود ضوضاء", + "When enabled small white dots will be displayed for raw BG data": "أثناء التفعيل سوف تُعرض نقاط بيضاء صغيرة للبيانات الخام لسكر الدم", + "Custom Title": "عنوان مخصص", + "Theme": "المظهر", + "Default": "الافتراضي", + "Colors": "الألوان", + "Colorblind-friendly colors": "ألوان متوافقة لعمي الألوان", + "Reset, and use defaults": "إعادة تعيين واستخدام الافتراضي", + "Calibrations": "المعايرة", + "Alarm Test / Smartphone Enable": "اختبار المنبه/ تفعيل الهاتف الذكي", + "Bolus Wizard": "معالج الجرعة", + "in the future": "في المستقبل", + "time ago": "منذ وقت مضى", + "hr ago": "منذ ساعة", + "hrs ago": "منذ ساعات", + "min ago": "منذ دقيقة", + "mins ago": "منذ دقائق", + "day ago": "منذ يوم", + "days ago": "منذ أيام", + "long ago": "منذ زمن طويل", + "Clean": "نظيف", + "Light": "خفيف", + "Medium": "متوسط", + "Heavy": "كثيف", + "Treatment type": "نوع العلاج", + "Raw BG": "BG خام", + "Device": "الجهاز", + "Noise": "الضوضاء", + "Calibration": "المعايرة", + "Show Plugins": "إظهار الإضافات", + "About": "عن", + "Value in": "القيمة ب", + "Carb Time": "وقت الكربوهيدرات", + "Language": "اللغة", + "Add new": "إضافة جديد", + "g": "جرام", + "ml": "مل", + "pcs": "قطع", + "Drag&drop food here": "اسحب واسقط الطعام هنا", + "Care Portal": "بوابة الرعاية", + "Medium/Unknown": "متوسط/ غير معروف", + "IN THE FUTURE": "في المستقبل", + "Order": "ترتيب", + "oldest on top": "الأقدم في الأعلى", + "newest on top": "الأحدث في الأعلى", + "All sensor events": "جميع أحداث المستشعر", + "Remove future items from mongo database": "إزالة العناصر المستقبلية من قاعدة مونغو دي بي", + "Find and remove treatments in the future": "إيجاد وإزالة العلاجات في المستقبل", + "This task find and remove treatments in the future.": "هذه المهمة تقوم بإيجاد وإزالة العلاجات في المستقبل.", + "Remove treatments in the future": "إزالة العلاجات في المستقبل", + "Find and remove entries in the future": "إيجاد وإزالة الإدخالات في المستقبل", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "هذه المهمة تقوم بإيجاد وإزالة بيانات مراقبة السكر المستمرة في المستقبل التي أنشأها الرافع بتاريخ/ توقيت خاطئين.", + "Remove entries in the future": "إزالة الإدخالات في المستقبل", + "Loading database ...": "جار تحميل قاعدة البيانات ...", + "Database contains %1 future records": "تحتوي قاعدة البيانات علي %1 من سجلات المستقبل", + "Remove %1 selected records?": "هل ترغب بإزالة السجلات %1 المحددة؟", + "Error loading database": "خطأ بتحميل قاعدة البيانات", + "Record %1 removed ...": "تم إزالة السجل %1 ...", + "Error removing record %1": "خطأ في إزالة السجل %1", + "Deleting records ...": "حذف السجلات ...", + "%1 records deleted": "1% من السجلات تم حذفها", + "Clean Mongo status database": "تنظيف حالة مونغو دي بي", + "Delete all documents from devicestatus collection": "جذف جميع المستندات من مجموعة حالة الجهاز", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "تزيل هذه المهمة كافة المستندات من مجموعة حالة الجهاز. مفيد عندما لا تكون حالة بطارية الرافع مُحدثة بطريقة صحيح.", + "Delete all documents": "إحذف كل السجلات", + "Delete all documents from devicestatus collection?": "حذف جميع الوثائق من مجموعة حالة الجهاز؟", + "Database contains %1 records": "تحتوي قاعدة البيانات علي سجلات %1", + "All records removed ...": "تم إزالة جميع السجلات ...", + "Delete all documents from devicestatus collection older than 30 days": "حذف جميع المستندات من مجموعة حالة الجهاز التي مضى عليها أكثر من 30 يومًا", + "Number of Days to Keep:": "عدد الأيام للإبقاء السجلات:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "تزيل هذه المهمة كافة المستندات من مجموعة حالة الجهاز التي مضي عليها أكثر من 30 يومًا. مفيد عندما لا تكون حالة بطارية الرافع مُحدثة بطريقة صحيح.", + "Delete old documents from devicestatus collection?": "هل ترغب بحذف جميع المستندات من مجموعة حالة الجهاز؟", + "Clean Mongo entries (glucose entries) database": "تنظيف جميع إدخالات (إدخالات السكر) قاعدة بيانات مونغو", + "Delete all documents from entries collection older than 180 days": "حذف جميع المستندات من مجموعة الإدخالات التي مضى عليها أكثر من 180 يومًا", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "تزيل هذه المهمة كافة المستندات من مجموعة الإدخالات التي مضى عليها أكثر من 180 يومًا. مفيد عندما لا تكون حالة بطارية الرافع مُحدثة بطريقة صحيح.", + "Delete old documents": "حذف المستندات القديمة", + "Delete old documents from entries collection?": "هل تريد حذف المستندات القديمة من مجموعة الإدخالات؟", + "%1 is not a valid number": "%1 ليس رقمًا صحيحًا", + "%1 is not a valid number - must be more than 2": "%1 ليس رقمًا صحيحًا - يجب أن يكون أكثر من 2", + "Clean Mongo treatments database": "تنظيف قاعدة بيانات علاجات مونغو", + "Delete all documents from treatments collection older than 180 days": "حذف جميع المستندات من مجموعة العلاجات التي مضى عليها أكثر من 180 يومًا", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "تزيل هذه المهمة كافة المستندات من مجموعة العلاجات التي مضى عليها أكثر من 180 يومًا. مفيد عندما لا تكون حالة بطارية الرافع مُحدثة بطريقة صحيح.", + "Delete old documents from treatments collection?": "هل ترغب بحذف كافة المستندات من مجموعة العلاجات؟", + "Admin Tools": "أدوات المسؤول", + "Nightscout reporting": "تقرير نايت سكاوت", + "Cancel": "إلغاء", + "Edit treatment": "تعديل المعالجة", + "Duration": "المدة", + "Duration in minutes": "المدة بالدقائق", + "Temp Basal": "درجة الحرارة القاعدية", + "Temp Basal Start": "بداية درجة الحرارة القاعدية", + "Temp Basal End": "نهاية درجة الحرارة القاعدية", + "Percent": "بالمائة", + "Basal change in %": "التغيير الأساسي بالمائة", + "Basal value": "القيمة الأساسية", + "Absolute basal value": "القيمة الأساسية المطلقة", + "Announcement": "إعلان", + "Loading temp basal data": "تحميل درجة حرارة البيانات القاعدية", + "Save current record before changing to new?": "حفظ السجل الحالي قبل التغيير إلى جديد؟", + "Profile Switch": "تبديل الملف الشخصي", + "Profile": "الملف الشخصي", + "General profile settings": "الإعدادات العامة للملف الشخصي", + "Title": "العنوان", + "Database records": "سجلات قاعدة البيانات", + "Add new record": "إضافة سجل جديد", + "Remove this record": "أزِل هذا السجل", + "Clone this record to new": "استنسِخ هذا السجل إلى سجل جديد", + "Record valid from": "سجل صالح من", + "Stored profiles": "الملفات الشخصية المخزنة", + "Timezone": "المنطقة الزمنية", + "Duration of Insulin Activity (DIA)": "مدة نشاط الأنسولين\n", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "تمثل المدة النموذجية التي يبدأ فيها مفعول الأنسولين. وتختلف من مريض لآخر وحسب نوع الأنسولين. عادةً تكون من ٤ إلى ٣ ساعات لمعظم الأنسولين المُضَخ ومعظم المرضى. كما يُطلَق عليها أحيانًا ايضاً عمر الأنسولين.", + "Insulin to carb ratio (I:C)": "نسبة الأنسولين إلى الكربوهيدرات", + "Hours:": "الساعات:", + "hours": "الساعات", + "g/hour": "جم/ساعة", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "جم من الكربوهيدرات لكل وحدة من الأنسولين. وهي النسبة بين عدد جرامات الكربوهيدرات ووحدات الأنسولين المقابلة لها.", + "Insulin Sensitivity Factor (ISF)": "عامل حساسية الأنسولين", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "مـليجرام/ديسيلتر أو ملي مول/لتر. نسبة تغير مستوى السكر في الدم مع كل وحدة من الأنسولين المصحح.", + "Carbs activity / absorption rate": "نشاط الكربوهيدرات/ معدل الامتصاص", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "غرام لكل وحدة زمنية. يمثل كلاً من التغيير في الكربوهيدرات النشط لكل وحدة زمنية، بالإضافة إلى كمية الكربوهيدرات التي يجب أن تصبح فعالة خلال ذلك الوقت. منحنيات امتصاص/ نشاط الكربوهيدرات غير مفهومة جيدًا مقارنة بنشاط الأنسولين، ولكن يمكن تقريبها باستخدام تأخير مبدأي متبوعًا بمعدل امتصاص ثابت (جم/ ساعة).", + "Basal rates [unit/hour]": "المعدلات القاعدية (وحدة/ ساعة)", + "Target BG range [mg/dL,mmol/L]": "نطاق سكر الدم المستهدف (ملجم/ ديسيلتر، مليمول/ لتر)", + "Start of record validity": "بداية صلاحية السجل", + "Icicle": "مخطط Icicle", + "Render Basal": "تقديم قاعدة أساسية", + "Profile used": "الملف الشخصي المستخدم", + "Calculation is in target range.": "العملية الحسابية في النطاق المستهدف.", + "Loading profile records ...": "تحميل سجلات الملف الشخصي ...", + "Values loaded.": "تم تحميل القيم.", + "Default values used.": "القيم الافتراضية المستخدمة.", + "Error. Default values used.": "خطأ. القيم الافتراضية المستخدمة.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "لا تتطابق النطاقات الزمنية بين الهدف المنخفض و الهدف المرتفع. تم استعادة القيم إلى الوضع الافتراضي.", + "Valid from:": "صالح من:", + "Save current record before switching to new?": "هل ترغب في حفظ السجل الحالي قبل التبديل إلى الجديد؟", + "Add new interval before": "إضافة فترة جديدة قبل", + "Delete interval": "حذف الفترة", + "I:C": "I:C", + "ISF": "عامل حساسية الأنسولين", + "Combo Bolus": "مزيج الجرعة", + "Difference": "الاختلاف", + "New time": "وقت جديد", + "Edit Mode": "وضع التعديل", + "When enabled icon to start edit mode is visible": "عند تفعيل الأيقونة للبدء يصبح وضع التعديل مرئي", + "Operation": "التشغيل", + "Move": "انقل", + "Delete": "احذف", + "Move insulin": "انقل الأنسولين", + "Move carbs": "انقل الكربوهيدرات", + "Remove insulin": "أزِل الأنسولين", + "Remove carbs": "أزِل الكربوهيدرات", + "Change treatment time to %1 ?": "هل تريد تغيير وقت العلاج إلى٪ 1؟", + "Change carbs time to %1 ?": "هل تريد تغيير وقتالكربوهيدرات إلى %1؟", + "Change insulin time to %1 ?": "هل ترغب في تغيير وقت الإنسولين إلى %1؟", + "Remove treatment ?": "حذف المعالجة", + "Remove insulin from treatment ?": "حذف الإنسولين من المعالجة", + "Remove carbs from treatment ?": "حذف الكربوهيدرات من المعالجة", + "Rendering": "معالجة", + "Loading OpenAPS data of": "تحميل بيانات مشروع نظام البنكرياس الاصطناعي المفتوح الخاصة ب", + "Loading profile switch data": "تحميل بيانات تبديل الملف الشخصي", + "Redirecting you to the Profile Editor to create a new profile.": "إعادة توجيهك إلى محرر الملف الشخصي لإنشاء ملف تعريف جديد.", + "Pump": "المضخة", + "Sensor Age": "عمر المستشعر", + "Insulin Age": "عمر الأنسولين", + "Temporary target": "هدف مؤقت", + "Reason": "السبب", + "Eating soon": "الأكل قريبًا", + "Top": "أعلي", + "Bottom": "أقل", + "Activity": "النشاط", + "Targets": "الأهداف", + "Bolus insulin:": "جرعة أنسولين:", + "Base basal insulin:": "الأنسولين القاعدي الأساسي:", + "Positive temp basal insulin:": "الأنسولين القاعدي ذو درجة الحرارة الإيجابية:", + "Negative temp basal insulin:": "الأنسولين القاعدي ذو درجة الحرارة السلبية:", + "Total basal insulin:": "إجمالي الأنسولين القاعدي:", + "Total daily insulin:": "إجمالي الأنسولين اليومي:", + "Unable to save Role": "تعذر حفظ الدور", + "Unable to delete Role": "تعذر حذف الدور", + "Database contains %1 roles": "تحتوي قاعدة البيانات علي أدوار %1", + "Edit Role": "تعديل الدور", + "admin, school, family, etc": "المسؤول، المدرسة، الأسرة، إلخ", + "Permissions": "الأذونات", + "Are you sure you want to delete: ": "هل ترغب حقا بالحذف: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "كل دور سيكون له إذن واحد أو أكثر. * الإذن عبارة عن بدل، والأذونات عبارة عن تسلسل هرمي يستخدم : كفاصل.", + "Add new Role": "إضافة دور جديد", + "Roles - Groups of People, Devices, etc": "الأدوار - مجموعات من الأشخاص، الأجهزة، إلخ", + "Edit this role": "تعديل هذا الدور", + "Admin authorized": "مدير مصرح به", + "Subjects - People, Devices, etc": "الموضوعات - الأشخاص، الأجهزة، إلخ", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "سيكون لكل موضوع رمز وصول منفرد ودور واحد أو أكثر. انقر فوق رمز الوصول لفتح عرض جديد بالموضوع المحدد، ويمكن بعد ذلك مشاركة هذا الرابط السري.", + "Add new Subject": "إضافة موضوع جديد", + "Unable to save Subject": "تعذر حفظ الموضوع", + "Unable to delete Subject": "تعذر حذف الموضوع", + "Database contains %1 subjects": "تحتوي قاعدة البيانات على %1 من الموضوعات", + "Edit Subject": "تعديل الموضوع", + "person, device, etc": "شخص، جهاز، إلخ", + "role1, role2": "الدور 1، الدور 2", + "Edit this subject": "تعديل هذا الموضوع", + "Delete this subject": "حذف هذا الموضوع", + "Roles": "أدوار", + "Access Token": "رمز وصول", + "hour ago": "منذ ساعة", + "hours ago": "منذ ساعات", + "Silence for %1 minutes": "صمت لمدة %1 دقيقة", + "Check BG": "فحص نسبة السكر في الدم", + "BASAL": "قاعدي", + "Current basal": "القاعدي الحالي", + "Sensitivity": "حساسية", + "Current Carb Ratio": "نسبة الكربوهيدرات الحالية", + "Basal timezone": "المنطقة الزمنية القاعدية", + "Active profile": "ملف نشط", + "Active temp basal": "درجة الحرارة القاعدية النشطة", + "Active temp basal start": "بداية درجة الحرارة القاعدية النشطة", + "Active temp basal duration": "مدة درجة الحرارة القاعدية النشطة", + "Active temp basal remaining": "درجة الحرارة القاعدية النشطة المتبقية", + "Basal profile value": "قيمة الملف القاعدي", + "Active combo bolus": "جرعة تركيبة نشطة", + "Active combo bolus start": "بداية جرعة تركيبة نشطة", + "Active combo bolus duration": "مدة جرعة تركيبة نشطة", + "Active combo bolus remaining": "جرعة تركيبة نشطة متبقية", + "BG Delta": "دلتا سكر الدم", + "Elapsed Time": "الوقت المُنْقَضِي", + "Absolute Delta": "دلتا مطلقة", + "Interpolated": "مُقْحَمَة", + "BWP": "معاينة معالج البلعة", + "Urgent": "عاجل", + "Warning": "تحذير", + "Info": "معلومات", + "Lowest": "الأقل", + "Snoozing high alarm since there is enough IOB": "غفوة التنبيه العالي نظرًا لكفاية الأنسولين النشط", + "Check BG, time to bolus?": "التحقق من سكر الدم، وقت الجرعة؟", + "Notice": "ملاحظة", + "required info missing": "المعلومات المطلوبة مفقودة", + "Insulin on Board": "الأنسولين النشط", + "Current target": "الهدف الحالي", + "Expected effect": "التأثير المتوقع", + "Expected outcome": "النتيجة المتوقعة", + "Carb Equivalent": "مكافئ الكربوهيدرات", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "مكافئ الأنسولين الزائد %1U أكثر منه الاحتياج المطلوب للوصول إلى الهدف الأدني، دون احتساب الكربوهيدرات", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "يعادل الأنسولين الزائد %1 أكثر من المطلوب للوصول إلى الهدف المنخفض، تأكد من تغطية الإنسولين النشط من خلال الكربوهيدرات", + "%1U reduction needed in active insulin to reach low target, too much basal?": "مطلوب تقليل %1 من الأنسولين النشط للوصول إلى الهدف المنخفض، والكثير من القاعدي؟", + "basal adjustment out of range, give carbs?": "التعديل القاعدي خارج النطاق، منح الكربوهيدرات؟", + "basal adjustment out of range, give bolus?": "التعديل القاعدي خارج النطاق، منح الجرعة؟", + "above high": "فوق العالي", + "below low": "أقل من منخفض", + "Projected BG %1 target": "هدف سكر الدم %1 المتوقع", + "aiming at": "يهدف إلى", + "Bolus %1 units": "الجرعة %1 من الوحدات", + "or adjust basal": "أو ضبط القاعدية", + "Check BG using glucometer before correcting!": "-> تحقق من نسبة السكر في الدم باستخدام مقياس السكر قبل التصحيح!", + "Basal reduction to account %1 units:": "التخفيض القاعدي لحساب%1 وحدة:", + "30m temp basal": "30 متر من درجة الحرارة القاعدية", + "1h temp basal": "ساعة واحدة من درجة الحرارة القاعدية", + "Cannula change overdue!": "تغيير قنية متأخر!", + "Time to change cannula": "ميعاد تغيير القنية الطبية", + "Change cannula soon": "تغيير القنية الطبية قريبا", + "Cannula age %1 hours": "عمر القنية الطبية هو %1 ساعة", + "Inserted": "مدخلة", + "CAGE": "صندوق", + "COB": "الكربوهيدرات النشط", + "Last Carbs": "أخيرًا الكربوهيدرات", + "IAGE": "الساعة العمرية", + "Insulin reservoir change overdue!": "لقد فات موعد تغيير خزان الأنسولين", + "Time to change insulin reservoir": "حان الوقت لتغيير خزان الأنسولين", + "Change insulin reservoir soon": "اقترب موعد تغيير خزان الأنسولين", + "Insulin reservoir age %1 hours": "عمر خزان الأنسولين %1 ساعة", + "Changed": "تم التغيير", + "IOB": "الإنسولين النشط", + "Careportal IOB": "بوابة رعاية الإنسولين النشط", + "Last Bolus": "الحرعة الأخيرة", + "Basal IOB": "الإنسولين الأساسي النشط", + "Source": "مصدر", + "Stale data, check rig?": "بيانات قديمة، تحقق من الجهاز؟", + "Last received:": "آخر استلام:", + "%1m ago": "%1 منذ دقيقة", + "%1h ago": "%1 مُنذُ ساعة", + "%1d ago": "%1 مُنذُ يوم", + "RETRO": "رجوع", + "SAGE": "نهج مبسط للدوران والتدرج", + "Sensor change/restart overdue!": "تغيير المستشعر/ إعادة التشغيل متأخر", + "Time to change/restart sensor": "حان الوقت لتغيير/ إعادة تشغيل جهاز الاستشعار", + "Change/restart sensor soon": "اقترب موعد تغيير/إعادة تشغيل المستشعر", + "Sensor age %1 days %2 hours": "عمر المستشعر %1 يوم %2 ساعة", + "Sensor Insert": "ادخال المجس", + "Sensor Start": "تم بدء المجس", + "days": "أيام", + "Insulin distribution": "توزيع الإنسولين", + "To see this report, press SHOW while in this view": "لمشاهدة هذا التقرير، اضغط على \"إظهار\" أثناء وجودك في هذا العرض", + "AR2 Forecast": "توقعات AR2", + "OpenAPS Forecasts": "توقعات مشروع نظام البنكرياس الاصطناعي المفتوح", + "Temporary Target": "الهدف المؤقت", + "Temporary Target Cancel": "إلغاء الهدف المؤقت", + "OpenAPS Offline": "مشروع نظام البنكرياس الاصطناعي المفتوح غير المتصل على الانترنت", + "Profiles": "الملفات الشخصية", + "Time in fluctuation": "وقت التقلبات", + "Time in rapid fluctuation": "الوقت عند التقلبات السريعة", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "هذا مجرد تقدير تقريبي يمكن أن يكون غير دقيق إطلاقًا ولا يحل محل اختبار الدم الفعلي، الصيغة المستخدمة مأخوذة من:", + "Filter by hours": "فلتر بالساعات", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "الوقت المستغرق في التقلبت والوقت المستغرق في التقلبات السريعة يقيسان النسبة المئوية للوقت خلال الفترة التي تم فحصها، والتي يتغير خلالها مستوى السكر في الدم بشكل سريع نسبيًا. القيم الأقل هي الأفضل.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "متوسط إجمالي التغيير اليومي هو مجموع القيمة المطلقة لجميع رحلات السكر خلال الفترة التي تم فحصها، مقسومًا على عدد الأيام، النسبة الاقل هي الافضل.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "متوسط إجمالي التغيير اليومي هو مجموع القيمة المطلقة لجميع رحلات السكر خلال الفترة التي تم فحصها، مقسومًا على عدد الساعات خلال الفترة، النسبة الاقل هي الافضل.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "يتم حساب خارج نطاق الجذر المتوسط المربع من خلال قطع المسافة خارج النطاق لجميع قراءات السكر للفترة موضع البحث، وجمعها وتقسيمها على العد واخذ الجذر التربيعي. هذا المقياس مشابه للنسبة المئوية في النطاق ولكن قراءات الأوزان بعيدة عن النطاق الأعلى. القيم السفلى هي الأفضل.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">يمكن إيجادها هنا.", + "Mean Total Daily Change": "متوسط إجمالي التغيير اليومي", + "Mean Hourly Change": "متوسط التغير فى الساعة", + "FortyFiveDown": "انخفاض طفيف", + "FortyFiveUp": "ارتفاع طفيف", + "Flat": "إمساك", + "SingleUp": "رفع", + "SingleDown": "إسقاط", + "DoubleDown": "إسقاط سريع", + "DoubleUp": "رفع سريع", + "virtAsstUnknown": "هذه القيمة غير معروفة في الوقت الحالي. يرجى الاطلاع على موقع نايتسكوت الخاص بك لمزيد من التفاصيل.", + "virtAsstTitleAR2Forecast": "توقعات AR2", + "virtAsstTitleCurrentBasal": "القاعدي الحالي", + "virtAsstTitleCurrentCOB": "الكربوهيدرات النشط في الوقت الحالي", + "virtAsstTitleCurrentIOB": "الإنسولين النشط في الوقت الحالي", + "virtAsstTitleLaunch": "مرحبًا بك في نايت سكاوت", + "virtAsstTitleLoopForecast": "إجمالي حلقة التوقعات", + "virtAsstTitleLastLoop": "آخر حلقة", + "virtAsstTitleOpenAPSForecast": "تنبؤات مشروع نظام البنكرياس الاصطناعي المفتوح", + "virtAsstTitlePumpReservoir": "الأنسولين المتبقي", + "virtAsstTitlePumpBattery": "بطارية المضخة", + "virtAsstTitleRawBG": "سكر الدم الخام الحالي", + "virtAsstTitleUploaderBattery": "بطارية الرافع", + "virtAsstTitleCurrentBG": "مستوى السكر في الدم الحالي", + "virtAsstTitleFullStatus": "الحالة الكاملة", + "virtAsstTitleCGMMode": "وضع جهاز المراقبة المستمرة للجلوكوز\n", + "virtAsstTitleCGMStatus": "حالة جهاز المراقبة المستمرة للجلوكوز\n", + "virtAsstTitleCGMSessionAge": "عمر دورة مراقبة السكر المستمرة", + "virtAsstTitleCGMTxStatus": "حالة جهاز إرسال مراقبة السكر المستمر", + "virtAsstTitleCGMTxAge": "عمر جهاز إرسال مراقبة السكر المستمر", + "virtAsstTitleCGMNoise": "تشويش المراقبة المستمرة للسكر", + "virtAsstTitleDelta": "دلتا جلوكوز الدم", + "virtAsstStatus": "%1 و%2 اعتبارًا من %3.", + "virtAsstBasal": "%1 القاعدي الحالي هو %2 وحدة في الساعة", + "virtAsstBasalTemp": "%1 درجة الحرارة القاعدية من %2 وحدة في الساعة ستنتهي %3", + "virtAsstIob": "ولديك %1 من الأنسولين النشط.", + "virtAsstIobIntent": "لديك %1 من الأنسولين النشط", + "virtAsstIobUnits": "%1 وحدة من", + "virtAsstLaunch": "ما الذي ترغب في التحقق منه في نايت سكاوت؟", + "virtAsstPreamble": "الخاص بك", + "virtAsstPreamble3person": "%1 لديه ", + "virtAsstNoInsulin": "لا", + "virtAsstUploadBattery": "مستوي بطارية الرافع هي %1", + "virtAsstReservoir": "لديك 1٪ وحدة متبقية", + "virtAsstPumpBattery": "بطارية المضخة الخاصة بك عند٪1 ٪2", + "virtAsstUploaderBattery": "بطارية برنامج التحميل الخاصة بك عند ٪1", + "virtAsstLastLoop": "آخر حلقة ناجحة كانت ١٪", + "virtAsstLoopNotAvailable": "لا يبدو أن ملحق الحلقة قد تم تمكينه", + "virtAsstLoopForecastAround": "وفقا لتوقعات الحلقة فمن المتوقع أن تكون بين ١٪ و٢٪ خلال ٣٪ التالية", + "virtAsstLoopForecastBetween": "وفقا لتنبؤات الحلقة من المتوقع أن تكون بين ١٪ و٢٪ خلال ٣٪ التالية", + "virtAsstAR2ForecastAround": "وفقا لتنبؤات AR2 فمن المتوقع أن تكون حوالي ٪١ خلال ٢٪ التالية", + "virtAsstAR2ForecastBetween": "ووفقا لتنبؤات AR2 فمن المتوقع أن تكون بين ٪١ و٢٪ خلال ٣٪ التالية", + "virtAsstForecastUnavailable": "غير قادر على التنبؤ بالبيانات المتوفرة", + "virtAsstRawBG": "Bg الخام الخاص بك هو ١٪", + "virtAsstOpenAPSForecast": "مستوى السكر في الدم لمشروع نظام البنكرياس الاصطناعي المفتوح النهائي هو %1", + "virtAsstCob3person": "%1 يحتوي على %2 كربوهيدرات نشط", + "virtAsstCob": "لديك ١٪ كربوهيدرات في جسمك", + "virtAsstCGMMode": "كان وضع جهاز المراقبة المستمرة للسكر الخاص بك %1 اعتباراً من %2.", + "virtAsstCGMStatus": "كانت حالة جهاز المراقبة المستمرة للسكر الخاص بك %1 اعتباراً من %2.", + "virtAsstCGMSessAge": "كانت جلسة مراقبة السكر المستمرة الخاصة بك نشطة لمدة %1 من الأيام و%2 من الساعات.", + "virtAsstCGMSessNotStarted": "لا توجد جلسات مراقبة السكر المستمرة نشطة في الوقت الحالي.", + "virtAsstCGMTxStatus": "كانت حالة مُرسِل جهاز المراقبة المستمرة للسكر الخاص بك %1 اعتباراً من %2.", + "virtAsstCGMTxAge": "عمر مُرسِل جهاز المراقبة المستمرة للسكر الخاص بك %1 من الأيام.", + "virtAsstCGMNoise": "كانت ضوضاء جهاز المراقبة المستمرة للسكر الخاص بك %1 اعتباراً من %2.", + "virtAsstCGMBattOne": "كانت بطارية جهاز المراقبة المستمرة للسكر الخاص بك %1 فولت اعتباراً من %2.", + "virtAsstCGMBattTwo": "كانت مستويات بطارية جهاز المراقبة المستمرة للسكر الخاص بك %1 فولت و %2 فولت اعتباراً من %3.", + "virtAsstDelta": "كانت الدلتا الخاصة بك ١٪ بين ٢٪ و٣٪.", + "virtAsstDeltaEstimated": "كانت الدلتا المُقدرَة الخاصة بك ١٪ بين ٢٪ و٣٪.", + "virtAsstUnknownIntentTitle": "هدف غبر معروف", + "virtAsstUnknownIntentText": "أنا آسف، لا أعرف ما الذي تطلبه.", + "Fat [g]": "الدهون [جم]", + "Protein [g]": "البروتين [جم]", + "Energy [kJ]": "الطاقة [كيلوجول]", + "Clock Views:": "عرض الساعة:", + "Clock": "ساعة", + "Color": "لون", + "Simple": "بسيط", + "TDD average": "متوسط الجرعة اليومية", + "Bolus average": "متوسط الجرعات", + "Basal average": "المتوسط الأساسي", + "Base basal average:": "المتوسط الأساسي للقاعدة:", + "Carbs average": "متوسط الكربوهيدرات", + "Eating Soon": "الأكل قريباً \n", + "Last entry {0} minutes ago": "كان آخر إدخال قبل {٠} دقيقة", + "change": "تغيير", + "Speech": "حديث", + "Target Top": "الهدف الأعلى", + "Target Bottom": "الهدف الأسفل", + "Canceled": "ملغية", + "Meter BG": "مقياس السكر فى الدم", + "predicted": "تنبأ", + "future": "مستقبل", + "ago": "منذ", + "Last data received": "آخر بيانات وردت", + "Clock View": "عرض الساعة", + "Protein": "البروتين", + "Fat": "الدهون", + "Protein average": "متوسط البروتين", + "Fat average": "متوسط الدهون", + "Total carbs": "متوسط الكربوهيدرات", + "Total protein": "إجمالي البروتين", + "Total fat": "إجمالي الدهون", + "Database Size": "حجم قاعدة البيانات", + "Database Size near its limits!": "حجم قاعدة البيانات بالقرب من حدودها!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "حجم قاعدة البيانات %1 ميبي بايت من %2 ميبي بايت. الرجاء النسخ الاحتياطي وتنظيف قاعدة البيانات!", + "Database file size": "حجم ملف قاعدة البيانات", + "%1 MiB of %2 MiB (%3%)": "%1 ميبي بايت من %2 ميبي بايت (%3%)", + "Data size": "حجم البيانات", + "virtAsstDatabaseSize": "٪ 1 ميبي بايت يمثل 2٪ من مساحة قاعدة البيانات المتوفرة.", + "virtAsstTitleDatabaseSize": "حجم ملف قاعدة البيانات", + "Carbs/Food/Time": "الكربوهيدرات/ الغذاء/ الوقت", + "You have administration messages": "لديك رسائل إدارية", + "Admin messages in queue": "رسائل المسؤول في قائمة الانتظار", + "Queue empty": "قائمة الانتظار فارغة", + "There are no admin messages in queue": "لا توجد رسائل مشرف في قائمة الانتظار", + "Please sign in using the API_SECRET to see your administration messages": "يرجى تسجيل الدخول باستخدام كلمة مرور واجهة برمجة التطبيقات لمشاهدة رسائلك الإدارية", + "Reads enabled in default permissions": "قراءات ممكّنة في الأذونات الافتراضية", + "Data reads enabled": "قراءة البيانات مفعلة", + "Data writes enabled": "قراءة البيانات مفعلة", + "Data writes not enabled": "كتابة البيانات غير مفعلة", + "Color prediction lines": "خطوط تنبؤ الألوان", + "Release Notes": "كتابة ملاحظات", + "Check for Updates": "التحقق من وجود تحديثات", + "Open Source": "مفتوح المصدر", + "Nightscout Info": "معلومات نايت سكاوت", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "الغرض الأساسي من لوبليزر هو تصور كيفية أداء نظام الحلقة المغلقة. قد تعمل أيضًا مع الإعدادات الأخرى، سواء كانت مغلقة أو مفتوحة أو بدون حلقة. ولكن اعتمادًا على القائم بالتحميل الذي تستخدمه، ومدى تكرار قدرته على التقاط بياناتك وتحميلها، وكيف أنه قادر على إعادة ملء البيانات المفقودة، فقد تحتوي بعض الرسوم البيانية على فجوات أو حتى تكون فارغة تمامًا. تأكد دائمًا من أن الرسوم البيانية تبدو معقولة. الأفضل هو عرض يوم واحد في كل مرة والتمرير خلال عدد من الأيام أولاً لمعرفة ذلك.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "يتضمن لوبليزر ميزة تغيير الوقت. على سبيل المثال، إذا تناولت وجبة الإفطار في الساعة 07:00 في أحد الأيام وفي الساعة 08:00 في اليوم التالي لمتوسط منحنى سكرالدم، فمن المرجح أن يبدو هذين اليومين مستويًا ولن يظهرا الاستجابة الفعلية بعد وجبة الإفطار. سيحسب التحول الزمني متوسط الوقت الذي تم فيه تناول هذه الوجبات ثم تحويل جميع البيانات (الكربوهيدرات والأنسولين والقاعدية وما إلى ذلك) خلال كلا اليومين بفارق الوقت المقابل بحيث تتوافق كلتا الوجبتين مع متوسط وقت بدء الوجبة.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "في هذا المثال تُدفع جميع البيانات من اليوم الأول 30 دقيقة إلى الأمام في الوقت المناسب وجميع البيانات من اليوم الثاني 30 دقيقة إلى الخلف في الوقت المناسب بحيث يبدو كما لو تناولت الإفطار الساعة 07:30 لمدة يومين. هذا يسمح لك برؤية متوسط استجابة سكر الدم الفعلية للوجبة.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "يُبرز التحول الزمني الفترة التي تلي متوسط وقت بدء الوجبة باللون الرمادي، طوال مدة عمل الإنسولين. ونظرًا لأنه يتم إزاحة جميع نقاط البيانات طوال اليوم، فقد لا تكون المنحنيات خارج المنطقة الرمادية دقيقة.", + "Note that time shift is available only when viewing multiple days.": "لاحظ أن تغيير الوقت متاح فقط عند عرض عدة أيام.", + "Please select a maximum of two weeks duration and click Show again.": "يرجى تحديد مدة أسبوعين كحد أقصى وانقر فوق إظهار مرة أخرى.", + "Show profiles table": "إظهار جدول الملفات الشخصية", + "Show predictions": "إظهار التنبؤات", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "تغيير الوقت فيما يتعلق بالوجبات الأكبر من ١٪ جم من الكربوهيدرات المستهلكة بين ٢٪ و٣٪", + "Previous": "السابق", + "Previous day": "اليوم السابق", + "Next day": "اليوم التالي", + "Next": "التالي", + "Temp basal delta": "درجة حرارة دلتا الأساسية", + "Authorized by token": "مصرح به من قبل رمز", + "Auth role": "دور المصادقة", + "view without token": "عرض بدون رمز", + "Remove stored token": "إزالة الرمز المخزن", + "Weekly Distribution": "التوزيع الأسبوعي", + "Failed authentication": "فشل المصادقة", + "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?": "حاول جهاز على عنوان البروتوكول %1 المصادقة باستخدام نايت سكاوت مستخدمًا بيانات اعتماد خاطئة. تحقق مما إذا كان لديك رافع مع API_SECRET خاطئ أو رمز وصول؟", + "Default (with leading zero and U)": "الافتراضي (دون صفر البداية والوحدة)", + "Concise (with U, without leading zero)": "الموجز (مع الوحدة، بدون صفر البداية)", + "Minimal (without leading zero and U)": "الحد الأدنى (دون صفر البداية والوحدة)", + "Small Bolus Display": "عرض الجرعة الصغيرة", + "Large Bolus Display": "عرض الجرعة الكبيرة", + "Bolus Display Threshold": "بداية عرض الجرعة", + "%1 U and Over": "%1 وحدة وأكثر", + "Event repeated %1 times.": "تكرار الحدث %1 مرة.", + "minutes": "دقائق", + "Last recorded %1 %2 ago.": "آخر تسجيل منذ %1 %2.", + "Security issue": "مشكلة أمنية", + "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.": "تم اكتشاف API_SECRET ضعيف. يرجى استخدام مزيج من الأحرف الصغيرة والكبيرة والأرقام والأحرف غير الأبجدية الرقمية مثل !#٪&/ لتقليل مخاطر الوصول غير المصرح به. الحد الأدنى لقوة API_SECRET هو 12 حرفًا.", + "less than 1": "أقل من", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "تطابق كلمة مرور مونغو دي بي و API_SECRET. فكرة سيئة حقًا. يرجى تغيير كليهما وعدم إعادة استخدام كلمات المرور داخل النظام." +} diff --git a/translations/bg_BG.json b/translations/bg_BG.json new file mode 100644 index 00000000000..e7072c68c25 --- /dev/null +++ b/translations/bg_BG.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Активиране на порта", + "Mo": "Пон", + "Tu": "Вт", + "We": "Ср", + "Th": "Четв", + "Fr": "Пет", + "Sa": "Съб", + "Su": "Нед", + "Monday": "Понеделник", + "Tuesday": "Вторник", + "Wednesday": "Сряда", + "Thursday": "Четвъртък", + "Friday": "Петък", + "Saturday": "Събота", + "Sunday": "Неделя", + "Category": "Категория", + "Subcategory": "Подкатегория", + "Name": "Име", + "Today": "Днес", + "Last 2 days": "Последните 2 дни", + "Last 3 days": "Последните 3 дни", + "Last week": "Последната седмица", + "Last 2 weeks": "Последните 2 седмици", + "Last month": "Последният месец", + "Last 3 months": "Последните 3 месеца", + "From": "От", + "To": "До", + "Notes": "Бележки", + "Food": "Храна", + "Insulin": "Инсулин", + "Carbs": "Въглехидрати", + "Notes contain": "бележките съдържат", + "Target BG range bottom": "Долна граница на КЗ", + "top": "горна", + "Show": "Покажи", + "Display": "Покажи", + "Loading": "Зареждане", + "Loading profile": "Зареждане на профил", + "Loading status": "Зареждане на статус", + "Loading food database": "Зареждане на данни с храни", + "not displayed": "Не се показва", + "Loading CGM data of": "Зареждане на CGM данни от", + "Loading treatments data of": "Зареждане на въведените лечения от", + "Processing data of": "Зареждане на данни от", + "Portion": "Порция", + "Size": "Големина", + "(none)": "(няма)", + "None": "няма", + "": "<няма>", + "Result is empty": "Няма резултат", + "Day to day": "Ден за ден", + "Week to week": "седмица към седмица", + "Daily Stats": "Дневна статистика", + "Percentile Chart": "Процентна графика", + "Distribution": "Разпределение", + "Hourly stats": "Статистика по часове", + "netIOB stats": "netIOB татистика", + "temp basals must be rendered to display this report": "временните базали трябва да са показани за да се покаже тази това", + "No data available": "Няма данни за показване", + "Low": "Ниска", + "In Range": "В граници", + "Period": "Период", + "High": "Висока", + "Average": "Средна", + "Low Quartile": "Ниска четвъртинка", + "Upper Quartile": "Висока четвъртинка", + "Quartile": "Четвъртинка", + "Date": "Дата", + "Normal": "Нормалнa", + "Median": "Средно", + "Readings": "Измервания", + "StDev": "Стандартно отклонение", + "Daily stats report": "Дневна статистика", + "Glucose Percentile report": "Графика на КЗ", + "Glucose distribution": "Разпределение на КЗ", + "days total": "общо за деня", + "Total per day": "общо за деня", + "Overall": "Общо", + "Range": "Диапазон", + "% of Readings": "% от измервания", + "# of Readings": "№ от измервания", + "Mean": "Средна стойност", + "Standard Deviation": "Стандартно отклонение", + "Max": "Макс.", + "Min": "Мин.", + "A1c estimation*": "Очакван HbA1c", + "Weekly Success": "Седмичен успех", + "There is not sufficient data to run this report. Select more days.": "Няма достатъчно данни за показване. Изберете повече дни.", + "Using stored API secret hash": "Използване на запаметена API парола", + "No API secret hash stored yet. You need to enter API secret.": "Няма запаметена API парола. Tрябва да въведете API парола", + "Database loaded": "База с данни заредена", + "Error: Database failed to load": "ГРЕШКА. Базата с данни не успя да се зареди", + "Error": "Грешка", + "Create new record": "Създаване на нов запис", + "Save record": "Запази запис", + "Portions": "Порции", + "Unit": "Единици", + "GI": "ГИ", + "Edit record": "Редактирай запис", + "Delete record": "Изтрий запис", + "Move to the top": "Преместване в началото", + "Hidden": "Скрити", + "Hide after use": "Скрий след употреба", + "Your API secret must be at least 12 characters long": "Вашата АPI парола трябва да е дълга поне 12 символа", + "Bad API secret": "Некоректна API парола", + "API secret hash stored": "УРА! API парола запаметена", + "Status": "Статус", + "Not loaded": "Не е заредено", + "Food Editor": "Редактор за храна", + "Your database": "Твоята база с данни", + "Filter": "Филтър", + "Save": "Запази", + "Clear": "Изчисти", + "Record": "Запиши", + "Quick picks": "Бърз избор", + "Show hidden": "Покажи скритото", + "Your API secret or token": "Вашата АПИ парола или токен", + "Remember this device. (Do not enable this on public computers.)": "Запомни това устройство. (Не активирайте на публични компютри.)", + "Treatments": "Събития", + "Time": "Време", + "Event Type": "Вид събитие", + "Blood Glucose": "Кръвна захар", + "Entered By": "Въведено от", + "Delete this treatment?": "Изтрий това събитие", + "Carbs Given": "ВХ", + "Insulin Given": "Инсулин", + "Event Time": "Въвеждане", + "Please verify that the data entered is correct": "Моля проверете, че датата е въведена правилно", + "BG": "КЗ", + "Use BG correction in calculation": "Използвай корекцията за КЗ в изчислението", + "BG from CGM (autoupdated)": "КЗ от сензора (автоматично)", + "BG from meter": "КЗ от глюкомер", + "Manual BG": "Ръчно въведена КЗ", + "Quickpick": "Бърз избор", + "or": "или", + "Add from database": "Добави от базата с данни", + "Use carbs correction in calculation": "Включи корекцията чрез ВХ в изчислението", + "Use COB correction in calculation": "Включи активните ВХ в изчислението", + "Use IOB in calculation": "Включи активния инсулин в изчислението", + "Other correction": "Друга корекция", + "Rounding": "Закръгляне", + "Enter insulin correction in treatment": "Въведи корекция с инсулин като лечение", + "Insulin needed": "Необходим инсулин", + "Carbs needed": "Необходими въглехидрати", + "Carbs needed if Insulin total is negative value": "Необходими въглехидрати, ако няма инсулин", + "Basal rate": "Базален инсулин", + "60 minutes earlier": "Преди 60 минути", + "45 minutes earlier": "Преди 45 минути", + "30 minutes earlier": "Преди 30 минути", + "20 minutes earlier": "Преди 20 минути", + "15 minutes earlier": "Преди 15 минути", + "Time in minutes": "Времето в минути", + "15 minutes later": "След 15 минути", + "20 minutes later": "След 20 минути", + "30 minutes later": "След 30 минути", + "45 minutes later": "След 45 минути", + "60 minutes later": "След 60 минути", + "Additional Notes, Comments": "Допълнителни бележки, коментари", + "RETRO MODE": "МИНАЛО ВРЕМЕ", + "Now": "Сега", + "Other": "Друго", + "Submit Form": "Въвеждане на данните", + "Profile Editor": "Редактор на профила", + "Reports": "Статистика", + "Add food from your database": "Добави храна от твоята база с данни", + "Reload database": "Презареди базата с данни", + "Add": "Добави", + "Unauthorized": "Неразрешен достъп", + "Entering record failed": "Въвеждане на записа не се осъществи", + "Device authenticated": "Устройстово е разпознато", + "Device not authenticated": "Устройсройството не е разпознато", + "Authentication status": "Статус на удостоверяване", + "Authenticate": "Удостоверяване", + "Remove": "Премахни", + "Your device is not authenticated yet": "Вашето устройство все още не е удостоверено", + "Sensor": "Сензор", + "Finger": "От пръстта", + "Manual": "Ръчно", + "Scale": "Скала", + "Linear": "Линейна", + "Logarithmic": "Логоритмична", + "Logarithmic (Dynamic)": "Логоритмична (Динамична)", + "Insulin-on-Board": "Активен инсулин", + "Carbs-on-Board": "Активни въглехидрати", + "Bolus Wizard Preview": "Болус калкулатор", + "Value Loaded": "Стойност заредена", + "Cannula Age": "Възраст на канюлата", + "Basal Profile": "Базален профил", + "Silence for 30 minutes": "Заглуши за 30 минути", + "Silence for 60 minutes": "Заглуши за 60 минути", + "Silence for 90 minutes": "Заглуши за 90 минути", + "Silence for 120 minutes": "Заглуши за 120 минути", + "Settings": "Настройки", + "Units": "Единици", + "Date format": "Формат на датата", + "12 hours": "12 часа", + "24 hours": "24 часа", + "Log a Treatment": "Въвеждане на събитие", + "BG Check": "Проверка на КЗ", + "Meal Bolus": "Болус-основно хранене", + "Snack Bolus": "Болус-лека закуска", + "Correction Bolus": "Болус корекция", + "Carb Correction": "Корекция чрез въглехидрати", + "Note": "Бележка", + "Question": "Въпрос", + "Exercise": "Спорт", + "Pump Site Change": "Смяна на сет", + "CGM Sensor Start": "Ре/Стартиране на сензор", + "CGM Sensor Stop": "Спиране на сензор", + "CGM Sensor Insert": "Смяна на сензор", + "Sensor Code": "Код на сензора", + "Transmitter ID": "Номер трансмитер", + "Dexcom Sensor Start": "Ре/Стартиране на Декском сензор", + "Dexcom Sensor Change": "Смяна на Декском сензор", + "Insulin Cartridge Change": "Смяна на резервоар", + "D.A.D. Alert": "Сигнал от обучено куче", + "Glucose Reading": "Кръвна захар", + "Measurement Method": "Метод на измерване", + "Meter": "Глюкомер", + "Amount in grams": "К-во в грамове", + "Amount in units": "К-во в единици", + "View all treatments": "Преглед на всички събития", + "Enable Alarms": "Активни аларми", + "Pump Battery Change": "Смяна на батерия на помпата", + "Pump Battery Age": "Възраст на батерията на помпата", + "Pump Battery Low Alarm": "Аларма за слаба батерия на помпата", + "Pump Battery change overdue!": "Смяната на батерията на помпата - наложителна", + "When enabled an alarm may sound.": "Когато е активирано, алармата ще има звук", + "Urgent High Alarm": "Много висока КЗ", + "High Alarm": "Висока КЗ", + "Low Alarm": "Ниска КЗ", + "Urgent Low Alarm": "Много ниска КЗ", + "Stale Data: Warn": "Стари данни", + "Stale Data: Urgent": "Много стари данни", + "mins": "мин", + "Night Mode": "Нощен режим", + "When enabled the page will be dimmed from 10pm - 6am.": "Когато е активирано, страницата ще е затъмнена от 22-06ч", + "Enable": "Активен", + "Show Raw BG Data": "Показвай RAW данни", + "Never": "Никога", + "Always": "Винаги", + "When there is noise": "Когато има шум", + "When enabled small white dots will be displayed for raw BG data": "Когато е активно, малки бели точки ще показват RAW данните", + "Custom Title": "Име на страницата", + "Theme": "Тема", + "Default": "Черно-бяла", + "Colors": "Цветна", + "Colorblind-friendly colors": "Цветове за далтонисти", + "Reset, and use defaults": "Нулирай и използвай стандартните настройки", + "Calibrations": "Калибрации", + "Alarm Test / Smartphone Enable": "Тестване на алармата / Активно за мобилни телефони", + "Bolus Wizard": "Болус съветник ", + "in the future": "в бъдещето", + "time ago": "преди време", + "hr ago": "час по-рано", + "hrs ago": "часа по-рано", + "min ago": "мин. по-рано", + "mins ago": "мин. по-рано", + "day ago": "ден по-рано", + "days ago": "дни по-рано", + "long ago": "преди много време", + "Clean": "Чист", + "Light": "Лек", + "Medium": "Среден", + "Heavy": "Висок", + "Treatment type": "Вид събитие", + "Raw BG": "Непреработена КЗ", + "Device": "Устройство", + "Noise": "Шум", + "Calibration": "Калибрация", + "Show Plugins": "Покажи добавките", + "About": "Относно", + "Value in": "Стойност в", + "Carb Time": "Ядене след", + "Language": "Език", + "Add new": "Добави нов", + "g": "гр", + "ml": "мл", + "pcs": "бр", + "Drag&drop food here": "Хвани и премести храна тук", + "Care Portal": "Въвеждане на данни", + "Medium/Unknown": "Среден/неизвестен", + "IN THE FUTURE": "В БЪДЕШЕТО", + "Order": "Ред", + "oldest on top": "Старите най-отгоре", + "newest on top": "Новите най-отгоре", + "All sensor events": "Всички събития от сензора", + "Remove future items from mongo database": "Премахни бъдещите точки от Монго базата с данни", + "Find and remove treatments in the future": "Намери и премахни събития в бъдещето", + "This task find and remove treatments in the future.": "Тази опция намира и премахва събития в бъдещето.", + "Remove treatments in the future": "Премахни събитията в бъдешето", + "Find and remove entries in the future": "Намери и премахни данни от сензора в бъдещето", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Тази опция ще намери и премахне данни от сензора в бъдещето, създадени поради грешна дата/време.", + "Remove entries in the future": "Премахни данните от сензора в бъдещето", + "Loading database ...": "Зареждане на базата с данни ...", + "Database contains %1 future records": "Базата с дани съдържа %1 бъдещи записи", + "Remove %1 selected records?": "Премахване на %1 от избраните записи?", + "Error loading database": "Грешка при зареждане на базата с данни", + "Record %1 removed ...": "%1 записи премахнати", + "Error removing record %1": "Грешка при премахването на %1 от записите", + "Deleting records ...": "Изтриване на записите...", + "%1 records deleted": "%1 записите са изтрити", + "Clean Mongo status database": "Изчисти статуса на Монго базата с данни", + "Delete all documents from devicestatus collection": "Изтрий всички документи от папката статус-устройство", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Тази опция премахва всички документи от папката статус-устройство. Полезно е, когато статусът на батерията не се обновява.", + "Delete all documents": "Изтрий всички документи", + "Delete all documents from devicestatus collection?": "Изтриване на всички документи от папката статус-устройство?", + "Database contains %1 records": "Базата с данни съдържа %1 записи", + "All records removed ...": "Всички записи премахнати ...", + "Delete all documents from devicestatus collection older than 30 days": "Изтрий всички файлове от папка статус устройство по-стари от 30 дена", + "Number of Days to Keep:": "Колко дни да се съхранява:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Тази опция премахва всички документи от папка статус устройство, които са по-стари от 30 дена. Полезно е, когато статусът на батерията не се обновява. ", + "Delete old documents from devicestatus collection?": "Сигирен ли си, че искаш да изтриеш всички документи от папка статус-устройство? ", + "Clean Mongo entries (glucose entries) database": "Изчистване на данните с измервания на КЗ от базата данни (Монго)", + "Delete all documents from entries collection older than 180 days": "Изтрий всички файлове от папка статус устройство по-стари от 180 дена", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Тази опция премахва всички документи от папка статус устройство, които са по-стари от 180 дена. Полезно е, когато статусът на батерията не се обновява правилно. ", + "Delete old documents": "Изтрий старите документи", + "Delete old documents from entries collection?": "Сигирен ли си, че искаш да изтриеш старите документи с въведени стойности за КЗ?", + "%1 is not a valid number": "%1 е невалиден номер.", + "%1 is not a valid number - must be more than 2": "%1 е невалиден номер - трябва да е по-голям от 2", + "Clean Mongo treatments database": "Почисти леченията в Mongo база данни", + "Delete all documents from treatments collection older than 180 days": "Почисти записаните лечения по-стари от 180 дена", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Тази опция премахва всички лечения, които са по-стари от 180 дена. Полезно е, когато статусът на батерията не се обновява. ", + "Delete old documents from treatments collection?": "Изтрий стари документи от папката статус-устройство?", + "Admin Tools": "Настройки на администратора", + "Nightscout reporting": "Найтскаут статистика", + "Cancel": "Откажи", + "Edit treatment": "Редакция на събитие", + "Duration": "Времетраене", + "Duration in minutes": "Времетраене в мин.", + "Temp Basal": "Временен базал", + "Temp Basal Start": "Начало на временен базал", + "Temp Basal End": "Край на временен базал", + "Percent": "Процент", + "Basal change in %": "Промяна на базала с %", + "Basal value": "Временен базал", + "Absolute basal value": "Базална стойност", + "Announcement": "Известяване", + "Loading temp basal data": "Зареждане на данни за временния базал", + "Save current record before changing to new?": "Запази текущият запис преди да промениш новия ", + "Profile Switch": "Смяна на профил", + "Profile": "Профил", + "General profile settings": "Основни настройки на профила", + "Title": "Заглавие", + "Database records": "Записи в базата с данни", + "Add new record": "Добави нов запис", + "Remove this record": "Премахни този запис", + "Clone this record to new": "Копирай този запис като нов", + "Record valid from": "Записът е валиден от ", + "Stored profiles": "Запаметени профили", + "Timezone": "Часова зона", + "Duration of Insulin Activity (DIA)": "Продължителност на инсулиновата активност DIA", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Представя типичната продължителност на действието на инсулина. Варира между отделните пациенти и различни инсулини. Обикновено е 3-4 часа за пациентите с помпа. Нарича се още живот на инсулина ", + "Insulin to carb ratio (I:C)": "Съотношение инсулин/въглехидратите ICR I:C И:ВХ", + "Hours:": "часове:", + "hours": "часове", + "g/hour": "гр/час", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "грам въглехидрат към 1 единица инсулин. Съотношението колко грама въглехидрат се покриват от 1 единица инсулин.", + "Insulin Sensitivity Factor (ISF)": "Фактор на инсулинова чувствителност ISF ", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "мг/дл или ммол към 1 единица инсулин. Съотношението как се променя кръвната захар със всяка единица инсулинова корекция", + "Carbs activity / absorption rate": "Активност на въглехидратите / време за абсорбиране", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "грам за единица време. Представлява както промяната в COB за единица време, така и количеството ВХ които биха се усвоили за това време.", + "Basal rates [unit/hour]": "Базална стойност [единица/час]", + "Target BG range [mg/dL,mmol/L]": "Целеви диапазон на КЗ [мг/дл , ммол]", + "Start of record validity": "Начало на записа", + "Icicle": "Висящ", + "Render Basal": "Базал", + "Profile used": "Използван профил", + "Calculation is in target range.": "Калкулацията е в граници", + "Loading profile records ...": "Зареждане на профили", + "Values loaded.": "Стойностите за заредени.", + "Default values used.": "Стойностите по подразбиране са използвани.", + "Error. Default values used.": "Грешка. Стойностите по подразбиране са използвани.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Времевите интервали за долна граница на кз и горна граница на кз не съвпадат. Стойностите са възстановени по подразбиране.", + "Valid from:": "Валиден от", + "Save current record before switching to new?": "Запазване текущият запис преди превключване на нов?", + "Add new interval before": "Добави интервал преди", + "Delete interval": "Изтрий интервал", + "I:C": "И:ВХ", + "ISF": "Инсулинова чувствителност", + "Combo Bolus": "Двоен болус", + "Difference": "Разлика", + "New time": "Ново време", + "Edit Mode": "Редактиране", + "When enabled icon to start edit mode is visible": "Когато е активно ,иконката за редактиране ще се вижда", + "Operation": "Операция", + "Move": "Премести", + "Delete": "Изтрий", + "Move insulin": "Премести инсулин", + "Move carbs": "Премести ВХ", + "Remove insulin": "Изтрий инсулин", + "Remove carbs": "Изтрий ВХ", + "Change treatment time to %1 ?": "Да променя ли времето на събитието с %1?", + "Change carbs time to %1 ?": "Да променя ли времето на ВХ с %1?", + "Change insulin time to %1 ?": "Да променя ли времето на инсулина с %1?", + "Remove treatment ?": "Изтрий събитието", + "Remove insulin from treatment ?": "Да изтрия ли инсулина от събитието?", + "Remove carbs from treatment ?": "Да изтрия ли ВХ от събитието?", + "Rendering": "Показване на графика", + "Loading OpenAPS data of": "Зареждане на OpenAPS данни от", + "Loading profile switch data": "Зареждане на данни от сменения профил", + "Redirecting you to the Profile Editor to create a new profile.": "Грешни настройки на профила. \nНяма определен профил към избраното време. \nПрепращане към редактора на профила, за създаване на нов профил.", + "Pump": "Помпа", + "Sensor Age": "Възраст на сензора (ВС)", + "Insulin Age": "Възраст на инсулина (ВИ)", + "Temporary target": "Временна граница", + "Reason": "Причина", + "Eating soon": "Ядене скоро", + "Top": "Горе", + "Bottom": "Долу", + "Activity": "Активност", + "Targets": "Граници", + "Bolus insulin:": "Болус инсулин", + "Base basal insulin:": "Основен базален инсулин", + "Positive temp basal insulin:": "Положителен временен базален инсулин", + "Negative temp basal insulin:": "Отрицателен временен базален инсулин", + "Total basal insulin:": "Общо базален инсулин", + "Total daily insulin:": "Общо инсулин за деня", + "Unable to save Role": "Неуспешно запазване на Роля", + "Unable to delete Role": "Невъзможно изтриването на Роля", + "Database contains %1 roles": "Базата данни съдържа %1 роли", + "Edit Role": "Промени Роля", + "admin, school, family, etc": "администратор,училище,семейство и т.н.", + "Permissions": "Права", + "Are you sure you want to delete: ": "Потвърдете изтриването", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Всяка роля ще има 1 или повече права. В * правото е маска, правата са йерархия използвайки : като разделител", + "Add new Role": "Добавете нова роля", + "Roles - Groups of People, Devices, etc": "Роли - Група хора,устройства,т.н.", + "Edit this role": "Промени тази роля", + "Admin authorized": "Оторизиран като администратор", + "Subjects - People, Devices, etc": "Субекти - Хора,Устройства,т.н.", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Всеки обект ще има уникален ключ за достъп и 1 или повече роли. Кликнете върху ключа за достъп, за да отворите нов изглед с избрания обект, тази секретна връзка може след това да се споделя", + "Add new Subject": "Добави нов субект", + "Unable to save Subject": "Неуспешно запазване на Субект", + "Unable to delete Subject": "Невъзможно изтриването на субекта", + "Database contains %1 subjects": "Базата данни съдържа %1 субекти", + "Edit Subject": "Промени субект", + "person, device, etc": "човек,устройство,т.н.", + "role1, role2": "Роля1, Роля2", + "Edit this subject": "Промени този субект", + "Delete this subject": "Изтрий този субект", + "Roles": "Роли", + "Access Token": "Ключ за достъп", + "hour ago": "Преди час", + "hours ago": "Преди часове", + "Silence for %1 minutes": "Заглушаване за %1 минути", + "Check BG": "Проверка КЗ", + "BASAL": "Базал", + "Current basal": "Актуален базал", + "Sensitivity": "Инсулинова чувствителност (ISF)", + "Current Carb Ratio": "Актуално Въглехидратно Съотношение", + "Basal timezone": "Базална часова зона", + "Active profile": "Активен профил", + "Active temp basal": "Активен временен базал", + "Active temp basal start": "Старт на активен временен базал", + "Active temp basal duration": "Продължителност на Активен временен базал", + "Active temp basal remaining": "Оставащ Активен временен базал", + "Basal profile value": "Базален профил стойност", + "Active combo bolus": "Активен комбиниран болус", + "Active combo bolus start": "Активен комбиниран болус", + "Active combo bolus duration": "Продължителност на активния комбиниран болус", + "Active combo bolus remaining": "Оставащ активен комбиниран болус", + "BG Delta": "Дельта ГК", + "Elapsed Time": "Изминало време", + "Absolute Delta": "Абсолютно изменение", + "Interpolated": "Интерполирано", + "BWP": "БП", + "Urgent": "Спешно", + "Warning": "Предупреждение", + "Info": "Информация", + "Lowest": "Най-ниско", + "Snoozing high alarm since there is enough IOB": "Изключване на аларма за висока КЗ, тъй като има достатъчно IOB", + "Check BG, time to bolus?": "Провери КЗ, не е ли време за болус?", + "Notice": "Известие", + "required info missing": "Липсва необходима информация", + "Insulin on Board": "Активен Инсулин (IOB)", + "Current target": "Настояща целева КЗ", + "Expected effect": "Очакван ефект", + "Expected outcome": "Очакван резултат", + "Carb Equivalent": "Равностойност във ВХ", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Излишният инсулин %1U е повече от необходимия за достигане до долната граница, ВХ не се вземат под внимание", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Излишният инсулин %1U е повече от необходимия за достигане до долната граница, ПРОВЕРИ ДАЛИ IOB СЕ ПОКРИВА ОТ ВЪГЛЕХИДРАТИТЕ", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U намаляне е необходимо в активния инсулин до достигане до долната граница, прекалено висок базал?", + "basal adjustment out of range, give carbs?": "Корекция на базала не е възможна, добавка на въглехидрати? ", + "basal adjustment out of range, give bolus?": "Корекция на базала не е възможна, добавка на болус? ", + "above high": "над горната", + "below low": "под долната", + "Projected BG %1 target": "Предполагаемата КЗ %1 в граници", + "aiming at": "цел към", + "Bolus %1 units": "Болус %1 единици", + "or adjust basal": "или корекция на базала", + "Check BG using glucometer before correcting!": "Провери КЗ с глюкомер, преди кореция!", + "Basal reduction to account %1 units:": "Намаляне на базала с %1 единици", + "30m temp basal": "30м временен базал", + "1h temp basal": "1 час временен базал", + "Cannula change overdue!": "Времето за смяна на сет просрочено", + "Time to change cannula": "Време за смяна на сет", + "Change cannula soon": "Смени сета скоро", + "Cannula age %1 hours": "Сетът е на %1 часове", + "Inserted": "Поставен", + "CAGE": "ВС", + "COB": "АВХ", + "Last Carbs": "Последни ВХ", + "IAGE": "ИнсСрок", + "Insulin reservoir change overdue!": "Смянатата на резервоара просрочена", + "Time to change insulin reservoir": "Време е за смяна на резервоара", + "Change insulin reservoir soon": "Смени резервоара скоро", + "Insulin reservoir age %1 hours": "Резервоарът е на %1 часа", + "Changed": "Сменен", + "IOB": "АИ", + "Careportal IOB": "АИ от Кеърпортал", + "Last Bolus": "Последен болус", + "Basal IOB": "Базален АИ", + "Source": "Източник", + "Stale data, check rig?": "Стари данни, провери телефона", + "Last received:": "Последно получени", + "%1m ago": "преди %1 мин.", + "%1h ago": "преди %1 час", + "%1d ago": "преди %1 ден", + "RETRO": "РЕТРО", + "SAGE": "ВС", + "Sensor change/restart overdue!": "Смяната/рестартът на сензора са пресрочени", + "Time to change/restart sensor": "Време за смяна/рестарт на сензора", + "Change/restart sensor soon": "Смени/рестартирай сензора скоро", + "Sensor age %1 days %2 hours": "Сензорът е на %1 дни %2 часа ", + "Sensor Insert": "Поставяне на сензора", + "Sensor Start": "Стартиране на сензора", + "days": "дни", + "Insulin distribution": "разпределение на инсулина", + "To see this report, press SHOW while in this view": "За да видите тази статистика, натиснете ПОКАЖИ", + "AR2 Forecast": "AR2 прогнози", + "OpenAPS Forecasts": "OpenAPS прогнози", + "Temporary Target": "временна цел", + "Temporary Target Cancel": "Отмяна на временна цел", + "OpenAPS Offline": "OpenAPS спрян", + "Profiles": "Профили", + "Time in fluctuation": "Време в промяна", + "Time in rapid fluctuation": "Време в бърза промяна", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Това е само грубо изчисление, което може да е много неточно и не изключва реалния кръвен тест. Формулата, кокято е използвана е взета от:", + "Filter by hours": "Филтър по часове", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Време в промяна и време в бърза промяна измерват % от време в разгледания период, през който КЗ са се променяли бързо или много бързо. По-ниски стойности са по-добри.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Средната дневна промяна е сумата на всички промени в стойностите на КЗ за разгледания период, разделена на броя дни в периода. По-ниската стойност е по-добра", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Средната промяна за час е сумата на всички промени в стойностите на КЗ за разгледания период, разделена на броя часове в периода. По-ниската стойност е по-добра", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Този параметър се изчислява от квадрата на растоянието на всички стойности на КЗ извън нормата, сумирането им, разделяне на броят им и прилагане на корен квадрат, Този измерител е подобен на време в границите (TiR ) , но тук стойностите с голямо отдалечаване са с по-голяма тежест. По-ниските стойности на този показател са по-добри.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">тук.", + "Mean Total Daily Change": "Средна промяна за ден", + "Mean Hourly Change": "Средна промяна за час", + "FortyFiveDown": "лек спад", + "FortyFiveUp": "леко покачване", + "Flat": "равна", + "SingleUp": "покачване", + "SingleDown": "спад", + "DoubleDown": "бързо спадане", + "DoubleUp": "бързо покачване", + "virtAsstUnknown": "Тази стойност е неизвестна в момента. Моля вижте вашият Nightscout сайт за повече подробности.", + "virtAsstTitleAR2Forecast": "AR2 прогноза", + "virtAsstTitleCurrentBasal": "Текущ базал", + "virtAsstTitleCurrentCOB": "Активни ВХ", + "virtAsstTitleCurrentIOB": "Активен инсулин", + "virtAsstTitleLaunch": "Добре дошли в Nightscout", + "virtAsstTitleLoopForecast": "Loop прогноза", + "virtAsstTitleLastLoop": "Последен Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS прогнозa", + "virtAsstTitlePumpReservoir": "Оставащ инсулин", + "virtAsstTitlePumpBattery": "Батерия на помпата", + "virtAsstTitleRawBG": "Текуща КЗ (нефилтр)", + "virtAsstTitleUploaderBattery": "Батерия на качващия тел", + "virtAsstTitleCurrentBG": "Текуща КЗ", + "virtAsstTitleFullStatus": "Пълен статус", + "virtAsstTitleCGMMode": "CGM режим", + "virtAsstTitleCGMStatus": "CGM статус", + "virtAsstTitleCGMSessionAge": "Възраст на сензора", + "virtAsstTitleCGMTxStatus": "Статус на трансмитер", + "virtAsstTitleCGMTxAge": "Възраст на трансмитер", + "virtAsstTitleCGMNoise": "Шум", + "virtAsstTitleDelta": "Делта на КЗ", + "virtAsstStatus": "%1 и %2 от %3.\n", + "virtAsstBasal": "%1 současný bazál je %2 jednotek za hodinu", + "virtAsstBasalTemp": "%1 dočasný bazál %2 jednotek za hodinu skončí %3", + "virtAsstIob": "a máte %1 jednotek aktivního inzulínu.", + "virtAsstIobIntent": "Máte %1 jednotek aktivního inzulínu", + "virtAsstIobUnits": "%1 единици от\n", + "virtAsstLaunch": "Какво искате да проверите в Nightscout?", + "virtAsstPreamble": "Вашият", + "virtAsstPreamble3person": "%1 има", + "virtAsstNoInsulin": "без", + "virtAsstUploadBattery": "Батерията на качващия телефон е %1", + "virtAsstReservoir": "Имате оставащи %1 единици", + "virtAsstPumpBattery": "Батерията на помпата е на %1 %2", + "virtAsstUploaderBattery": "Батерията на качващия телефон е %1", + "virtAsstLastLoop": "Последният успешен цикъл беше в %1", + "virtAsstLoopNotAvailable": "Този плъгин изглежда не е активиран", + "virtAsstLoopForecastAround": "Според loop прогнозата ще сте около %1 през следващите %2", + "virtAsstLoopForecastBetween": "Според loop прогнозата ще сте между %1 и %2 през следващите %3", + "virtAsstAR2ForecastAround": "Според AR2 прогнозата ще сте около %1 през следващите %2", + "virtAsstAR2ForecastBetween": "Според АР2 прогнозата ще сте между %1 и %2 през следващите %3", + "virtAsstForecastUnavailable": "Невъзможо е прогнозиране с тези данни", + "virtAsstRawBG": "Чистата КЗ е %1", + "virtAsstOpenAPSForecast": "Прогнозираната захар според OpenAPS е %1", + "virtAsstCob3person": "%1 има %2 активни въглехидрати", + "virtAsstCob": "Имате %1 активни въглехидрати", + "virtAsstCGMMode": "Режим на сензора е %1 от %2", + "virtAsstCGMStatus": "Статус на сензора беше %1 от %2", + "virtAsstCGMSessAge": "Сесията на сензора е активна от %1 дни и %2 часа.", + "virtAsstCGMSessNotStarted": "Няма активен сензор в момента.", + "virtAsstCGMTxStatus": "Статус на трансмитера беше %1 от %2", + "virtAsstCGMTxAge": "Трансмитерът ви е на %1 дни.", + "virtAsstCGMNoise": "Шум на сензора беше %1 в %2", + "virtAsstCGMBattOne": "Батерията беше %1 волта в %2", + "virtAsstCGMBattTwo": "Батерийте са %1 волта и %2 волта в %3.", + "virtAsstDelta": "Делтата беше %1 между %2 и %3.", + "virtAsstDeltaEstimated": "Очакваната делта беше %1 между %2 и %3.", + "virtAsstUnknownIntentTitle": "Неуспешен опит", + "virtAsstUnknownIntentText": "Съжалявам, не разбирам за какво питате.", + "Fat [g]": "Мазнини [гр]", + "Protein [g]": "Протеини [гр]", + "Energy [kJ]": "Енергия [kJ]", + "Clock Views:": "Часовник изглед:", + "Clock": "Часовник", + "Color": "Цвят", + "Simple": "Прост", + "TDD average": "Обща дневна доза средно", + "Bolus average": "Среден болус", + "Basal average": "Среден базал", + "Base basal average:": "Среден базов базал:", + "Carbs average": "Въглехидрати средно", + "Eating Soon": "Преди хранене", + "Last entry {0} minutes ago": "Последен запис преди {0} минути", + "change": "промяна", + "Speech": "Глас", + "Target Top": "Горна граница", + "Target Bottom": "Долна граница", + "Canceled": "Отказан", + "Meter BG": "Измерена КЗ", + "predicted": "прогнозна", + "future": "бъдеще", + "ago": "преди", + "Last data received": "Последни данни преди", + "Clock View": "Изглед часовник", + "Protein": "Протеини", + "Fat": "Мазнини", + "Protein average": "Протеини средно", + "Fat average": "Мазнини средно", + "Total carbs": "Общо въглехидрати", + "Total protein": "Общо протеини", + "Total fat": "Общо мазнини", + "Database Size": "Големина на базата данни", + "Database Size near its limits!": "Базата данни е близо до лимита!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Размерът на базата е %1 Мб от %2 Мб. Моля да я архивирате и почистите!", + "Database file size": "Файл на БД", + "%1 MiB of %2 MiB (%3%)": "%1 Мб от %2 Мб (%3%)", + "Data size": "Размер на данни", + "virtAsstDatabaseSize": "%1 Мб. Това са %2% от свободното пространство.", + "virtAsstTitleDatabaseSize": "Файл на БД", + "Carbs/Food/Time": "Въгл/Храна/Време", + "You have administration messages": "Имате съобшение от администратор", + "Admin messages in queue": "Съобшения от админ в опашката", + "Queue empty": "Опашката е празна", + "There are no admin messages in queue": "Няма съобщения от админ в опашката", + "Please sign in using the API_SECRET to see your administration messages": "Логнете се с API_SECRET за да видите съобщенията от администратор", + "Reads enabled in default permissions": "Четенето позволено в правата по поодразбиране", + "Data reads enabled": "Четенето на данни позволено", + "Data writes enabled": "Записа на данни позволено", + "Data writes not enabled": "Записа на данни рабранен", + "Color prediction lines": "Линии на евентуална КЗ", + "Release Notes": "Бележки към тази версия", + "Check for Updates": "Проверка за нови версии", + "Open Source": "Отворен код", + "Nightscout Info": "Nightscout инфо", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Основната цел на това е да визуализира колко успешно се справя системата Loop. Може да работи и с други системи, независимо отворени или затворени. В зависимост от ползвания софтуер може да има дупки в графиката или тя да е напълно празна. Винаги преглеждайте за цялост на графиката. Стартирайте с период от един ден и плавно увеличавайте,", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer вклюва отместване на времето. Например ако един ден закусвате в 7:00 и в 8:00 на следващия то кривата на средната КЗ за тези 2 дни ще бъде гладка и няма да показва адекватната реакция след закуска. Превъртането на времето ще изчисли следната времева стойност на тези харанения и ще премести отмести всички данни ( въгл, инсулин, базал и др ) от двата дни в тази зона, така и двете хранения ще съвпадат със средното време.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "В този пример всички данни от първия ден са изместени 30 минути напред, а тези от втория 30 минути назад във времето така че все едно сте закусвали в 7:30 в двата дни. Това ви показва средната КЗ в следствие на храненето.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Отместването оцветява периода след средното време на старт на хранене в сиво, за ПИД( Продължителност на Инсулинвото Действие ). Понеже всички данни за деня са отместени, кривите извън сивата зона може да не са акуратни. ", + "Note that time shift is available only when viewing multiple days.": "Отместването е възможно само когато преглеждате няколко дни.", + "Please select a maximum of two weeks duration and click Show again.": "Моля изберете максимално 2 седмици и натиснете Покажи отново.", + "Show profiles table": "Покажи таблицата с профили", + "Show predictions": "Покажи прогнози", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Времево изместване на хранения, по-големи от %1 гр въглехидрати, консумирани между %2 и %3", + "Previous": "Предишен", + "Previous day": "Преден ден", + "Next day": "Следващ ден", + "Next": "Напред", + "Temp basal delta": "Промяна на врем базал", + "Authorized by token": "Ауторизиран чрез токен", + "Auth role": "Авторизационна роля", + "view without token": "виж без токен", + "Remove stored token": "Премахни токен", + "Weekly Distribution": "Седмично разпределение", + "Failed authentication": "Неуспешна ауторизация", + "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?": "Устр с IP адрес %1 опита да се ауторизира в Nightscout с грешни парола или токен. Проверете в софтуера си дали не е сгрешен вашия API_SECRET или токен?", + "Default (with leading zero and U)": "Стандартен ( с водеща нула и Е)", + "Concise (with U, without leading zero)": "Кратък( с Е, без водеща нула)", + "Minimal (without leading zero and U)": "Минимален (без водеща нула и Е)", + "Small Bolus Display": "Малък болусен бутон", + "Large Bolus Display": "Голям болусен бутон", + "Bolus Display Threshold": "Отстояние на болусния бутон", + "%1 U and Over": "%1 Е и повече", + "Event repeated %1 times.": "Събитието се повтаря %1 пъти.", + "minutes": "минути", + "Last recorded %1 %2 ago.": "Последно записано преди %1 %2.", + "Security issue": "Проблем със сигурността", + "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.": "Открит е слаб API_SECRET. Моля, използвайте комбинация от малки и ГЛАВНИ букви, цифри и небуквено-цифрови знаци като !#%&/, за да намалите риска от неоторизиран достъп. Минималната дължина на API_SECRET е 12 знака.", + "less than 1": "по-малко от 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Паролите на MongoDB и API_SECRET съвпадат. Това не е добра идея. Моля, променете и двете и не използвайте повторно пароли в системата." +} diff --git a/translations/cs_CZ.json b/translations/cs_CZ.json new file mode 100644 index 00000000000..2ebf56821e1 --- /dev/null +++ b/translations/cs_CZ.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Naslouchám na portu", + "Mo": "Po", + "Tu": "Út", + "We": "St", + "Th": "Čt", + "Fr": "Pá", + "Sa": "So", + "Su": "Ne", + "Monday": "Pondělí", + "Tuesday": "Úterý", + "Wednesday": "Středa", + "Thursday": "Čtvrtek", + "Friday": "Pátek", + "Saturday": "Sobota", + "Sunday": "Neděle", + "Category": "Kategorie", + "Subcategory": "Podkategorie", + "Name": "Jméno", + "Today": "Dnes", + "Last 2 days": "Poslední 2 dny", + "Last 3 days": "Poslední 3 dny", + "Last week": "Poslední týden", + "Last 2 weeks": "Poslední 2 týdny", + "Last month": "Poslední měsíc", + "Last 3 months": "Poslední 3 měsíce", + "From": "Od", + "To": "Do", + "Notes": "Poznámky", + "Food": "Jídlo", + "Insulin": "Inzulín", + "Carbs": "Sacharidy", + "Notes contain": "Poznámky obsahují", + "Target BG range bottom": "Cílová glykémie dolní", + "top": "horní", + "Show": "Zobrazit", + "Display": "Zobraz", + "Loading": "Nahrávám", + "Loading profile": "Nahrávám profil", + "Loading status": "Nahrávám status", + "Loading food database": "Nahrávám databázi jídel", + "not displayed": "není zobrazeno", + "Loading CGM data of": "Nahrávám CGM data", + "Loading treatments data of": "Nahrávám data ošetření", + "Processing data of": "Zpracovávám data", + "Portion": "Porce", + "Size": "Velikost", + "(none)": "(Žádný)", + "None": "Žádný", + "": "<Žádný>", + "Result is empty": "Prázdný výsledek", + "Day to day": "Den po dni", + "Week to week": "Týden po týdnu", + "Daily Stats": "Denní statistiky", + "Percentile Chart": "Percentil", + "Distribution": "Rozložení", + "Hourly stats": "Statistika po hodinách", + "netIOB stats": "statistiky netIOB", + "temp basals must be rendered to display this report": "pro zobrazení reportu musí být vykresleny dočasné bazály", + "No data available": "Žádná dostupná data", + "Low": "Nízká", + "In Range": "V rozsahu", + "Period": "Období", + "High": "Vysoká", + "Average": "Průměr", + "Low Quartile": "Nízký kvartil", + "Upper Quartile": "Vysoký kvartil", + "Quartile": "Kvartil", + "Date": "Datum", + "Normal": "Normální", + "Median": "Medián", + "Readings": "Záznamů", + "StDev": "Směrodatná odchylka", + "Daily stats report": "Denní statistiky", + "Glucose Percentile report": "Tabulka percentilu glykémií", + "Glucose distribution": "Rozložení glykémií", + "days total": "dní celkem", + "Total per day": "Celkem za den", + "Overall": "Celkem", + "Range": "Rozsah", + "% of Readings": "% záznamů", + "# of Readings": "počet záznamů", + "Mean": "Střední hodnota", + "Standard Deviation": "Standardní odchylka", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "Předpokládané HBA1c*", + "Weekly Success": "Týdenní úspěšnost", + "There is not sufficient data to run this report. Select more days.": "Není dostatek dat. Vyberte delší časové období.", + "Using stored API secret hash": "Používám uložený hash API hesla", + "No API secret hash stored yet. You need to enter API secret.": "Není uložený žádný hash API hesla. Musíte zadat API heslo.", + "Database loaded": "Databáze načtena", + "Error: Database failed to load": "Chyba při načítání databáze", + "Error": "Chyba", + "Create new record": "Vytvořit nový záznam", + "Save record": "Uložit záznam", + "Portions": "Porce", + "Unit": "Jedn", + "GI": "GI", + "Edit record": "Upravit záznam", + "Delete record": "Smazat záznam", + "Move to the top": "Přesuň na začátek", + "Hidden": "Skrytý", + "Hide after use": "Skryj po použití", + "Your API secret must be at least 12 characters long": "Vaše API heslo musí mít alespoň 12 znaků", + "Bad API secret": "Chybné API heslo", + "API secret hash stored": "Hash API hesla uložen", + "Status": "Stav", + "Not loaded": "Nenačtený", + "Food Editor": "Editor jídel", + "Your database": "Vaše databáze", + "Filter": "Filtr", + "Save": "Ulož", + "Clear": "Vymaž", + "Record": "Záznam", + "Quick picks": "Rychlý výběr", + "Show hidden": "Zobraz skryté", + "Your API secret or token": "Vaše API heslo nebo token", + "Remember this device. (Do not enable this on public computers.)": "Pamatovat si toto zařízení. (nepovolujte na veřejných počítačích.)", + "Treatments": "Ošetření", + "Time": "Čas", + "Event Type": "Typ události", + "Blood Glucose": "Glykémie", + "Entered By": "Zadal", + "Delete this treatment?": "Vymazat toto ošetření?", + "Carbs Given": "Sacharidů", + "Insulin Given": "Inzulín", + "Event Time": "Čas události", + "Please verify that the data entered is correct": "Prosím zkontrolujte, zda jsou údaje zadány správně", + "BG": "Glykémie", + "Use BG correction in calculation": "Použij korekci na glykémii", + "BG from CGM (autoupdated)": "Glykémie z CGM (automaticky aktualizovaná)", + "BG from meter": "Glykémie z glukoměru", + "Manual BG": "Ručně zadaná glykémie", + "Quickpick": "Rychlý výběr", + "or": "nebo", + "Add from database": "Přidat z databáze", + "Use carbs correction in calculation": "Použij korekci na sacharidy", + "Use COB correction in calculation": "Použij korekci na COB", + "Use IOB in calculation": "Použij IOB ve výpočtu", + "Other correction": "Jiná korekce", + "Rounding": "Zaokrouhlení", + "Enter insulin correction in treatment": "Zahrň inzulín do záznamu ošetření", + "Insulin needed": "Potřebný inzulín", + "Carbs needed": "Potřebné sach", + "Carbs needed if Insulin total is negative value": "Chybějící sacharidy v případě, že výsledek je záporný", + "Basal rate": "Bazál", + "60 minutes earlier": "60 min předem", + "45 minutes earlier": "45 min předem", + "30 minutes earlier": "30 min předem", + "20 minutes earlier": "20 min předem", + "15 minutes earlier": "15 min předem", + "Time in minutes": "Čas v minutách", + "15 minutes later": "15 min po", + "20 minutes later": "20 min po", + "30 minutes later": "30 min po", + "45 minutes later": "45 min po", + "60 minutes later": "60 min po", + "Additional Notes, Comments": "Dalši poznámky, komentáře", + "RETRO MODE": "V MINULOSTI", + "Now": "Nyní", + "Other": "Jiný", + "Submit Form": "Odeslat formulář", + "Profile Editor": "Editor profilu", + "Reports": "Výkazy", + "Add food from your database": "Přidat jidlo z Vaší databáze", + "Reload database": "Znovu nahraj databázi", + "Add": "Přidej", + "Unauthorized": "Neautorizováno", + "Entering record failed": "Vložení záznamu selhalo", + "Device authenticated": "Zařízení ověřeno", + "Device not authenticated": "Zařízení není ověřeno", + "Authentication status": "Stav ověření", + "Authenticate": "Ověřit", + "Remove": "Vymazat", + "Your device is not authenticated yet": "Toto zařízení nebylo dosud ověřeno", + "Sensor": "Senzor", + "Finger": "Glukoměr", + "Manual": "Ručně", + "Scale": "Měřítko", + "Linear": "Lineární", + "Logarithmic": "Logaritmické", + "Logarithmic (Dynamic)": "Logaritmické (Dynamické)", + "Insulin-on-Board": "Aktivní inzulín", + "Carbs-on-Board": "Aktivní sacharidy", + "Bolus Wizard Preview": "BWP-Náhled bolusového kalk.", + "Value Loaded": "Hodnoty načteny", + "Cannula Age": "Stáří kanyly", + "Basal Profile": "Bazál", + "Silence for 30 minutes": "Ztlumit na 30 minut", + "Silence for 60 minutes": "Ztlumit na 60 minut", + "Silence for 90 minutes": "Ztlumit na 90 minut", + "Silence for 120 minutes": "Ztlumit na 120 minut", + "Settings": "Nastavení", + "Units": "Jednotky", + "Date format": "Formát datumu", + "12 hours": "12 hodin", + "24 hours": "24 hodin", + "Log a Treatment": "Záznam ošetření", + "BG Check": "Kontrola glykémie", + "Meal Bolus": "Bolus na jídlo", + "Snack Bolus": "Bolus na svačinu", + "Correction Bolus": "Bolus na glykémii", + "Carb Correction": "Přídavek sacharidů", + "Note": "Poznámka", + "Question": "Otázka", + "Exercise": "Cvičení", + "Pump Site Change": "Výměna setu", + "CGM Sensor Start": "Spuštění senzoru", + "CGM Sensor Stop": "Zastavení senzoru", + "CGM Sensor Insert": "Výměna senzoru", + "Sensor Code": "Kód senzoru", + "Transmitter ID": "ID vysílače", + "Dexcom Sensor Start": "Spuštění senzoru Dexcom", + "Dexcom Sensor Change": "Výměna senzoru Dexcom", + "Insulin Cartridge Change": "Výměna inzulínu", + "D.A.D. Alert": "Hlášení psa", + "Glucose Reading": "Hodnota glykémie", + "Measurement Method": "Metoda měření", + "Meter": "Glukoměr", + "Amount in grams": "Množství v gramech", + "Amount in units": "Množství v jednotkách", + "View all treatments": "Zobraz všechny ošetření", + "Enable Alarms": "Povolit alarmy", + "Pump Battery Change": "Výměna baterie pumpy", + "Pump Battery Age": "Stáří baterie v pumpě", + "Pump Battery Low Alarm": "Upozornění na nízký stav baterie pumpy", + "Pump Battery change overdue!": "Překročen čas pro výměnu baterie!", + "When enabled an alarm may sound.": "Při povoleném alarmu zní zvuk.", + "Urgent High Alarm": "Urgentní vysoká glykémie", + "High Alarm": "Vysoká glykémie", + "Low Alarm": "Nízká glykémie", + "Urgent Low Alarm": "Urgentní nízká glykémie", + "Stale Data: Warn": "Zastaralá data", + "Stale Data: Urgent": "Zastaralá data urgentní", + "mins": "min", + "Night Mode": "Noční mód", + "When enabled the page will be dimmed from 10pm - 6am.": "Když je povoleno, obrazovka je ztlumena 22:00 - 6:00.", + "Enable": "Povoleno", + "Show Raw BG Data": "Zobraz RAW data", + "Never": "Nikdy", + "Always": "Vždy", + "When there is noise": "Při šumu", + "When enabled small white dots will be displayed for raw BG data": "Když je povoleno, malé tečky budou zobrazeny pro RAW data", + "Custom Title": "Vlastní název stránky", + "Theme": "Téma", + "Default": "Výchozí", + "Colors": "Barevné", + "Colorblind-friendly colors": "Pro barvoslepé", + "Reset, and use defaults": "Vymaž a nastav výchozí hodnoty", + "Calibrations": "Kalibrace", + "Alarm Test / Smartphone Enable": "Test alarmu", + "Bolus Wizard": "Bolusový kalkulátor", + "in the future": "v budoucnosti", + "time ago": "min zpět", + "hr ago": "hod zpět", + "hrs ago": "hod zpět", + "min ago": "min zpět", + "mins ago": "min zpět", + "day ago": "den zpět", + "days ago": "dnů zpět", + "long ago": "dlouho zpět", + "Clean": "Čistý", + "Light": "Lehký", + "Medium": "Střední", + "Heavy": "Velký", + "Treatment type": "Typ ošetření", + "Raw BG": "Glykémie z RAW dat", + "Device": "Zařízení", + "Noise": "Šum", + "Calibration": "Kalibrace", + "Show Plugins": "Zobrazuj pluginy", + "About": "O aplikaci", + "Value in": "Hodnota v", + "Carb Time": "Čas jídla", + "Language": "Jazyk", + "Add new": "Přidat nový", + "g": "g", + "ml": "ml", + "pcs": "ks", + "Drag&drop food here": "Sem táhni & pusť jídlo", + "Care Portal": "Portál ošetření", + "Medium/Unknown": "Střední/Neznámá", + "IN THE FUTURE": "V BUDOUCNOSTI", + "Order": "Pořadí", + "oldest on top": "nejstarší nahoře", + "newest on top": "nejnovější nahoře", + "All sensor events": "Všechny události sensoru", + "Remove future items from mongo database": "Odebrání položek v budoucnosti z Mongo databáze", + "Find and remove treatments in the future": "Najít a odstranit záznamy ošetření v budoucnosti", + "This task find and remove treatments in the future.": "Tento úkol najde a odstraní ošetření v budoucnosti.", + "Remove treatments in the future": "Odstraň ošetření v budoucnosti", + "Find and remove entries in the future": "Najít a odstranit CGM data v budoucnosti", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Tento úkol najde a odstraní CGM data v budoucnosti vzniklé špatně nastaveným datem v uploaderu.", + "Remove entries in the future": "Odstraň CGM data v budoucnosti", + "Loading database ...": "Nahrávám databázi ...", + "Database contains %1 future records": "Databáze obsahuje %1 záznamů v budoucnosti", + "Remove %1 selected records?": "Odstranit %1 vybraných záznamů?", + "Error loading database": "Chyba při nahrávání databáze", + "Record %1 removed ...": "Záznam %1 odstraněn ...", + "Error removing record %1": "Chyba při odstaňování záznamu %1", + "Deleting records ...": "Odstraňování záznamů ...", + "%1 records deleted": "%1 záznamů odstraněno", + "Clean Mongo status database": "Vyčištění Mongo databáze statusů", + "Delete all documents from devicestatus collection": "Odstranění všech záznamů z kolekce devicestatus", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Tento úkol odstraní všechny dokumenty z kolekce devicestatus. Je to vhodné udělat, pokud se ukazatel stavu baterie neobnovuje správně.", + "Delete all documents": "Odstranit všechny dokumenty", + "Delete all documents from devicestatus collection?": "Odstranit všechny dokumenty z kolekce devicestatus?", + "Database contains %1 records": "Databáze obsahuje %1 záznamů", + "All records removed ...": "Všechny záznamy odstraněny ...", + "Delete all documents from devicestatus collection older than 30 days": "Odstranit všechny dokumenty z tabulky devicestatus starší než 30 dní", + "Number of Days to Keep:": "Počet dní k zachovaní:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Tento úkol odstraní všechny záznamy z tabulky devicestatus. Je to vhodné udělat, pokud se ukazatel stavu baterie neukazuje správně.", + "Delete old documents from devicestatus collection?": "Odstranit všechny staré záznamy z tabulky devicestatus?", + "Clean Mongo entries (glucose entries) database": "Vymazat záznamy (glykémie) z Mongo databáze", + "Delete all documents from entries collection older than 180 days": "Odstranit z kolekce entries všechny záznamy starší než 180 dní", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Tato úloha odstraní záznamy z kolekce entries, které jsou starší než 180 dní. Je vhodné ji spustit, pokud máte problémy se zobrazováním stavu baterie uploaderu.", + "Delete old documents": "Smazat staré záznamy", + "Delete old documents from entries collection?": "Odstranit staré záznamy z kolekce entries?", + "%1 is not a valid number": "%1 není platné číslo", + "%1 is not a valid number - must be more than 2": "%1 není platné číslo - musí být větší než 2", + "Clean Mongo treatments database": "Vyčistit kolekci treatments v Mongo databázi", + "Delete all documents from treatments collection older than 180 days": "Odstranit z kolekce treatments všechny záznamy starší než 180 dní", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Tato úloha odstraní záznamy z kolekce treatments, které jsou starší než 180 dní. Je vhodné ji spustit, pokud máte problémy se zobrazováním stavu baterie uploaderu.", + "Delete old documents from treatments collection?": "Odstranit staré záznamy z kolekce treatments?", + "Admin Tools": "Nástroje pro správu", + "Nightscout reporting": "Nightscout - Výkazy", + "Cancel": "Zrušit", + "Edit treatment": "Upravit ošetření", + "Duration": "Doba trvání", + "Duration in minutes": "Doba trvání v minutách", + "Temp Basal": "Dočasný bazál", + "Temp Basal Start": "Dočasný bazál začátek", + "Temp Basal End": "Dočasný bazál konec", + "Percent": "Procenta", + "Basal change in %": "Změna bazálu v %", + "Basal value": "Hodnota bazálu", + "Absolute basal value": "Absolutní hodnota bazálu", + "Announcement": "Oznámení", + "Loading temp basal data": "Nahrávám dočasné bazály", + "Save current record before changing to new?": "Uložit současný záznam před změnou na nový?", + "Profile Switch": "Přepnutí profilu", + "Profile": "Profil", + "General profile settings": "Obecná nastavení profilu", + "Title": "Název", + "Database records": "Záznamy v databázi", + "Add new record": "Přidat nový záznam", + "Remove this record": "Vymazat tento záznam", + "Clone this record to new": "Zkopíruj tento záznam do nového", + "Record valid from": "Záznam platný od", + "Stored profiles": "Uložené profily", + "Timezone": "Časová zóna", + "Duration of Insulin Activity (DIA)": "Doba působnosti inzulínu (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Představuje typickou dobu, po kterou inzulín působí. Bývá různá podle pacienta a inzulínu. Typicky 3-4 hodiny pro pacienty s pumpou.", + "Insulin to carb ratio (I:C)": "Inzulíno-sacharidový poměr (I:C).", + "Hours:": "Hodin:", + "hours": "hodin", + "g/hour": "g/hod", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "gramy na jednotku inzulínu. Poměr, jaké množství sacharidů pokryje jednotku inzulínu.", + "Insulin Sensitivity Factor (ISF)": "Citlivost inzulínu (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL nebo mmol/L na jednotku inzulínu. Poměr, jak se změní glykémie po podaní jednotky inzulínu", + "Carbs activity / absorption rate": "Rychlost absorbce sacharidů", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gramy za jednotku času. Reprezentuje jak změnu COB za jednoku času, tak množství sacharidů, které se za tu dobu projevily. Křivka absorbce sacharidů je mnohem méně pochopitelná než IOB, ale může být aproximována počáteční pauzou následovanou konstantní hodnotou absorbce (g/hod).", + "Basal rates [unit/hour]": "Bazály [U/hod].", + "Target BG range [mg/dL,mmol/L]": "Cílový rozsah glykémií [mg/dL,mmol/L]", + "Start of record validity": "Začátek platnosti záznamu", + "Icicle": "Rampouch", + "Render Basal": "Zobrazení bazálu", + "Profile used": "Použitý profil", + "Calculation is in target range.": "Kalkulace je v cílovém rozsahu.", + "Loading profile records ...": "Nahrávám profily ...", + "Values loaded.": "Data nahrána.", + "Default values used.": "Použity výchozí hodnoty.", + "Error. Default values used.": "CHYBA: Použity výchozí hodnoty.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Rozsahy časů pro limity glykémií si neodpovídají. Budou nastaveny výchozí hodnoty.", + "Valid from:": "Platné od:", + "Save current record before switching to new?": "Uložit současný záznam před přepnutím na nový?", + "Add new interval before": "Přidat nový interval před", + "Delete interval": "Smazat interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Kombinovaný bolus", + "Difference": "Rozdíl", + "New time": "Nový čas", + "Edit Mode": "Editační mód", + "When enabled icon to start edit mode is visible": "Pokud je povoleno, ikona pro vstup do editačního módu je zobrazena", + "Operation": "Operace", + "Move": "Přesunout", + "Delete": "Odstranit", + "Move insulin": "Přesunout inzulín", + "Move carbs": "Přesunout sacharidy", + "Remove insulin": "Odstranit inzulín", + "Remove carbs": "Odstranit sacharidy", + "Change treatment time to %1 ?": "Změnit čas ošetření na %1 ?", + "Change carbs time to %1 ?": "Změnit čas sacharidů na %1 ?", + "Change insulin time to %1 ?": "Změnit čas inzulínu na %1 ?", + "Remove treatment ?": "Odstranit ošetření ?", + "Remove insulin from treatment ?": "Odstranit inzulín z ošetření ?", + "Remove carbs from treatment ?": "Odstranit sacharidy z ošetření ?", + "Rendering": "Vykresluji", + "Loading OpenAPS data of": "Nahrávám OpenAPS data z", + "Loading profile switch data": "Nahrávám data přepnutí profilu", + "Redirecting you to the Profile Editor to create a new profile.": "Provádím přesměrování na editor profilu pro zadání nového profilu.", + "Pump": "Pumpa", + "Sensor Age": "Stáří senzoru", + "Insulin Age": "Stáří inzulínu", + "Temporary target": "Dočasný cíl", + "Reason": "Důvod", + "Eating soon": "Následuje jídlo", + "Top": "Horní", + "Bottom": "Dolní", + "Activity": "Aktivita", + "Targets": "Cíl", + "Bolus insulin:": "Bolusový inzulín:", + "Base basal insulin:": "Základní bazální inzulín:", + "Positive temp basal insulin:": "Pozitivní dočasný bazální inzulín:", + "Negative temp basal insulin:": "Negativní dočasný bazální inzulín:", + "Total basal insulin:": "Celkový bazální inzulín:", + "Total daily insulin:": "Celkový denní inzulín:", + "Unable to save Role": "Role nelze uložit", + "Unable to delete Role": "Nelze odstranit Roli", + "Database contains %1 roles": "Databáze obsahuje %1 rolí", + "Edit Role": "Editovat roli", + "admin, school, family, etc": "administrátor, škola, rodina atd...", + "Permissions": "Oprávnění", + "Are you sure you want to delete: ": "Opravdu vymazat: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Každá role má 1 nebo více oprávnění. Oprávnění * je zástupný znak, oprávnění jsou hiearchie používající : jako oddělovač.", + "Add new Role": "Přidat novou roli", + "Roles - Groups of People, Devices, etc": "Role - Skupiny lidí, zařízení atd.", + "Edit this role": "Editovat tuto roli", + "Admin authorized": "Admin autorizován", + "Subjects - People, Devices, etc": "Subjekty - Lidé, zařízení atd.", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Každý subjekt má svůj unikátní token a 1 nebo více rolí. Klikem na přístupový token se otevře nové okno pro tento subjekt. Tento link je možné sdílet.", + "Add new Subject": "Přidat nový subjekt", + "Unable to save Subject": "Subjekt nelze uložit", + "Unable to delete Subject": "Nelze odstranit Subjekt", + "Database contains %1 subjects": "Databáze obsahuje %1 subjektů", + "Edit Subject": "Editovat subjekt", + "person, device, etc": "osoba, zařízeni atd.", + "role1, role2": "role1, role2", + "Edit this subject": "Editovat tento subjekt", + "Delete this subject": "Smazat tento subjekt", + "Roles": "Role", + "Access Token": "Přístupový token", + "hour ago": "hodina zpět", + "hours ago": "hodin zpět", + "Silence for %1 minutes": "Ztlumit na %1 minut", + "Check BG": "Zkontrolovat glykémii", + "BASAL": "BAZÁL", + "Current basal": "Současný bazál", + "Sensitivity": "Citlivost", + "Current Carb Ratio": "Sacharidový poměr", + "Basal timezone": "Časová zóna", + "Active profile": "Aktivní profil", + "Active temp basal": "Aktivní dočasný bazál", + "Active temp basal start": "Začátek dočasného bazálu", + "Active temp basal duration": "Trvání dočasného bazálu", + "Active temp basal remaining": "Zbývající dočasný bazál", + "Basal profile value": "Základní hodnota bazálu", + "Active combo bolus": "Aktivní kombinovaný bolus", + "Active combo bolus start": "Začátek kombinovaného bolusu", + "Active combo bolus duration": "Trvání kombinovaného bolusu", + "Active combo bolus remaining": "Zbývající kombinovaný bolus", + "BG Delta": "Změna glykémie", + "Elapsed Time": "Dosažený čas", + "Absolute Delta": "Absolutní rozdíl", + "Interpolated": "Interpolováno", + "BWP": "BWP", + "Urgent": "Urgentní", + "Warning": "Varování", + "Info": "Informativní", + "Lowest": "Nejnižší", + "Snoozing high alarm since there is enough IOB": "Vypínání alarmu vyskoké glykémie, protože je dostatek IOB", + "Check BG, time to bolus?": "Zkontrolovat glykémii, čas na bolus?", + "Notice": "Poznámka", + "required info missing": "chybějící informace", + "Insulin on Board": "Aktivní inzulín", + "Current target": "Aktuální cílová hodnota", + "Expected effect": "Očekávaný efekt", + "Expected outcome": "Očekávaný výsledek", + "Carb Equivalent": "Ekvivalent v sacharidech", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Nadbytek inzulínu: o %1U více, než na dosažení spodní hranice cíle. Nepočítáno se sacharidy.", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Nadbytek inzulínu: o %1U více, než na dosažení spodní hranice cíle. UJISTĚTE SE, ŽE JE TO POKRYTO SACHARIDY", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Nutné snížení aktivního inzulínu o %1U k dosažení spodního cíle. Příliž mnoho bazálu?", + "basal adjustment out of range, give carbs?": "úprava změnou bazálu není možná. Podat sacharidy?", + "basal adjustment out of range, give bolus?": "úprava změnou bazálu není možná. Podat bolus?", + "above high": "nad horním", + "below low": "pod spodním", + "Projected BG %1 target": "Předpokládaná glykémie %1 cílem", + "aiming at": "s cílem", + "Bolus %1 units": "Bolus %1 jednotek", + "or adjust basal": "nebo úprava bazálu", + "Check BG using glucometer before correcting!": "Před korekcí zkontrolujte glukometrem glykémii!", + "Basal reduction to account %1 units:": "Úprava bazálu pro náhradu bolusu %1 U ", + "30m temp basal": "30ti minutový dočasný bazál", + "1h temp basal": "hodinový dočasný bazál", + "Cannula change overdue!": "Čas na výměnu set vypršel!", + "Time to change cannula": "Čas na výměnu setu", + "Change cannula soon": "Blíží se čas výměny setu", + "Cannula age %1 hours": "Stáří setu %1 hodin", + "Inserted": "Nasazený", + "CAGE": "SET", + "COB": "COB", + "Last Carbs": "Poslední sacharidy", + "IAGE": "INZ", + "Insulin reservoir change overdue!": "Čas na výměnu zásobníku vypršel!", + "Time to change insulin reservoir": "Čas na výměnu zásobníku", + "Change insulin reservoir soon": "Blíží se čas výměny zásobníku", + "Insulin reservoir age %1 hours": "Stáří zásobníku %1 hodin", + "Changed": "Vyměněno", + "IOB": "IOB", + "Careportal IOB": "IOB z ošetření", + "Last Bolus": "Poslední bolus", + "Basal IOB": "IOB z bazálů", + "Source": "Zdroj", + "Stale data, check rig?": "Zastaralá data, zkontrolovat mobil?", + "Last received:": "Naposledy přijato:", + "%1m ago": "%1m zpět", + "%1h ago": "%1h zpět", + "%1d ago": "%1d zpět", + "RETRO": "Zastaralé", + "SAGE": "SENZ", + "Sensor change/restart overdue!": "Čas na výměnu senzoru vypršel!", + "Time to change/restart sensor": "Čas na výměnu senzoru", + "Change/restart sensor soon": "Blíží se čas na výměnu senzoru", + "Sensor age %1 days %2 hours": "Stáří senzoru %1 dní %2 hodin", + "Sensor Insert": "Výměna sensoru", + "Sensor Start": "Spuštění sensoru", + "days": "dní", + "Insulin distribution": "Rozložení inzulínu", + "To see this report, press SHOW while in this view": "Pro zobrazení toho výkazu stiskněte Zobraz na této záložce", + "AR2 Forecast": "AR2 predikci", + "OpenAPS Forecasts": "OpenAPS predikci", + "Temporary Target": "Dočasný cíl glykémie", + "Temporary Target Cancel": "Dočasný cíl glykémie konec", + "OpenAPS Offline": "OpenAPS vypnuto", + "Profiles": "Profily", + "Time in fluctuation": "Doba měnící se glykémie", + "Time in rapid fluctuation": "Doba rychle se měnící glykémie", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Toto je pouze hrubý odhad, který může být nepřesný a nenahrazuje kontrolu z krve. Vzorec je převzatý z:", + "Filter by hours": " Filtr podle hodin", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Doba měnící se glykémie a rapidně se měnící glykémie měří % času ve zkoumaném období, během kterého se glykémie měnila relativně rychle nebo rapidně. Nižší hodnota je lepší.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Průměrná celková denní změna je součet absolutních hodnoty všech glykémií za sledované období, děleno počtem dní. Nižší hodnota je lepší.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Průměrná hodinová změna je součet absolutní hodnoty všech glykémií za sledované období, dělených počtem hodin v daném období. Nižší hodnota je lepší.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Mimo dosah RMS se vypočítá tak, že se odečte vzdálenost od rozsahu pro všechny hodnoty glykémie za vyšetřované období, hodnoty se sečtou, vydělí počtem a odmocní. Tato metrika je podobná v procentech z rozsahu, ale hodnoty váhy jsou mnohem vyšší. Nižší hodnoty jsou lepší.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">zde.", + "Mean Total Daily Change": "Průměrná celková denní změna", + "Mean Hourly Change": "Průměrná hodinová změna", + "FortyFiveDown": "lehce dolů", + "FortyFiveUp": "lehce nahoru", + "Flat": "stabilní", + "SingleUp": "nahoru", + "SingleDown": "dolů", + "DoubleDown": "rychle dolů", + "DoubleUp": "rychle nahoru", + "virtAsstUnknown": "Hodnota je v tuto chvíli neznámá. Pro více informací se podívejte na stránky Nightscout.", + "virtAsstTitleAR2Forecast": "AR2 predikce", + "virtAsstTitleCurrentBasal": "Aktuální bazál", + "virtAsstTitleCurrentCOB": "Aktuální COB", + "virtAsstTitleCurrentIOB": "Aktuální IOB", + "virtAsstTitleLaunch": "Vítejte v Nightscoutu", + "virtAsstTitleLoopForecast": "Loop predikce", + "virtAsstTitleLastLoop": "Poslední běh", + "virtAsstTitleOpenAPSForecast": "OpenAPS predikce", + "virtAsstTitlePumpReservoir": "Zbývající inzulín", + "virtAsstTitlePumpBattery": "Baterie pumpy", + "virtAsstTitleRawBG": "Aktuální Raw glykemie", + "virtAsstTitleUploaderBattery": "Baterie telefonu", + "virtAsstTitleCurrentBG": "Aktuální glykemie", + "virtAsstTitleFullStatus": "Plný status", + "virtAsstTitleCGMMode": "Režim CGM", + "virtAsstTitleCGMStatus": "Stav CGM", + "virtAsstTitleCGMSessionAge": "Staří senzoru CGM", + "virtAsstTitleCGMTxStatus": "Stav vysílače CGM", + "virtAsstTitleCGMTxAge": "Stáří vysílače CGM", + "virtAsstTitleCGMNoise": "Šum CGM", + "virtAsstTitleDelta": "Změna krevní glukózy", + "virtAsstStatus": "%1 %2 čas %3.", + "virtAsstBasal": "%1 aktuální bazál je %2 jednotek za hodinu", + "virtAsstBasalTemp": "%1 dočasný bazál %2 jednotek za hodinu skončí v %3", + "virtAsstIob": "a máte %1 aktivního inzulínu.", + "virtAsstIobIntent": "Máte %1 aktivního inzulínu", + "virtAsstIobUnits": "%1 jednotek", + "virtAsstLaunch": "Co chcete v Nightscoutu zkontrolovat?", + "virtAsstPreamble": "Vaše", + "virtAsstPreamble3person": "%1 má ", + "virtAsstNoInsulin": "žádný", + "virtAsstUploadBattery": "Baterie mobilu má %1", + "virtAsstReservoir": "V zásobníku zbývá %1 jednotek", + "virtAsstPumpBattery": "Baterie v pumpě má %1 %2", + "virtAsstUploaderBattery": "Baterie uploaderu má %1", + "virtAsstLastLoop": "Poslední úšpěšné provedení smyčky %1", + "virtAsstLoopNotAvailable": "Plugin Loop není patrně povolený", + "virtAsstLoopForecastAround": "Podle přepovědi smyčky je očekávána glykémie okolo %1 během následujících %2", + "virtAsstLoopForecastBetween": "Podle přepovědi smyčky je očekávána glykémie mezi %1 a %2 během následujících %3", + "virtAsstAR2ForecastAround": "Podle AR2 predikce je očekávána glykémie okolo %1 během následujících %2", + "virtAsstAR2ForecastBetween": "Podle AR2 predikce je očekávána glykémie mezi %1 a %2 během následujících %3", + "virtAsstForecastUnavailable": "S dostupnými daty přepověď není možná", + "virtAsstRawBG": "Raw glykémie je %1", + "virtAsstOpenAPSForecast": "OpenAPS Eventual BG je %1", + "virtAsstCob3person": "%1 má %2 aktivních sacharidů", + "virtAsstCob": "Máte %1 aktivních sacharidů", + "virtAsstCGMMode": "Váš CGM režim byl %1 od %2.", + "virtAsstCGMStatus": "Váš stav CGM byl %1 od %2.", + "virtAsstCGMSessAge": "Vaše CGM relace byla aktivní %1 dní a %2 hodin.", + "virtAsstCGMSessNotStarted": "V tuto chvíli není aktivní žádná relace CGM.", + "virtAsstCGMTxStatus": "Stav CGM vysílače byl %1 od %2.", + "virtAsstCGMTxAge": "Stáří CGM vysílače je %1 dní.", + "virtAsstCGMNoise": "Šum CGM byl %1 od %2.", + "virtAsstCGMBattOne": "Baterie CGM vysílače byla %1 voltů od %2.", + "virtAsstCGMBattTwo": "Stav baterie CGM vysílače byla mezi %1 voltů a %2 voltů od %3.", + "virtAsstDelta": "Vaše delta byla %1 mezi %2 a %3.", + "virtAsstDeltaEstimated": "Vaše odhadovaná delta byla %1 mezi %2 a %3.", + "virtAsstUnknownIntentTitle": "Neznámý úmysl", + "virtAsstUnknownIntentText": "Je mi líto. Nevím, na co se ptáte.", + "Fat [g]": "Tuk [g]", + "Protein [g]": "Proteiny [g]", + "Energy [kJ]": "Energie [kJ]", + "Clock Views:": "Hodiny:", + "Clock": "Hodiny", + "Color": "Barevné", + "Simple": "Jednoduché", + "TDD average": "Průměrná denní dávka", + "Bolus average": "Průměrný bolus", + "Basal average": "Průměrný bazál", + "Base basal average:": "Průměrná hodnota bazálu:", + "Carbs average": "Průměrné množství sacharidů", + "Eating Soon": "Blížící se jídlo", + "Last entry {0} minutes ago": "Poslední hodnota {0} minut zpět", + "change": "změna", + "Speech": "Hlas", + "Target Top": "Horní cíl", + "Target Bottom": "Dolní cíl", + "Canceled": "Zrušený", + "Meter BG": "Hodnota z glukoměru", + "predicted": "přepověď", + "future": "budoucnost", + "ago": "zpět", + "Last data received": "Poslední data přiajata", + "Clock View": "Hodiny", + "Protein": "Bílkoviny", + "Fat": "Tuk", + "Protein average": "Průměrně bílkovin", + "Fat average": "Průměrně tuků", + "Total carbs": "Celkové sacharidy", + "Total protein": "Celkem bílkovin", + "Total fat": "Celkový tuk", + "Database Size": "Velikost databáze", + "Database Size near its limits!": "Databáze bude brzy plná!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Velikost databáze je %1 MiB z %2 MiB. Udělejte si zálohu dat a vyčistěte databázi!", + "Database file size": "Velikost databáze", + "%1 MiB of %2 MiB (%3%)": "%1 MiB z %2 MiB (%3%)", + "Data size": "Velikost dat", + "virtAsstDatabaseSize": "%1 MiB. To odpovídá %2% dostupného místa v databázi.", + "virtAsstTitleDatabaseSize": "Velikost databáze", + "Carbs/Food/Time": "Sacharidy/Jídlo/Čas", + "You have administration messages": "Máte zprávy pro administrátora", + "Admin messages in queue": "Ve frontě jsou zprávy pro adminstrátora", + "Queue empty": "Fronta je prázdná", + "There are no admin messages in queue": "Ve frontě nejsou žádné zprávy pro administrátora", + "Please sign in using the API_SECRET to see your administration messages": "Pro zobrazení zpráv pro administrátora se přihlašte pomocí API_SECRET", + "Reads enabled in default permissions": "Ve výchozích oprávněních je čtení povoleno", + "Data reads enabled": "Čtení dat povoleno", + "Data writes enabled": "Zápis dat povolen", + "Data writes not enabled": "Zápis dat není povolen", + "Color prediction lines": "Barevné křivky předpovědí", + "Release Notes": "Poznámky k vydání", + "Check for Updates": "Zkontrolovat aktualizace", + "Open Source": "Open Source", + "Nightscout Info": "Informace o Nightscoutu", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Hlavním úkolem Loopalyzeru je vizualizace systému uzavřené smyčky. Funguje to při jakémkoliv nastavení - s uzavřenou či otevřenou smyčkou, nebo bez smyčky. Nicméně v návaznosti na tom, jaký uploader používáte, jaká data je schopen načíst a následně odeslat, jak je schopen doplňovat chybějící údaje, mohou některé grafy obsahovat chybějící data, nebo dokonce mohou být prázdné. Vždy si zajistěte, aby grafy vypadaly rozumně. Nejlepší je zobrazit nejprve jeden den, a až pak zobrazit více dní najednou.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer obsahuje funkci časového posunu. Například máte-li snídani jeden den v 7:00, a den poté v 8:00, budou průměrné křivky glykémie za tyto 2 dny pravděpodobně vypadat zploštělé, a odezva smyčky pak bude zkreslená. Časový posun vypočítá průměrnou dobu, po kterou byla tato jídla konzumována, poté posune všechna data (sacharidy, inzulín, bazál atd.) během obou dnů o odpovídající časový rozdíl tak, aby obě jídla odpovídala průměrné době začátku jídla.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "V tomto příkladu jsou všechna data z prvního dne posunuta o 30 minut vpřed, a všechna data z druhého dne o 30 minut zpět, takže to vypadá, jako byste oba dny měli snídani v 7:30. To vám umožní zobrazit průměrnou reakci na glukózu v krvi z jídla.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Časový posun zvýrazňuje šedou barvou období po průměrné době zahájení jídla po dobu trvání DIA (doba působení inzulínu). Protože jsou všechny datové body za celý den posunuté, nemusí být křivky mimo šedou oblast přesné.", + "Note that time shift is available only when viewing multiple days.": "Berte na vědomí, že časový posun je k dispozici pouze při prohlížení více dní.", + "Please select a maximum of two weeks duration and click Show again.": "Vyberte, prosím, dobu trvání maximálně 2 týdny, a klepněte na tlačítko Zobrazit znovu.", + "Show profiles table": "Zobrazit tabulku profilů", + "Show predictions": "Zobrazit predikce", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Časový posun jídla větší než %1 g sacharidů spotřebovaných mezi %2 a %3", + "Previous": "Předchozí", + "Previous day": "Předchozí den", + "Next day": "Následující den", + "Next": "Další", + "Temp basal delta": "Rozdíl dočasného bazálu", + "Authorized by token": "Autorizováno pomocí tokenu", + "Auth role": "Autorizační role", + "view without token": "zobrazit bez tokenu", + "Remove stored token": "Odstranit uložený token", + "Weekly Distribution": "Týdenní rozložení", + "Failed authentication": "Ověření selhalo", + "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?": "Zařízení s IP adresou %1 se pokusilo o příhlášení do Nightscoutu s nesprávnými údaji. Zkontrolujte, zda nemáte v uploaderu nastaveno špatné API_SECRET nebo token.", + "Default (with leading zero and U)": "Výchozí (s nulou a U)", + "Concise (with U, without leading zero)": "Stručný (s U, bez úvodní nuly)", + "Minimal (without leading zero and U)": "Minimální (bez nuly a U)", + "Small Bolus Display": "Malý bolus displej", + "Large Bolus Display": "Velký bolus displej", + "Bolus Display Threshold": "Limit zobrazení bolusu", + "%1 U and Over": "%1 U a více", + "Event repeated %1 times.": "Událost se opakovala %1 krát.", + "minutes": "minut", + "Last recorded %1 %2 ago.": "Naposledy zaznamenáno před %1 %2.", + "Security issue": "Bezpečnostní problém", + "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.": "Zjištěno slabé API_SECRET. Ke snížení rizika neoprávněného přístupu prosím použijte kombinaci malých a VELKÝCH písmen, čísel a jiných než alfanumerických znaků, jako je například !#%&/. Minimální délka API_SECRET je 12 znaků.", + "less than 1": "méně než 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Heslo do MongoDB a API_SECRET jsou stejné. To je opravdu velmi špatný nápad. Změňte prosím obojí, a už nikdy nepoužívejte pro různé služby v jednom systému stejná hesla." +} diff --git a/translations/da_DK.json b/translations/da_DK.json new file mode 100644 index 00000000000..576c2a1de9a --- /dev/null +++ b/translations/da_DK.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Lytter på port", + "Mo": "Man", + "Tu": "Tir", + "We": "Ons", + "Th": "Tor", + "Fr": "Fre", + "Sa": "Lør", + "Su": "Søn", + "Monday": "Mandag", + "Tuesday": "Tirsdag", + "Wednesday": "Onsdag", + "Thursday": "Torsdag", + "Friday": "Fredag", + "Saturday": "Lørdag", + "Sunday": "Søndag", + "Category": "Kategori", + "Subcategory": "Underkategori", + "Name": "Navn", + "Today": "I dag", + "Last 2 days": "Sidste 2 dage", + "Last 3 days": "Sidste 3 dage", + "Last week": "Sidste uge", + "Last 2 weeks": "Sidste 2 uger", + "Last month": "Sidste måned", + "Last 3 months": "Sidste 3 måneder", + "From": "Fra", + "To": "Til", + "Notes": "Noter", + "Food": "Mad", + "Insulin": "Insulin", + "Carbs": "Kulhydrater", + "Notes contain": "Noter indeholder", + "Target BG range bottom": "Nedre grænse for blodsukkerværdier", + "top": "top", + "Show": "Vis", + "Display": "Vis", + "Loading": "Indlæser", + "Loading profile": "Indlæser profil", + "Loading status": "Indlæsningsstatus", + "Loading food database": "Indlæser mad database", + "not displayed": "vises ikke", + "Loading CGM data of": "Indlæser CGM-data for", + "Loading treatments data of": "Indlæser behandlingsdata for", + "Processing data of": "Behandler data for", + "Portion": "Portion", + "Size": "Størrelse", + "(none)": "(ingen)", + "None": "Ingen", + "": "", + "Result is empty": "Tomt resultat", + "Day to day": "Dag til dag", + "Week to week": "Uge til uge", + "Daily Stats": "Daglig statistik", + "Percentile Chart": "Percentildiagram", + "Distribution": "Fordeling", + "Hourly stats": "Timestatistik", + "netIOB stats": "netIOB statistik", + "temp basals must be rendered to display this report": "temp basaler skal være tilgængelige for at vise denne rapport", + "No data available": "Ingen tilgængelige data", + "Low": "Lav", + "In Range": "Indenfor intervallet", + "Period": "Periode", + "High": "Høj", + "Average": "Gennemsnit", + "Low Quartile": "Nedre kvartil", + "Upper Quartile": "Øvre kvartil", + "Quartile": "Kvartil", + "Date": "Dato", + "Normal": "Normal", + "Median": "Median", + "Readings": "Aflæsninger", + "StDev": "Standard afvigelse", + "Daily stats report": "Daglig statistik rapport", + "Glucose Percentile report": "Rapport af glukose Percentiler", + "Glucose distribution": "Glukosefordeling", + "days total": "dage i alt", + "Total per day": "Total pr. dag", + "Overall": "Samlet", + "Range": "Interval", + "% of Readings": "% af aflæsninger", + "# of Readings": "Antal aflæsninger", + "Mean": "Gennemsnit", + "Standard Deviation": "Standardafvigelse", + "Max": "Maks", + "Min": "Min", + "A1c estimation*": "A1c estimering*", + "Weekly Success": "Ugentlig succes", + "There is not sufficient data to run this report. Select more days.": "Der er utilstrækkeligt data til at generere rapporten. Vælg flere dage.", + "Using stored API secret hash": "Anvender gemt API-nøgle", + "No API secret hash stored yet. You need to enter API secret.": "Mangler API-nøgle. Du skal indtaste API nøglen.", + "Database loaded": "Database indlæst", + "Error: Database failed to load": "Fejl: Database kan ikke indlæses", + "Error": "Fejl", + "Create new record": "Opret ny post", + "Save record": "Gemmer post", + "Portions": "Portioner", + "Unit": "Enheder", + "GI": "GI", + "Edit record": "Rediger post", + "Delete record": "Slet post", + "Move to the top": "Flyt til toppen", + "Hidden": "Skjult", + "Hide after use": "Skjul efter brug", + "Your API secret must be at least 12 characters long": "Din API nøgle skal være mindst 12 tegn lang", + "Bad API secret": "Forkert API-nøgle", + "API secret hash stored": "Hemmelig API-hash gemt", + "Status": "Status", + "Not loaded": "Ikke indlæst", + "Food Editor": "Mad editor", + "Your database": "Din database", + "Filter": "Filtrer", + "Save": "Gem", + "Clear": "Rense", + "Record": "Post", + "Quick picks": "Hurtig valg", + "Show hidden": "Vis skjulte", + "Your API secret or token": "Din API hemmelighed eller token", + "Remember this device. (Do not enable this on public computers.)": "Husk denne enhed. (Aktiver ikke dette på offentlige computere)", + "Treatments": "Behandling", + "Time": "Tid", + "Event Type": "Hændelsestype", + "Blood Glucose": "Blodsukker", + "Entered By": "Indtastet af", + "Delete this treatment?": "Slet denne hændelse?", + "Carbs Given": "Kulhydrater givet", + "Insulin Given": "Insulin givet", + "Event Time": "Tidspunkt for hændelsen", + "Please verify that the data entered is correct": "Venligst verificer at indtastet data er korrekt", + "BG": "BS", + "Use BG correction in calculation": "Anvend BS-korrektion i beregning", + "BG from CGM (autoupdated)": "BG fra CGM (automatisk)", + "BG from meter": "BG fra blodsukkerapperat", + "Manual BG": "Manuelt BG", + "Quickpick": "Hurtig valg", + "or": "eller", + "Add from database": "Tilføj fra database", + "Use carbs correction in calculation": "Benyt kulhydratkorrektion i beregning", + "Use COB correction in calculation": "Benyt aktive kulhydrater i beregning", + "Use IOB in calculation": "Benyt aktivt insulin i beregningen", + "Other correction": "Øvrig korrektion", + "Rounding": "Afrunding", + "Enter insulin correction in treatment": "Indtast insulinkorrektion", + "Insulin needed": "Insulin påkrævet", + "Carbs needed": "Kulhydrater påkrævet", + "Carbs needed if Insulin total is negative value": "Kulhydrater er nødvendige når total insulin mængde er negativ", + "Basal rate": "Basalrate", + "60 minutes earlier": "60 min tidligere", + "45 minutes earlier": "45 min tidligere", + "30 minutes earlier": "30 min tidigere", + "20 minutes earlier": "20 min tidligere", + "15 minutes earlier": "15 min tidligere", + "Time in minutes": "Tid i minutter", + "15 minutes later": "15 min senere", + "20 minutes later": "20 min senere", + "30 minutes later": "30 min senere", + "45 minutes later": "45 min senere", + "60 minutes later": "60 min senere", + "Additional Notes, Comments": "Ekstra noter, kommentarer", + "RETRO MODE": "Retro mode", + "Now": "Nu", + "Other": "Øvrige", + "Submit Form": "Gem", + "Profile Editor": "Profil editor", + "Reports": "Rapporter", + "Add food from your database": "Tilføj mad fra din database", + "Reload database": "Genindlæs databasen", + "Add": "Tilføj", + "Unauthorized": "Uautoriseret", + "Entering record failed": "Tilføjelse af post fejlede", + "Device authenticated": "Enhed godkendt", + "Device not authenticated": "Enhed ikke godkendt", + "Authentication status": "Godkendelsesstatus", + "Authenticate": "Godkend", + "Remove": "Fjern", + "Your device is not authenticated yet": "Din enhed er ikke godkendt endnu", + "Sensor": "Sensor", + "Finger": "Finger", + "Manual": "Manuel", + "Scale": "Skala", + "Linear": "Lineær", + "Logarithmic": "Logaritmisk", + "Logarithmic (Dynamic)": "Logaritmisk (dynamisk)", + "Insulin-on-Board": "Aktivt insulin (IOB)", + "Carbs-on-Board": "Aktive kulhydrater (COB)", + "Bolus Wizard Preview": "Bolus Wizard (BWP)", + "Value Loaded": "Værdi indlæst", + "Cannula Age": "Indstik alder (CAGE)", + "Basal Profile": "Basal profil", + "Silence for 30 minutes": "Stilhed i 30 min", + "Silence for 60 minutes": "Stilhed i 60 min", + "Silence for 90 minutes": "Stilhed i 90 min", + "Silence for 120 minutes": "Stilhed i 120 min", + "Settings": "Indstillinger", + "Units": "Enheder", + "Date format": "Dato format", + "12 hours": "12 timer", + "24 hours": "24 timer", + "Log a Treatment": "Log en hændelse", + "BG Check": "BG kontrol", + "Meal Bolus": "Måltidsbolus", + "Snack Bolus": "Mellemmåltidsbolus", + "Correction Bolus": "Korrektionsbolus", + "Carb Correction": "Kulhydratskorrektion", + "Note": "Kommentar", + "Question": "Spørgsmål", + "Exercise": "Træning", + "Pump Site Change": "Skiftet infusionssted", + "CGM Sensor Start": "Sensorstart", + "CGM Sensor Stop": "Sensor Stop", + "CGM Sensor Insert": "Sensor påsat", + "Sensor Code": "Sensor Kode", + "Transmitter ID": "Transmitter ID", + "Dexcom Sensor Start": "Dexcom sensor start", + "Dexcom Sensor Change": "Dexcom sensor ombytning", + "Insulin Cartridge Change": "Skift insulin beholder", + "D.A.D. Alert": "Vuf Vuf! (Diabeteshundealarm!)", + "Glucose Reading": "Blodsukker aflæsning", + "Measurement Method": "Målemetode", + "Meter": "Blodsukkermåler", + "Amount in grams": "Antal gram", + "Amount in units": "Antal enheder", + "View all treatments": "Se alle behandlinger", + "Enable Alarms": "Aktivere alarmer", + "Pump Battery Change": "Udskiftet pumpebatteri", + "Pump Battery Age": "Pumpe batterialder", + "Pump Battery Low Alarm": "Pumpebatteri lav Alarm", + "Pump Battery change overdue!": "Pumpebatteri skal skiftes!", + "When enabled an alarm may sound.": "Når aktiveret kan en alarm starte.", + "Urgent High Alarm": "Kritisk høj alarm", + "High Alarm": "Høj alarm", + "Low Alarm": "Lav alarm", + "Urgent Low Alarm": "Kritisk lav alarm", + "Stale Data: Warn": "Advarsel: Gamle data", + "Stale Data: Urgent": "Kritisk: Gamle data", + "mins": "min", + "Night Mode": "Nattilstand", + "When enabled the page will be dimmed from 10pm - 6am.": "Når aktiveret vil denne side dæmpes fra 22:00-6:00.", + "Enable": "Aktiver", + "Show Raw BG Data": "Vis rå BG data", + "Never": "Aldrig", + "Always": "Altid", + "When there is noise": "Når der er støj", + "When enabled small white dots will be displayed for raw BG data": "Ved aktivering vil små hvide prikker blive vist for rå BG tal", + "Custom Title": "Brugerdefineret titel", + "Theme": "Tema", + "Default": "Standard", + "Colors": "Farver", + "Colorblind-friendly colors": "Farveblindvenlige farver", + "Reset, and use defaults": "Nulstil og brug standardindstillinger", + "Calibrations": "Kalibrering", + "Alarm Test / Smartphone Enable": "Alarm test / Smartphone aktiveret", + "Bolus Wizard": "Bolusberegner", + "in the future": "i fremtiden", + "time ago": "tid siden", + "hr ago": "time siden", + "hrs ago": "timer siden", + "min ago": "minut siden", + "mins ago": "minutter siden", + "day ago": "dag siden", + "days ago": "dage siden", + "long ago": "længe siden", + "Clean": "Rens", + "Light": "Let", + "Medium": "Middel", + "Heavy": "Kraftig", + "Treatment type": "Behandlingstype", + "Raw BG": "Råt BG", + "Device": "Enhed", + "Noise": "Støj", + "Calibration": "Kalibrering", + "Show Plugins": "Vis plugins", + "About": "Om", + "Value in": "Værdi i", + "Carb Time": "Kulhydratstid", + "Language": "Sprog", + "Add new": "Tilføj ny", + "g": "g", + "ml": "ml", + "pcs": "stk", + "Drag&drop food here": "Træk og slip mad her", + "Care Portal": "Omsorgsportal", + "Medium/Unknown": "Medium/Ukendt", + "IN THE FUTURE": "I FREMTIDEN", + "Order": "Sorter", + "oldest on top": "ældste først", + "newest on top": "nyeste først", + "All sensor events": "Alle sensorhændelser", + "Remove future items from mongo database": "Fjern fremtidige værdier fra mongo databasen", + "Find and remove treatments in the future": "Find og fjern fremtidige behandlinger", + "This task find and remove treatments in the future.": "Denne handling finder og fjerner fremtidige behandlinger.", + "Remove treatments in the future": "Fjern behandlinger i fremtiden", + "Find and remove entries in the future": "Find og slet poster i fremtiden", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Denne handling finder og sletter CGM data i fremtiden forårsaget af indlæsning med forkert dato/tid.", + "Remove entries in the future": "Fjern poster i fremtiden", + "Loading database ...": "Indlæser database ...", + "Database contains %1 future records": "Databasen indeholder %1 fremtidige poster", + "Remove %1 selected records?": "Fjern %1 valgte poster?", + "Error loading database": "Fejl ved indlæsning af database", + "Record %1 removed ...": "Post %1 fjernet ...", + "Error removing record %1": "Fejl ved fjernelse af post %1", + "Deleting records ...": "Sletter poster...", + "%1 records deleted": "%1 poster slettet", + "Clean Mongo status database": "Slet Mongo status database", + "Delete all documents from devicestatus collection": "Sletter alle dokumenter fra devicestatus collection", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Denne handling sletter alle dokumenter fra devicestatus collection. Brugbart når uploader batteri status ikke opdateres korrekt.", + "Delete all documents": "Slet alle dokumenter", + "Delete all documents from devicestatus collection?": "Fjern alle dokumenter fra devicestatus collection?", + "Database contains %1 records": "Databasen indeholder %1 poster", + "All records removed ...": "Alle poster fjernet ...", + "Delete all documents from devicestatus collection older than 30 days": "Fjerne alle dokumenter fra devicestatus collection der er ældre end 30 dage", + "Number of Days to Keep:": "Antal af dage, der skal beholdes:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Denne handling fjerner alle dokumenter fra devicestatus collection. Brugbart når uploader batteri status ikke opdateres korrekt.", + "Delete old documents from devicestatus collection?": "Fjerne gamle dokumenter fra devicestatus collection?", + "Clean Mongo entries (glucose entries) database": "Slet Mongo poster (glucose poster) database", + "Delete all documents from entries collection older than 180 days": "Slet alle dokumenter i entries collection, som er ældre end 180 dage", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Denne handling fjerner alle dokumenter fra entries collection, der er ældre end 180 dage. Nyttigt når uploaderens batteristatus ikke opdateres korrekt.", + "Delete old documents": "Slet gamle dokumenter", + "Delete old documents from entries collection?": "Slet gamle dokumenter fra entries collection?", + "%1 is not a valid number": "%1 er ikke et gyldigt tal", + "%1 is not a valid number - must be more than 2": "%1 er ikke et gyldigt tal - skal være højere end 2", + "Clean Mongo treatments database": "Slet Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Slet alle dokumenter i treatments collection, som er ældre end 180 dage", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Denne handling sletter alle dokumenter fra treatments collection, der er ældre end 180 dage. Nyttigt, når uploaderens batteristatus ikke er korrekt opdateret.", + "Delete old documents from treatments collection?": "Slet gamle dokumenter fra treatments collection?", + "Admin Tools": "Admin værktøjer", + "Nightscout reporting": "Nightscout - rapporter", + "Cancel": "Annuller", + "Edit treatment": "Rediger behandling", + "Duration": "Varighed", + "Duration in minutes": "Varighed i minutter", + "Temp Basal": "Midlertidig basal", + "Temp Basal Start": "Midlertidig basal start", + "Temp Basal End": "Midlertidig basal slut", + "Percent": "Procent", + "Basal change in %": "Basal ændring i %", + "Basal value": "Basalværdi", + "Absolute basal value": "Absolut basalværdi", + "Announcement": "Meddelelse", + "Loading temp basal data": "Indlæser midlertidig basal data", + "Save current record before changing to new?": "Gem aktuelle post før der skiftes til ny?", + "Profile Switch": "Skift profil", + "Profile": "Profil", + "General profile settings": "Generelle profil indstillinger", + "Title": "Overskrift", + "Database records": "Database poster", + "Add new record": "Tilføj ny post", + "Remove this record": "Fjern denne post", + "Clone this record to new": "Kopier denne post til ny", + "Record valid from": "Post gyldig fra", + "Stored profiles": "Gemte profiler", + "Timezone": "Tidszone", + "Duration of Insulin Activity (DIA)": "Varighed af insulin aktivitet (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Repræsenterer den typiske tid hvor insulinen virker. Varierer per patient og per insulin type. Typisk 3-4 timer for de fleste pumpe insulin og for de fleste patienter.", + "Insulin to carb ratio (I:C)": "Insulin til Kulhydratsforhold (I:C)", + "Hours:": "Timer:", + "hours": "timer", + "g/hour": "g/time", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "gr kulhydrater per enhed insulin. Den mængde kulhydrat, som dækkes af en enhed insulin.", + "Insulin Sensitivity Factor (ISF)": "Insulinfølsomhedsfaktor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dl eller mmol per enhed insulin. Beskriver hvor mange mmol eller mg/dl blodsukkeret sænkes per enhed insulin (Insulinfølsomheden)", + "Carbs activity / absorption rate": "Kulhydrattid / optagelsestid", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gram per tidsenhed. Repræsentere både ændring i aktive kulhydrater per tidsenhed, samt mængden af kulhydrater, der indtages i dette tidsrum. Kulhydratsoptag / aktivitetskurver er svære at forudse i forhold til aktiv insulin, men man kan lave en tilnærmet beregning, ved at inkludere en forsinkelse i beregningen, sammen med en konstant absorberingstid (g/time).", + "Basal rates [unit/hour]": "Basal [enhed/t]", + "Target BG range [mg/dL,mmol/L]": "Ønsket blodsukkerinterval [mg/dl,mmol]", + "Start of record validity": "Starttid for gyldighed", + "Icicle": "Istap", + "Render Basal": "Gengiv basal", + "Profile used": "Profil brugt", + "Calculation is in target range.": "Beregning er i målområdet", + "Loading profile records ...": "Indlæser profilposter...", + "Values loaded.": "Værdier hentet.", + "Default values used.": "Standardværdier brugt.", + "Error. Default values used.": "Fejl. Standardværdier brugt.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Tidsintervaller for lav og høj matcher ikke. Værdier gendannes til standardværdier.", + "Valid from:": "Gyldig fra:", + "Save current record before switching to new?": "Gem nuværende data inden der skiftes til ny?", + "Add new interval before": "Tilføj nyt interval før", + "Delete interval": "Slet interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Kombineret bolus", + "Difference": "Forskel", + "New time": "Ny tid", + "Edit Mode": "Redigerings tilstand", + "When enabled icon to start edit mode is visible": "Ikon vises når redigering er aktivt", + "Operation": "Handling", + "Move": "Flyt", + "Delete": "Slet", + "Move insulin": "Flyt insulin", + "Move carbs": "Flyt kulhydrater", + "Remove insulin": "Fjern insulin", + "Remove carbs": "Fjern kulhydrater", + "Change treatment time to %1 ?": "Ændre behandlingstid til %1?", + "Change carbs time to %1 ?": "Ændre kulhydratstid til %1 ?", + "Change insulin time to %1 ?": "Ændre insulintid til %1 ?", + "Remove treatment ?": "Slet behandling ?", + "Remove insulin from treatment ?": "Fjern insulin fra behandling ?", + "Remove carbs from treatment ?": "Fjern kulhydrater fra behandling ?", + "Rendering": "Arbejder...", + "Loading OpenAPS data of": "Henter OpenAPS data for", + "Loading profile switch data": "Indlæser profilskiftdata", + "Redirecting you to the Profile Editor to create a new profile.": "Omdirigere til profil editoren for at lave en ny profil.", + "Pump": "Pumpe", + "Sensor Age": "Sensor alder (SAGE)", + "Insulin Age": "Insulinalder (IAGE)", + "Temporary target": "Midlertidig mål", + "Reason": "Årsag", + "Eating soon": "Spiser snart", + "Top": "Øverst", + "Bottom": "Bund", + "Activity": "Aktivitet", + "Targets": "Mål", + "Bolus insulin:": "Bolusinsulin:", + "Base basal insulin:": "Base basalinsulin:", + "Positive temp basal insulin:": "Positiv midlertidig basalinsulin:", + "Negative temp basal insulin:": "Negativ midlertidig basalinsulin:", + "Total basal insulin:": "Total daglig basalinsulin:", + "Total daily insulin:": "Total dagsdosis insulin", + "Unable to save Role": "Rolle kan ikke gemmes", + "Unable to delete Role": "Kan ikke slette rolle", + "Database contains %1 roles": "Databasen indeholder %1 roller", + "Edit Role": "Rediger rolle", + "admin, school, family, etc": "Administrator, skole, familie, etc", + "Permissions": "Rettigheder", + "Are you sure you want to delete: ": "Er du sikker på at du vil slette:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Hver rolle vil have en eller flere rettigheder. Rollen * fungere som wildcard / joker, rettigheder sættes hierakisk med : som skilletegn.", + "Add new Role": "Tilføj ny rolle", + "Roles - Groups of People, Devices, etc": "Roller - Grupper af brugere, Enheder, etc", + "Edit this role": "Rediger denne rolle", + "Admin authorized": "Administrator godkendt", + "Subjects - People, Devices, etc": "Emner - Brugere, Enheder, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Hvert emne vil have en unik sikkerhedsnøgle samt en eller flere roller. Klik på sikkerhedsnøglen for at åbne et nyt view med det valgte emne, dette hemmelige link kan derefter blive delt.", + "Add new Subject": "Tilføj nyt emne", + "Unable to save Subject": "Emne kan ikke gemmes", + "Unable to delete Subject": "Kan ikke slette emne", + "Database contains %1 subjects": "Databasen indeholder %1 emner", + "Edit Subject": "Rediger emne", + "person, device, etc": "person, emne, etc", + "role1, role2": "Rolle1, Rolle2", + "Edit this subject": "Rediger emne", + "Delete this subject": "Slet dette emne", + "Roles": "Roller", + "Access Token": "Adgangs Token", + "hour ago": "time siden", + "hours ago": "timer siden", + "Silence for %1 minutes": "Stilhed i %1 minutter", + "Check BG": "Kontroller BG", + "BASAL": "BASAL", + "Current basal": "Nuværende basal", + "Sensitivity": "Følsomhed", + "Current Carb Ratio": "Nuværende kulhydratratio", + "Basal timezone": "Basal tidszone", + "Active profile": "Aktiv profil", + "Active temp basal": "Aktiv midlertidig basal", + "Active temp basal start": "Aktiv midlertidig basal start", + "Active temp basal duration": "Aktiv midlertidig basal varighed", + "Active temp basal remaining": "Aktiv temp basal resterende", + "Basal profile value": "Basalprofil værdi", + "Active combo bolus": "Aktiv kombobolus", + "Active combo bolus start": "Aktiv kombibolus start", + "Active combo bolus duration": "Aktiv kombibolus varighed", + "Active combo bolus remaining": "Resterende aktiv kombibolus", + "BG Delta": "BG delta", + "Elapsed Time": "Forløbet tid", + "Absolute Delta": "Absolut delta", + "Interpolated": "Interpoleret", + "BWP": "Bolusberegner (BWP)", + "Urgent": "Kritisk", + "Warning": "Advarsel", + "Info": "Info", + "Lowest": "Laveste", + "Snoozing high alarm since there is enough IOB": "Udsætter høj alarm siden der er nok aktiv insulin (IOB)", + "Check BG, time to bolus?": "Kontroler BG, tid til en bolus?", + "Notice": "Bemærk", + "required info missing": "påkrævet info mangler", + "Insulin on Board": "Aktivt insulin (IOB)", + "Current target": "Aktuelt mål", + "Expected effect": "Forventet effekt", + "Expected outcome": "Forventet resultat", + "Carb Equivalent": "Kulhydrat ækvivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Overskydende insulin svarende til %1U mere end nødvendigt for at nå lav målværdi, kulhydrater ikke medregnet", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Overskydende insulin svarende til %1U mere end nødvendigt for at nå lav målværdi, VÆR SIKKER PÅ AT IOB ER DÆKKET IND AF KULHYDRATER", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U reduktion nødvendig i aktiv insulin for at nå lav mål, for meget basal?", + "basal adjustment out of range, give carbs?": "basalændring uden for grænseværdi, giv kulhydrater?", + "basal adjustment out of range, give bolus?": "basalændring udenfor grænseværdi, giv bolus?", + "above high": "over høj", + "below low": "under lav", + "Projected BG %1 target": "Projekteret mål for BG %1", + "aiming at": "sigte mod", + "Bolus %1 units": "Bolus %1 enheder", + "or adjust basal": "eller juster basal", + "Check BG using glucometer before correcting!": "Kontrollere blodsukker med fingerprikker / blodsukkermåler før der korrigeres!", + "Basal reduction to account %1 units:": "Basalsænkning for at nå %1 enheder", + "30m temp basal": "30 minuters midlertidig basal", + "1h temp basal": "60 minutters midlertidig basal", + "Cannula change overdue!": "Tid for skift af infusionssæt overskredet!", + "Time to change cannula": "Tid til at skifte infusionssæt", + "Change cannula soon": "Skift infusionssæt snart", + "Cannula age %1 hours": "Infusionssæt alder %1 timer", + "Inserted": "Isat", + "CAGE": "Indstik alder", + "COB": "COB", + "Last Carbs": "Seneste kulhydrater", + "IAGE": "Insulinalder", + "Insulin reservoir change overdue!": "Tid for skift af insulinreservoir overskredet!", + "Time to change insulin reservoir": "Tid til at skifte insulinreservoir", + "Change insulin reservoir soon": "Skift insulinreservoir snart", + "Insulin reservoir age %1 hours": "Insulinreservoiralder %1 timer", + "Changed": "Skiftet", + "IOB": "IOB", + "Careportal IOB": "IOB i Careportal", + "Last Bolus": "Seneste Bolus", + "Basal IOB": "Basal IOB", + "Source": "Kilde", + "Stale data, check rig?": "Gammel data, kontrollere uploader?", + "Last received:": "Senest modtaget:", + "%1m ago": "%1m siden", + "%1h ago": "%1t siden", + "%1d ago": "%1d siden", + "RETRO": "RETRO", + "SAGE": "Sensoralder", + "Sensor change/restart overdue!": "Sensor skift/genstart overskredet!", + "Time to change/restart sensor": "Tid til at skifte/genstarte sensor", + "Change/restart sensor soon": "Skift eller genstart sensor snart", + "Sensor age %1 days %2 hours": "Sensoralder %1 dage %2 timer", + "Sensor Insert": "Sensor isat", + "Sensor Start": "Sensor start", + "days": "dage", + "Insulin distribution": "Insulinfordeling", + "To see this report, press SHOW while in this view": "For at se denne rapport, klik på \"VIS\"", + "AR2 Forecast": "AR2 Prognose", + "OpenAPS Forecasts": "OpenAPS Prognose", + "Temporary Target": "Midlertidigt mål", + "Temporary Target Cancel": "Annullér midlertidig mål", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profiler", + "Time in fluctuation": "Tid i udsving", + "Time in rapid fluctuation": "Tid i hurtige udsving", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Dette er kun en grov estimering som kan være misvisende. Det erstatter ikke en blodprøve. Formelen er hentet fra:", + "Filter by hours": "Filtrer efter timer", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Tid i udsving og tid i hurtig udsving måler % af tiden i den undersøgte periode, under hvilket blodsukkeret har ændret sig relativt hurtigt. Lavere værdier er bedre.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Middel Total Daglig Ændring er summen af absolutværdier af alla glukoseændringer i den undersøgte periode, divideret med antallet af dage. Lavere er bedre.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Middelværdier per time er summen af absolutværdier fra alle glukoseændringer i den undersøgte periode divideret med antallet af timer. Lavere er bedre.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Ud af RMS beregnes ved at kvadratere afstanden ud af intervallet for alle glucoseaflæsninger for den undersøgte periode, summere dem, dividere med tæller og tage kvadratroden. Denne måling svarer til in-range procenten, men vægte aflæsninger langt ud af rækkevidde højere. Lavere værdier er bedre.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">kan findes her.", + "Mean Total Daily Change": "Middel Total Daglig Ændring", + "Mean Hourly Change": "Middelværdier per time", + "FortyFiveDown": "svagt faldende", + "FortyFiveUp": "svagt stigende", + "Flat": "stabilt", + "SingleUp": "stigende", + "SingleDown": "faldende", + "DoubleDown": "hurtigt faldende", + "DoubleUp": "hurtigt stigende", + "virtAsstUnknown": "Denne værdi er ukendt i øjeblikket. Se venligst dit Nightscout website for flere detaljer.", + "virtAsstTitleAR2Forecast": "AR2 Prognose", + "virtAsstTitleCurrentBasal": "Nuværende Basal", + "virtAsstTitleCurrentCOB": "Nuværende COB", + "virtAsstTitleCurrentIOB": "Nuværende IOB", + "virtAsstTitleLaunch": "Velkommen til Nightscout", + "virtAsstTitleLoopForecast": "Loop Prognose", + "virtAsstTitleLastLoop": "Sidste Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Prognose", + "virtAsstTitlePumpReservoir": "Resterende Insulin", + "virtAsstTitlePumpBattery": "Pumpe batteri", + "virtAsstTitleRawBG": "Nuværende rå BG", + "virtAsstTitleUploaderBattery": "Uploader batteri", + "virtAsstTitleCurrentBG": "Nuværende BG", + "virtAsstTitleFullStatus": "Fuld Status", + "virtAsstTitleCGMMode": "CGM mode", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Sessionsalder", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM transmitter alder", + "virtAsstTitleCGMNoise": "CGM Støj", + "virtAsstTitleDelta": "Blodsukker Delta", + "virtAsstStatus": "%1 og %2 af %3.", + "virtAsstBasal": "%1 nuværende basal er %2 enheder per time", + "virtAsstBasalTemp": "%1 midlertidig basal af %2 enheder per time stopper %3", + "virtAsstIob": "og du har %1 aktivt insulin.", + "virtAsstIobIntent": "Du har %1 aktivt insulin", + "virtAsstIobUnits": "%1 enheder af", + "virtAsstLaunch": "Hvad ønsker du at tjekke på Nightscout?", + "virtAsstPreamble": "Dine", + "virtAsstPreamble3person": "%1 har en ", + "virtAsstNoInsulin": "nej", + "virtAsstUploadBattery": "Din uploaders batteri er %1", + "virtAsstReservoir": "Du har %1 enheder tilbage", + "virtAsstPumpBattery": "Din pumpes batteri er %1 %2", + "virtAsstUploaderBattery": "Din uploaders batteri er %1", + "virtAsstLastLoop": "Seneste successfulde loop var %1", + "virtAsstLoopNotAvailable": "Loop plugin lader ikke til at være slået til", + "virtAsstLoopForecastAround": "Ifølge Loops prognose forventes du at blive around %1 i den næste %2", + "virtAsstLoopForecastBetween": "Ifølge Loops prognose forventes du at blive between %1 and %2 i den næste %3", + "virtAsstAR2ForecastAround": "Ifølge AR2-prognosen forventes du at være omkring %1 i løbet af de næste %2", + "virtAsstAR2ForecastBetween": "Ifølge AR2-prognosen forventes du at være mellem %1 og %2 i løbet af de næste %3", + "virtAsstForecastUnavailable": "Det er ikke muligt at forudsige med de tilgængelige data", + "virtAsstRawBG": "Dit rå BG er %1", + "virtAsstOpenAPSForecast": "OpenAPS's forventet BG er %1", + "virtAsstCob3person": "%1 har %2 aktive kulhydrater", + "virtAsstCob": "Du har %1 aktive kulhydrater", + "virtAsstCGMMode": "Din CGM-mode var %1 pr. %2.", + "virtAsstCGMStatus": "Din CGM-status var %1 fra %2.", + "virtAsstCGMSessAge": "Din CGM-session har været aktiv i %1 dage og %2 timer.", + "virtAsstCGMSessNotStarted": "Der er ingen aktiv CGM-session i øjeblikket.", + "virtAsstCGMTxStatus": "Din CGM transmitter status var %1 fra %2.", + "virtAsstCGMTxAge": "Din CGM-sender er %1 dage gammel.", + "virtAsstCGMNoise": "Din CGM-støj var %1 pr. %2.", + "virtAsstCGMBattOne": "Dit CGM batteri var %1 volt fra %2.", + "virtAsstCGMBattTwo": "Dine CGM batteriniveauer var %1 volt og %2 volt fra %3.", + "virtAsstDelta": "Dit delta var %1 mellem %2 og %3.", + "virtAsstDeltaEstimated": "Dit anslåede delta var %1 mellem %2 og %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "Beklager, jeg ved ikke, hvad du beder om.", + "Fat [g]": "Fedt [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energi [kJ]", + "Clock Views:": "Ur visninger:", + "Clock": "Klokken", + "Color": "Farve", + "Simple": "Simpel", + "TDD average": "Gennemsnitlig TDD", + "Bolus average": "Bolus gennemsnit", + "Basal average": "Basal gennemsnit", + "Base basal average:": "Base basal gennemsnit:", + "Carbs average": "Kulhydrater gennemsnit", + "Eating Soon": "Spiser snart", + "Last entry {0} minutes ago": "Seneste post {0} minutter siden", + "change": "ændre", + "Speech": "Tale", + "Target Top": "Højt mål", + "Target Bottom": "Lavt mål", + "Canceled": "Annulleret", + "Meter BG": "Blodsukkermåler BG", + "predicted": "forventet", + "future": "fremtid", + "ago": "siden", + "Last data received": "Sidste data modtaget", + "Clock View": "Ur visning", + "Protein": "Protein", + "Fat": "Fedt", + "Protein average": "Gennemsnitlig protein", + "Fat average": "Fedt gennemsnit", + "Total carbs": "Kulhydrater i alt", + "Total protein": "Protein i alt", + "Total fat": "Fedt i alt", + "Database Size": "Databasestørrelse", + "Database Size near its limits!": "Database størrelse nær dens grænser!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database størrelse er %1 MiB ud af %2 MiB. Lav en backup og rydde op i databasen!", + "Database file size": "Database filstørrelse", + "%1 MiB of %2 MiB (%3%)": "%1 MiB af %2 MiB (%3%)", + "Data size": "Data størrelse", + "virtAsstDatabaseSize": "%1 MiB. Det er %2% af den tilgængelige databaseplads.", + "virtAsstTitleDatabaseSize": "Database filstørrelse", + "Carbs/Food/Time": "Kulhydrater/mad/tid", + "You have administration messages": "Du har administrative beskeder", + "Admin messages in queue": "Admin beskeder i kø", + "Queue empty": "Kø tom", + "There are no admin messages in queue": "Der er ingen admin-beskeder i kø", + "Please sign in using the API_SECRET to see your administration messages": "Log venligst ind med API_SECRET for at se dine administrationsmeddelelser", + "Reads enabled in default permissions": "'Reads' aktiveret i standard tilladelser", + "Data reads enabled": "Data 'reads' aktiveret", + "Data writes enabled": "Data 'writes' aktiveret", + "Data writes not enabled": "Data 'writes' ikke aktiveret", + "Color prediction lines": "Farve prognose linjer", + "Release Notes": "Udgivelsesnoter", + "Check for Updates": "Søg efter opdateringer", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Det primære formål med Loopalyzer er at visualisere, hvordan Loop lukket-loop system udføres. Det kan virke med andre opsætninger samt, både lukket og åben loop, og ikke loop. Men afhængigt af, hvilken uploader du bruger, hvor ofte det er i stand til at fange dine data og uploade, og hvordan det er i stand til at fylde manglende data kan nogle grafer kan have huller eller endda være helt tom. Sikre altid at graferne ser fornuftige ud. Bedst er at se en dag ad gangen og rulle gennem en række dage først at se.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer indeholder en tidsforskydning funktion. Hvis du for eksempel har morgenmad kl 07:00 en dag og kl 08:00 dagen efter din gennemsnitlige blodglukose kurve disse to dage vil sandsynligvis se fladt og ikke vise den faktiske respons efter en morgenmad. Tidsforskydning vil beregne den gennemsnitlige tid disse måltider blev spist og derefter skifte alle data (kulhydrater, insulin, basal osv. i begge dage den tilsvarende tidsforskel, således at begge måltider stemmer overens med den gennemsnitlige starttid for måltidet.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "I dette eksempel er alle data fra første dag skubbet 30 minutter frem i tid og alle data fra anden dag 30 minutter tilbage i tid, så det ser ud som om du havde haft morgenmad på 07:30 begge dage. Dette giver dig mulighed for at se din gennemsnitlige blodsukkerrespons fra et måltid.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Tidsforskydning fremhæver perioden efter det gennemsnitlige måltid starttidspunkt i grå, i varigheden af DIA (Duration of Insulin Action). Da alle datapunkter hele dagen forskydes kurverne uden for det grå område er muligvis ikke nøjagtige.", + "Note that time shift is available only when viewing multiple days.": "Bemærk, at tidsforskydning kun er tilgængelig, når du ser flere dage.", + "Please select a maximum of two weeks duration and click Show again.": "Vælg højst to ugers varighed og klik på Vis igen.", + "Show profiles table": "Vis profil tabellen", + "Show predictions": "Vis forudsigelser", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Tidsforskydning på måltider større end %1 g kulhydrater forbruges mellem %2 og %3", + "Previous": "Forrige", + "Previous day": "Forrige dag", + "Next day": "Næste dag", + "Next": "Næste", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Godkendt af token", + "Auth role": "Auth rolle", + "view without token": "se uden token", + "Remove stored token": "Fjern gemt token", + "Weekly Distribution": "Ugentlig Distribution", + "Failed authentication": "Mislykket godkendelse", + "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?": "En enhed på IP-adressen %1 forsøgte at godkende med Nightscout med forkerte legitimationsoplysninger. Tjek om du har en uploader opsætning med forkert API_SECRET eller token?", + "Default (with leading zero and U)": "Standard (med foranstillet nul og U)", + "Concise (with U, without leading zero)": "Kortfattet (med U, uden foranstillet nul)", + "Minimal (without leading zero and U)": "Minimal (uden foranstillet nul og U)", + "Small Bolus Display": "Lille bolus visning", + "Large Bolus Display": "Stor bolus visning", + "Bolus Display Threshold": "Bolus visning grænse", + "%1 U and Over": "%1 U og over", + "Event repeated %1 times.": "Hændelsen skete %1 gange.", + "minutes": "minutter", + "Last recorded %1 %2 ago.": "Sidst optaget %1 %2 siden.", + "Security issue": "Problemer med sikkerhed", + "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.": "Svag API_SECRET fundet. For at reducere risikoen for uautoriseret adgang, så brug venligst en blanding af små og STORE bogstaver, tal og ikke-alfanumeriske tegn som !#%&/ . Den mindste længde af API_SECRET er 12 tegn.", + "less than 1": "mindre end 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB adgangskode og API_SECRET match. Dette er en virkelig dårlig idé. Skift begge og genbrug ikke adgangskoder på tværs af systemet." +} diff --git a/translations/de_DE.json b/translations/de_DE.json new file mode 100644 index 00000000000..c3e707f585b --- /dev/null +++ b/translations/de_DE.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Lauscht auf Port", + "Mo": "Mo", + "Tu": "Di", + "We": "Mi", + "Th": "Do", + "Fr": "Fr", + "Sa": "Sa", + "Su": "So", + "Monday": "Montag", + "Tuesday": "Dienstag", + "Wednesday": "Mittwoch", + "Thursday": "Donnerstag", + "Friday": "Freitag", + "Saturday": "Samstag", + "Sunday": "Sonntag", + "Category": "Kategorie", + "Subcategory": "Unterkategorie", + "Name": "Name", + "Today": "Heute", + "Last 2 days": "Letzten 2 Tage", + "Last 3 days": "Letzten 3 Tage", + "Last week": "Letzte Woche", + "Last 2 weeks": "Letzten 2 Wochen", + "Last month": "Letzter Monat", + "Last 3 months": "Letzten 3 Monate", + "From": "Von", + "To": "Bis", + "Notes": "Notiz", + "Food": "Ernährung", + "Insulin": "Insulin", + "Carbs": "Kohlenhydrate", + "Notes contain": "Notizen enthalten", + "Target BG range bottom": "Vorgabe unteres BG-Ziel", + "top": "oben", + "Show": "Zeigen", + "Display": "Anzeigen", + "Loading": "Laden", + "Loading profile": "Lade Profil", + "Loading status": "Lade Status", + "Loading food database": "Lade Ernährungs-Datenbank", + "not displayed": "nicht angezeigt", + "Loading CGM data of": "Lade CGM-Daten von", + "Loading treatments data of": "Lade Behandlungsdaten von", + "Processing data of": "Verarbeite Daten von", + "Portion": "Abschnitt", + "Size": "Größe", + "(none)": "(nichts)", + "None": "Nichts", + "": "", + "Result is empty": "Ergebnis ist leer", + "Day to day": "Von Tag zu Tag", + "Week to week": "Woche zu Woche", + "Daily Stats": "Tägliche Statistik", + "Percentile Chart": "Perzentil-Diagramm", + "Distribution": "Verteilung", + "Hourly stats": "Stündliche Statistik", + "netIOB stats": "netIOB Statistiken", + "temp basals must be rendered to display this report": "temporäre Basalraten müssen für diesen Report sichtbar sein", + "No data available": "Keine Daten verfügbar", + "Low": "Tief", + "In Range": "Im Zielbereich", + "Period": "Zeitabschnitt", + "High": "Hoch", + "Average": "Mittelwert", + "Low Quartile": "Unteres Quartil", + "Upper Quartile": "Oberes Quartil", + "Quartile": "Quartil", + "Date": "Datum", + "Normal": "Regulär", + "Median": "Median", + "Readings": "Messwerte", + "StDev": "StAbw", + "Daily stats report": "Tagesstatistik Bericht", + "Glucose Percentile report": "Glukose-Perzentil Bericht", + "Glucose distribution": "Glukose Verteilung", + "days total": "Gesamttage", + "Total per day": "Gesamt pro Tag", + "Overall": "Insgesamt", + "Range": "Bereich", + "% of Readings": "% der Messwerte", + "# of Readings": "Anzahl der Messwerte", + "Mean": "Mittelwert", + "Standard Deviation": "Standardabweichung", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "Einschätzung HbA1c*", + "Weekly Success": "Wöchentlicher Erfolg", + "There is not sufficient data to run this report. Select more days.": "Für diesen Bericht sind nicht genug Daten verfügbar. Bitte weitere Tage auswählen.", + "Using stored API secret hash": "Gespeicherte API-Prüfsumme verwenden", + "No API secret hash stored yet. You need to enter API secret.": "Keine API-Prüfsumme gespeichert. Bitte API-Prüfsumme eingeben.", + "Database loaded": "Datenbank geladen", + "Error: Database failed to load": "Fehler: Datenbank konnte nicht geladen werden", + "Error": "Fehler", + "Create new record": "Erstelle neuen Datensatz", + "Save record": "Speichere Datensatz", + "Portions": "Portionen", + "Unit": "Einheit", + "GI": "GI", + "Edit record": "Bearbeite Datensatz", + "Delete record": "Lösche Datensatz", + "Move to the top": "Gehe zum Anfang", + "Hidden": "Verborgen", + "Hide after use": "Verberge nach Gebrauch", + "Your API secret must be at least 12 characters long": "Dein API-Geheimnis muss mindestens 12 Zeichen lang sein", + "Bad API secret": "Fehlerhaftes API-Geheimnis", + "API secret hash stored": "API-Geheimnis-Prüfsumme gespeichert", + "Status": "Status", + "Not loaded": "Nicht geladen", + "Food Editor": "Nahrungsmittel-Editor", + "Your database": "Deine Datenbank", + "Filter": "Filter", + "Save": "Speichern", + "Clear": "Löschen", + "Record": "Datensatz", + "Quick picks": "Schnellauswahl", + "Show hidden": "Verborgenes zeigen", + "Your API secret or token": "Dein API-Geheimnis oder Token", + "Remember this device. (Do not enable this on public computers.)": "An dieses Gerät erinnern. (Nicht auf öffentlichen Geräten verwenden)", + "Treatments": "Behandlungen", + "Time": "Zeit", + "Event Type": "Ereignis-Typ", + "Blood Glucose": "Blutglukose", + "Entered By": "Eingabe durch", + "Delete this treatment?": "Diese Behandlung löschen?", + "Carbs Given": "Kohlenhydratgabe", + "Insulin Given": "Insulingabe", + "Event Time": "Ereignis-Zeit", + "Please verify that the data entered is correct": "Bitte Daten auf Plausibilität prüfen", + "BG": "BZ", + "Use BG correction in calculation": "Verwende BG-Korrektur zur Berechnung", + "BG from CGM (autoupdated)": "Blutglukose vom CGM (Auto-Update)", + "BG from meter": "Blutzucker vom Messgerät", + "Manual BG": "BG von Hand", + "Quickpick": "Schnellauswahl", + "or": "oder", + "Add from database": "Ergänze aus Datenbank", + "Use carbs correction in calculation": "Verwende Kohlenhydrate-Korrektur zur Berechnung", + "Use COB correction in calculation": "Verwende verzehrte Kohlenhydrate zur Berechnung", + "Use IOB in calculation": "Verwende Insulin on Board zur Berechnung", + "Other correction": "Weitere Korrektur", + "Rounding": "Gerundet", + "Enter insulin correction in treatment": "Insulin Korrektur zur Behandlung eingeben", + "Insulin needed": "Benötigtes Insulin", + "Carbs needed": "Benötigte Kohlenhydrate", + "Carbs needed if Insulin total is negative value": "Benötigte Kohlenhydrate sofern Gesamtinsulin einen negativen Wert aufweist", + "Basal rate": "Basalrate", + "60 minutes earlier": "60 Minuten früher", + "45 minutes earlier": "45 Minuten früher", + "30 minutes earlier": "30 Minuten früher", + "20 minutes earlier": "20 Minuten früher", + "15 minutes earlier": "15 Minuten früher", + "Time in minutes": "Zeit in Minuten", + "15 minutes later": "15 Minuten später", + "20 minutes later": "20 Minuten später", + "30 minutes later": "30 Minuten später", + "45 minutes later": "45 Minuten später", + "60 minutes later": "60 Minuten später", + "Additional Notes, Comments": "Ergänzende Hinweise/Kommentare", + "RETRO MODE": "RETRO-MODUS", + "Now": "Jetzt", + "Other": "Weitere", + "Submit Form": "Formular absenden", + "Profile Editor": "Profil-Editor", + "Reports": "Berichte", + "Add food from your database": "Ergänze Nahrung aus Deiner Datenbank", + "Reload database": "Datenbank nachladen", + "Add": "Hinzufügen", + "Unauthorized": "Unbefugt", + "Entering record failed": "Eingabe Datensatz fehlerhaft", + "Device authenticated": "Gerät authentifiziert", + "Device not authenticated": "Gerät nicht authentifiziert", + "Authentication status": "Authentifizierungsstatus", + "Authenticate": "Authentifizieren", + "Remove": "Entfernen", + "Your device is not authenticated yet": "Dein Gerät ist noch nicht authentifiziert", + "Sensor": "Sensor", + "Finger": "Finger", + "Manual": "Von Hand", + "Scale": "Skalierung", + "Linear": "Linear", + "Logarithmic": "Logarithmisch", + "Logarithmic (Dynamic)": "Logarithmisch (dynamisch)", + "Insulin-on-Board": "Aktives Insulin", + "Carbs-on-Board": "Aktiv wirksame Kohlenhydrate", + "Bolus Wizard Preview": "Bolus-Assistent Vorschau (BWP)", + "Value Loaded": "Geladener Wert", + "Cannula Age": "Kanülenalter", + "Basal Profile": "Basalraten-Profil", + "Silence for 30 minutes": "Lautlos für 30 Minuten", + "Silence for 60 minutes": "Lautlos für 60 Minuten", + "Silence for 90 minutes": "Lautlos für 90 Minuten", + "Silence for 120 minutes": "Lautlos für 120 Minuten", + "Settings": "Einstellungen", + "Units": "Einheiten", + "Date format": "Datumsformat", + "12 hours": "12 Stunden", + "24 hours": "24 Stunden", + "Log a Treatment": "Behandlung eingeben", + "BG Check": "BG-Messung", + "Meal Bolus": "Mahlzeiten-Bolus", + "Snack Bolus": "Snack-Bolus", + "Correction Bolus": "Korrektur-Bolus", + "Carb Correction": "Kohlenhydrat-Korrektur", + "Note": "Bemerkung", + "Question": "Frage", + "Exercise": "Bewegung", + "Pump Site Change": "Pumpen-Katheter Wechsel", + "CGM Sensor Start": "CGM Sensor Start", + "CGM Sensor Stop": "CGM Sensor Stopp", + "CGM Sensor Insert": "CGM Sensor Wechsel", + "Sensor Code": "Sensor-Code", + "Transmitter ID": "Transmitter-ID", + "Dexcom Sensor Start": "Dexcom Sensor Start", + "Dexcom Sensor Change": "Dexcom Sensor Wechsel", + "Insulin Cartridge Change": "Insulin Ampullenwechsel", + "D.A.D. Alert": "Diabeteswarnhund-Alarm", + "Glucose Reading": "Glukosemesswert", + "Measurement Method": "Messmethode", + "Meter": "BZ-Messgerät", + "Amount in grams": "Menge in Gramm", + "Amount in units": "Anzahl in Einheiten", + "View all treatments": "Zeige alle Behandlungen", + "Enable Alarms": "Alarme einschalten", + "Pump Battery Change": "Pumpenbatterie wechseln", + "Pump Battery Age": "Alter der Pumpenbatterie", + "Pump Battery Low Alarm": "Pumpenbatterie niedrig Alarm", + "Pump Battery change overdue!": "Pumpenbatterie Wechsel überfällig!", + "When enabled an alarm may sound.": "Sofern eingeschaltet ertönt ein Alarm", + "Urgent High Alarm": "Akuter Hoch Alarm", + "High Alarm": "Hoch Alarm", + "Low Alarm": "Niedrig Alarm", + "Urgent Low Alarm": "Akuter Niedrig Alarm", + "Stale Data: Warn": "Warnung: Daten nicht mehr gültig", + "Stale Data: Urgent": "Achtung: Daten nicht mehr gültig", + "mins": "Minuten", + "Night Mode": "Nacht Modus", + "When enabled the page will be dimmed from 10pm - 6am.": "Sofern aktiviert wird die Anzeige von 22h - 6h gedimmt", + "Enable": "Aktivieren", + "Show Raw BG Data": "Zeige Roh-BG Daten", + "Never": "Nie", + "Always": "Immer", + "When there is noise": "Sofern Sensorrauschen vorhanden", + "When enabled small white dots will be displayed for raw BG data": "Bei Aktivierung erscheinen kleine weiße Punkte für Roh-BG Daten", + "Custom Title": "Benutzerdefinierter Titel", + "Theme": "Aussehen", + "Default": "Standard", + "Colors": "Farben", + "Colorblind-friendly colors": "Farbenblind-freundliche Darstellung", + "Reset, and use defaults": "Zurücksetzen und Voreinstellungen verwenden", + "Calibrations": "Kalibrierung", + "Alarm Test / Smartphone Enable": "Alarm Test / Smartphone aktivieren", + "Bolus Wizard": "Bolus-Assistent", + "in the future": "in der Zukunft", + "time ago": "seit Kurzem", + "hr ago": "Stunde her", + "hrs ago": "Stunden her", + "min ago": "Minute her", + "mins ago": "Minuten her", + "day ago": "Tag her", + "days ago": "Tage her", + "long ago": "lange her", + "Clean": "Löschen", + "Light": "Leicht", + "Medium": "Mittel", + "Heavy": "Stark", + "Treatment type": "Behandlungstyp", + "Raw BG": "Roh-BG", + "Device": "Gerät", + "Noise": "Rauschen", + "Calibration": "Kalibrierung", + "Show Plugins": "Zeige Plugins", + "About": "Über", + "Value in": "Wert in", + "Carb Time": "Kohlenhydrat-Zeit", + "Language": "Sprache", + "Add new": "Neu hinzufügen", + "g": "g", + "ml": "ml", + "pcs": "Stk.", + "Drag&drop food here": "Mahlzeit hierher verschieben", + "Care Portal": "Behandlungs-Portal", + "Medium/Unknown": "Mittel/Unbekannt", + "IN THE FUTURE": "IN DER ZUKUNFT", + "Order": "Reihenfolge", + "oldest on top": "älteste oben", + "newest on top": "neueste oben", + "All sensor events": "Alle Sensor-Ereignisse", + "Remove future items from mongo database": "Entferne zukünftige Objekte aus Mongo-Datenbank", + "Find and remove treatments in the future": "Finde und entferne zukünftige Behandlungen", + "This task find and remove treatments in the future.": "Finde und entferne Behandlungen in der Zukunft.", + "Remove treatments in the future": "Entferne Behandlungen in der Zukunft", + "Find and remove entries in the future": "Finde und entferne Einträge in der Zukunft", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Finde und entferne CGM-Daten in der Zukunft, die vom Uploader mit falschem Datum/Uhrzeit erstellt wurden.", + "Remove entries in the future": "Entferne Einträge in der Zukunft", + "Loading database ...": "Lade Datenbank ...", + "Database contains %1 future records": "Datenbank enthält %1 zukünftige Einträge", + "Remove %1 selected records?": "Lösche ausgewählten %1 Eintrag?", + "Error loading database": "Fehler beim Laden der Datenbank", + "Record %1 removed ...": "Eintrag %1 entfernt ...", + "Error removing record %1": "Fehler beim Entfernen des Eintrags %1", + "Deleting records ...": "Entferne Einträge ...", + "%1 records deleted": "%1 Einträge gelöscht", + "Clean Mongo status database": "Bereinige Mongo Status-Datenbank", + "Delete all documents from devicestatus collection": "Lösche alle Dokumente der Gerätestatus-Sammlung", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Diese Aufgabe entfernt alle Dokumente aus der Gerätestatus-Sammlung. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.", + "Delete all documents": "Lösche alle Dokumente", + "Delete all documents from devicestatus collection?": "Löschen aller Dokumente der Gerätestatus-Sammlung?", + "Database contains %1 records": "Datenbank enthält %1 Einträge", + "All records removed ...": "Alle Einträge entfernt...", + "Delete all documents from devicestatus collection older than 30 days": "Alle Dokumente der Gerätestatus-Sammlung löschen, die älter als 30 Tage sind", + "Number of Days to Keep:": "Daten löschen, die älter sind (in Tagen) als:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Diese Aufgabe entfernt alle Dokumente aus der Gerätestatus-Sammlung, die älter sind als 30 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.", + "Delete old documents from devicestatus collection?": "Alte Dokumente aus der Gerätestatus-Sammlung entfernen?", + "Clean Mongo entries (glucose entries) database": "Mongo-Einträge (Glukose-Einträge) Datenbank bereinigen", + "Delete all documents from entries collection older than 180 days": "Alle Dokumente aus der Einträge-Sammlung löschen, die älter sind als 180 Tage", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Diese Aufgabe entfernt alle Dokumente aus der Einträge-Sammlung, die älter sind als 180 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.", + "Delete old documents": "Alte Dokumente löschen", + "Delete old documents from entries collection?": "Alte Dokumente aus der Einträge-Sammlung entfernen?", + "%1 is not a valid number": "%1 ist keine gültige Zahl", + "%1 is not a valid number - must be more than 2": "%1 ist keine gültige Zahl - Eingabe muss größer als 2 sein", + "Clean Mongo treatments database": "Mongo-Behandlungsdatenbank bereinigen", + "Delete all documents from treatments collection older than 180 days": "Alle Dokumente aus der Behandlungs-Sammlung löschen, die älter sind als 180 Tage", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Diese Aufgabe entfernt alle Dokumente aus der Behandlungs-Sammlung, die älter sind als 180 Tage. Nützlich wenn der Uploader-Batteriestatus sich nicht aktualisiert.", + "Delete old documents from treatments collection?": "Alte Dokumente aus der Behandlungs-Sammlung entfernen?", + "Admin Tools": "Administrator-Werkzeuge", + "Nightscout reporting": "Nightscout-Berichte", + "Cancel": "Abbrechen", + "Edit treatment": "Bearbeite Behandlung", + "Duration": "Dauer", + "Duration in minutes": "Dauer in Minuten", + "Temp Basal": "Temporäre Basalrate", + "Temp Basal Start": "Start Temporäre Basalrate", + "Temp Basal End": "Ende Temporäre Basalrate", + "Percent": "Prozent", + "Basal change in %": "Basalratenänderung in %", + "Basal value": "Basalrate", + "Absolute basal value": "Absoluter Basalratenwert", + "Announcement": "Ankündigung", + "Loading temp basal data": "Lade temporäre Basaldaten", + "Save current record before changing to new?": "Aktuelle Einträge speichern?", + "Profile Switch": "Profil wechseln", + "Profile": "Profil", + "General profile settings": "Allgemeine Profileinstellungen", + "Title": "Überschrift", + "Database records": "Datenbankeinträge", + "Add new record": "Neuen Eintrag hinzufügen", + "Remove this record": "Diesen Eintrag löschen", + "Clone this record to new": "Diesen Eintrag duplizieren", + "Record valid from": "Eintrag gültig ab", + "Stored profiles": "Gespeicherte Profile", + "Timezone": "Zeitzone", + "Duration of Insulin Activity (DIA)": "Dauer der Insulinaktivität (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Entspricht der typischen Dauer, die das Insulin wirkt. Variiert je nach Patient und Art des Insulins. Häufig 3-4 Stunden für die meisten Pumpeninsuline und die meisten Patienten. Manchmal auch Insulin-Wirkungsdauer genannt.", + "Insulin to carb ratio (I:C)": "Insulin/Kohlenhydrate-Verhältnis (I:KH)", + "Hours:": "Stunden:", + "hours": "Stunden", + "g/hour": "g/h", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g Kohlenhydrate pro Einheit Insulin. Das Verhältnis wie viele Gramm Kohlenhydrate je Einheit Insulin verbraucht werden.", + "Insulin Sensitivity Factor (ISF)": "Insulinsensibilitätsfaktor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL oder mmol/L pro Einheit Insulin. Verhältnis von BG-Veränderung je Einheit Korrekturinsulin.", + "Carbs activity / absorption rate": "Kohlenhydrataktivität / Kohlenhydrat-Absorptionsrate", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "Gramm pro Zeiteinheit. Bedeutet sowohl die Änderung in COB je Zeiteinheit, als auch die Menge an Kohlenhydraten, die über diese Zeit wirken sollten. Kohlenhydrat-Absorption / Aktivitätskurven werden weniger genau verstanden als Insulinaktivität, aber sie können angenähert werden indem eine Anfangsverzögerung mit konstanter Aufnahme (g/Std.) verwendet wird.", + "Basal rates [unit/hour]": "Basalraten [Einheit/h]", + "Target BG range [mg/dL,mmol/L]": "Blutzucker-Zielbereich [mg/dL, mmol/L]", + "Start of record validity": "Beginn der Aufzeichnungsgültigkeit", + "Icicle": "Eiszapfen", + "Render Basal": "Basalraten-Darstellung", + "Profile used": "Verwendetes Profil", + "Calculation is in target range.": "Berechnung ist innerhalb des Zielbereichs", + "Loading profile records ...": "Lade Profilaufzeichnungen ...", + "Values loaded.": "Werte geladen.", + "Default values used.": "Standardwerte werden verwendet.", + "Error. Default values used.": "Fehler. Standardwerte werden verwendet.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Zeitspannen vom untern und oberen Zielwert stimmen nicht überein. Werte auf Standard zurückgesetzt.", + "Valid from:": "Gültig ab:", + "Save current record before switching to new?": "Aktuellen Datensatz speichern?", + "Add new interval before": "Neues Intervall vorher hinzufügen", + "Delete interval": "Intervall löschen", + "I:C": "IE:KH", + "ISF": "ISF", + "Combo Bolus": "Verzögerter Bolus", + "Difference": "Unterschied", + "New time": "Neue Zeit", + "Edit Mode": "Bearbeitungsmodus", + "When enabled icon to start edit mode is visible": "Wenn aktiviert wird das Icons zum Start des Bearbeitungsmodus sichtbar", + "Operation": "Operation", + "Move": "Verschieben", + "Delete": "Löschen", + "Move insulin": "Insulin verschieben", + "Move carbs": "Kohlenhydrate verschieben", + "Remove insulin": "Insulin löschen", + "Remove carbs": "Kohlenhydrate löschen", + "Change treatment time to %1 ?": "Behandlungsdauer ändern zu %1 ?", + "Change carbs time to %1 ?": "Kohlenhydrat-Zeit ändern zu %1 ?", + "Change insulin time to %1 ?": "Insulinzeit ändern zu %1 ?", + "Remove treatment ?": "Behandlung löschen?", + "Remove insulin from treatment ?": "Insulin der Behandlung löschen?", + "Remove carbs from treatment ?": "Kohlenhydrate der Behandlung löschen?", + "Rendering": "Darstellung", + "Loading OpenAPS data of": "Lade OpenAPS-Daten von", + "Loading profile switch data": "Lade Daten Profil-Wechsel", + "Redirecting you to the Profile Editor to create a new profile.": "Sie werden zum Profil-Editor weitergeleitet, um ein neues Profil anzulegen.", + "Pump": "Pumpe", + "Sensor Age": "Sensor-Alter", + "Insulin Age": "Insulin-Alter", + "Temporary target": "Temporäres Ziel", + "Reason": "Begründung", + "Eating soon": "Bald essen", + "Top": "Oben", + "Bottom": "Unten", + "Activity": "Aktivität", + "Targets": "Ziele", + "Bolus insulin:": "Bolus Insulin:", + "Base basal insulin:": "Basis Basal Insulin:", + "Positive temp basal insulin:": "Positives temporäres Basal Insulin:", + "Negative temp basal insulin:": "Negatives temporäres Basal Insulin:", + "Total basal insulin:": "Gesamt Basal Insulin:", + "Total daily insulin:": "Gesamtes tägliches Insulin:", + "Unable to save Role": "Rolle kann nicht gespeichert werden", + "Unable to delete Role": "Rolle nicht löschbar", + "Database contains %1 roles": "Datenbank enthält %1 Rollen", + "Edit Role": "Rolle editieren", + "admin, school, family, etc": "Administrator, Schule, Familie, etc", + "Permissions": "Berechtigungen", + "Are you sure you want to delete: ": "Sind sie sicher, das Sie löschen wollen:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Jede Rolle hat eine oder mehrere Berechtigungen. Die * Berechtigung ist ein Platzhalter, Berechtigungen sind hierarchisch mit : als Separator.", + "Add new Role": "Eine neue Rolle hinzufügen", + "Roles - Groups of People, Devices, etc": "Rollen - Gruppierungen von Menschen, Geräten, etc.", + "Edit this role": "Editiere diese Rolle", + "Admin authorized": "als Administrator autorisiert", + "Subjects - People, Devices, etc": "Subjekte - Menschen, Geräte, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Jedes Subjekt erhält einen einzigartigen Zugriffsschlüssel und eine oder mehrere Rollen. Klicke auf den Zugriffsschlüssel, um eine neue Ansicht mit dem ausgewählten Subjekt zu erhalten. Dieser geheime Link kann geteilt werden.", + "Add new Subject": "Füge ein neues Subjekt hinzu", + "Unable to save Subject": "Subjekt kann nicht gespeichert werden", + "Unable to delete Subject": "Kann Subjekt nicht löschen", + "Database contains %1 subjects": "Datenbank enthält %1 Subjekte", + "Edit Subject": "Editiere Subjekt", + "person, device, etc": "Person, Gerät, etc", + "role1, role2": "Rolle1, Rolle2", + "Edit this subject": "Editiere dieses Subjekt", + "Delete this subject": "Lösche dieses Subjekt", + "Roles": "Rollen", + "Access Token": "Zugriffsschlüssel (Token)", + "hour ago": "vor einer Stunde", + "hours ago": "vor mehreren Stunden", + "Silence for %1 minutes": "Ruhe für %1 Minuten", + "Check BG": "BZ kontrollieren", + "BASAL": "BASAL", + "Current basal": "Aktuelle Basalrate", + "Sensitivity": "Empfindlichkeit", + "Current Carb Ratio": "Aktuelles KH-Verhältnis", + "Basal timezone": "Basal Zeitzone", + "Active profile": "Aktives Profil", + "Active temp basal": "Aktive temp. Basalrate", + "Active temp basal start": "Start aktive temp. Basalrate", + "Active temp basal duration": "Dauer aktive temp. Basalrate", + "Active temp basal remaining": "Verbleibene Dauer temp. Basalrate", + "Basal profile value": "Basal-Profil Wert", + "Active combo bolus": "Aktiver verzögerter Bolus", + "Active combo bolus start": "Start des aktiven verzögerten Bolus", + "Active combo bolus duration": "Dauer des aktiven verzögerten Bolus", + "Active combo bolus remaining": "Verbleibender aktiver verzögerter Bolus", + "BG Delta": "BZ Differenz", + "Elapsed Time": "Vergangene Zeit", + "Absolute Delta": "Absolute Differenz", + "Interpolated": "Interpoliert", + "BWP": "Bolus-Assistent (BWP)", + "Urgent": "Akut", + "Warning": "Warnung", + "Info": "Info", + "Lowest": "Niedrigster", + "Snoozing high alarm since there is enough IOB": "Ignoriere Alarm hoch, da genügend aktives Insulin (IOB) vorhanden", + "Check BG, time to bolus?": "BZ kontrollieren, ggf. Bolus abgeben?", + "Notice": "Notiz", + "required info missing": "Benötigte Information(en) fehl(t/en)", + "Insulin on Board": "Aktives Insulin", + "Current target": "Aktueller Zielbereich", + "Expected effect": "Erwarteter Effekt", + "Expected outcome": "Erwartetes Ergebnis", + "Carb Equivalent": "Kohlenhydrat-Äquivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Überschüssiges Insulin: %1U mehr als zum Erreichen der Untergrenze notwendig, Kohlenhydrate sind unberücksichtigt", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Überschüssiges Insulin: %1 IE mehr als zum Erreichen der Untergrenze notwendig. SICHERSTELLEN, DASS IOB DURCH KOHLENHYDRATE ABGEDECKT WIRD", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Aktives Insulin um %1U reduzieren, um Untergrenze zu erreichen. Basal zu hoch?", + "basal adjustment out of range, give carbs?": "Basalrate geht außerhalb des Zielbereiches. Kohlenhydrate nehmen?", + "basal adjustment out of range, give bolus?": "Anpassung der Basalrate außerhalb des Zielbereichs. Bolus abgeben?", + "above high": "oberhalb Obergrenze", + "below low": "unterhalb Untergrenze", + "Projected BG %1 target": "Erwarteter BZ %1", + "aiming at": "angestrebt werden", + "Bolus %1 units": "Bolus von %1 Einheiten", + "or adjust basal": "oder Basal anpassen", + "Check BG using glucometer before correcting!": "Überprüfe deinen BZ mit dem Messgerät, bevor du eine Korrektur vornimmst!", + "Basal reduction to account %1 units:": "Reduktion der Basalrate um %1 Einheiten zu kompensieren:", + "30m temp basal": "30min temporäres Basal", + "1h temp basal": "1h temporäres Basal", + "Cannula change overdue!": "Kanülenwechsel überfällig!", + "Time to change cannula": "Es ist Zeit, die Kanüle zu wechseln", + "Change cannula soon": "Kanüle bald wechseln", + "Cannula age %1 hours": "Kanülen Alter %1 Stunden", + "Inserted": "Eingesetzt", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Letzte Kohlenhydrate", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Ampullenwechsel überfällig!", + "Time to change insulin reservoir": "Es ist Zeit die Ampulle zu wechseln", + "Change insulin reservoir soon": "Ampulle bald wechseln", + "Insulin reservoir age %1 hours": "Ampullen Alter %1 Stunden", + "Changed": "Gewechselt", + "IOB": "IOB", + "Careportal IOB": "Behandlungs-Portal IOB", + "Last Bolus": "Letzter Bolus", + "Basal IOB": "Basal IOB", + "Source": "Quelle", + "Stale data, check rig?": "Daten sind veraltet, Übertragungsgerät prüfen?", + "Last received:": "Zuletzt empfangen:", + "%1m ago": "vor %1m", + "%1h ago": "vor %1h", + "%1d ago": "vor %1d", + "RETRO": "Rückschau", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Sensorwechsel/-neustart überfällig!", + "Time to change/restart sensor": "Es ist Zeit den Sensor zu wechseln/neuzustarten", + "Change/restart sensor soon": "Sensor bald wechseln/neustarten", + "Sensor age %1 days %2 hours": "Sensor Alter %1 Tage %2 Stunden", + "Sensor Insert": "Sensor eingesetzt", + "Sensor Start": "Sensorstart", + "days": "Tage", + "Insulin distribution": "Insulinverteilung", + "To see this report, press SHOW while in this view": "Auf ZEIGEN drücken, um den Bericht in dieser Ansicht anzuzeigen", + "AR2 Forecast": "AR2-Vorhersage", + "OpenAPS Forecasts": "OpenAPS-Vorhersage", + "Temporary Target": "Temporäres Ziel", + "Temporary Target Cancel": "Temporäres Ziel abbrechen", + "OpenAPS Offline": "OpenAPS offline", + "Profiles": "Profile", + "Time in fluctuation": "Zeit in Fluktuation (Schwankung)", + "Time in rapid fluctuation": "Zeit in starker Fluktuation (Schwankung)", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Dies ist lediglich eine grobe Schätzung, die sehr ungenau sein kann und eine Überprüfung des tatsächlichen Blutzuckers nicht ersetzen kann. Die verwendete Formel wurde genommen von:", + "Filter by hours": "Filtern nach Stunden", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Zeit in Fluktuation und Zeit in starker Fluktuation messen den Teil der Zeit, in der sich der Blutzuckerwert relativ oder sehr schnell verändert hat. Niedrigere Werte sind besser.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Die gesamte mittlere Änderung pro Tag ist die Summe der absoluten Werte aller Glukoseveränderungen im Betrachtungszeitraum geteilt durch die Anzahl der Tage. Niedrigere Werte sind besser.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Die mittlere Änderung pro Stunde ist die Summe der absoluten Werte aller Glukoseveränderungen im Betrachtungszeitraum geteilt durch die Anzahl der Stunden. Niedrigere Werte sind besser.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Der \"außerhalb des Zielbereichs RMS-Wert\" wird berechnet, indem der Abstand außerhalb des Zielbereichs für alle Glukosemesswerte im untersuchten Zeitraum quadriert, addiert, durch die Anzahl geteilt und die Quadratwurzel genommen wird. Diese Metrik ähnelt dem Im-Zielbereich-Prozentsatz, gewichtet aber Messwerte weit außerhalb des Bereichs höher. Niedrigere Werte sind besser.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">hier.", + "Mean Total Daily Change": "Gesamte mittlere Änderung pro Tag", + "Mean Hourly Change": "Mittlere Änderung pro Stunde", + "FortyFiveDown": "leicht sinkend", + "FortyFiveUp": "leicht steigend", + "Flat": "gleichbleibend", + "SingleUp": "steigend", + "SingleDown": "sinkend", + "DoubleDown": "schnell sinkend", + "DoubleUp": "schnell steigend", + "virtAsstUnknown": "Dieser Wert ist momentan unbekannt. Prüfe Deine Nightscout-Webseite für weitere Details!", + "virtAsstTitleAR2Forecast": "AR2-Vorhersage", + "virtAsstTitleCurrentBasal": "Aktuelles Basalinsulin", + "virtAsstTitleCurrentCOB": "Aktuelle Kohlenhydrate", + "virtAsstTitleCurrentIOB": "Aktuelles Restinsulin", + "virtAsstTitleLaunch": "Willkommen bei Nightscout", + "virtAsstTitleLoopForecast": "Loop-Vorhersage", + "virtAsstTitleLastLoop": "Letzter Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS-Vorhersage", + "virtAsstTitlePumpReservoir": "Verbleibendes Insulin", + "virtAsstTitlePumpBattery": "Pumpenbatterie", + "virtAsstTitleRawBG": "Aktueller Blutzucker-Rohwert", + "virtAsstTitleUploaderBattery": "Uploader Batterie", + "virtAsstTitleCurrentBG": "Aktueller Blutzucker", + "virtAsstTitleFullStatus": "Gesamtstatus", + "virtAsstTitleCGMMode": "CGM Modus", + "virtAsstTitleCGMStatus": "CGM-Status", + "virtAsstTitleCGMSessionAge": "CGM Sitzungs-Alter", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM-Transmitteralter", + "virtAsstTitleCGMNoise": "CGM Sensorrauschen", + "virtAsstTitleDelta": "Blutzucker-Delta", + "virtAsstStatus": "%1 und bis %3 %2.", + "virtAsstBasal": "%1 aktuelle Basalrate ist %2 Einheiten je Stunde", + "virtAsstBasalTemp": "%1 temporäre Basalrate von %2 Einheiten endet %3", + "virtAsstIob": "und Du hast %1 Insulin wirkend.", + "virtAsstIobIntent": "Du hast noch %1 Insulin wirkend", + "virtAsstIobUnits": "noch %1 Einheiten", + "virtAsstLaunch": "Was möchtest du von Nightscout wissen?", + "virtAsstPreamble": "Deine", + "virtAsstPreamble3person": "%1 hat eine", + "virtAsstNoInsulin": "kein", + "virtAsstUploadBattery": "Der Akku Deines Uploader-Handys ist bei %1", + "virtAsstReservoir": "Du hast %1 Einheiten übrig", + "virtAsstPumpBattery": "Der Batteriestand deiner Pumpe ist bei %1 %2", + "virtAsstUploaderBattery": "Der Akku deines Uploaders ist bei %1", + "virtAsstLastLoop": "Der letzte erfolgreiche Loop war %1", + "virtAsstLoopNotAvailable": "Das Loop-Plugin scheint nicht aktiviert zu sein", + "virtAsstLoopForecastAround": "Entsprechend der Loop-Vorhersage wirst in den nächsten %2 bei %1 sein", + "virtAsstLoopForecastBetween": "Entsprechend der Loop-Vorhersage wirst du zwischen %1 und %2 während der nächsten %3 sein", + "virtAsstAR2ForecastAround": "Entsprechend der AR2-Vorhersage wirst du in %2 bei %1 sein", + "virtAsstAR2ForecastBetween": "Entsprechend der AR2-Vorhersage wirst du in %3 zwischen %1 and %2 sein", + "virtAsstForecastUnavailable": "Mit den verfügbaren Daten ist eine Loop-Vorhersage nicht möglich", + "virtAsstRawBG": "Dein Rohblutzucker ist %1", + "virtAsstOpenAPSForecast": "Der von OpenAPS vorhergesagte Blutzucker ist %1", + "virtAsstCob3person": "%1 hat %2 Kohlenhydrate wirkend", + "virtAsstCob": "Du hast noch %1 Kohlenhydrate wirkend", + "virtAsstCGMMode": "Ihr CGM Modus von %2 war %1.", + "virtAsstCGMStatus": "Ihr CGM Status von %2 war %1.", + "virtAsstCGMSessAge": "Deine CGM Sitzung ist seit %1 Tagen und %2 Stunden aktiv.", + "virtAsstCGMSessNotStarted": "Derzeit gibt es keine aktive CGM Sitzung.", + "virtAsstCGMTxStatus": "Dein CGM Status von %2 war %1.", + "virtAsstCGMTxAge": "Dein CGM Transmitter ist %1 Tage alt.", + "virtAsstCGMNoise": "Dein CGM Sensorrauschen von %2 war %1.", + "virtAsstCGMBattOne": "Dein CGM Akku von %2 war %1 Volt.", + "virtAsstCGMBattTwo": "Deine CGM Akkustände von %3 waren %1 Volt und %2 Volt.", + "virtAsstDelta": "Dein Delta war %1 zwischen %2 und %3.", + "virtAsstDeltaEstimated": "Dein geschätztes Delta war %1 zwischen %2 und %3.", + "virtAsstUnknownIntentTitle": "Unbekanntes Vorhaben", + "virtAsstUnknownIntentText": "Tut mir leid, ich hab Deine Frage nicht verstanden.", + "Fat [g]": "Fett [g]", + "Protein [g]": "Proteine [g]", + "Energy [kJ]": "Energie [kJ]", + "Clock Views:": "Uhr-Anzeigen:", + "Clock": "Uhr", + "Color": "Farbe", + "Simple": "Einfach", + "TDD average": "durchschnittliches Insulin pro Tag (TDD)", + "Bolus average": "Bolus-Durchschnitt", + "Basal average": "Basal-Durchschnitt", + "Base basal average:": "Basis-Basal-Durchschnitt:", + "Carbs average": "durchschnittliche Kohlenhydrate pro Tag", + "Eating Soon": "Bald Essen", + "Last entry {0} minutes ago": "Letzter Eintrag vor {0} Minuten", + "change": "verändern", + "Speech": "Sprache", + "Target Top": "Oberes Ziel", + "Target Bottom": "Unteres Ziel", + "Canceled": "Abgebrochen", + "Meter BG": "Wert Blutzuckermessgerät", + "predicted": "vorhergesagt", + "future": "Zukunft", + "ago": "vor", + "Last data received": "Zuletzt Daten empfangen", + "Clock View": "Uhr-Anzeige", + "Protein": "Protein", + "Fat": "Fett", + "Protein average": "Proteine Durchschnitt", + "Fat average": "Fett Durchschnitt", + "Total carbs": "Kohlenhydrate gesamt", + "Total protein": "Protein gesamt", + "Total fat": "Fett gesamt", + "Database Size": "Datenbankgröße", + "Database Size near its limits!": "Datenbank fast voll!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Die Datenbankgröße beträgt %1 MiB von %2 MiB. Bitte sichere deine Daten und bereinige die Datenbank!", + "Database file size": "Datenbank-Dateigröße", + "%1 MiB of %2 MiB (%3%)": "%1 MiB von %2 MiB (%3%)", + "Data size": "Datengröße", + "virtAsstDatabaseSize": "%1 MiB. Das sind %2% des verfügbaren Datenbank-Speicherplatzes.", + "virtAsstTitleDatabaseSize": "Datenbank-Dateigröße", + "Carbs/Food/Time": "Kohlenhydrate/Nahrung/Zeit", + "You have administration messages": "Neue Admin-Nachrichten liegen vor", + "Admin messages in queue": "Admin-Nachrichten in Warteschlange", + "Queue empty": "Warteschlange leer", + "There are no admin messages in queue": "Keine Admin-Nachrichten in der Warteschlange", + "Please sign in using the API_SECRET to see your administration messages": "Bitte melde dich mit dem API_SECRET an, um die Admin-Nachrichten sehen zu können", + "Reads enabled in default permissions": "Leseberechtigung aktiviert durch Standardberechtigungen", + "Data reads enabled": "Daten lesen aktiviert", + "Data writes enabled": "Datenschreibvorgänge aktiviert", + "Data writes not enabled": "Datenschreibvorgänge deaktiviert", + "Color prediction lines": "Farbige Vorhersage-Linien", + "Release Notes": "Versionshinweise", + "Check for Updates": "Nach Updates suchen", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Der Hauptzweck von Loopalyzer ist die Visualisierung der Leistung des Loop-Closed-Loop-Systems. Es kann auch mit anderen Setups funktionieren, sowohl mit geschlossener als auch mit offener Loop und ohne Loop. Abhängig davon, welchen Uploader Du verwendest, wie häufig Deine Daten erfasst und hochgeladen werden können und wie fehlende Daten nachgefüllt werden können, können einige Diagramme Lücken aufweisen oder sogar vollständig leer sein. Stelle immer sicher, dass die Grafiken angemessen aussehen. Am besten sehe Dir einen Tag nach dem anderen an und scrolle zuerst durch einige Tage, um sie zu betrachten.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer enthält eine Zeitverschiebungsfunktion. Wenn Du beispielsweise an einem Tag um 07:00 Uhr und an einem anderen Tag um 08:00 Uhr frühstückst, sehen die durchschnittlichen Blutzuckerkurven beiden Tage höchstwahrscheinlich abgeflacht aus und zeigen nach dem Frühstück nicht die tatsächliche Reaktion. Die Zeitverschiebung berechnet die durchschnittliche Zeit, in der diese Mahlzeiten gegessen wurden, und verschiebt dann alle Daten (Kohlenhydrate, Insulin, Basal usw.) an beiden Tagen um den entsprechenden Zeitunterschied, sodass beide Mahlzeiten mit der durchschnittlichen Startzeit der Mahlzeit übereinstimmen.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In diesem Beispiel werden alle Daten vom ersten Tag 30 Minuten vorwärts und alle Daten vom zweiten Tag 30 Minuten rückwärts verschoben, sodass es so aussieht, als hättest Du an beiden Tagen um 07:30 Uhr gefrühstückt. Auf diese Weise kannst Du Deine tatsächliche durchschnittliche Blutzuckerreaktion nach einer Mahlzeit anzeigen.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Die Zeitverschiebung hebt den Zeitraum nach der durchschnittlichen Beginnzeit der Mahlzeit für die Dauer der DIA (Dauer der Insulinwirkung) grau hervor. Da alle Datenpunkte den ganzen Tag verschoben sind, sind die Kurven außerhalb des grauen Bereichs möglicherweise nicht genau.", + "Note that time shift is available only when viewing multiple days.": "Beachte, dass die Zeitverschiebung nur verfügbar ist, wenn mehrere Tage angezeigt werden.", + "Please select a maximum of two weeks duration and click Show again.": "Bitte wählen Sie eine maximale Dauer von zwei Wochen aus und klicke erneut auf Anzeigen.", + "Show profiles table": "Profiltabelle anzeigen", + "Show predictions": "Vorhersage anzeigen", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Zeitverschiebung bei Mahlzeiten mit mehr als %1 g Kohlenhydraten, die zwischen %2 und %3 verzehrt werden", + "Previous": "Vorherige", + "Previous day": "Vorheriger Tag", + "Next day": "Nächster Tag", + "Next": "Nächste", + "Temp basal delta": "Temporäre Basal Delta", + "Authorized by token": "Autorisiert durch Token", + "Auth role": "Auth-Rolle", + "view without token": "Ohne Token anzeigen", + "Remove stored token": "Gespeichertes Token entfernen", + "Weekly Distribution": "Wöchentliche Verteilung", + "Failed authentication": "Authentifizierung fehlgeschlagen", + "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?": "Ein Gerät mit der IP-Adresse %1 hat versucht sich mit falschen Zugangsdaten bei Nightscout anzumelden. Prüfe, ob einer deiner Uploader ein falsches API_SECRET oder Token verwendet.", + "Default (with leading zero and U)": "Standard (mit führender Null und U)", + "Concise (with U, without leading zero)": "Knapp (mit U, ohne führende Null)", + "Minimal (without leading zero and U)": "Minimal (ohne führende Null und U)", + "Small Bolus Display": "Kleine Bolusanzeige", + "Large Bolus Display": "Große Bolusanzeige", + "Bolus Display Threshold": "Schwellenwert für Bolusanzeige", + "%1 U and Over": "%1 U und darüber", + "Event repeated %1 times.": "Ereignis %1 mal wiederholt.", + "minutes": "Minuten", + "Last recorded %1 %2 ago.": "Zuletzt vor %1 %2 aufgezeichnet.", + "Security issue": "Sicherheitsproblem", + "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.": "Schwaches Passwort API_SECRET erkannt. Bitte verwende eine Mischung aus Klein- und Groß--Buchstaben, Zahlen und nicht-alphanumerischen Zeichen wie !#%&/ um das Risiko eines unberechtigten Zugriffs zu verringern. Die Mindestlänge des API_SECRET beträgt 12 Zeichen.", + "less than 1": "kleiner als 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB-Passwort und API_SECRET stimmen überein. Dies ist eine wirklich schlechte Idee. Bitte ändern Sie beide Passwörter und verwenden Sie Passwörter nicht mehrfach." +} diff --git a/translations/el_GR.json b/translations/el_GR.json new file mode 100644 index 00000000000..6d6fba18c14 --- /dev/null +++ b/translations/el_GR.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Παρακολούθηση θύρας", + "Mo": "Δε", + "Tu": "Τρ", + "We": "Τε", + "Th": "Πε", + "Fr": "Πα", + "Sa": "Σα", + "Su": "Κυ", + "Monday": "Δευτέρα", + "Tuesday": "Τρίτη", + "Wednesday": "Τετάρτη", + "Thursday": "Πέμπτη", + "Friday": "Παρασκευή", + "Saturday": "Σάββατο", + "Sunday": "Κυριακή", + "Category": "Κατηγορία", + "Subcategory": "Υποκατηγορία", + "Name": "Όνομα", + "Today": "Σήμερα", + "Last 2 days": "Τελευταίες 2 μέρες", + "Last 3 days": "Τελευταίες 3 μέρες", + "Last week": "Τελευταία εβδομάδα", + "Last 2 weeks": "Τελευταίες 2 εβδομάδες", + "Last month": "Τελευταίος μήνας", + "Last 3 months": "Τελευταίοι 3 μήνες", + "From": "Από", + "To": "Έως", + "Notes": "Σημειώσεις", + "Food": "Φαγητό", + "Insulin": "Ινσουλίνη", + "Carbs": "Υδατάνθρακες", + "Notes contain": "Οι σημειώσεις περιέχουν", + "Target BG range bottom": "Στόχος γλυκόζης - Ελάχιστη τιμή", + "top": "Πάνω όριο", + "Show": "Εμφάνιση", + "Display": "Εμφάνιση", + "Loading": "Φορτώνει", + "Loading profile": "Ανάκτηση Προφίλ", + "Loading status": "Φόρτωση Κατάστασης", + "Loading food database": "Φόρτωση βάσης δεδομένων φαγητών", + "not displayed": "Δεν απεικονίζεται", + "Loading CGM data of": "Φόρτωση δεδομένων CGM", + "Loading treatments data of": "Φόρτωση δεδομένων ενεργειών", + "Processing data of": "Επεξεργασία Δεδομένων", + "Portion": "Μερίδα", + "Size": "Μέγεθος", + "(none)": "(κενό)", + "None": "Κενό", + "": "<κενό>", + "Result is empty": "Το αποτέλεσμα είναι κενό", + "Day to day": "Ανά ημέρα", + "Week to week": "Ανά εβδομάδα", + "Daily Stats": "Στατιστικά ημέρας", + "Percentile Chart": "Ποσοστιαίο γράφημα (%)", + "Distribution": "Κατανομή", + "Hourly stats": "Ωριαία Στατιστικά", + "netIOB stats": "στατιστικά netIOB", + "temp basals must be rendered to display this report": "οι προσωρινοί βασικοί ρυθμοί είναι απαραίτητοι για την προβολή της αναφοράς", + "No data available": "Τα δεδομένα δεν είναι διαθέσιμα", + "Low": "Χαμηλό", + "In Range": "Εντός στόχου", + "Period": "Περίοδος", + "High": "Υψηλό", + "Average": "Μέση τιμή", + "Low Quartile": "Χαμηλότερο Τεταρτημόριο", + "Upper Quartile": "Υψηλότερο τεταρτημόριο", + "Quartile": "Τεταρτημόριο", + "Date": "Ημερομηνία", + "Normal": "Εντός Στόχου", + "Median": "Διάμεσος", + "Readings": "Μετρήσεις", + "StDev": "Τυπική Απόκλιση", + "Daily stats report": "Αναφορά ημερήσιων στατιστικών", + "Glucose Percentile report": "Ποσοστιαία αναφορά γλυκόζης", + "Glucose distribution": "Κατανομή Τιμών Γλυκόζης", + "days total": "ημέρες συνολικά", + "Total per day": "Σύνολο ανά ημέρα", + "Overall": "Σύνολο", + "Range": "Εύρος τιμών", + "% of Readings": "% μετρήσεων", + "# of Readings": "# μετρήσεων", + "Mean": "Μέσος Όρος", + "Standard Deviation": "Τυπική Απόκλιση", + "Max": "Μέγιστο", + "Min": "Ελάχιστο", + "A1c estimation*": "Εκτίμηση HbA1c", + "Weekly Success": "Εβδομαδιαία Κατανομή", + "There is not sufficient data to run this report. Select more days.": "Δεν υπάρχουν επαρκή δεδομένα για αυτή την αναφορά. Επιλέξτε περισσότερες ημέρες.", + "Using stored API secret hash": "Χρηση αποθηκευμένου συνθηματικού", + "No API secret hash stored yet. You need to enter API secret.": "Δεν υπάρχει αποθηκευμένο συνθηματικό API. Πρέπει να εισάγετε το συνθηματικό API", + "Database loaded": "Φόρτωση βάσης δεδομένων", + "Error: Database failed to load": "Σφάλμα: Αποτυχία φόρτωσης της Βάσης δεδομένων", + "Error": "Σφάλμα", + "Create new record": "Δημιουργία νέας εγγραφής", + "Save record": "Αποθήκευση εγγραφής", + "Portions": "Μερίδες", + "Unit": "Μονάδα", + "GI": "Γλυκαιμικός Δείκτης", + "Edit record": "Επεξεργασία εγγραφής", + "Delete record": "Διαγραφή εγγραφής", + "Move to the top": "Μετακίνηση στην κορυφή", + "Hidden": "Απόκρυψη", + "Hide after use": "Απόκρυψη μετά τη χρήση", + "Your API secret must be at least 12 characters long": "Το API secret πρέπει να έχει τουλάχιστον 12 χαρακτήρες", + "Bad API secret": "Λανθασμένο API secret", + "API secret hash stored": "Το συνθηματικό αποθηκεύτηκε", + "Status": "Κατάσταση", + "Not loaded": "Δεν έγινε μεταφόρτωση", + "Food Editor": "Επεξεργασία Δεδομένων Φαγητών", + "Your database": "Η Βάση Δεδομένων σας", + "Filter": "Φίλτρο", + "Save": "Αποθήκευση", + "Clear": "Καθαρισμός", + "Record": "Εγγραφή", + "Quick picks": "Γρήγορες επιλογές", + "Show hidden": "Εμφάνιση κρυφών εγγραφών", + "Your API secret or token": "Το API secret ή το token σας", + "Remember this device. (Do not enable this on public computers.)": "Αποθήκευση κωδικού σε αυτή την συσκευή. (Μην το επιλέγετε σε κοινόχρηστους υπολογιστές.)", + "Treatments": "Θεραπείες", + "Time": "Ώρα", + "Event Type": "Ενέργεια", + "Blood Glucose": "Γλυκόζη Αίματος", + "Entered By": "Εισαγωγή από", + "Delete this treatment?": "Διαγραφή θεραπείας;", + "Carbs Given": "Υδατάνθρακες που δόθηκαν", + "Insulin Given": "Ινσουλίνη που δόθηκε", + "Event Time": "Ώρα ενέργειας", + "Please verify that the data entered is correct": "Παρακαλώ ελέγξτε ότι τα δεδομένα είναι σωστά", + "BG": "Γλυκόζη αίματος", + "Use BG correction in calculation": "Χρήση της διόρθωσης της τιμής γλυκόζης για τον υπολογισμό", + "BG from CGM (autoupdated)": "Τιμή γλυκόζης από τον αισθητήρα (αυτόματη ενημέρωση)", + "BG from meter": "Τιμή γλυκόζης από τον μετρητή", + "Manual BG": "Χειροκίνητη εισαγωγή τιμής γλυκόζης", + "Quickpick": "Γρήγορη επιλογή", + "or": "ή", + "Add from database": "Προσθήκη από τη βάση δεδομένων", + "Use carbs correction in calculation": "Χρήση της διόρθωσης με υδατάνθρακες στον υπολογισμό", + "Use COB correction in calculation": "Χρήση των υδατανθράκων που απομένουν για τον υπολογισμό", + "Use IOB in calculation": "Χρήση της ινσουλίνης που έχει απομείνει για τον υπολογισμό", + "Other correction": "Άλλη διόρθωση", + "Rounding": "Στρογγυλοποίηση", + "Enter insulin correction in treatment": "Υπολογισθείσα ποσότητα ινσουλίνης που απαιτείται", + "Insulin needed": "Απαιτούμενη ινσουλίνη", + "Carbs needed": "Απαιτούμενοι υδατάνθρακες", + "Carbs needed if Insulin total is negative value": "Απαιτούμενοι υδατάνθρακες εάν η συνολική ινσουλίνη έχει αρνητική τιμή", + "Basal rate": "Βασικός ρυθμός", + "60 minutes earlier": "60 λεπτά νωρίτερα", + "45 minutes earlier": "45 λεπτά νωρίτερα", + "30 minutes earlier": "30 λεπτά νωρίτερα", + "20 minutes earlier": "20 λεπτά νωρίτερα", + "15 minutes earlier": "15 λεπτά νωρίτερα", + "Time in minutes": "Ώρα σε λεπτά", + "15 minutes later": "15 λεπτά αργότερα", + "20 minutes later": "20 λεπτά αργότερα", + "30 minutes later": "30 λεπτά αργότερα", + "45 minutes later": "45 λεπτά αργότερα", + "60 minutes later": "60 λεπτά αργότερα", + "Additional Notes, Comments": "Επιπλέον σημειώσεις/σχόλια", + "RETRO MODE": "Ιστορικά στοιχεία", + "Now": "Τώρα", + "Other": "Άλλο", + "Submit Form": "Υποβολή Φόρμας", + "Profile Editor": "Επεξεργασία Προφίλ", + "Reports": "Αναφορές", + "Add food from your database": "Προσθήκη φαγητού από τη Βάση Δεδομένων", + "Reload database": "Επαναφόρτωση Βάσης Δεδομένων", + "Add": "Προσθήκη", + "Unauthorized": "Χωρίς εξουσιοδότηση", + "Entering record failed": "Η εισαγωγή της εγγραφής απέτυχε", + "Device authenticated": "Η συσκευή ταυτοποιήθηκε", + "Device not authenticated": "Μη ταυτοποιημένη συσκευή", + "Authentication status": "Κατάσταση ταυτοποίησης", + "Authenticate": "Πιστοποίηση", + "Remove": "Αφαίρεση", + "Your device is not authenticated yet": "Η συκευή σας δεν έχει ταυτοποιηθεί ακόμα", + "Sensor": "Αισθητήρας", + "Finger": "Δάχτυλο", + "Manual": "Χειροκίνητο", + "Scale": "Κλίμακα", + "Linear": "Γραμμική", + "Logarithmic": "Λογαριθμική", + "Logarithmic (Dynamic)": "Λογαριθμική (Δυναμική)", + "Insulin-on-Board": "Ενεργή Ινσουλίνη (IOB)", + "Carbs-on-Board": "Ενεργοί Υδατάνθρακες (COB)", + "Bolus Wizard Preview": "Οδηγός υπολογισμού bolus ινσουλίνης (ΒWP)", + "Value Loaded": "Φόρτωση τιμής", + "Cannula Age": "Ημέρες Χρήσης Κάνουλας (CAGE)", + "Basal Profile": "Προφίλ βασικού ρυθμού ινσουλίνης", + "Silence for 30 minutes": "Σίγαση για 30 λεπτά", + "Silence for 60 minutes": "Σίγαση για 60 λεπτά", + "Silence for 90 minutes": "Σίγαση για 90 λεπτά", + "Silence for 120 minutes": "Σίγαση για 120 λεπτά", + "Settings": "Ρυθμίσεις", + "Units": "Μονάδες", + "Date format": "Μορφή Ώρας", + "12 hours": "12ωρο", + "24 hours": "24ωρο", + "Log a Treatment": "Καταγραφή Θεραπείας", + "BG Check": "Έλεγχος Γλυκόζης", + "Meal Bolus": "Ινσουλίνη Γεύματος", + "Snack Bolus": "Ινσουλίνη Σνακ", + "Correction Bolus": "Διόρθωση με Ινσουλίνη", + "Carb Correction": "Διόρθωση με Υδατάνθρακες", + "Note": "Σημείωση", + "Question": "Ερώτηση", + "Exercise": "Άσκηση", + "Pump Site Change": "Αλλαγή σημείου αντλίας", + "CGM Sensor Start": "Έναρξη αισθητήρα καταγραφής γλυκόζης", + "CGM Sensor Stop": "Παύση αισθητήρα καταγραφής γλυκόζης", + "CGM Sensor Insert": "Τοποθέτηση αισθητήρα γλυκόζης", + "Sensor Code": "Κωδικός αισθητήρα", + "Transmitter ID": "ID αναμεταδότη", + "Dexcom Sensor Start": "Εκκίνηση αισθητήρα Dexcom", + "Dexcom Sensor Change": "Αλλαγή αισθητήρα Dexcom", + "Insulin Cartridge Change": "Αλλαγή αμπούλας ινσουλίνης", + "D.A.D. Alert": "Προειδοποίηση συνοδού σκύλου για διαβητικό", + "Glucose Reading": "Τιμή Γλυκόζης", + "Measurement Method": "Μέθοδος Μέτρησης", + "Meter": "Μετρητής", + "Amount in grams": "Ποσότητα σε γραμμάρια", + "Amount in units": "Ποσότητα σε μονάδες", + "View all treatments": "Προβολή όλων των θεραπειών", + "Enable Alarms": "Ενεργοποίηση συναγερμών", + "Pump Battery Change": "Αλλαγή μπαταρίας αντλίας", + "Pump Battery Age": "Χρόνος ζωής μπαταρίας αντλίας", + "Pump Battery Low Alarm": "Συναγερμός χαμηλής στάθμης μπαταρίας", + "Pump Battery change overdue!": "Η αλλαγή της μπαταρίας της αντλίας έχει καθυστερήσει!", + "When enabled an alarm may sound.": "Όταν είναι ενεργοποιημένο, μπορεί να ακουστεί συναγερμός.", + "Urgent High Alarm": "Ειδοποίηση ιδιαίτερα υψηλής γλυκόζης", + "High Alarm": "Ειδοποίηση υψηλής γλυκόζης", + "Low Alarm": "Ειδοποίηση χαμηλής γλυκόζης", + "Urgent Low Alarm": "Ειδοποίηση ιδιαίτερα χαμηλής γλυκόζης", + "Stale Data: Warn": "Τα δεδομένα δεν ενημερώνονται: Προειδοποίηση", + "Stale Data: Urgent": "Τα δεδομένα δεν ενημερώνονται: Επείγον", + "mins": "λεπτά", + "Night Mode": "Λειτουργία Νυκτός", + "When enabled the page will be dimmed from 10pm - 6am.": "Όταν ενεργοποιηθεί, η φωτεινότητα της οθόνης θα είναι μειωμένη μεταξύ 22.00 - 6.00.", + "Enable": "Ενεργοποίηση", + "Show Raw BG Data": "Εμφάνιση δεδομένων αισθητήρα χωρίς εξομάλυνση", + "Never": "Ποτέ", + "Always": "Πάντα", + "When there is noise": "Όταν υπάρχει θόρυβος στις μετρήσεις", + "When enabled small white dots will be displayed for raw BG data": "Όταν είναι ενεργοποιημένο, μικρές λευκές κουκίδες θα αναπαριστούν τα δεδομένα χωρίς εξομάλυσνη του αισθητήρα", + "Custom Title": "Προσαρμοσμένος τίτλος", + "Theme": "Θέμα απεικόνισης", + "Default": "Αρχική ρύθμιση", + "Colors": "Χρώματα", + "Colorblind-friendly colors": "Χρώματα για χρήστες με αχρωματοψία", + "Reset, and use defaults": "Επαναφορά και χρήση των προκαθορισμένων ρυθμίσεων", + "Calibrations": "Βαθμονομήσεις", + "Alarm Test / Smartphone Enable": "Δοκιμή Συναγερμού / Ενεργοποίηση smartphone", + "Bolus Wizard": "Οδηγός υπολογισμού ινσουλίνης", + "in the future": "στο μέλλον", + "time ago": "πριν από ώρα", + "hr ago": "ώρα πριν", + "hrs ago": "ώρες πριν", + "min ago": "λεπτό πριν", + "mins ago": "λεπτά πριν", + "day ago": "ημέρα πριν", + "days ago": "ημέρες πριν", + "long ago": "πολύ πριν από", + "Clean": "Εκκαθάριση", + "Light": "Ελαφριά", + "Medium": "Μέση", + "Heavy": "Βαριά", + "Treatment type": "Τύπος Θεραπείας", + "Raw BG": "Τιμές γλυκόζης χωρίς εξομάλυνση", + "Device": "Συσκευή", + "Noise": "Θόρυβος μετρήσεων", + "Calibration": "Βαθμονόμηση", + "Show Plugins": "Εμφάνιση πρόσθετων συστήματος", + "About": "Σχετικά με", + "Value in": "Τιμή σε", + "Carb Time": "Ώρα χορήγησης υδ/κων", + "Language": "Γλώσσα", + "Add new": "Προσθήκη νέου", + "g": "g", + "ml": "ml", + "pcs": "τεμάχια", + "Drag&drop food here": "Σύρετε εδώ το φαγητό", + "Care Portal": "Πύλη Φροντίδας", + "Medium/Unknown": "Μέσος/Άγνωστος", + "IN THE FUTURE": "ΣΤΟ ΜΕΛΛΟΝ", + "Order": "Ταξινόμηση", + "oldest on top": "τα παλαιότερα πρώτα", + "newest on top": "τα νεότερα πρώτα", + "All sensor events": "Όλα τα συμβάντα του αισθητήρα", + "Remove future items from mongo database": "Αφαίρεση μελλοντικών εγγραφών από τη βάση δεδομένων mongo", + "Find and remove treatments in the future": "Εύρεση και αφαίρεση μελλοντικών θεραπειών από τη βάση δεδομένων", + "This task find and remove treatments in the future.": "Αυτή η ενέργεια βρίσκει και αφαιρεί θεραπείες στο μέλλον.", + "Remove treatments in the future": "Αφαίρεση μελλοντικών θεραπειών", + "Find and remove entries in the future": "Εύρεση και αφαίρεση μελλοντικών εγγραφών", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Αυτή η ενέργεια βρίσκει και αφαιρεί δεδομένα αιθητήρα τα οποία εισήχθησαν σε μελλοντικό χρόνο από αισθητήρα με λάθος ρυθμίσεις χρόνου/ώρας.", + "Remove entries in the future": "Αφαίρεση μελλοντικών εγγραφών", + "Loading database ...": "Φόρτωση Βάσης Δεδομένων", + "Database contains %1 future records": "Η Βάση Δεδομένων περιέχει %1 μελλοντικές εγγραφές", + "Remove %1 selected records?": "Αφαίρεση %1 των επιλεγμένων εγγραφών;", + "Error loading database": "Σφάλμα στη φόρτωση της βάσης δεδομένων", + "Record %1 removed ...": "%1 εγγραφές αφαιρέθηκαν...", + "Error removing record %1": "Σφάλμα αφαίρεσης %1 εγγραφών", + "Deleting records ...": "Αφαίρεση Εγγραφών...", + "%1 records deleted": "%1 εγγραφών διαγράφηκαν", + "Clean Mongo status database": "Καθαρισμός κατάστασης βάσης δεδομένων Mongo", + "Delete all documents from devicestatus collection": "Διαγραφή όλων των εγγράφων από τη συλλογή καταστάσεων της συσκευής ανάγνωσης του αισθητήρα", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Αυτή η ενέργεια διαγράφει όλα τα έγγραφα της συλλογής με τις καταστάσεις της συσκευής ανάγνωσης. Χρήσιμη όταν η κατάσταση της μπαταρίας της συσκευής ανάγνωσης δεν ανανεώνεται σωστά.", + "Delete all documents": "Διαγραφή όλων των εγγράφων", + "Delete all documents from devicestatus collection?": "Διαγραφή όλων των εγγράφων κατάστασης της συσκευής ανάγνωσης;", + "Database contains %1 records": "Η βάση δεδομένων περιέχει %1 εγγραφές", + "All records removed ...": "Όλες οι εγγραφές διαγράφηκαν...", + "Delete all documents from devicestatus collection older than 30 days": "Διαγραφή όλων των εγγράφων από τη συλλογή καταστάσεων της συσκευής ανάγνωσης του αισθητήρα άνω των 30 ημερών", + "Number of Days to Keep:": "Αριθμός ημερών προς διατήρηση:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Αυτή η ενέργεια διαγράφει όλα τα έγγραφα της κατάστασης της συσκευής ανάγνωσης που είναι παλαιότερα από 30 ημέρες. Χρήσιμη όταν η κατάσταση της μπαταρίας της συσκευής ανάγνωσης δεν ανανεώνεται σωστά.", + "Delete old documents from devicestatus collection?": "Διαγραφή όλων των εγγράφων από τη συλλογή καταστάσεων της συσκευής;", + "Clean Mongo entries (glucose entries) database": "Καθαρίστε τις καταχωρίσεις της βάσης δεδομένων Mongo (καταγραφή γλυκόζης)", + "Delete all documents from entries collection older than 180 days": "Διαγραφή όλων των εγγράφων από τη συλλογή καταχωρίσεων παλαιότερων των 180 ημερών", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Αυτή η ενέργεια αφαιρεί όλα τα έγγραφα από τη συλλογή καταχωρίσεων που είναι παλαιότερα από 180 ημέρες. Χρήσιμη όταν η κατάσταση της μπαταρίας της συσκευής ανάγνωσης δεν ανανεώνεται σωστά.", + "Delete old documents": "Διαγραφή παλαιών εγγράφων", + "Delete old documents from entries collection?": "Διαγραφή παλαιών δεδομένων από τη συλλογή καταχωρίσεων;", + "%1 is not a valid number": "Το %1 δεν είναι έγκυρος αριθμός", + "%1 is not a valid number - must be more than 2": "Το %1 δεν είναι έγκυρος αριθμός - πρέπει να είναι μεγαλύτερος από 2", + "Clean Mongo treatments database": "Εκκαθάριση θεραπειών από τη βάση δεδομένων Mongo", + "Delete all documents from treatments collection older than 180 days": "Διαγραφή όλων των εγγράφων από τη συλλογή θεραπειών ηλικίας παλαιότερων των 180 ημερών", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Αυτή η ενέργεια αφαιρεί όλα τα έγγραφα από τη συλλογή θεραπειών που είναι παλαιότερα από 180 ημέρες. Χρήσιμη όταν η κατάσταση της μπαταρίας της συσκευής ανάγνωσης δεν ανανεώνεται σωστά.", + "Delete old documents from treatments collection?": "Διαγραφή παλαιών εγγράφων από τη συλλογή θεραπειών;", + "Admin Tools": "Εργαλεία Διαχειριστή", + "Nightscout reporting": "Αναφορές του Nightscout", + "Cancel": "Ακύρωση", + "Edit treatment": "Επεξεργασία θεραπείας", + "Duration": "Διάρκεια", + "Duration in minutes": "Διάρκεια σε λεπτά", + "Temp Basal": "Προσωρινός βασικός ρυθμός", + "Temp Basal Start": "Έναρξη προσωρινού βασικού ρυθμού", + "Temp Basal End": "Παύση προσωρινού βασικού", + "Percent": "Ποσοστό", + "Basal change in %": "% Αλλαγής βασικού", + "Basal value": "Τιμή Βασικού ρυθμού", + "Absolute basal value": "Απόλυτη τιμή βασικού ρυθμού", + "Announcement": "Ανακοίνωση", + "Loading temp basal data": "Φόρτωση δεδομένων προσωρινού βασικού ρυθμού", + "Save current record before changing to new?": "Αποθήκευση τρεχουσας εγγραφής πριν την αλλαγή σε νέα;", + "Profile Switch": "Αλλαγή προφίλ", + "Profile": "Προφίλ", + "General profile settings": "Γενικές Ρυθμίσεις Προφίλ", + "Title": "Τίτλος", + "Database records": "Εγραφές Βάσης Δεδομένων", + "Add new record": "Προσθήκη νέας εγγραφής", + "Remove this record": "Αφαίρεση εγγραφής", + "Clone this record to new": "Αντιγραφή εγγραφής σε νέα", + "Record valid from": "Η εγγραφή ισχύει από", + "Stored profiles": "Αποθηκευμένα προφίλ", + "Timezone": "Ζώνη Ώρας", + "Duration of Insulin Activity (DIA)": "Διάρκεια Δράσης Ινσουλίνης (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Αντιπροσωπευει την τυπική διάρκεια δράσης της χορηγηθείσας ινσουλίνης. Διαφοροποιείται ανά ασθενή και ανά τύπο ινσουλίνης. Τυπικά γύρω στις 3-4 ώρες για την ινσουλίνη μέσω αντλίας και τους περισσότερους ασθενείς. Μερικές φορές αναφέρεται ως χρόνος ζωής της ινσουλίνης.", + "Insulin to carb ratio (I:C)": "Αναλογία Ινσουλίνης/Υδατανθράκων (I:C)", + "Hours:": "Ώρες:", + "hours": "ώρες", + "g/hour": "γρ/ώρα", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "γραμμάρια (gr) υδατανθράκων ανά μονάδα (U) ινσουλίνης. Ο λόγος των γραμμαρίων υδατανθράκων που εξουδετερώνονται από κάθε μία μονάδα ινσουλίνης.", + "Insulin Sensitivity Factor (ISF)": "Ευαισθησία στην Ινοσυλίνη (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dl ή mmol/L ανά μονάδα U ινσουλίνης. Ο λόγος της μεταβολής της τιμής της γλυκόζης ανά μια μονάδα ινσουλίνης που δίνεται για διόρθωση.", + "Carbs activity / absorption rate": "Ρυθμός απορρόφησης υδατανθράκων", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "Γραμμάρια ανά μονάδα χρόνου. Αναπαριστά τόσο την μεταβολή του COB στη μονάδα του χρόνου. Οι καμπύλες της απορρόφησης υδατανθράκων και της άσκησης δεν έχουν κατανοηθεί πλήρως από την επιστημονική κοινότητα, αλλά μπορούν να προσεγγιστούν βάζοντας μία αρχική καθυστέρηση ακολουθούμενη από έναν σταθερό ρυθμό απορρόφησης (g/hr).", + "Basal rates [unit/hour]": "Βασικός ρυθμός ινσουλίνης [μον/ώρα]", + "Target BG range [mg/dL,mmol/L]": "Στόχος Γλυκόζης Αίματος [mg/dl,mmol/l]", + "Start of record validity": "Ισχύει από", + "Icicle": "Απεικόνιση Icicle", + "Render Basal": "Απεικόνιση βασικού ρυθμού", + "Profile used": "Προφίλ σε χρήση", + "Calculation is in target range.": "Ο υπολογισμός είναι εντός στόχου", + "Loading profile records ...": "Φόρτωση δεδομένων προφίλ ...", + "Values loaded.": "Φόρτωση δεδομένων.", + "Default values used.": "Χρήση προκαθορισμένων τιμών.", + "Error. Default values used.": "Σφάλμα: Χρήση προκαθορισμένων τιμών.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Το χρονικό διάστημα του χαμηλού_στόχου και του υψηλού_στόχου, δεν συμβαδίζουν. Γίνεται επαναφορά στις προκαθορισμένες τιμές.", + "Valid from:": "Ισχύει από:", + "Save current record before switching to new?": "Αποθήκευση αλλαγών στην εγγραφή πριν γίνει εναλλαγή σε νέα;", + "Add new interval before": "Προσθήκη νέου διαστήματος, πριν από", + "Delete interval": "Διαγραφή διαστήματος", + "I:C": "Ινσ:Υδ", + "ISF": "ISF", + "Combo Bolus": "Εκτεταμένη γευματική ινσουλίνη Bolus", + "Difference": "Διαφορά", + "New time": "Νέα ώρα", + "Edit Mode": "Λειτουργία Επεξεργασίας", + "When enabled icon to start edit mode is visible": "Όταν ενεργοποιηθεί, το εικονίδιο της λειτουργίας επεξεργασίας είναι ορατό", + "Operation": "Λειτουργία", + "Move": "Μετακίνηση", + "Delete": "Διαγραφή", + "Move insulin": "Μετακίνηση ινσουλίνης", + "Move carbs": "Μετακίνηση υδατανθράκων", + "Remove insulin": "Αφαίρεση ινσουλίνης", + "Remove carbs": "Αφαίρεση υδατανθράκων", + "Change treatment time to %1 ?": "Αλλαγή του χρόνου της θεραπείας σε %1 ;", + "Change carbs time to %1 ?": "Αλλαγή του χρόνου πρόσληψης υδατανθράκων σε %1 ;", + "Change insulin time to %1 ?": "Αλλαγή του χρόνου χορήγησης ινσουλίνης σε %1 ;", + "Remove treatment ?": "Διαγραφή θεραπείας ;", + "Remove insulin from treatment ?": "Διαγραφή ινσουλίνης από τη θεραπεία ;", + "Remove carbs from treatment ?": "Διαγραφή των υδατανθράκων από τη θεραπεία ;", + "Rendering": "Απεικόνιση", + "Loading OpenAPS data of": "Φόρτωση δεδομένων OpenAPS του", + "Loading profile switch data": "Φόρτωση εναλλαγής δεδομένων προφίλ", + "Redirecting you to the Profile Editor to create a new profile.": "Ανακατεύθυνση στην επεξεργασία Προφίλ για τη δημιουργία νέου προφίλ.", + "Pump": "Αντλία", + "Sensor Age": "Ηλικία αισθητήρα", + "Insulin Age": "Ηλικία ινσουλίνης", + "Temporary target": "Προσωρινός στόχος", + "Reason": "Αιτία", + "Eating soon": "Εκκίνηση γεύματος", + "Top": "Άνω", + "Bottom": "Κάτω", + "Activity": "Δραστηριότητα", + "Targets": "Στόχοι", + "Bolus insulin:": "Ινσουλίνη γευματική/διόρθωσης:", + "Base basal insulin:": "Βασικός ρυθμός ινσουλίνης:", + "Positive temp basal insulin:": "Θετικός προσωρινός ρυθμός ινσουλίνης:", + "Negative temp basal insulin:": "Αρνητικός προσωρινός ρυθμός ινσουλίνης:", + "Total basal insulin:": "Συνολικός βασικός ρυθμός ινσουλίνης:", + "Total daily insulin:": "Συνολική Ημερήσια Ινσουλίνη", + "Unable to save Role": "Δεν ήταν δυνατή η αποθήκευση του Ρόλου", + "Unable to delete Role": "Δεν είναι δυνατή η διαγραφή του Ρόλου", + "Database contains %1 roles": "Η βάση δεδομένων περιέχει %1 ρόλους", + "Edit Role": "Επεξεργασία ρόλου", + "admin, school, family, etc": "διαχειριστής, σχολείο, οικογένεια, κλπ", + "Permissions": "Δικαιώματα", + "Are you sure you want to delete: ": "Είστε σίγουρος/η ότι θέλετε να διαγράψετε; ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Κάθε ρόλος θα έχει 1 ή περισσότερα δικαιώματα. Το δικαίωμα * είναι ένα μπαλαντέρ, τα δικαιώματα είναι μια ιεραρχία χρησιμοποιώντας : ως διαχωριστικό.", + "Add new Role": "Προσθήκη νέου Ρόλου", + "Roles - Groups of People, Devices, etc": "Ρόλοι - Ομάδες ανθρώπων, Συσκευές, κλπ", + "Edit this role": "Επεξεργασία του ρόλου", + "Admin authorized": "Ο διαχειριστής εξουσιοδοτήθηκε", + "Subjects - People, Devices, etc": "Θέματα - Άνθρωποι, Συσκευές, κλπ", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Κάθε αντικείμενο θα έχει ένα μοναδικό αναγνωριστικό πρόσβασης και 1 ή περισσότερους ρόλους. Κάνετε κλικ στο αναγνωριστικό πρόσβασης για να ανοίξετε μια νέα απεικόνιση με το επιλεγμένο αντικείμενο. Αυτός ο μυστικός σύνδεσης μπορεί να μοιραστεί.", + "Add new Subject": "Προσθήκη νέου Αντικειμένου", + "Unable to save Subject": "Δεν ήταν δυνατή η αποθήκευση του αντικειμένου", + "Unable to delete Subject": "Δεν ήταν δυνατή η διαγραφή του αντικειμένου", + "Database contains %1 subjects": "Η βάση δεδομένων περιέχει %1 αντικείμενα", + "Edit Subject": "Επεξεργασία Αντικειμένου", + "person, device, etc": "πρόσωπο, συσκευή, κλπ", + "role1, role2": "ρόλος1, ρόλος2", + "Edit this subject": "Επεξεργασία αυτού του αντικειμένου", + "Delete this subject": "Διαγραφή αυτού του αντικειμένου", + "Roles": "Ρόλοι", + "Access Token": "Αναγνωριστικό Πρόσβασης", + "hour ago": "ώρα πριν", + "hours ago": "ώρες πριν", + "Silence for %1 minutes": "Σίγαση για %1 λεπτά", + "Check BG": "Έλεγχος γλυκόζης", + "BASAL": "Βασικός ρυθμός", + "Current basal": "Τρέχων βασικός ρυθμός", + "Sensitivity": "Ευαισθησία", + "Current Carb Ratio": "Τρέχουσα Αναλογία Υδατανθράκων", + "Basal timezone": "Ζώνη ώρας βασικού ρυθμού", + "Active profile": "Ενεργό προφίλ", + "Active temp basal": "Ενεργός προσωρινός βασικός ρυθμός", + "Active temp basal start": "Εκκίνηση ενεργού προσωρινού βασικού ρυθμού", + "Active temp basal duration": "Διάρκεια ενεργού προσωρινού βασικού ρυθμού", + "Active temp basal remaining": "Ενεργός προσωρινός βασικός ρυθμός που απομένει", + "Basal profile value": "Τιμή προφίλ βασικού ρυθμού", + "Active combo bolus": "Ενεργή εκτεταμένη ένεση", + "Active combo bolus start": "Έναρξη ενεργής εκτεταμένης ένεσης ινσουλίνης", + "Active combo bolus duration": "Διάρκεια εκτεταμένης ένεσης ινσουλίνης", + "Active combo bolus remaining": "Διάρκεια εκτεταμένης ένεσης ινσουλίνης που απομένει", + "BG Delta": "Διαφορά γλυκόζης", + "Elapsed Time": "Χρόνος που έχει περάσει", + "Absolute Delta": "Απόλυτη τιμή διαφοράς γλυκόζης", + "Interpolated": "Παρεμβολή", + "BWP": "BWP", + "Urgent": "Επείγον", + "Warning": "Προειδοποίηση", + "Info": "Πληροφορίες", + "Lowest": "Ελάχιστο", + "Snoozing high alarm since there is enough IOB": "Αναβολή συναγερμού υψηλής τιμής γλυκόζης καθώς υπάρχει αρκετή IOB", + "Check BG, time to bolus?": "Ελέγξτε τη γλυκόζη. Μήπως είναι ώρα για ένεση ινσουλίνης;", + "Notice": "Σημείωση", + "required info missing": "οι απαιτούμενες πληροφορίες λείπουν", + "Insulin on Board": "Ενεργή Ινσουλίνη", + "Current target": "Τρέχων στόχος", + "Expected effect": "Αναμενόμενο αποτέλεσμα", + "Expected outcome": "Αναμενόμενη έκβαση", + "Carb Equivalent": "Ισοδύναμο υδατανθράκων", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Υπέρβαση ισοδύναμου ινσουλίνης %1 πάνω από ότι χρειάζεται για να επιτευχθεί ο χαμηλός στόχος, χωρίς να υπολογιστούν οι υδατάνθρακες", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Υπέρβαση ισοδύναμου ινσουλίνης %1 πάνω από ότι χρειάζεται για να επιτευχθεί ο χαμηλός στόχος, ΔΙΑΣΦΑΛΙΣΤΕ ΟΤΙ Η ΕΝΕΡΓΗ ΙΝΣΟΥΛΙΝΗ ΚΑΛΥΠΤΕΤΑΙ ΑΠΟ ΥΔΑΤΑΝΘΡΑΚΕΣ", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Απαιτείται %1U μείωση της ενεργής ινσουλίνης για την επίτευξη του χαμηλού στόχου, μήπως είναι υψηλός ο βασικός;", + "basal adjustment out of range, give carbs?": "ρύθμιση βασικού ρυθμού εκτός στόχου, Μήπως χρειάζονται υδατάνθρακες;", + "basal adjustment out of range, give bolus?": "ρύθμιση βασικού ρυθμού εκτός στόχου, Μήπως χρειάζεται ένεση ινσουλίνης;", + "above high": "πάνω από τον υψηλό", + "below low": "κάτω από τον χαμηλό", + "Projected BG %1 target": "Προβλεπόμενη γλυκόζη %1 στόχο", + "aiming at": "στοχεύοντας σε", + "Bolus %1 units": "Κάντε ένεση %1 μονάδες", + "or adjust basal": "ή ρυθμίστε το βασικό ρυθμό", + "Check BG using glucometer before correcting!": "Ελέγξτε τη γλυκόζη με αίμα πριν κάνετε διόρθωση!", + "Basal reduction to account %1 units:": "Μείωση βασικού για να καλύψει %1 μονάδες:", + "30m temp basal": "Προσωρινός βασικός για 30 λεπτά", + "1h temp basal": "Προσωρινός βασικός για 1 ώρα", + "Cannula change overdue!": "Υπέρβαση χρόνου αλλαγής καθετήρα!", + "Time to change cannula": "Ώρα για να αλλαγή καθετήρα", + "Change cannula soon": "Αλλάξτε καθετήρα σύντομα", + "Cannula age %1 hours": "Ηλικία καθετήρα %1 ώρες", + "Inserted": "Εισήχθη", + "CAGE": "CAGE", + "COB": "Ενεργοί υδατάνθρακες", + "Last Carbs": "Τελευταίοι Υδατάνθρακες", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Υπέρβαση χρόνου αλλαγής δεξαμενής ινσουλίνης!", + "Time to change insulin reservoir": "Ώρα για αλλαγή δεξαμενής ινσουλίνης", + "Change insulin reservoir soon": "Αλλάξτε σύντομα τη δεξαμενή ινσουλίνης", + "Insulin reservoir age %1 hours": "Ηλικία δεξαμενής ινσουλίνης %1 ώρες", + "Changed": "Αντικαταστάθηκε", + "IOB": "IOB", + "Careportal IOB": "Πύλη φροντίδας IOB", + "Last Bolus": "Τελευταία ένεση ινσουλίνης", + "Basal IOB": "Βασικός ρυθμός IOB", + "Source": "Προέλευση", + "Stale data, check rig?": "Παλιότερα δεδομένα, έλεγχος συστήματος;", + "Last received:": "Τελευταία λήψη:", + "%1m ago": "%1λ πριν", + "%1h ago": "%1ω πριν", + "%1d ago": "%1d πριν", + "RETRO": "Ιστορικά στοιχεία", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Υπέρβαση χρόνου αλλαγής/επανεκκίνησης αιθητήρα!", + "Time to change/restart sensor": "Ώρα για αλλαγή/επανεκκίνηση αισθητήρα", + "Change/restart sensor soon": "Αλλάξτε/επανεκκινήστε τον αισθητήρα σύντομα", + "Sensor age %1 days %2 hours": "Ηλικία αισθητήρα %1 ημέρες %2 ώρες", + "Sensor Insert": "Εισαγωγή αισθητήρα", + "Sensor Start": "Εκκίνηση αισθητήρα", + "days": "ημέρες", + "Insulin distribution": "Κατανομή ινσουλίνης", + "To see this report, press SHOW while in this view": "Για να δείτε αυτή την αναφορά, πιέστε το κουμπί \"ΕΜΦΑΝΙΣΗ\" όσο βρίσκεστε σε αυτή την προβολή", + "AR2 Forecast": "Πρόβλεψη από AR2", + "OpenAPS Forecasts": "Προβλέψεις OpenAPS", + "Temporary Target": "Προσωρινός στόχος", + "Temporary Target Cancel": "Ακύρωση προσωρινού στόχου", + "OpenAPS Offline": "Το OpenAPS είνα ανενεργό", + "Profiles": "Προφίλ", + "Time in fluctuation": "Χρόνος σε διακύμανση", + "Time in rapid fluctuation": "Χρόνος σε μεγάλη διακύμανση", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Αυτή είναι μια πρόχειρη εκτίμηση που μπορεί να είναι πολύ ανακριβής και δεν υποκαθιστά την πραγματική εξέταση αίματος. Ο τύπος που χρησιμοποιείται είναι από:", + "Filter by hours": "Φίλτρο ανά ώρες", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Ο χρόνος σε διακύμανση και ο χρόνος σε γρήγορη διακύμανση μετρά το % του χρόνου της εξεταζόμενης περιόδου, κατά την οποία η γλυκόζη του αίματος μεταβλήθηκε σχετικά γρήγορα ή και πολύ γρήγορα. Οι χαμηλότερες τιμές είναι καλύτερες.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Μέση Συνολική Ημερήσια Αλλαγή είναι το σύνολο των απόλυτων τιμών όλων των εκτός ορίων τιμών γλυκόζης για την εξεταζόμενη περίοδο, διαιρεμένο από τον αριθμό των ημερών. Χαμηλότερες τιμές είναι καλύτερες.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Μέση Συνολική Ωριαία Αλλαγή είναι το σύνολο των απόλυτων τιμών όλων των εκτός ορίων τιμών γλυκόζης για την εξεταζόμενη περίοδο, διαιρεμένο από τον αριθμό των ωρών της περιόδου. Χαμηλότερες τιμές είναι καλύτερες.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">μπορεί να βρεθεί εδώ.", + "Mean Total Daily Change": "Μέση Συνολική Ημερήσια Αλλαγή", + "Mean Hourly Change": "Μέση Ωριαία Αλλαγή", + "FortyFiveDown": "ελαφρά πτωτική", + "FortyFiveUp": "ελαφρά αυξητική", + "Flat": "σταθερή", + "SingleUp": "αυξητική", + "SingleDown": "πτωτική", + "DoubleDown": "ταχεία πτωτική", + "DoubleUp": "ταχχεία αυξητική", + "virtAsstUnknown": "Αυτή η τιμή είναι άγνωστη αυτή τη στιγμή. Παρακαλώ δείτε τον ιστότοπό σας Nightscout για περισσότερες λεπτομέρειες.", + "virtAsstTitleAR2Forecast": "Πρόβλεψη AR2", + "virtAsstTitleCurrentBasal": "Τρέχων βασικός ρυθμός", + "virtAsstTitleCurrentCOB": "Τρέχοντες ενεργοί υδατάνθρακες", + "virtAsstTitleCurrentIOB": "Τρέχουσα ενεργή ινσουλίνη", + "virtAsstTitleLaunch": "Καλώς ήρθατε στο Nightscout", + "virtAsstTitleLoopForecast": "Πρόβλεψη Loop", + "virtAsstTitleLastLoop": "Τελευταίο loop", + "virtAsstTitleOpenAPSForecast": "Πρόβλεψη OpenAPS", + "virtAsstTitlePumpReservoir": "Εναπομείνουσα Ινσουλίνη", + "virtAsstTitlePumpBattery": "Μπαταρία Αντλίας", + "virtAsstTitleRawBG": "Τρέχουσα τιμή γλυκόζης χωρίς εξομάλυνση", + "virtAsstTitleUploaderBattery": "Μπαταρία uploader", + "virtAsstTitleCurrentBG": "Τρέχουσα τιμή γλυκόζης", + "virtAsstTitleFullStatus": "Πλήρης κατάσταση", + "virtAsstTitleCGMMode": "Λειτουργία CGM", + "virtAsstTitleCGMStatus": "Κατάσταση CGM", + "virtAsstTitleCGMSessionAge": "Ηλικία του CGM", + "virtAsstTitleCGMTxStatus": "Κατάσταση πομπού CGM", + "virtAsstTitleCGMTxAge": "Ηλικία πομπού CGM", + "virtAsstTitleCGMNoise": "Θόρυβος μετρήσεων CGM", + "virtAsstTitleDelta": "Διαφορά γλυκόζης αίματος", + "virtAsstStatus": "%1 και %2 από %3.", + "virtAsstBasal": "%1 του τρέχοντος βασικού ρυθμού είναι %2 μονάδες ανά ώρα", + "virtAsstBasalTemp": "%1 του προσωρινού βασικού ρυθμού των %2 μονάδων ανά ώρα θα σταματήσει %3", + "virtAsstIob": "και έχετε ακόμα %1 ενεργή ινσουλίνη.", + "virtAsstIobIntent": "Έχετε %1 ενεργή ινσουλίνη", + "virtAsstIobUnits": "%1 μονάδες από", + "virtAsstLaunch": "Τι θα θέλατε να ελέγξετε στο Nightscout;", + "virtAsstPreamble": "Σας", + "virtAsstPreamble3person": "%1 έχει ένα ", + "virtAsstNoInsulin": "όχι", + "virtAsstUploadBattery": "Η μπαταρία του uploader είναι στο %1", + "virtAsstReservoir": "Έχετε ακόμη %1 μονάδες", + "virtAsstPumpBattery": "Η μπαταρία της αντλίας σας είναι στο %1 %2", + "virtAsstUploaderBattery": "Η μπαταρία του uploader είναι στο %1", + "virtAsstLastLoop": "Το τελευταίο επιτυχές loop ήταν %1", + "virtAsstLoopNotAvailable": "Το πρόσθετο κλειστού κυκλώματος δεν φαίνεται να είναι ενεργοποιημένο", + "virtAsstLoopForecastAround": "Σύμφωνα με την πρόβλεψη του κλειστού κυκλώματος, αναμένεται να είναι περίπου %1 στα επόμενα %2", + "virtAsstLoopForecastBetween": "Σύμφωνα με την πρόβλεψη του κλειστού κυκλώματος, αναμένεται να είστε ανάμεσα στο %1 και %2 στα επόμενα %3", + "virtAsstAR2ForecastAround": "Σύμφωνα μετην πρόβλεψη AR2 αναμένεται να είστε γύρω στο %1 στα επόμενα %2", + "virtAsstAR2ForecastBetween": "Σύμφωνα μετην πρόβλεψη AR2 αναμένεται να είστε ανάμεσα στο %1 και %2 στα επόμενα %3", + "virtAsstForecastUnavailable": "Δεν υπάρχει η δυνατότητα πρόβλεψηε με τα διαθέσιμα δεδομένα", + "virtAsstRawBG": "Η μη φιλτραρισμένη τιμή ζαχάρου είναι %1", + "virtAsstOpenAPSForecast": "Η πιθανή τιμή ζαχάρου βάσει OpenAPS is %1", + "virtAsstCob3person": "%1 έχει %2 ενεργούς υδατάνθρακες", + "virtAsstCob": "Έχετε %1 ενεργούς υδατάνθρακες", + "virtAsstCGMMode": "Η λειτουργία καταγραφής ήταν %1 από %2.", + "virtAsstCGMStatus": "Η κατάσταση καταγραφής ήταν %1 από %2.", + "virtAsstCGMSessAge": "Η περίοδος χρήσης της καταγραφής είναι %1 ημέρες και %2 ώρες.", + "virtAsstCGMSessNotStarted": "Δεν υπάρχει ενεργή καταγραφή ατυή τη στιγμή.", + "virtAsstCGMTxStatus": "Η κατάσταση του πομπού καταγραφής ήταν %1 από %2.", + "virtAsstCGMTxAge": "Ο πομπός καταγραφής είναι %1 ημέρών.", + "virtAsstCGMNoise": "Ο θόρυβος του συστήματος καταγραφής ήταν %1 από %2.", + "virtAsstCGMBattOne": "Η μπαταρία της καταγραφής ήταν %1 vols από τα %2.", + "virtAsstCGMBattTwo": "Το επίπεδο μπαταρίας της καταγραφής ήταν %1 volts και %2 volts από %3.", + "virtAsstDelta": "Η διαφορά τιμών ήταν %1 ανάμεσα στο %2 και το %3.", + "virtAsstDeltaEstimated": "Η εκτιμώμενη διαφορά τιμών ήταν %1 ανάμεσα σε %2 και %3.", + "virtAsstUnknownIntentTitle": "Άγνωστη πρόθεση", + "virtAsstUnknownIntentText": "Συγγνώμη, δεν γνωρίζω τι είναι αυτό που μου ζητάτε.", + "Fat [g]": "Λιπαρά [g]", + "Protein [g]": "Πρωτεΐνη [g]", + "Energy [kJ]": "Ενέργεια [kJ]", + "Clock Views:": "Εμφανίσεις ρολογιού:", + "Clock": "Ρολόι", + "Color": "Χρώμα", + "Simple": "Απλή", + "TDD average": "Μέσος όρος Συνολικής πρόσληψης ημερήσιας ινσουλίνης", + "Bolus average": "Μέσος όρος bolus", + "Basal average": "Μέσος όρος βασικού ρυθμού", + "Base basal average:": "Μέσος όρος βασικού ρυθμού:", + "Carbs average": "Μέσος όρος υδατανθράκων", + "Eating Soon": "Eating Soon", + "Last entry {0} minutes ago": "Τελευταία καταχώρηση πριν από {0} λεπτά", + "change": "αλλαγή", + "Speech": "Εκφώνηση", + "Target Top": "Υψηλός στόχος", + "Target Bottom": "Χαμηλός στόχος", + "Canceled": "Canceled", + "Meter BG": "Meter BG", + "predicted": "προβλεπόμενο", + "future": "μέλλον", + "ago": "πριν", + "Last data received": "Τελευταία λήψη δεδομένων", + "Clock View": "Εμφάνιση ρολογιού", + "Protein": "Πρωτεΐνες", + "Fat": "Λιπαρά", + "Protein average": "Μέσος όρος πρωτεϊνών", + "Fat average": "Μέσος όρος λιπαρών", + "Total carbs": "Σύνολο υδατανθράκων", + "Total protein": "Σύνολο πρωτεϊνών", + "Total fat": "Σύνολο λιπαρών", + "Database Size": "Μέγεθος βάσης δεδομένων", + "Database Size near its limits!": "Μέγεθος βάσης δεδομένων κοντά στα όριά της!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Το μέγεθος της βάσης δεδομένων είναι %1 MiB από %2 MiB. Παρακαλώ δημιουργήστε αντίγραφα ασφαλείας και καθαρίστε τη!", + "Database file size": "Μέγεθος αρχείου βάσης δεδομένων", + "%1 MiB of %2 MiB (%3%)": "%1 MiB από %2 MiB (%3%)", + "Data size": "Μέγεθος δεδομένων", + "virtAsstDatabaseSize": "%1 MiB. Αυτό είναι %2% του διαθέσιμου χώρου βάσης δεδομένων.", + "virtAsstTitleDatabaseSize": "Μέγεθος αρχείου βάσης δεδομένων", + "Carbs/Food/Time": "Υδατάνθρακες/Φαγητό/Χρόνος", + "You have administration messages": "Έχετε μηνύματα διαχείρισης", + "Admin messages in queue": "Διαθέσιμα μηνύματα διαχειριστή", + "Queue empty": "Σειρά μηνυμάτων κενή", + "There are no admin messages in queue": "Δεν υπάρχουν αδιάβαστα μηνύματα διαχείρισης", + "Please sign in using the API_SECRET to see your administration messages": "Παρακαλούμε συνδεθείτε χρησιμοποιώντας το API_SECRET για να δείτε τα μηνύματα διαχείρισης", + "Reads enabled in default permissions": "Οι αναγνώσεις είναι ενεργοποιημένες με προεπιλεγμένα δικαιώματα", + "Data reads enabled": "Ανάγνωση δεδομένων ενεργή", + "Data writes enabled": "Εγγραφή δεδομένων ενεργή", + "Data writes not enabled": "Εγγραφή δεδομένων ανενεργή", + "Color prediction lines": "Γραμμές πρόβλεψης με χρωματικό κώδικα", + "Release Notes": "Σημειώσεις έκδοσης", + "Check for Updates": "Έλεγχος για ενημερώσεις", + "Open Source": "Ανοικτός κώδικας", + "Nightscout Info": "Πληροφορίες Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Ο κύριος σκοπός του Loopalyzer είναι να απεικονίσει την επίδοση του συστήματος κλειστού κυκλώματος. Μπορεί να λειτουργήσει είστε με τη λειτουργία κλειστού κυκλώματος, ανοιχτού κυκλώματος, ή και χωρίς κύκλωμα καθόλου. Ανάλογα με τον uploader που χρησιμοποιείτε, το πόσο συχνά καταγράφει και ανεβάζει τα δεδομένα σας, και το πώς μπορεί να συμπληρώσει προηγούμενα δεδομένα, ενδέχεται μερικά γραφήματα να έχουν κενά ή να είναι εντελώς άδεια. Πάντα να σιγουρεύεστε πως το γράφημα απεικονίζει λογικές πληροφορίες. Καλό θα ήταν αρχικά να κοιτάξετε δεδομένα μια μέρα τη φορά για μερικές ημέρες.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Εμφάνιση προβλέψεων", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Χρονική μετατόπιση για γεύματα μεγαλύτερα από %1 γρ. υδατανθράκων που καταναλώνονται μεταξύ %2 και %3", + "Previous": "Προηγούμενο", + "Previous day": "Προηγούμενη ημέρα", + "Next day": "Επόμενη ημέρα", + "Next": "Επόμενο", + "Temp basal delta": "Μεταβολή προσωρινού βασικού ρυθμού", + "Authorized by token": "Πιστοποίηση από Token", + "Auth role": "Πιστοποίηση ρόλου", + "view without token": "προβολή χωρίς συνθηματικό", + "Remove stored token": "Αφαίρεση αποθηκευμένου token", + "Weekly Distribution": "Εβδομαδιαία Κατανομή", + "Failed authentication": "Αποτυχία ταυτοποίησης στοιχείων", + "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?": "Έγινε προσπάθεια πιστοποίησης στο Nightscout μια συσκευής από τη διεύθυνση IP %1 με λάθος διαπιστευτήρια. Ελέγξτε αν έχετε ρυθμίσει τον uploader με λάθος API_SECRET ή token.", + "Default (with leading zero and U)": "Προεπιλογή (με αρχικό μηδέν και U)", + "Concise (with U, without leading zero)": "Συνοπτική (με U, χωρίς μηδενικό προβάδισμα)", + "Minimal (without leading zero and U)": "Ελάχιστη (χωρίς μηδενικό προβάδισμα και U)", + "Small Bolus Display": "Απεικόνιση μικρού Bolus", + "Large Bolus Display": "Απεικόνιση μεγάλου Bolus", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Η ενέργεια επανλήφθηκε %1 φορές.", + "minutes": "λεπτά", + "Last recorded %1 %2 ago.": "Τελευταία καταγραφή πριν από %1 %2.", + "Security issue": "Πρόβλημα ασφάλειας", + "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.": "Ανίχνευση αδύναμου API_SECRET. Χρησιμοποιείστε ένα συνδυασμό από μικρά και ΚΕΦΑΛΑΙΑ γράμματα, αριθμούς και σύμβολα, όπως τα !#%&/ για να μειώσετε τον κίνδυνο μη εξουσιοδοτημένης πρόσβασης. Το ελάχιστο μέγεθος για το API_SECRET είναι 12 χαρακτήρες.", + "less than 1": "λιγότερο από 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Ο κωδικός πρόσβασης στο MongoDB και το API_SECRET είναι ίδια. Αυή δεν είναι καθόλου καλή πρακτική. Σας παρακαλούμε αλλάξτε τα και τα δύο και μην χρησιμοποιήσετε ίδια passwords στο σύστημα." +} diff --git a/translations/en/en.json b/translations/en/en.json new file mode 100644 index 00000000000..13782a2f503 --- /dev/null +++ b/translations/en/en.json @@ -0,0 +1,708 @@ +{ + "Listening on port":"Listening on port" + ,"Mo":"Mo" + ,"Tu":"Tu" + ,"We":"We" + ,"Th":"Th" + ,"Fr":"Fr" + ,"Sa":"Sa" + ,"Su":"Su" + ,"Monday":"Monday" + ,"Tuesday":"Tuesday" + ,"Wednesday":"Wednesday" + ,"Thursday":"Thursday" + ,"Friday":"Friday" + ,"Saturday":"Saturday" + ,"Sunday":"Sunday" + ,"Category":"Category" + ,"Subcategory":"Subcategory" + ,"Name":"Name" + ,"Today":"Today" + ,"Last 2 days":"Last 2 days" + ,"Last 3 days":"Last 3 days" + ,"Last week":"Last week" + ,"Last 2 weeks":"Last 2 weeks" + ,"Last month":"Last month" + ,"Last 3 months":"Last 3 months" + ,"From":"From" + ,"To":"To" + ,"Notes":"Notes" + ,"Food":"Food" + ,"Insulin":"Insulin" + ,"Carbs":"Carbs" + ,"Notes contain":"Notes contain" + ,"Target BG range bottom":"Target BG range bottom" + ,"top":"top" + ,"Show":"Show" + ,"Display":"Display" + ,"Loading":"Loading" + ,"Loading profile":"Loading profile" + ,"Loading status":"Loading status" + ,"Loading food database":"Loading food database" + ,"not displayed":"not displayed" + ,"Loading CGM data of":"Loading CGM data of" + ,"Loading treatments data of":"Loading treatments data of" + ,"Processing data of":"Processing data of" + ,"Portion":"Portion" + ,"Size":"Size" + ,"(none)":"(none)" + ,"None":"None" + ,"":"" + ,"Result is empty":"Result is empty" + ,"Day to day":"Day to day" + ,"Week to week":"Week to week" + ,"Daily Stats":"Daily Stats" + ,"Percentile Chart":"Percentile Chart" + ,"Distribution":"Distribution" + ,"Hourly stats":"Hourly stats" + ,"netIOB stats":"netIOB stats" + ,"temp basals must be rendered to display this report":"temp basals must be rendered to display this report" + ,"No data available":"No data available" + ,"Low":"Low" + ,"In Range":"In Range" + ,"Period":"Period" + ,"High":"High" + ,"Average":"Average" + ,"Low Quartile":"Low Quartile" + ,"Upper Quartile":"Upper Quartile" + ,"Quartile":"Quartile" + ,"Date":"Date" + ,"Normal":"Normal" + ,"Median":"Median" + ,"Readings":"Readings" + ,"StDev":"StDev" + ,"Daily stats report":"Daily stats report" + ,"Glucose Percentile report":"Glucose Percentile report" + ,"Glucose distribution":"Glucose distribution" + ,"days total":"days total" + ,"Total per day":"Total per day" + ,"Overall":"Overall" + ,"Range":"Range" + ,"% of Readings":"% of Readings" + ,"# of Readings":"# of Readings" + ,"Mean":"Mean" + ,"Standard Deviation":"Standard Deviation" + ,"Max":"Max" + ,"Min":"Min" + ,"A1c estimation*":"A1c estimation*" + ,"Weekly Success":"Weekly Success" + ,"There is not sufficient data to run this report. Select more days.":"There is not sufficient data to run this report. Select more days." + ,"Using stored API secret hash":"Using stored API secret hash" + ,"No API secret hash stored yet. You need to enter API secret.":"No API secret hash stored yet. You need to enter API secret." + ,"Database loaded":"Database loaded" + ,"Error: Database failed to load":"Error: Database failed to load" + ,"Error":"Error" + ,"Create new record":"Create new record" + ,"Save record":"Save record" + ,"Portions":"Portions" + ,"Unit":"Unit" + ,"GI":"GI" + ,"Edit record":"Edit record" + ,"Delete record":"Delete record" + ,"Move to the top":"Move to the top" + ,"Hidden":"Hidden" + ,"Hide after use":"Hide after use" + ,"Your API secret must be at least 12 characters long":"Your API secret must be at least 12 characters long" + ,"Bad API secret":"Bad API secret" + ,"API secret hash stored":"API secret hash stored" + ,"Status":"Status" + ,"Not loaded":"Not loaded" + ,"Food Editor":"Food Editor" + ,"Your database":"Your database" + ,"Filter":"Filter" + ,"Save":"Save" + ,"Clear":"Clear" + ,"Record":"Record" + ,"Quick picks":"Quick picks" + ,"Show hidden":"Show hidden" + ,"Your API secret or token":"Your API secret or token" + ,"Remember this device. (Do not enable this on public computers.)":"Remember this device. (Do not enable this on public computers.)" + ,"Treatments":"Treatments" + ,"Time":"Time" + ,"Event Type":"Event Type" + ,"Blood Glucose":"Blood Glucose" + ,"Entered By":"Entered By" + ,"Delete this treatment?":"Delete this treatment?" + ,"Carbs Given":"Carbs Given" + ,"Insulin Given":"Insulin Given" + ,"Event Time":"Event Time" + ,"Please verify that the data entered is correct":"Please verify that the data entered is correct" + ,"BG":"BG" + ,"Use BG correction in calculation":"Use BG correction in calculation" + ,"BG from CGM (autoupdated)":"BG from CGM (autoupdated)" + ,"BG from meter":"BG from meter" + ,"Manual BG":"Manual BG" + ,"Quickpick":"Quickpick" + ,"or":"or" + ,"Add from database":"Add from database" + ,"Use carbs correction in calculation":"Use carbs correction in calculation" + ,"Use COB correction in calculation":"Use COB correction in calculation" + ,"Use IOB in calculation":"Use IOB in calculation" + ,"Other correction":"Other correction" + ,"Rounding":"Rounding" + ,"Enter insulin correction in treatment":"Enter insulin correction in treatment" + ,"Insulin needed":"Insulin needed" + ,"Carbs needed":"Carbs needed" + ,"Carbs needed if Insulin total is negative value":"Carbs needed if Insulin total is negative value" + ,"Basal rate":"Basal rate" + ,"60 minutes earlier":"60 minutes earlier" + ,"45 minutes earlier":"45 minutes earlier" + ,"30 minutes earlier":"30 minutes earlier" + ,"20 minutes earlier":"20 minutes earlier" + ,"15 minutes earlier":"15 minutes earlier" + ,"Time in minutes":"Time in minutes" + ,"15 minutes later":"15 minutes later" + ,"20 minutes later":"20 minutes later" + ,"30 minutes later":"30 minutes later" + ,"45 minutes later":"45 minutes later" + ,"60 minutes later":"60 minutes later" + ,"Additional Notes, Comments":"Additional Notes, Comments" + ,"RETRO MODE":"RETRO MODE" + ,"Now":"Now" + ,"Other":"Other" + ,"Submit Form":"Submit Form" + ,"Profile Editor":"Profile Editor" + ,"Reports":"Reports" + ,"Add food from your database":"Add food from your database" + ,"Reload database":"Reload database" + ,"Add":"Add" + ,"Unauthorized":"Unauthorized" + ,"Entering record failed":"Entering record failed" + ,"Device authenticated":"Device authenticated" + ,"Device not authenticated":"Device not authenticated" + ,"Authentication status":"Authentication status" + ,"Authenticate":"Authenticate" + ,"Remove":"Remove" + ,"Your device is not authenticated yet":"Your device is not authenticated yet" + ,"Sensor":"Sensor" + ,"Finger":"Finger" + ,"Manual":"Manual" + ,"Scale":"Scale" + ,"Linear":"Linear" + ,"Logarithmic":"Logarithmic" + ,"Logarithmic (Dynamic)":"Logarithmic (Dynamic)" + ,"Insulin-on-Board":"Insulin-on-Board" + ,"Carbs-on-Board":"Carbs-on-Board" + ,"Bolus Wizard Preview":"Bolus Wizard Preview" + ,"Value Loaded":"Value Loaded" + ,"Cannula Age":"Cannula Age" + ,"Basal Profile":"Basal Profile" + ,"Silence for 30 minutes":"Silence for 30 minutes" + ,"Silence for 60 minutes":"Silence for 60 minutes" + ,"Silence for 90 minutes":"Silence for 90 minutes" + ,"Silence for 120 minutes":"Silence for 120 minutes" + ,"Settings":"Settings" + ,"Units":"Units" + ,"Date format":"Date format" + ,"12 hours":"12 hours" + ,"24 hours":"24 hours" + ,"Log a Treatment":"Log a Treatment" + ,"BG Check":"BG Check" + ,"Meal Bolus":"Meal Bolus" + ,"Snack Bolus":"Snack Bolus" + ,"Correction Bolus":"Correction Bolus" + ,"Carb Correction":"Carb Correction" + ,"Note":"Note" + ,"Question":"Question" + ,"Exercise":"Exercise" + ,"Pump Site Change":"Pump Site Change" + ,"CGM Sensor Start":"CGM Sensor Start" + ,"CGM Sensor Stop":"CGM Sensor Stop" + ,"CGM Sensor Insert":"CGM Sensor Insert" + ,"Sensor Code":"Sensor Code" + ,"Transmitter ID":"Transmitter ID" + ,"Dexcom Sensor Start":"Dexcom Sensor Start" + ,"Dexcom Sensor Change":"Dexcom Sensor Change" + ,"Insulin Cartridge Change":"Insulin Cartridge Change" + ,"D.A.D. Alert":"D.A.D. Alert" + ,"Glucose Reading":"Glucose Reading" + ,"Measurement Method":"Measurement Method" + ,"Meter":"Meter" + ,"Amount in grams":"Amount in grams" + ,"Amount in units":"Amount in units" + ,"View all treatments":"View all treatments" + ,"Enable Alarms":"Enable Alarms" + ,"Pump Battery Change":"Pump Battery Change" + ,"Pump Battery Age":"Pump Battery Age" + ,"Pump Battery Low Alarm":"Pump Battery Low Alarm" + ,"Pump Battery change overdue!":"Pump Battery change overdue!" + ,"When enabled an alarm may sound.":"When enabled an alarm may sound." + ,"Urgent High Alarm":"Urgent High Alarm" + ,"High Alarm":"High Alarm" + ,"Low Alarm":"Low Alarm" + ,"Urgent Low Alarm":"Urgent Low Alarm" + ,"Stale Data: Warn":"Stale Data: Warn" + ,"Stale Data: Urgent":"Stale Data: Urgent" + ,"mins":"mins" + ,"Night Mode":"Night Mode" + ,"When enabled the page will be dimmed from 10pm - 6am.":"When enabled the page will be dimmed from 10pm - 6am." + ,"Enable":"Enable" + ,"Show Raw BG Data":"Show Raw BG Data" + ,"Never":"Never" + ,"Always":"Always" + ,"When there is noise":"When there is noise" + ,"When enabled small white dots will be displayed for raw BG data":"When enabled small white dots will be displayed for raw BG data" + ,"Custom Title":"Custom Title" + ,"Theme":"Theme" + ,"Default":"Default" + ,"Colors":"Colors" + ,"Colorblind-friendly colors":"Colorblind-friendly colors" + ,"Reset, and use defaults":"Reset, and use defaults" + ,"Calibrations":"Calibrations" + ,"Alarm Test / Smartphone Enable":"Alarm Test / Smartphone Enable" + ,"Bolus Wizard":"Bolus Wizard" + ,"in the future":"in the future" + ,"time ago":"time ago" + ,"hr ago":"hr ago" + ,"hrs ago":"hrs ago" + ,"min ago":"min ago" + ,"mins ago":"mins ago" + ,"day ago":"day ago" + ,"days ago":"days ago" + ,"long ago":"long ago" + ,"Clean":"Clean" + ,"Light":"Light" + ,"Medium":"Medium" + ,"Heavy":"Heavy" + ,"Treatment type":"Treatment type" + ,"Raw BG":"Raw BG" + ,"Device":"Device" + ,"Noise":"Noise" + ,"Calibration":"Calibration" + ,"Show Plugins":"Show Plugins" + ,"About":"About" + ,"Value in":"Value in" + ,"Carb Time":"Carb Time" + ,"Language":"Language" + ,"Add new":"Add new" + ,"g":"g" + ,"ml":"ml" + ,"pcs":"pcs" + ,"Drag&drop food here":"Drag&drop food here" + ,"Care Portal":"Care Portal" + ,"Medium/Unknown":"Medium/Unknown" + ,"IN THE FUTURE":"IN THE FUTURE" + ,"Order":"Order" + ,"oldest on top":"oldest on top" + ,"newest on top":"newest on top" + ,"All sensor events":"All sensor events" + ,"Remove future items from mongo database":"Remove future items from mongo database" + ,"Find and remove treatments in the future":"Find and remove treatments in the future" + ,"This task find and remove treatments in the future.":"This task find and remove treatments in the future." + ,"Remove treatments in the future":"Remove treatments in the future" + ,"Find and remove entries in the future":"Find and remove entries in the future" + ,"This task find and remove CGM data in the future created by uploader with wrong date/time.":"This task find and remove CGM data in the future created by uploader with wrong date/time." + ,"Remove entries in the future":"Remove entries in the future" + ,"Loading database ...":"Loading database ..." + ,"Database contains %1 future records":"Database contains %1 future records" + ,"Remove %1 selected records?":"Remove %1 selected records?" + ,"Error loading database":"Error loading database" + ,"Record %1 removed ...":"Record %1 removed ..." + ,"Error removing record %1":"Error removing record %1" + ,"Deleting records ...":"Deleting records ..." + ,"%1 records deleted":"%1 records deleted" + ,"Clean Mongo status database":"Clean Mongo status database" + ,"Delete all documents from devicestatus collection":"Delete all documents from devicestatus collection" + ,"This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.":"This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated." + ,"Delete all documents":"Delete all documents" + ,"Delete all documents from devicestatus collection?":"Delete all documents from devicestatus collection?" + ,"Database contains %1 records":"Database contains %1 records" + ,"All records removed ...":"All records removed ..." + ,"Delete all documents from devicestatus collection older than 30 days":"Delete all documents from devicestatus collection older than 30 days" + ,"Number of Days to Keep:":"Number of Days to Keep:" + ,"This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.":"This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated." + ,"Delete old documents from devicestatus collection?":"Delete old documents from devicestatus collection?" + ,"Clean Mongo entries (glucose entries) database":"Clean Mongo entries (glucose entries) database" + ,"Delete all documents from entries collection older than 180 days":"Delete all documents from entries collection older than 180 days" + ,"This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.":"This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated." + ,"Delete old documents":"Delete old documents" + ,"Delete old documents from entries collection?":"Delete old documents from entries collection?" + ,"%1 is not a valid number":"%1 is not a valid number" + ,"%1 is not a valid number - must be more than 2":"%1 is not a valid number - must be more than 2" + ,"Clean Mongo treatments database":"Clean Mongo treatments database" + ,"Delete all documents from treatments collection older than 180 days":"Delete all documents from treatments collection older than 180 days" + ,"This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.":"This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated." + ,"Delete old documents from treatments collection?":"Delete old documents from treatments collection?" + ,"Admin Tools":"Admin Tools" + ,"Nightscout reporting":"Nightscout reporting" + ,"Cancel":"Cancel" + ,"Edit treatment":"Edit treatment" + ,"Duration":"Duration" + ,"Duration in minutes":"Duration in minutes" + ,"Temp Basal":"Temp Basal" + ,"Temp Basal Start":"Temp Basal Start" + ,"Temp Basal End":"Temp Basal End" + ,"Percent":"Percent" + ,"Basal change in %":"Basal change in %" + ,"Basal value":"Basal value" + ,"Absolute basal value":"Absolute basal value" + ,"Announcement":"Announcement" + ,"Loading temp basal data":"Loading temp basal data" + ,"Save current record before changing to new?":"Save current record before changing to new?" + ,"Profile Switch":"Profile Switch" + ,"Profile":"Profile" + ,"General profile settings":"General profile settings" + ,"Title":"Title" + ,"Database records":"Database records" + ,"Add new record":"Add new record" + ,"Remove this record":"Remove this record" + ,"Clone this record to new":"Clone this record to new" + ,"Record valid from":"Record valid from" + ,"Stored profiles":"Stored profiles" + ,"Timezone":"Timezone" + ,"Duration of Insulin Activity (DIA)":"Duration of Insulin Activity (DIA)" + ,"Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.":"Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime." + ,"Insulin to carb ratio (I:C)":"Insulin to carb ratio (I:C)" + ,"Hours:":"Hours:" + ,"hours":"hours" + ,"g/hour":"g/hour" + ,"g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.":"g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin." + ,"Insulin Sensitivity Factor (ISF)":"Insulin Sensitivity Factor (ISF)" + ,"mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.":"mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin." + ,"Carbs activity / absorption rate":"Carbs activity / absorption rate" + ,"grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).":"grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr)." + ,"Basal rates [unit/hour]":"Basal rates [unit/hour]" + ,"Target BG range [mg/dL,mmol/L]":"Target BG range [mg/dL,mmol/L]" + ,"Start of record validity":"Start of record validity" + ,"Icicle":"Icicle" + ,"Render Basal":"Render Basal" + ,"Profile used":"Profile used" + ,"Calculation is in target range.":"Calculation is in target range." + ,"Loading profile records ...":"Loading profile records ..." + ,"Values loaded.":"Values loaded." + ,"Default values used.":"Default values used." + ,"Error. Default values used.":"Error. Default values used." + ,"Time ranges of target_low and target_high don't match. Values are restored to defaults.":"Time ranges of target_low and target_high don't match. Values are restored to defaults." + ,"Valid from:":"Valid from:" + ,"Save current record before switching to new?":"Save current record before switching to new?" + ,"Add new interval before":"Add new interval before" + ,"Delete interval":"Delete interval" + ,"I:C":"I:C" + ,"ISF":"ISF" + ,"Combo Bolus":"Combo Bolus" + ,"Difference":"Difference" + ,"New time":"New time" + ,"Edit Mode":"Edit Mode" + ,"When enabled icon to start edit mode is visible":"When enabled icon to start edit mode is visible" + ,"Operation":"Operation" + ,"Move":"Move" + ,"Delete":"Delete" + ,"Move insulin":"Move insulin" + ,"Move carbs":"Move carbs" + ,"Remove insulin":"Remove insulin" + ,"Remove carbs":"Remove carbs" + ,"Change treatment time to %1 ?":"Change treatment time to %1 ?" + ,"Change carbs time to %1 ?":"Change carbs time to %1 ?" + ,"Change insulin time to %1 ?":"Change insulin time to %1 ?" + ,"Remove treatment ?":"Remove treatment ?" + ,"Remove insulin from treatment ?":"Remove insulin from treatment ?" + ,"Remove carbs from treatment ?":"Remove carbs from treatment ?" + ,"Rendering":"Rendering" + ,"Loading OpenAPS data of":"Loading OpenAPS data of" + ,"Loading profile switch data":"Loading profile switch data" + ,"Redirecting you to the Profile Editor to create a new profile.":"Redirecting you to the Profile Editor to create a new profile." + ,"Pump":"Pump" + ,"Sensor Age":"Sensor Age" + ,"Insulin Age":"Insulin Age" + ,"Temporary target":"Temporary target" + ,"Reason":"Reason" + ,"Eating soon":"Eating soon" + ,"Top":"Top" + ,"Bottom":"Bottom" + ,"Activity":"Activity" + ,"Targets":"Targets" + ,"Bolus insulin:":"Bolus insulin:" + ,"Base basal insulin:":"Base basal insulin:" + ,"Positive temp basal insulin:":"Positive temp basal insulin:" + ,"Negative temp basal insulin:":"Negative temp basal insulin:" + ,"Total basal insulin:":"Total basal insulin:" + ,"Total daily insulin:":"Total daily insulin:" + ,"Unable to save Role":"Unable to save Role" + ,"Unable to delete Role":"Unable to delete Role" + ,"Database contains %1 roles":"Database contains %1 roles" + ,"Edit Role":"Edit Role" + ,"admin, school, family, etc":"admin, school, family, etc" + ,"Permissions":"Permissions" + ,"Are you sure you want to delete: ":"Are you sure you want to delete: " + ,"Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.":"Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator." + ,"Add new Role":"Add new Role" + ,"Roles - Groups of People, Devices, etc":"Roles - Groups of People, Devices, etc" + ,"Edit this role":"Edit this role" + ,"Admin authorized":"Admin authorized" + ,"Subjects - People, Devices, etc":"Subjects - People, Devices, etc" + ,"Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.":"Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared." + ,"Add new Subject":"Add new Subject" + ,"Unable to save Subject":"Unable to save Subject" + ,"Unable to delete Subject":"Unable to delete Subject" + ,"Database contains %1 subjects":"Database contains %1 subjects" + ,"Edit Subject":"Edit Subject" + ,"person, device, etc":"person, device, etc" + ,"role1, role2":"role1, role2" + ,"Edit this subject":"Edit this subject" + ,"Delete this subject":"Delete this subject" + ,"Roles":"Roles" + ,"Access Token":"Access Token" + ,"hour ago":"hour ago" + ,"hours ago":"hours ago" + ,"Silence for %1 minutes":"Silence for %1 minutes" + ,"Check BG":"Check BG" + ,"BASAL":"BASAL" + ,"Current basal":"Current basal" + ,"Sensitivity":"Sensitivity" + ,"Current Carb Ratio":"Current Carb Ratio" + ,"Basal timezone":"Basal timezone" + ,"Active profile":"Active profile" + ,"Active temp basal":"Active temp basal" + ,"Active temp basal start":"Active temp basal start" + ,"Active temp basal duration":"Active temp basal duration" + ,"Active temp basal remaining":"Active temp basal remaining" + ,"Basal profile value":"Basal profile value" + ,"Active combo bolus":"Active combo bolus" + ,"Active combo bolus start":"Active combo bolus start" + ,"Active combo bolus duration":"Active combo bolus duration" + ,"Active combo bolus remaining":"Active combo bolus remaining" + ,"BG Delta":"BG Delta" + ,"Elapsed Time":"Elapsed Time" + ,"Absolute Delta":"Absolute Delta" + ,"Interpolated":"Interpolated" + ,"BWP":"BWP" + ,"Urgent":"Urgent" + ,"Warning":"Warning" + ,"Info":"Info" + ,"Lowest":"Lowest" + ,"Snoozing high alarm since there is enough IOB":"Snoozing high alarm since there is enough IOB" + ,"Check BG, time to bolus?":"Check BG, time to bolus?" + ,"Notice":"Notice" + ,"required info missing":"required info missing" + ,"Insulin on Board":"Insulin on Board" + ,"Current target":"Current target" + ,"Expected effect":"Expected effect" + ,"Expected outcome":"Expected outcome" + ,"Carb Equivalent":"Carb Equivalent" + ,"Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs":"Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs" + ,"Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS":"Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS" + ,"%1U reduction needed in active insulin to reach low target, too much basal?":"%1U reduction needed in active insulin to reach low target, too much basal?" + ,"basal adjustment out of range, give carbs?":"basal adjustment out of range, give carbs?" + ,"basal adjustment out of range, give bolus?":"basal adjustment out of range, give bolus?" + ,"above high":"above high" + ,"below low":"below low" + ,"Projected BG %1 target":"Projected BG %1 target" + ,"aiming at":"aiming at" + ,"Bolus %1 units":"Bolus %1 units" + ,"or adjust basal":"or adjust basal" + ,"Check BG using glucometer before correcting!":"Check BG using glucometer before correcting!" + ,"Basal reduction to account %1 units:":"Basal reduction to account %1 units:" + ,"30m temp basal":"30m temp basal" + ,"1h temp basal":"1h temp basal" + ,"Cannula change overdue!":"Cannula change overdue!" + ,"Time to change cannula":"Time to change cannula" + ,"Change cannula soon":"Change cannula soon" + ,"Cannula age %1 hours":"Cannula age %1 hours" + ,"Inserted":"Inserted" + ,"CAGE":"CAGE" + ,"COB":"COB" + ,"Last Carbs":"Last Carbs" + ,"IAGE":"IAGE" + ,"Insulin reservoir change overdue!":"Insulin reservoir change overdue!" + ,"Time to change insulin reservoir":"Time to change insulin reservoir" + ,"Change insulin reservoir soon":"Change insulin reservoir soon" + ,"Insulin reservoir age %1 hours":"Insulin reservoir age %1 hours" + ,"Changed":"Changed" + ,"IOB":"IOB" + ,"Careportal IOB":"Careportal IOB" + ,"Last Bolus":"Last Bolus" + ,"Basal IOB":"Basal IOB" + ,"Source":"Source" + ,"Stale data, check rig?":"Stale data, check rig?" + ,"Last received:":"Last received:" + ,"%1m ago":"%1m ago" + ,"%1h ago":"%1h ago" + ,"%1d ago":"%1d ago" + ,"RETRO":"RETRO" + ,"SAGE":"SAGE" + ,"Sensor change/restart overdue!":"Sensor change/restart overdue!" + ,"Time to change/restart sensor":"Time to change/restart sensor" + ,"Change/restart sensor soon":"Change/restart sensor soon" + ,"Sensor age %1 days %2 hours":"Sensor age %1 days %2 hours" + ,"Sensor Insert":"Sensor Insert" + ,"Sensor Start":"Sensor Start" + ,"days":"days" + ,"Insulin distribution":"Insulin distribution" + ,"To see this report, press SHOW while in this view":"To see this report, press SHOW while in this view" + ,"AR2 Forecast":"AR2 Forecast" + ,"OpenAPS Forecasts":"OpenAPS Forecasts" + ,"Temporary Target":"Temporary Target" + ,"Temporary Target Cancel":"Temporary Target Cancel" + ,"OpenAPS Offline":"OpenAPS Offline" + ,"Profiles":"Profiles" + ,"Time in fluctuation":"Time in fluctuation" + ,"Time in rapid fluctuation":"Time in rapid fluctuation" + ,"This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:":"This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:" + ,"Filter by hours":"Filter by hours" + ,"Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.":"Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better." + ,"Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.":"Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better." + ,"Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.":"Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better." + ,"Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.":"Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better." + ,"GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.":"\">can be found here." + ,"Mean Total Daily Change":"Mean Total Daily Change" + ,"Mean Hourly Change":"Mean Hourly Change" + ,"FortyFiveDown":"slightly dropping" + ,"FortyFiveUp":"slightly rising" + ,"Flat":"holding" + ,"SingleUp":"rising" + ,"SingleDown":"dropping" + ,"DoubleDown":"rapidly dropping" + ,"DoubleUp":"rapidly rising" + ,"virtAsstUnknown":"That value is unknown at the moment. Please see your Nightscout site for more details." + ,"virtAsstTitleAR2Forecast":"AR2 Forecast" + ,"virtAsstTitleCurrentBasal":"Current Basal" + ,"virtAsstTitleCurrentCOB":"Current COB" + ,"virtAsstTitleCurrentIOB":"Current IOB" + ,"virtAsstTitleLaunch":"Welcome to Nightscout" + ,"virtAsstTitleLoopForecast":"Loop Forecast" + ,"virtAsstTitleLastLoop":"Last Loop" + ,"virtAsstTitleOpenAPSForecast":"OpenAPS Forecast" + ,"virtAsstTitlePumpReservoir":"Insulin Remaining" + ,"virtAsstTitlePumpBattery":"Pump Battery" + ,"virtAsstTitleRawBG":"Current Raw BG" + ,"virtAsstTitleUploaderBattery":"Uploader Battery" + ,"virtAsstTitleCurrentBG":"Current BG" + ,"virtAsstTitleFullStatus":"Full Status" + ,"virtAsstTitleCGMMode":"CGM Mode" + ,"virtAsstTitleCGMStatus":"CGM Status" + ,"virtAsstTitleCGMSessionAge":"CGM Session Age" + ,"virtAsstTitleCGMTxStatus":"CGM Transmitter Status" + ,"virtAsstTitleCGMTxAge":"CGM Transmitter Age" + ,"virtAsstTitleCGMNoise":"CGM Noise" + ,"virtAsstTitleDelta":"Blood Glucose Delta" + ,"virtAsstStatus":"%1 and %2 as of %3." + ,"virtAsstBasal":"%1 current basal is %2 units per hour" + ,"virtAsstBasalTemp":"%1 temp basal of %2 units per hour will end %3" + ,"virtAsstIob":"and you have %1 insulin on board." + ,"virtAsstIobIntent":"You have %1 insulin on board" + ,"virtAsstIobUnits":"%1 units of" + ,"virtAsstLaunch":"What would you like to check on Nightscout?" + ,"virtAsstPreamble":"Your" + ,"virtAsstPreamble3person":"%1 has a " + ,"virtAsstNoInsulin":"no" + ,"virtAsstUploadBattery":"Your uploader battery is at %1" + ,"virtAsstReservoir":"You have %1 units remaining" + ,"virtAsstPumpBattery":"Your pump battery is at %1 %2" + ,"virtAsstUploaderBattery":"Your uploader battery is at %1" + ,"virtAsstLastLoop":"The last successful loop was %1" + ,"virtAsstLoopNotAvailable":"Loop plugin does not seem to be enabled" + ,"virtAsstLoopForecastAround":"According to the loop forecast you are expected to be around %1 over the next %2" + ,"virtAsstLoopForecastBetween":"According to the loop forecast you are expected to be between %1 and %2 over the next %3" + ,"virtAsstAR2ForecastAround":"According to the AR2 forecast you are expected to be around %1 over the next %2" + ,"virtAsstAR2ForecastBetween":"According to the AR2 forecast you are expected to be between %1 and %2 over the next %3" + ,"virtAsstForecastUnavailable":"Unable to forecast with the data that is available" + ,"virtAsstRawBG":"Your raw bg is %1" + ,"virtAsstOpenAPSForecast":"The OpenAPS Eventual BG is %1" + ,"virtAsstCob3person":"%1 has %2 carbohydrates on board" + ,"virtAsstCob":"You have %1 carbohydrates on board" + ,"virtAsstCGMMode":"Your CGM mode was %1 as of %2." + ,"virtAsstCGMStatus":"Your CGM status was %1 as of %2." + ,"virtAsstCGMSessAge":"Your CGM session has been active for %1 days and %2 hours." + ,"virtAsstCGMSessNotStarted":"There is no active CGM session at the moment." + ,"virtAsstCGMTxStatus":"Your CGM transmitter status was %1 as of %2." + ,"virtAsstCGMTxAge":"Your CGM transmitter is %1 days old." + ,"virtAsstCGMNoise":"Your CGM noise was %1 as of %2." + ,"virtAsstCGMBattOne":"Your CGM battery was %1 volts as of %2." + ,"virtAsstCGMBattTwo":"Your CGM battery levels were %1 volts and %2 volts as of %3." + ,"virtAsstDelta":"Your delta was %1 between %2 and %3." + ,"virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3." + ,"virtAsstUnknownIntentTitle":"Unknown Intent" + ,"virtAsstUnknownIntentText":"I'm sorry, I don't know what you're asking for." + ,"Fat [g]":"Fat [g]" + ,"Protein [g]":"Protein [g]" + ,"Energy [kJ]":"Energy [kJ]" + ,"Clock Views:":"Clock Views:" + ,"Clock":"Clock" + ,"Color":"Color" + ,"Simple":"Simple" + ,"TDD average":"TDD average" + ,"Bolus average":"Bolus average" + ,"Basal average":"Basal average" + ,"Base basal average:":"Base basal average:" + ,"Carbs average":"Carbs average" + ,"Eating Soon":"Eating Soon" + ,"Last entry {0} minutes ago":"Last entry {0} minutes ago" + ,"change":"change" + ,"Speech":"Speech" + ,"Target Top":"Target Top" + ,"Target Bottom":"Target Bottom" + ,"Canceled":"Canceled" + ,"Meter BG":"Meter BG" + ,"predicted":"predicted" + ,"future":"future" + ,"ago":"ago" + ,"Last data received":"Last data received" + ,"Clock View":"Clock View" + ,"Protein":"Protein" + ,"Fat":"Fat" + ,"Protein average":"Protein average" + ,"Fat average":"Fat average" + ,"Total carbs":"Total carbs" + ,"Total protein":"Total protein" + ,"Total fat":"Total fat" + ,"Database Size":"Database Size" + ,"Database Size near its limits!":"Database Size near its limits!" + ,"Database size is %1 MiB out of %2 MiB. Please backup and clean up database!":"Database size is %1 MiB out of %2 MiB. Please backup and clean up database!" + ,"Database file size":"Database file size" + ,"%1 MiB of %2 MiB (%3%)":"%1 MiB of %2 MiB (%3%)" + ,"Data size":"Data size" + ,"virtAsstDatabaseSize":"%1 MiB. That is %2% of available database space." + ,"virtAsstTitleDatabaseSize":"Database file size" + ,"Carbs/Food/Time":"Carbs/Food/Time" + ,"You have administration messages":"You have administration messages" + ,"Admin messages in queue":"Admin messages in queue" + ,"Queue empty":"Queue empty" + ,"There are no admin messages in queue":"There are no admin messages in queue" + ,"Please sign in using the API_SECRET to see your administration messages":"Please sign in using the API_SECRET to see your administration messages" + ,"Reads enabled in default permissions":"Reads enabled in default permissions" + ,"Data reads enabled": "Data reads enabled" + ,"Data writes enabled": "Data writes enabled" + ,"Data writes not enabled": "Data writes not enabled" + ,"Color prediction lines": "Color prediction lines" + ,"Release Notes":"Release Notes" + ,"Check for Updates":"Check for Updates" + ,"Open Source":"Open Source" + ,"Nightscout Info":"Nightscout Info" + ,"The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.":"The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see." + ,"Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.":"Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time." + ,"In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.":"In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal." + ,"Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.":"Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate." + ,"Note that time shift is available only when viewing multiple days.":"Note that time shift is available only when viewing multiple days." + ,"Please select a maximum of two weeks duration and click Show again.":"Please select a maximum of two weeks duration and click Show again." + ,"Show profiles table":"Show profiles table" + ,"Show predictions":"Show predictions" + ,"Timeshift on meals larger than %1 g carbs consumed between %2 and %3":"Timeshift on meals larger than %1 g carbs consumed between %2 and %3" + ,"Previous":"Previous" + ,"Previous day":"Previous day" + ,"Next day":"Next day" + ,"Next":"Next" + ,"Temp basal delta":"Temp basal delta" + ,"Authorized by token":"Authorized by token" + ,"Auth role":"Auth role" + ,"view without token":"view without token" + ,"Remove stored token":"Remove stored token" + ,"Weekly Distribution":"Weekly Distribution" + ,"Failed authentication":"Failed authentication" + ,"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?": "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?" + ,"Default (with leading zero and U)":"Default (with leading zero and U)" + ,"Concise (with U, without leading zero)":"Concise (with U, without leading zero)" + ,"Minimal (without leading zero and U)":"Minimal (without leading zero and U)" + ,"Small Bolus Display":"Small Bolus Display" + ,"Large Bolus Display":"Large Bolus Display" + ,"Bolus Display Threshold":"Bolus Display Threshold" + ,"%1 U and Over":"%1 U and Over" + ,"Event repeated %1 times.":"Event repeated %1 times." + ,"minutes":"minutes" + ,"hours":"hours" + ,"Last recorded %1 %2 ago.":"Last recorded %1 %2 ago." + ,"Security issue":"Security issue" + ,"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.":"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." + ,"less than 1": "less than 1" + ,"MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.":"MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/translations/es_ES.json b/translations/es_ES.json new file mode 100644 index 00000000000..580a5695f75 --- /dev/null +++ b/translations/es_ES.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Escuchando en el puerto", + "Mo": "Lu", + "Tu": "Mar", + "We": "Mie", + "Th": "Jue", + "Fr": "Vie", + "Sa": "Sab", + "Su": "Dom", + "Monday": "Lunes", + "Tuesday": "Martes", + "Wednesday": "Miércoles", + "Thursday": "Jueves", + "Friday": "Viernes", + "Saturday": "Sábado", + "Sunday": "Domingo", + "Category": "Categoría", + "Subcategory": "Subcategoría", + "Name": "Nombre", + "Today": "Hoy", + "Last 2 days": "Últimos 2 días", + "Last 3 days": "Últimos 3 días", + "Last week": "Semana pasada", + "Last 2 weeks": "Últimas 2 semanas", + "Last month": "Mes pasado", + "Last 3 months": "Últimos 3 meses", + "From": "Desde", + "To": "Hasta", + "Notes": "Notas", + "Food": "Comida", + "Insulin": "Insulina", + "Carbs": "Hidratos de carbono", + "Notes contain": "Contenido de las notas", + "Target BG range bottom": "Objetivo inferior de glucemia", + "top": "Superior", + "Show": "Mostrar", + "Display": "Visualizar", + "Loading": "Cargando", + "Loading profile": "Cargando perfil", + "Loading status": "Cargando estado", + "Loading food database": "Cargando base de datos de alimentos", + "not displayed": "no mostrado", + "Loading CGM data of": "Cargando datos de MCG de", + "Loading treatments data of": "Cargando datos de tratamientos de", + "Processing data of": "Procesando datos de", + "Portion": "Porción", + "Size": "Tamaño", + "(none)": "(ninguno)", + "None": "Ninguno", + "": "", + "Result is empty": "Resultado vacío", + "Day to day": "Día a día", + "Week to week": "Semana a semana", + "Daily Stats": "Estadísticas diarias", + "Percentile Chart": "Percentiles", + "Distribution": "Distribución", + "Hourly stats": "Estadísticas por hora", + "netIOB stats": "estadísticas netIOB", + "temp basals must be rendered to display this report": "basales temporales deben ser terminadas para mostrar este informe", + "No data available": "No hay datos disponibles", + "Low": "Bajo", + "In Range": "En rango", + "Period": "Periodo", + "High": "Alto", + "Average": "Media", + "Low Quartile": "Cuartil inferior", + "Upper Quartile": "Cuartil superior", + "Quartile": "Cuartil", + "Date": "Fecha", + "Normal": "Normal", + "Median": "Mediana", + "Readings": "Valores", + "StDev": "Desviación estándar", + "Daily stats report": "Informe de estadísticas diarias", + "Glucose Percentile report": "Informe de percetiles de glucemia", + "Glucose distribution": "Distribución de glucemias", + "days total": "Total de días", + "Total per day": "Total por días", + "Overall": "General", + "Range": "Intervalo", + "% of Readings": "% de valores", + "# of Readings": "N° de valores", + "Mean": "Media", + "Standard Deviation": "Desviación estándar", + "Max": "Máx", + "Min": "Mín", + "A1c estimation*": "Estimación de HbA1c*", + "Weekly Success": "Resultados semanales", + "There is not sufficient data to run this report. Select more days.": "No hay datos suficientes para generar este informe. Seleccione más días.", + "Using stored API secret hash": "Usando hash del API secreto pre-almacenado", + "No API secret hash stored yet. You need to enter API secret.": "No se ha almacenado ningún hash todavía. Debe introducir su secreto API.", + "Database loaded": "Base de datos cargada", + "Error: Database failed to load": "Error: Carga de base de datos fallida", + "Error": "Error", + "Create new record": "Crear nuevo registro", + "Save record": "Guardar registro", + "Portions": "Porciones", + "Unit": "Unidades", + "GI": "IG", + "Edit record": "Editar registro", + "Delete record": "Borrar registro", + "Move to the top": "Mover arriba", + "Hidden": "Oculto", + "Hide after use": "Ocultar después de utilizar", + "Your API secret must be at least 12 characters long": "Su API secreo debe contener al menos 12 carácteres", + "Bad API secret": "API secreto incorrecto", + "API secret hash stored": "Hash del API secreto guardado", + "Status": "Estado", + "Not loaded": "No cargado", + "Food Editor": "Editor de alimentos", + "Your database": "Su base de datos", + "Filter": "Filtro", + "Save": "Salvar", + "Clear": "Limpiar", + "Record": "Guardar", + "Quick picks": "Selección rápida", + "Show hidden": "Mostrar ocultos", + "Your API secret or token": "Tu API secret o token", + "Remember this device. (Do not enable this on public computers.)": "Recordar este dispositivo. (No activar esto en ordenadores públicos.)", + "Treatments": "Tratamientos", + "Time": "Hora", + "Event Type": "Tipo de evento", + "Blood Glucose": "Glucemia", + "Entered By": "Introducido por", + "Delete this treatment?": "¿Borrar este tratamiento?", + "Carbs Given": "Hidratos de carbono puestos", + "Insulin Given": "Insulina introducida", + "Event Time": "Hora del evento", + "Please verify that the data entered is correct": "Por favor, verifique que los datos introducidos son correctos", + "BG": "Glucemia en sangre", + "Use BG correction in calculation": "Usar la corrección de glucemia en los cálculos", + "BG from CGM (autoupdated)": "Glucemia del sensor (Auto-actualizado)", + "BG from meter": "Glucemia del glucómetro", + "Manual BG": "Glucemia manual", + "Quickpick": "Selección rápida", + "or": "o", + "Add from database": "Añadir desde la base de datos", + "Use carbs correction in calculation": "Usar la corrección de hidratos de carbono en los cálculos", + "Use COB correction in calculation": "Usar carbohidratos activos para los cálculos", + "Use IOB in calculation": "Usar Insulina activa en los cálculos", + "Other correction": "Otra corrección", + "Rounding": "Redondeo", + "Enter insulin correction in treatment": "Introducir correción de insulina en tratamiento", + "Insulin needed": "Insulina necesaria", + "Carbs needed": "Hidratos de carbono necesarios", + "Carbs needed if Insulin total is negative value": "Carbohidratos necesarios si total insulina es un valor negativo", + "Basal rate": "Tasa basal", + "60 minutes earlier": "60 min antes", + "45 minutes earlier": "45 min antes", + "30 minutes earlier": "30 min antes", + "20 minutes earlier": "20 min antes", + "15 minutes earlier": "15 min antes", + "Time in minutes": "Tiempo en minutos", + "15 minutes later": "15 min más tarde", + "20 minutes later": "20 min más tarde", + "30 minutes later": "30 min más tarde", + "45 minutes later": "45 min más tarde", + "60 minutes later": "60 min más tarde", + "Additional Notes, Comments": "Notas adicionales, Comentarios", + "RETRO MODE": "Modo Retrospectivo", + "Now": "Ahora", + "Other": "Otro", + "Submit Form": "Enviar formulario", + "Profile Editor": "Editor de perfil", + "Reports": "Informes", + "Add food from your database": "Añadir alimento a su base de datos", + "Reload database": "Recargar base de datos", + "Add": "Añadir", + "Unauthorized": "No autorizado", + "Entering record failed": "Entrada de registro fallida", + "Device authenticated": "Dispositivo autorizado", + "Device not authenticated": "Dispositivo no autorizado", + "Authentication status": "Estado de autorización", + "Authenticate": "Autentificar", + "Remove": "Eliminar", + "Your device is not authenticated yet": "Su dispositivo no ha sido autentificado todavía", + "Sensor": "Sensor", + "Finger": "Dedo", + "Manual": "Manual", + "Scale": "Escala", + "Linear": "Lineal", + "Logarithmic": "Logarítmica", + "Logarithmic (Dynamic)": "Logarítmo (Dinámico)", + "Insulin-on-Board": "Insulina activa", + "Carbs-on-Board": "Carbohidratos activos", + "Bolus Wizard Preview": "Vista previa del cálculo del bolo", + "Value Loaded": "Valor cargado", + "Cannula Age": "Antigüedad cánula", + "Basal Profile": "Perfil Basal", + "Silence for 30 minutes": "Silenciar durante 30 minutos", + "Silence for 60 minutes": "Silenciar durante 60 minutos", + "Silence for 90 minutes": "Silenciar durante 90 minutos", + "Silence for 120 minutes": "Silenciar durante 120 minutos", + "Settings": "Ajustes", + "Units": "Unidades", + "Date format": "Formato de fecha", + "12 hours": "12 horas", + "24 hours": "24 horas", + "Log a Treatment": "Apuntar un tratamiento", + "BG Check": "Control de glucemia", + "Meal Bolus": "Bolo de comida", + "Snack Bolus": "Bolo de aperitivo", + "Correction Bolus": "Bolo corrector", + "Carb Correction": "Carbohidratos de corrección", + "Note": "Nota", + "Question": "Pregunta", + "Exercise": "Ejercicio", + "Pump Site Change": "Cambio de catéter", + "CGM Sensor Start": "Inicio de sensor", + "CGM Sensor Stop": "Detener sensor MCG", + "CGM Sensor Insert": "Cambio de sensor", + "Sensor Code": "Código del sensor", + "Transmitter ID": "ID del transmisor", + "Dexcom Sensor Start": "Inicio de sensor Dexcom", + "Dexcom Sensor Change": "Cambio de sensor Dexcom", + "Insulin Cartridge Change": "Cambio de reservorio de insulina", + "D.A.D. Alert": "Alerta de perro de alerta diabética", + "Glucose Reading": "Valor de glucemia", + "Measurement Method": "Método de medida", + "Meter": "Glucómetro", + "Amount in grams": "Cantidad en gramos", + "Amount in units": "Cantidad en unidades", + "View all treatments": "Visualizar todos los tratamientos", + "Enable Alarms": "Activar las alarmas", + "Pump Battery Change": "Cambio batería bomba", + "Pump Battery Age": "Edad de la batería en la bomba", + "Pump Battery Low Alarm": "Alarma de Bomba Baja", + "Pump Battery change overdue!": "Cambio de batería de bomba atrasado!", + "When enabled an alarm may sound.": "Cuando estén activas, una alarma podrá sonar", + "Urgent High Alarm": "Alarma de glucemia alta urgente", + "High Alarm": "Alarma de glucemia alta", + "Low Alarm": "Alarma de glucemia baja", + "Urgent Low Alarm": "Alarma de glucemia baja urgente", + "Stale Data: Warn": "Datos obsoletos: aviso", + "Stale Data: Urgent": "Datos obsoletos: Urgente", + "mins": "min", + "Night Mode": "Modo nocturno", + "When enabled the page will be dimmed from 10pm - 6am.": "Cuando esté activo, el brillo de la página bajará de 10pm a 6am.", + "Enable": "Activar", + "Show Raw BG Data": "Mostrat datos en glucemia en crudo", + "Never": "Nunca", + "Always": "Siempre", + "When there is noise": "Cuando hay ruido", + "When enabled small white dots will be displayed for raw BG data": "Cuando esté activo, pequeños puntos blancos mostrarán los datos en crudo", + "Custom Title": "Título personalizado", + "Theme": "Tema", + "Default": "Por defecto", + "Colors": "Colores", + "Colorblind-friendly colors": "Colores para Daltónicos", + "Reset, and use defaults": "Inicializar y utilizar los valores por defecto", + "Calibrations": "Calibraciones", + "Alarm Test / Smartphone Enable": "Test de Alarma / Activar teléfono", + "Bolus Wizard": "Calculador Bolos", + "in the future": "en el futuro", + "time ago": "tiempo atrás", + "hr ago": "hr atrás", + "hrs ago": "hrs atrás", + "min ago": "min atrás", + "mins ago": "mins atrás", + "day ago": "día atrás", + "days ago": "días atrás", + "long ago": "Hace mucho tiempo", + "Clean": "Limpio", + "Light": "Ligero", + "Medium": "Medio", + "Heavy": "Fuerte", + "Treatment type": "Tipo de tratamiento", + "Raw BG": "Glucemia en crudo", + "Device": "Dispositivo", + "Noise": "Ruido", + "Calibration": "Calibración", + "Show Plugins": "Mostrar Plugins", + "About": "Sobre", + "Value in": "Valor en", + "Carb Time": "Momento de la ingesta", + "Language": "Lenguaje", + "Add new": "Añadir nuevo", + "g": "gr", + "ml": "ml", + "pcs": "pza", + "Drag&drop food here": "Arrastre y suelte aquí alimentos", + "Care Portal": "Portal cuidador", + "Medium/Unknown": "Medio/Desconocido", + "IN THE FUTURE": "EN EL FUTURO", + "Order": "Ordenar", + "oldest on top": "Más antiguo arriba", + "newest on top": "Más nuevo arriba", + "All sensor events": "Todos los eventos del sensor", + "Remove future items from mongo database": "Remover elementos futuros desde basedatos Mongo", + "Find and remove treatments in the future": "Encontrar y eliminar tratamientos futuros", + "This task find and remove treatments in the future.": "Este comando encuentra y elimina tratamientos futuros.", + "Remove treatments in the future": "Elimina tratamientos futuros", + "Find and remove entries in the future": "Encuentra y elimina entradas futuras", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Este comando encuentra y elimina datos del sensor futuros creados por actualizaciones con errores en fecha/hora", + "Remove entries in the future": "Elimina entradas futuras", + "Loading database ...": "Cargando base de datos ...", + "Database contains %1 future records": "Base de datos contiene %1 registros futuros", + "Remove %1 selected records?": "Eliminar %1 registros seleccionados?", + "Error loading database": "Error al cargar base de datos", + "Record %1 removed ...": "Registro %1 eliminado ...", + "Error removing record %1": "Error al eliminar registro %1", + "Deleting records ...": "Eliminando registros ...", + "%1 records deleted": "%1 registros eliminados", + "Clean Mongo status database": "Limpiar estado de la base de datos Mongo", + "Delete all documents from devicestatus collection": "Borrar todos los documentos desde colección devicesatatus", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Este comando elimina todos los documentos desde la colección devicestatus. Útil cuando el estado de la batería cargadora no se actualiza correctamente", + "Delete all documents": "Borra todos los documentos", + "Delete all documents from devicestatus collection?": "Borrar todos los documentos desde la colección devicestatus?", + "Database contains %1 records": "La Base de datos contiene %1 registros", + "All records removed ...": "Todos los registros eliminados ...", + "Delete all documents from devicestatus collection older than 30 days": "Eliminar todos los documentos de la colección devicestatus de hace más de 30 días", + "Number of Days to Keep:": "Número de días a guardar:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Esta tarea elimina todos los documentos de la colección de devicestatus que tienen más de 30 días. Es útil cuando el estado de la batería del cargador no se actualiza correctamente.", + "Delete old documents from devicestatus collection?": "Borrar todos los documentos desde la colección devicestatus?", + "Clean Mongo entries (glucose entries) database": "Limpiar base de datos Mongo entradas (entradas glucosas)", + "Delete all documents from entries collection older than 180 days": "Eliminar todos los documentos de la colección entries de hace más de 180 días", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Esta tarea elimina todos los documentos de la colección entradas que tienen más de 180 días. Es útil cuando el estado de la batería del cargador no se actualiza correctamente.", + "Delete old documents": "Eliminar documentos antiguos", + "Delete old documents from entries collection?": "¿Eliminar documentos antiguos de la colección de entradas?", + "%1 is not a valid number": "%1 no es un número válido", + "%1 is not a valid number - must be more than 2": "%1 no es un número válido - debe ser más de 2", + "Clean Mongo treatments database": "Limpiar base de datos de tratamientos de Mongo", + "Delete all documents from treatments collection older than 180 days": "Eliminar todos los documentos de la colección de tratamientos de hace más de 180 días", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Esta tarea elimina todos los documentos de la colección de tratamientos que tienen más de 180 días. Es útil cuando el estado de la batería del cargador no se actualiza correctamente.", + "Delete old documents from treatments collection?": "¿Eliminar documentos antiguos de la colección de entradas?", + "Admin Tools": "Herramientas Administrativas", + "Nightscout reporting": "Informes - Nightscout", + "Cancel": "Cancelar", + "Edit treatment": "Editar tratamiento", + "Duration": "Duración", + "Duration in minutes": "Duración en minutos", + "Temp Basal": "Tasa basal temporal", + "Temp Basal Start": "Inicio Tasa Basal temporal", + "Temp Basal End": "Fin Tasa Basal temporal", + "Percent": "Porcentaje", + "Basal change in %": "Basal modificada en %", + "Basal value": "Valor basal", + "Absolute basal value": "Valor basal absoluto", + "Announcement": "Aviso", + "Loading temp basal data": "Cargando datos tasa basal temporal", + "Save current record before changing to new?": "Grabar datos actuales antes de cambiar por nuevos?", + "Profile Switch": "Cambiar Perfil", + "Profile": "Perfil", + "General profile settings": "Configuración perfil genérico", + "Title": "Titulo", + "Database records": "Registros grabados en base datos", + "Add new record": "Añadir nuevo registro", + "Remove this record": "Eliminar este registro", + "Clone this record to new": "Duplicar este registro como nuevo", + "Record valid from": "Registro válido desde", + "Stored profiles": "Perfiles guardados", + "Timezone": "Zona horaria", + "Duration of Insulin Activity (DIA)": "Duración Insulina Activa (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Representa la duración típica durante la cual la insulina tiene efecto. Varía por paciente y por tipo de insulina. Típicamente 3-4 horas para la mayoría de la insulina bombeada y la mayoría de los pacientes. A veces también se llama insulina de por vida ", + "Insulin to carb ratio (I:C)": "Relación Insulina/Carbohidratos (I:C)", + "Hours:": "Horas:", + "hours": "horas", + "g/hour": "gr/hora", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "gr. de carbohidratos por unidad de insulina. La proporción de cuántos gramos de carbohidratos se consumen por unidad de insulina.", + "Insulin Sensitivity Factor (ISF)": "Factor Sensibilidad a la Insulina (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dl o mmol/L por unidad Insulina. La relación de la caída de glucosa y cada unidad de insulina de corrección administrada.", + "Carbs activity / absorption rate": "Actividad de carbohidratos / tasa de absorción", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gramos por unidad de tiempo. Representa tanto el cambio en carbohidratos activos por unidad de tiempo como la cantidad de carbohidratos absorbidos durante este tiempo. Las curvas de captación / actividad de carbohidratos son más difíciles de predecir que la insulina activa, pero se pueden calcular utilizando un retraso de inicio seguido de una tasa de absorción constante (gr/h)", + "Basal rates [unit/hour]": "Tasas basales [Unidad/hora]", + "Target BG range [mg/dL,mmol/L]": "Intervalo glucemia dentro del objetivo [mg/dL,mmol/L]", + "Start of record validity": "Inicio de validez de datos", + "Icicle": "Inverso", + "Render Basal": "Representación Basal", + "Profile used": "Perfil utilizado", + "Calculation is in target range.": "El cálculo está dentro del rango objetivo", + "Loading profile records ...": "Cargando datos perfil ....", + "Values loaded.": "Valores cargados", + "Default values used.": "Se usan valores predeterminados", + "Error. Default values used.": "Error. Se usan valores predeterminados.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Los marcos temporales para objetivo inferior y objetivo superior no coinciden. Los valores predeterminados son usados.", + "Valid from:": "Válido desde:", + "Save current record before switching to new?": "Grabar datos actuales antes cambiar a uno nuevo?", + "Add new interval before": "Agregar nuevo intervalo antes", + "Delete interval": "Borrar intervalo", + "I:C": "I:C", + "ISF": "ISF (Factor Sensibilidad Insulina)", + "Combo Bolus": "Combo-Bolo", + "Difference": "Diferencia", + "New time": "Nueva hora", + "Edit Mode": "Modo edición", + "When enabled icon to start edit mode is visible": "Si está activado, el icono estará visible al inicio del modo de edición", + "Operation": "Operación", + "Move": "Mover", + "Delete": "Borrar", + "Move insulin": "Mover insulina", + "Move carbs": "Mover carbohidratos", + "Remove insulin": "Eliminar insulina", + "Remove carbs": "Eliminar carbohidratos", + "Change treatment time to %1 ?": "Cambiar horario tratamiento a %1 ?", + "Change carbs time to %1 ?": "Cambiar horario carbohidratos a %1 ?", + "Change insulin time to %1 ?": "Cambiar horario insulina a %1 ?", + "Remove treatment ?": "Eliminar tratamiento?", + "Remove insulin from treatment ?": "Eliminar insulina del tratamiento?", + "Remove carbs from treatment ?": "Eliminar carbohidratos del tratamiento?", + "Rendering": "Gráfica", + "Loading OpenAPS data of": "Cargando datos OpenAPS de", + "Loading profile switch data": "Cargando el cambio de perfil de datos", + "Redirecting you to the Profile Editor to create a new profile.": "Configuración incorrecta del perfil. \n No establecido ningún perfil en el tiempo mostrado. \n Continuar en editor de perfil para crear perfil nuevo.", + "Pump": "Bomba", + "Sensor Age": "Días uso del sensor", + "Insulin Age": "Días uso cartucho insulina", + "Temporary target": "Objetivo temporal", + "Reason": "Razonamiento", + "Eating soon": "Comer pronto", + "Top": "Superior", + "Bottom": "Inferior", + "Activity": "Actividad", + "Targets": "Objetivos", + "Bolus insulin:": "Bolo de Insulina", + "Base basal insulin:": "Insulina basal básica", + "Positive temp basal insulin:": "Insulina Basal temporal positiva:", + "Negative temp basal insulin:": "Insulina basal temporal negativa:", + "Total basal insulin:": "Total Insulina basal:", + "Total daily insulin:": "Total Insulina diaria:", + "Unable to save Role": "No se ha podido guardar el perfil", + "Unable to delete Role": "Imposible elimar Rol", + "Database contains %1 roles": "Base de datos contiene %1 Roles", + "Edit Role": "Editar Rol", + "admin, school, family, etc": "Adminitrador, escuela, família, etc", + "Permissions": "Permisos", + "Are you sure you want to delete: ": "Seguro que quieres eliminarlo:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Cada Rol tiene uno o más permisos. El permiso * es un marcador de posición y los permisos son jerárquicos con : como separador.", + "Add new Role": "Añadir nuevo Rol", + "Roles - Groups of People, Devices, etc": "Roles - Grupos de Gente, Dispositivos, etc.", + "Edit this role": "Editar este Rol", + "Admin authorized": "Administrador autorizado", + "Subjects - People, Devices, etc": "Sujetos - Personas, Dispositivos, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Cada sujeto tendrá un identificador de acceso único y 1 o más roles. Haga clic en el identificador de acceso para abrir una nueva vista con el tema seleccionado, este enlace secreto puede ser compartido.", + "Add new Subject": "Añadir nuevo Sujeto", + "Unable to save Subject": "No se puede guardar el objeto", + "Unable to delete Subject": "Imposible eliminar objeto", + "Database contains %1 subjects": "Base de datos contiene %1 objetos", + "Edit Subject": "Editar objeto", + "person, device, etc": "Persona, dispositivo, etc", + "role1, role2": "Rol1, Rol2", + "Edit this subject": "Editar este objeto", + "Delete this subject": "Eliminar este sujeto", + "Roles": "Roles", + "Access Token": "Acceso Identificador", + "hour ago": "hora atrás", + "hours ago": "horas atrás", + "Silence for %1 minutes": "Silenciado por %1 minutos", + "Check BG": "Verificar glucemia", + "BASAL": "BASAL", + "Current basal": "Basal actual", + "Sensitivity": "Sensibilidad", + "Current Carb Ratio": "Relación actual Insulina:Carbohidratos", + "Basal timezone": "Zona horaria basal", + "Active profile": "Perfil activo", + "Active temp basal": "Basal temporal activa", + "Active temp basal start": "Inicio Basal temporal activa", + "Active temp basal duration": "Duración Basal Temporal activa", + "Active temp basal remaining": "Basal temporal activa restante", + "Basal profile value": "Valor perfil Basal", + "Active combo bolus": "Activo combo-bolo", + "Active combo bolus start": "Inicio del combo-bolo activo", + "Active combo bolus duration": "Duración del Combo-Bolo activo", + "Active combo bolus remaining": "Restante Combo-Bolo activo", + "BG Delta": "Diferencia de glucemia", + "Elapsed Time": "Tiempo transcurrido", + "Absolute Delta": "Diferencia absoluta", + "Interpolated": "Interpolado", + "BWP": "VistaPreviaCalculoBolo (BWP)", + "Urgent": "Urgente", + "Warning": "Aviso", + "Info": "Información", + "Lowest": "Más bajo", + "Snoozing high alarm since there is enough IOB": "Ignorar alarma de Hiperglucemia al tener suficiente insulina activa", + "Check BG, time to bolus?": "Controle su glucemia, ¿quizás un bolo Insulina?", + "Notice": "Nota", + "required info missing": "Falta información requerida", + "Insulin on Board": "Insulina activa (IOB)", + "Current target": "Objetivo actual", + "Expected effect": "Efecto previsto", + "Expected outcome": "Resultado previsto", + "Carb Equivalent": "Equivalencia en Carbohidratos", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Exceso de insulina en %1U más de lo necesario para alcanzar un objetivo bajo, sin tener en cuenta los carbohidratos", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Exceso de insulina en %1U más de la necesaria para alcanzar objetivo inferior. ASEGÚRESE QUE LA INSULINA ACTIVA IOB ESTA CUBIERTA POR CARBOHIDRATOS", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Se necesita una reducción del %1U insulina activa para objetivo inferior, exceso en basal?", + "basal adjustment out of range, give carbs?": "ajuste basal fuera de rango, dar carbohidratos?", + "basal adjustment out of range, give bolus?": "Ajuste de basal fuera de rango, corregir con insulina?", + "above high": "por encima límite superior", + "below low": "por debajo límite inferior", + "Projected BG %1 target": "Glucemia estimada %1 en objetivo", + "aiming at": "resultado deseado", + "Bolus %1 units": "Bolus %1 unidades", + "or adjust basal": "o ajustar basal", + "Check BG using glucometer before correcting!": "Verifique glucemia antes de corregir!", + "Basal reduction to account %1 units:": "Reducir basal para compesar %1 unidades:", + "30m temp basal": "30 min. temporal basal", + "1h temp basal": "1h temporal Basal", + "Cannula change overdue!": "¡Cambio de agujas vencido!", + "Time to change cannula": " Hora sustituir cánula", + "Change cannula soon": "Cambiar cannula pronto", + "Cannula age %1 hours": "Cánula usada %1 horas", + "Inserted": "Insertado", + "CAGE": "Cánula desde", + "COB": "Carb. activos", + "Last Carbs": "último carbohidrato", + "IAGE": "Insul.desde", + "Insulin reservoir change overdue!": "Excedido plazo del cambio depósito de insulina!", + "Time to change insulin reservoir": "Hora de sustituir depósito insulina", + "Change insulin reservoir soon": "Sustituir depósito insulina en breve", + "Insulin reservoir age %1 hours": "Depósito insulina desde %1 horas", + "Changed": "Cambiado", + "IOB": "Insulina Activa IOB", + "Careportal IOB": "Insulina activa en Careportal", + "Last Bolus": "Último bolo", + "Basal IOB": "Basal Insulina activa", + "Source": "Fuente", + "Stale data, check rig?": "Datos desactualizados, controlar la subida?", + "Last received:": "Último recibido:", + "%1m ago": "%1min. atrás", + "%1h ago": "%1h. atrás", + "%1d ago": "%1d atrás", + "RETRO": "RETRO", + "SAGE": "Sensor desde", + "Sensor change/restart overdue!": "Sustituir/reiniciar, sensor vencido", + "Time to change/restart sensor": "Hora de sustituir/reiniciar sensor", + "Change/restart sensor soon": "Cambiar/Reiniciar sensor en breve", + "Sensor age %1 days %2 hours": "Sensor desde %1 días %2 horas", + "Sensor Insert": "Insertar sensor", + "Sensor Start": "Inicio del sensor", + "days": "días", + "Insulin distribution": "Distribución de la insulina", + "To see this report, press SHOW while in this view": "Presione SHOW para mostrar el informe en esta vista", + "AR2 Forecast": "Pronóstico AR2", + "OpenAPS Forecasts": "Pronóstico OpenAPS", + "Temporary Target": "Objetivo temporal", + "Temporary Target Cancel": "Objetivo temporal cancelado", + "OpenAPS Offline": "OpenAPS desconectado", + "Profiles": "Perfiles", + "Time in fluctuation": "Tiempo fluctuando", + "Time in rapid fluctuation": "Tiempo fluctuando rápido", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Esto es sólo una estimación apróximada que puede ser muy inexacta y no reemplaza las pruebas de sangre reales. La fórmula utilizada está tomada de: ", + "Filter by hours": "Filtrar por horas", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Tiempo en fluctuación y Tiempo en fluctuación rápida miden el % de tiempo del período exáminado, durante la cual la glucosa en sangre ha estado cambiando relativamente rápido o rápidamente. Valores más bajos son mejores.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "El cambio medio diario total es la suma de los valores absolutos de todas las glucémias en el período examinado, dividido por el número de días. Mejor valores bajos.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "El cambio medio por hora, es la suma del valor absoluto de todas las glucemias para el período examinado, dividido por el número de horas en el período. Más bajo es mejor.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "El RMS fuera de rango se calcula al cuadrar la distancia fuera de rango para todas las lecturas de glucosa durante el período examinado. resumiéndolos, dividiéndolos por el contador y tomando la raíz cuadrada. Esta métrica es similar al porcentaje en el rango pero pesa lecturas fuera del rango más alto. Valores más bajos son mejores.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">here.", + "Mean Total Daily Change": "Variación media total diaria", + "Mean Hourly Change": "Variación media total por horas", + "FortyFiveDown": "Disminuye lentamente", + "FortyFiveUp": "Asciende lentamente", + "Flat": "Sin variación", + "SingleUp": "Ascendiendo", + "SingleDown": "Bajando", + "DoubleDown": "Bajando rápido", + "DoubleUp": "Subiendo rápido", + "virtAsstUnknown": "Ese valor es desconocido en este momento. Por favor, consulta tu sitio de Nightscout para más detalles.", + "virtAsstTitleAR2Forecast": "Pronóstico AR2", + "virtAsstTitleCurrentBasal": "Basal actual", + "virtAsstTitleCurrentCOB": "COB actual", + "virtAsstTitleCurrentIOB": "IOB actual", + "virtAsstTitleLaunch": "Bienvenido a Nightscout", + "virtAsstTitleLoopForecast": "Pronóstico del lazo", + "virtAsstTitleLastLoop": "Último ciclo del lazo", + "virtAsstTitleOpenAPSForecast": "Pronóstico OpenAPS", + "virtAsstTitlePumpReservoir": "Insulina restante", + "virtAsstTitlePumpBattery": "Batería de la Bomba", + "virtAsstTitleRawBG": "GS Actual en crudo", + "virtAsstTitleUploaderBattery": "Batería del Uploader", + "virtAsstTitleCurrentBG": "BG actual", + "virtAsstTitleFullStatus": "Estado completo", + "virtAsstTitleCGMMode": "Modo MCG", + "virtAsstTitleCGMStatus": "Estado de MCG", + "virtAsstTitleCGMSessionAge": "Edad de la sesión MCG", + "virtAsstTitleCGMTxStatus": "Estado del transmisor MCG", + "virtAsstTitleCGMTxAge": "Edad del transmisor MCG", + "virtAsstTitleCGMNoise": "Ruido MCG", + "virtAsstTitleDelta": "Delta glucosa de sangre", + "virtAsstStatus": "%1 y %2 como de %3.", + "virtAsstBasal": "%1 basal actual es %2 unidades por hora", + "virtAsstBasalTemp": "%1 Basal temporal de %2 unidades por hora hasta el fin %3", + "virtAsstIob": "y tu tienes %1 insulina activa.", + "virtAsstIobIntent": "Tienes %1 insulina activa", + "virtAsstIobUnits": "%1 unidades de", + "virtAsstLaunch": "¿Qué te gustaría comprobar en Nightscout?", + "virtAsstPreamble": "Tu", + "virtAsstPreamble3person": "%1 tiene un ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "La batería del cargador está en %1", + "virtAsstReservoir": "Tienes %1 unidades restantes", + "virtAsstPumpBattery": "La batería de la bomba está en %1 %2", + "virtAsstUploaderBattery": "La batería del cargador está en %1", + "virtAsstLastLoop": "El último lazo exitoso fue %1", + "virtAsstLoopNotAvailable": "El plugin Loop parece no estar habilitado", + "virtAsstLoopForecastAround": "De acuerdo con la previsión del lazo se espera que estés alrededor de %1 durante los siguientes %2", + "virtAsstLoopForecastBetween": "De acuerdo con la previsión del lazo se espera que estés entre %1 y %2 en los siguientes %3", + "virtAsstAR2ForecastAround": "De acuerdo con la previsión del lazo se espera que estés alrededor de %1 durante los siguientes %2", + "virtAsstAR2ForecastBetween": "De acuerdo con la previsión del lazo se espera que estés entre %1 y %2 en los siguientes %2", + "virtAsstForecastUnavailable": "No se puede predecir con los datos que están disponibles", + "virtAsstRawBG": "Tu Bg crudo es %1", + "virtAsstOpenAPSForecast": "El pronóstico de BG de OpenAPS es %1", + "virtAsstCob3person": "%1 tiene %2 hidratos de carbono a bordo", + "virtAsstCob": "Tienes %1 hidratos de carbono a bordo", + "virtAsstCGMMode": "Tu modo MCG era %1 a partir de %2.", + "virtAsstCGMStatus": "Su estado MCG fue %1 a partir de %2.", + "virtAsstCGMSessAge": "Su sesión MCG ha estado activa durante %1 días y %2 horas.", + "virtAsstCGMSessNotStarted": "No hay ninguna sesión MCG activa en este momento.", + "virtAsstCGMTxStatus": "Su estado del transmisor MCG fue %1 a %2.", + "virtAsstCGMTxAge": "Tu transmisor MCG tiene %1 días.", + "virtAsstCGMNoise": "El ruido MCG fue %1 a partir de %2.", + "virtAsstCGMBattOne": "Su batería MCG fue de %1 voltios a %2.", + "virtAsstCGMBattTwo": "Sus niveles de batería MCG eran %1 voltios y %2 voltios a %3.", + "virtAsstDelta": "Tu delta estaba %1 entre %2 y %3.", + "virtAsstDeltaEstimated": "Tu delta estimada era %1 entre %2 y %3.", + "virtAsstUnknownIntentTitle": "Intento desconocido", + "virtAsstUnknownIntentText": "Lo siento, no sé lo que estás pidiendo.", + "Fat [g]": "Grasas [g]", + "Protein [g]": "Proteina [g]", + "Energy [kJ]": "Energía [Kj]", + "Clock Views:": "Vista del reloj:", + "Clock": "Reloj", + "Color": "Color", + "Simple": "Simple", + "TDD average": "Promedio de TDD", + "Bolus average": "Bolo medio", + "Basal average": "Basal media", + "Base basal average:": "Media basal base:", + "Carbs average": "Carbohidratos promedio", + "Eating Soon": "ComidendoPronto", + "Last entry {0} minutes ago": "Última entrada hace {0} minutos", + "change": "cambio", + "Speech": "Voz", + "Target Top": "Objetivo alto", + "Target Bottom": "Objetivo Bajo", + "Canceled": "Cancelado", + "Meter BG": "Meter BG", + "predicted": "previsto", + "future": "próximo", + "ago": "hace", + "Last data received": "Últimos datos recibidos", + "Clock View": "Vista del reloj", + "Protein": "Proteínas", + "Fat": "Grasa", + "Protein average": "Proteína media", + "Fat average": "Media de grasa", + "Total carbs": "Total de glúcidos", + "Total protein": "Total de proteínas", + "Total fat": "Grasa total", + "Database Size": "Tamaño de la Base de Datos", + "Database Size near its limits!": "Tamaño de la base de datos cerca de sus límites!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "El tamaño de la base de datos es %1 MiB de %2 MiB. ¡Por favor, haz una copia de seguridad y limpia la base de datos!", + "Database file size": "Tamaño del archivo de la base de datos", + "%1 MiB of %2 MiB (%3%)": "%1 MiB de %2 MiB (%3%)", + "Data size": "Tamaño de los datos", + "virtAsstDatabaseSize": "%1 MiB. Es el %2% del espacio disponible en la base de datos.", + "virtAsstTitleDatabaseSize": "Tamaño del archivo de la base de datos", + "Carbs/Food/Time": "Carbs/Comida/Hora", + "You have administration messages": "Tienes mensajes de administración", + "Admin messages in queue": "Mensajes de administración en cola", + "Queue empty": "Cola vacía", + "There are no admin messages in queue": "No hay mensajes de administración en cola", + "Please sign in using the API_SECRET to see your administration messages": "Por favor, inicia sesión usando API_SECRET para ver tus mensajes de administración", + "Reads enabled in default permissions": "Lecturas habilitadas en los permisos por defecto", + "Data reads enabled": "Lecturas de datos habilitadas", + "Data writes enabled": "Escrituras de datos habilitadas", + "Data writes not enabled": "Escrituras de datos no habilitadas", + "Color prediction lines": "Líneas de predicción de color", + "Release Notes": "Notas informativas", + "Check for Updates": "Buscar actualizaciones", + "Open Source": "Código abierto", + "Nightscout Info": "Información de Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "El objetivo principal de Loopalyzer es visualizar cómo funciona el sistema de lazo cerrado. También puede funcionar con otras configuraciones, tanto de lazo cerrado como abierto, y sin loop. Sin embargo, dependiendo del uploader que utilice, con qué frecuencia es capaz de capturar sus datos y cargar, y cómo es capaz de rellenar datos faltantes, algunos gráficos pueden tener lagunas o incluso estar completamente vacíos. Siempre asegúrese de que los gráficos se vean razonables. Mejor es ver un día a la vez y pasar a través de un número de días para ver.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer incluye una función de cambio de tiempo. Si por ejemplo usted toma el desayuno a las 07:00 un día y a las 08:00 el día después de su curva media de glucosa, estos dos días muy probablemente se verán aplanados y no mostrarán la respuesta real después de un desayuno. El cambio de tiempo calculará el tiempo promedio de estas comidas se comieron y luego desplazará todos los datos (carbones, insulina, basal etc. durante ambos días la diferencia horaria correspondiente de modo que ambas comidas se alineen con la hora media de inicio de la comida.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "En este ejemplo todos los datos del primer día se empujan 30 minutos hacia adelante en el tiempo y todos los datos del segundo día 30 minutos hacia atrás en el tiempo así que parece como si usted tuviera el desayuno a las 07:30 ambos días. Esto le permite ver su respuesta real promedio de glucosa en la sangre a través de una comida.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "El cambio de tiempo resalta el período después de la comida media de inicio en gris, durante la duración de la DIA (Duración de la acción de la insulina). Como todos los datos apuntan el día entero se desplazan las curvas fuera del área gris pueden no ser exactas.", + "Note that time shift is available only when viewing multiple days.": "Tenga en cuenta que el cambio de tiempo sólo está disponible cuando se visualiza varios días.", + "Please select a maximum of two weeks duration and click Show again.": "Por favor, seleccione un máximo de dos semanas de duración y haga clic en Mostrar de nuevo.", + "Show profiles table": "Mostrar tabla de perfiles", + "Show predictions": "Mostrar las predicciones", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Diferencia de horas entre comidas con más de %1 g de carbohidratos consumidos entre %2 y %3", + "Previous": "Anterior", + "Previous day": "Día anterior", + "Next day": "Día siguiente", + "Next": "Siguiente", + "Temp basal delta": "Delta basal temporal", + "Authorized by token": "Autorizado por token", + "Auth role": "Rol de autenticación", + "view without token": "ver sin token", + "Remove stored token": "Eliminar token almacenado", + "Weekly Distribution": "Distribución semanal", + "Failed authentication": "Autenticación fallida", + "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?": "Un dispositivo en la dirección IP %1 intentó autenticarse con Nightscout con credenciales incorrectas. Compruebe si tiene una configuración del cargador/uploader con API_SECRET o token incorrecto?", + "Default (with leading zero and U)": "Por defecto (con cero a la izquierda y U)", + "Concise (with U, without leading zero)": "Conciso (con U, sin cero a la izquierda)", + "Minimal (without leading zero and U)": "Mínimo (sin cero a la izquierda y U)", + "Small Bolus Display": "Visualización de bolo pequeño", + "Large Bolus Display": "Visualización de bolo grande", + "Bolus Display Threshold": "Umbral de visualización de bolo", + "%1 U and Over": "%1 U y más", + "Event repeated %1 times.": "Evento repetido %1 veces.", + "minutes": "minutos", + "Last recorded %1 %2 ago.": "Última grabación hace %1 %2.", + "Security issue": "Problema de seguridad", + "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.": "Detectada API_SECRET débil. Por favor, utilice una mezcla de letras en minúscula y en mayúscula, números y caracteres no alfanuméricos como !#%&/ para reducir el riesgo de acceso no autorizado. La longitud mínima de la API_SECRET es de 12 caracteres.", + "less than 1": "menos de 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "La contraseña de MongoDB y la API_SECRET coinciden. Esto es una muy mala idea. Por favor, cambie ambas contraseñas y no reutilice contraseñas en el sistema." +} diff --git a/translations/et_EE.json b/translations/et_EE.json new file mode 100644 index 00000000000..daa8c7186c7 --- /dev/null +++ b/translations/et_EE.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Kuulamine pordil", + "Mo": "E", + "Tu": "T", + "We": "K", + "Th": "N", + "Fr": "R", + "Sa": "L", + "Su": "P", + "Monday": "Esmaspäev", + "Tuesday": "Teisipäev", + "Wednesday": "Kolmapäev", + "Thursday": "Neljapäev", + "Friday": "Reede", + "Saturday": "Laupäev", + "Sunday": "Pühapäev", + "Category": "Kategooria", + "Subcategory": "Alamkategooria", + "Name": "Nimi", + "Today": "Täna", + "Last 2 days": "Viimased 2 päeva", + "Last 3 days": "Viimased 3 päeva", + "Last week": "Viimane nädal", + "Last 2 weeks": "Viimased 2 nädalat", + "Last month": "Viimane kuu", + "Last 3 months": "Viimased 3 kuud", + "From": "Alates", + "To": "Kuni", + "Notes": "Märkused", + "Food": "Toit", + "Insulin": "Insuliin", + "Carbs": "Süsivesikud", + "Notes contain": "Märkused sisaldavad", + "Target BG range bottom": "Veresuhkru vahemik alates", + "top": "kuni", + "Show": "Näita", + "Display": "Näita", + "Loading": "Laadimine", + "Loading profile": "Profiili laadimine", + "Loading status": "Staatuse laadimine", + "Loading food database": "Toidu andmebaasi laadimine", + "not displayed": "not displayed", + "Loading CGM data of": "Loading CGM data of", + "Loading treatments data of": "Loading treatments data of", + "Processing data of": "Processing data of", + "Portion": "Portsjon", + "Size": "Suurus", + "(none)": "(puudub)", + "None": "Puudub", + "": "", + "Result is empty": "Result is empty", + "Day to day": "Day to day", + "Week to week": "Week to week", + "Daily Stats": "Daily Stats", + "Percentile Chart": "Percentile Chart", + "Distribution": "Distribution", + "Hourly stats": "Hourly stats", + "netIOB stats": "netIOB stats", + "temp basals must be rendered to display this report": "temp basals must be rendered to display this report", + "No data available": "No data available", + "Low": "Madal", + "In Range": "Vahemikus", + "Period": "Period", + "High": "Kõrge", + "Average": "Keskmine", + "Low Quartile": "Low Quartile", + "Upper Quartile": "Upper Quartile", + "Quartile": "Quartile", + "Date": "Kuupäev", + "Normal": "Normal", + "Median": "Mediaan", + "Readings": "Readings", + "StDev": "StDev", + "Daily stats report": "Daily stats report", + "Glucose Percentile report": "Glucose Percentile report", + "Glucose distribution": "Glucose distribution", + "days total": "päeva kokku", + "Total per day": "Total per day", + "Overall": "Overall", + "Range": "Vahemik", + "% of Readings": "% näitudest", + "# of Readings": "Näitude arv", + "Mean": "Keskmine", + "Standard Deviation": "Standardhälve", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "A1c ennustus*", + "Weekly Success": "Weekly Success", + "There is not sufficient data to run this report. Select more days.": "Aruande koostamiseks pole piisavalt andmeid. Vali rohkem päevi.", + "Using stored API secret hash": "Using stored API secret hash", + "No API secret hash stored yet. You need to enter API secret.": "No API secret hash stored yet. You need to enter API secret.", + "Database loaded": "Andmebaas laaditud", + "Error: Database failed to load": "Viga: Andmebaasi laadimine ebaõnnestus", + "Error": "Viga", + "Create new record": "Lisa uus kirje", + "Save record": "Salvesta kirje", + "Portions": "Portsjonid", + "Unit": "Ühik", + "GI": "GI", + "Edit record": "Muuda kirjet", + "Delete record": "Kustuta kirje", + "Move to the top": "Tagasi algusesse", + "Hidden": "Peidetud", + "Hide after use": "Peida pärast kasutamist", + "Your API secret must be at least 12 characters long": "Sinu API salasõna peab olema vähemalt 12 tähemärki", + "Bad API secret": "Bad API secret", + "API secret hash stored": "API salasõna räsi salvestatud", + "Status": "Staatus", + "Not loaded": "Laadimata", + "Food Editor": "Food Editor", + "Your database": "Sinu andmebaas", + "Filter": "Filter", + "Save": "Salvesta", + "Clear": "Tühjenda", + "Record": "Kirje", + "Quick picks": "Kiirvalikud", + "Show hidden": "Näita peidetuid", + "Your API secret or token": "Sinu API salasõna", + "Remember this device. (Do not enable this on public computers.)": "Jäta see seade meelde. (Ära kasuta avalikus arvutis.)", + "Treatments": "Treatments", + "Time": "Aeg", + "Event Type": "Sündmuse tüüp", + "Blood Glucose": "Veresuhkur", + "Entered By": "Sisestas", + "Delete this treatment?": "Delete this treatment?", + "Carbs Given": "Süsivesikud", + "Insulin Given": "Insuliin", + "Event Time": "Kellaaeg", + "Please verify that the data entered is correct": "Palun veendu, et sisestatud andmed on õiged", + "BG": "VS", + "Use BG correction in calculation": "Use BG correction in calculation", + "BG from CGM (autoupdated)": "VS sensorist (automaatselt uuendatud)", + "BG from meter": "VS glükomeetrist", + "Manual BG": "Käsitsi sisestatud VS", + "Quickpick": "Kiirvalik", + "or": "või", + "Add from database": "Lisa andmebaasist", + "Use carbs correction in calculation": "Use carbs correction in calculation", + "Use COB correction in calculation": "Use COB correction in calculation", + "Use IOB in calculation": "Use IOB in calculation", + "Other correction": "Other correction", + "Rounding": "Ümardus", + "Enter insulin correction in treatment": "Enter insulin correction in treatment", + "Insulin needed": "Insulin needed", + "Carbs needed": "Carbs needed", + "Carbs needed if Insulin total is negative value": "Carbs needed if Insulin total is negative value", + "Basal rate": "Basal rate", + "60 minutes earlier": "60 minutit varem", + "45 minutes earlier": "45 minutit varem", + "30 minutes earlier": "30 minutit varem", + "20 minutes earlier": "20 minutit varem", + "15 minutes earlier": "15 minutit varem", + "Time in minutes": "Aeg minutites", + "15 minutes later": "15 minutit hiljem", + "20 minutes later": "20 minutit hiljem", + "30 minutes later": "30 minutit hiljem", + "45 minutes later": "45 minutit hiljem", + "60 minutes later": "60 minutit hiljem", + "Additional Notes, Comments": "Additional Notes, Comments", + "RETRO MODE": "RETRO MODE", + "Now": "Nüüd", + "Other": "Other", + "Submit Form": "Saada vorm", + "Profile Editor": "Profiiliredaktor", + "Reports": "Aruanded", + "Add food from your database": "Lisa toit oma andmebaasist", + "Reload database": "Laadi andmebaas uuesti", + "Add": "Lisa", + "Unauthorized": "Volitamata", + "Entering record failed": "Kirje lisamine ebaõnnestus", + "Device authenticated": "Seadme autentimine", + "Device not authenticated": "Seade pole autenditud", + "Authentication status": "Autentimise staatus", + "Authenticate": "Autendi", + "Remove": "Eemalda", + "Your device is not authenticated yet": "Su seade pole veel autenditud", + "Sensor": "Sensor", + "Finger": "Sõrm", + "Manual": "Manual", + "Scale": "Skaala", + "Linear": "Lineaarne", + "Logarithmic": "Logaritmiline", + "Logarithmic (Dynamic)": "Logaritmiline (Dünaamiline)", + "Insulin-on-Board": "Insulin-on-Board", + "Carbs-on-Board": "Carbs-on-Board", + "Bolus Wizard Preview": "Bolus Wizard Preview", + "Value Loaded": "Value Loaded", + "Cannula Age": "Kanüüli vanus", + "Basal Profile": "Basal Profile", + "Silence for 30 minutes": "Vaigista 30 minutiks", + "Silence for 60 minutes": "Vaigista 60 minutiks", + "Silence for 90 minutes": "Vaigista 90 minutiks", + "Silence for 120 minutes": "Vaigista 120 minutiks", + "Settings": "Seaded", + "Units": "Ühikud", + "Date format": "Kellaaja formaat", + "12 hours": "12 tundi", + "24 hours": "24 tundi", + "Log a Treatment": "Log a Treatment", + "BG Check": "VS kontroll", + "Meal Bolus": "Toiduboolus", + "Snack Bolus": "Snäkiboolus", + "Correction Bolus": "Korrektsiooniboolus", + "Carb Correction": "Süsivesikute parandus", + "Note": "Märkus", + "Question": "Küsimus", + "Exercise": "Füüsiline tegevus", + "Pump Site Change": "Pumba asukoha muutus", + "CGM Sensor Start": "CGM sensori algus", + "CGM Sensor Stop": "CGM sensori lõpp", + "CGM Sensor Insert": "CGM sensori sisestamine", + "Sensor Code": "Sensori kood", + "Transmitter ID": "Saatja ID", + "Dexcom Sensor Start": "Dexcom sensori algus", + "Dexcom Sensor Change": "Dexcom sensori lõpp", + "Insulin Cartridge Change": "Insulin Cartridge Change", + "D.A.D. Alert": "D.A.D. Alert", + "Glucose Reading": "Veresuhkru näit", + "Measurement Method": "Mõõtmismeetod", + "Meter": "Glükomeeter", + "Amount in grams": "Kogus grammides", + "Amount in units": "Kogus ühikutes", + "View all treatments": "View all treatments", + "Enable Alarms": "Enable Alarms", + "Pump Battery Change": "Pumba patarei vahetus", + "Pump Battery Age": "Pumba aku vanus", + "Pump Battery Low Alarm": "Pump Battery Low Alarm", + "Pump Battery change overdue!": "Pump Battery change overdue!", + "When enabled an alarm may sound.": "When enabled an alarm may sound.", + "Urgent High Alarm": "Urgent High Alarm", + "High Alarm": "High Alarm", + "Low Alarm": "Low Alarm", + "Urgent Low Alarm": "Urgent Low Alarm", + "Stale Data: Warn": "Aegunud andmed: hoiatus", + "Stale Data: Urgent": "Stale Data: Urgent", + "mins": "minutit", + "Night Mode": "Öörežiim", + "When enabled the page will be dimmed from 10pm - 6am.": "Kui aktiivne, hämardatakse lehte vahemikus 22:00 - 06:00.", + "Enable": "Aktiveeri", + "Show Raw BG Data": "Show Raw BG Data", + "Never": "Never", + "Always": "Alati", + "When there is noise": "When there is noise", + "When enabled small white dots will be displayed for raw BG data": "When enabled small white dots will be displayed for raw BG data", + "Custom Title": "Custom Title", + "Theme": "Theme", + "Default": "Vaikimisi", + "Colors": "Colors", + "Colorblind-friendly colors": "Colorblind-friendly colors", + "Reset, and use defaults": "Lähtesta ja kasuta vaikeväärtuseid", + "Calibrations": "Kalibreerimised", + "Alarm Test / Smartphone Enable": "Alarm Test / Smartphone Enable", + "Bolus Wizard": "Boolusearvesti", + "in the future": "tulevikus", + "time ago": "aega tagasi", + "hr ago": "tund tagasi", + "hrs ago": "tundi tagasi", + "min ago": "minut tagasi", + "mins ago": "minutit tagasi", + "day ago": "päev tagasi", + "days ago": "päeva tagasi", + "long ago": "ammu", + "Clean": "Clean", + "Light": "Light", + "Medium": "Medium", + "Heavy": "Heavy", + "Treatment type": "Treatment type", + "Raw BG": "Raw BG", + "Device": "Seade", + "Noise": "Noise", + "Calibration": "Calibration", + "Show Plugins": "Show Plugins", + "About": "Teave", + "Value in": "Väärtus", + "Carb Time": "Carb Time", + "Language": "Keel", + "Add new": "Lisa uus", + "g": "g", + "ml": "ml", + "pcs": "tk", + "Drag&drop food here": "Drag&drop food here", + "Care Portal": "Care Portal", + "Medium/Unknown": "Medium/Unknown", + "IN THE FUTURE": "TULEVIKUS", + "Order": "Järjekord", + "oldest on top": "vanemad eespool", + "newest on top": "uuemad eespool", + "All sensor events": "Kõik sensori sündmused", + "Remove future items from mongo database": "Eemalda tuleviku sissekanded MongoDB andmebaasist", + "Find and remove treatments in the future": "Find and remove treatments in the future", + "This task find and remove treatments in the future.": "This task find and remove treatments in the future.", + "Remove treatments in the future": "Remove treatments in the future", + "Find and remove entries in the future": "Find and remove entries in the future", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "This task find and remove CGM data in the future created by uploader with wrong date/time.", + "Remove entries in the future": "Remove entries in the future", + "Loading database ...": "Andmebaasi laadimine...", + "Database contains %1 future records": "Andmebaasis on %1 tulevikukirjet", + "Remove %1 selected records?": "Eemalda %1 valitud kirjet?", + "Error loading database": "Viga andmebaasi laadimisel", + "Record %1 removed ...": "Kirje %1 eemaldatud ...", + "Error removing record %1": "Viga kirje %1 eemaldamisel", + "Deleting records ...": "Kirjete kustutamine ...", + "%1 records deleted": "%1 kirjet kustutatud", + "Clean Mongo status database": "Clean Mongo status database", + "Delete all documents from devicestatus collection": "Delete all documents from devicestatus collection", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.", + "Delete all documents": "Delete all documents", + "Delete all documents from devicestatus collection?": "Delete all documents from devicestatus collection?", + "Database contains %1 records": "Andmebaasis on %1 kirjet", + "All records removed ...": "Kõik kirjed eemaldatud ...", + "Delete all documents from devicestatus collection older than 30 days": "Delete all documents from devicestatus collection older than 30 days", + "Number of Days to Keep:": "Number of Days to Keep:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from devicestatus collection?": "Delete old documents from devicestatus collection?", + "Clean Mongo entries (glucose entries) database": "Clean Mongo entries (glucose entries) database", + "Delete all documents from entries collection older than 180 days": "Delete all documents from entries collection older than 180 days", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents": "Kustuta vanad dokumendid", + "Delete old documents from entries collection?": "Delete old documents from entries collection?", + "%1 is not a valid number": "%1 ei ole korrektne arv", + "%1 is not a valid number - must be more than 2": "%1 ei ole korrektne arv - peab olema suurem kui 2", + "Clean Mongo treatments database": "Clean Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Delete all documents from treatments collection older than 180 days", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "Administraatori tööriistad", + "Nightscout reporting": "Nightscout aruanded", + "Cancel": "Katkesta", + "Edit treatment": "Edit treatment", + "Duration": "Kestus", + "Duration in minutes": "Kestus minutites", + "Temp Basal": "Ajutine basaal", + "Temp Basal Start": "Ajutise basaali algus", + "Temp Basal End": "Ajutise basaali lõpp", + "Percent": "Protsent", + "Basal change in %": "Basal change in %", + "Basal value": "Basal value", + "Absolute basal value": "Absolute basal value", + "Announcement": "Teadaanne", + "Loading temp basal data": "Loading temp basal data", + "Save current record before changing to new?": "Save current record before changing to new?", + "Profile Switch": "Profile Switch", + "Profile": "Profiil", + "General profile settings": "Üldised profiili seaded", + "Title": "Pealkiri", + "Database records": "Andmebaasi kirjed", + "Add new record": "Lisa uus kirje", + "Remove this record": "Kustuta see kirje", + "Clone this record to new": "Clone this record to new", + "Record valid from": "Kirje kehtib alates", + "Stored profiles": "Salvestatud profiilid", + "Timezone": "Ajavöönd", + "Duration of Insulin Activity (DIA)": "Duration of Insulin Activity (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.", + "Insulin to carb ratio (I:C)": "Insulin to carb ratio (I:C)", + "Hours:": "Tunnid:", + "hours": "tundi", + "g/hour": "g/tund", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.", + "Insulin Sensitivity Factor (ISF)": "Insulin Sensitivity Factor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.", + "Carbs activity / absorption rate": "Carbs activity / absorption rate", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).", + "Basal rates [unit/hour]": "Basal rates [unit/hour]", + "Target BG range [mg/dL,mmol/L]": "Target BG range [mg/dL,mmol/L]", + "Start of record validity": "Start of record validity", + "Icicle": "Icicle", + "Render Basal": "Render Basal", + "Profile used": "Profile used", + "Calculation is in target range.": "Calculation is in target range.", + "Loading profile records ...": "Loading profile records ...", + "Values loaded.": "Values loaded.", + "Default values used.": "Default values used.", + "Error. Default values used.": "Error. Default values used.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Time ranges of target_low and target_high don't match. Values are restored to defaults.", + "Valid from:": "Kehtiv alates:", + "Save current record before switching to new?": "Save current record before switching to new?", + "Add new interval before": "Add new interval before", + "Delete interval": "Delete interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Komboboolus", + "Difference": "Difference", + "New time": "New time", + "Edit Mode": "Edit Mode", + "When enabled icon to start edit mode is visible": "When enabled icon to start edit mode is visible", + "Operation": "Operation", + "Move": "Liiguta", + "Delete": "Kustuta", + "Move insulin": "Liiguta insuliin", + "Move carbs": "Liiguta süsivesikud", + "Remove insulin": "Eemalda insuliin", + "Remove carbs": "Eemalda süsivesikud", + "Change treatment time to %1 ?": "Change treatment time to %1 ?", + "Change carbs time to %1 ?": "Change carbs time to %1 ?", + "Change insulin time to %1 ?": "Change insulin time to %1 ?", + "Remove treatment ?": "Remove treatment ?", + "Remove insulin from treatment ?": "Remove insulin from treatment ?", + "Remove carbs from treatment ?": "Remove carbs from treatment ?", + "Rendering": "Rendering", + "Loading OpenAPS data of": "Loading OpenAPS data of", + "Loading profile switch data": "Loading profile switch data", + "Redirecting you to the Profile Editor to create a new profile.": "Redirecting you to the Profile Editor to create a new profile.", + "Pump": "Pump", + "Sensor Age": "Sensori vanus", + "Insulin Age": "Insuliini vanus", + "Temporary target": "Temporary target", + "Reason": "Põhjus", + "Eating soon": "Eating soon", + "Top": "Top", + "Bottom": "Bottom", + "Activity": "Activity", + "Targets": "Targets", + "Bolus insulin:": "Bolus insulin:", + "Base basal insulin:": "Basaali põhidoos:", + "Positive temp basal insulin:": "Ajutine basaali tõstmine:", + "Negative temp basal insulin:": "Ajutine basaali vähendamine:", + "Total basal insulin:": "Total basal insulin:", + "Total daily insulin:": "Total daily insulin:", + "Unable to save Role": "Viga rolli salvestamisel", + "Unable to delete Role": "Viga rolli kustutamisel", + "Database contains %1 roles": "Andmebaasis on %1 rolli", + "Edit Role": "Rolli muutmine", + "admin, school, family, etc": "administraator, kool, pere jne.", + "Permissions": "Õigused", + "Are you sure you want to delete: ": "Kas oled kindel, et soovid kustutada: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Igal rollil on 1 või rohkem õigust. Märgend * annab kõik õigused. Õigused on hierarhilised, eraldajaks : (koolon).", + "Add new Role": "Lisa uus roll", + "Roles - Groups of People, Devices, etc": "Rollid - Inimeste, seadmete jm. grupid", + "Edit this role": "Muuda seda rolli", + "Admin authorized": "Admin authorized", + "Subjects - People, Devices, etc": "Subjektid - inimesed, seadmed jne.", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Igal subjektil on oma ligipääsutunnus ning 1 või rohkem rolli. Kliki ligipääsutunnusel, et avada uus vaade valitud subjektina, seejärel saad ligipääsulinki jagada.", + "Add new Subject": "Lisa uus subjekt", + "Unable to save Subject": "Viga subjekti salvestamisel", + "Unable to delete Subject": "Viga subjekti kustutamisel", + "Database contains %1 subjects": "Andmebaasis on %1 subjekti", + "Edit Subject": "Subjekti muutmine", + "person, device, etc": "isik, seade jne.", + "role1, role2": "roll1, roll2", + "Edit this subject": "Muuda seda subjekti", + "Delete this subject": "Kustuta see subjekt", + "Roles": "Rollid", + "Access Token": "Ligipääsutunnus", + "hour ago": "tund tagasi", + "hours ago": "tundi tagasi", + "Silence for %1 minutes": "Vaigista %1 minutiks", + "Check BG": "Check BG", + "BASAL": "BASAL", + "Current basal": "Praegune basaal", + "Sensitivity": "Sensitivity", + "Current Carb Ratio": "Current Carb Ratio", + "Basal timezone": "Basal timezone", + "Active profile": "Active profile", + "Active temp basal": "Active temp basal", + "Active temp basal start": "Active temp basal start", + "Active temp basal duration": "Active temp basal duration", + "Active temp basal remaining": "Active temp basal remaining", + "Basal profile value": "Basal profile value", + "Active combo bolus": "Active combo bolus", + "Active combo bolus start": "Active combo bolus start", + "Active combo bolus duration": "Active combo bolus duration", + "Active combo bolus remaining": "Active combo bolus remaining", + "BG Delta": "BG Delta", + "Elapsed Time": "Elapsed Time", + "Absolute Delta": "Absolute Delta", + "Interpolated": "Interpolated", + "BWP": "BWP", + "Urgent": "Kriitiline", + "Warning": "Hoiatus", + "Info": "Info", + "Lowest": "Lowest", + "Snoozing high alarm since there is enough IOB": "Snoozing high alarm since there is enough IOB", + "Check BG, time to bolus?": "Check BG, time to bolus?", + "Notice": "Notice", + "required info missing": "required info missing", + "Insulin on Board": "Insulin on Board", + "Current target": "Current target", + "Expected effect": "Expected effect", + "Expected outcome": "Expected outcome", + "Carb Equivalent": "Carb Equivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U reduction needed in active insulin to reach low target, too much basal?", + "basal adjustment out of range, give carbs?": "basal adjustment out of range, give carbs?", + "basal adjustment out of range, give bolus?": "basal adjustment out of range, give bolus?", + "above high": "above high", + "below low": "below low", + "Projected BG %1 target": "Projected BG %1 target", + "aiming at": "aiming at", + "Bolus %1 units": "Boolus %1 ühikut", + "or adjust basal": "or adjust basal", + "Check BG using glucometer before correcting!": "Kontrolli veresuhkrut glükomeetriga enne korrigeerimist!", + "Basal reduction to account %1 units:": "Basal reduction to account %1 units:", + "30m temp basal": "30m ajutine basaal", + "1h temp basal": "1h ajutine basaal", + "Cannula change overdue!": "Cannula change overdue!", + "Time to change cannula": "Aeg kanüüli vahetada", + "Change cannula soon": "Vaheta kanüüli varsti", + "Cannula age %1 hours": "Kanüüli vanus %1 tundi", + "Inserted": "Inserted", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Last Carbs", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Insulin reservoir change overdue!", + "Time to change insulin reservoir": "Time to change insulin reservoir", + "Change insulin reservoir soon": "Change insulin reservoir soon", + "Insulin reservoir age %1 hours": "Insulin reservoir age %1 hours", + "Changed": "Vahetatud", + "IOB": "IOB", + "Careportal IOB": "Careportal IOB", + "Last Bolus": "Last Bolus", + "Basal IOB": "Basal IOB", + "Source": "Source", + "Stale data, check rig?": "Stale data, check rig?", + "Last received:": "Last received:", + "%1m ago": "%1m tagasi", + "%1h ago": "%1h tagasi", + "%1d ago": "%1p tagasi", + "RETRO": "RETRO", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Sensor change/restart overdue!", + "Time to change/restart sensor": "Aeg sensor vahetada/taaskäivitada", + "Change/restart sensor soon": "Vaheta/taaskäivita sensor varsti", + "Sensor age %1 days %2 hours": "Sensori vanus %1 päeva %2 tundi", + "Sensor Insert": "Sensori sisestamine", + "Sensor Start": "Sensori algus", + "days": "päeva", + "Insulin distribution": "Insulin distribution", + "To see this report, press SHOW while in this view": "To see this report, press SHOW while in this view", + "AR2 Forecast": "AR2 Forecast", + "OpenAPS Forecasts": "OpenAPS Forecasts", + "Temporary Target": "Temporary Target", + "Temporary Target Cancel": "Temporary Target Cancel", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profiilid", + "Time in fluctuation": "Time in fluctuation", + "Time in rapid fluctuation": "Time in rapid fluctuation", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:", + "Filter by hours": "Filter by hours", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">can be found here.", + "Mean Total Daily Change": "Mean Total Daily Change", + "Mean Hourly Change": "Mean Hourly Change", + "FortyFiveDown": "kerge langus", + "FortyFiveUp": "kerge tõus", + "Flat": "stabiilne", + "SingleUp": "tõus", + "SingleDown": "langus", + "DoubleDown": "kiire langus", + "DoubleUp": "kiire tõus", + "virtAsstUnknown": "That value is unknown at the moment. Please see your Nightscout site for more details.", + "virtAsstTitleAR2Forecast": "AR2 Forecast", + "virtAsstTitleCurrentBasal": "Current Basal", + "virtAsstTitleCurrentCOB": "Current COB", + "virtAsstTitleCurrentIOB": "Current IOB", + "virtAsstTitleLaunch": "Tere tulemast Nightscouti", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Forecast", + "virtAsstTitlePumpReservoir": "Insulin Remaining", + "virtAsstTitlePumpBattery": "Pumba aku", + "virtAsstTitleRawBG": "Current Raw BG", + "virtAsstTitleUploaderBattery": "Uploader Battery", + "virtAsstTitleCurrentBG": "Current BG", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "CGM-i režiim", + "virtAsstTitleCGMStatus": "CGM-i staatus", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM saatja staatus", + "virtAsstTitleCGMTxAge": "CGM saatja vanus", + "virtAsstTitleCGMNoise": "CGM Noise", + "virtAsstTitleDelta": "Blood Glucose Delta", + "virtAsstStatus": "%1 ja %2 seisuga %3.", + "virtAsstBasal": "%1 current basal is %2 units per hour", + "virtAsstBasalTemp": "%1 temp basal of %2 units per hour will end %3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "You have %1 insulin on board", + "virtAsstIobUnits": "%1 units of", + "virtAsstLaunch": "Mida soovid Nightscoutilt küsida?", + "virtAsstPreamble": "Sinu", + "virtAsstPreamble3person": "%1 on ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "Your pump battery is at %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "Vabandust, ma ei saanud sinust aru.", + "Fat [g]": "Rasvad [g]", + "Protein [g]": "Valgud [g]", + "Energy [kJ]": "Energia [kJ]", + "Clock Views:": "Clock Views:", + "Clock": "Clock", + "Color": "Color", + "Simple": "Simple", + "TDD average": "TDD average", + "Bolus average": "Bolus average", + "Basal average": "Basal average", + "Base basal average:": "Base basal average:", + "Carbs average": "Carbs average", + "Eating Soon": "Eating Soon", + "Last entry {0} minutes ago": "Last entry {0} minutes ago", + "change": "change", + "Speech": "Speech", + "Target Top": "Target Top", + "Target Bottom": "Target Bottom", + "Canceled": "Canceled", + "Meter BG": "Meter BG", + "predicted": "predicted", + "future": "future", + "ago": "ago", + "Last data received": "Last data received", + "Clock View": "Clock View", + "Protein": "Valgud", + "Fat": "Rasvad", + "Protein average": "Protein average", + "Fat average": "Fat average", + "Total carbs": "Total carbs", + "Total protein": "Total protein", + "Total fat": "Total fat", + "Database Size": "Andmebaasi maht", + "Database Size near its limits!": "Andmebaasi maht on piiri peal!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Andmebaasi maht on %1 MiB %2 MiB-st. Palun varunda ja puhasta andmebaas!", + "Database file size": "Andmebaasi faili suurus", + "%1 MiB of %2 MiB (%3%)": "%1 MiB / %2 MiB (%3%)", + "Data size": "Andmete maht", + "virtAsstDatabaseSize": "%1 MiB. See on %2% saadaolevast andmebaasi mahust.", + "virtAsstTitleDatabaseSize": "Andmebaasi faili suurus", + "Carbs/Food/Time": "Carbs/Food/Time", + "You have administration messages": "Sulle on administraatori märguandeid", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "Andmete lugemine lubatud", + "Data writes enabled": "Andmete kirjutamine lubatud", + "Data writes not enabled": "Andmete kirjutamine keelatud", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "Kontrolli uuendusi", + "Open Source": "Avatud lähtekood", + "Nightscout Info": "Nightscouti info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "Eelmine", + "Previous day": "Eelmine päev", + "Next day": "Järgmine päev", + "Next": "Järgmine", + "Temp basal delta": "Ajutise basaali muutus", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB parool ja API_SECRET on samad. See on väga halb. Palun muuda mõlemad ära ja ära taaskasuta paroole süsteemi erinevates kohtades." +} diff --git a/translations/fi_FI.json b/translations/fi_FI.json new file mode 100644 index 00000000000..bf2a37b4b90 --- /dev/null +++ b/translations/fi_FI.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Kuuntelen porttia", + "Mo": "Ma", + "Tu": "Ti", + "We": "Ke", + "Th": "To", + "Fr": "Pe", + "Sa": "La", + "Su": "Su", + "Monday": "Maanantai", + "Tuesday": "Tiistai", + "Wednesday": "Keskiviikko", + "Thursday": "Torstai", + "Friday": "Perjantai", + "Saturday": "Lauantai", + "Sunday": "Sunnuntai", + "Category": "Luokka", + "Subcategory": "Alaluokka", + "Name": "Nimi", + "Today": "Tänään", + "Last 2 days": "Edelliset 2 päivää", + "Last 3 days": "Edelliset 3 päivää", + "Last week": "Viime viikko", + "Last 2 weeks": "Viimeiset 2 viikkoa", + "Last month": "Viime kuu", + "Last 3 months": "Viimeiset 3 kuukautta", + "From": "Alkaen", + "To": "Asti", + "Notes": "Merkinnät", + "Food": "Ruoka", + "Insulin": "Insuliini", + "Carbs": "Hiilihydraatit", + "Notes contain": "Merkinnät sisältävät", + "Target BG range bottom": "Tavoitealueen alaraja", + "top": "yläraja", + "Show": "Näytä", + "Display": "Näyttö", + "Loading": "Lataan", + "Loading profile": "Lataan profiilia", + "Loading status": "Lataan tilaa", + "Loading food database": "Lataan ruokatietokantaa", + "not displayed": "ei näytetä", + "Loading CGM data of": "Lataan sensoritietoja", + "Loading treatments data of": "Lataan toimenpidetietoja", + "Processing data of": "Käsittelen tietoja: ", + "Portion": "Annos", + "Size": "Koko", + "(none)": "(tyhjä)", + "None": "Tyhjä", + "": "", + "Result is empty": "Ei tuloksia", + "Day to day": "Päivittäinen", + "Week to week": "Viikosta viikkoon", + "Daily Stats": "Päivittäiset tilastot", + "Percentile Chart": "Suhteellinen kuvaaja", + "Distribution": "Jakauma", + "Hourly stats": "Tunneittainen tilasto", + "netIOB stats": "netIOB tilasto", + "temp basals must be rendered to display this report": "tämä raportti vaatii, että basaalien piirto on päällä", + "No data available": "Tietoja ei saatavilla", + "Low": "Matala", + "In Range": "Tavoitealueella", + "Period": "Aikaväli", + "High": "Korkea", + "Average": "Keskiarvo", + "Low Quartile": "Alin neljäsosa", + "Upper Quartile": "Ylin neljäsosa", + "Quartile": "Neljäsosa", + "Date": "Päivämäärä", + "Normal": "Normaali", + "Median": "Mediaani", + "Readings": "Lukemia", + "StDev": "Keskijakauma", + "Daily stats report": "Päivittäinen tilasto", + "Glucose Percentile report": "Verensokeriarvojen jakauma", + "Glucose distribution": "Glukoosijakauma", + "days total": "päivän arvio", + "Total per day": "Päivän kokonaismäärä", + "Overall": "Yhteenveto", + "Range": "Alue", + "% of Readings": "% lukemista", + "# of Readings": "Lukemien määrä", + "Mean": "Keskiarvo", + "Standard Deviation": "Keskijakauma", + "Max": "Maks", + "Min": "Min", + "A1c estimation*": "A1c arvio*", + "Weekly Success": "Viikottainen tulos", + "There is not sufficient data to run this report. Select more days.": "Raporttia ei voida luoda liian vähäisen tiedon vuoksi. Valitse useampia päiviä.", + "Using stored API secret hash": "Tallennettu salainen API-tarkiste käytössä", + "No API secret hash stored yet. You need to enter API secret.": "Salainen API-tarkiste puuttuu. Syötä API tarkiste.", + "Database loaded": "Tietokanta ladattu", + "Error: Database failed to load": "Virhe: Tietokannan lataaminen epäonnistui", + "Error": "Virhe", + "Create new record": "Luo uusi tallenne", + "Save record": "Tallenna", + "Portions": "Annokset", + "Unit": "Yksikkö", + "GI": "GI", + "Edit record": "Muokkaa tallennetta", + "Delete record": "Tuhoa tallenne", + "Move to the top": "Siirrä ylimmäksi", + "Hidden": "Piilotettu", + "Hide after use": "Piilota käytön jälkeen", + "Your API secret must be at least 12 characters long": "API-avaimen tulee olla ainakin 12 merkin mittainen", + "Bad API secret": "Väärä API-avain", + "API secret hash stored": "API salaisuus talletettu", + "Status": "Tila", + "Not loaded": "Ei ladattu", + "Food Editor": "Muokkaa ruokia", + "Your database": "Tietokantasi", + "Filter": "Suodatin", + "Save": "Tallenna", + "Clear": "Tyhjennä", + "Record": "Tietue", + "Quick picks": "Nopeat valinnat", + "Show hidden": "Näytä piilotettu", + "Your API secret or token": "API salaisuus tai tunniste", + "Remember this device. (Do not enable this on public computers.)": "Muista tämä laite (Älä valitse julkisilla tietokoneilla)", + "Treatments": "Hoitotoimenpiteet", + "Time": "Aika", + "Event Type": "Tapahtumatyyppi", + "Blood Glucose": "Verensokeri", + "Entered By": "Tiedot syötti", + "Delete this treatment?": "Tuhoa tämä hoitotoimenpide?", + "Carbs Given": "Hiilihydraatit", + "Insulin Given": "Insuliiniannos", + "Event Time": "Aika", + "Please verify that the data entered is correct": "Varmista, että tiedot ovat oikein", + "BG": "VS", + "Use BG correction in calculation": "Käytä korjausannosta laskentaan", + "BG from CGM (autoupdated)": "VS sensorilta (päivitetty automaattisesti)", + "BG from meter": "VS mittarilta", + "Manual BG": "Käsin syötetty VS", + "Quickpick": "Pikavalinta", + "or": "tai", + "Add from database": "Lisää tietokannasta", + "Use carbs correction in calculation": "Käytä hiilihydraattikorjausta laskennassa", + "Use COB correction in calculation": "Käytä aktiivisia hiilihydraatteja laskennassa", + "Use IOB in calculation": "Käytä aktiviivista insuliinia laskennassa", + "Other correction": "Muu korjaus", + "Rounding": "Pyöristys", + "Enter insulin correction in treatment": "Syötä insuliinikorjaus", + "Insulin needed": "Insuliinitarve", + "Carbs needed": "Hiilihydraattitarve", + "Carbs needed if Insulin total is negative value": "Hiilihydraattitarve, jos yhteenlaskettu insuliini on negatiivinen", + "Basal rate": "Perusannos", + "60 minutes earlier": "60 minuuttia aiemmin", + "45 minutes earlier": "45 minuuttia aiemmin", + "30 minutes earlier": "30 minuuttia aiemmin", + "20 minutes earlier": "20 minuuttia aiemmin", + "15 minutes earlier": "15 minuuttia aiemmin", + "Time in minutes": "Aika minuuteissa", + "15 minutes later": "15 minuuttia myöhemmin", + "20 minutes later": "20 minuuttia myöhemmin", + "30 minutes later": "30 minuuttia myöhemmin", + "45 minutes later": "45 minuuttia myöhemmin", + "60 minutes later": "60 minuuttia myöhemmin", + "Additional Notes, Comments": "Lisähuomiot, kommentit", + "RETRO MODE": "VANHENTUNEET TIEDOT", + "Now": "Nyt", + "Other": "Muu", + "Submit Form": "Lähetä tiedot", + "Profile Editor": "Profiilin muokkaus", + "Reports": "Raportointityökalu", + "Add food from your database": "Lisää ruoka tietokannasta", + "Reload database": "Lataa tietokanta uudelleen", + "Add": "Lisää", + "Unauthorized": "Et ole autentikoitunut", + "Entering record failed": "Tiedon tallentaminen epäonnistui", + "Device authenticated": "Laite autentikoitu", + "Device not authenticated": "Laite ei ole autentikoitu", + "Authentication status": "Autentikoinnin tila", + "Authenticate": "Autentikoi", + "Remove": "Poista", + "Your device is not authenticated yet": "Laitettasi ei ole vielä autentikoitu", + "Sensor": "Sensori", + "Finger": "Sormi", + "Manual": "Manuaalinen", + "Scale": "Skaala", + "Linear": "Lineaarinen", + "Logarithmic": "Logaritminen", + "Logarithmic (Dynamic)": "Logaritminen (Dynaaminen)", + "Insulin-on-Board": "Aktiivinen Insuliini (IOB)", + "Carbs-on-Board": "Aktiivinen Hiilihydraatti (COB)", + "Bolus Wizard Preview": "Aterialaskurin Esikatselu (BWP)", + "Value Loaded": "Arvo ladattu", + "Cannula Age": "Kanyylin Ikä (CAGE)", + "Basal Profile": "Basaaliprofiili", + "Silence for 30 minutes": "Hiljennä 30 minuutiksi", + "Silence for 60 minutes": "Hiljennä tunniksi", + "Silence for 90 minutes": "Hiljennä 1.5 tunniksi", + "Silence for 120 minutes": "Hiljennä 2 tunniksi", + "Settings": "Asetukset", + "Units": "Yksikköä", + "Date format": "Aikamuoto", + "12 hours": "12 tuntia", + "24 hours": "24 tuntia", + "Log a Treatment": "Tallenna tapahtuma", + "BG Check": "Verensokerin tarkistus", + "Meal Bolus": "Ruokailubolus", + "Snack Bolus": "Ruokakorjaus", + "Correction Bolus": "Korjausbolus", + "Carb Correction": "Hiilihydraattikorjaus", + "Note": "Merkintä", + "Question": "Kysymys", + "Exercise": "Fyysinen harjoitus", + "Pump Site Change": "Kanyylin vaihto", + "CGM Sensor Start": "Sensorin aloitus", + "CGM Sensor Stop": "CGM Sensori Pysäytys", + "CGM Sensor Insert": "Sensorin vaihto", + "Sensor Code": "Sensorin koodi", + "Transmitter ID": "Lähettimen ID", + "Dexcom Sensor Start": "Sensorin aloitus", + "Dexcom Sensor Change": "Sensorin vaihto", + "Insulin Cartridge Change": "Insuliinisäiliön vaihto", + "D.A.D. Alert": "Diabeteskoirahälytys", + "Glucose Reading": "Verensokeri", + "Measurement Method": "Mittaustapa", + "Meter": "Sokerimittari", + "Amount in grams": "Määrä grammoissa", + "Amount in units": "Annos yksiköissä", + "View all treatments": "Katso kaikki hoitotoimenpiteet", + "Enable Alarms": "Aktivoi hälytykset", + "Pump Battery Change": "Pumpun patterin vaihto", + "Pump Battery Age": "Pumpun Patterin Ikä", + "Pump Battery Low Alarm": "Varoitus! Pumpun patteri loppumassa", + "Pump Battery change overdue!": "Pumpun patterin vaihto myöhässä!", + "When enabled an alarm may sound.": "Aktivointi mahdollistaa äänihälytykset", + "Urgent High Alarm": "Kriittinen korkea", + "High Alarm": "Korkea verensokeri", + "Low Alarm": "Matala verensokeri", + "Urgent Low Alarm": "Kriittinen matala", + "Stale Data: Warn": "Vanhat tiedot: varoitus", + "Stale Data: Urgent": "Vanhat tiedot: hälytys", + "mins": "minuuttia", + "Night Mode": "Yömoodi", + "When enabled the page will be dimmed from 10pm - 6am.": "Aktivoimalla sivu himmenee kello 22 ja 06 välillä", + "Enable": "Aktivoi", + "Show Raw BG Data": "Näytä raaka VS tieto", + "Never": "Ei koskaan", + "Always": "Aina", + "When there is noise": "Signaalihäiriöiden yhteydessä", + "When enabled small white dots will be displayed for raw BG data": "Aktivoituna raaka VS tieto piirtyy aikajanalle valkoisina pisteinä", + "Custom Title": "Omavalintainen otsikko", + "Theme": "Teema", + "Default": "Oletus", + "Colors": "Värit", + "Colorblind-friendly colors": "Värisokeille sopivat värit", + "Reset, and use defaults": "Palauta oletusasetukset", + "Calibrations": "Kalibraatiot", + "Alarm Test / Smartphone Enable": "Hälytyksien testaus / Älypuhelimien äänet päälle", + "Bolus Wizard": "Annosopas", + "in the future": "tulevaisuudessa", + "time ago": "aikaa sitten", + "hr ago": "tunti sitten", + "hrs ago": "tuntia sitten", + "min ago": "minuutti sitten", + "mins ago": "minuuttia sitten", + "day ago": "päivä sitten", + "days ago": "päivää sitten", + "long ago": "Pitkän aikaa sitten", + "Clean": "Puhdas", + "Light": "Kevyt", + "Medium": "Keskiverto", + "Heavy": "Raskas", + "Treatment type": "Hoidon tyyppi", + "Raw BG": "Raaka VS", + "Device": "Laite", + "Noise": "Kohina", + "Calibration": "Kalibraatio", + "Show Plugins": "Näytä pluginit", + "About": "Nightscoutista", + "Value in": "Arvo yksiköissä", + "Carb Time": "Syöntiaika", + "Language": "Kieli", + "Add new": "Lisää uusi", + "g": "g", + "ml": "ml", + "pcs": "kpl", + "Drag&drop food here": "Pudota ruoka tähän", + "Care Portal": "Hoidot", + "Medium/Unknown": "Keskiarvo/Ei tiedossa", + "IN THE FUTURE": "TULEVAISUUDESSA", + "Order": "Järjestys", + "oldest on top": "vanhin ylhäällä", + "newest on top": "uusin ylhäällä", + "All sensor events": "Kaikki sensorin tapahtumat", + "Remove future items from mongo database": "Poista tapahtumat mongo-tietokannasta", + "Find and remove treatments in the future": "Etsi ja poista tapahtumat joiden aikamerkintä on tulevaisuudessa", + "This task find and remove treatments in the future.": "Tämä työkalu poistaa tapahtumat joiden aikamerkintä on tulevaisuudessa.", + "Remove treatments in the future": "Poista tapahtumat", + "Find and remove entries in the future": "Etsi ja poista tapahtumat", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Tämä työkalu etsii ja poistaa sensorimerkinnät joiden aikamerkintä sijaitsee tulevaisuudessa.", + "Remove entries in the future": "Poista tapahtumat", + "Loading database ...": "Lataan tietokantaa...", + "Database contains %1 future records": "Tietokanta sisältää %1 merkintää tulevaisuudessa", + "Remove %1 selected records?": "Poista %1 valittua merkintää?", + "Error loading database": "Ongelma tietokannan lataamisessa", + "Record %1 removed ...": "Merkintä %1 poistettu ...", + "Error removing record %1": "Virhe poistaessa merkintää numero %1", + "Deleting records ...": "Poistan merkintöjä...", + "%1 records deleted": "%1 tietuetta poistettu", + "Clean Mongo status database": "Siivoa statustietokanta", + "Delete all documents from devicestatus collection": "Poista kaikki tiedot statustietokannasta", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Tämä työkalu poistaa kaikki tiedot statustietokannasta, mikä korjaa tilanteen, jossa puhelimen akun lataustilanne ei näy oikein.", + "Delete all documents": "Poista kaikki tiedot", + "Delete all documents from devicestatus collection?": "Poista tiedot statustietokannasta?", + "Database contains %1 records": "Tietokanta sisältää %1 merkintää", + "All records removed ...": "Kaikki merkinnät poistettu ...", + "Delete all documents from devicestatus collection older than 30 days": "Poista yli 30 päivää vanhat tietueet devicestatus kokoelmasta", + "Number of Days to Keep:": "Säilyttävien päivien lukumäärä:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Tämä työkalu poistaa kaikki yli 30 päivää vanhat tietueet devicestatus kokoelmasta.", + "Delete old documents from devicestatus collection?": "Poista vanhat tietueet devicestatus tietokannasta?", + "Clean Mongo entries (glucose entries) database": "Poista verensokeritiedot tietokannasta", + "Delete all documents from entries collection older than 180 days": "Poista yli 180 päivää vanhat verensokeritiedot", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Tämä työkalu poistaa yli 180 päivää vanhat verensokeritiedot.", + "Delete old documents": "Tuhoa vanhat dokumentit", + "Delete old documents from entries collection?": "Tuhoa vanhat verensokeritiedot?", + "%1 is not a valid number": "%1 ei ole numero", + "%1 is not a valid number - must be more than 2": "%1 ei ole kelvollinen numero - täytyy olla yli 2", + "Clean Mongo treatments database": "Poista hoitotiedot Mongo tietokannasta", + "Delete all documents from treatments collection older than 180 days": "Poista yli 180 päivää vanhat tiedot hoitotietokannasta", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Tämä työkalu poistaa kaikki yli 180 päivää vanhat hoitotietueet.", + "Delete old documents from treatments collection?": "Poista vanhat hoitotiedot?", + "Admin Tools": "Ylläpitotyökalut", + "Nightscout reporting": "Nightscout raportointi", + "Cancel": "Peruuta", + "Edit treatment": "Muuta merkintää", + "Duration": "Kesto", + "Duration in minutes": "Kesto minuuteissa", + "Temp Basal": "Tilapäinen basaali", + "Temp Basal Start": "Tilapäinen basaali alku", + "Temp Basal End": "Tilapäinen basaali loppu", + "Percent": "Prosentti", + "Basal change in %": "Basaalimuutos prosenteissa", + "Basal value": "Basaalin määrä", + "Absolute basal value": "Absoluuttinen basaalimäärä", + "Announcement": "Tiedote", + "Loading temp basal data": "Lataan tilapäisten basaalien tietoja", + "Save current record before changing to new?": "Tallenna nykyinen merkintä ennen vaihtoa uuteen?", + "Profile Switch": "Vaihda profiilia", + "Profile": "Profiili", + "General profile settings": "Yleiset profiiliasetukset", + "Title": "Otsikko", + "Database records": "Tietokantamerkintöjä", + "Add new record": "Lisää uusi merkintä", + "Remove this record": "Poista tämä merkintä", + "Clone this record to new": "Kopioi tämä merkintä uudeksi", + "Record valid from": "Merkintä voimassa alkaen", + "Stored profiles": "Tallennetut profiilit", + "Timezone": "Aikavyöhyke", + "Duration of Insulin Activity (DIA)": "Insuliinin vaikutusaika (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Kertoo insuliinin tyypillisen vaikutusajan. Vaihtelee potilaan ja insuliinin tyypin mukaan. Tyypillisesti 3-4 tuntia pumpuissa käytettävällä insuliinilla.", + "Insulin to carb ratio (I:C)": "Insuliiniannoksen hiilihydraattisuhde (I:HH)", + "Hours:": "Tunnit:", + "hours": "tuntia", + "g/hour": "g/tunti", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g hiilihydraattia / yksikkö insuliinia. Suhde, joka kertoo montako grammaa hiilihydraattia vastaa yhtä yksikköä insuliinia.", + "Insulin Sensitivity Factor (ISF)": "Insuliiniherkkyys (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL tai mmol/L / 1 yksikkö insuliinia. Suhde, joka kertoo montako yksikköä verensokeria yksi yksikkö insuliinia laskee.", + "Carbs activity / absorption rate": "Hiilihydraattiaktiivisuus / imeytymisnopeus", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grammaa / aika. Kertoo tyypillisen nopeuden, jolla hiilihydraatit imeytyvät syömisen jälkeen. Imeytyminen tunnetaan jokseenkin huonosti, mutta voidaan arvioida keskimääräisesti. Yksikkönä grammaa tunnissa (g/h).", + "Basal rates [unit/hour]": "Basaali [yksikköä/tunti]", + "Target BG range [mg/dL,mmol/L]": "Tavoitealue [mg/dL tai mmol/L]", + "Start of record validity": "Merkinnän alkupäivämäärä", + "Icicle": "Jääpuikko", + "Render Basal": "Näytä basaali", + "Profile used": "Käytetty profiili", + "Calculation is in target range.": "Laskettu arvo on tavoitealueella.", + "Loading profile records ...": "Ladataan profiileja ...", + "Values loaded.": "Arvot ladattu.", + "Default values used.": "Oletusarvot ladattu.", + "Error. Default values used.": "Virhe! Käytetään oletusarvoja.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Matalan ja korkean tavoitteen aikarajat eivät täsmää. Arvot on vaihdettu oletuksiin.", + "Valid from:": "Alkaen:", + "Save current record before switching to new?": "Tallenna nykyinen merkintä ennen vaihtamista uuteen?", + "Add new interval before": "Lisää uusi aikaväli ennen", + "Delete interval": "Poista aikaväli", + "I:C": "I:HH", + "ISF": "ISF", + "Combo Bolus": "Yhdistelmäbolus", + "Difference": "Ero", + "New time": "Uusi aika", + "Edit Mode": "Muokkausmoodi", + "When enabled icon to start edit mode is visible": "Muokkausmoodin ikoni tulee näkyviin kun laitat tämän päälle", + "Operation": "Operaatio", + "Move": "Liikuta", + "Delete": "Poista", + "Move insulin": "Liikuta insuliinia", + "Move carbs": "Liikuta hiilihydraatteja", + "Remove insulin": "Poista insuliini", + "Remove carbs": "Poista hiilihydraatit", + "Change treatment time to %1 ?": "Muuta hoidon aika? Uusi: %1", + "Change carbs time to %1 ?": "Muuta hiilihydraattien aika? Uusi: %1", + "Change insulin time to %1 ?": "Muuta insuliinin aika? Uusi: %1", + "Remove treatment ?": "Poista hoito?", + "Remove insulin from treatment ?": "Poista insuliini hoidosta?", + "Remove carbs from treatment ?": "Poista hiilihydraatit hoidosta?", + "Rendering": "Piirrän graafeja", + "Loading OpenAPS data of": "Lataan OpenAPS tietoja", + "Loading profile switch data": "Lataan profiilinvaihtotietoja", + "Redirecting you to the Profile Editor to create a new profile.": "Väärä profiiliasetus tai profiilia ei löydy.\nSiirrytään profiilin muokkaamiseen uuden profiilin luontia varten.", + "Pump": "Pumppu", + "Sensor Age": "Sensorin ikä", + "Insulin Age": "Insuliinin ikä", + "Temporary target": "Tilapäinen tavoite", + "Reason": "Syy", + "Eating soon": "Syödään pian", + "Top": "Ylä", + "Bottom": "Ala", + "Activity": "Aktiviteetti", + "Targets": "Tavoitteet", + "Bolus insulin:": "Bolusinsuliini:", + "Base basal insulin:": "Basaalin perusannos:", + "Positive temp basal insulin:": "Positiivinen tilapäisbasaali:", + "Negative temp basal insulin:": "Negatiivinen tilapäisbasaali:", + "Total basal insulin:": "Basaali yhteensä:", + "Total daily insulin:": "Päivän kokonaisinsuliiniannos:", + "Unable to save Role": "Roolin tallentaminen epäonnistui", + "Unable to delete Role": "Roolin poistaminen epäonnistui", + "Database contains %1 roles": "Tietokanta sisältää %1 roolia", + "Edit Role": "Muokkaa roolia", + "admin, school, family, etc": "ylläpitäjä, koulu, perhe jne", + "Permissions": "Oikeudet", + "Are you sure you want to delete: ": "Oletko varmat että haluat tuhota: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Jokaisella roolilla on yksi tai useampia oikeuksia. * on jokeri (tunnistuu kaikkina oikeuksina), oikeudet ovat hierarkia joka käyttää : merkkiä erottimena.", + "Add new Role": "Lisää uusi rooli", + "Roles - Groups of People, Devices, etc": "Roolit - Ihmisten ja laitteiden muodostamia ryhmiä", + "Edit this role": "Muokkaa tätä roolia", + "Admin authorized": "Ylläpitäjä valtuutettu", + "Subjects - People, Devices, etc": "Käyttäjät (Ihmiset, laitteet jne)", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Jokaisella käyttäjällä on uniikki pääsytunniste ja yksi tai useampi rooli. Klikkaa pääsytunnistetta nähdäksesi käyttäjän, jolloin saat jaettavan osoitteen tämän käyttäjän oikeuksilla.", + "Add new Subject": "Lisää uusi käyttäjä", + "Unable to save Subject": "Käyttäjän tallentaminen epäonnistui", + "Unable to delete Subject": "Käyttäjän poistaminen epäonnistui", + "Database contains %1 subjects": "Tietokanta sisältää %1 käyttäjää", + "Edit Subject": "Muokkaa käyttäjää", + "person, device, etc": "henkilö, laite jne", + "role1, role2": "rooli1, rooli2", + "Edit this subject": "Muokkaa tätä käyttäjää", + "Delete this subject": "Poista tämä käyttäjä", + "Roles": "Rooli", + "Access Token": "Pääsytunniste", + "hour ago": "tunti sitten", + "hours ago": "tuntia sitten", + "Silence for %1 minutes": "Hiljennä %1 minuutiksi", + "Check BG": "Tarkista VS", + "BASAL": "Basaali", + "Current basal": "Nykyinen basaali", + "Sensitivity": "Herkkyys", + "Current Carb Ratio": "Nykyinen hiilihydraattiherkkyys", + "Basal timezone": "Aikavyöhyke", + "Active profile": "Aktiivinen profiili", + "Active temp basal": "Aktiivinen tilapäisbasaali", + "Active temp basal start": "Aktiivisen tilapäisbasaalin aloitus", + "Active temp basal duration": "Aktiivisen tilapäisbasaalin kesto", + "Active temp basal remaining": "Aktiivista tilapäisbasaalia jäljellä", + "Basal profile value": "Basaaliprofiilin arvo", + "Active combo bolus": "Aktiivinen yhdistelmäbolus", + "Active combo bolus start": "Aktiivisen yhdistelmäboluksen alku", + "Active combo bolus duration": "Aktiivisen yhdistelmäboluksen kesto", + "Active combo bolus remaining": "Aktiivista yhdistelmäbolusta jäljellä", + "BG Delta": "VS muutos", + "Elapsed Time": "Kulunut aika", + "Absolute Delta": "Absoluuttinen muutos", + "Interpolated": "Laskettu", + "BWP": "Annoslaskuri", + "Urgent": "Kiireellinen", + "Warning": "Varoitus", + "Info": "Info", + "Lowest": "Matalin", + "Snoozing high alarm since there is enough IOB": "Korkean sokerin varoitus poistettu riittävän insuliinin vuoksi", + "Check BG, time to bolus?": "Tarkista VS, aika bolustaa?", + "Notice": "Huomio", + "required info missing": "tarvittava tieto puuttuu", + "Insulin on Board": "Aktiivinen insuliini", + "Current target": "Tämänhetkinen tavoite", + "Expected effect": "Oletettu vaikutus", + "Expected outcome": "Oletettu lopputulos", + "Carb Equivalent": "Hiilihydraattivastaavuus", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Liikaa insuliinia: %1U enemmän kuin tarvitaan tavoitteeseen pääsyyn (huomioimatta hiilihydraatteja)", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Liikaa insuliinia: %1U enemmän kuin tarvitaan tavoitteeseen pääsyyn, VARMISTA RIITTÄVÄ HIILIHYDRAATTIEN SAANTI", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Pääset tavoitteesen vähentämällä %1U aktiivista insuliinia, liikaa basaalia?", + "basal adjustment out of range, give carbs?": "säätö liian suuri, anna hiilihydraatteja?", + "basal adjustment out of range, give bolus?": "säätö liian suuri, anna bolus?", + "above high": "yli korkean", + "below low": "alle matalan", + "Projected BG %1 target": "Laskettu VS %1 tavoitteen", + "aiming at": "tavoitellaan", + "Bolus %1 units": "Bolusta %1 yksikköä", + "or adjust basal": "tai säädä basaalia", + "Check BG using glucometer before correcting!": "Tarkista VS mittarilla ennen korjaamista!", + "Basal reduction to account %1 units:": "Basaalin vähennys saadaksesi %1 yksikön vaikutuksen:", + "30m temp basal": "30m tilapäinen basaali", + "1h temp basal": "1h tilapäinen basaali", + "Cannula change overdue!": "Kanyylin ikä yli määräajan!", + "Time to change cannula": "Aika vaihtaa kanyyli", + "Change cannula soon": "Vaihda kanyyli pian", + "Cannula age %1 hours": "Kanyylin ikä %1 tuntia", + "Inserted": "Asetettu", + "CAGE": "KIKÄ", + "COB": "AH", + "Last Carbs": "Viimeisimmät hiilihydraatit", + "IAGE": "IIKÄ", + "Insulin reservoir change overdue!": "Insuliinisäiliö vanhempi kuin määräaika!", + "Time to change insulin reservoir": "Aika vaihtaa insuliinisäiliö", + "Change insulin reservoir soon": "Vaihda insuliinisäiliö pian", + "Insulin reservoir age %1 hours": "Insuliinisäiliön ikä %1 tuntia", + "Changed": "Vaihdettu", + "IOB": "IOB", + "Careportal IOB": "Careportal IOB", + "Last Bolus": "Viimeisin bolus", + "Basal IOB": "Basaalin IOB", + "Source": "Lähde", + "Stale data, check rig?": "Tiedot vanhoja, tarkista lähetin?", + "Last received:": "Viimeksi vastaanotettu:", + "%1m ago": "%1m sitten", + "%1h ago": "%1h sitten", + "%1d ago": "%1d sitten", + "RETRO": "RETRO", + "SAGE": "SIKÄ", + "Sensor change/restart overdue!": "Sensorin vaihto/uudelleenkäynnistys yli määräajan!", + "Time to change/restart sensor": "Aika vaihtaa / käynnistää sensori uudelleen", + "Change/restart sensor soon": "Vaihda/käynnistä sensori uudelleen pian", + "Sensor age %1 days %2 hours": "Sensorin ikä %1 päivää, %2 tuntia", + "Sensor Insert": "Sensorin Vaihto", + "Sensor Start": "Sensorin Aloitus", + "days": "päivää", + "Insulin distribution": "Insuliinijakauma", + "To see this report, press SHOW while in this view": "Nähdäksesi tämän raportin, paina NÄYTÄ tässä näkymässä", + "AR2 Forecast": "AR2 Ennusteet", + "OpenAPS Forecasts": "OpenAPS Ennusteet", + "Temporary Target": "Tilapäinen tavoite", + "Temporary Target Cancel": "Peruuta tilapäinen tavoite", + "OpenAPS Offline": "OpenAPS poissa verkosta", + "Profiles": "Profiilit", + "Time in fluctuation": "Aika muutoksessa", + "Time in rapid fluctuation": "Aika nopeassa muutoksessa", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Tämä on epätarkka arvio joka saattaa heittää huomattavasti mittaustuloksesta, eikä korvaa laboratoriotestiä. Laskentakaava on otettu artikkelista: ", + "Filter by hours": "Huomioi raportissa seuraavat tunnit", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Aika Muutoksessa ja Aika Nopeassa Muutoksessa mittaa osuutta tarkkailtavasta aikaperiodista, jolloin glukoosi on ollut nopeassa tai hyvin nopeassa muutoksessa. Pienempi arvo on parempi.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Keskimääräinen Kokonaismuutos kertoo kerkimääräisen päivätason verensokerimuutoksien yhteenlasketun arvon. Pienempi arvo on parempi.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Keskimääräinen tunti kertoo keskimääräisen tuntitason verensokerimuutoksien yhteenlasketun arvon. Pienempi arvo on parempi.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Alueen ulkopuolella oleva RMS lasketaan siten, että kaikkien tavoitealueen ulkopuolisten glukoosilukemien etäisyys alueesta lasketaan toiseen potenssiin, luvut summataan, jaetaan niiden määrällä ja otetaan neliöjuuri. Tämä tieto on samankaltainen kuin lukemien osuus tavoitealueella, mutta painottaa lukemia kauempana alueen ulkopuolella. Matalampi arvo on parempi.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">here.", + "Mean Total Daily Change": "Keskimääräinen Kokonaismuutos", + "Mean Hourly Change": "Keskimääräinen tuntimuutos", + "FortyFiveDown": "laskee hitaasti", + "FortyFiveUp": "nousee hitaasti", + "Flat": "tasainen", + "SingleUp": "nousussa", + "SingleDown": "laskussa", + "DoubleDown": "laskee nopeasti", + "DoubleUp": "nousee nopeasti", + "virtAsstUnknown": "Tämä arvo on tällä hetkellä tuntematon. Katso lisätietoja Nightscout-sivustostasi.", + "virtAsstTitleAR2Forecast": "AR2 Ennuste", + "virtAsstTitleCurrentBasal": "Nykyinen Basaali", + "virtAsstTitleCurrentCOB": "Tämänhetkinen COB", + "virtAsstTitleCurrentIOB": "Tämänhetkinen IOB", + "virtAsstTitleLaunch": "Tervetuloa Nightscoutiin", + "virtAsstTitleLoopForecast": "Loop ennuste", + "virtAsstTitleLastLoop": "Viimeisin Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Ennuste", + "virtAsstTitlePumpReservoir": "Insuliinia jäljellä", + "virtAsstTitlePumpBattery": "Pumpun paristo", + "virtAsstTitleRawBG": "Nykyinen Raw BG", + "virtAsstTitleUploaderBattery": "Lähettimen akku", + "virtAsstTitleCurrentBG": "Nykyinen VS", + "virtAsstTitleFullStatus": "Täysi status", + "virtAsstTitleCGMMode": "CGM tila", + "virtAsstTitleCGMStatus": "CGM tila", + "virtAsstTitleCGMSessionAge": "CGM Istunnon Ikä", + "virtAsstTitleCGMTxStatus": "CGM Lähettimen Tila", + "virtAsstTitleCGMTxAge": "CGM Lähettimen Ikä", + "virtAsstTitleCGMNoise": "CGM Häiriöt", + "virtAsstTitleDelta": "Verensokerin muutos", + "virtAsstStatus": "%1 ja %2 alkaen %3.", + "virtAsstBasal": "%1 nykyinen basaali on %2 yksikköä tunnissa", + "virtAsstBasalTemp": "%1 tilapäinen basaali on %2 tunnissa, päättyy %3", + "virtAsstIob": "ja sinulla on %1 aktivista insuliinia.", + "virtAsstIobIntent": "Sinulla on %1 aktiivista insuliinia", + "virtAsstIobUnits": "%1 yksikköä", + "virtAsstLaunch": "Mitä haluat tarkistaa Nightscoutissa?", + "virtAsstPreamble": "Sinun", + "virtAsstPreamble3person": "%1 on ", + "virtAsstNoInsulin": "ei", + "virtAsstUploadBattery": "Lähettimen paristoa jäljellä %1", + "virtAsstReservoir": "%1 yksikköä insuliinia jäljellä", + "virtAsstPumpBattery": "Pumppu on %1 %2", + "virtAsstUploaderBattery": "Lähettimen pariston lataustaso on %1", + "virtAsstLastLoop": "Viimeisin onnistunut loop oli %1", + "virtAsstLoopNotAvailable": "Loop plugin ei ole aktivoitu", + "virtAsstLoopForecastAround": "Ennusteen mukaan olet around %1 seuraavan %2 ajan", + "virtAsstLoopForecastBetween": "Ennusteen mukaan olet between %1 and %2 seuraavan %3 ajan", + "virtAsstAR2ForecastAround": "AR2 ennusteen mukaan olet %1 seuraavan %2 aikana", + "virtAsstAR2ForecastBetween": "AR2 ennusteen mukaan olet %1 ja %2 välissä seuraavan %3 aikana", + "virtAsstForecastUnavailable": "Ennusteet eivät ole toiminnassa puuttuvan tiedon vuoksi", + "virtAsstRawBG": "Suodattamaton verensokeriarvo on %1", + "virtAsstOpenAPSForecast": "OpenAPS verensokeriarvio on %1", + "virtAsstCob3person": "%1 on %2 aktiivista hiilihydraattia", + "virtAsstCob": "Sinulla on %1 aktiivista hiilihydraattia", + "virtAsstCGMMode": "CGM moodi oli %1 %2 alkaen.", + "virtAsstCGMStatus": "CGM tila oli %1 %2 alkaen.", + "virtAsstCGMSessAge": "CGM istunto on ollut aktiivisena %1 päivää ja %2 tuntia.", + "virtAsstCGMSessNotStarted": "Tällä hetkellä ei ole aktiivista CGM-istuntoa.", + "virtAsstCGMTxStatus": "CGM lähettimen tila oli %1 hetkellä %2.", + "virtAsstCGMTxAge": "CGM lähetin on %1 päivää vanha.", + "virtAsstCGMNoise": "CGM kohina oli %1 hetkellä %2.", + "virtAsstCGMBattOne": "CGM akku oli %1 volttia hetkellä %2.", + "virtAsstCGMBattTwo": "CGM akun tasot olivat %1 volttia ja %2 volttia hetkellä %3.", + "virtAsstDelta": "Muutoksesi oli %1 %2 ja %3 välillä.", + "virtAsstDeltaEstimated": "Arvioitu muutos oli %1 välillä %2 ja %3.", + "virtAsstUnknownIntentTitle": "Tuntematon intentio", + "virtAsstUnknownIntentText": "Anteeksi, en tiedä mitä pyydät.", + "Fat [g]": "Rasva [g]", + "Protein [g]": "Proteiini [g]", + "Energy [kJ]": "Energia [kJ]", + "Clock Views:": "Kellonäkymä:", + "Clock": "Kello", + "Color": "Väri", + "Simple": "Yksinkertainen", + "TDD average": "Päivän kokonaisinsuliinin keskiarvo", + "Bolus average": "Boluksien keskiarvo", + "Basal average": "Basaalien keskiarvo", + "Base basal average:": "Perusbasaalin keskiarvo:", + "Carbs average": "Hiilihydraatit keskiarvo", + "Eating Soon": "Ruokailu pian", + "Last entry {0} minutes ago": "Edellinen verensokeri {0} minuuttia sitten", + "change": "muutos", + "Speech": "Puhe", + "Target Top": "Tavoite ylä", + "Target Bottom": "Tavoite ala", + "Canceled": "Peruutettu", + "Meter BG": "Mittarin VS", + "predicted": "ennuste", + "future": "tulevaisuudessa", + "ago": "sitten", + "Last data received": "Tietoa vastaanotettu viimeksi", + "Clock View": "Kellonäkymä", + "Protein": "Proteiini", + "Fat": "Rasva", + "Protein average": "Proteiini keskiarvo", + "Fat average": "Rasva keskiarvo", + "Total carbs": "Hiilihydraatit yhteensä", + "Total protein": "Proteiini yhteensä", + "Total fat": "Rasva yhteensä", + "Database Size": "Tietokannan koko", + "Database Size near its limits!": "Tietokannan koko lähellä rajaa!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Tietokannan koko on %1, raja on %2. Muista varmistaa tietokanta!", + "Database file size": "Tietokannan tiedoston koko", + "%1 MiB of %2 MiB (%3%)": "%1 MiB / %2 MiB (%3%)", + "Data size": "Tietojen koko", + "virtAsstDatabaseSize": "%1 MiB. Se on %2% saatavilla olevasta koosta.", + "virtAsstTitleDatabaseSize": "Tietokantatiedoston koko", + "Carbs/Food/Time": "Hiilihydraatit/Ruoka/Aika", + "You have administration messages": "Sinulle on ylläpitoviestejä", + "Admin messages in queue": "Jonossa olevat ylläpitoviestit", + "Queue empty": "Jono on tyhjä", + "There are no admin messages in queue": "Jonossa ei ole ylläpitoviestejä", + "Please sign in using the API_SECRET to see your administration messages": "Kirjaudu sisään käyttäen API_SECRET salasanaa nähdäksesi ylläpitoviestit", + "Reads enabled in default permissions": "Tietojen lukeminen käytössä oletuskäyttöoikeuksilla", + "Data reads enabled": "Tiedon lukeminen mahdollista", + "Data writes enabled": "Tiedon kirjoittaminen mahdollista", + "Data writes not enabled": "Tiedon kirjoittaminen estetty", + "Color prediction lines": "Värilliset ennusteviivat", + "Release Notes": "Nightscout versioiden kuvaukset", + "Check for Updates": "Tarkista päivityksen saatavuus", + "Open Source": "Avoin lähdekoodi", + "Nightscout Info": "Tietoa Nightscoutista", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Loopalyzerin tarkoitus on visualisoida miten Loop-järjestelmä on toiminut. Visualisointi toimii jossain määrin myös muita järjestelmiä käyttävillä, mutta graafista saattaa puuttua tietoja ja se saattaa näyttää oudolta jos et käytä Loopia. Tarkista huolellisesti, että Loopalyzerin raportti näyttää toimivan.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer sisältää aikasiirto-ominaisuuden. Jos syöt esimerkiksi aamupalan yhtenä päivänä seitsemältä ja seuraavana kahdeksalta, näyttää päivien keskiarvoglukoosi harhaanjohtavaa käyrää. Aikasiirto huomaa eri ajan aamupalalle, ja siirtää automaattisesti päivien tiedot niin, että käyrät piirtävät aamupalan samalle hetkelle, jolloin päivien vertaaminen on helpompaa.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "Yllämainitussa esimerkissä ensimmäisen päivän tietoja siirrettäisiin 30 minuuttia eteenpäin ja toisen päivän tietoja 30 taaksepäin ajassa, ikään kuin molemmat aamupalat olisi syöty kello 7:30. Näin näet miten verensokeri on muuttunut aamupalojen jälkeen.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Aikasiirto näyttää insuliinin vaikutusajan verran tietoa aterioiden jälkeen. Koska kaikki tiedot koko päivän ajalla ovat siirtyneet, käyrä harmaan alueen ulkopuolella ei välttämättä ole tarkka.", + "Note that time shift is available only when viewing multiple days.": "Huomioi että aikasiirto toimii vain kun haet monen päivän tiedot samaan aikaan.", + "Please select a maximum of two weeks duration and click Show again.": "Valitse enintään kahden viikon tiedot kerrallaan, ja paina Näytä -nappia uudellen.", + "Show profiles table": "Näytä profiilitaulukko", + "Show predictions": "Näytä ennusteet", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Tee aikasiirto yli %1 hh aterioille jotka on syöty välillä %2 ja %3", + "Previous": "Edellinen", + "Previous day": "Edellinen päivä", + "Next day": "Seuraava päivä", + "Next": "Seuraava", + "Temp basal delta": "Tilapäisen basaalin muutos", + "Authorized by token": "Kirjautuminen tunnisteella", + "Auth role": "Authentikaatiorooli", + "view without token": "näytä ilman tunnistetta", + "Remove stored token": "Poista talletettu tunniste", + "Weekly Distribution": "Viikkojakauma", + "Failed authentication": "Sisäänkirjautuminen epäonnistui", + "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?": "Laita IP-osoitteessa %1 yritti kirjautua Nightscoutiin väärällä tunnisteella. Tarkista onko uploaderissasi oikea API_SECRET tai avain?", + "Default (with leading zero and U)": "Oletus (U ja 0 näytetään)", + "Concise (with U, without leading zero)": "Lyhyt (U näytetään, 0 ei)", + "Minimal (without leading zero and U)": "Minimi (U ja 0 piilossa)", + "Small Bolus Display": "Pienien boluksien näyttömuoto", + "Large Bolus Display": "Suurien boluksien näyttömuoto", + "Bolus Display Threshold": "Boluksen asetusraja", + "%1 U and Over": "%1 U ja yli", + "Event repeated %1 times.": "Tapahtuma toistunut %1 kertaa.", + "minutes": "minuuttia", + "Last recorded %1 %2 ago.": "Tapahtui viimeksi %1 %2 sitten.", + "Security issue": "Tietoturvaongelma", + "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.": "Heikko API_SECRET havaittu. Ole hyvä ja käytä pieniä ja SUURIA kirjaimia, numeroita ja ei-aakkosnumeerisia merkkejä, kuten !#%&/ vähentääksesi luvattoman pääsyn riskiä. API_SECRET vähimmäispituus on 12 merkkiä.", + "less than 1": "alle 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB salasana ja API_SECRET on asetettu samaksi. Tämä on tietoturvan kannalta erittäin huono idea. Vaihda molemmat ja älä uudelleenkäytä salasanoja järjestelmien välillä." +} diff --git a/translations/fr_FR.json b/translations/fr_FR.json new file mode 100644 index 00000000000..7448619d6f7 --- /dev/null +++ b/translations/fr_FR.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Écoute sur port", + "Mo": "Lu", + "Tu": "Ma", + "We": "Me", + "Th": "Je", + "Fr": "Ve", + "Sa": "Sa", + "Su": "Di", + "Monday": "Lundi", + "Tuesday": "Mardi", + "Wednesday": "Mercredi", + "Thursday": "Jeudi", + "Friday": "Vendredi", + "Saturday": "Samedi", + "Sunday": "Dimanche", + "Category": "Catégorie", + "Subcategory": "Sous-catégorie", + "Name": "Nom", + "Today": "Aujourd'hui", + "Last 2 days": "2 derniers jours", + "Last 3 days": "3 derniers jours", + "Last week": "Semaine dernière", + "Last 2 weeks": "2 dernières semaines", + "Last month": "Mois dernier", + "Last 3 months": "3 derniers mois", + "From": "Du", + "To": "Au", + "Notes": "Remarques", + "Food": "Nourriture", + "Insulin": "Insuline", + "Carbs": "Glucides", + "Notes contain": "Les notes contiennent", + "Target BG range bottom": "Limite inférieure glycémie", + "top": "supérieure", + "Show": "Montrer", + "Display": "Afficher", + "Loading": "Chargement", + "Loading profile": "Chargement du profil", + "Loading status": "Statut du chargement", + "Loading food database": "Chargement de la base de données alimentaire", + "not displayed": "non affiché", + "Loading CGM data of": "Chargement données CGM de", + "Loading treatments data of": "Chargement données traitement de", + "Processing data of": "Traitement des données de", + "Portion": "Portion", + "Size": "Taille", + "(none)": "(aucun)", + "None": "Aucun", + "": "", + "Result is empty": "Pas de résultat", + "Day to day": "Jour par jour", + "Week to week": "Semaine après semaine", + "Daily Stats": "Statistiques quotidiennes", + "Percentile Chart": "Percentiles", + "Distribution": "Distribution", + "Hourly stats": "Statistiques horaires", + "netIOB stats": "statistiques netiob", + "temp basals must be rendered to display this report": "la basale temporaire doit être activé pour afficher ce rapport", + "No data available": "Pas de données disponibles", + "Low": "Bas", + "In Range": "Dans la cible", + "Period": "Période", + "High": "Haut", + "Average": "Moyenne", + "Low Quartile": "Quartile inférieur", + "Upper Quartile": "Quartile supérieur", + "Quartile": "Quartile", + "Date": "Date", + "Normal": "Normal", + "Median": "Médiane", + "Readings": "Valeurs", + "StDev": "Déviation St.", + "Daily stats report": "Rapport quotidien", + "Glucose Percentile report": "Rapport percentiles Glycémie", + "Glucose distribution": "Distribution glycémies", + "days total": "jours totaux", + "Total per day": "Total journalier", + "Overall": "En général", + "Range": "Intervalle", + "% of Readings": "% de lectures", + "# of Readings": "Nombre de lectures", + "Mean": "Moyenne", + "Standard Deviation": "Déviation Standard", + "Max": "Maximum", + "Min": "Minimum", + "A1c estimation*": "Estimation HbA1c*", + "Weekly Success": "Réussite hebdomadaire", + "There is not sufficient data to run this report. Select more days.": "Il n'y a pas assez de données pour exécuter ce rapport. Sélectionnez plus de jours.", + "Using stored API secret hash": "Utilisation du hash API existant", + "No API secret hash stored yet. You need to enter API secret.": "Pas de secret API existant. Vous devez en entrer un.", + "Database loaded": "Base de données chargée", + "Error: Database failed to load": "Erreur: le chargement de la base de données a échoué", + "Error": "Erreur", + "Create new record": "Créer un nouvel enregistrement", + "Save record": "Sauvegarder l'enregistrement", + "Portions": "Portions", + "Unit": "Unités", + "GI": "IG", + "Edit record": "Modifier enregistrement", + "Delete record": "Effacer enregistrement", + "Move to the top": "Déplacer en haut", + "Hidden": "Caché", + "Hide after use": "Cacher après utilisation", + "Your API secret must be at least 12 characters long": "Votre secret API doit contenir au moins 12 caractères", + "Bad API secret": "Secret API erroné", + "API secret hash stored": "Hash API secret sauvegardé", + "Status": "Statut", + "Not loaded": "Non chargé", + "Food Editor": "Editeur aliments", + "Your database": "Votre base de données", + "Filter": "Filtre", + "Save": "Sauvegarder", + "Clear": "Effacer", + "Record": "Enregistrement", + "Quick picks": "Sélection rapide", + "Show hidden": "Montrer cachés", + "Your API secret or token": "Votre mot de passe API secret ou jeton", + "Remember this device. (Do not enable this on public computers.)": "Se souvenir de cet appareil. (Ne pas activer sur les ordinateurs publics.)", + "Treatments": "Traitements", + "Time": "Heure", + "Event Type": "Type d'événement", + "Blood Glucose": "Glycémie", + "Entered By": "Entré par", + "Delete this treatment?": "Effacer ce traitement?", + "Carbs Given": "Glucides donnés", + "Insulin Given": "Insuline donnée", + "Event Time": "Heure de l'événement", + "Please verify that the data entered is correct": "Veuillez vérifier que les données saisies sont correctes", + "BG": "Glycémie", + "Use BG correction in calculation": "Utiliser la correction de glycémie dans les calculs", + "BG from CGM (autoupdated)": "Glycémie à partir du CGM (automatique)", + "BG from meter": "Glycémie du glucomètre", + "Manual BG": "Glycémie manuelle", + "Quickpick": "Sélection rapide", + "or": "ou", + "Add from database": "Ajouter à partir de la base de données", + "Use carbs correction in calculation": "Utiliser la correction en glucides dans les calculs", + "Use COB correction in calculation": "Utiliser les glucides actifs dans les calculs", + "Use IOB in calculation": "Utiliser l'insuline active dans les calculs", + "Other correction": "Autre correction", + "Rounding": "Arrondi", + "Enter insulin correction in treatment": "Entrer correction insuline dans le traitement", + "Insulin needed": "Insuline nécessaire", + "Carbs needed": "Glucides nécessaires", + "Carbs needed if Insulin total is negative value": "Glucides nécessaires si insuline totale est un valeur négative", + "Basal rate": "Taux basal", + "60 minutes earlier": "60 min plus tôt", + "45 minutes earlier": "45 min plus tôt", + "30 minutes earlier": "30 min plus tôt", + "20 minutes earlier": "20 min plus tôt", + "15 minutes earlier": "15 min plus tôt", + "Time in minutes": "Durée en minutes", + "15 minutes later": "15 min plus tard", + "20 minutes later": "20 min plus tard", + "30 minutes later": "30 min plus tard", + "45 minutes later": "45 min plus tard", + "60 minutes later": "60 min plus tard", + "Additional Notes, Comments": "Notes additionnelles, commentaires", + "RETRO MODE": "MODE RETROSPECTIF", + "Now": "Maintenant", + "Other": "Autre", + "Submit Form": "Soumettre le formulaire", + "Profile Editor": "Éditeur de profil", + "Reports": "Outil de rapport", + "Add food from your database": "Ajouter aliment de votre base de données", + "Reload database": "Recharger la base de données", + "Add": "Ajouter", + "Unauthorized": "Non autorisé", + "Entering record failed": "Entrée enregistrement a échoué", + "Device authenticated": "Appareil authentifié", + "Device not authenticated": "Appareil non authentifié", + "Authentication status": "Status de l'authentification", + "Authenticate": "Authentifier", + "Remove": "Retirer", + "Your device is not authenticated yet": "Votre appareil n'est pas encore authentifié", + "Sensor": "Senseur", + "Finger": "Doigt", + "Manual": "Manuel", + "Scale": "Échelle", + "Linear": "Linéaire", + "Logarithmic": "Logarithmique", + "Logarithmic (Dynamic)": "Logarithmique (Dynamique)", + "Insulin-on-Board": "Insuline active", + "Carbs-on-Board": "Glucides actifs", + "Bolus Wizard Preview": "Aperçu de l'assistant de bolus", + "Value Loaded": "Valeur chargée", + "Cannula Age": "Age de la canule", + "Basal Profile": "Profil Basal", + "Silence for 30 minutes": "Silence pendant 30 minutes", + "Silence for 60 minutes": "Silence pendant 60 minutes", + "Silence for 90 minutes": "Silence pendant 90 minutes", + "Silence for 120 minutes": "Silence pendant 120 minutes", + "Settings": "Paramètres", + "Units": "Unités", + "Date format": "Format de la date", + "12 hours": "12 heures", + "24 hours": "24 heures", + "Log a Treatment": "Entrer un traitement", + "BG Check": "Contrôle glycémie", + "Meal Bolus": "Bolus repas", + "Snack Bolus": "Bolus en cas", + "Correction Bolus": "Bolus de correction", + "Carb Correction": "Ressucrage", + "Note": "Note", + "Question": "Question", + "Exercise": "Exercice physique", + "Pump Site Change": "Changement de zone d'insertion", + "CGM Sensor Start": "Démarrage senseur", + "CGM Sensor Stop": "GSC Sensor Arrêt", + "CGM Sensor Insert": "Changement senseur", + "Sensor Code": "Code du capteur", + "Transmitter ID": "ID du transmetteur", + "Dexcom Sensor Start": "Démarrage senseur Dexcom", + "Dexcom Sensor Change": "Changement senseur Dexcom", + "Insulin Cartridge Change": "Changement cartouche d'insuline", + "D.A.D. Alert": "Wouf! Wouf! Chien d'alerte diabète", + "Glucose Reading": "Valeur de glycémie", + "Measurement Method": "Méthode de mesure", + "Meter": "Glucomètre", + "Amount in grams": "Quantité en grammes", + "Amount in units": "Quantité en unités", + "View all treatments": "Voir tous les traitements", + "Enable Alarms": "Activer les alarmes", + "Pump Battery Change": "Changer les batteries de la pompe", + "Pump Battery Age": "Âge de la batterie de la pompe", + "Pump Battery Low Alarm": "Alarme batterie de la pompe faible", + "Pump Battery change overdue!": "Changement de la batterie de la pompe nécessaire !", + "When enabled an alarm may sound.": "Si activée, un alarme peut sonner.", + "Urgent High Alarm": "Alarme hyperglycémie urgente", + "High Alarm": "Alarme hyperglycémie", + "Low Alarm": "Alarme hypoglycémie", + "Urgent Low Alarm": "Alarme hypoglycémie urgente", + "Stale Data: Warn": "Données échues: avertissement", + "Stale Data: Urgent": "Données échues: avertissement urgent", + "mins": "minutes", + "Night Mode": "Mode nocturne", + "When enabled the page will be dimmed from 10pm - 6am.": "Si activé, la page sera assombrie de 22:00 à 6:00", + "Enable": "Activer", + "Show Raw BG Data": "Montrer les données BG brutes", + "Never": "Jamais", + "Always": "Toujours", + "When there is noise": "Quand il y a du bruit", + "When enabled small white dots will be displayed for raw BG data": "Si activé, des points blancs représenteront les données brutes", + "Custom Title": "Titre personalisé", + "Theme": "Thème", + "Default": "Par défaut", + "Colors": "Couleurs", + "Colorblind-friendly colors": "Couleurs pour daltoniens", + "Reset, and use defaults": "Remettre à zéro et utiliser les valeurs par défaut", + "Calibrations": "Calibration", + "Alarm Test / Smartphone Enable": "Test alarme / Activer Smartphone", + "Bolus Wizard": "Calculateur de bolus", + "in the future": "dans le futur", + "time ago": "temps avant", + "hr ago": "heure avant", + "hrs ago": "heures avant", + "min ago": "minute avant", + "mins ago": "minutes avant", + "day ago": "jour avant", + "days ago": "jours avant", + "long ago": "il y a longtemps", + "Clean": "Propre", + "Light": "Léger", + "Medium": "Moyen", + "Heavy": "Important", + "Treatment type": "Type de traitement", + "Raw BG": "Glycémie brute", + "Device": "Appareil", + "Noise": "Bruit", + "Calibration": "Calibrage", + "Show Plugins": "Montrer Plugins", + "About": "À propos de", + "Value in": "Valeur en", + "Carb Time": "Moment de l'ingestion de Glucides", + "Language": "Langue", + "Add new": "Ajouter nouveau", + "g": "g", + "ml": "ml", + "pcs": "unités", + "Drag&drop food here": "Glisser et déposer repas ici ", + "Care Portal": "Portail de soins", + "Medium/Unknown": "Moyen/Inconnu", + "IN THE FUTURE": "DANS LE FUTUR", + "Order": "Mise en ordre", + "oldest on top": "plus vieux en tête de liste", + "newest on top": "nouveau en tête de liste", + "All sensor events": "Tous les événement du senseur", + "Remove future items from mongo database": "Effacer les éléments futurs de la base de données mongo", + "Find and remove treatments in the future": "Chercher et effacer les traitements dont la date est dans le futur", + "This task find and remove treatments in the future.": "Cette tâche cherche et efface les traitements dont la date est dans le futur.", + "Remove treatments in the future": "Efface les traitements ayant une date dans le futur", + "Find and remove entries in the future": "Cherche et efface les événements dans le futur", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Cet tâche cherche et efface les valeurs CGM dont la date est dans le futur, créées par le téléphone avec une mauvaise date / heure.", + "Remove entries in the future": "Efface les événement dans le futur", + "Loading database ...": "Charge la base de données...", + "Database contains %1 future records": "La base de données contient %1 valeurs futures", + "Remove %1 selected records?": "Effacer %1 valeurs choisies?", + "Error loading database": "Erreur chargement de la base de données", + "Record %1 removed ...": "Enregistrement %1 effacé ...", + "Error removing record %1": "Échec d'effacement de l'enregistrement %1", + "Deleting records ...": "Effacement d'enregistrements...", + "%1 records deleted": "%1 enregistrements effacés", + "Clean Mongo status database": "Nettoyage de la base de données Mongo", + "Delete all documents from devicestatus collection": "Effacer tous les documents de la collection devicestatus", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Efface tous les documents de la collection devicestatus. Utile lorsque l'indicateur de chargement de la batterie du Smartphone n'est pas affichée correctement.", + "Delete all documents": "Effacer toutes les données", + "Delete all documents from devicestatus collection?": "Effacer toutes les données de la collection devicestatus ?", + "Database contains %1 records": "La base de données contient %1 enregistrements", + "All records removed ...": "Tous les enregistrements ont été effacés ...", + "Delete all documents from devicestatus collection older than 30 days": "Effacer tout les documents de l'appareil plus vieux que 30 jours", + "Number of Days to Keep:": "Nombre de jours à conserver:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Cette tâche efface tout les documents au sujet de l'appareil qui sont âgés de plus de 30 jours. Soyez sûr que votre batterie est à pleine charge sinon votre synchronisation ne sera pas mise a jour.", + "Delete old documents from devicestatus collection?": "Effacer les vieux résultats de votre appareil?", + "Clean Mongo entries (glucose entries) database": "Nettoyer la base de données des entrées Mongo (entrées de glycémie)", + "Delete all documents from entries collection older than 180 days": "Effacer les résultats de plus de 180 jours", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Cette action efface tous les documents de la collection qui sont vieux de plus de 180 jours. Utile lorsque l'indicateur de chargement de la batterie n'est pas affichée correctement.", + "Delete old documents": "Effacer les anciens documents", + "Delete old documents from entries collection?": "Voulez vous effacer les plus anciens résultats?", + "%1 is not a valid number": "%1 n'est pas un nombre valide", + "%1 is not a valid number - must be more than 2": "%1 n'est pas un nombre valide - doit être supérieur à 2", + "Clean Mongo treatments database": "Nettoyage de la base de données des traitements Mongo", + "Delete all documents from treatments collection older than 180 days": "Effacer les données des traitements de plus de 180 jours", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Cette action efface tous les documents de la collection des traitements qui sont vieux de plus de 180 jours. Utile lorsque l'indicateur de chargement de la batterie n'est pas affiché correctement.", + "Delete old documents from treatments collection?": "Voulez vous effacer les plus vieux résultats?", + "Admin Tools": "Outils d'administration", + "Nightscout reporting": "Rapports Nightscout", + "Cancel": "Interrompre", + "Edit treatment": "Modifier un traitement", + "Duration": "Durée", + "Duration in minutes": "Durée en minutes", + "Temp Basal": "Débit basal temporaire", + "Temp Basal Start": "Début du débit basal temporaire", + "Temp Basal End": "Fin du débit basal temporaire", + "Percent": "Pourcent", + "Basal change in %": "Changement du débit basal en %", + "Basal value": "Valeur du débit basal", + "Absolute basal value": "Débit basal absolu", + "Announcement": "Annonce", + "Loading temp basal data": "Chargement des données de débit basal", + "Save current record before changing to new?": "Sauvegarder l'événement actuel avant d'avancer au suivant ?", + "Profile Switch": "Changement de profil", + "Profile": "Profil", + "General profile settings": "Réglages principaux du profil", + "Title": "Titre", + "Database records": "Entrées de la base de données", + "Add new record": "Ajouter une nouvelle entrée", + "Remove this record": "Supprimer cette entrée", + "Clone this record to new": "Dupliquer cette entrée", + "Record valid from": "Entrée valide à partir de", + "Stored profiles": "Profils sauvegardés", + "Timezone": "Zone horaire", + "Duration of Insulin Activity (DIA)": "Durée d'action de l'insuline (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Représente la durée d'action typique de l'insuline. Varie individuellement et selon le type d'insuline. Typiquement 3-4 heures pour la plupart des insulines utilisées dans les pompes et la plupart des patients. Appelé également durée de vie de l'insuline.", + "Insulin to carb ratio (I:C)": "Ratio Insuline-Glucides (I:C)", + "Hours:": "Heures:", + "hours": "heures", + "g/hour": "g/heure", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g de glucides par Unité d'insuline. Ce rapport représente la quantité de glucides compensée par une unité d'insuline.", + "Insulin Sensitivity Factor (ISF)": "Facteur de sensibilité à l'insuline (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL ou mmol/l par unité d'insuline. Il s'agit du ratio représentant la modification de la glycémie résultant de l'administration d'une unité d'insuline corrective.", + "Carbs activity / absorption rate": "Activité glucidique / vitesse d'absorption", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grammes par unité de temps. Représente le changement de COB (glucides actifs) par unité de temps et la quantité de glucides actifs durant cette période. L'absorption des glucides est imprécise et est évaluée en moyenne. L'unité est grammes par heure (g/h).", + "Basal rates [unit/hour]": "Débit basal [unités/ heure]", + "Target BG range [mg/dL,mmol/L]": "Cible d'intervalle de glycémie", + "Start of record validity": "Début de validité des données", + "Icicle": "Mode stalactite", + "Render Basal": "Afficher le débit basal", + "Profile used": "Profil utilisé", + "Calculation is in target range.": "La valeur calculée est dans l'intervalle cible", + "Loading profile records ...": "Chargement des profils...", + "Values loaded.": "Valeurs chargées.", + "Default values used.": "Valeurs par défaut utilisées.", + "Error. Default values used.": "Erreur! Valeurs par défaut utilisées.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Les intervalles de temps pour la cible glycémique supérieure et inférieure diffèrent. Les valeurs par défaut sont restaurées.", + "Valid from:": "Valide à partir de:", + "Save current record before switching to new?": "Sauvegarder cette entrée avant de procéder à l'entrée suivante?", + "Add new interval before": "Ajouter un intervalle de temps avant", + "Delete interval": "Effacer l'intervalle", + "I:C": "I:C", + "ISF": "ISF (Sensibilité à l'insuline)", + "Combo Bolus": "Bolus Duo/Combo", + "Difference": "Différence", + "New time": "Nouveau temps", + "Edit Mode": "Mode Édition", + "When enabled icon to start edit mode is visible": "Lorsque cette option est activée, l'icône du Mode Édition devient visible", + "Operation": "Opération", + "Move": "Déplacer", + "Delete": "Effacer", + "Move insulin": "Déplacer l'insuline", + "Move carbs": "Déplacer les glucides", + "Remove insulin": "Effacer l'insuline", + "Remove carbs": "Effacer les glucides", + "Change treatment time to %1 ?": "Modifier l'horaire du traitement à %1 ?", + "Change carbs time to %1 ?": "Modifier l'horaire des glucides à %1 ?", + "Change insulin time to %1 ?": "Modifier l'horaire de l'insuline à %1 ?", + "Remove treatment ?": "Effacer le traitement ?", + "Remove insulin from treatment ?": "Effacer l'insuline du traitement?", + "Remove carbs from treatment ?": "Effacer les glucides du traitement?", + "Rendering": "Représentation graphique", + "Loading OpenAPS data of": "Chargement des données OpenAPS de", + "Loading profile switch data": "Chargement de données de changement de profil", + "Redirecting you to the Profile Editor to create a new profile.": "Redirection vers l'éditeur de profil pour la création d'un nouveau profil.", + "Pump": "Pompe", + "Sensor Age": "Âge du senseur (SAGE)", + "Insulin Age": "Âge de l'insuline (IAGE)", + "Temporary target": "Cible temporaire", + "Reason": "Raison", + "Eating soon": "Repas sous peu", + "Top": "Haut", + "Bottom": "Bas", + "Activity": "Activité", + "Targets": "Cibles", + "Bolus insulin:": "Bolus d'Insuline:", + "Base basal insulin:": "Débit basal de base:", + "Positive temp basal insulin:": "Débit basal temporaire positif:", + "Negative temp basal insulin:": "Débit basal temporaire négatif:", + "Total basal insulin:": "Total insuline basale:", + "Total daily insulin:": "Insuline totale journalière:", + "Unable to save Role": "Impossible d'enregistrer le role", + "Unable to delete Role": "Effacement de rôle impossible", + "Database contains %1 roles": "La base de données contient %1 rôles", + "Edit Role": "Éditer le rôle", + "admin, school, family, etc": "administrateur, école, famille, etc", + "Permissions": "Autorisations", + "Are you sure you want to delete: ": "Êtes-vous sûr de vouloir effacer: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Chaque rôle aura une ou plusieurs permissions. La permission * est un joker (permission universelle), les permissions sont une hiérarchie utilisant : comme séparateur.", + "Add new Role": "Ajouter un nouveau rôle", + "Roles - Groups of People, Devices, etc": "Rôles - Groupe de Personnes ou d'appareils", + "Edit this role": "Editer ce rôle", + "Admin authorized": "Administrateur autorisé", + "Subjects - People, Devices, etc": "Utilisateurs - Personnes, Appareils, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Chaque utilisateur aura un identificateur unique et un ou plusieurs rôles. Cliquez sur l'identificateur pour ouvrir une nouvelle vue avec l'utilisateur sélectionné, ce lien secret peut être partagé.", + "Add new Subject": "Ajouter un nouvel Utilisateur", + "Unable to save Subject": "Impossible d'enregistrer l'Utilisateur", + "Unable to delete Subject": "Impossible d'effacer l'Utilisateur", + "Database contains %1 subjects": "La base de données contient %1 utilisateurs", + "Edit Subject": "Éditer l'Utilisateur", + "person, device, etc": "personne, appareil, etc", + "role1, role2": "rôle1, rôle2", + "Edit this subject": "Éditer cet utilisateur", + "Delete this subject": "Effacer cet utilisateur:", + "Roles": "Rôles", + "Access Token": "Identificateur unique", + "hour ago": "heure avant", + "hours ago": "heures avant", + "Silence for %1 minutes": "Silence pour %1 minutes", + "Check BG": "Vérifier la glycémie", + "BASAL": "Basale", + "Current basal": "Débit basal actuel", + "Sensitivity": "Sensibilité à l'insuline (ISF)", + "Current Carb Ratio": "Rapport Insuline-glucides actuel (I:C)", + "Basal timezone": "Fuseau horaire", + "Active profile": "Profil actif", + "Active temp basal": "Débit basal temporaire actif", + "Active temp basal start": "Début du débit basal temporaire", + "Active temp basal duration": "Durée du débit basal temporaire", + "Active temp basal remaining": "Durée restante de débit basal temporaire", + "Basal profile value": "Valeur du débit basal", + "Active combo bolus": "Bolus Duo/Combo actif", + "Active combo bolus start": "Début de Bolus Duo/Combo", + "Active combo bolus duration": "Durée du Bolus Duo/Combo", + "Active combo bolus remaining": "Activité restante du Bolus Duo/Combo", + "BG Delta": "Différence de glycémie", + "Elapsed Time": "Temps écoulé", + "Absolute Delta": "Différence absolue", + "Interpolated": "Interpolé", + "BWP": "Calculateur de bolus (BWP)", + "Urgent": "Urgent", + "Warning": "Attention", + "Info": "Information", + "Lowest": "Valeur la plus basse", + "Snoozing high alarm since there is enough IOB": "Alarme haute ignorée car suffisamment d'insuline à bord (IOB)", + "Check BG, time to bolus?": "Vérifier la glycémie, bolus nécessaire ?", + "Notice": "Notification", + "required info missing": "Information nécessaire manquante", + "Insulin on Board": "Insuline à bord (IOB)", + "Current target": "Cible actuelle", + "Expected effect": "Effect escompté", + "Expected outcome": "Résultat escompté", + "Carb Equivalent": "Equivalent glucidique", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Insuline en excès: %1U de plus que nécessaire pour atteindre la cible inférieure, sans prendre en compte les glucides", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Insuline en excès: %1U de plus que nécessaire pour atteindre la cible inférieure, ASSUREZ UN APPORT SUFFISANT DE GLUCIDES", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U réduction d'insuline active nécessaire pour atteindre la cible inférieure. Débit basal trop élevé ?", + "basal adjustment out of range, give carbs?": "ajustement de débit basal hors des limites, prenez des glucides?", + "basal adjustment out of range, give bolus?": "ajustement de débit basal hors des limites, injectez un bolus?", + "above high": "plus haut que la limite supérieure", + "below low": "plus bas que la limite inférieure", + "Projected BG %1 target": "Glycémie cible projetée %1", + "aiming at": "visant", + "Bolus %1 units": "Bolus %1 unités", + "or adjust basal": "ou ajuster le débit basal", + "Check BG using glucometer before correcting!": "Vérifier la glycémie avec un glucomètre avant de corriger!", + "Basal reduction to account %1 units:": "Réduction du débit basal pour obtenir l'effet d' %1 unité", + "30m temp basal": "débit basal temporaire de 30 min", + "1h temp basal": "débit basal temporaire de 1 heure", + "Cannula change overdue!": "Dépassement de date de changement de canule!", + "Time to change cannula": "Le moment est venu de changer de canule", + "Change cannula soon": "Changement de canule bientôt", + "Cannula age %1 hours": "Age de la canule %1 heures", + "Inserted": "Insérée", + "CAGE": "Age de la canule (ie dernier changement de cathéter)", + "COB": "COB glucides actifs", + "Last Carbs": "Derniers glucides", + "IAGE": "Age de l'insuline", + "Insulin reservoir change overdue!": "Changement de réservoir d'insuline en retard!", + "Time to change insulin reservoir": "Le moment est venu de changer de réservoir d'insuline", + "Change insulin reservoir soon": "Changement de réservoir d'insuline bientôt", + "Insulin reservoir age %1 hours": "Âge du réservoir d'insuline %1 heures", + "Changed": "Changé", + "IOB": "IOB Insuline Active", + "Careportal IOB": "Insuline active du careportal", + "Last Bolus": "Dernier Bolus", + "Basal IOB": "IOB du débit basal", + "Source": "Source", + "Stale data, check rig?": "Valeurs trop anciennes, vérifier l'uploadeur", + "Last received:": "Dernière réception:", + "%1m ago": "il y a %1 min", + "%1h ago": "%1 heures plus tôt", + "%1d ago": "%1 jours plus tôt", + "RETRO": "RETRO", + "SAGE": "Age du sensor", + "Sensor change/restart overdue!": "Changement/Redémarrage du senseur dépassé!", + "Time to change/restart sensor": "C'est le moment de changer/redémarrer le senseur", + "Change/restart sensor soon": "Changement/Redémarrage du senseur bientôt", + "Sensor age %1 days %2 hours": "Âge su senseur %1 jours et %2 heures", + "Sensor Insert": "Insertion du senseur", + "Sensor Start": "Démarrage du senseur", + "days": "jours", + "Insulin distribution": "Distribution de l'insuline", + "To see this report, press SHOW while in this view": "Pour voir le rapport, cliquer sur MONTRER dans cette fenêtre", + "AR2 Forecast": "Prédiction AR2", + "OpenAPS Forecasts": "Prédictions OpenAPS", + "Temporary Target": "Cible temporaire", + "Temporary Target Cancel": "Annuler la cible temporaire", + "OpenAPS Offline": "OpenAPS déconnecté", + "Profiles": "Profils", + "Time in fluctuation": "Temps passé en fluctuation", + "Time in rapid fluctuation": "Temps passé en fluctuation rapide", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Ceci est seulement une estimation approximative qui peut être très imprécise et ne remplace pas une mesure sanguine. La formule utilisée provient de:", + "Filter by hours": "Filtrer par heures", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Le temps passé en fluctuation et le temps passé en fluctuation rapide mesurent la part de temps durant la période examinée, pendant laquelle la glycémie a évolué relativement ou très rapidement. Les valeurs basses sont les meilleures.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "La Variation Totale Journalière Moyenne est la somme de toute les excursions glycémiques absolues pour une période analysée, divisée par le nombre de jours. Les valeurs basses sont les meilleures.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "La Variation Horaire Moyenne est la somme de toute les excursions glycémiques absolues pour une période analysée, divisée par le nombre d'heures dans la période. Les valeures basses sont les meilleures.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "RMS hors limites de RMS est calculé en mettant au carré la distance en dehors des limites pour toutes les lectures de glucose pour la période examinée, les additionnant, en divisant par le nombre et en prenant la racine carrée. Cette métrique est similaire au pourcentage dans l'intervalle mais les lectures de poids sont loin en dehors de l'intervalle. Les valeurs inférieures sont meilleures.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">ici.", + "Mean Total Daily Change": "Variation Totale Journalière Moyenne", + "Mean Hourly Change": "Variation Horaire Moyenne", + "FortyFiveDown": "en chute lente", + "FortyFiveUp": "en montée lente", + "Flat": "stable", + "SingleUp": "en montée", + "SingleDown": "en chute", + "DoubleDown": "en chute rapide", + "DoubleUp": "en montée rapide", + "virtAsstUnknown": "Cette valeur est inconnue pour le moment. Veuillez consulter votre site Nightscout pour plus de détails.", + "virtAsstTitleAR2Forecast": "Prévision AR2", + "virtAsstTitleCurrentBasal": "Débit basal actuel", + "virtAsstTitleCurrentCOB": "Glucides actuels", + "virtAsstTitleCurrentIOB": "Insuline actuelle", + "virtAsstTitleLaunch": "Bienvenue dans Nightscout", + "virtAsstTitleLoopForecast": "Prévision de Loop", + "virtAsstTitleLastLoop": "Dernière boucle", + "virtAsstTitleOpenAPSForecast": "Prédictions OpenAPS", + "virtAsstTitlePumpReservoir": "Insuline restante", + "virtAsstTitlePumpBattery": "Batterie Pompe", + "virtAsstTitleRawBG": "Glycémie actuelle", + "virtAsstTitleUploaderBattery": "Batterie du téléphone", + "virtAsstTitleCurrentBG": "Glycémie actuelle", + "virtAsstTitleFullStatus": "Statut complet", + "virtAsstTitleCGMMode": "Mode CGM", + "virtAsstTitleCGMStatus": "Statut CGM", + "virtAsstTitleCGMSessionAge": "L’âge de la session du CGM", + "virtAsstTitleCGMTxStatus": "Statut du transmetteur CGM", + "virtAsstTitleCGMTxAge": "Age du transmetteur", + "virtAsstTitleCGMNoise": "Bruit CGM", + "virtAsstTitleDelta": "Delta de glycémie", + "virtAsstStatus": "%1 et %2 à partir de %3.", + "virtAsstBasal": "%1 le Basal actuel est %2 unités par heure", + "virtAsstBasalTemp": "%1 basal temporaire de %2 unités par heure se terminera %3", + "virtAsstIob": "et vous avez %1 insuline active.", + "virtAsstIobIntent": "Vous avez %1 insuline active", + "virtAsstIobUnits": "%1 unités de", + "virtAsstLaunch": "Qu'est ce que vous voulez voir sur Nightscout?", + "virtAsstPreamble": "Vous", + "virtAsstPreamble3person": "%1 a un ", + "virtAsstNoInsulin": "non", + "virtAsstUploadBattery": "Votre appareil est chargé a %1", + "virtAsstReservoir": "Vous avez %1 unités non utilisé", + "virtAsstPumpBattery": "La batterie de votre pompe est à %1 %2", + "virtAsstUploaderBattery": "La batterie de votre téléphone est à %1", + "virtAsstLastLoop": "La dernière boucle effetive a été %1", + "virtAsstLoopNotAvailable": "Le plugin Loop ne semble pas être activé", + "virtAsstLoopForecastAround": "Selon les prévisions de Loop, vous devriez être aux alentours de %1 dans les prochaines %2", + "virtAsstLoopForecastBetween": "Selon les prévisions de Loop, vous devriez être entre %1 et %2 dans les prochaines %3", + "virtAsstAR2ForecastAround": "Selon les prévisions de l'AR2, vous devriez être aux alentours de %1 dans les prochaines %2", + "virtAsstAR2ForecastBetween": "Selon les prévisions de l'AR2, vous devriez être entre %1 et %2 dans les prochaines %3", + "virtAsstForecastUnavailable": "Impossible de prévoir avec les données disponibles", + "virtAsstRawBG": "Votre glycémie brut est %1", + "virtAsstOpenAPSForecast": "La glycémie éventuelle OpenAPS est %1", + "virtAsstCob3person": "%1 a %2 glucides actifs", + "virtAsstCob": "Vous avez %1 glucides actifs", + "virtAsstCGMMode": "Le mode du CGM était %1 à partir de %2.", + "virtAsstCGMStatus": "Le statut de votre CGM était de %1 sur %2.", + "virtAsstCGMSessAge": "Votre session CGM est active depuis %1 jours et %2 heures.", + "virtAsstCGMSessNotStarted": "Il n'y a pas de session CGM active pour le moment.", + "virtAsstCGMTxStatus": "Le statut de votre transmetteur CGM était %1 de %2.", + "virtAsstCGMTxAge": "Votre transmetteur CGM a %1 jours.", + "virtAsstCGMNoise": "Le bruit de votre CGM était de %1 sur %2.", + "virtAsstCGMBattOne": "Votre batterie du CGM était de %1 volts sur %2.", + "virtAsstCGMBattTwo": "Les batteries de votre CGM étaient de %1 volts et de %2 volts sur %3.", + "virtAsstDelta": "Votre delta estimé est %1 entre %2 et %3.", + "virtAsstDeltaEstimated": "Votre delta estimé est %1 entre %2 et %3.", + "virtAsstUnknownIntentTitle": "Intentions inconnues", + "virtAsstUnknownIntentText": "« Je suis désolée, je ne sais pas ce que vous demandez. ».", + "Fat [g]": "Graisses [g]", + "Protein [g]": "Protéines [g]", + "Energy [kJ]": "Énergie [kJ]", + "Clock Views:": "Vue Horloge:", + "Clock": "L'horloge", + "Color": "Couleur", + "Simple": "Simple", + "TDD average": "Moyenne TDD (dose journalière totale)", + "Bolus average": "Bolus moyen", + "Basal average": "Basale moyenne", + "Base basal average:": "Débit de base moyen:", + "Carbs average": "Glucides moyens", + "Eating Soon": "Repas à venir bientôt", + "Last entry {0} minutes ago": "Dernière entrée : il y a {0} minutes", + "change": "modifier", + "Speech": "Voix", + "Target Top": "Cible haute", + "Target Bottom": "Cible basse", + "Canceled": "Annulé", + "Meter BG": "Glucomètre", + "predicted": "prédiction", + "future": "futur", + "ago": "plus tôt", + "Last data received": "Dernières données reçues", + "Clock View": "Vue Horloge", + "Protein": "Protéines", + "Fat": "Matières grasses", + "Protein average": "Moyenne des protéines", + "Fat average": "Moyenne des matières grasses", + "Total carbs": "Glucides totaux", + "Total protein": "Protéines totales", + "Total fat": "Matières grasses totales", + "Database Size": "Taille de la base de données", + "Database Size near its limits!": "Taille de la base de données proche de ses limites !", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "La taille de la base de données est de %1 MiB sur %2 Mio. Veuillez sauvegarder et nettoyer la base de données !", + "Database file size": "Taille de la base de données", + "%1 MiB of %2 MiB (%3%)": "%1 MiB sur %2 Mio (%3%)", + "Data size": "Taille des données", + "virtAsstDatabaseSize": "%1 MiB. Cela représente %2% de l'espace disponible dans la base de données.", + "virtAsstTitleDatabaseSize": "Taille du fichier de la base de données", + "Carbs/Food/Time": "Glucides/Alimentation/Temps", + "You have administration messages": "Vous avez des messages d'administration", + "Admin messages in queue": "Messages de l'administrateur dans la file d'attente", + "Queue empty": "File d'attente vide", + "There are no admin messages in queue": "Il n'y a pas de messages d'administration dans la file d'attente", + "Please sign in using the API_SECRET to see your administration messages": "Veuillez vous connecter à l'aide de l'API_SECRET pour voir vos messages d'administration", + "Reads enabled in default permissions": "Lectures activées dans les autorisations par défaut", + "Data reads enabled": "Lectures des données activées", + "Data writes enabled": "Écriture de données activée", + "Data writes not enabled": "Les écritures de données ne sont pas activées", + "Color prediction lines": "Lignes de prédiction de couleurs", + "Release Notes": "Notes de version", + "Check for Updates": "Rechercher des mises à jour", + "Open Source": "Open source. Libre de droit", + "Nightscout Info": "Informations Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Le but principal de Loopalyzer est de visualiser comment le système de boucle fermée Loop fonctionne. Il peut aussi fonctionner avec d'autres configurations, à la fois en boucle fermée et ouverte, et sans boucle. Cependant, en fonction du système que vous utilisez, de la fréquence à laquelle il est capable de capter vos données et de les télécharger, et comment il est en mesure de compléter des données manquantes, certains graphiques peuvent avoir des lacunes ou même être complètement vides. Assurez vous toujours que les graphiques ont l'air raisonnables. Le mieux est de voir un jour à la fois et de faire défiler plusieurs jours d'abord pour voir.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer comprend une fonction de décalage de temps. Si vous prenez le petit déjeuner par exemple à 07h00 un jour et à 08h00 le jour suivant, la courbe de votre glycémie moyenne de ces deux jours sera probablement aplatie et ne montrera pas la tendance réelle après le petit déjeuner. Le décalage de temps calculera le temps moyen entre la consommation de ces repas, puis déplacera toutes les données (glucides, insuline, basal, etc.) pendant les deux jours à la différence de temps correspondante de sorte que les deux repas s'alignent sur une heure moyenne de début du repas.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "Dans cet exemple, toutes les données du premier jour sont avancées de 30 minutes dans le temps et toutes les données du deuxième jour sont repoussées de 30 minutes dans le temps, de sorte qu'il apparaît comme si vous aviez pris le petit déjeuner à 07h30 les deux jours. Cela vous permet de voir votre réponse glycémique moyenne réelle à partir d'un repas.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Le décalage de temps met en évidence en gris la période après le temps moyen de démarrage des repas, pour la durée de la DIA (Durée de l'action de l'insuline). Comme tous les points de données de la journée entière sont déplacés, les courbes en dehors de la zone grise peuvent ne pas être précises.", + "Note that time shift is available only when viewing multiple days.": "Notez que le décalage de temps n'est disponible que lors de l'affichage de plusieurs jours.", + "Please select a maximum of two weeks duration and click Show again.": "Veuillez sélectionner une durée maximale de deux semaines et cliquez à nouveau sur Afficher.", + "Show profiles table": "Afficher la table des profils", + "Show predictions": "Afficher les prédictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Décalage horaire pour les repas contenant plus de %1 g de glucides consommés entre %2 et %3", + "Previous": "Précédent", + "Previous day": "Jour précédent", + "Next day": "Jour suivant", + "Next": "Suivant", + "Temp basal delta": "Delta de basale temporaire", + "Authorized by token": "Autorisé par le jeton", + "Auth role": "Rôle d'authentification", + "view without token": "afficher sans jeton", + "Remove stored token": "Supprimer le jeton stocké", + "Weekly Distribution": "Distribution hebdomadaire", + "Failed authentication": "Échec de l'authentification", + "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?": "Un appareil à l'adresse IP %1 a tenté de s'authentifier avec Nightscout avec des informations d'identification erronées. Vérifiez si vous avez une configuration avec une mauvaise API_SECRET ou un mauvais jeton ?", + "Default (with leading zero and U)": "Standard (avec U et zéro non significatif)", + "Concise (with U, without leading zero)": "Concis (avec U, sans zéro non significatif)", + "Minimal (without leading zero and U)": "Minimal (sans zéro et U)", + "Small Bolus Display": "Petit affichage de Bolus", + "Large Bolus Display": "Grand affichage de Bolus", + "Bolus Display Threshold": "Seuil d'affichage du bolus", + "%1 U and Over": "%1 U et plus", + "Event repeated %1 times.": "Événement répété %1 fois.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Dernier enregistrement il y a %1 %2.", + "Security issue": "Problème de sécurité", + "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.": "API_SECRET faible détectée. Veuillez utiliser un mélange de lettres minuscules et MAJUSCULES, de chiffres et de caractères non alphanumériques tels que! #% & / Pour réduire le risque d'accès non autorisé. La longueur minimale de l'API_SECRET est de 12 caractères.", + "less than 1": "moins que 1 minute", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Le mot de passe de MongoDB et de l'API_SECRET sont identiques. C'est une très mauvaise idée. Veuillez changer les deux et ne pas réutiliser ces mots de passe dans ce système." +} diff --git a/translations/he_IL.json b/translations/he_IL.json new file mode 100644 index 00000000000..9a630f46fcb --- /dev/null +++ b/translations/he_IL.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "מקשיב לפורט", + "Mo": "ב\\'", + "Tu": "ג\\'", + "We": "ד\\'", + "Th": "ה\\'", + "Fr": "ו\\'", + "Sa": "ש\\'", + "Su": "א\\'", + "Monday": "שני", + "Tuesday": "שלישי", + "Wednesday": "רביעי", + "Thursday": "חמישי", + "Friday": "שישי", + "Saturday": "שבת", + "Sunday": "ראשון", + "Category": "קטגוריה", + "Subcategory": "תת-קטגוריה", + "Name": "שם", + "Today": "היום", + "Last 2 days": "יומיים אחרונים", + "Last 3 days": "שלושה ימים אחרונים", + "Last week": "שבוע אחרון", + "Last 2 weeks": "שבועיים אחרונים", + "Last month": "חודש אחרון", + "Last 3 months": "שלושה חודשים אחרונים", + "From": "מ-", + "To": "עד", + "Notes": "הערות", + "Food": "אוכל", + "Insulin": "אינסולין", + "Carbs": "פחמימות", + "Notes contain": "ההערות מכילות", + "Target BG range bottom": "טווח מטרה סף תחתון", + "top": "למעלה", + "Show": "הצג", + "Display": "תצוגה", + "Loading": "טוען", + "Loading profile": "טוען פרופיל", + "Loading status": "טוען סטטוס", + "Loading food database": "טוען נתוני אוכל", + "not displayed": "לא מוצג", + "Loading CGM data of": "טוען נתוני חיישן סוכר של", + "Loading treatments data of": "טוען נתוני טיפולים של", + "Processing data of": "מעבד נתונים של", + "Portion": "מנה", + "Size": "גודל", + "(none)": "(ללא)", + "None": "ללא", + "": "", + "Result is empty": "אין תוצאה", + "Day to day": "יום-יום", + "Week to week": "שבוע לשבוע", + "Daily Stats": "סטטיסטיקה יומית", + "Percentile Chart": "טבלת עשירונים", + "Distribution": "התפלגות", + "Hourly stats": "סטטיסטיקה שעתית", + "netIOB stats": "netIOB סטטיסטיקת", + "temp basals must be rendered to display this report": "חובה לאפשר רמה בזלית זמנית כדי לרות דוח זה", + "No data available": "אין מידע זמין", + "Low": "נמוך", + "In Range": "בטווח", + "Period": "תקופה", + "High": "גבוה", + "Average": "ממוצע", + "Low Quartile": "רבעון תחתון", + "Upper Quartile": "רבעון עליון", + "Quartile": "רבעון", + "Date": "תאריך", + "Normal": "נורמלי", + "Median": "חציון", + "Readings": "קריאות", + "StDev": "סטיית תקן", + "Daily stats report": "דוח סטטיסטיקה יומית", + "Glucose Percentile report": "דוח אחוזון סוכר", + "Glucose distribution": "התפלגות סוכר", + "days total": "מספר ימים", + "Total per day": "מספר ימים", + "Overall": "סה\"כ", + "Range": "טווח", + "% of Readings": "אחוז קריאות", + "# of Readings": "מספר קריאות", + "Mean": "ממוצע", + "Standard Deviation": "סטיית תקן", + "Max": "מקס'", + "Min": "מינ'", + "A1c estimation*": "משוער A1c", + "Weekly Success": "הצלחה שבועית", + "There is not sufficient data to run this report. Select more days.": "לא נמצא מספיק מידע ליצירת הדוח. בחרו ימים נוספים.", + "Using stored API secret hash": "משתמש בסיסמת ממשק תכנות יישומים הסודית ", + "No API secret hash stored yet. You need to enter API secret.": "הכנס את הסיסמא הסודית של ה API", + "Database loaded": "בסיס נתונים נטען", + "Error: Database failed to load": "שגיאה: לא ניתן לטעון בסיס נתונים", + "Error": "שגיאה", + "Create new record": "צור רשומה חדשה", + "Save record": "שמור רשומה", + "Portions": "מנות", + "Unit": "יחידות", + "GI": "אינדקס גליקמי", + "Edit record": "ערוך רשומה", + "Delete record": "מחק רשומה", + "Move to the top": "עבור למעלה", + "Hidden": "מוסתר", + "Hide after use": "הסתר לאחר שימוש", + "Your API secret must be at least 12 characters long": "הסיסמא חייבת להיות באורך 12 תווים לפחות", + "Bad API secret": "סיסמא שגויה", + "API secret hash stored": "סיסמא אוכסנה", + "Status": "מצב מערכת", + "Not loaded": "לא נטען", + "Food Editor": "עורך המזון", + "Your database": "בסיס הנתונים שלך", + "Filter": "סנן", + "Save": "שמור", + "Clear": "נקה", + "Record": "רשומה", + "Quick picks": "בחירה מהירה", + "Show hidden": "הצג נתונים מוסתרים", + "Your API secret or token": "Your API secret or token", + "Remember this device. (Do not enable this on public computers.)": "זכור מכשיר זה. (אין לאפשר במחשבים ציבוריים)", + "Treatments": "טיפולים", + "Time": "זמן", + "Event Type": "סוג אירוע", + "Blood Glucose": "סוכר בדם", + "Entered By": "הוזן על-ידי", + "Delete this treatment?": "למחוק רשומה זו?", + "Carbs Given": "פחמימות שנאכלו", + "Insulin Given": "אינסולין שניתן", + "Event Time": "זמן האירוע", + "Please verify that the data entered is correct": "נא לוודא שהמידע שהוזן הוא נכון ומדוייק", + "BG": "סוכר בדם", + "Use BG correction in calculation": "השתמש ברמת סוכר בדם לצורך החישוב", + "BG from CGM (autoupdated)": "רמת סוכר מהחיישן , מעודכן אוטומטית", + "BG from meter": "רמת סוכר ממד הסוכר", + "Manual BG": "רמת סוכר ידנית", + "Quickpick": "בחירה מהירה", + "or": "או", + "Add from database": "הוסף מבסיס נתונים", + "Use carbs correction in calculation": "השתמש בתיקון פחמימות במהלך החישוב", + "Use COB correction in calculation": "השתמש בתיקון פחמימות בגוף במהלך החישוב", + "Use IOB in calculation": "השתמש בתיקון אינסולין בגוף במהלך החישוב", + "Other correction": "תיקון אחר", + "Rounding": "עיגול", + "Enter insulin correction in treatment": "הזן תיקון אינסולין בטיפול", + "Insulin needed": "אינסולין נדרש", + "Carbs needed": "פחמימות נדרשות", + "Carbs needed if Insulin total is negative value": "פחמימות דרושות אם סך אינסולין הוא שלילי", + "Basal rate": "קצב בזלי", + "60 minutes earlier": "שישים דקות מוקדם יותר", + "45 minutes earlier": "ארבעים דקות מוקדם יותר", + "30 minutes earlier": "שלושים דקות מוקדם יותר", + "20 minutes earlier": "עשרים דקות מוקדם יותר", + "15 minutes earlier": "חמש עשרה דקות מוקדם יותר", + "Time in minutes": "זמן בדקות", + "15 minutes later": "רבע שעה מאוחר יותר", + "20 minutes later": "עשרים דקות מאוחר יותר", + "30 minutes later": "חצי שעה מאוחר יותר", + "45 minutes later": "שלושת רבעי שעה מאוחר יותר", + "60 minutes later": "שעה מאוחר יותר", + "Additional Notes, Comments": "הערות נוספות", + "RETRO MODE": "מצב רטרו", + "Now": "עכשיו", + "Other": "אחר", + "Submit Form": "שמור", + "Profile Editor": "ערוך פרופיל", + "Reports": "דוחות", + "Add food from your database": "הוסף אוכל מבסיס הנתונים שלך", + "Reload database": "טען בסיס נתונים שוב", + "Add": "הוסף", + "Unauthorized": "אין אישור", + "Entering record failed": "הוספת רשומה נכשלה", + "Device authenticated": "התקן מאושר", + "Device not authenticated": "התקן לא מאושר", + "Authentication status": "סטטוס אימות", + "Authenticate": "אמת", + "Remove": "הסר", + "Your device is not authenticated yet": "ההתקן שלך עדיין לא מאושר", + "Sensor": "חיישן סוכר", + "Finger": "אצבע", + "Manual": "ידני", + "Scale": "סקלה", + "Linear": "לינארי", + "Logarithmic": "לוגריתמי", + "Logarithmic (Dynamic)": "לוגריטמי - דינמי", + "Insulin-on-Board": "אינסולין בגוף", + "Carbs-on-Board": "פחמימות בגוף", + "Bolus Wizard Preview": "סקירת אשף הבולוס", + "Value Loaded": "ערך נטען", + "Cannula Age": "גיל הקנולה", + "Basal Profile": "פרופיל רמה בזלית", + "Silence for 30 minutes": "השתק לשלושים דקות", + "Silence for 60 minutes": "השתק לששים דקות", + "Silence for 90 minutes": "השתק לתשעים דקות", + "Silence for 120 minutes": "השתק לשעתיים", + "Settings": "הגדרות", + "Units": "יחידות", + "Date format": "פורמט תאריך", + "12 hours": "שתים עשרה שעות", + "24 hours": "עשרים וארבע שעות", + "Log a Treatment": "הזן רשומה", + "BG Check": "בדיקת סוכר", + "Meal Bolus": "בולוס ארוחה", + "Snack Bolus": "בולוס ארוחת ביניים", + "Correction Bolus": "בולוס תיקון", + "Carb Correction": "בולוס פחמימות", + "Note": "הערה", + "Question": "שאלה", + "Exercise": "פעילות גופנית", + "Pump Site Change": "החלפת צינורית משאבה", + "CGM Sensor Start": "אתחול חיישן סוכר", + "CGM Sensor Stop": "הפסקת חיישן סוכר", + "CGM Sensor Insert": "החלפת חיישן סוכר", + "Sensor Code": "קוד חיישן", + "Transmitter ID": "מספר זיהוי המשדר", + "Dexcom Sensor Start": "אתחול חיישן סוכר של דקסקום", + "Dexcom Sensor Change": "החלפת חיישן סוכר של דקסקום", + "Insulin Cartridge Change": "החלפת מחסנית אינסולין", + "D.A.D. Alert": "ווף! ווף! התראת גשג", + "Glucose Reading": "מדידת סוכר", + "Measurement Method": "אמצעי מדידה", + "Meter": "מד סוכר", + "Amount in grams": "כמות בגרמים", + "Amount in units": "כמות ביחידות", + "View all treatments": "הצג את כל הטיפולים", + "Enable Alarms": "הפעל התראות", + "Pump Battery Change": "החלפת סוללת משאבה", + "Pump Battery Age": "גיל סוללת המשאבה", + "Pump Battery Low Alarm": "התראת סוללת משאבה חלשה", + "Pump Battery change overdue!": "הגיע הזמן להחליף סוללת משאבה!", + "When enabled an alarm may sound.": "כשמופעל התראות יכולות להישמע.", + "Urgent High Alarm": "התראת גבוה דחופה", + "High Alarm": "התראת גבוה", + "Low Alarm": "התראת נמוך", + "Urgent Low Alarm": "התראת נמוך דחופה", + "Stale Data: Warn": "אזהרה-נתונים ישנים", + "Stale Data: Urgent": "דחוף-נתונים ישנים", + "mins": "דקות", + "Night Mode": "מצב לילה", + "When enabled the page will be dimmed from 10pm - 6am.": "במצב זה המסך יעומעם בין השעות עשר בלילה לשש בבוקר", + "Enable": "אפשר", + "Show Raw BG Data": "הראה את רמת הסוכר ללא עיבוד", + "Never": "אף פעם", + "Always": "תמיד", + "When there is noise": "בנוכחות רעש", + "When enabled small white dots will be displayed for raw BG data": "במצב זה,רמות סוכר לפני עיבוד יוצגו כנקודות לבנות קטנות", + "Custom Title": "כותרת מותאמת אישית", + "Theme": "נושא", + "Default": "בְּרִירַת מֶחדָל", + "Colors": "צבעים", + "Colorblind-friendly colors": "צבעים ידידותיים לעוורי צבעים", + "Reset, and use defaults": "איפוס ושימוש ברירות מחדל", + "Calibrations": "כיולים", + "Alarm Test / Smartphone Enable": "אזעקה מבחן / הפעל טלפון", + "Bolus Wizard": "אשף בולוס", + "in the future": "בעתיד", + "time ago": "פעם", + "hr ago": "לפני שעה", + "hrs ago": "לפני שעות", + "min ago": "לפני דקה", + "mins ago": "לפני דקות", + "day ago": "לפני יום", + "days ago": "לפני ימים", + "long ago": "לפני הרבה זמן", + "Clean": "נקה", + "Light": "אוֹר", + "Medium": "בינוני", + "Heavy": "כבד", + "Treatment type": "סוג הטיפול", + "Raw BG": "רמת סוכר גולמית", + "Device": "התקן", + "Noise": "רַעַשׁ", + "Calibration": "כִּיוּל", + "Show Plugins": "הצג תוספים", + "About": "על אודות", + "Value in": "ערך", + "Carb Time": "זמן פחמימה", + "Language": "שפה", + "Add new": "הוסף חדש", + "g": "גרמים", + "ml": "מיליטרים", + "pcs": "יחידות", + "Drag&drop food here": "גרור ושחרר אוכל כאן", + "Care Portal": "פורטל טיפולים", + "Medium/Unknown": "בינוני/לא ידוע", + "IN THE FUTURE": "בעתיד", + "Order": "סֵדֶר", + "oldest on top": "העתיק ביותר למעלה", + "newest on top": "החדש ביותר למעלה", + "All sensor events": "כל האירועים חיישן", + "Remove future items from mongo database": "הסרת פריטים עתידיים ממסד הנתונים מונגו", + "Find and remove treatments in the future": "מצא ולהסיר טיפולים בעתיד", + "This task find and remove treatments in the future.": "משימה זו למצוא ולהסיר טיפולים בעתיד", + "Remove treatments in the future": "הסר טיפולים בעתיד", + "Find and remove entries in the future": "מצא והסר רשומות בעתיד", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "משימה זו מוצאת נתונים של סנסור סוכר ומסירה אותם בעתיד, שנוצרו על ידי העלאת נתונים עם תאריך / שעה שגויים", + "Remove entries in the future": "הסר רשומות בעתיד", + "Loading database ...": "טוען מסד נתונים ... ", + "Database contains %1 future records": " מסד הנתונים מכיל% 1 רשומות עתידיות ", + "Remove %1 selected records?": "האם להסיר% 1 רשומות שנבחרו? ", + "Error loading database": "שגיאה בטעינת מסד הנתונים ", + "Record %1 removed ...": "רשומה% 1 הוסרה ... ", + "Error removing record %1": "שגיאה בהסרת הרשומה% 1 ", + "Deleting records ...": "מוחק רשומות ... ", + "%1 records deleted": "%1 רשומות נמחקו", + "Clean Mongo status database": "נקי מסד הנתונים מצב מונגו ", + "Delete all documents from devicestatus collection": "מחק את כל המסמכים מאוסף סטטוס המכשיר ", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "הפעולה הבאה מסירה את כל התיעוד ממכשיר האיסוף, פעולה זו שימושית כאשר מצב הסוללה לא מתעדכן כראוי.", + "Delete all documents": "מחק את כל המסמכים ", + "Delete all documents from devicestatus collection?": "מחק את כל המסמכים מרשימת סטטוס ההתקנים ", + "Database contains %1 records": "מסד נתונים מכיל %1 רשומות ", + "All records removed ...": "כל הרשומות נמחקו ", + "Delete all documents from devicestatus collection older than 30 days": "מחיקת כל המסמכים מאוסף הרשומות שגילם מעל 30 יום", + "Number of Days to Keep:": "מספר ימים לשמירה:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "הפעולה הבאה מסירה את כל רשומות התיעוד ממכשיר האיסוף שגילן מעל 30 יום, פעולה זו שימושית כאשר מצב הסוללה לא מתעדכן כראוי.", + "Delete old documents from devicestatus collection?": "האם למחוק את כל המסמכים מרשימת סטטוס ההתקנים?", + "Clean Mongo entries (glucose entries) database": "ניקוי מסד נתוני רשומות Mongo (רשומות סוכר)", + "Delete all documents from entries collection older than 180 days": "מחיקת כל המסמכים מאוסף הרשומות שגילם מעל 180 יום", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents": "מחק את כל המסמכים", + "Delete old documents from entries collection?": "למחוק את כל המסמכים מאוסף הרשומות?", + "%1 is not a valid number": "זה לא מיספר %1", + "%1 is not a valid number - must be more than 2": "%1 אינו מספר חוקי - עליו להיות גדול מ-2", + "Clean Mongo treatments database": "נקה את כל הטיפולים ממסד הנתונים מונגו", + "Delete all documents from treatments collection older than 180 days": "מחיקת כל המסמכים מאוסף הרשומות שגילם מעל 180 יום", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "כלי אדמיניסטרציה ", + "Nightscout reporting": "דוחות נייססקאוט", + "Cancel": "בטל ", + "Edit treatment": "ערוך טיפול ", + "Duration": "משך ", + "Duration in minutes": "משך בדקות ", + "Temp Basal": "רמה בזלית זמנית", + "Temp Basal Start": "התחלת רמה בזלית זמנית ", + "Temp Basal End": "סיום רמה בזלית זמנית", + "Percent": "אחוז ", + "Basal change in %": "שינוי קצב בזלי באחוזים ", + "Basal value": "ערך בזלי", + "Absolute basal value": "ערך בזלי מוחלט ", + "Announcement": "הודעה", + "Loading temp basal data": "טוען ערך בזלי זמני ", + "Save current record before changing to new?": "שמור רשומה נוכחית לפני שעוברים לרשומה חדשה? ", + "Profile Switch": "החלף פרופיל ", + "Profile": "פרופיל ", + "General profile settings": "הגדרות פרופיל כלליות", + "Title": "כותרת ", + "Database records": "רשומות ", + "Add new record": "הוסף רשומה חדשה ", + "Remove this record": "מחק רשומה זו ", + "Clone this record to new": "שכפל רשומה זו לרשומה חדשה ", + "Record valid from": "רשומה תקפה מ ", + "Stored profiles": "פרופילים מאוכסנים ", + "Timezone": "אזור זמן ", + "Duration of Insulin Activity (DIA)": "משך פעילות האינסולין ", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "מייצג את משך הטיפוסי שבו האינסולין נכנס לתוקף. משתנה לכל חולה וסוג האינסולין. בדרך כלל 3-4 שעות עבור רוב אינסולין שאיבה רוב המטופלים. לפעמים נקרא גם אורך חיי אינסולין. ", + "Insulin to carb ratio (I:C)": "יחס אינסולין פחמימות ", + "Hours:": "שעות:", + "hours": "שעות ", + "g/hour": "גרם לשעה ", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "היחס בין כמה גרם של פחמימות מקוזז על ידי כל יחידת אינסולין ", + "Insulin Sensitivity Factor (ISF)": "גורם רגישות לאינסולין ", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "כמה סוכר בדם משתנה עם כל יחידת אינסולין ", + "Carbs activity / absorption rate": "פעילות פחמימות / קצב קליטה ", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "גרם ליחידת זמן. מייצג הן את השינוי בפחמימות ליחידת זמן והן את כמות הפחמימות אשר אמורות להיכנס לתוקף במהלך אותה תקופה. ספיגת הפחמימות / פעילות הם פחות מובן מאשר פעילות האינסולין, אך ניתן להתקרב באמצעות עיכוב ראשוני ואחריו שיעור קבוע של קליטה (g / hr) ", + "Basal rates [unit/hour]": "קצב בסיסי (יחידה לשעה) ", + "Target BG range [mg/dL,mmol/L]": "יעד טווח גלוקוז בדם ", + "Start of record validity": "תחילת תוקף הרשומה ", + "Icicle": "קרחון ", + "Render Basal": "הראה רמה בזלית ", + "Profile used": "פרופיל בשימוש ", + "Calculation is in target range.": "חישוב הוא בטווח היעד ", + "Loading profile records ...": "טוען רשומות פרופיל ", + "Values loaded.": "ערכים נטענו ", + "Default values used.": "משתמשים בערכי בררת המחדל ", + "Error. Default values used.": "משתמשים בערכי בררת המחדל שגיאה - ", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "טווחי יעד נמוכים ויעדים גבוהים אינם תואמים. הערכים משוחזרים לברירות המחדל ", + "Valid from:": "בתוקף מ: ", + "Save current record before switching to new?": "שמור את הרשומה הנוכחית לפני המעבר ל חדש? ", + "Add new interval before": "הוסף מרווח חדש לפני ", + "Delete interval": "בטל מרווח ", + "I:C": "יחס אינסולין לפחמימות ", + "ISF": "ISF ", + "Combo Bolus": "בולוס קומבו ", + "Difference": "הבדל ", + "New time": "זמן חדש ", + "Edit Mode": "מצב עריכה ", + "When enabled icon to start edit mode is visible": "כאשר הסמל מופעל כדי להתחיל במצב עריכה גלוי ", + "Operation": "פעולה ", + "Move": "הזז ", + "Delete": "בטל ", + "Move insulin": "הזז אינסולין ", + "Move carbs": "הזז פחמימות ", + "Remove insulin": "בטל אינסולין ", + "Remove carbs": "בטל פחמימות ", + "Change treatment time to %1 ?": "שנה זמן לטיפול ל %1 ", + "Change carbs time to %1 ?": "שנה זמן פחמימות ל %1 ", + "Change insulin time to %1 ?": "שנה זמן אינסולין ל %1 ", + "Remove treatment ?": "בטל טיפול ", + "Remove insulin from treatment ?": "הסר אינסולין מהטיפול? ", + "Remove carbs from treatment ?": "הסר פחמימות מהטיפול? ", + "Rendering": "מציג... ", + "Loading OpenAPS data of": "טוען מידע מ ", + "Loading profile switch data": "טוען מידע החלפת פרופיל ", + "Redirecting you to the Profile Editor to create a new profile.": "הגדרת פרופיל שגוי. \n פרופיל מוגדר לזמן המוצג. מפנה מחדש לעורך פרופיל כדי ליצור פרופיל חדש. ", + "Pump": "משאבה ", + "Sensor Age": "גיל סנסור סוכר ", + "Insulin Age": "גיל אינסולין ", + "Temporary target": "מטרה זמנית ", + "Reason": "סיבה ", + "Eating soon": "אוכל בקרוב", + "Top": "למעלה ", + "Bottom": "למטה ", + "Activity": "פעילות ", + "Targets": "מטרות ", + "Bolus insulin:": "אינסולין בולוס ", + "Base basal insulin:": "אינסולין בזלי בסיס ", + "Positive temp basal insulin:": "אינסולין בזלי זמני חיובי ", + "Negative temp basal insulin:": "אינסולין בזלי זמני שלילי ", + "Total basal insulin:": "סך אינסולין בזלי ", + "Total daily insulin:": "סך אינסולין ביום ", + "Unable to save Role": "Unable to save Role", + "Unable to delete Role": "לא יכול לבטל תפקיד ", + "Database contains %1 roles": "בסיס הנתונים מכיל %1 תפקידים ", + "Edit Role": "ערוך תפקיד ", + "admin, school, family, etc": "מנהל, בית ספר, משפחה, וכו ", + "Permissions": "הרשאות ", + "Are you sure you want to delete: ": "אתה בטוח שאתה רוצה למחוק", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.", + "Add new Role": "הוסף תפקיד חדש ", + "Roles - Groups of People, Devices, etc": "תפקידים - קבוצות של אנשים, התקנים, וכו ", + "Edit this role": "ערוך תפקיד זה ", + "Admin authorized": "מנהל אושר ", + "Subjects - People, Devices, etc": "נושאים - אנשים, התקנים וכו ", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "לכל נושא תהיה אסימון גישה ייחודי ותפקיד אחד או יותר. לחץ על אסימון הגישה כדי לפתוח תצוגה חדשה עם הנושא הנבחר, קישור סודי זה יכול להיות משותף ", + "Add new Subject": "הוסף נושא חדש ", + "Unable to save Subject": "Unable to save Subject", + "Unable to delete Subject": "לא יכול לבטל נושא ", + "Database contains %1 subjects": "מסד נתונים מכיל %1 נושאים ", + "Edit Subject": "ערוך נושא ", + "person, device, etc": "אדם, מכשיר, וכו ", + "role1, role2": "תפקיד1, תפקיד2 ", + "Edit this subject": "ערוך נושא זה ", + "Delete this subject": "בטל נושא זה ", + "Roles": "תפקידים ", + "Access Token": "אסימון גישה ", + "hour ago": "לפני שעה ", + "hours ago": "לפני שעות ", + "Silence for %1 minutes": "שקט ל %1 דקות ", + "Check BG": "בדוק סוכר בדם ", + "BASAL": "רמה בזלית ", + "Current basal": "רמה בזלית נוכחית ", + "Sensitivity": "רגישות ", + "Current Carb Ratio": "וחס פחמימות לאינסולין נוכחי ", + "Basal timezone": "איזור זמן לרמה הבזלית ", + "Active profile": "פרופיל פעיל ", + "Active temp basal": "רמה בזלית זמנית פעילה ", + "Active temp basal start": "התחלה רמה בזלית זמנית ", + "Active temp basal duration": "משך רמה בזלית זמנית ", + "Active temp basal remaining": "זמן שנשאר ברמה בזלית זמנית ", + "Basal profile value": "רמה בזלית מפרופיל ", + "Active combo bolus": "בולוס קומבו פעיל ", + "Active combo bolus start": "התחלת בולוס קומבו פעיל ", + "Active combo bolus duration": "משך בולוס קומבו פעיל ", + "Active combo bolus remaining": "זמן שנשאר בבולוס קומבו פעיל ", + "BG Delta": "הפרש רמת סוכר ", + "Elapsed Time": "זמן שעבר ", + "Absolute Delta": "הפרש רמת סוכר אבסולוטית ", + "Interpolated": "אינטרפולציה ", + "BWP": "BWP", + "Urgent": "דחוף ", + "Warning": "אזהרה ", + "Info": "לידיעה ", + "Lowest": "הנמוך ביותר ", + "Snoozing high alarm since there is enough IOB": "נודניק את ההתראה הגבוהה מפני שיש מספיק אינסולין פעיל בגוף", + "Check BG, time to bolus?": "בדוק רמת סוכר. צריך להוסיף אינסולין? ", + "Notice": "הודעה ", + "required info missing": "חסרה אינפורמציה ", + "Insulin on Board": "אינסולין פעיל בגוף ", + "Current target": "מטרה נוכחית ", + "Expected effect": "אפקט צפוי ", + "Expected outcome": "תוצאת צפויה ", + "Carb Equivalent": "מקבילה בפחמימות ", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "עודף אינסולין שווה ערך ליחידה אחת%1 יותר מאשר הצורך להגיע ליעד נמוך, לא לוקח בחשבון פחמימות ", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "עודף אינסולין %1 נדרש כדי להגיע למטרה התחתונה. שים לב כי עליך לכסות אינסולין בגוף על ידי פחמימות ", + "%1U reduction needed in active insulin to reach low target, too much basal?": "צריך %1 פחות אינסולין כדי להגיע לגבול התחתון. האם הרמה הבזלית גבוהה מדי? ", + "basal adjustment out of range, give carbs?": "השינוי ברמה הבזלית גדול מדי. תן פחמימות? ", + "basal adjustment out of range, give bolus?": "השינוי ברמה הבזלית גדול מדי. תן אינסולין? ", + "above high": "מעל גבוה ", + "below low": "מתחת נמוך ", + "Projected BG %1 target": "תחזית רמת סןכר %1 ", + "aiming at": "מטרה ", + "Bolus %1 units": "בולוס %1 יחידות ", + "or adjust basal": "או שנה רמה בזלית ", + "Check BG using glucometer before correcting!": "מדוד רמת סוכר בדם באמצעות מד סוכר לפני תיקון ", + "Basal reduction to account %1 units:": "משנה רמה בזלית בשביל %1 יחידות ", + "30m temp basal": "שלושים דקות רמה בזלית זמנית ", + "1h temp basal": "שעה רמה בזלית זמנית ", + "Cannula change overdue!": "יש צורך בהחלפת קנולה! ", + "Time to change cannula": "הגיע הזמן להחליף קנולה ", + "Change cannula soon": "החלף קנולה בקרוב ", + "Cannula age %1 hours": "כיל הקנולה %1 שעות ", + "Inserted": "הוכנס ", + "CAGE": "גיל הקנולה ", + "COB": "COB ", + "Last Carbs": "פחמימות אחרונות ", + "IAGE": "גיל אינסולין ", + "Insulin reservoir change overdue!": "החלף מאגר אינסולין! ", + "Time to change insulin reservoir": "הגיע הזמן להחליף מאגר אינסולין ", + "Change insulin reservoir soon": "החלף מאגר אינסולין בקרוב ", + "Insulin reservoir age %1 hours": "גיל מאגר אינסולין %1 שעות ", + "Changed": "הוחלף ", + "IOB": "IOB ", + "Careportal IOB": "Careportal IOB ", + "Last Bolus": "בולוס אחרון ", + "Basal IOB": "IOB בזלי ", + "Source": "מקור ", + "Stale data, check rig?": "מידע ישן, בדוק את המערכת? ", + "Last received:": "התקבל לאחרונה: ", + "%1m ago": "לפני %1 דקות ", + "%1h ago": "לפני %1 שעות ", + "%1d ago": "לפני %1 ימים ", + "RETRO": "רטרו ", + "SAGE": "גיל הסנסור ", + "Sensor change/restart overdue!": "שנה או אתחל את הסנסור! ", + "Time to change/restart sensor": "הגיע הזמן לשנות או לאתחל את הסנסור ", + "Change/restart sensor soon": "שנה או אתחל את הסנסור בקרוב ", + "Sensor age %1 days %2 hours": "גיל הסנסור %1 ימים %2 שעות ", + "Sensor Insert": "הכנס סנסור ", + "Sensor Start": "סנסור התחיל ", + "days": "ימים ", + "Insulin distribution": "התפלגות אינסולין ", + "To see this report, press SHOW while in this view": "כדי להציג דוח זה, לחץ על\"הראה\" בתצוגה זו ", + "AR2 Forecast": "AR2 תחזית ", + "OpenAPS Forecasts": "תחזית OPENAPS ", + "Temporary Target": "מטרה זמנית ", + "Temporary Target Cancel": "בטל מטרה זמנית ", + "OpenAPS Offline": "OPENAPS לא פעיל ", + "Profiles": "פרופילים ", + "Time in fluctuation": "זמן בתנודות ", + "Time in rapid fluctuation": "זמן בתנודות מהירות ", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "זוהי רק הערכה גסה שיכולה להיות מאוד לא מדויקת ואינה מחליפה את בדיקת הדם בפועל. הנוסחה המשמשת נלקחת מ: ", + "Filter by hours": "סנן לפי שעות", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "ממוצע השינוי היומי מחושב כך:\nהסכום של הערך המוחלט של ערכי הסוכר (גלוקוז) מחוץ לטווח עבור התקופה המבוקשת, חלקי מספר הימים בתקופה המבוקשת.\nתוצאה נמוכה יותר היא טובה יותר.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "ממוצע השינוי השעתי מחושב כך:\nהסכום של הערך המוחלט של ערכי הסוכר (גלוקוז) מחוץ לטווח עבור התקופה המבוקשת, חלקי מספר השעות בתקופה המבוקשת.\nתוצאה נמוכה יותר היא טובה יותר.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">here.", + "Mean Total Daily Change": "שינוי יומי ממוצע ", + "Mean Hourly Change": "שינוי ממוצע לשעה ", + "FortyFiveDown": "slightly dropping", + "FortyFiveUp": "slightly rising", + "Flat": "מחזיק", + "SingleUp": "עולה", + "SingleDown": "נופל", + "DoubleDown": "נופל במהירות", + "DoubleUp": "עולה במהירות", + "virtAsstUnknown": "הערך המבוקש אינו ידוע כרגע. בקרו באתר ה\"Nighscout\" שלכם למידע נוסף.", + "virtAsstTitleAR2Forecast": "תחזית AR2", + "virtAsstTitleCurrentBasal": "ערך באזלי נוכחי", + "virtAsstTitleCurrentCOB": "פחמ' פעילות כעת", + "virtAsstTitleCurrentIOB": "אינסולין פעיל כעת", + "virtAsstTitleLaunch": "ברוכים הבאים ל-Nightscout", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "תחזית OpenAPS", + "virtAsstTitlePumpReservoir": "אינסולין נותר", + "virtAsstTitlePumpBattery": "סוללת משאבה", + "virtAsstTitleRawBG": "רמת סוכר גולמית", + "virtAsstTitleUploaderBattery": "רמת סוללה במשדר", + "virtAsstTitleCurrentBG": "רמת סוכר נוכחית", + "virtAsstTitleFullStatus": "סטאטוס מלא", + "virtAsstTitleCGMMode": "CGM Mode", + "virtAsstTitleCGMStatus": "מצב חיישן", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM Transmitter Age", + "virtAsstTitleCGMNoise": "רעש נתוני חיישן", + "virtAsstTitleDelta": "שינוי ברמת הסוכר", + "virtAsstStatus": "%1 ו-%2 החל מ-%3.", + "virtAsstBasal": "%1 מינון בזאלי נוכחי הוא %2 יח' לשעה", + "virtAsstBasalTemp": "%1 בזאלי זמני במינון %2 יח' לשעה יסתיים ב-%3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "יש %1 יח' אינסולין פעיל", + "virtAsstIobUnits": "%1 יח' של", + "virtAsstLaunch": "מה תרצו לבדוק ב-Nightscout?", + "virtAsstPreamble": "שלך", + "virtAsstPreamble3person": "%1 has a ", + "virtAsstNoInsulin": "לא", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "רמת סוללת המשאבה %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "כוונה לא ידועה", + "virtAsstUnknownIntentText": "סליחה, אינני יודע מה אתה שואל.", + "Fat [g]": "[g] שמן", + "Protein [g]": "[g] חלבון", + "Energy [kJ]": "[kJ] אנרגיה", + "Clock Views:": "צגים השעון", + "Clock": "שעון", + "Color": "צבע", + "Simple": "פשוט", + "TDD average": "ממוצע מינון יומי כולל", + "Bolus average": "ממוצע בולוס", + "Basal average": "ממוצע בזאלי", + "Base basal average:": "ממוצע בזאלי בסיסי:", + "Carbs average": "פחמימות ממוצע", + "Eating Soon": "אוכל בקרוב", + "Last entry {0} minutes ago": "Last entry {0} minutes ago", + "change": "שינוי", + "Speech": "דיבור", + "Target Top": "ראש היעד", + "Target Bottom": "תחתית היעד", + "Canceled": "מבוטל", + "Meter BG": "סוכר הדם של מד", + "predicted": "חזה", + "future": "עתיד", + "ago": "לפני", + "Last data received": "הנתונים המקבל אחרונים", + "Clock View": "צג השעון", + "Protein": "חלבון", + "Fat": "שמן", + "Protein average": "חלבון ממוצע", + "Fat average": "שמן ממוצע", + "Total carbs": "כל פחמימות", + "Total protein": "כל חלבונים", + "Total fat": "כל שומנים", + "Database Size": "גודל מסד הנתונים", + "Database Size near its limits!": "גודל מסד הנתונים מתקרב למגבלה!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "גודל קובץ מסד נתונים", + "%1 MiB of %2 MiB (%3%)": "%1 MiB מתוך %2 MiB (%3%)", + "Data size": "גודל נתונים", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "גודל קובץ מסד נתונים", + "Carbs/Food/Time": "פחמ'\\אוכל\\זמן", + "You have administration messages": "You have administration messages", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "קריאת נתונים מופעלת תחת הרשאות ברירת מחדל", + "Data reads enabled": "קריאת נתונים מופעלת", + "Data writes enabled": "רישום נתונים מופעל", + "Data writes not enabled": "רישום נתונים מופסק", + "Color prediction lines": "צבע עקומי חיזוי", + "Release Notes": "הערות הגרסה", + "Check for Updates": "בדוק עדכונים", + "Open Source": "קוד פתוח", + "Nightscout Info": "מידע על Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "הקודם", + "Previous day": "היום הקודם", + "Next day": "היום הבא", + "Next": "הבא", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "ברירת מחדל (עם אפסים מקדימים ו U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "מינימלי (עם אפסים מקדימים ו U)", + "Small Bolus Display": "תצוגת בולוסים קטנים", + "Large Bolus Display": "תצודת בולוסים גדולים", + "Bolus Display Threshold": "בולוס מינימלי להצגה", + "%1 U and Over": "1% U ויותר", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "דקות", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/translations/hi_IN.json b/translations/hi_IN.json new file mode 100644 index 00000000000..37e1d47e97d --- /dev/null +++ b/translations/hi_IN.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Listening on port", + "Mo": "Mo", + "Tu": "Tu", + "We": "We", + "Th": "Th", + "Fr": "Fr", + "Sa": "Sa", + "Su": "Su", + "Monday": "Monday", + "Tuesday": "Tuesday", + "Wednesday": "Wednesday", + "Thursday": "Thursday", + "Friday": "Friday", + "Saturday": "Saturday", + "Sunday": "Sunday", + "Category": "Category", + "Subcategory": "Subcategory", + "Name": "Name", + "Today": "Today", + "Last 2 days": "Last 2 days", + "Last 3 days": "Last 3 days", + "Last week": "Last week", + "Last 2 weeks": "Last 2 weeks", + "Last month": "Last month", + "Last 3 months": "Last 3 months", + "From": "From", + "To": "To", + "Notes": "Notes", + "Food": "Food", + "Insulin": "Insulin", + "Carbs": "Carbs", + "Notes contain": "Notes contain", + "Target BG range bottom": "Target BG range bottom", + "top": "top", + "Show": "Show", + "Display": "Display", + "Loading": "Loading", + "Loading profile": "Loading profile", + "Loading status": "Loading status", + "Loading food database": "Loading food database", + "not displayed": "not displayed", + "Loading CGM data of": "Loading CGM data of", + "Loading treatments data of": "Loading treatments data of", + "Processing data of": "Processing data of", + "Portion": "Portion", + "Size": "Size", + "(none)": "(none)", + "None": "None", + "": "", + "Result is empty": "Result is empty", + "Day to day": "Day to day", + "Week to week": "Week to week", + "Daily Stats": "Daily Stats", + "Percentile Chart": "Percentile Chart", + "Distribution": "Distribution", + "Hourly stats": "Hourly stats", + "netIOB stats": "netIOB stats", + "temp basals must be rendered to display this report": "temp basals must be rendered to display this report", + "No data available": "No data available", + "Low": "Low", + "In Range": "In Range", + "Period": "Period", + "High": "High", + "Average": "Average", + "Low Quartile": "Low Quartile", + "Upper Quartile": "Upper Quartile", + "Quartile": "Quartile", + "Date": "Date", + "Normal": "Normal", + "Median": "Median", + "Readings": "Readings", + "StDev": "StDev", + "Daily stats report": "Daily stats report", + "Glucose Percentile report": "Glucose Percentile report", + "Glucose distribution": "Glucose distribution", + "days total": "days total", + "Total per day": "Total per day", + "Overall": "Overall", + "Range": "Range", + "% of Readings": "% of Readings", + "# of Readings": "# of Readings", + "Mean": "Mean", + "Standard Deviation": "Standard Deviation", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "A1c estimation*", + "Weekly Success": "Weekly Success", + "There is not sufficient data to run this report. Select more days.": "There is not sufficient data to run this report. Select more days.", + "Using stored API secret hash": "Using stored API secret hash", + "No API secret hash stored yet. You need to enter API secret.": "No API secret hash stored yet. You need to enter API secret.", + "Database loaded": "Database loaded", + "Error: Database failed to load": "Error: Database failed to load", + "Error": "Error", + "Create new record": "Create new record", + "Save record": "Save record", + "Portions": "Portions", + "Unit": "Unit", + "GI": "GI", + "Edit record": "Edit record", + "Delete record": "Delete record", + "Move to the top": "Move to the top", + "Hidden": "Hidden", + "Hide after use": "Hide after use", + "Your API secret must be at least 12 characters long": "Your API secret must be at least 12 characters long", + "Bad API secret": "Bad API secret", + "API secret hash stored": "API secret hash stored", + "Status": "Status", + "Not loaded": "Not loaded", + "Food Editor": "Food Editor", + "Your database": "Your database", + "Filter": "Filter", + "Save": "Save", + "Clear": "Clear", + "Record": "Record", + "Quick picks": "Quick picks", + "Show hidden": "Show hidden", + "Your API secret or token": "Your API secret or token", + "Remember this device. (Do not enable this on public computers.)": "Remember this device. (Do not enable this on public computers.)", + "Treatments": "Treatments", + "Time": "Time", + "Event Type": "Event Type", + "Blood Glucose": "Blood Glucose", + "Entered By": "Entered By", + "Delete this treatment?": "Delete this treatment?", + "Carbs Given": "Carbs Given", + "Insulin Given": "Insulin Given", + "Event Time": "Event Time", + "Please verify that the data entered is correct": "Please verify that the data entered is correct", + "BG": "BG", + "Use BG correction in calculation": "Use BG correction in calculation", + "BG from CGM (autoupdated)": "BG from CGM (autoupdated)", + "BG from meter": "BG from meter", + "Manual BG": "Manual BG", + "Quickpick": "Quickpick", + "or": "or", + "Add from database": "Add from database", + "Use carbs correction in calculation": "Use carbs correction in calculation", + "Use COB correction in calculation": "Use COB correction in calculation", + "Use IOB in calculation": "Use IOB in calculation", + "Other correction": "Other correction", + "Rounding": "Rounding", + "Enter insulin correction in treatment": "Enter insulin correction in treatment", + "Insulin needed": "Insulin needed", + "Carbs needed": "Carbs needed", + "Carbs needed if Insulin total is negative value": "Carbs needed if Insulin total is negative value", + "Basal rate": "Basal rate", + "60 minutes earlier": "60 minutes earlier", + "45 minutes earlier": "45 minutes earlier", + "30 minutes earlier": "30 minutes earlier", + "20 minutes earlier": "20 minutes earlier", + "15 minutes earlier": "15 minutes earlier", + "Time in minutes": "Time in minutes", + "15 minutes later": "15 minutes later", + "20 minutes later": "20 minutes later", + "30 minutes later": "30 minutes later", + "45 minutes later": "45 minutes later", + "60 minutes later": "60 minutes later", + "Additional Notes, Comments": "Additional Notes, Comments", + "RETRO MODE": "RETRO MODE", + "Now": "Now", + "Other": "Other", + "Submit Form": "Submit Form", + "Profile Editor": "Profile Editor", + "Reports": "Reports", + "Add food from your database": "Add food from your database", + "Reload database": "Reload database", + "Add": "Add", + "Unauthorized": "Unauthorized", + "Entering record failed": "Entering record failed", + "Device authenticated": "Device authenticated", + "Device not authenticated": "Device not authenticated", + "Authentication status": "Authentication status", + "Authenticate": "Authenticate", + "Remove": "Remove", + "Your device is not authenticated yet": "Your device is not authenticated yet", + "Sensor": "Sensor", + "Finger": "Finger", + "Manual": "Manual", + "Scale": "Scale", + "Linear": "Linear", + "Logarithmic": "Logarithmic", + "Logarithmic (Dynamic)": "Logarithmic (Dynamic)", + "Insulin-on-Board": "Insulin-on-Board", + "Carbs-on-Board": "Carbs-on-Board", + "Bolus Wizard Preview": "Bolus Wizard Preview", + "Value Loaded": "Value Loaded", + "Cannula Age": "Cannula Age", + "Basal Profile": "Basal Profile", + "Silence for 30 minutes": "Silence for 30 minutes", + "Silence for 60 minutes": "Silence for 60 minutes", + "Silence for 90 minutes": "Silence for 90 minutes", + "Silence for 120 minutes": "Silence for 120 minutes", + "Settings": "Settings", + "Units": "Units", + "Date format": "Date format", + "12 hours": "12 hours", + "24 hours": "24 hours", + "Log a Treatment": "Log a Treatment", + "BG Check": "BG Check", + "Meal Bolus": "Meal Bolus", + "Snack Bolus": "Snack Bolus", + "Correction Bolus": "Correction Bolus", + "Carb Correction": "Carb Correction", + "Note": "Note", + "Question": "Question", + "Exercise": "Exercise", + "Pump Site Change": "Pump Site Change", + "CGM Sensor Start": "CGM Sensor Start", + "CGM Sensor Stop": "CGM Sensor Stop", + "CGM Sensor Insert": "CGM Sensor Insert", + "Sensor Code": "Sensor Code", + "Transmitter ID": "Transmitter ID", + "Dexcom Sensor Start": "Dexcom Sensor Start", + "Dexcom Sensor Change": "Dexcom Sensor Change", + "Insulin Cartridge Change": "Insulin Cartridge Change", + "D.A.D. Alert": "D.A.D. Alert", + "Glucose Reading": "Glucose Reading", + "Measurement Method": "Measurement Method", + "Meter": "Meter", + "Amount in grams": "Amount in grams", + "Amount in units": "Amount in units", + "View all treatments": "View all treatments", + "Enable Alarms": "Enable Alarms", + "Pump Battery Change": "Pump Battery Change", + "Pump Battery Age": "Pump Battery Age", + "Pump Battery Low Alarm": "Pump Battery Low Alarm", + "Pump Battery change overdue!": "Pump Battery change overdue!", + "When enabled an alarm may sound.": "When enabled an alarm may sound.", + "Urgent High Alarm": "Urgent High Alarm", + "High Alarm": "High Alarm", + "Low Alarm": "Low Alarm", + "Urgent Low Alarm": "Urgent Low Alarm", + "Stale Data: Warn": "Stale Data: Warn", + "Stale Data: Urgent": "Stale Data: Urgent", + "mins": "mins", + "Night Mode": "Night Mode", + "When enabled the page will be dimmed from 10pm - 6am.": "When enabled the page will be dimmed from 10pm - 6am.", + "Enable": "Enable", + "Show Raw BG Data": "Show Raw BG Data", + "Never": "Never", + "Always": "Always", + "When there is noise": "When there is noise", + "When enabled small white dots will be displayed for raw BG data": "When enabled small white dots will be displayed for raw BG data", + "Custom Title": "Custom Title", + "Theme": "Theme", + "Default": "Default", + "Colors": "Colors", + "Colorblind-friendly colors": "Colorblind-friendly colors", + "Reset, and use defaults": "Reset, and use defaults", + "Calibrations": "Calibrations", + "Alarm Test / Smartphone Enable": "Alarm Test / Smartphone Enable", + "Bolus Wizard": "Bolus Wizard", + "in the future": "in the future", + "time ago": "time ago", + "hr ago": "hr ago", + "hrs ago": "hrs ago", + "min ago": "min ago", + "mins ago": "mins ago", + "day ago": "day ago", + "days ago": "days ago", + "long ago": "long ago", + "Clean": "Clean", + "Light": "Light", + "Medium": "Medium", + "Heavy": "Heavy", + "Treatment type": "Treatment type", + "Raw BG": "Raw BG", + "Device": "Device", + "Noise": "Noise", + "Calibration": "Calibration", + "Show Plugins": "Show Plugins", + "About": "About", + "Value in": "Value in", + "Carb Time": "Carb Time", + "Language": "Language", + "Add new": "Add new", + "g": "g", + "ml": "ml", + "pcs": "pcs", + "Drag&drop food here": "Drag&drop food here", + "Care Portal": "Care Portal", + "Medium/Unknown": "Medium/Unknown", + "IN THE FUTURE": "IN THE FUTURE", + "Order": "Order", + "oldest on top": "oldest on top", + "newest on top": "newest on top", + "All sensor events": "All sensor events", + "Remove future items from mongo database": "Remove future items from mongo database", + "Find and remove treatments in the future": "Find and remove treatments in the future", + "This task find and remove treatments in the future.": "This task find and remove treatments in the future.", + "Remove treatments in the future": "Remove treatments in the future", + "Find and remove entries in the future": "Find and remove entries in the future", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "This task find and remove CGM data in the future created by uploader with wrong date/time.", + "Remove entries in the future": "Remove entries in the future", + "Loading database ...": "Loading database ...", + "Database contains %1 future records": "Database contains %1 future records", + "Remove %1 selected records?": "Remove %1 selected records?", + "Error loading database": "Error loading database", + "Record %1 removed ...": "Record %1 removed ...", + "Error removing record %1": "Error removing record %1", + "Deleting records ...": "Deleting records ...", + "%1 records deleted": "%1 records deleted", + "Clean Mongo status database": "Clean Mongo status database", + "Delete all documents from devicestatus collection": "Delete all documents from devicestatus collection", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.", + "Delete all documents": "Delete all documents", + "Delete all documents from devicestatus collection?": "Delete all documents from devicestatus collection?", + "Database contains %1 records": "Database contains %1 records", + "All records removed ...": "All records removed ...", + "Delete all documents from devicestatus collection older than 30 days": "Delete all documents from devicestatus collection older than 30 days", + "Number of Days to Keep:": "Number of Days to Keep:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from devicestatus collection?": "Delete old documents from devicestatus collection?", + "Clean Mongo entries (glucose entries) database": "Clean Mongo entries (glucose entries) database", + "Delete all documents from entries collection older than 180 days": "Delete all documents from entries collection older than 180 days", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents": "Delete old documents", + "Delete old documents from entries collection?": "Delete old documents from entries collection?", + "%1 is not a valid number": "%1 is not a valid number", + "%1 is not a valid number - must be more than 2": "%1 is not a valid number - must be more than 2", + "Clean Mongo treatments database": "Clean Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Delete all documents from treatments collection older than 180 days", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "Admin Tools", + "Nightscout reporting": "Nightscout reporting", + "Cancel": "Cancel", + "Edit treatment": "Edit treatment", + "Duration": "Duration", + "Duration in minutes": "Duration in minutes", + "Temp Basal": "Temp Basal", + "Temp Basal Start": "Temp Basal Start", + "Temp Basal End": "Temp Basal End", + "Percent": "Percent", + "Basal change in %": "Basal change in %", + "Basal value": "Basal value", + "Absolute basal value": "Absolute basal value", + "Announcement": "Announcement", + "Loading temp basal data": "Loading temp basal data", + "Save current record before changing to new?": "Save current record before changing to new?", + "Profile Switch": "Profile Switch", + "Profile": "Profile", + "General profile settings": "General profile settings", + "Title": "Title", + "Database records": "Database records", + "Add new record": "Add new record", + "Remove this record": "Remove this record", + "Clone this record to new": "Clone this record to new", + "Record valid from": "Record valid from", + "Stored profiles": "Stored profiles", + "Timezone": "Timezone", + "Duration of Insulin Activity (DIA)": "Duration of Insulin Activity (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.", + "Insulin to carb ratio (I:C)": "Insulin to carb ratio (I:C)", + "Hours:": "Hours:", + "hours": "hours", + "g/hour": "g/hour", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.", + "Insulin Sensitivity Factor (ISF)": "Insulin Sensitivity Factor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.", + "Carbs activity / absorption rate": "Carbs activity / absorption rate", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).", + "Basal rates [unit/hour]": "Basal rates [unit/hour]", + "Target BG range [mg/dL,mmol/L]": "Target BG range [mg/dL,mmol/L]", + "Start of record validity": "Start of record validity", + "Icicle": "Icicle", + "Render Basal": "Render Basal", + "Profile used": "Profile used", + "Calculation is in target range.": "Calculation is in target range.", + "Loading profile records ...": "Loading profile records ...", + "Values loaded.": "Values loaded.", + "Default values used.": "Default values used.", + "Error. Default values used.": "Error. Default values used.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Time ranges of target_low and target_high don't match. Values are restored to defaults.", + "Valid from:": "Valid from:", + "Save current record before switching to new?": "Save current record before switching to new?", + "Add new interval before": "Add new interval before", + "Delete interval": "Delete interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Combo Bolus", + "Difference": "Difference", + "New time": "New time", + "Edit Mode": "Edit Mode", + "When enabled icon to start edit mode is visible": "When enabled icon to start edit mode is visible", + "Operation": "Operation", + "Move": "Move", + "Delete": "Delete", + "Move insulin": "Move insulin", + "Move carbs": "Move carbs", + "Remove insulin": "Remove insulin", + "Remove carbs": "Remove carbs", + "Change treatment time to %1 ?": "Change treatment time to %1 ?", + "Change carbs time to %1 ?": "Change carbs time to %1 ?", + "Change insulin time to %1 ?": "Change insulin time to %1 ?", + "Remove treatment ?": "Remove treatment ?", + "Remove insulin from treatment ?": "Remove insulin from treatment ?", + "Remove carbs from treatment ?": "Remove carbs from treatment ?", + "Rendering": "Rendering", + "Loading OpenAPS data of": "Loading OpenAPS data of", + "Loading profile switch data": "Loading profile switch data", + "Redirecting you to the Profile Editor to create a new profile.": "Redirecting you to the Profile Editor to create a new profile.", + "Pump": "Pump", + "Sensor Age": "Sensor Age", + "Insulin Age": "Insulin Age", + "Temporary target": "Temporary target", + "Reason": "Reason", + "Eating soon": "Eating soon", + "Top": "Top", + "Bottom": "Bottom", + "Activity": "Activity", + "Targets": "Targets", + "Bolus insulin:": "Bolus insulin:", + "Base basal insulin:": "Base basal insulin:", + "Positive temp basal insulin:": "Positive temp basal insulin:", + "Negative temp basal insulin:": "Negative temp basal insulin:", + "Total basal insulin:": "Total basal insulin:", + "Total daily insulin:": "Total daily insulin:", + "Unable to save Role": "Unable to save Role", + "Unable to delete Role": "Unable to delete Role", + "Database contains %1 roles": "Database contains %1 roles", + "Edit Role": "Edit Role", + "admin, school, family, etc": "admin, school, family, etc", + "Permissions": "Permissions", + "Are you sure you want to delete: ": "Are you sure you want to delete: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.", + "Add new Role": "Add new Role", + "Roles - Groups of People, Devices, etc": "Roles - Groups of People, Devices, etc", + "Edit this role": "Edit this role", + "Admin authorized": "Admin authorized", + "Subjects - People, Devices, etc": "Subjects - People, Devices, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.", + "Add new Subject": "Add new Subject", + "Unable to save Subject": "Unable to save Subject", + "Unable to delete Subject": "Unable to delete Subject", + "Database contains %1 subjects": "Database contains %1 subjects", + "Edit Subject": "Edit Subject", + "person, device, etc": "person, device, etc", + "role1, role2": "role1, role2", + "Edit this subject": "Edit this subject", + "Delete this subject": "Delete this subject", + "Roles": "Roles", + "Access Token": "Access Token", + "hour ago": "hour ago", + "hours ago": "hours ago", + "Silence for %1 minutes": "Silence for %1 minutes", + "Check BG": "Check BG", + "BASAL": "BASAL", + "Current basal": "Current basal", + "Sensitivity": "Sensitivity", + "Current Carb Ratio": "Current Carb Ratio", + "Basal timezone": "Basal timezone", + "Active profile": "Active profile", + "Active temp basal": "Active temp basal", + "Active temp basal start": "Active temp basal start", + "Active temp basal duration": "Active temp basal duration", + "Active temp basal remaining": "Active temp basal remaining", + "Basal profile value": "Basal profile value", + "Active combo bolus": "Active combo bolus", + "Active combo bolus start": "Active combo bolus start", + "Active combo bolus duration": "Active combo bolus duration", + "Active combo bolus remaining": "Active combo bolus remaining", + "BG Delta": "BG Delta", + "Elapsed Time": "Elapsed Time", + "Absolute Delta": "Absolute Delta", + "Interpolated": "Interpolated", + "BWP": "BWP", + "Urgent": "Urgent", + "Warning": "Warning", + "Info": "Info", + "Lowest": "Lowest", + "Snoozing high alarm since there is enough IOB": "Snoozing high alarm since there is enough IOB", + "Check BG, time to bolus?": "Check BG, time to bolus?", + "Notice": "Notice", + "required info missing": "required info missing", + "Insulin on Board": "Insulin on Board", + "Current target": "Current target", + "Expected effect": "Expected effect", + "Expected outcome": "Expected outcome", + "Carb Equivalent": "Carb Equivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U reduction needed in active insulin to reach low target, too much basal?", + "basal adjustment out of range, give carbs?": "basal adjustment out of range, give carbs?", + "basal adjustment out of range, give bolus?": "basal adjustment out of range, give bolus?", + "above high": "above high", + "below low": "below low", + "Projected BG %1 target": "Projected BG %1 target", + "aiming at": "aiming at", + "Bolus %1 units": "Bolus %1 units", + "or adjust basal": "or adjust basal", + "Check BG using glucometer before correcting!": "Check BG using glucometer before correcting!", + "Basal reduction to account %1 units:": "Basal reduction to account %1 units:", + "30m temp basal": "30m temp basal", + "1h temp basal": "1h temp basal", + "Cannula change overdue!": "Cannula change overdue!", + "Time to change cannula": "Time to change cannula", + "Change cannula soon": "Change cannula soon", + "Cannula age %1 hours": "Cannula age %1 hours", + "Inserted": "Inserted", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Last Carbs", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Insulin reservoir change overdue!", + "Time to change insulin reservoir": "Time to change insulin reservoir", + "Change insulin reservoir soon": "Change insulin reservoir soon", + "Insulin reservoir age %1 hours": "Insulin reservoir age %1 hours", + "Changed": "Changed", + "IOB": "IOB", + "Careportal IOB": "Careportal IOB", + "Last Bolus": "Last Bolus", + "Basal IOB": "Basal IOB", + "Source": "Source", + "Stale data, check rig?": "Stale data, check rig?", + "Last received:": "Last received:", + "%1m ago": "%1m ago", + "%1h ago": "%1h ago", + "%1d ago": "%1d ago", + "RETRO": "RETRO", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Sensor change/restart overdue!", + "Time to change/restart sensor": "Time to change/restart sensor", + "Change/restart sensor soon": "Change/restart sensor soon", + "Sensor age %1 days %2 hours": "Sensor age %1 days %2 hours", + "Sensor Insert": "Sensor Insert", + "Sensor Start": "Sensor Start", + "days": "days", + "Insulin distribution": "Insulin distribution", + "To see this report, press SHOW while in this view": "To see this report, press SHOW while in this view", + "AR2 Forecast": "AR2 Forecast", + "OpenAPS Forecasts": "OpenAPS Forecasts", + "Temporary Target": "Temporary Target", + "Temporary Target Cancel": "Temporary Target Cancel", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profiles", + "Time in fluctuation": "Time in fluctuation", + "Time in rapid fluctuation": "Time in rapid fluctuation", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:", + "Filter by hours": "Filter by hours", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">can be found here.", + "Mean Total Daily Change": "Mean Total Daily Change", + "Mean Hourly Change": "Mean Hourly Change", + "FortyFiveDown": "slightly dropping", + "FortyFiveUp": "slightly rising", + "Flat": "holding", + "SingleUp": "rising", + "SingleDown": "dropping", + "DoubleDown": "rapidly dropping", + "DoubleUp": "rapidly rising", + "virtAsstUnknown": "That value is unknown at the moment. Please see your Nightscout site for more details.", + "virtAsstTitleAR2Forecast": "AR2 Forecast", + "virtAsstTitleCurrentBasal": "Current Basal", + "virtAsstTitleCurrentCOB": "Current COB", + "virtAsstTitleCurrentIOB": "Current IOB", + "virtAsstTitleLaunch": "Welcome to Nightscout", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Forecast", + "virtAsstTitlePumpReservoir": "Insulin Remaining", + "virtAsstTitlePumpBattery": "Pump Battery", + "virtAsstTitleRawBG": "Current Raw BG", + "virtAsstTitleUploaderBattery": "Uploader Battery", + "virtAsstTitleCurrentBG": "Current BG", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "CGM Mode", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM Transmitter Age", + "virtAsstTitleCGMNoise": "CGM Noise", + "virtAsstTitleDelta": "Blood Glucose Delta", + "virtAsstStatus": "%1 and %2 as of %3.", + "virtAsstBasal": "%1 current basal is %2 units per hour", + "virtAsstBasalTemp": "%1 temp basal of %2 units per hour will end %3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "You have %1 insulin on board", + "virtAsstIobUnits": "%1 units of", + "virtAsstLaunch": "What would you like to check on Nightscout?", + "virtAsstPreamble": "Your", + "virtAsstPreamble3person": "%1 has a ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "Your pump battery is at %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "I'm sorry, I don't know what you're asking for.", + "Fat [g]": "Fat [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energy [kJ]", + "Clock Views:": "Clock Views:", + "Clock": "Clock", + "Color": "Color", + "Simple": "Simple", + "TDD average": "TDD average", + "Bolus average": "Bolus average", + "Basal average": "Basal average", + "Base basal average:": "Base basal average:", + "Carbs average": "Carbs average", + "Eating Soon": "Eating Soon", + "Last entry {0} minutes ago": "Last entry {0} minutes ago", + "change": "change", + "Speech": "Speech", + "Target Top": "Target Top", + "Target Bottom": "Target Bottom", + "Canceled": "Canceled", + "Meter BG": "Meter BG", + "predicted": "predicted", + "future": "future", + "ago": "ago", + "Last data received": "Last data received", + "Clock View": "Clock View", + "Protein": "Protein", + "Fat": "Fat", + "Protein average": "Protein average", + "Fat average": "Fat average", + "Total carbs": "Total carbs", + "Total protein": "Total protein", + "Total fat": "Total fat", + "Database Size": "Database Size", + "Database Size near its limits!": "Database Size near its limits!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "Database file size", + "%1 MiB of %2 MiB (%3%)": "%1 MiB of %2 MiB (%3%)", + "Data size": "Data size", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "Database file size", + "Carbs/Food/Time": "Carbs/Food/Time", + "You have administration messages": "You have administration messages", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "Data reads enabled", + "Data writes enabled": "Data writes enabled", + "Data writes not enabled": "Data writes not enabled", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "Check for Updates", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "Previous", + "Previous day": "Previous day", + "Next day": "Next day", + "Next": "Next", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/translations/hr_HR.json b/translations/hr_HR.json new file mode 100644 index 00000000000..0ef7223a18f --- /dev/null +++ b/translations/hr_HR.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Slušanje na portu", + "Mo": "Pon", + "Tu": "Uto", + "We": "Sri", + "Th": "Čet", + "Fr": "Pet", + "Sa": "Sub", + "Su": "Ned", + "Monday": "Ponedjeljak", + "Tuesday": "Utorak", + "Wednesday": "Srijeda", + "Thursday": "Četvrtak", + "Friday": "Petak", + "Saturday": "Subota", + "Sunday": "Nedjelja", + "Category": "Kategorija", + "Subcategory": "Podkategorija", + "Name": "Ime", + "Today": "Danas", + "Last 2 days": "Posljednja 2 dana", + "Last 3 days": "Posljednja 3 dana", + "Last week": "Protekli tjedan", + "Last 2 weeks": "Protekla 2 tjedna", + "Last month": "Protekli mjesec", + "Last 3 months": "Protekla 3 mjeseca", + "From": "Od", + "To": "Do", + "Notes": "Bilješke", + "Food": "Hrana", + "Insulin": "Inzulin", + "Carbs": "Ugljikohidrati", + "Notes contain": "Sadržaj bilješki", + "Target BG range bottom": "Ciljna donja granica GUK-a", + "top": "Gornja", + "Show": "Prikaži", + "Display": "Prikaži", + "Loading": "Učitavanje", + "Loading profile": "Učitavanje profila", + "Loading status": "Učitavanje statusa", + "Loading food database": "Učitavanje baze podataka o hrani", + "not displayed": "Ne prikazuje se", + "Loading CGM data of": "Učitavanja podataka CGM-a", + "Loading treatments data of": "Učitavanje podataka o tretmanu", + "Processing data of": "Obrada podataka", + "Portion": "Dio", + "Size": "Veličina", + "(none)": "(Prazno)", + "None": "Prazno", + "": "", + "Result is empty": "Prazan rezultat", + "Day to day": "Svakodnevno", + "Week to week": "Tjedno", + "Daily Stats": "Dnevna statistika", + "Percentile Chart": "Percentili", + "Distribution": "Distribucija", + "Hourly stats": "Statistika po satu", + "netIOB stats": "netIOB statistika", + "temp basals must be rendered to display this report": "temp bazali moraju biti prikazani kako bi se vidio ovaj izvještaj", + "No data available": "Nema raspoloživih podataka", + "Low": "Nizak", + "In Range": "U rasponu", + "Period": "Razdoblje", + "High": "Visok", + "Average": "Prosjek", + "Low Quartile": "Donji kvartil", + "Upper Quartile": "Gornji kvartil", + "Quartile": "Kvartil", + "Date": "Datum", + "Normal": "Normalno", + "Median": "Srednje", + "Readings": "Vrijednosti", + "StDev": "Standardna devijacija", + "Daily stats report": "Izvješće o dnevnim statistikama", + "Glucose Percentile report": "Izvješće o postotku GUK-a", + "Glucose distribution": "Distribucija GUK-a", + "days total": "ukupno dana", + "Total per day": "Ukupno po danu", + "Overall": "Sveukupno", + "Range": "Raspon", + "% of Readings": "% očitanja", + "# of Readings": "broj očitanja", + "Mean": "Prosjek", + "Standard Deviation": "Standardna devijacija", + "Max": "Maksimalan", + "Min": "Minimalan", + "A1c estimation*": "Procjena HbA1c-a", + "Weekly Success": "Tjedni uspjeh", + "There is not sufficient data to run this report. Select more days.": "Nema dovoljno podataka za izvođenje izvještaja. Odaberite još dana.", + "Using stored API secret hash": "Koristi se pohranjeni API tajni hash", + "No API secret hash stored yet. You need to enter API secret.": "Nema pohranjenog API tajnog hasha. Unesite tajni API", + "Database loaded": "Baza podataka je učitana", + "Error: Database failed to load": "Greška: Baza podataka nije učitana", + "Error": "Greška", + "Create new record": "Kreiraj novi zapis", + "Save record": "Spremi zapis", + "Portions": "Dijelovi", + "Unit": "Jedinica", + "GI": "GI", + "Edit record": "Uredi zapis", + "Delete record": "Izbriši zapis", + "Move to the top": "Premjesti na vrh", + "Hidden": "Skriveno", + "Hide after use": "Sakrij nakon korištenja", + "Your API secret must be at least 12 characters long": "Vaš tajni API mora sadržavati barem 12 znakova", + "Bad API secret": "Neispravan tajni API", + "API secret hash stored": "API tajni hash je pohranjen", + "Status": "Status", + "Not loaded": "Nije učitano", + "Food Editor": "Editor hrane", + "Your database": "Vaša baza podataka", + "Filter": "Filter", + "Save": "Spremi", + "Clear": "Očisti", + "Record": "Zapis", + "Quick picks": "Brzi izbor", + "Show hidden": "Prikaži skriveno", + "Your API secret or token": "Vaš API tajni ključ ili token", + "Remember this device. (Do not enable this on public computers.)": "Zapamti ovaj uređaj. (Ne uključujte ovo na javnim računalima.)", + "Treatments": "Tretmani", + "Time": "Vrijeme", + "Event Type": "Vrsta događaja", + "Blood Glucose": "GUK", + "Entered By": "Unos izvršio", + "Delete this treatment?": "Izbriši ovaj tretman?", + "Carbs Given": "Količina UGH", + "Insulin Given": "Količina iznulina", + "Event Time": "Vrijeme događaja", + "Please verify that the data entered is correct": "Molim Vas provjerite jesu li uneseni podaci ispravni", + "BG": "GUK", + "Use BG correction in calculation": "Koristi korekciju GUK-a u izračunu", + "BG from CGM (autoupdated)": "GUK sa CGM-a (ažuriran automatski)", + "BG from meter": "GUK s glukometra", + "Manual BG": "Ručno unesen GUK", + "Quickpick": "Brzi izbor", + "or": "ili", + "Add from database": "Dodaj iz baze podataka", + "Use carbs correction in calculation": "Koristi korekciju za UH u izračunu", + "Use COB correction in calculation": "Koristi aktivne UGH u izračunu", + "Use IOB in calculation": "Koristi aktivni inzulin u izračunu", + "Other correction": "Druga korekcija", + "Rounding": "Zaokruživanje", + "Enter insulin correction in treatment": "Unesi korekciju inzulinom u tretman", + "Insulin needed": "Potrebno inzulina", + "Carbs needed": "Potrebno UGH", + "Carbs needed if Insulin total is negative value": "Potrebno UGH ako je ukupna vrijednost inzulina negativna", + "Basal rate": "Bazal", + "60 minutes earlier": "Prije 60 minuta", + "45 minutes earlier": "Prije 45 minuta", + "30 minutes earlier": "Prije 30 minuta", + "20 minutes earlier": "Prije 20 minuta", + "15 minutes earlier": "Prije 15 minuta", + "Time in minutes": "Vrijeme u minutama", + "15 minutes later": "15 minuta kasnije", + "20 minutes later": "20 minuta kasnije", + "30 minutes later": "30 minuta kasnije", + "45 minutes later": "45 minuta kasnije", + "60 minutes later": "60 minuta kasnije", + "Additional Notes, Comments": "Dodatne bilješke, komentari", + "RETRO MODE": "Retrospektivni način", + "Now": "Sad", + "Other": "Drugo", + "Submit Form": "Spremi", + "Profile Editor": "Editor profila", + "Reports": "Izvještaji", + "Add food from your database": "Dodajte hranu iz svoje baze podataka", + "Reload database": "Ponovo učitajte bazu podataka", + "Add": "Dodaj", + "Unauthorized": "Neautorizirano", + "Entering record failed": "Neuspjeli unos podataka", + "Device authenticated": "Uređaj autenticiran", + "Device not authenticated": "Uređaj nije autenticiran", + "Authentication status": "Status autentikacije", + "Authenticate": "Autenticirati", + "Remove": "Ukloniti", + "Your device is not authenticated yet": "Vaš uređaj još nije autenticiran", + "Sensor": "Senzor", + "Finger": "Prst", + "Manual": "Ručno", + "Scale": "Skala", + "Linear": "Linearno", + "Logarithmic": "Logaritamski", + "Logarithmic (Dynamic)": "Logaritamski (Dinamički)", + "Insulin-on-Board": "Aktivni inzulin", + "Carbs-on-Board": "Aktivni ugljikohidrati", + "Bolus Wizard Preview": "Pregled bolus čarobnjaka", + "Value Loaded": "Vrijednost učitana", + "Cannula Age": "Starost kanile", + "Basal Profile": "Bazalni profil", + "Silence for 30 minutes": "Tišina 30 minuta", + "Silence for 60 minutes": "Tišina 60 minuta", + "Silence for 90 minutes": "Tišina 90 minuta", + "Silence for 120 minutes": "Tišina 120 minuta", + "Settings": "Postavke", + "Units": "Jedinice", + "Date format": "Format datuma", + "12 hours": "12 sati", + "24 hours": "24 sata", + "Log a Treatment": "Evidencija tretmana", + "BG Check": "Kontrola GUK-a", + "Meal Bolus": "Bolus za obrok", + "Snack Bolus": "Bolus za užinu", + "Correction Bolus": "Korekcija", + "Carb Correction": "Bolus za hranu", + "Note": "Bilješka", + "Question": "Pitanje", + "Exercise": "Aktivnost", + "Pump Site Change": "Promjena seta", + "CGM Sensor Start": "Start senzora", + "CGM Sensor Stop": "Zaustavi CGM senzor", + "CGM Sensor Insert": "Promjena senzora", + "Sensor Code": "Sensor Code", + "Transmitter ID": "Transmitter ID", + "Dexcom Sensor Start": "Start Dexcom senzora", + "Dexcom Sensor Change": "Promjena Dexcom senzora", + "Insulin Cartridge Change": "Promjena spremnika inzulina", + "D.A.D. Alert": "Obavijest dijabetičkog psa", + "Glucose Reading": "Vrijednost GUK-a", + "Measurement Method": "Metoda mjerenja", + "Meter": "Glukometar", + "Amount in grams": "Količina u gramima", + "Amount in units": "Količina u jedinicama", + "View all treatments": "Prikaži sve tretmane", + "Enable Alarms": "Aktiviraj alarme", + "Pump Battery Change": "Zamjena baterije pumpe", + "Pump Battery Age": "Pump Battery Age", + "Pump Battery Low Alarm": "Upozorenje slabe baterije pumpe", + "Pump Battery change overdue!": "Prošao je rok za zamjenu baterije pumpe!", + "When enabled an alarm may sound.": "Kad je aktiviran, alarm se može oglasiti", + "Urgent High Alarm": "Hitni alarm za hiper", + "High Alarm": "Alarm za hiper", + "Low Alarm": "Alarm za hipo", + "Urgent Low Alarm": "Hitni alarm za hipo", + "Stale Data: Warn": "Pažnja: Stari podaci", + "Stale Data: Urgent": "Hitno: Stari podaci", + "mins": "min", + "Night Mode": "Noćni način", + "When enabled the page will be dimmed from 10pm - 6am.": "Kad je uključen, stranica će biti zatamnjena od 22-06", + "Enable": "Aktiviraj", + "Show Raw BG Data": "Prikazuj sirove podatke o GUK-u", + "Never": "Nikad", + "Always": "Uvijek", + "When there is noise": "Kad postoji šum", + "When enabled small white dots will be displayed for raw BG data": "Kad je omogućeno, male bijele točkice će prikazivati sirove podatke o GUK-u.", + "Custom Title": "Vlastiti naziv", + "Theme": "Tema", + "Default": "Predefinirano", + "Colors": "Boje", + "Colorblind-friendly colors": "Boje za daltoniste", + "Reset, and use defaults": "Resetiraj i koristi defaultne vrijednosti", + "Calibrations": "Kalibriranje", + "Alarm Test / Smartphone Enable": "Alarm test / Aktiviraj smartphone", + "Bolus Wizard": "Bolus wizard", + "in the future": "U budućnosti", + "time ago": "prije", + "hr ago": "sat unazad", + "hrs ago": "sati unazad", + "min ago": "minuta unazad", + "mins ago": "minuta unazad", + "day ago": "dan unazad", + "days ago": "dana unazad", + "long ago": "prije dosta vremena", + "Clean": "Čisto", + "Light": "Lagano", + "Medium": "Srednje", + "Heavy": "Teško", + "Treatment type": "Vrsta tretmana", + "Raw BG": "Sirovi podaci o GUK-u", + "Device": "Uređaj", + "Noise": "Šum", + "Calibration": "Kalibriranje", + "Show Plugins": "Prikaži plugine", + "About": "O aplikaciji", + "Value in": "Vrijednost u", + "Carb Time": "Vrijeme unosa UGH", + "Language": "Jezik", + "Add new": "Dodaj novi", + "g": "g", + "ml": "ml", + "pcs": "kom", + "Drag&drop food here": "Privuci hranu ovdje", + "Care Portal": "Care Portal", + "Medium/Unknown": "Srednji/Nepoznat", + "IN THE FUTURE": "U BUDUĆNOSTI", + "Order": "Sortiranje", + "oldest on top": "najstarije na vrhu", + "newest on top": "najnovije na vrhu", + "All sensor events": "Svi događaji senzora", + "Remove future items from mongo database": "Obriši buduće zapise iz baze podataka", + "Find and remove treatments in the future": "Nađi i obriši tretmane u budućnosti", + "This task find and remove treatments in the future.": "Ovo nalazi i briše tretmane u budućnosti", + "Remove treatments in the future": "Obriši tretmane u budućnosti", + "Find and remove entries in the future": "Nađi i obriši zapise u budućnosti", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Ovo nalazi i briše podatke sa senzora u budućnosti nastalih s uploaderom sa krivim datumom/vremenom", + "Remove entries in the future": "Obriši zapise u budućnosti", + "Loading database ...": "Učitavanje podataka", + "Database contains %1 future records": "Baza sadrži %1 zapisa u budućnosti", + "Remove %1 selected records?": "Obriši %1 odabrani zapis?", + "Error loading database": "Greška pri učitavanju podataka", + "Record %1 removed ...": "Zapis %1 obrisan...", + "Error removing record %1": "Greška prilikom brisanja zapisa %1", + "Deleting records ...": "Brisanje zapisa", + "%1 records deleted": "obrisano %1 zapisa", + "Clean Mongo status database": "Obriši bazu statusa", + "Delete all documents from devicestatus collection": "Obriši sve zapise o statusima", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Ovo briše sve zapise o statusima. Korisno kada se status baterije uploadera ne osvježava ispravno.", + "Delete all documents": "Obriši sve zapise", + "Delete all documents from devicestatus collection?": "Obriši sve zapise statusa?", + "Database contains %1 records": "Baza sadrži %1 zapisa", + "All records removed ...": "Svi zapisi obrisani", + "Delete all documents from devicestatus collection older than 30 days": "Obriši sve statuse starije od 30 dana", + "Number of Days to Keep:": "Broj dana za sačuvati:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Ovo uklanja sve statuse starije od 30 dana. Korisno kada se status baterije uploadera ne osvježava ispravno.", + "Delete old documents from devicestatus collection?": "Obriši stare statuse", + "Clean Mongo entries (glucose entries) database": "Obriši GUK zapise iz baze", + "Delete all documents from entries collection older than 180 days": "Obriši sve zapise starije od 180 dana", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Ovo briše sve zapise starije od 180 dana. Korisno kada se status baterije uploadera ne osvježava.", + "Delete old documents": "Obriši stare zapise", + "Delete old documents from entries collection?": "Obriši stare zapise?", + "%1 is not a valid number": "%1 nije valjan broj", + "%1 is not a valid number - must be more than 2": "%1 nije valjan broj - mora biti veći od 2", + "Clean Mongo treatments database": "Obriši tretmane iz baze", + "Delete all documents from treatments collection older than 180 days": "Obriši tretmane starije od 180 dana iz baze", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Ovo briše sve tretmane starije od 180 dana iz baze. Korisno kada se status baterije uploadera ne osvježava.", + "Delete old documents from treatments collection?": "Obriši stare tretmane?", + "Admin Tools": "Administracija", + "Nightscout reporting": "Nightscout izvješća", + "Cancel": "Odustani", + "Edit treatment": "Uredi tretman", + "Duration": "Trajanje", + "Duration in minutes": "Trajanje u minutama", + "Temp Basal": "Privremeni bazal", + "Temp Basal Start": "Početak privremenog bazala", + "Temp Basal End": "Kraj privremenog bazala", + "Percent": "Postotak", + "Basal change in %": "Promjena bazala u %", + "Basal value": "Vrijednost bazala", + "Absolute basal value": "Apsolutna vrijednost bazala", + "Announcement": "Objava", + "Loading temp basal data": "Učitavanje podataka o privremenom bazalu", + "Save current record before changing to new?": "Spremi trenutni zapis prije promjene na idući?", + "Profile Switch": "Promjena profila", + "Profile": "Profil", + "General profile settings": "Opće postavke profila", + "Title": "Naslov", + "Database records": "Zapisi u bazi", + "Add new record": "Dodaj zapis", + "Remove this record": "Obriši ovaj zapis", + "Clone this record to new": "Kloniraj zapis", + "Record valid from": "Zapis vrijedi od", + "Stored profiles": "Pohranjeni profili", + "Timezone": "Vremenska zona", + "Duration of Insulin Activity (DIA)": "Trajanje aktivnosti inzulina (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Predstavlja uobičajeno trajanje djelovanje inzulina. Varira po vrstama inzulina i osobama. Tipično je to 3-4 sata za inzuline u pumpama za većinu osoba. Ponekad se naziva i vijek inzulina", + "Insulin to carb ratio (I:C)": "Omjer UGH:Inzulin (I:C)", + "Hours:": "Sati:", + "hours": "sati", + "g/hour": "g/h", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "grama UGH po jedinici inzulina. Omjer koliko grama UGH pokriva jedna jedinica inzulina.", + "Insulin Sensitivity Factor (ISF)": "Faktor inzulinske osjetljivosti (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "md/dL ili mmol/L po jedinici inzulina. Omjer koliko se GUK mijenja sa jednom jedinicom korekcije inzulina.", + "Carbs activity / absorption rate": "UGH aktivnost / omjer apsorpcije", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grama po jedinici vremena. Predstavlja promjenu aktivnih UGH u jedinici vremena, kao i količinu UGH koja bi trebala utjecati kroz to vrijeme.", + "Basal rates [unit/hour]": "Bazali [jedinica/sat]", + "Target BG range [mg/dL,mmol/L]": "Ciljani raspon GUK [mg/dL,mmol/L]", + "Start of record validity": "Trenutak važenja zapisa", + "Icicle": "Padajuće", + "Render Basal": "Iscrtaj bazale", + "Profile used": "Korišteni profil", + "Calculation is in target range.": "Izračun je u ciljanom rasponu.", + "Loading profile records ...": "Učitavanje profila...", + "Values loaded.": "Vrijednosti učitane.", + "Default values used.": "Koriste se zadane vrijednosti.", + "Error. Default values used.": "Pogreška. Koristiti će se zadane vrijednosti.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Vremenski rasponi donje ciljane i gornje ciljane vrijednosti nisu ispravni. Vrijednosti vraćene na zadano.", + "Valid from:": "Vrijedi od:", + "Save current record before switching to new?": "Spremi trenutni zapis prije prelaska na novi?", + "Add new interval before": "Dodaj novi interval iznad", + "Delete interval": "Obriši interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Dual bolus", + "Difference": "Razlika", + "New time": "Novo vrijeme", + "Edit Mode": "Uređivanje", + "When enabled icon to start edit mode is visible": "Kada je omogućeno, mod uređivanje je omogućen", + "Operation": "Operacija", + "Move": "Pomakni", + "Delete": "Obriši", + "Move insulin": "Premjesti inzulin", + "Move carbs": "Premejsti UGH", + "Remove insulin": "Obriši inzulin", + "Remove carbs": "Obriši UGH", + "Change treatment time to %1 ?": "Promijeni vrijeme tretmana na %1?", + "Change carbs time to %1 ?": "Promijeni vrijeme UGH na %1?", + "Change insulin time to %1 ?": "Promijeni vrijeme inzulina na %1?", + "Remove treatment ?": "Obriši tretman?", + "Remove insulin from treatment ?": "Obriši inzulin iz tretmana?", + "Remove carbs from treatment ?": "Obriši UGH iz tretmana?", + "Rendering": "Iscrtavanje", + "Loading OpenAPS data of": "Učitavanje OpenAPS podataka od", + "Loading profile switch data": "Učitavanje podataka promjene profila", + "Redirecting you to the Profile Editor to create a new profile.": "Krive postavke profila.\nNiti jedan profil nije definiran za prikazano vrijeme.\nPreusmjeravanje u editor profila kako biste stvorili novi.", + "Pump": "Pumpa", + "Sensor Age": "Starost senzora", + "Insulin Age": "Starost inzulina", + "Temporary target": "Privremeni cilj", + "Reason": "Razlog", + "Eating soon": "Uskoro jelo", + "Top": "Vrh", + "Bottom": "Dno", + "Activity": "Aktivnost", + "Targets": "Ciljevi", + "Bolus insulin:": "Bolus:", + "Base basal insulin:": "Osnovni bazal:", + "Positive temp basal insulin:": "Pozitivni temp bazal:", + "Negative temp basal insulin:": "Negativni temp bazal:", + "Total basal insulin:": "Ukupno bazali:", + "Total daily insulin:": "Ukupno dnevni inzulin:", + "Unable to save Role": "Nije moguće spremanje", + "Unable to delete Role": "Ne mogu obrisati ulogu", + "Database contains %1 roles": "baza sadrži %1 uloga", + "Edit Role": "Uredi ulogu", + "admin, school, family, etc": "admin, škola, obitelj, itd.", + "Permissions": "Prava", + "Are you sure you want to delete: ": "Sigurno želite obrisati?", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Svaka uloga ima 1 ili više prava. Pravo * je univerzalno, a prava su hijerarhija koja koristi znak : kao graničnik.", + "Add new Role": "Dodaj novu ulogu", + "Roles - Groups of People, Devices, etc": "Uloge - Grupe ljudi, uređaja, itd.", + "Edit this role": "Uredi ovu ulogu", + "Admin authorized": "Administrator ovlašten", + "Subjects - People, Devices, etc": "Subjekti - Ljudi, uređaji, itd.", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Svaki subjekt će dobiti jedinstveni pristupni token i jednu ili više uloga. Kliknite na pristupni token kako bi se otvorio novi pogled sa odabranim subjektom, a tada se tajni link može podijeliti.", + "Add new Subject": "Dodaj novi subjekt", + "Unable to save Subject": "Nije moguće spremiti Predmet", + "Unable to delete Subject": "Ne mogu obrisati subjekt", + "Database contains %1 subjects": "Baza sadrži %1 subjekata", + "Edit Subject": "Uredi subjekt", + "person, device, etc": "osoba, uređaj, itd.", + "role1, role2": "uloga1, uloga2", + "Edit this subject": "Uredi ovaj subjekt", + "Delete this subject": "Obriši ovaj subjekt", + "Roles": "Uloge", + "Access Token": "Pristupni token", + "hour ago": "sat ranije", + "hours ago": "sati ranije", + "Silence for %1 minutes": "Utišaj na %1 minuta", + "Check BG": "Provjeri GUK", + "BASAL": "Bazal", + "Current basal": "Trenutni bazal", + "Sensitivity": "Osjetljivost", + "Current Carb Ratio": "Trenutni I:C", + "Basal timezone": "Vremenska zona bazala", + "Active profile": "Aktivni profil", + "Active temp basal": "Aktivni temp bazal", + "Active temp basal start": "Početak aktivnog tamp bazala", + "Active temp basal duration": "Trajanje aktivnog temp bazala", + "Active temp basal remaining": "Prestali aktivni temp bazal", + "Basal profile value": "Profilna vrijednost bazala", + "Active combo bolus": "Aktivni dual bolus", + "Active combo bolus start": "Početak aktivnog dual bolusa", + "Active combo bolus duration": "Trajanje aktivnog dual bolusa", + "Active combo bolus remaining": "Preostali aktivni dual bolus", + "BG Delta": "GUK razlika", + "Elapsed Time": "Proteklo vrijeme", + "Absolute Delta": "Apsolutna razlika", + "Interpolated": "Interpolirano", + "BWP": "Čarobnjak bolusa", + "Urgent": "Hitno", + "Warning": "Upozorenje", + "Info": "Info", + "Lowest": "Najniže", + "Snoozing high alarm since there is enough IOB": "Stišan alarm za hiper pošto ima dovoljno aktivnog inzulina", + "Check BG, time to bolus?": "Provjeri GUK, vrijeme je za bolus?", + "Notice": "Poruka", + "required info missing": "nedostaju potrebne informacije", + "Insulin on Board": "Aktivni inzulín", + "Current target": "Trenutni cilj", + "Expected effect": "Očekivani efekt", + "Expected outcome": "Očekivani ishod", + "Carb Equivalent": "Ekvivalent u UGH", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Višak inzulina je %1U više nego li je potrebno da se postigne donja ciljana granica, ne uzevši u obzir UGH", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Višak inzulina je %1U više nego li je potrebno da se postigne donja ciljana granica, OBAVEZNO POKRIJTE SA UGH", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Potrebno je smanjiti aktivni inzulin za %1U kako bi se dostigla donja ciljana granica, previše bazala? ", + "basal adjustment out of range, give carbs?": "prilagodba bazala je izvan raspona, dodati UGH?", + "basal adjustment out of range, give bolus?": "prilagodna bazala je izvan raspona, dati bolus?", + "above high": "iznad gornje granice", + "below low": "ispod donje granice", + "Projected BG %1 target": "Procjena GUK %1 cilja", + "aiming at": "ciljano", + "Bolus %1 units": "Bolus %1 jedinica", + "or adjust basal": "ili prilagodba bazala", + "Check BG using glucometer before correcting!": "Provjeri GUK glukometrom prije korekcije!", + "Basal reduction to account %1 units:": "Smanjeni bazal da uračuna %1 jedinica:", + "30m temp basal": "30m temp bazal", + "1h temp basal": "1h temp bazal", + "Cannula change overdue!": "Prošao rok za zamjenu kanile!", + "Time to change cannula": "Vrijeme za zamjenu kanile", + "Change cannula soon": "Zamijena kanile uskoro", + "Cannula age %1 hours": "Staros kanile %1 sati", + "Inserted": "Postavljanje", + "CAGE": "Starost kanile", + "COB": "Aktivni UGH", + "Last Carbs": "Posljednji UGH", + "IAGE": "Starost inzulina", + "Insulin reservoir change overdue!": "Prošao rok za zamjenu spremnika!", + "Time to change insulin reservoir": "Vrijeme za zamjenu spremnika", + "Change insulin reservoir soon": "Zamjena spremnika uskoro", + "Insulin reservoir age %1 hours": "Spremnik zamijenjen prije %1 sati", + "Changed": "Promijenjeno", + "IOB": "Aktivni inzulin", + "Careportal IOB": "Careportal IOB", + "Last Bolus": "Prethodni bolus", + "Basal IOB": "Bazalni aktivni inzulin", + "Source": "Izvor", + "Stale data, check rig?": "Nedostaju podaci, provjera opreme?", + "Last received:": "Zadnji podaci od:", + "%1m ago": "prije %1m", + "%1h ago": "prije %1 sati", + "%1d ago": "prije %1 dana", + "RETRO": "RETRO", + "SAGE": "Starost senzora", + "Sensor change/restart overdue!": "Prošao rok za zamjenu/restart senzora!", + "Time to change/restart sensor": "Vrijeme za zamjenu/restart senzora", + "Change/restart sensor soon": "Zamijena/restart senzora uskoro", + "Sensor age %1 days %2 hours": "Starost senzora %1 dana i %2 sati", + "Sensor Insert": "Postavljanje senzora", + "Sensor Start": "Pokretanje senzora", + "days": "dana", + "Insulin distribution": "Raspodjela inzulina", + "To see this report, press SHOW while in this view": "Za prikaz ovog izvješća, pritisnite PRIKAŽI na ovom prozoru", + "AR2 Forecast": "AR2 procjena", + "OpenAPS Forecasts": "OpenAPS prognoze", + "Temporary Target": "Privremeni cilj", + "Temporary Target Cancel": "Otkaz privremenog cilja", + "OpenAPS Offline": "OpenAPS odspojen", + "Profiles": "Profili", + "Time in fluctuation": "Vrijeme u fluktuaciji", + "Time in rapid fluctuation": "Vrijeme u brzoj fluktuaciji", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Ovo je samo gruba procjena koja može biti neprecizna i ne mijenja testiranje iz krvi. Formula je uzeta iz:", + "Filter by hours": "Filter po satima", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Vrijeme u fluktuaciji i vrijeme u brzoj fluktuaciji mjere % vremena u gledanom periodu, tijekom kojeg se GUK mijenja relativno brzo ili brzo. Niže vrijednosti su bolje.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Srednja ukupna dnevna promjena je suma apsolutnih vrijednosti svih pomaka u gledanom periodu, podijeljeno s brojem dana. Niže vrijednosti su bolje.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Srednja ukupna promjena po satu je suma apsolutnih vrijednosti svih pomaka u gledanom periodu, podijeljeno s brojem sati. Niže vrijednosti su bolje.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">ovdje.", + "Mean Total Daily Change": "Srednja ukupna dnevna promjena", + "Mean Hourly Change": "Srednja ukupna promjena po satu", + "FortyFiveDown": "sporo padajuće", + "FortyFiveUp": "sporo rastuće", + "Flat": "ravno", + "SingleUp": "rastuće", + "SingleDown": "padajuće", + "DoubleDown": "brzo padajuće", + "DoubleUp": "brzo rastuće", + "virtAsstUnknown": "Ova vrijednost je trenutno nepoznata. Molimo provjerite Nightscout stranicu za više detalja.", + "virtAsstTitleAR2Forecast": "AR2 procjena", + "virtAsstTitleCurrentBasal": "Trenutni Bazal", + "virtAsstTitleCurrentCOB": "Trenutni COB", + "virtAsstTitleCurrentIOB": "Trenutni IOB", + "virtAsstTitleLaunch": "Dobrodošli na Nightscout", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS prognoza", + "virtAsstTitlePumpReservoir": "Preostali inzulin", + "virtAsstTitlePumpBattery": "Baterija pumpe", + "virtAsstTitleRawBG": "Current Raw BG", + "virtAsstTitleUploaderBattery": "Uploader Battery", + "virtAsstTitleCurrentBG": "Current BG", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "CGM Mode", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM Transmitter Age", + "virtAsstTitleCGMNoise": "CGM Noise", + "virtAsstTitleDelta": "Blood Glucose Delta", + "virtAsstStatus": "%1 i %2 od %3.", + "virtAsstBasal": "%1 trenutni bazal je %2 jedinica(e) po satu", + "virtAsstBasalTemp": "%1 temp basal of %2 units per hour will end %3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "You have %1 insulin on board", + "virtAsstIobUnits": "%1 units of", + "virtAsstLaunch": "What would you like to check on Nightscout?", + "virtAsstPreamble": "Vaš", + "virtAsstPreamble3person": "%1 has a ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "Your pump battery is at %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "I'm sorry, I don't know what you're asking for.", + "Fat [g]": "Masnoće [g]", + "Protein [g]": "Proteini [g]", + "Energy [kJ]": "Energija [kJ]", + "Clock Views:": "Satovi:", + "Clock": "Sat", + "Color": "Boja", + "Simple": "Jednostavan", + "TDD average": "Srednji TDD", + "Bolus average": "Bolus average", + "Basal average": "Basal average", + "Base basal average:": "Base basal average:", + "Carbs average": "Prosjek UGH", + "Eating Soon": "Uskoro obrok", + "Last entry {0} minutes ago": "Posljednji zapis prije {0} minuta", + "change": "promjena", + "Speech": "Govor", + "Target Top": "Gornja granica", + "Target Bottom": "Donja granica", + "Canceled": "Otkazano", + "Meter BG": "GUK iz krvi", + "predicted": "prognozirano", + "future": "budućnost", + "ago": "prije", + "Last data received": "Podaci posljednji puta primljeni", + "Clock View": "Prikaz sata", + "Protein": "Proteini", + "Fat": "Masti", + "Protein average": "Prosjek proteina", + "Fat average": "Prosjek masti", + "Total carbs": "Ukupno ugh", + "Total protein": "Ukupno proteini", + "Total fat": "Ukupno masti", + "Database Size": "Database Size", + "Database Size near its limits!": "Database Size near its limits!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "Database file size", + "%1 MiB of %2 MiB (%3%)": "%1 MiB of %2 MiB (%3%)", + "Data size": "Data size", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "Database file size", + "Carbs/Food/Time": "Carbs/Food/Time", + "You have administration messages": "You have administration messages", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "Data reads enabled", + "Data writes enabled": "Data writes enabled", + "Data writes not enabled": "Data writes not enabled", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "Check for Updates", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "Previous", + "Previous day": "Previous day", + "Next day": "Next day", + "Next": "Next", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/translations/hu_HU.json b/translations/hu_HU.json new file mode 100644 index 00000000000..1e2faca8e69 --- /dev/null +++ b/translations/hu_HU.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Port figyelése", + "Mo": "H", + "Tu": "K", + "We": "Sze", + "Th": "Cs", + "Fr": "P", + "Sa": "Szo", + "Su": "V", + "Monday": "Hétfő", + "Tuesday": "Kedd", + "Wednesday": "Szerda", + "Thursday": "Csütörtök", + "Friday": "Péntek", + "Saturday": "Szombat", + "Sunday": "Vasárnap", + "Category": "Kategória", + "Subcategory": "Alkategória", + "Name": "Név", + "Today": "Ma", + "Last 2 days": "Elmúlt 2 nap", + "Last 3 days": "Elmúlt 3 nap", + "Last week": "Elmúlt hét", + "Last 2 weeks": "Elmúlt 2 hét", + "Last month": "Elmúlt hónap", + "Last 3 months": "Elmúlt 3 hónap", + "From": "Ettől", + "To": "Eddig", + "Notes": "Megjegyzések", + "Food": "Étel", + "Insulin": "Inzulin", + "Carbs": "Szénhidrát", + "Notes contain": "Megjegyzés tartalmazza", + "Target BG range bottom": "Céltartomány alja", + "top": "teteje", + "Show": "Megjelenítés", + "Display": "Megjelenítés", + "Loading": "Betöltés", + "Loading profile": "Profil betöltése", + "Loading status": "Állapot betöltése", + "Loading food database": "Étel adatbázis betöltése", + "not displayed": "nincs megjelenítve", + "Loading CGM data of": "CGM adatok betöltése tőle", + "Loading treatments data of": "Kezelési adatok betöltése tőle", + "Processing data of": "Adatok feldolgozása tőle", + "Portion": "Adag", + "Size": "Méret", + "(none)": "(nincs)", + "None": "Kikapcsolva", + "": "", + "Result is empty": "Az eredmény üres", + "Day to day": "Napi", + "Week to week": "Heti", + "Daily Stats": "Napi statisztika", + "Percentile Chart": "Percentilis", + "Distribution": "Eloszlás", + "Hourly stats": "Óránkénti statisztika", + "netIOB stats": "netIOB statisztika", + "temp basals must be rendered to display this report": "Az átmeneti bázisokat meg kell jeleníteni ehhez a jelentéshez", + "No data available": "Nincs elérhető adat", + "Low": "Alacsony", + "In Range": "Tartományon belül", + "Period": "Időszak", + "High": "Magas", + "Average": "Átlag", + "Low Quartile": "Alacsony kvartilis", + "Upper Quartile": "Magas kvartilis", + "Quartile": "Kvartilis", + "Date": "Dátum", + "Normal": "Céltartományban", + "Median": "Medián", + "Readings": "Értékek", + "StDev": "Szórás", + "Daily stats report": "Napi statisztika jelentés", + "Glucose Percentile report": "Szenzorglükóz percentilis jelentés", + "Glucose distribution": "Szenzorglükóz eloszlás", + "days total": "nap összesen", + "Total per day": "Napi összes", + "Overall": "Összesen", + "Range": "Tartomány", + "% of Readings": "Értékek %-a", + "# of Readings": "Értékek száma", + "Mean": "Középérték", + "Standard Deviation": "Szórás", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "HbA1c becslés*", + "Weekly Success": "Heti eloszlás", + "There is not sufficient data to run this report. Select more days.": "Nincs elég adat a jelentés elkészítéséhez. Válassz több napot!", + "Using stored API secret hash": "Az eltárolt API_SECRET hash felhasználásával", + "No API secret hash stored yet. You need to enter API secret.": "Az API_SECRET hash nincs eltárolva. Add meg az API_SECRET-et!", + "Database loaded": "Adatbázis betöltve", + "Error: Database failed to load": "Hiba: adatbázis betöltése sikertelen", + "Error": "Hiba", + "Create new record": "Bejegyzés létrehozása", + "Save record": "Bejegyzés mentése", + "Portions": "Adag", + "Unit": "Mértékegység", + "GI": "GI", + "Edit record": "Bejegyzés szerkesztése", + "Delete record": "Bejegyzés törlése", + "Move to the top": "Mozgasd legfelülre", + "Hidden": "Rejtett", + "Hide after use": "Elrejtés használat után", + "Your API secret must be at least 12 characters long": "Az API_SECRET több mint 12 karakterből kell hogy álljon", + "Bad API secret": "Helytelen API_SECRET", + "API secret hash stored": "API_SECRET hash elmentve", + "Status": "Állapot", + "Not loaded": "Nincs betöltve", + "Food Editor": "Ételszerkesztő", + "Your database": "Saját adatbázis", + "Filter": "Szűrő", + "Save": "Mentés", + "Clear": "Törlés", + "Record": "Bejegyzés", + "Quick picks": "Gyors választások", + "Show hidden": "Rejtettek mutatása", + "Your API secret or token": "Saját API_secret vagy token", + "Remember this device. (Do not enable this on public computers.)": "Eszköz megjegyzése (ne jelöld be nyilvános eszközökön)", + "Treatments": "Kezelések", + "Time": "Idő", + "Event Type": "Esemény típusa", + "Blood Glucose": "Szenzorglükóz", + "Entered By": "Rögzítette", + "Delete this treatment?": "Kezelés törlése?", + "Carbs Given": "Szénhidrát", + "Insulin Given": "Inzulin", + "Event Time": "Esemény időpontja", + "Please verify that the data entered is correct": "Kérlek ellenőrizd az adatok helyességét", + "BG": "SzG", + "Use BG correction in calculation": "Használj SzG korrekciót a számításban", + "BG from CGM (autoupdated)": "SzG a CGM-től (auto. frissítve)", + "BG from meter": "VC a mérőtől", + "Manual BG": "Kézi VC", + "Quickpick": "Gyors választás", + "or": "vagy", + "Add from database": "Hozzáadás adatbázisból", + "Use carbs correction in calculation": "Használj szénhidrát korrekciót a számításban", + "Use COB correction in calculation": "Használj COB korrekciót a számításban", + "Use IOB in calculation": "Használd az IOB-t a számításban", + "Other correction": "Egyéb korrekció", + "Rounding": "Kerekítés", + "Enter insulin correction in treatment": "A korrekciós inzulint a kezeléseknél add meg", + "Insulin needed": "Inzulin szükséges", + "Carbs needed": "Szénhidrát szükséges", + "Carbs needed if Insulin total is negative value": "Szénhidrát szükséges ha a teljes inzulin negatív", + "Basal rate": "Bázisinzulin", + "60 minutes earlier": "60 perccel korábban", + "45 minutes earlier": "45 perccel korábban", + "30 minutes earlier": "30 perccel korábban", + "20 minutes earlier": "20 perccel korábban", + "15 minutes earlier": "15 perccel korábban", + "Time in minutes": "perc-ben", + "15 minutes later": "15 perccel később", + "20 minutes later": "20 perccel később", + "30 minutes later": "30 perccel később", + "45 minutes later": "45 perccel később", + "60 minutes later": "60 perccel később", + "Additional Notes, Comments": "További megjegyzések, kommentárok", + "RETRO MODE": "RETRO MÓD", + "Now": "Most", + "Other": "Egyéb:", + "Submit Form": "Ok", + "Profile Editor": "Profilszerkesztő", + "Reports": "Jelentések", + "Add food from your database": "Étel hozzáadása saját adatbázisból", + "Reload database": "Adatbázis újratöltése", + "Add": "Hozzáad", + "Unauthorized": "Nem hitelesített", + "Entering record failed": "Bejegyzés hozzáadása sikertelen", + "Device authenticated": "Eszköz hitelesítve", + "Device not authenticated": "Nem hitelesített eszköz", + "Authentication status": "Hitelesítés állapota", + "Authenticate": "Hitelesít", + "Remove": "Eltávolít", + "Your device is not authenticated yet": "Az eszköz még nincs hitelesítve", + "Sensor": "Szenzor", + "Finger": "Ujj", + "Manual": "Kézi", + "Scale": "Skála", + "Linear": "Lineáris", + "Logarithmic": "Logaritmikus", + "Logarithmic (Dynamic)": "Logaritmikus (dinamikus)", + "Insulin-on-Board": "Aktív inzulin (IOB)", + "Carbs-on-Board": "Aktív szénhidrát (COB)", + "Bolus Wizard Preview": "Bolus varázsló előnézet", + "Value Loaded": "Érték betöltve", + "Cannula Age": "Kanül kora (KKOR)", + "Basal Profile": "Bázisprofil", + "Silence for 30 minutes": "Némítás 30 percre", + "Silence for 60 minutes": "Némítás 60 percre", + "Silence for 90 minutes": "Némítás 90 percre", + "Silence for 120 minutes": "Némítás 120 percre", + "Settings": "Beállítások", + "Units": "Mértékegység", + "Date format": "Időformátum", + "12 hours": "12 óra", + "24 hours": "24 óra", + "Log a Treatment": "Kezelés rögzítése", + "BG Check": "VC ellenőrzés", + "Meal Bolus": "Étkezési bólus", + "Snack Bolus": "Kis étkezési bólus", + "Correction Bolus": "Korrekciós bólus", + "Carb Correction": "Korrekciós szénhidrát", + "Note": "Megjegyzés", + "Question": "Kérdés", + "Exercise": "Edzés", + "Pump Site Change": "Kanülcsere", + "CGM Sensor Start": "CGM szenzor indítás", + "CGM Sensor Stop": "CGM szenzor leállítás", + "CGM Sensor Insert": "CGM szenzor behelyezés", + "Sensor Code": "Szenzor kód", + "Transmitter ID": "Jeladó ID", + "Dexcom Sensor Start": "Dexcom szenzor indítás", + "Dexcom Sensor Change": "Dexcom szenzor csere", + "Insulin Cartridge Change": "Inzulintartály-csere", + "D.A.D. Alert": "D.A.D riasztás", + "Glucose Reading": "Cukorérték", + "Measurement Method": "Mérés módja", + "Meter": "Vércukormérő", + "Amount in grams": "gramm-ban", + "Amount in units": "Egység-ben", + "View all treatments": "Összes kezelés", + "Enable Alarms": "Riasztások bekapcsolása", + "Pump Battery Change": "Pumpaelem csere", + "Pump Battery Age": "Pumpaelem kora (EKOR)", + "Pump Battery Low Alarm": "Pumpa elem merül riasztás", + "Pump Battery change overdue!": "Pumpa elem cserére szorul!", + "When enabled an alarm may sound.": "hanggal történő riasztások", + "Urgent High Alarm": "Sürgős: magas riasztás", + "High Alarm": "Magas riasztás", + "Low Alarm": "Alacsony riasztás", + "Urgent Low Alarm": "Sürgős: alcsony riasztás", + "Stale Data: Warn": "Elavult adatok", + "Stale Data: Urgent": "Sürgős: elavult adatok", + "mins": "perc", + "Night Mode": "Éjszakai mód", + "When enabled the page will be dimmed from 10pm - 6am.": "alacsonyabb fényerő 22-től 6-ig", + "Enable": "Bekapcsolva", + "Show Raw BG Data": "Nyers SzG értékek megjelenítése", + "Never": "Soha", + "Always": "Mindig", + "When there is noise": "Ha zajos a mérés", + "When enabled small white dots will be displayed for raw BG data": "kis fehér pontok jelzik a nyers SzG értékeket", + "Custom Title": "Egyéni cím", + "Theme": "Téma", + "Default": "Alapértelmezett", + "Colors": "Színes", + "Colorblind-friendly colors": "Színes színvakok számára", + "Reset, and use defaults": "Visszaállítás az alapértelmezett beállításokra", + "Calibrations": "Kalibrációk", + "Alarm Test / Smartphone Enable": "Riasztás teszt / mobil aktiválása", + "Bolus Wizard": "Bólus varázsló", + "in the future": "a jövőben", + "time ago": "idővel ezelőtt", + "hr ago": "órája", + "hrs ago": "órája", + "min ago": "perce", + "mins ago": "perce", + "day ago": "napja", + "days ago": "napja", + "long ago": "nagyon régen", + "Clean": "Tiszta", + "Light": "Könnyű", + "Medium": "Közepes", + "Heavy": "Magas", + "Treatment type": "Kezelés típusa", + "Raw BG": "Nyers SzG", + "Device": "Készülék", + "Noise": "Zaj", + "Calibration": "Kalibráció", + "Show Plugins": "Bővítmények", + "About": "Az alkalmazásról", + "Value in": "Mértékegység", + "Carb Time": "Étkezés ideje", + "Language": "Nyelv", + "Add new": "Új hozzáadása", + "g": "g", + "ml": "ml", + "pcs": "db", + "Drag&drop food here": "Húzd és ejtsd ide az ételt", + "Care Portal": "Careportal", + "Medium/Unknown": "Közepes/ismeretlen", + "IN THE FUTURE": "JÖVŐBEN", + "Order": "Rendezés", + "oldest on top": "legrégebbi fent", + "newest on top": "legújabb fent", + "All sensor events": "Összes szenzor esemény", + "Remove future items from mongo database": "Jövőbeni elemek eltávolítása a Mongo adatbázisból", + "Find and remove treatments in the future": "Jövőbeni kezelések keresése és eltávolítása", + "This task find and remove treatments in the future.": "Jövőbeni kezelések keresése és eltávolítása", + "Remove treatments in the future": "Jövőbeni kezelések eltávolítása", + "Find and remove entries in the future": "Jövőbeni bejegyzések keresése és eltávolítása", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Rossz dátummal és idővel feltöltött SzG adatok keresése és eltávolítása", + "Remove entries in the future": "Jövőbeli bejegyzések törlése", + "Loading database ...": "Adatbázis betöltése ...", + "Database contains %1 future records": "Az adatbázis %1 jövőbeni adatot tartalmaz", + "Remove %1 selected records?": "%1 kiválasztott adat törlése?", + "Error loading database": "Hiba az adatbázis betöltése közben", + "Record %1 removed ...": "%1 bejegyzés törölve ...", + "Error removing record %1": "Hiba a %1 bejegyzés törlése közben", + "Deleting records ...": "Bejegyzések törlése ...", + "%1 records deleted": "%1 bejegyzés törölve", + "Clean Mongo status database": "Mongo állapot (status) adatbázis tisztítása", + "Delete all documents from devicestatus collection": "Összes dokumentum törlése a devicestatus gyűjteményből", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Kitörli az összes dokumentumot a devicestatus gyűjteményből. Hasznos, ha a feltöltő akkumulátorának állapota nincs megfelelően frissítve.", + "Delete all documents": "Összes dokumentum törlése", + "Delete all documents from devicestatus collection?": "Összes dokumentum törlése a devicestatus gyűjteményből?", + "Database contains %1 records": "Az adatbázis %1 bejegyzést tartalmaz", + "All records removed ...": "Összes bejegyzés törölve ...", + "Delete all documents from devicestatus collection older than 30 days": "30 napnál régebbi dokumentumok törlése a devicestatus gyűjteményből", + "Number of Days to Keep:": "Ennyi nap megtartása:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Kitörli a 30 napnál régebbi dokumentumokat a devicestatus gyűjteményből. Hasznos, ha a feltöltő akkumulátorának állapota nincs megfelelően frissítve.", + "Delete old documents from devicestatus collection?": "Régi dokumentumok törlése a devicestatus gyűjteményből?", + "Clean Mongo entries (glucose entries) database": "Mongo adatbázis glükóz bejegyzéseinek tisztítása", + "Delete all documents from entries collection older than 180 days": "180 napnál régebbi dokumentumok törlése a bejegyzések gyűjteményéből", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Kitörli az összes 180 napnál régebbi dokumentumot a bejegyzések gyűjteményéből.", + "Delete old documents": "Régi dokumentumok törlése", + "Delete old documents from entries collection?": "Régi dokumentumok törlése a bejegyzések gyűjteményéből?", + "%1 is not a valid number": "A(z) %1 nem érvényes szám", + "%1 is not a valid number - must be more than 2": "A(z) %1 nem érvényes szám - nagyobb kell legyen, mint 2", + "Clean Mongo treatments database": "Mongo kezelési adatbázis tisztítása", + "Delete all documents from treatments collection older than 180 days": "180 napnál régebbi dokumentumok törlése a kezelések gyűjteményéből", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Kitörli az összes 180 napnál régebbi dokumentumot a kezelések gyűjteményéből.", + "Delete old documents from treatments collection?": "Összes doumentum törlése a kezelési gyűjteményből?", + "Admin Tools": "Adminisztrációs eszközök", + "Nightscout reporting": "Nightscout jelentések", + "Cancel": "Vissza", + "Edit treatment": "Kezelés szerkesztése", + "Duration": "Időtartam", + "Duration in minutes": "Időtartam percekben", + "Temp Basal": "Átmeneti bázis", + "Temp Basal Start": "Átmeneti bázis kezdete", + "Temp Basal End": "Átmeneti bázis vége", + "Percent": "Százalék", + "Basal change in %": "Bázis változása %-ban", + "Basal value": "Bázis értéke", + "Absolute basal value": "Abszolút bázis érték", + "Announcement": "Közlemény", + "Loading temp basal data": "Átmeneti bázis adatok betöltése", + "Save current record before changing to new?": "Aktuális bejegyzés mentése újra váltás előtt?", + "Profile Switch": "Profilváltás", + "Profile": "Profil", + "General profile settings": "Profil általános beállításai", + "Title": "Cím", + "Database records": "Adatbázis bejegyzések", + "Add new record": "Új bejegyzés", + "Remove this record": "Bejegyzés törlése", + "Clone this record to new": "Bejegyzés másolása újba", + "Record valid from": "Érvényesség kezdete", + "Stored profiles": "Tárolt profilok", + "Timezone": "Időzóna", + "Duration of Insulin Activity (DIA)": "Inzulin aktivitás időtartama (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Az inzulin hatásának tipikus időtartama. Személyenként és inzulintípusonként változik. Általában 3-4 óra a legtöbb pumpában használt inzulin esetén a legtöbbeknél. Néha inzulin élettartamnak is nevezik.", + "Insulin to carb ratio (I:C)": "Inzulin-szénhidrát arány (I:C)", + "Hours:": "Megjelenített órák:", + "hours": "óra", + "g/hour": "g/óra", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g szénhidrát / egység inzulin. Megmutatja, hogy 1 egység inzulin hány gramm szénhidrátot fed le.", + "Insulin Sensitivity Factor (ISF)": "Inzulinérzékenység (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mmol/L / egység inzulin. Megmutatja, hogy 1 egység inzulin mennyire csökkenti a cukorszintet.", + "Carbs activity / absorption rate": "Szénhidrát felszívódási sebessége", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gramm / óra. Egyrészt a COB, másrészt a felszívódott szénhidrát egységnyi idő alatti változását mutatja. A szénhidrát felszívódása, aktivitása kevésbé ismert, mint az inzuliné, de közelíthető egy kezdeti késleltetéssel, azt követően pedig egy állandó felszívódási sebességgel.", + "Basal rates [unit/hour]": "Bázisinzulin [Egység/óra]", + "Target BG range [mg/dL,mmol/L]": "Cukorszint céltartomány [mmol/L]", + "Start of record validity": "Bejegyzés érvényességének kezdete", + "Icicle": "Lefelé", + "Render Basal": "Bázis megjelenítése", + "Profile used": "Használatban lévő profil", + "Calculation is in target range.": "A számítás a cél tartományban található.", + "Loading profile records ...": "Profil bejegyzéseinek betöltése ...", + "Values loaded.": "Értékek betöltve.", + "Default values used.": "Az alapértékek vannak használatban.", + "Error. Default values used.": "Hiba: az alapértékek vannak használatban.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "A céltartomány aljához és tetejéhez tartozó idők nem egyeznek. Az alapértékek lettek visszaállítva.", + "Valid from:": "Érvényesség kezdete:", + "Save current record before switching to new?": "Aktuális bejegyzés mentése újra váltás előtt?", + "Add new interval before": "Új intervallum hozzáadása ezt megelőzően", + "Delete interval": "Intervallum törlése", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Kombinált bólus", + "Difference": "Különbség", + "New time": "Új idő", + "Edit Mode": "Szerkesztési mód", + "When enabled icon to start edit mode is visible": "a szerkesztési mód ikonja láthatóvá válik", + "Operation": "Működés", + "Move": "Áthelyezés", + "Delete": "Törlés", + "Move insulin": "Inzulin áthelyezése", + "Move carbs": "Szénhidrát áthelyezése", + "Remove insulin": "Inzulin törlése", + "Remove carbs": "Szénhidrát törlése", + "Change treatment time to %1 ?": "Kezelés időpontjának módosítása erre: %1 ?", + "Change carbs time to %1 ?": "Szénhidrát időpontjának módosítása erre: %1 ?", + "Change insulin time to %1 ?": "Inzulin időpontjának módosítása erre: %1 ?", + "Remove treatment ?": "Kezelés törlése?", + "Remove insulin from treatment ?": "Inzulin törlése a kezelésből?", + "Remove carbs from treatment ?": "Szénhidrát törlése a kezelésből?", + "Rendering": "Kirajzolás", + "Loading OpenAPS data of": "OpenAPS adatainak betöltése innen", + "Loading profile switch data": "Profil váltás adatainak betöltése", + "Redirecting you to the Profile Editor to create a new profile.": "Átirányítás a profil szerkesztőre, új profil készítéséhez", + "Pump": "Pumpa", + "Sensor Age": "Szenzor kora (SKOR)", + "Insulin Age": "Inzulin kora (IKOR)", + "Temporary target": "Ideiglenes cél", + "Reason": "Indok", + "Eating soon": "Hamarosan eszem", + "Top": "Teteje", + "Bottom": "Alja", + "Activity": "Tevékenység", + "Targets": "Célok", + "Bolus insulin:": "Bólus inzulin", + "Base basal insulin:": "Alap bázis inzulin:", + "Positive temp basal insulin:": "Pozitív átmeneti bázis inzulin:", + "Negative temp basal insulin:": "Negatív átmeneti bázis inzulin:", + "Total basal insulin:": "Teljes bázis inzulin:", + "Total daily insulin:": "Teljes napi inzulin:", + "Unable to save Role": "Szerep mentése sikertelen", + "Unable to delete Role": "Szerep törlése sikertelen", + "Database contains %1 roles": "Az adatbázis %1 szerepet tartalmaz", + "Edit Role": "Szerep szerkesztése", + "admin, school, family, etc": "admin, iskola, család, stb.", + "Permissions": "Engedélyek", + "Are you sure you want to delete: ": "Biztos, hogy törölni szeretnéd: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Minden szerepnek egy vagy több engedélye lehet. A * engedély egy helyettesítő, az engedélyek hierarchikusak : az elválasztójel.", + "Add new Role": "Új szerep hozzáadása", + "Roles - Groups of People, Devices, etc": "Szerepek - emberek csoportja, eszközök, stb.", + "Edit this role": "Kiválasztott szerep szerkesztése", + "Admin authorized": "Adminisztrátor engedélyezve", + "Subjects - People, Devices, etc": "Alanyok - emberek csoportja, eszközök, stb.", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Minden alanynak egyedi hozzáférési tokenje és 1 vagy több szerepe lesz. Kattints a hozzáférési tokenre, hogy új nézetet nyiss a kiválasztott alannyal - a kapott titkos link megosztható.", + "Add new Subject": "Új alany hozzáadása", + "Unable to save Subject": "Alany mentése sikertelen", + "Unable to delete Subject": "Alany törlése sikertelen", + "Database contains %1 subjects": "Az adatbázis %1 alanyt tartalmaz", + "Edit Subject": "Alany szerkesztése", + "person, device, etc": "személy, eszköz, stb.", + "role1, role2": "szerep1, szerep2", + "Edit this subject": "Kiválasztott alany szerkesztése", + "Delete this subject": "Kiválasztott alany törlése", + "Roles": "Szerepek", + "Access Token": "Hozzáférési token", + "hour ago": "órája", + "hours ago": "órája", + "Silence for %1 minutes": "Némítás %1 percre", + "Check BG": "Ellenőrizd a cukorszintet", + "BASAL": "BÁZIS", + "Current basal": "Jelenlegi bázis", + "Sensitivity": "Inzulinérzékenység (ISF)", + "Current Carb Ratio": "Inzulin-szénhidrát arány (I:C)", + "Basal timezone": "Bázis időzóna", + "Active profile": "Aktív profil", + "Active temp basal": "Aktív átmeneti bázis", + "Active temp basal start": "Aktív átmeneti bázis kezdete", + "Active temp basal duration": "Aktív átmeneti bázis időtartalma", + "Active temp basal remaining": "Aktív átmeneti bázisból hátralévő idő", + "Basal profile value": "Bázis profil értéke", + "Active combo bolus": "Aktív kombinált bólus", + "Active combo bolus start": "Aktív kombinált bólus kezdete", + "Active combo bolus duration": "Aktív kombinált bólus időtartama", + "Active combo bolus remaining": "Aktív kombinált bólusból hátralévő idő", + "BG Delta": "SzG változás", + "Elapsed Time": "Eltelt idő", + "Absolute Delta": "Abszolút változás", + "Interpolated": "Interpolált", + "BWP": "Bólus varázsló előnézet", + "Urgent": "Sűrgős", + "Warning": "Figyelmeztetés", + "Info": "Info", + "Lowest": "Legalacsonyabb", + "Snoozing high alarm since there is enough IOB": "Magas riasztás késleltetése, ha van elég aktív inzulin (IOB)", + "Check BG, time to bolus?": "Ellenőrizd a cukorszintet, ideje bólusolni?", + "Notice": "Figyelem", + "required info missing": "Hiányzik a szükséges információ", + "Insulin on Board": "Aktív inzulin (IOB)", + "Current target": "Jelenlegi cél", + "Expected effect": "Várható hatás", + "Expected outcome": "Várható eredmény", + "Carb Equivalent": "Szénhidrát egyenérték", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Az alacsony cél eléréséhez szükségesnél %1E-gel több inzulin van, nem számolva a szénhidrátokkal", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Az alacsony cél eléréséhez szükségesnél %1E-gel több inzulin van, FONTOS, HOGY AZ IOB SZÉNHIDRÁTTAL LE LEGYEN FEDVE", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1E-gel kevesebb inzulin kell az alacsony cél eléréséhez, túl magas a bázis?", + "basal adjustment out of range, give carbs?": "A bázis változtatása a tartományon kívül esik. Szénhidrát bevitel?", + "basal adjustment out of range, give bolus?": "A bázis változtatása a tartományon kívül esik. Bólus beadás?", + "above high": "magas felett", + "below low": "alacsony alatt", + "Projected BG %1 target": "A várható SzG %1", + "aiming at": "céloz", + "Bolus %1 units": "Bólusolj %1E", + "or adjust basal": "vagy állíts a bázison", + "Check BG using glucometer before correcting!": "Ellenőrizd a vércukorszintet mérővel korrekció előtt!", + "Basal reduction to account %1 units:": "Bázis csökkentése %1 E kompenzálásához:", + "30m temp basal": "30p átmeneti bázis", + "1h temp basal": "1h átmeneti bázis", + "Cannula change overdue!": "Kanülcsere esedékes!", + "Time to change cannula": "Ideje kanült cserélni", + "Change cannula soon": "Hamarosan kanülcsere", + "Cannula age %1 hours": "Kanül életkora %1 óra", + "Inserted": "Behelyezve", + "CAGE": "KKOR", + "COB": "Aktív szénhidrát (COB)", + "Last Carbs": "Utolsó szénhidrátok", + "IAGE": "IKOR", + "Insulin reservoir change overdue!": "Inzulintartály csere esedékes!", + "Time to change insulin reservoir": "Ideje inzulintartályt cserélni", + "Change insulin reservoir soon": "Hamarosan cseréld ki az inzulintartályt", + "Insulin reservoir age %1 hours": "Az inzulintartály %1 órája volt cserélve", + "Changed": "Cserélve", + "IOB": "Aktív inzulin (IOB)", + "Careportal IOB": "Careportal IOB", + "Last Bolus": "Utolsó bólus", + "Basal IOB": "Bázis IOB", + "Source": "Forrás", + "Stale data, check rig?": "Régi adatok, ellenőrizd a feltöltőt", + "Last received:": "Legutóbb fogadott:", + "%1m ago": "%1 perce", + "%1h ago": "%1 órája", + "%1d ago": "%1 napja", + "RETRO": "RETRO", + "SAGE": "SKOR", + "Sensor change/restart overdue!": "Szenzor csere/újraindítás esedékes!", + "Time to change/restart sensor": "Ideje szenzort cserélni/újraindítani", + "Change/restart sensor soon": "Hamarosan indítsd újra vagy cseréld ki a szenzort", + "Sensor age %1 days %2 hours": "Szenzor kora %1 nap %2 óra", + "Sensor Insert": "Szenzor behelyezve", + "Sensor Start": "Szenzor indítása", + "days": "nap", + "Insulin distribution": "Bólus/bázis arány", + "To see this report, press SHOW while in this view": "A jelentés megtekintéséhez kattints a MEGJELENÍTÉS gombra ebben a nézetben", + "AR2 Forecast": "AR2 előrejelzés", + "OpenAPS Forecasts": "OpenAPS előrejelzés", + "Temporary Target": "Ideiglenes cél", + "Temporary Target Cancel": "Ideiglenes cél törlése", + "OpenAPS Offline": "OpenAPS offline", + "Profiles": "Profilok", + "Time in fluctuation": "Ingadozásban töltött idő", + "Time in rapid fluctuation": "Erős ingadozásban töltött idő", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Ez csak egy durva becslés, amely nagyon pontatlan lehet, és nem helyettesíti a tényleges vérből mérést. A felhasznált képlet a következő helyről lett véve: ", + "Filter by hours": "Szűrés órák alapján", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Az ingadozásban töltött idő és az erős ingadozásban töltött idő a vizsgált időtartam %-ban fejezi ki, hogy a szenzorglükóz mennyi ideig változott gyorsan vagy nagyon gyorsan. A kisebb érték a jobb.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Az átlagos teljes napi változás a vizsgált időszak összes glükóz elmozdulásai abszolút értékének összege, elosztva a napok számával. A kisebb érték a jobb.\n", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Az átlagos óránkénti változás a vizsgált időszak összes glükóz elmozdulásai abszolút értékének összege, elosztva az időszak óráinak számával. A kisebb érték a jobb.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "A tartományon kívüli RMS számítása: a vizsgált időszak összes glükózértékének tartományon kívüli távolsága négyzetre lesz emelve, összegzve, elosztva a glükózértékek számával, és négyzetgyököt vonva belőle. Ez a mutató hasonló a tartományon belül töltött százalékhoz, de jobban súlyozza a tartománytól távolabbi értékeket. A kisebb érték jobb.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">itt találhatóak.", + "Mean Total Daily Change": "Átlagos teljes napi változás", + "Mean Hourly Change": "Átlagos óránkénti változás", + "FortyFiveDown": "kissé esik", + "FortyFiveUp": "kissé emelkedik", + "Flat": "stabil", + "SingleUp": "emelkedik", + "SingleDown": "esik", + "DoubleDown": "gyorsan esik", + "DoubleUp": "gyorsan emelkedik", + "virtAsstUnknown": "Ezen adat jelenleg ismeretlen. Kérlek, nézd meg a Nightscout oldalt a részletekért.", + "virtAsstTitleAR2Forecast": "AR2 előrejelzés", + "virtAsstTitleCurrentBasal": "Jelenlegi bázis", + "virtAsstTitleCurrentCOB": "Jelenlegi COB", + "virtAsstTitleCurrentIOB": "Jelenlegi IOB", + "virtAsstTitleLaunch": "Üdvözlet a Nightscouton", + "virtAsstTitleLoopForecast": "Loop előrejelzés", + "virtAsstTitleLastLoop": "Utolsó loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS előrejelzés", + "virtAsstTitlePumpReservoir": "Hártalévő inzulin", + "virtAsstTitlePumpBattery": "Pumpa töltöttsége", + "virtAsstTitleRawBG": "Jelenlegi nyers SzG", + "virtAsstTitleUploaderBattery": "Feltöltő töltöttsége", + "virtAsstTitleCurrentBG": "Jelenlegi SzG", + "virtAsstTitleFullStatus": "Teljes státusz", + "virtAsstTitleCGMMode": "CGM mód", + "virtAsstTitleCGMStatus": "CGM státusz", + "virtAsstTitleCGMSessionAge": "CGM életkora", + "virtAsstTitleCGMTxStatus": "CGM jeladó állapota", + "virtAsstTitleCGMTxAge": "CGM jeladó kora", + "virtAsstTitleCGMNoise": "CGM zaj", + "virtAsstTitleDelta": "Szenzorglükóz változás", + "virtAsstStatus": "%1 és %2 mint a %3.", + "virtAsstBasal": "%1 a jelenlegi bázis %2 Egység/óra", + "virtAsstBasalTemp": "%1 átmeneti bázis %2 Egység/óra %3-kor jár le", + "virtAsstIob": "és %1 aktív inzulinod (IOB) van.", + "virtAsstIobIntent": "%1 aktív inzulinod (IOB) van.", + "virtAsstIobUnits": "%1 Egység", + "virtAsstLaunch": "Mit szeretnél ellenőrizni a Nightscout oldalon?", + "virtAsstPreamble": "Saját", + "virtAsstPreamble3person": "%1 -nak van ", + "virtAsstNoInsulin": "nincs", + "virtAsstUploadBattery": "A feltöltő töltöttsége %1", + "virtAsstReservoir": "%1 Egység maradt", + "virtAsstPumpBattery": "A pumpaelem töltöttsége %1 %2", + "virtAsstUploaderBattery": "A feltöltő töltöttsége %1", + "virtAsstLastLoop": "Az utolsó sikeres loop %1-kor volt", + "virtAsstLoopNotAvailable": "A loop beépülő valószínűleg nincs bekapcsolva", + "virtAsstLoopForecastAround": "A loop előrejelzése alapján a következő %2 időszakban körülbelül %1 lesz", + "virtAsstLoopForecastBetween": "A loop előrejelzése alapján a következő %3 időszakban %1 és %2 között leszel", + "virtAsstAR2ForecastAround": "Az AR2 előrejelzése alapján a következő %2 időszakban körülbelül %1 lesz", + "virtAsstAR2ForecastBetween": "Az AR2 előrejelzése alapján a következő %3 időszakban %1 és %2 között leszel", + "virtAsstForecastUnavailable": "Nem lehet előrejelezni a rendelkezésre álló adatokkal", + "virtAsstRawBG": "A nyers SzG-od %1", + "virtAsstOpenAPSForecast": "Az OpenAPS esetleges szenzorglükóz %1", + "virtAsstCob3person": "%1 aktív szénhidrátja = %2", + "virtAsstCob": "%1 aktív szénhidrátod van", + "virtAsstCGMMode": "A CGM mód %1 volt %2 -kor.", + "virtAsstCGMStatus": "A CGM státusz %1 volt %2-kor.", + "virtAsstCGMSessAge": "A CGM munkamenet %1 napja és %2 órája aktív.", + "virtAsstCGMSessNotStarted": "Jelenleg nincs aktív CGM munkamenet.", + "virtAsstCGMTxStatus": "A CGM jeladó állapota %1 volt %2-kor.", + "virtAsstCGMTxAge": "A CGM jeladó %1 napos.", + "virtAsstCGMNoise": "A CGM zaj %1 volt %2-kor.", + "virtAsstCGMBattOne": "A CGM töltöttsége %1 V volt %2-kor.", + "virtAsstCGMBattTwo": "A CGM töltöttsége %1 és %2 V volt %3-kor.", + "virtAsstDelta": "A változás %1 volt %2 és %3 között.", + "virtAsstDeltaEstimated": "A becsült változás %1 volt %2 és %3 között.", + "virtAsstUnknownIntentTitle": "Ismeretlen szándék", + "virtAsstUnknownIntentText": "Sajnálom, nem tudom mit szeretnél.", + "Fat [g]": "Zsír [g]", + "Protein [g]": "Fehérje [g]", + "Energy [kJ]": "Energia [kJ]", + "Clock Views:": "Óra nézetek:", + "Clock": "Óra", + "Color": "Színes", + "Simple": "Egyszerű", + "TDD average": "Teljes napi inzulin (TDD) átlaga", + "Bolus average": "Átlag bólus", + "Basal average": "Átlag bázis", + "Base basal average:": "Átlag alap bázis:", + "Carbs average": "Átlag szénhidrát", + "Eating Soon": "Hamarosan étkezés", + "Last entry {0} minutes ago": "Utolsó bejegyzés {0} perce", + "change": "változás", + "Speech": "Beszéd", + "Target Top": "Felső cél", + "Target Bottom": "Alsó cél", + "Canceled": "Törölve", + "Meter BG": "VC a mérőből", + "predicted": "előrejelezve", + "future": "jövő", + "ago": "ezelőtt", + "Last data received": "Utolsó fogadott adatok", + "Clock View": "Óra nézet", + "Protein": "Fehérje", + "Fat": "Zsír", + "Protein average": "Fehérje átlag", + "Fat average": "Zsír átlag", + "Total carbs": "Összes szénhidrát", + "Total protein": "Összes fehérje", + "Total fat": "Összes zsír", + "Database Size": "Adatbázis mérete", + "Database Size near its limits!": "Az adatbázis majdnem megtelt!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Az adatbázis mérete %1 MiB a rendelkezésre álló %2 MiB-ból. Készíts biztonsági mentést és tisztítsd ki az adatbázist!", + "Database file size": "Adatbázis file mérete", + "%1 MiB of %2 MiB (%3%)": "%1 MiB a(z) %2 MiB-ból (%3%)", + "Data size": "Adatbázis mérete", + "virtAsstDatabaseSize": "%1 MiB. Ez %2%-a a rendelkezésre álló helynek.", + "virtAsstTitleDatabaseSize": "Adatbázis file méret", + "Carbs/Food/Time": "Szénhidrát/étel/idő", + "You have administration messages": "Új rendszergazda-üzenetek", + "Admin messages in queue": "Sorban álló rendszergazda-üzenetek", + "Queue empty": "Várólista üres", + "There are no admin messages in queue": "Nincs sorban álló rendszergazda-üzenet", + "Please sign in using the API_SECRET to see your administration messages": "Kérlek, jelentkezz be API_SECRET-tel, hogy lásd a rendszergazda-üzeneteket", + "Reads enabled in default permissions": "Olvasási jog bekapcsolva az alapértelmezett engedélyeknél", + "Data reads enabled": "Adat olvasási jog bekapcsolva", + "Data writes enabled": "Adat írási jog bekapcsolva", + "Data writes not enabled": "Adat írási jog nincs bekapcsolva", + "Color prediction lines": "Színes előrejelzési görbék", + "Release Notes": "Kiadási megjegyzések", + "Check for Updates": "Frissítések ellenőrzése", + "Open Source": "Nyílt forráskód", + "Nightscout Info": "Nightscout dokumentáció", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "A Loopalyzer elsődleges célja a zárt hurkú rendszer működésének megjelenítése. Működhet különböző beállításokkal is: zárt és nyitott hurokkal, és hurok nélkül is. Azonban attól függően, hogy milyen feltöltőt használsz, az milyen gyakorisággal képes rögzíteni és feltölteni az adataidat, valamint hogy képes pótolni a hiányzó adatokat, néhány grafikonon hiányos vagy akár teljesen üres részek is lehetnek. Mindig győződj meg arról, hogy a grafikonok megfelelőek-e. A legjobb, ha egyszerre egy napot jelenítesz meg, és több napon görgetsz végig.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "A Loopalyzer tartalmaz egy időeltolás funkciót. Ha például egy nap 07:00-kor, másnap 08:00-kor reggelizel, a két nap átlagában valószínűleg lapos lesz a szenzorglükóz-görbéd, és nem látszik a tényleges reggeli utáni hatás. Az időeltolás funkció kiszámítja az étkezések átlagos időpontját, majd mindkét nap összes adatát (szénhidrát, inzulin, bázisok, stb.) eltolja annak megfelelően, hogy mindkét étkezés igazodjon az étkezések átlagos kezdési időpontjához.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "Ebben a példában az első nap összes adata 30 perccel előre lett tolva, a második nap összes adata pedig 30 perccel hátra lett tolva, tehát úgy tűnik, mintha mindkét nap 07:30-kor reggeliztél volna. Ez láthatóvá teszi az étkezés tényleges szenzorglükózra gyakorolt átlagos hatását.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Az időeltolás az étkezés átlagos kezdési időpontja utáni időszakot szürke színnel emeli ki, a DIA (inzulin aktivitás időtartama) alatt. Mivel egész nap az összes adatpont eltolódik, előfordulhat, hogy a görbék a szürke területen kívül nem pontosak.", + "Note that time shift is available only when viewing multiple days.": "Ne feledd: az időeltolás csak több nap megtekintése esetén érhető el.", + "Please select a maximum of two weeks duration and click Show again.": "Legfeljebb két hét időtartamot válassz, majd kattints újra a Megjelenítés gombra.", + "Show profiles table": "Profil táblázat megjelenítése", + "Show predictions": "Előrejelzés", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Időeltolás a %1 g-nál nagyobb szénhidrátú étkezéseknél %2 és %3 között", + "Previous": "Előző", + "Previous day": "Előző nap", + "Next day": "Következő nap", + "Next": "Következő", + "Temp basal delta": "Átmeneti bázis különbség", + "Authorized by token": "Tokennel engedélyezve", + "Auth role": "Hitelesítési szerep", + "view without token": "megtekintés token nélkül", + "Remove stored token": "Tárolt token eltávolítása", + "Weekly Distribution": "Heti eloszlás", + "Failed authentication": "Sikertelen hitelesítés", + "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?": "A(z) %1 IP-címmel rendelkező eszköz hibás adatokkal próbálta meg a hitelesítést a Nightscouttal. Ellenőrizd, hogy a feltöltők helyes API_SECRET-tel vagy tokennel rendelkeznek-e.", + "Default (with leading zero and U)": "Alapért. (kezdő nullával és E-gel)", + "Concise (with U, without leading zero)": "Tömör (kezdő nulla nélkül, de E-gel)", + "Minimal (without leading zero and U)": "Minimalista (kezdő nulla és E nélkül)", + "Small Bolus Display": "Kis bólus megjelenítése", + "Large Bolus Display": "Nagy bólus megjelenítése", + "Bolus Display Threshold": "Kis/nagy bólus határ", + "%1 U and Over": "%1 E és felette", + "Event repeated %1 times.": "Az esemény %1 alkalommal ismétlődött meg.", + "minutes": "perc", + "Last recorded %1 %2 ago.": "Legutóbb rögzítve: %1 %2", + "Security issue": "Biztonsági probléma", + "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.": "Gyenge API_SECRET észlelve! Kérjük, használj kis- és nagybetűk, számok és nem-alfanumerikus karakterek, például !#%&/ keverékét az illetéktelen hozzáférés kockázatának csökkentésére. Az API_SECRET minimális hossza 12 karakter.", + "less than 1": "kevesebb, mint 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "A MongoDB jelszó és az API_SECRET egyezik. Ez nagyon rossz ötlet. Kérjük, változtasd meg mindkettőt, és ne használj egyező jelszavakat sehol." +} diff --git a/translations/it_IT.json b/translations/it_IT.json new file mode 100644 index 00000000000..5e35437aafd --- /dev/null +++ b/translations/it_IT.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Porta in ascolto", + "Mo": "Lun", + "Tu": "Mar", + "We": "Mer", + "Th": "Gio", + "Fr": "Ven", + "Sa": "Sab", + "Su": "Dom", + "Monday": "Lunedì", + "Tuesday": "Martedì", + "Wednesday": "Mercoledì", + "Thursday": "Giovedì", + "Friday": "Venerdì", + "Saturday": "Sabato", + "Sunday": "Domenica", + "Category": "Categoria", + "Subcategory": "Sottocategoria", + "Name": "Nome", + "Today": "Oggi", + "Last 2 days": "Ultimi 2 giorni", + "Last 3 days": "Ultimi 3 giorni", + "Last week": "Settimana scorsa", + "Last 2 weeks": "Ultime 2 settimane", + "Last month": "Mese scorso", + "Last 3 months": "Ultimi 3 mesi", + "From": "Da", + "To": "A", + "Notes": "Note", + "Food": "Cibo", + "Insulin": "Insulina", + "Carbs": "Carboidrati", + "Notes contain": "Contiene note", + "Target BG range bottom": "Limite inferiore della glicemia", + "top": "Superiore", + "Show": "Mostra", + "Display": "Schermo", + "Loading": "Carico", + "Loading profile": "Carico il profilo", + "Loading status": "Stato di caricamento", + "Loading food database": "Carico database alimenti", + "not displayed": "Non visualizzato", + "Loading CGM data of": "Carico dati CGM", + "Loading treatments data of": "Carico dati dei trattamenti", + "Processing data of": "Elaborazione dei dati", + "Portion": "Porzione", + "Size": "Formato", + "(none)": "(Nessuno)", + "None": "Nessuno", + "": "", + "Result is empty": "Risultato vuoto", + "Day to day": "Giorno per giorno", + "Week to week": "Da settimana a settimana", + "Daily Stats": "Statistiche giornaliere", + "Percentile Chart": "Grafico percentile", + "Distribution": "Distribuzione", + "Hourly stats": "Statistiche per ore", + "netIOB stats": "netIOB statistiche", + "temp basals must be rendered to display this report": "le basali temp devono essere renderizzate per visualizzare questo report", + "No data available": "Dati non disponibili", + "Low": "Basso", + "In Range": "Nell'intervallo", + "Period": "Periodo", + "High": "Alto", + "Average": "Media", + "Low Quartile": "Quartile basso", + "Upper Quartile": "Quartile alto", + "Quartile": "Quartile", + "Date": "Data", + "Normal": "Normale", + "Median": "Mediana", + "Readings": "Letture", + "StDev": "Dev.std", + "Daily stats report": "Report delle statistiche giornaliere", + "Glucose Percentile report": "Report Percentuali Glicemie", + "Glucose distribution": "Distribuzione glicemie", + "days total": "Giorni totali", + "Total per day": "Totale al giorno", + "Overall": "Generale", + "Range": "Intervallo", + "% of Readings": "% delle letture", + "# of Readings": "# delle letture", + "Mean": "Media", + "Standard Deviation": "Deviazione Standard", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "Stima glicata", + "Weekly Success": "Risultati settimanali", + "There is not sufficient data to run this report. Select more days.": "Non ci sono dati sufficienti per eseguire questo report. Selezionare più giorni.", + "Using stored API secret hash": "Stai utilizzando API hash segreta", + "No API secret hash stored yet. You need to enter API secret.": "API hash segreto non è ancora memorizzato. È necessario inserire API segreto.", + "Database loaded": "Database caricato", + "Error: Database failed to load": "Errore: database non è stato caricato", + "Error": "Errore", + "Create new record": "Crea nuovo registro", + "Save record": "Salva Registro", + "Portions": "Porzioni", + "Unit": "Unità", + "GI": "IG-Ind.Glic.", + "Edit record": "Modifica registro", + "Delete record": "Cancella registro", + "Move to the top": "Spostare verso l'alto", + "Hidden": "Nascosto", + "Hide after use": "Nascondi dopo l'uso", + "Your API secret must be at least 12 characters long": "Il tuo API segreto deve essere lungo almeno 12 caratteri", + "Bad API secret": "API segreto non corretto", + "API secret hash stored": "Hash API segreto memorizzato", + "Status": "Stato", + "Not loaded": "Non caricato", + "Food Editor": "Database Alimenti", + "Your database": "Il tuo database", + "Filter": "Filtro", + "Save": "Salva", + "Clear": "Pulisci", + "Record": "Registro", + "Quick picks": "Scelta rapida", + "Show hidden": "Mostra nascosto", + "Your API secret or token": "Il tuo API segreto o token", + "Remember this device. (Do not enable this on public computers.)": "Ricorda questo dispositivo. (Da non abilitare su computer condivisi con altri.)", + "Treatments": "Somministrazioni", + "Time": "Tempo", + "Event Type": "Tipo di evento", + "Blood Glucose": "Glicemie", + "Entered By": "inserito da", + "Delete this treatment?": "Eliminare questa somministrazione?", + "Carbs Given": "Carboidrati", + "Insulin Given": "Insulina", + "Event Time": "Ora Evento", + "Please verify that the data entered is correct": "Verificare che i dati inseriti siano corretti", + "BG": "Glicemie", + "Use BG correction in calculation": "Utilizza la correzione delle Glicemia nel calcolo", + "BG from CGM (autoupdated)": "Glicemie da sensore (aggiornamento automatico)", + "BG from meter": "Glicemie da glucometro", + "Manual BG": "Inserisci Glicemia", + "Quickpick": "Scelta rapida", + "or": "o", + "Add from database": "Aggiungi dal database", + "Use carbs correction in calculation": "Utilizza la correzione dei carboidrati nel calcolo", + "Use COB correction in calculation": "Utilizzare la correzione COB nel calcolo", + "Use IOB in calculation": "Utilizza l'IOB nel calcolo", + "Other correction": "Altre correzioni", + "Rounding": "Arrotondamento", + "Enter insulin correction in treatment": "Inserisci la correzione d'insulina in somministrazioni", + "Insulin needed": "Insulina necessaria", + "Carbs needed": "Carboidrati necessari", + "Carbs needed if Insulin total is negative value": "Carboidrati necessari se l'insulina totale è un valore negativo", + "Basal rate": "Velocità basale", + "60 minutes earlier": "60 minuti prima", + "45 minutes earlier": "45 minuti prima", + "30 minutes earlier": "30 minuti prima", + "20 minutes earlier": "20 minuti prima", + "15 minutes earlier": "15 minuti prima", + "Time in minutes": "Tempo in minuti", + "15 minutes later": "15 minuti più tardi", + "20 minutes later": "20 minuti più tardi", + "30 minutes later": "30 minuti più tardi", + "45 minutes later": "45 minuti più tardi", + "60 minutes later": "60 minuti più tardi", + "Additional Notes, Comments": "Note aggiuntive, commenti", + "RETRO MODE": "Modalità retrospettiva", + "Now": "Ora", + "Other": "Altro", + "Submit Form": "Invia il modulo", + "Profile Editor": "Editor dei profili", + "Reports": "Statistiche", + "Add food from your database": "Aggiungere cibo al database", + "Reload database": "Ricarica database", + "Add": "Aggiungere", + "Unauthorized": "Non Autorizzato", + "Entering record failed": "Inserimento nel registro fallito", + "Device authenticated": "Disp. autenticato", + "Device not authenticated": "Disp. non autenticato", + "Authentication status": "Stato di autenticazione", + "Authenticate": "Autenticare", + "Remove": "Rimuovere", + "Your device is not authenticated yet": "Il tuo dispositivo non è ancora stato autenticato", + "Sensor": "Sensore", + "Finger": "Dito", + "Manual": "Manuale", + "Scale": "Scala", + "Linear": "Lineare", + "Logarithmic": "Logaritmica", + "Logarithmic (Dynamic)": "Logaritmica (Dinamica)", + "Insulin-on-Board": "Insulina attiva", + "Carbs-on-Board": "Carboidrati attivi", + "Bolus Wizard Preview": "Calcolatore di bolo", + "Value Loaded": "Valori Caricati", + "Cannula Age": "Cambio Ago", + "Basal Profile": "Profilo Basale", + "Silence for 30 minutes": "Silenzia per 30 minuti", + "Silence for 60 minutes": "Silenzia per 60 minuti", + "Silence for 90 minutes": "Silenzia per 90 minuti", + "Silence for 120 minutes": "Silenzia per 120 minuti", + "Settings": "Impostazioni", + "Units": "Unità", + "Date format": "Formato data", + "12 hours": "12 ore", + "24 hours": "24 ore", + "Log a Treatment": "Inserisci una somministrazione", + "BG Check": "Controllo Glicemia", + "Meal Bolus": "Bolo Pasto", + "Snack Bolus": "Bolo Merenda", + "Correction Bolus": "Bolo Correttivo", + "Carb Correction": "Carboidrati Correttivi", + "Note": "Nota", + "Question": "Domanda", + "Exercise": "Esercizio Fisico", + "Pump Site Change": "Cambio Ago", + "CGM Sensor Start": "Avvio sensore CGM", + "CGM Sensor Stop": "Arresto Sensore CGM", + "CGM Sensor Insert": "Cambio sensore CGM", + "Sensor Code": "Codice sensore", + "Transmitter ID": "Codice trasmettitore", + "Dexcom Sensor Start": "Avvio sensore Dexcom", + "Dexcom Sensor Change": "Cambio sensore Dexcom", + "Insulin Cartridge Change": "Cambio Cartuccia Insulina", + "D.A.D. Alert": "Allarme Cane da allerta Diabete", + "Glucose Reading": "Lettura glicemia", + "Measurement Method": "Metodo di misurazione", + "Meter": "Glucometro", + "Amount in grams": "Quantità in grammi", + "Amount in units": "Quantità in unità", + "View all treatments": "Visualizza tutti le somministrazioni", + "Enable Alarms": "Attiva Allarmi", + "Pump Battery Change": "Cambio Batteria Microinfusore", + "Pump Battery Age": "Età batteria micro", + "Pump Battery Low Alarm": "Allarme Batteria Microinfusore Bassa", + "Pump Battery change overdue!": "Sostituzione Batteria del Microinfusore in ritardo!", + "When enabled an alarm may sound.": "Quando è abilitato, può suonare un allarme.", + "Urgent High Alarm": "Urgente Glicemia Alta", + "High Alarm": "Glicemia Alta", + "Low Alarm": "Glicemia bassa", + "Urgent Low Alarm": "Urgente Glicemia Bassa", + "Stale Data: Warn": "Avviso: Dati Obsoleti", + "Stale Data: Urgent": "Urgente: Dati Obsoleti", + "mins": "min", + "Night Mode": "Modalità Notte", + "When enabled the page will be dimmed from 10pm - 6am.": "Attivandola, la pagina sarà oscurata dalle 22.00 - 06.00.", + "Enable": "Attivare", + "Show Raw BG Data": "Mostra dati Grezzi Glicemia", + "Never": "Mai", + "Always": "Sempre", + "When there is noise": "Quando i dati sono disturbati", + "When enabled small white dots will be displayed for raw BG data": "Quando abilitato, saranno visualizzati piccoli punti bianchi per i dati grezzi della glicemia", + "Custom Title": "Titolo personalizzato", + "Theme": "Tema", + "Default": "Predefinito", + "Colors": "Colori", + "Colorblind-friendly colors": "Colori per daltonici", + "Reset, and use defaults": "Reimposta i valori predefiniti", + "Calibrations": "Calibrazioni", + "Alarm Test / Smartphone Enable": "Test Allarme / Abilita Smartphone", + "Bolus Wizard": "Calcolatore di Bolo", + "in the future": "nel futuro", + "time ago": "tempo fa", + "hr ago": "ora fa", + "hrs ago": "ore fa", + "min ago": "minuto fa", + "mins ago": "minuti fa", + "day ago": "giorno fa", + "days ago": "giorni fa", + "long ago": "molto tempo fa", + "Clean": "Pulito", + "Light": "Leggero", + "Medium": "Medio", + "Heavy": "Pesante", + "Treatment type": "Tipo di somministrazione", + "Raw BG": "Glicemia Grezza", + "Device": "Dispositivo", + "Noise": "Rumore", + "Calibration": "Calibrazione", + "Show Plugins": "Mostra Plugin", + "About": "Informazioni", + "Value in": "Valore in", + "Carb Time": "Tempo Assorbimento Carboidrati", + "Language": "Lingua", + "Add new": "Aggiungi nuovo", + "g": "g", + "ml": "ml", + "pcs": "pz", + "Drag&drop food here": "Trascina&rilascia cibo qui", + "Care Portal": "Somministrazioni", + "Medium/Unknown": "Media/Sconosciuto", + "IN THE FUTURE": "NEL FUTURO", + "Order": "Ordina", + "oldest on top": "più vecchio in alto", + "newest on top": "più recente in alto", + "All sensor events": "Tutti gli eventi del sensore", + "Remove future items from mongo database": "Rimuovi gli elementi futuri dal database mongo", + "Find and remove treatments in the future": "Trova e rimuovi i trattamenti in futuro", + "This task find and remove treatments in the future.": "Trovare e rimuovere le somministrazioni in futuro", + "Remove treatments in the future": "Rimuovere somministrazioni in futuro", + "Find and remove entries in the future": "Trovare e rimuovere le voci in futuro", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Trovare e rimuovere i dati CGM in futuro creato da uploader/xdrip con data/ora sbagliato.", + "Remove entries in the future": "Rimuovere le voci in futuro", + "Loading database ...": "Caricamento Database ...", + "Database contains %1 future records": "Il Database contiene %1 record futuri", + "Remove %1 selected records?": "Rimuovere i %1 registri selezionati?", + "Error loading database": "Errore di caricamento del database", + "Record %1 removed ...": "Registro %1 rimosso ...", + "Error removing record %1": "Errore rimozione registro %1", + "Deleting records ...": "Elimino registro ...", + "%1 records deleted": "%1Registro Cancellato", + "Clean Mongo status database": "Pulisci database di Mongo", + "Delete all documents from devicestatus collection": "Eliminare tutti i documenti dal registro \"devicestatus\"", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Questa attività elimina tutti i documenti dalla collezione \"devicestatus\". Utile quando lo stato della batteria uploader/xdrip non si aggiorna.", + "Delete all documents": "Eliminare tutti i documenti", + "Delete all documents from devicestatus collection?": "Eliminare tutti i documenti dalla collezione devicestatus?", + "Database contains %1 records": "Il Database contiene %1 registri", + "All records removed ...": "Tutti i registri rimossi ...", + "Delete all documents from devicestatus collection older than 30 days": "Eliminare tutti i documenti della collezione \"devicestatus\" più vecchi di 30 giorni", + "Number of Days to Keep:": "Numero di Giorni da osservare:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Questa attività elimina tutti i documenti dalla collezione \"devicestatus\". Utile quando lo stato della batteria uploader/xdrip non si aggiorna.", + "Delete old documents from devicestatus collection?": "Eliminare tutti i documenti dalla collezione devicestatus?", + "Clean Mongo entries (glucose entries) database": "Pulisci il database Mongo entries (glicemie sensore)", + "Delete all documents from entries collection older than 180 days": "Eliminare tutti i documenti della collezione \"devicestatus\" più vecchi di 180 giorni", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Questa attività elimina tutti i documenti dalla collezione \"devicestatus\". Utile quando lo stato della batteria uploader/xdrip non si aggiorna.", + "Delete old documents": "Eliminare tutti i documenti", + "Delete old documents from entries collection?": "Eliminare tutti i documenti dalla collezione devicestatus?", + "%1 is not a valid number": "%1 non è un numero valido", + "%1 is not a valid number - must be more than 2": "%1 non è un numero valido - deve essere più di 2", + "Clean Mongo treatments database": "Pulisci database \"treatments\" di Mongo", + "Delete all documents from treatments collection older than 180 days": "Eliminare tutti i documenti dal registro delle somministrazioni più vecchi di 180 giorni", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Questa attività elimina tutti i documenti dalla collezione \"devicestatus\". Utile quando lo stato della batteria uploader/xdrip non si aggiorna.", + "Delete old documents from treatments collection?": "Eliminare tutti i documenti dalla collezione devicestatus?", + "Admin Tools": "Dati Mongo", + "Nightscout reporting": "Nightscout - Statistiche", + "Cancel": "Cancellare", + "Edit treatment": "Modifica Somministrazione", + "Duration": "Tempo", + "Duration in minutes": "Tempo in minuti", + "Temp Basal": "Basale Temp", + "Temp Basal Start": "Inizio Basale Temp", + "Temp Basal End": "Fine Basale Temp", + "Percent": "Percentuale", + "Basal change in %": "Variazione basale in %", + "Basal value": "Valore Basale", + "Absolute basal value": "Valore Basale Assoluto", + "Announcement": "Annuncio", + "Loading temp basal data": "Caricamento basale temp", + "Save current record before changing to new?": "Salvare i dati correnti prima di cambiarli?", + "Profile Switch": "Cambio profilo", + "Profile": "Profilo", + "General profile settings": "Impostazioni generali profilo", + "Title": "Titolo", + "Database records": "Registro del database", + "Add new record": "Aggiungi un nuovo registro", + "Remove this record": "Rimuovi questo registro", + "Clone this record to new": "Clona questo registro in uno nuovo", + "Record valid from": "Registro valido da", + "Stored profiles": "Profili salvati", + "Timezone": "Fuso orario", + "Duration of Insulin Activity (DIA)": "Durata Attività Insulinica (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Rappresenta la durata tipica nel quale l'insulina ha effetto. Varia in base al paziente ed al tipo d'insulina. Tipicamente 3-4 ore per la maggior parte dei microinfusori e dei pazienti. Chiamata anche durata d'azione insulinica.", + "Insulin to carb ratio (I:C)": "Rapporto Insulina-Carboidrati (I:C)", + "Hours:": "Ore:", + "hours": "ore", + "g/hour": "g/ora", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g carboidrati per U di insulina. Il rapporto tra quanti grammi di carboidrati sono compensati da ogni U di insulina.", + "Insulin Sensitivity Factor (ISF)": "Fattore di Sensibilità Insulinica (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL o mmol/L per U insulina. Il rapporto di quanto la glicemia varia per ogni U di correzione insulinica.", + "Carbs activity / absorption rate": "Attività carboidrati / Velocità di assorbimento", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grammi per unità di tempo. Rappresentano sia il cambio di COB per unità di tempo, sia la quantità di carboidrati che faranno effetto nel tempo. Assorbimento di carboidrati / curva di attività sono meno conosciute rispetto all'attività insulinica, ma possono essere approssimate usando un ritardo iniziale seguito da un rapporto costante di assorbimento (g/hr).", + "Basal rates [unit/hour]": "Basale [unità/ora]", + "Target BG range [mg/dL,mmol/L]": "Obiettivo dell'intervallo glicemico [mg/dL,mmol/L]", + "Start of record validity": "Inizio di validità del dato", + "Icicle": "Inverso", + "Render Basal": "Grafico Basale", + "Profile used": "Profilo usato", + "Calculation is in target range.": "Calcolo all'interno dell'intervallo", + "Loading profile records ...": "Caricamento dati del profilo ...", + "Values loaded.": "Valori caricati.", + "Default values used.": "Usati Valori standard.", + "Error. Default values used.": "Errore. Valori standard usati.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Intervalli di tempo della glicemia obiettivo inferiore e superiore non corretti. Valori ripristinati a quelli standard.", + "Valid from:": "Valido da:", + "Save current record before switching to new?": "Salvare il dato corrente prima di passare ad uno nuovo?", + "Add new interval before": "Aggiungere prima un nuovo intervallo", + "Delete interval": "Elimina intervallo", + "I:C": "Insulina:Carboidrati", + "ISF": "Sensibilità insulinica", + "Combo Bolus": "Combo Bolo", + "Difference": "Differenza", + "New time": "Nuovo Orario", + "Edit Mode": "Modalità di Modifica", + "When enabled icon to start edit mode is visible": "Quando abilitata, l'icona della Modalità di Modifica è visibile", + "Operation": "Operazione", + "Move": "Sposta", + "Delete": "Elimina", + "Move insulin": "Sposta Insulina", + "Move carbs": "Sposta carboidrati", + "Remove insulin": "Rimuovi insulina", + "Remove carbs": "Rimuovi carboidrati", + "Change treatment time to %1 ?": "Cambiare tempo alla somministrazione a %1 ?", + "Change carbs time to %1 ?": "Cambiare durata carboidrati a %1 ?", + "Change insulin time to %1 ?": "Cambiare durata insulina a %1 ?", + "Remove treatment ?": "Rimuovere somministrazione ?", + "Remove insulin from treatment ?": "Rimuovere insulina dalla somministrazione ?", + "Remove carbs from treatment ?": "Rimuovere carboidrati dalla somministrazione ?", + "Rendering": "Rendering", + "Loading OpenAPS data of": "Caricamento dei dati OpenAPS di", + "Loading profile switch data": "Caricamento dei dati cambio profilo", + "Redirecting you to the Profile Editor to create a new profile.": "Reindirizzamento all'editor dei profili per creare un nuovo profilo.", + "Pump": "Microinfusore", + "Sensor Age": "Durata Sensore", + "Insulin Age": "Durata Insulina", + "Temporary target": "Target Temporaneo", + "Reason": "Ragionare", + "Eating soon": "Pasto a breve", + "Top": "Superiore", + "Bottom": "Inferiore", + "Activity": "Attività", + "Targets": "Obiettivi", + "Bolus insulin:": "Bolo insulina:", + "Base basal insulin:": "Insulina Basale:", + "Positive temp basal insulin:": "Insulina basale temp positiva:", + "Negative temp basal insulin:": "Insulina basale Temp negativa:", + "Total basal insulin:": "Insulina Basale Totale:", + "Total daily insulin:": "Totale giornaliero d'insulina:", + "Unable to save Role": "Impossibile salvare il ruolo", + "Unable to delete Role": "Impossibile eliminare il Ruolo", + "Database contains %1 roles": "Database contiene %1 ruolo", + "Edit Role": "Modifica ruolo", + "admin, school, family, etc": "amministrazione, scuola, famiglia, etc", + "Permissions": "Permessi", + "Are you sure you want to delete: ": "Sei sicuro di voler eliminare:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Ogni ruolo avrà un 1 o più autorizzazioni. Il * il permesso è un jolly, i permessi sono una gerarchia utilizzando : come separatore.", + "Add new Role": "Aggiungere un nuovo ruolo", + "Roles - Groups of People, Devices, etc": "Ruoli - gruppi di persone, dispositivi, etc", + "Edit this role": "Modifica questo ruolo", + "Admin authorized": "Amministratore autorizzato", + "Subjects - People, Devices, etc": "Soggetti - persone, dispositivi, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Ogni soggetto avrà un gettone d'accesso unico e 1 o più ruoli. Fare clic sul gettone d'accesso per aprire una nuova vista con il soggetto selezionato, questo legame segreto può quindi essere condiviso.", + "Add new Subject": "Aggiungere un nuovo Soggetto", + "Unable to save Subject": "Impossibile salvare il Soggetto", + "Unable to delete Subject": "Impossibile eliminare Soggetto", + "Database contains %1 subjects": "Database contiene %1 soggetti", + "Edit Subject": "Modifica Oggetto", + "person, device, etc": "persona, dispositivo, ecc", + "role1, role2": "ruolo1, ruolo2", + "Edit this subject": "Modifica questo argomento", + "Delete this subject": "Eliminare questo argomento", + "Roles": "Ruoli", + "Access Token": "Gettone d'accesso", + "hour ago": "ora fa", + "hours ago": "ore fa", + "Silence for %1 minutes": "Silenzio per %1 minuti", + "Check BG": "Controlla Glicemia", + "BASAL": "BASALE", + "Current basal": "Basale corrente", + "Sensitivity": "Sensibilità", + "Current Carb Ratio": "Attuale rapporto I:C", + "Basal timezone": "fuso orario basale", + "Active profile": "Profilo Attivo", + "Active temp basal": "Basale Temporanea Attiva", + "Active temp basal start": "Avvio Basale temporanea attiva", + "Active temp basal duration": "Durata basale temporanea Attiva", + "Active temp basal remaining": "Residuo basale temporanea Attiva", + "Basal profile value": "Valore profilo basale", + "Active combo bolus": "Combo bolo Attivo", + "Active combo bolus start": "Avvio Combo bolo Attivo", + "Active combo bolus duration": "Durata Combo bolo Attivo", + "Active combo bolus remaining": "Residuo Combo bolo Attivo", + "BG Delta": "Differenza di Glicemia", + "Elapsed Time": "Tempo Trascorso", + "Absolute Delta": "Delta Assoluto", + "Interpolated": "Interpolata", + "BWP": "Calcolatore di Bolo", + "Urgent": "Urgente", + "Warning": "Avviso", + "Info": "Informazioni", + "Lowest": "Minore", + "Snoozing high alarm since there is enough IOB": "Addormenta allarme alto poiché non vi è sufficiente Insulina attiva", + "Check BG, time to bolus?": "Controllare Glicemia, ora di bolo?", + "Notice": "Preavviso", + "required info missing": "richiesta informazioni mancanti", + "Insulin on Board": "Insulina Attiva", + "Current target": "Obiettivo attuale", + "Expected effect": "Effetto Previsto", + "Expected outcome": "Risultato previsto", + "Carb Equivalent": "Carb equivalenti", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "L'eccesso d'insulina equivalente %1U più che necessari per raggiungere l'obiettivo basso, non rappresentano i carboidrati.", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "L' eccesso d'insulina equivale a %1U in più di quanto necessario per raggiungere l'obiettivo basso, ASSICURARSI CHE L'INSULINA ATTIVA SIA COPERTA DAI CARBOIDRATI", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Riduzione %1U necessaria dell'insulina attiva per raggiungere l'obiettivo basso, troppa basale?", + "basal adjustment out of range, give carbs?": "regolazione basale fuori intervallo, dare carboidrati?", + "basal adjustment out of range, give bolus?": "regolazione basale fuori campo, dare bolo?", + "above high": "sopra alto", + "below low": "sotto bassa", + "Projected BG %1 target": "Proiezione Glicemia %1 obiettivo", + "aiming at": "puntare a", + "Bolus %1 units": "Bolo %1 unità", + "or adjust basal": "o regolare basale", + "Check BG using glucometer before correcting!": "Controllare Glicemia utilizzando glucometro prima di correggere!", + "Basal reduction to account %1 units:": "Riduzione basale per conto %1 unità:", + "30m temp basal": "30m basale temp", + "1h temp basal": "1h basale temp", + "Cannula change overdue!": "Cambio Cannula in ritardo!", + "Time to change cannula": "Tempo di cambiare la cannula", + "Change cannula soon": "Cambio cannula prossimamente", + "Cannula age %1 hours": "Durata Cannula %1 ore", + "Inserted": "Inserito", + "CAGE": "Cambio Ago", + "COB": "Carboidrati Attivi", + "Last Carbs": "Ultimi carboidrati", + "IAGE": "Età Insulina", + "Insulin reservoir change overdue!": "Cambio serbatoio d'insulina in ritardo!", + "Time to change insulin reservoir": "Momento di cambiare serbatoio d'insulina", + "Change insulin reservoir soon": "Cambiare serbatoio d'insulina prossimamente", + "Insulin reservoir age %1 hours": "IAGE - Durata Serbatoio d'insulina %1 ore", + "Changed": "Cambiato", + "IOB": "Insulina Attiva", + "Careportal IOB": "IOB Somministrazioni", + "Last Bolus": "Ultimo bolo", + "Basal IOB": "Basale IOB", + "Source": "Fonte", + "Stale data, check rig?": "dati non aggiornati, controllare il telefono?", + "Last received:": "Ultime ricevute:", + "%1m ago": "%1m fa", + "%1h ago": "%1h fa", + "%1d ago": "%1d fa", + "RETRO": "RETRO", + "SAGE": "Età Sensore", + "Sensor change/restart overdue!": "Cambio/riavvio del sensore in ritardo!", + "Time to change/restart sensor": "Tempo di cambiare/riavvio sensore", + "Change/restart sensor soon": "Modifica/riavvio sensore prossimamente", + "Sensor age %1 days %2 hours": "Durata Sensore %1 giorni %2 ore", + "Sensor Insert": "SAGE - inserimento sensore", + "Sensor Start": "SAGE - partenza sensore", + "days": "giorni", + "Insulin distribution": "Distribuzione di insulina", + "To see this report, press SHOW while in this view": "Per guardare questo report, premere MOSTRA all'interno della finestra", + "AR2 Forecast": "Previsione AR2", + "OpenAPS Forecasts": "Previsione OpenAPS", + "Temporary Target": "Obiettivo temporaneo", + "Temporary Target Cancel": "Obiettivo temporaneo cancellato", + "OpenAPS Offline": "OpenAPS disconnesso", + "Profiles": "Profili", + "Time in fluctuation": "Tempo in fluttuazione", + "Time in rapid fluctuation": "Tempo in rapida fluttuazione", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Questa è solo un'approssimazione che può essere molto inaccurata e che non sostituisce la misurazione capillare. La formula usata è presa da:", + "Filter by hours": "Filtra per ore", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Tempo in fluttuazione e Tempo in rapida fluttuazione misurano la % di tempo durante il periodo esaminato, durante il quale la glicemia stà variando velocemente o rapidamente. Bassi valori sono migliori.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Media Totale Giornaliera Variazioni è la somma dei valori assoluti di tutte le escursioni glicemiche per il periodo esaminato, diviso per il numero di giorni. Bassi valori sono migliori.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Media Oraria Variazioni è la somma del valore assoluto di tutte le escursioni glicemiche per il periodo esaminato, diviso per il numero di ore. Bassi valori sono migliori.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Il RMS fuori gamma è calcolato facendo quadrare la distanza fuori campo per tutte le letture di glucosio per il periodo esaminato, sommando loro, dividendo per il conteggio e prendendo la radice quadrata. Questa metrica è simile alla percentuale dell'intervallo ma le letture dei pesi sono molto più alte. I valori più bassi sono migliori.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">qui.", + "Mean Total Daily Change": "Media Totale Giornaliera Variazioni", + "Mean Hourly Change": "Media Oraria Variazioni", + "FortyFiveDown": "leggera diminuzione", + "FortyFiveUp": "leggero aumento", + "Flat": "stabile", + "SingleUp": "aumento", + "SingleDown": "diminuzione", + "DoubleDown": "rapida diminuzione", + "DoubleUp": "rapido aumento", + "virtAsstUnknown": "Questo valore al momento non è noto. Per maggiori dettagli consulta il tuo sito Nightscout.", + "virtAsstTitleAR2Forecast": "Previsione AR2", + "virtAsstTitleCurrentBasal": "Basale corrente", + "virtAsstTitleCurrentCOB": "COB Corrente", + "virtAsstTitleCurrentIOB": "IOB Corrente", + "virtAsstTitleLaunch": "Benvenuto in Nightscout", + "virtAsstTitleLoopForecast": "Previsione Ciclo", + "virtAsstTitleLastLoop": "Ultimo Ciclo", + "virtAsstTitleOpenAPSForecast": "Previsione OpenAPS", + "virtAsstTitlePumpReservoir": "Insulina Rimanente", + "virtAsstTitlePumpBattery": "Batteria Microinfusore", + "virtAsstTitleRawBG": "Glicemia Grezza Corrente", + "virtAsstTitleUploaderBattery": "Batteria del Caricatore", + "virtAsstTitleCurrentBG": "Glicemia Corrente", + "virtAsstTitleFullStatus": "Stato Completo", + "virtAsstTitleCGMMode": "Modalità CGM", + "virtAsstTitleCGMStatus": "Stato CGM", + "virtAsstTitleCGMSessionAge": "Età Sessione Sensore", + "virtAsstTitleCGMTxStatus": "Stato Trasmettitore Sensore", + "virtAsstTitleCGMTxAge": "Età Trasmettitore del Sensore", + "virtAsstTitleCGMNoise": "Disturbo Sensore", + "virtAsstTitleDelta": "Delta Glicemia", + "virtAsstStatus": "%1 e %2 come %3.", + "virtAsstBasal": "%1 basale attuale è %2 unità per ora", + "virtAsstBasalTemp": "%1 basale temporanea di %2 unità per ora finirà %3", + "virtAsstIob": "e tu hai %1 insulina attiva.", + "virtAsstIobIntent": "Tu hai %1 insulina attiva", + "virtAsstIobUnits": "%1 unità di", + "virtAsstLaunch": "Cosa vorresti controllare su Nightscout?", + "virtAsstPreamble": "Tuo", + "virtAsstPreamble3person": "%1 ha un ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "La batteria del caricatore è a %1", + "virtAsstReservoir": "Hai %1 unità rimanenti", + "virtAsstPumpBattery": "La batteria del microinsusore è a %1 %2", + "virtAsstUploaderBattery": "La batteria del caricatore è a %1", + "virtAsstLastLoop": "L'ultimo successo del ciclo è stato %1", + "virtAsstLoopNotAvailable": "Il plugin del ciclo non sembra essere abilitato", + "virtAsstLoopForecastAround": "Secondo le previsioni del ciclo ci si aspetta che ci siano circa %1 nei prossimi %2", + "virtAsstLoopForecastBetween": "Secondo le previsioni di ciclo ci si aspetta che siano tra %1 e %2 per i prossimi %3", + "virtAsstAR2ForecastAround": "Secondo le previsioni AR2 ci si aspetta che ci siano circa %1 nei prossimi %2", + "virtAsstAR2ForecastBetween": "Secondo le previsioni AR2 ci si aspetta che siano tra %1 e %2 nei prossimi %3", + "virtAsstForecastUnavailable": "Impossibile prevedere con i dati disponibili", + "virtAsstRawBG": "La tua Glicemia grezza è %1", + "virtAsstOpenAPSForecast": "L'eventuale Glicemia OpenAPS è %1", + "virtAsstCob3person": "%1 ha %2 carboidrati attivi", + "virtAsstCob": "Hai %1 carboidrati attivi", + "virtAsstCGMMode": "La modalità del tuo Sensore era %1 a partire da %2.", + "virtAsstCGMStatus": "Il stato del tuo Sensore era %1 a partire da %2.", + "virtAsstCGMSessAge": "La tua sessione del Sensore e' attiva da %1 giorni e %2 ore.", + "virtAsstCGMSessNotStarted": "Al momento non c'è una sessione attiva del Sensore.", + "virtAsstCGMTxStatus": "Lo stato del trasmettitore del tuo Sensore era %1 a partire da %2.", + "virtAsstCGMTxAge": "Il trasmettitore del tuo sensore ha %1 giorni.", + "virtAsstCGMNoise": "Il rumore del tuo sensore era %1 a partire da %2.", + "virtAsstCGMBattOne": "La batteria del tuo Sensore era %1 volt da %2.", + "virtAsstCGMBattTwo": "I livelli della batteria del Sensore erano %1 volt e %2 volt a partire da %3.", + "virtAsstDelta": "Il tuo delta era %1 tra %2 e %3.", + "virtAsstDeltaEstimated": "Il tuo delta stimato era %1 tra %2 e %3.", + "virtAsstUnknownIntentTitle": "Intenzioni Sconosciute", + "virtAsstUnknownIntentText": "Mi dispiace, non so cosa state chiedendo.", + "Fat [g]": "Grassi [g]", + "Protein [g]": "Proteine [g]", + "Energy [kJ]": "Energia [kJ]", + "Clock Views:": "Vista orologio:", + "Clock": "Orologio", + "Color": "Colore", + "Simple": "Semplice", + "TDD average": "Totale Dose Giornaliera media (TDD)", + "Bolus average": "Media bolo", + "Basal average": "Media basale", + "Base basal average:": "Media basale originale:", + "Carbs average": "Media carboidrati", + "Eating Soon": "Mangiare presto", + "Last entry {0} minutes ago": "Ultimo inserimento {0} minuti fa", + "change": "cambio", + "Speech": "Voce", + "Target Top": "Limite superiore", + "Target Bottom": "Limite inferiore", + "Canceled": "Cancellato", + "Meter BG": "Glicemia Capillare", + "predicted": "predetto", + "future": "futuro", + "ago": "fa", + "Last data received": "Ultimo dato ricevuto", + "Clock View": "Vista orologio", + "Protein": "Proteine", + "Fat": "Grasso", + "Protein average": "Media delle proteine", + "Fat average": "Media dei grassi", + "Total carbs": "Carboidrati Totali", + "Total protein": "Proteine Totali", + "Total fat": "Grasso Totale", + "Database Size": "Dimensione Del Database", + "Database Size near its limits!": "Dimensione del database vicino ai suoi limiti!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "La dimensione del database è %1 MiB su %2 MiB. Si prega di eseguire il backup e ripulire il database!", + "Database file size": "Dimensione file database", + "%1 MiB of %2 MiB (%3%)": "%1 MiB di %2 MiB (%3%)", + "Data size": "Dimensione dati", + "virtAsstDatabaseSize": "%1 MiB. Questo è il %2% dello spazio del database disponibile.", + "virtAsstTitleDatabaseSize": "Dimensione file database", + "Carbs/Food/Time": "Carboidrati/Cibo/Tempo", + "You have administration messages": "Hai messaggi di amministrazione", + "Admin messages in queue": "Messaggi di amministrazione in coda", + "Queue empty": "Coda vuota", + "There are no admin messages in queue": "Non ci sono messaggi di amministrazione in coda", + "Please sign in using the API_SECRET to see your administration messages": "Effettua il login utilizzando API_SECRET per vedere i messaggi di amministrazione", + "Reads enabled in default permissions": "Letture abilitate nelle autorizzazioni predefinite", + "Data reads enabled": "Lettura dati abilitata", + "Data writes enabled": "Scrittura dati abilitata", + "Data writes not enabled": "Scrittura dati non abilitata", + "Color prediction lines": "Colore linee di previsione", + "Release Notes": "Note sulla versione", + "Check for Updates": "Cerca aggiornamenti", + "Open Source": "Open Source", + "Nightscout Info": "Info su Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Lo scopo principale del Loopalyzer è quello di visualizzare le prestazioni del sistema closed loop. Può funzionare anche con altri setup, sia closed che open loop, e senza loop. Tuttavia, a seconda di quale uploader si utilizza, quanto spesso è in grado di raccogliere i dati e caricare, e come è in grado di riempire i dati mancanti alcuni grafici possono avere lacune o anche essere completamente vuoti. Assicuratevi sempre che i grafici siano accettabili. Il modo migliore è quello di visualizzare un giorno alla volta e di scorrere un certo numero di giorni prima di vederli.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Il Loopalyzer include una funzione di cambio di orario. Se ad esempio fate colazione alle 07:00 e alle 08:00 il giorno dopo, la curva glicemica media di questi due giorni probabilmente risulterà appiattita e non mostrerà la risposta effettiva dopo la colazione. Il cambio orario calcolerà il tempo medio in cui questi pasti sono stati consumati e poi sposterà tutti i dati (carboidrati, insulina, basale, ecc.) in entrambi i giorni della corrispondente differenza di tempo, in modo che entrambi i pasti siano allineati con l'ora media di inizio del pasto.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In questo esempio tutti i dati del primo giorno sono spostati di 30 minuti in avanti e tutti i dati del secondo giorno di 30 minuti indietro, in modo da sembrare come se aveste fatto colazione alle 07:30 in entrambi i giorni. In questo modo è possibile vedere la risposta media effettiva della glicemia da un pasto.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Il cambio di orario evidenzia in grigio il periodo dopo l'ora media di inizio del pasto, per la durata della DIA (Durata Azione dell'Insulina). Poiché tutti i dati vengono spostati per l'intera giornata, le curve al di fuori dell'area grigia potrebbero non essere precise.", + "Note that time shift is available only when viewing multiple days.": "Nota che il cambio di orario è disponibile solo quando si visualizzano più giorni.", + "Please select a maximum of two weeks duration and click Show again.": "Seleziona un periodo massimo di due settimane e fai clic su Mostra di nuovo.", + "Show profiles table": "Mostra tabella profili", + "Show predictions": "Visualizza previsioni", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Spostamento d'ora su pasti con più di %1 g di carboidrati consumati tra %2 e %3", + "Previous": "Precendente", + "Previous day": "Giorno precedente", + "Next day": "Giorno seguente", + "Next": "Seguente", + "Temp basal delta": "Differenza basale temporanea", + "Authorized by token": "Autorizzato da token", + "Auth role": "Ruolo autenticazione", + "view without token": "visualizza senza token", + "Remove stored token": "Rimuovi token memorizzato", + "Weekly Distribution": "Distribuzione settimanale", + "Failed authentication": "Autenticazione non riuscita", + "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?": "Un dispositivo all'indirizzo IP %1 ha tentato di autenticarsi con Nightscout con credenziali errate. Controlla se hai una configurazione di uploader con API_SECRET o token errati.", + "Default (with leading zero and U)": "Predefinito (con zero iniziale e U)", + "Concise (with U, without leading zero)": "Ridotto (con U, senza zero iniziale)", + "Minimal (without leading zero and U)": "Minimo (senza zero iniziale e U)", + "Small Bolus Display": "Visualizzazione bolo piccolo", + "Large Bolus Display": "Visualizzazione bolo grande", + "Bolus Display Threshold": "Soglia visualizzazione bolo", + "%1 U and Over": "%1 U e maggiore", + "Event repeated %1 times.": "Evento ripetuto %1 volte.", + "minutes": "minuti", + "Last recorded %1 %2 ago.": "Ultima registrazione %1 %2 fa.", + "Security issue": "Problema di sicurezza", + "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.": "Rilevata API_SECRET debole. Si prega di utilizzare lettere minuscole e maiuscole, numeri e caratteri non alfanumerici come !#%&/ per ridurre il rischio di accesso non autorizzato. La lunghezza minima dell'API_SECRET è di 12 caratteri.", + "less than 1": "meno di 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "La password di MongoDB e API_SECRET coincidono. Questa è una cattiva idea. Cambiale entrambe e non riutilizzare le password in tutto il sistema." +} diff --git a/translations/ja_JP.json b/translations/ja_JP.json new file mode 100644 index 00000000000..7b53b4aee78 --- /dev/null +++ b/translations/ja_JP.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "接続可能", + "Mo": "月", + "Tu": "火", + "We": "水", + "Th": "木", + "Fr": "金", + "Sa": "土", + "Su": "日", + "Monday": "月曜日", + "Tuesday": "火曜日", + "Wednesday": "水曜日", + "Thursday": "木曜日", + "Friday": "金曜日", + "Saturday": "土曜日", + "Sunday": "日曜日", + "Category": "カテゴリー", + "Subcategory": "サブカテゴリー", + "Name": "名前", + "Today": "今日", + "Last 2 days": "直近の2日間", + "Last 3 days": "直近の3日間", + "Last week": "直近の1週間", + "Last 2 weeks": "直近の2週間", + "Last month": "直近の1ヶ月", + "Last 3 months": "直近の3ヶ月", + "From": "開始日", + "To": "終了日", + "Notes": "メモ", + "Food": "食事", + "Insulin": "インスリン", + "Carbs": "炭水化物", + "Notes contain": "メモ内容", + "Target BG range bottom": "目標血糖値 下限", + "top": "上限", + "Show": "作成", + "Display": "表示", + "Loading": "ロード中", + "Loading profile": "プロフィールロード中", + "Loading status": "ステータスロード中", + "Loading food database": "食事データベースロード中", + "not displayed": "表示できません", + "Loading CGM data of": "CGMデータロード中", + "Loading treatments data of": "治療データロード中", + "Processing data of": "データ処理中の日付:", + "Portion": "一食分", + "Size": "量", + "(none)": "(データなし)", + "None": "データなし", + "": "<データなし>", + "Result is empty": "結果がありません", + "Day to day": "日々", + "Week to week": "Week to week", + "Daily Stats": "1日統計", + "Percentile Chart": "パーセント図", + "Distribution": "配分", + "Hourly stats": "1時間統計", + "netIOB stats": "体内残存インスリン量", + "temp basals must be rendered to display this report": "このレポートを表示する為には日々レポートを先に表示する必要があります。", + "No data available": "利用できるデータがありません", + "Low": "目標血糖値低値", + "In Range": "目標血糖値範囲内", + "Period": "期間", + "High": "目標血糖値高値", + "Average": "平均値", + "Low Quartile": "下四分位値", + "Upper Quartile": "高四分位値", + "Quartile": "四分位値", + "Date": "日付", + "Normal": "通常", + "Median": "中央値", + "Readings": "読み込み", + "StDev": "標準偏差", + "Daily stats report": "1日ごとの統計のレポート", + "Glucose Percentile report": "グルコース(%)レポート", + "Glucose distribution": "血糖値の分布", + "days total": "合計日数", + "Total per day": "日毎の合計", + "Overall": "総合", + "Range": "範囲", + "% of Readings": "%精度", + "# of Readings": "#精度", + "Mean": "意味", + "Standard Deviation": "標準偏差", + "Max": "最大値", + "Min": "最小値", + "A1c estimation*": "予想HbA1c", + "Weekly Success": "週間達成度", + "There is not sufficient data to run this report. Select more days.": "レポートするためのデータが足りません。もっと多くの日を選択してください。", + "Using stored API secret hash": "保存されたAPI secret hashを使用する", + "No API secret hash stored yet. You need to enter API secret.": "API secret hashがまだ保存されていません。API secretの入力が必要です。", + "Database loaded": "データベースロード完了", + "Error: Database failed to load": "エラー:データベースを読み込めません", + "Error": "エラー", + "Create new record": "新しい記録を作る", + "Save record": "保存", + "Portions": "一食分", + "Unit": "単位", + "GI": "GI", + "Edit record": "記録編集", + "Delete record": "記録削除", + "Move to the top": "トップ画面へ", + "Hidden": "隠す", + "Hide after use": "使用後に隠す", + "Your API secret must be at least 12 characters long": "APIシークレットは12文字以上の長さが必要です", + "Bad API secret": "APIシークレットは正しくありません", + "API secret hash stored": "APIシークレットを保存出来ました", + "Status": "統計", + "Not loaded": "読み込めません", + "Food Editor": "食事編集", + "Your database": "あなたのデータベース", + "Filter": "フィルター", + "Save": "保存", + "Clear": "クリア", + "Record": "記録", + "Quick picks": "クイック選択", + "Show hidden": "表示する", + "Your API secret or token": "Your API secret or token", + "Remember this device. (Do not enable this on public computers.)": "Remember this device. (Do not enable this on public computers.)", + "Treatments": "治療", + "Time": "時間", + "Event Type": "イベント", + "Blood Glucose": "血糖値", + "Entered By": "入力者", + "Delete this treatment?": "この治療データを削除しますか?", + "Carbs Given": "摂取糖質量", + "Insulin Given": "投与されたインスリン", + "Event Time": "イベント時間", + "Please verify that the data entered is correct": "入力したデータが正しいか確認をお願いします。", + "BG": "血糖", + "Use BG correction in calculation": "ボーラス計算機能使用", + "BG from CGM (autoupdated)": "CGMグルコース値", + "BG from meter": "血糖測定器使用グルコース値", + "Manual BG": "手動入力グルコース値", + "Quickpick": "クイック選択", + "or": "または", + "Add from database": "データベースから追加", + "Use carbs correction in calculation": "糖質量計算機能を使用", + "Use COB correction in calculation": "COB補正計算を使用", + "Use IOB in calculation": "IOB計算を使用", + "Other correction": "その他の補正", + "Rounding": "端数処理", + "Enter insulin correction in treatment": "治療にインスリン補正を入力する。", + "Insulin needed": "必要インスリン単位", + "Carbs needed": "必要糖質量", + "Carbs needed if Insulin total is negative value": "インスリン合計値がマイナスであればカーボ値入力が必要です。", + "Basal rate": "基礎インスリン割合", + "60 minutes earlier": "60分前", + "45 minutes earlier": "45分前", + "30 minutes earlier": "30分前", + "20 minutes earlier": "20分前", + "15 minutes earlier": "15分前", + "Time in minutes": "Time in minutes", + "15 minutes later": "15分後", + "20 minutes later": "20分後", + "30 minutes later": "30分後", + "45 minutes later": "45分後", + "60 minutes later": "60分後", + "Additional Notes, Comments": "追加メモ、コメント", + "RETRO MODE": "振り返りモード", + "Now": "今", + "Other": "他の", + "Submit Form": "フォームを投稿する", + "Profile Editor": "プロフィール編集", + "Reports": "報告", + "Add food from your database": "データベースから食べ物を追加", + "Reload database": "データベース再読み込み", + "Add": "追加", + "Unauthorized": "認証されていません", + "Entering record failed": "入力されたものは記録できませんでした", + "Device authenticated": "機器は認証されました。", + "Device not authenticated": "機器は認証されていません。", + "Authentication status": "認証ステータス", + "Authenticate": "認証", + "Remove": "除く", + "Your device is not authenticated yet": "機器はまだ承認されていません。", + "Sensor": "センサー", + "Finger": "指", + "Manual": "手動入力", + "Scale": "グラフ縦軸", + "Linear": "線形軸表示", + "Logarithmic": "対数軸表示", + "Logarithmic (Dynamic)": "対数軸(動的)表示", + "Insulin-on-Board": "IOB・残存インスリン", + "Carbs-on-Board": "COB・残存カーボ", + "Bolus Wizard Preview": "BWP・ボーラスウィザード参照", + "Value Loaded": "数値読み込み完了", + "Cannula Age": "CAGE・カニューレ使用日数", + "Basal Profile": "ベーサルプロフィール", + "Silence for 30 minutes": "30分静かにする", + "Silence for 60 minutes": "60分静かにする", + "Silence for 90 minutes": "90分静かにする", + "Silence for 120 minutes": "120分静かにする", + "Settings": "設定", + "Units": "単位", + "Date format": "日数初期化", + "12 hours": "12時間制", + "24 hours": "24時間制", + "Log a Treatment": "治療を記録", + "BG Check": "BG測定", + "Meal Bolus": "食事ボーラス", + "Snack Bolus": "間食ボーラス", + "Correction Bolus": "補正ボーラス", + "Carb Correction": "カーボ治療", + "Note": "メモ", + "Question": "質問", + "Exercise": "運動", + "Pump Site Change": "CAGE・ポンプ注入場所変更", + "CGM Sensor Start": "CGMセンサー開始", + "CGM Sensor Stop": "CGM センサー停止", + "CGM Sensor Insert": "CGMセンサー挿入", + "Sensor Code": "Sensor Code", + "Transmitter ID": "Transmitter ID", + "Dexcom Sensor Start": "Dexcomセンサー開始", + "Dexcom Sensor Change": "Dexcomセンサー挿入", + "Insulin Cartridge Change": "インスリンリザーバー交換", + "D.A.D. Alert": "メディカルアラート犬(DAD)の知らせ", + "Glucose Reading": "血糖値測定", + "Measurement Method": "測定方法", + "Meter": "血糖測定器", + "Amount in grams": "グラム換算", + "Amount in units": "単位換算", + "View all treatments": "全治療内容を参照", + "Enable Alarms": "アラームを有効にする", + "Pump Battery Change": "ポンプバッテリー交換", + "Pump Battery Age": "Pump Battery Age", + "Pump Battery Low Alarm": "ポンプバッテリーが低下", + "Pump Battery change overdue!": "ポンプバッテリー交換期限切れてます!", + "When enabled an alarm may sound.": "有効にすればアラームが鳴動します。", + "Urgent High Alarm": "緊急高血糖アラーム", + "High Alarm": "高血糖アラーム", + "Low Alarm": "低血糖アラーム", + "Urgent Low Alarm": "緊急低血糖アラーム", + "Stale Data: Warn": "注意:古いデータ", + "Stale Data: Urgent": "緊急:古いデータ", + "mins": "分", + "Night Mode": "夜間モード", + "When enabled the page will be dimmed from 10pm - 6am.": "有効にすると、ページは 夜22時から 朝6時まで単色表示になります。", + "Enable": "有効", + "Show Raw BG Data": "素のBGデータを表示する", + "Never": "決して", + "Always": "いつも", + "When there is noise": "測定不良があった時", + "When enabled small white dots will be displayed for raw BG data": "有効にすると、小さい白ドットが素のBGデータ用に表示されます", + "Custom Title": "カスタムタイトル", + "Theme": "テーマ", + "Default": "デフォルト", + "Colors": "色付き", + "Colorblind-friendly colors": "色覚異常の方向けの色", + "Reset, and use defaults": "リセットしてデフォルト設定を使用", + "Calibrations": "較生", + "Alarm Test / Smartphone Enable": "アラームテスト/スマートフォンを有効にする", + "Bolus Wizard": "ボーラスウィザード", + "in the future": "先の時間", + "time ago": "時間前", + "hr ago": "時間前", + "hrs ago": "時間前", + "min ago": "分前", + "mins ago": "分前", + "day ago": "日前", + "days ago": "日前", + "long ago": "前の期間", + "Clean": "なし", + "Light": "軽い", + "Medium": "中間", + "Heavy": "重たい", + "Treatment type": "治療タイプ", + "Raw BG": "Raw BG", + "Device": "機器", + "Noise": "測定不良", + "Calibration": "較正", + "Show Plugins": "プラグイン表示", + "About": "約", + "Value in": "数値", + "Carb Time": "カーボ時間", + "Language": "言語", + "Add new": "新たに加える", + "g": "g", + "ml": "ml", + "pcs": "pcs", + "Drag&drop food here": "Drag&drop food here", + "Care Portal": "ケアポータル", + "Medium/Unknown": "Medium/Unknown", + "IN THE FUTURE": "IN THE FUTURE", + "Order": "Order", + "oldest on top": "古いものを先頭に", + "newest on top": "新しいものを先頭に", + "All sensor events": "全てのセンサーのイベント", + "Remove future items from mongo database": "Remove future items from mongo database", + "Find and remove treatments in the future": "先の時間の治療を見つけて削除する", + "This task find and remove treatments in the future.": "このタスクで先の時間の治療を見つけて削除する", + "Remove treatments in the future": "先の時間の治療を削除する", + "Find and remove entries in the future": "先の時間の記入を見つけて削除する", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "このタスクでアップローダーによって、間違った日時で作られた先の時間のCGMデータを見つけて削除します。", + "Remove entries in the future": "先の時間の記入を削除する", + "Loading database ...": "データベース読み込み。。。", + "Database contains %1 future records": "データベースは %1件今後の記録を含みます", + "Remove %1 selected records?": "%1件の選択した記録を削除しますか?", + "Error loading database": "データベースの読み込み時にエラーが発生しました", + "Record %1 removed ...": "Record %1 removed ...", + "Error removing record %1": "Error removing record %1", + "Deleting records ...": "記録を削除します。。。", + "%1 records deleted": "%1件の記録を削除しました", + "Clean Mongo status database": "Clean Mongo status database", + "Delete all documents from devicestatus collection": "Delete all documents from devicestatus collection", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.", + "Delete all documents": "Delete all documents", + "Delete all documents from devicestatus collection?": "Delete all documents from devicestatus collection?", + "Database contains %1 records": "Database contains %1 records", + "All records removed ...": "All records removed ...", + "Delete all documents from devicestatus collection older than 30 days": "Delete all documents from devicestatus collection older than 30 days", + "Number of Days to Keep:": "Number of Days to Keep:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from devicestatus collection?": "Delete old documents from devicestatus collection?", + "Clean Mongo entries (glucose entries) database": "Clean Mongo entries (glucose entries) database", + "Delete all documents from entries collection older than 180 days": "Delete all documents from entries collection older than 180 days", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents": "Delete old documents", + "Delete old documents from entries collection?": "Delete old documents from entries collection?", + "%1 is not a valid number": "%1 is not a valid number", + "%1 is not a valid number - must be more than 2": "%1 is not a valid number - must be more than 2", + "Clean Mongo treatments database": "Clean Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Delete all documents from treatments collection older than 180 days", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "Admin Tools", + "Nightscout reporting": "Nightscout reporting", + "Cancel": "中止", + "Edit treatment": "Edit treatment", + "Duration": "Duration", + "Duration in minutes": "Duration in minutes", + "Temp Basal": "Temp Basal", + "Temp Basal Start": "Temp Basal Start", + "Temp Basal End": "Temp Basal End", + "Percent": "Percent", + "Basal change in %": "Basal change in %", + "Basal value": "Basal value", + "Absolute basal value": "Absolute basal value", + "Announcement": "Announcement", + "Loading temp basal data": "Loading temp basal data", + "Save current record before changing to new?": "Save current record before changing to new?", + "Profile Switch": "Profile Switch", + "Profile": "Profile", + "General profile settings": "General profile settings", + "Title": "Title", + "Database records": "Database records", + "Add new record": "新しい記録を加える", + "Remove this record": "この記録を除く", + "Clone this record to new": "Clone this record to new", + "Record valid from": "Record valid from", + "Stored profiles": "Stored profiles", + "Timezone": "Timezone", + "Duration of Insulin Activity (DIA)": "Duration of Insulin Activity (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.", + "Insulin to carb ratio (I:C)": "Insulin to carb ratio (I:C)", + "Hours:": "Hours:", + "hours": "hours", + "g/hour": "g/hour", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.", + "Insulin Sensitivity Factor (ISF)": "Insulin Sensitivity Factor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.", + "Carbs activity / absorption rate": "Carbs activity / absorption rate", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).", + "Basal rates [unit/hour]": "Basal rates [unit/hour]", + "Target BG range [mg/dL,mmol/L]": "Target BG range [mg/dL,mmol/L]", + "Start of record validity": "Start of record validity", + "Icicle": "Icicle", + "Render Basal": "Render Basal", + "Profile used": "Profile used", + "Calculation is in target range.": "Calculation is in target range.", + "Loading profile records ...": "Loading profile records ...", + "Values loaded.": "Values loaded.", + "Default values used.": "Default values used.", + "Error. Default values used.": "Error. Default values used.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Time ranges of target_low and target_high don't match. Values are restored to defaults.", + "Valid from:": "Valid from:", + "Save current record before switching to new?": "Save current record before switching to new?", + "Add new interval before": "Add new interval before", + "Delete interval": "Delete interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Combo Bolus", + "Difference": "Difference", + "New time": "New time", + "Edit Mode": "編集モード", + "When enabled icon to start edit mode is visible": "When enabled icon to start edit mode is visible", + "Operation": "Operation", + "Move": "Move", + "Delete": "削除", + "Move insulin": "Move insulin", + "Move carbs": "Move carbs", + "Remove insulin": "Remove insulin", + "Remove carbs": "Remove carbs", + "Change treatment time to %1 ?": "Change treatment time to %1 ?", + "Change carbs time to %1 ?": "Change carbs time to %1 ?", + "Change insulin time to %1 ?": "Change insulin time to %1 ?", + "Remove treatment ?": "Remove treatment ?", + "Remove insulin from treatment ?": "Remove insulin from treatment ?", + "Remove carbs from treatment ?": "Remove carbs from treatment ?", + "Rendering": "Rendering", + "Loading OpenAPS data of": "Loading OpenAPS data of", + "Loading profile switch data": "Loading profile switch data", + "Redirecting you to the Profile Editor to create a new profile.": "Redirecting you to the Profile Editor to create a new profile.", + "Pump": "Pump", + "Sensor Age": "Sensor Age", + "Insulin Age": "Insulin Age", + "Temporary target": "Temporary target", + "Reason": "Reason", + "Eating soon": "Eating soon", + "Top": "Top", + "Bottom": "Bottom", + "Activity": "Activity", + "Targets": "Targets", + "Bolus insulin:": "Bolus insulin:", + "Base basal insulin:": "Base basal insulin:", + "Positive temp basal insulin:": "Positive temp basal insulin:", + "Negative temp basal insulin:": "Negative temp basal insulin:", + "Total basal insulin:": "Total basal insulin:", + "Total daily insulin:": "Total daily insulin:", + "Unable to save Role": "Unable to save Role", + "Unable to delete Role": "Unable to delete Role", + "Database contains %1 roles": "Database contains %1 roles", + "Edit Role": "Edit Role", + "admin, school, family, etc": "admin, school, family, etc", + "Permissions": "Permissions", + "Are you sure you want to delete: ": "Are you sure you want to delete: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.", + "Add new Role": "Add new Role", + "Roles - Groups of People, Devices, etc": "Roles - Groups of People, Devices, etc", + "Edit this role": "Edit this role", + "Admin authorized": "Admin authorized", + "Subjects - People, Devices, etc": "Subjects - People, Devices, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.", + "Add new Subject": "Add new Subject", + "Unable to save Subject": "Unable to save Subject", + "Unable to delete Subject": "Unable to delete Subject", + "Database contains %1 subjects": "Database contains %1 subjects", + "Edit Subject": "Edit Subject", + "person, device, etc": "person, device, etc", + "role1, role2": "role1, role2", + "Edit this subject": "Edit this subject", + "Delete this subject": "Delete this subject", + "Roles": "Roles", + "Access Token": "Access Token", + "hour ago": "hour ago", + "hours ago": "hours ago", + "Silence for %1 minutes": "Silence for %1 minutes", + "Check BG": "Check BG", + "BASAL": "BASAL", + "Current basal": "Current basal", + "Sensitivity": "Sensitivity", + "Current Carb Ratio": "Current Carb Ratio", + "Basal timezone": "Basal timezone", + "Active profile": "Active profile", + "Active temp basal": "Active temp basal", + "Active temp basal start": "Active temp basal start", + "Active temp basal duration": "Active temp basal duration", + "Active temp basal remaining": "Active temp basal remaining", + "Basal profile value": "Basal profile value", + "Active combo bolus": "Active combo bolus", + "Active combo bolus start": "Active combo bolus start", + "Active combo bolus duration": "Active combo bolus duration", + "Active combo bolus remaining": "Active combo bolus remaining", + "BG Delta": "BG Delta", + "Elapsed Time": "Elapsed Time", + "Absolute Delta": "Absolute Delta", + "Interpolated": "Interpolated", + "BWP": "BWP", + "Urgent": "緊急", + "Warning": "Warning", + "Info": "Info", + "Lowest": "Lowest", + "Snoozing high alarm since there is enough IOB": "Snoozing high alarm since there is enough IOB", + "Check BG, time to bolus?": "Check BG, time to bolus?", + "Notice": "Notice", + "required info missing": "required info missing", + "Insulin on Board": "Insulin on Board", + "Current target": "Current target", + "Expected effect": "Expected effect", + "Expected outcome": "Expected outcome", + "Carb Equivalent": "Carb Equivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U reduction needed in active insulin to reach low target, too much basal?", + "basal adjustment out of range, give carbs?": "basal adjustment out of range, give carbs?", + "basal adjustment out of range, give bolus?": "basal adjustment out of range, give bolus?", + "above high": "above high", + "below low": "below low", + "Projected BG %1 target": "Projected BG %1 target", + "aiming at": "aiming at", + "Bolus %1 units": "Bolus %1 units", + "or adjust basal": "or adjust basal", + "Check BG using glucometer before correcting!": "Check BG using glucometer before correcting!", + "Basal reduction to account %1 units:": "Basal reduction to account %1 units:", + "30m temp basal": "30m temp basal", + "1h temp basal": "1h temp basal", + "Cannula change overdue!": "Cannula change overdue!", + "Time to change cannula": "Time to change cannula", + "Change cannula soon": "Change cannula soon", + "Cannula age %1 hours": "Cannula age %1 hours", + "Inserted": "Inserted", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Last Carbs", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Insulin reservoir change overdue!", + "Time to change insulin reservoir": "Time to change insulin reservoir", + "Change insulin reservoir soon": "Change insulin reservoir soon", + "Insulin reservoir age %1 hours": "Insulin reservoir age %1 hours", + "Changed": "Changed", + "IOB": "IOB", + "Careportal IOB": "Careportal IOB", + "Last Bolus": "Last Bolus", + "Basal IOB": "Basal IOB", + "Source": "Source", + "Stale data, check rig?": "Stale data, check rig?", + "Last received:": "Last received:", + "%1m ago": "%1m ago", + "%1h ago": "%1h ago", + "%1d ago": "%1d ago", + "RETRO": "RETRO", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Sensor change/restart overdue!", + "Time to change/restart sensor": "Time to change/restart sensor", + "Change/restart sensor soon": "Change/restart sensor soon", + "Sensor age %1 days %2 hours": "Sensor age %1 days %2 hours", + "Sensor Insert": "Sensor Insert", + "Sensor Start": "Sensor Start", + "days": "days", + "Insulin distribution": "Insulin distribution", + "To see this report, press SHOW while in this view": "To see this report, press SHOW while in this view", + "AR2 Forecast": "AR2 Forecast", + "OpenAPS Forecasts": "OpenAPS Forecasts", + "Temporary Target": "Temporary Target", + "Temporary Target Cancel": "Temporary Target Cancel", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profiles", + "Time in fluctuation": "Time in fluctuation", + "Time in rapid fluctuation": "Time in rapid fluctuation", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:", + "Filter by hours": "Filter by hours", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">can be found here.", + "Mean Total Daily Change": "Mean Total Daily Change", + "Mean Hourly Change": "Mean Hourly Change", + "FortyFiveDown": "slightly dropping", + "FortyFiveUp": "slightly rising", + "Flat": "holding", + "SingleUp": "rising", + "SingleDown": "dropping", + "DoubleDown": "rapidly dropping", + "DoubleUp": "rapidly rising", + "virtAsstUnknown": "That value is unknown at the moment. Please see your Nightscout site for more details.", + "virtAsstTitleAR2Forecast": "AR2 Forecast", + "virtAsstTitleCurrentBasal": "Current Basal", + "virtAsstTitleCurrentCOB": "Current COB", + "virtAsstTitleCurrentIOB": "Current IOB", + "virtAsstTitleLaunch": "Welcome to Nightscout", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Forecast", + "virtAsstTitlePumpReservoir": "Insulin Remaining", + "virtAsstTitlePumpBattery": "Pump Battery", + "virtAsstTitleRawBG": "Current Raw BG", + "virtAsstTitleUploaderBattery": "Uploader Battery", + "virtAsstTitleCurrentBG": "Current BG", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "CGM Mode", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM Transmitter Age", + "virtAsstTitleCGMNoise": "CGM Noise", + "virtAsstTitleDelta": "Blood Glucose Delta", + "virtAsstStatus": "%1 and %2 as of %3.", + "virtAsstBasal": "%1 current basal is %2 units per hour", + "virtAsstBasalTemp": "%1 temp basal of %2 units per hour will end %3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "You have %1 insulin on board", + "virtAsstIobUnits": "%1 units of", + "virtAsstLaunch": "What would you like to check on Nightscout?", + "virtAsstPreamble": "Your", + "virtAsstPreamble3person": "%1 has a ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "Your pump battery is at %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "I'm sorry, I don't know what you're asking for.", + "Fat [g]": "Fat [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energy [kJ]", + "Clock Views:": "Clock Views:", + "Clock": "Clock", + "Color": "Color", + "Simple": "Simple", + "TDD average": "TDD average", + "Bolus average": "Bolus average", + "Basal average": "Basal average", + "Base basal average:": "Base basal average:", + "Carbs average": "Carbs average", + "Eating Soon": "Eating Soon", + "Last entry {0} minutes ago": "Last entry {0} minutes ago", + "change": "change", + "Speech": "Speech", + "Target Top": "Target Top", + "Target Bottom": "Target Bottom", + "Canceled": "Canceled", + "Meter BG": "Meter BG", + "predicted": "predicted", + "future": "future", + "ago": "ago", + "Last data received": "Last data received", + "Clock View": "Clock View", + "Protein": "Protein", + "Fat": "Fat", + "Protein average": "Protein average", + "Fat average": "Fat average", + "Total carbs": "Total carbs", + "Total protein": "Total protein", + "Total fat": "Total fat", + "Database Size": "Database Size", + "Database Size near its limits!": "Database Size near its limits!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "Database file size", + "%1 MiB of %2 MiB (%3%)": "%1 MiB of %2 MiB (%3%)", + "Data size": "Data size", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "Database file size", + "Carbs/Food/Time": "Carbs/Food/Time", + "You have administration messages": "You have administration messages", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "Data reads enabled", + "Data writes enabled": "Data writes enabled", + "Data writes not enabled": "Data writes not enabled", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "Check for Updates", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "Previous", + "Previous day": "Previous day", + "Next day": "Next day", + "Next": "Next", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/translations/ko_KR.json b/translations/ko_KR.json new file mode 100644 index 00000000000..6404a9b6bc3 --- /dev/null +++ b/translations/ko_KR.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "포트에서 수신", + "Mo": "월", + "Tu": "화", + "We": "수", + "Th": "목", + "Fr": "금", + "Sa": "토", + "Su": "일", + "Monday": "월요일", + "Tuesday": "화요일", + "Wednesday": "수요일", + "Thursday": "목요일", + "Friday": "금요일", + "Saturday": "토요일", + "Sunday": "일요일", + "Category": "분류", + "Subcategory": "세부 분류", + "Name": "프로파일 명", + "Today": "오늘", + "Last 2 days": "지난 2일", + "Last 3 days": "지난 3일", + "Last week": "지난주", + "Last 2 weeks": "지난 2주", + "Last month": "지난달", + "Last 3 months": "지난 3달", + "From": "시작일", + "To": "종료일", + "Notes": "메모", + "Food": "음식", + "Insulin": "인슐린", + "Carbs": "탄수화물", + "Notes contain": "메모 포함", + "Target BG range bottom": "최저 목표 혈당 범위", + "top": "최고치", + "Show": "확인", + "Display": "출력", + "Loading": "로딩", + "Loading profile": "프로파일 로딩", + "Loading status": "상태 로딩", + "Loading food database": "음식 데이터 베이스 로딩", + "not displayed": "출력되지 않음", + "Loading CGM data of": "CGM 데이터 로딩", + "Loading treatments data of": "처리 데이터 로딩", + "Processing data of": "데이터 처리 중", + "Portion": "부분", + "Size": "크기", + "(none)": "(없음)", + "None": "없음", + "": "<없음>", + "Result is empty": "결과 없음", + "Day to day": "일별 그래프", + "Week to week": "주별 그래프", + "Daily Stats": "일간 통계", + "Percentile Chart": "백분위 그래프", + "Distribution": "분포", + "Hourly stats": "시간대별 통계", + "netIOB stats": "netIOB stats", + "temp basals must be rendered to display this report": "temp basals must be rendered to display this report", + "No data available": "활용할 수 있는 데이터 없음", + "Low": "낮음", + "In Range": "범위 안 ", + "Period": "기간 ", + "High": "높음", + "Average": "평균", + "Low Quartile": "낮은 4분위", + "Upper Quartile": "높은 4분위", + "Quartile": "4분위", + "Date": "날짜", + "Normal": "보통", + "Median": "중간값", + "Readings": "혈당", + "StDev": "표준 편차", + "Daily stats report": "일간 통계 보고서", + "Glucose Percentile report": "혈당 백분위 보고서", + "Glucose distribution": "혈당 분포", + "days total": "일 전체", + "Total per day": "하루 총량", + "Overall": "전체", + "Range": "범위", + "% of Readings": "수신된 혈당 비율(%)", + "# of Readings": "수신된 혈당 개수(#)", + "Mean": "평균", + "Standard Deviation": "표준 편차", + "Max": "최대값", + "Min": "최소값", + "A1c estimation*": "예상 당화혈 색소", + "Weekly Success": "주간 통계", + "There is not sufficient data to run this report. Select more days.": "이 보고서를 실행하기 위한 데이터가 충분하지 않습니다. 더 많은 날들을 선택해 주세요.", + "Using stored API secret hash": "저장된 API secret hash를 사용 중", + "No API secret hash stored yet. You need to enter API secret.": "API secret hash가 아직 저장되지 않았습니다. API secret를 입력해 주세요.", + "Database loaded": "데이터베이스 로드", + "Error: Database failed to load": "에러: 데이터베이스 로드 실패", + "Error": "Error", + "Create new record": "새입력", + "Save record": "저장", + "Portions": "부분", + "Unit": "단위", + "GI": "혈당 지수", + "Edit record": "편집기록", + "Delete record": "삭제기록", + "Move to the top": "맨처음으로 이동", + "Hidden": "숨김", + "Hide after use": "사용 후 숨김", + "Your API secret must be at least 12 characters long": "API secret는 최소 12자 이상이여야 합니다.", + "Bad API secret": "잘못된 API secret", + "API secret hash stored": "API secret hash가 저장 되었습니다.", + "Status": "상태", + "Not loaded": "로드되지 않음", + "Food Editor": "음식 편집", + "Your database": "당신의 데이터베이스", + "Filter": "필터", + "Save": "저장", + "Clear": "취소", + "Record": "기록", + "Quick picks": "빠른 선택", + "Show hidden": "숨김 보기", + "Your API secret or token": "Your API secret or token", + "Remember this device. (Do not enable this on public computers.)": "Remember this device. (Do not enable this on public computers.)", + "Treatments": "관리", + "Time": "시간", + "Event Type": "입력 유형", + "Blood Glucose": "혈당", + "Entered By": "입력 내용", + "Delete this treatment?": "이 대처를 지울까요?", + "Carbs Given": "탄수화물 요구량", + "Insulin Given": "인슐린 요구량", + "Event Time": "입력 시간", + "Please verify that the data entered is correct": "입력한 데이터가 정확한지 확인해 주세요.", + "BG": "혈당", + "Use BG correction in calculation": "계산에 보정된 혈당을 사용하세요.", + "BG from CGM (autoupdated)": "CGM 혈당(자동 업데이트)", + "BG from meter": "혈당 측정기에서의 혈당", + "Manual BG": "수동 입력 혈당", + "Quickpick": "빠른 선택", + "or": "또는", + "Add from database": "데이터베이스로 부터 추가", + "Use carbs correction in calculation": "계산에 보정된 탄수화물을 사용하세요.", + "Use COB correction in calculation": "계산에 보정된 COB를 사용하세요.", + "Use IOB in calculation": "계산에 IOB를 사용하세요.", + "Other correction": "다른 보정", + "Rounding": "라운딩", + "Enter insulin correction in treatment": "대처를 위해 보정된 인슐린을 입력하세요.", + "Insulin needed": "인슐린 필요", + "Carbs needed": "탄수화물 필요", + "Carbs needed if Insulin total is negative value": "인슐린 전체가 마이너스 값이면 탄수화물이 필요합니다.", + "Basal rate": "Basal 단위", + "60 minutes earlier": "60분 더 일찍", + "45 minutes earlier": "45분 더 일찍", + "30 minutes earlier": "30분 더 일찍", + "20 minutes earlier": "20분 더 일찍", + "15 minutes earlier": "15분 더 일찍", + "Time in minutes": "분", + "15 minutes later": "15분 더 나중에", + "20 minutes later": "20분 더 나중에", + "30 minutes later": "30분 더 나중에", + "45 minutes later": "45분 더 나중에", + "60 minutes later": "60분 더 나중에", + "Additional Notes, Comments": "추가 메모", + "RETRO MODE": "PETRO MODE", + "Now": "현재", + "Other": "다른", + "Submit Form": "양식 제출", + "Profile Editor": "프로파일 편집", + "Reports": "보고서", + "Add food from your database": "데이터베이스에서 음식을 추가하세요.", + "Reload database": "데이터베이스 재로드", + "Add": "추가", + "Unauthorized": "미인증", + "Entering record failed": "입력 실패", + "Device authenticated": "기기 인증", + "Device not authenticated": "미인증 기기", + "Authentication status": "인증 상태", + "Authenticate": "인증", + "Remove": "삭제", + "Your device is not authenticated yet": "당신의 기기는 아직 인증되지 않았습니다.", + "Sensor": "센서", + "Finger": "손가락", + "Manual": "수", + "Scale": "스케일", + "Linear": "Linear", + "Logarithmic": "Logarithmic", + "Logarithmic (Dynamic)": "다수(동적인)", + "Insulin-on-Board": "IOB", + "Carbs-on-Board": "COB", + "Bolus Wizard Preview": "Bolus 마법사 미리보기", + "Value Loaded": "데이터가 로드됨", + "Cannula Age": "캐뉼라 사용기간", + "Basal Profile": "Basal 프로파일", + "Silence for 30 minutes": "30분간 무음", + "Silence for 60 minutes": "60분간 무음", + "Silence for 90 minutes": "90분간 무음", + "Silence for 120 minutes": "120분간 무음", + "Settings": "설정", + "Units": "단위", + "Date format": "날짜 형식", + "12 hours": "12 시간", + "24 hours": "24 시간", + "Log a Treatment": "Treatment 로그", + "BG Check": "혈당 체크", + "Meal Bolus": "식사 인슐린", + "Snack Bolus": "스넥 인슐린", + "Correction Bolus": "수정 인슐린", + "Carb Correction": "탄수화물 수정", + "Note": "메모", + "Question": "질문", + "Exercise": "운동", + "Pump Site Change": "펌프 위치 변경", + "CGM Sensor Start": "CGM 센서 시작", + "CGM Sensor Stop": "CGM Sensor Stop", + "CGM Sensor Insert": "CGM 센서 삽입", + "Sensor Code": "Sensor Code", + "Transmitter ID": "Transmitter ID", + "Dexcom Sensor Start": "Dexcom 센서 시작", + "Dexcom Sensor Change": "Dexcom 센서 교체", + "Insulin Cartridge Change": "인슐린 카트리지 교체", + "D.A.D. Alert": "D.A.D(Diabetes Alert Dog) 알림", + "Glucose Reading": "혈당 읽기", + "Measurement Method": "측정 방법", + "Meter": "혈당 측정기", + "Amount in grams": "합계(grams)", + "Amount in units": "합계(units)", + "View all treatments": "모든 treatments 보기", + "Enable Alarms": "알람 켜기", + "Pump Battery Change": "Pump Battery Change", + "Pump Battery Age": "Pump Battery Age", + "Pump Battery Low Alarm": "Pump Battery Low Alarm", + "Pump Battery change overdue!": "Pump Battery change overdue!", + "When enabled an alarm may sound.": "알림을 활성화 하면 알람이 울립니다.", + "Urgent High Alarm": "긴급 고혈당 알람", + "High Alarm": "고혈당 알람", + "Low Alarm": "저혈당 알람", + "Urgent Low Alarm": "긴급 저혈당 알람", + "Stale Data: Warn": "손실 데이터 : 경고", + "Stale Data: Urgent": "손실 데이터 : 긴급", + "mins": "분", + "Night Mode": "나이트 모드", + "When enabled the page will be dimmed from 10pm - 6am.": "페이지를 켜면 오후 10시 부터 오전 6시까지 비활성화 될 것이다.", + "Enable": "활성화", + "Show Raw BG Data": "Raw 혈당 데이터 보기", + "Never": "보지 않기", + "Always": "항상", + "When there is noise": "노이즈가 있을 때", + "When enabled small white dots will be displayed for raw BG data": "활성화 하면 작은 흰점들이 raw 혈당 데이터를 표시하게 될 것이다.", + "Custom Title": "사용자 정의 제목", + "Theme": "테마", + "Default": "초기설정", + "Colors": "색상", + "Colorblind-friendly colors": "색맹 친화적인 색상", + "Reset, and use defaults": "초기화 그리고 초기설정으로 사용", + "Calibrations": "보정", + "Alarm Test / Smartphone Enable": "알람 테스트 / 스마트폰 활성화", + "Bolus Wizard": "Bolus 마법사", + "in the future": "미래", + "time ago": "시간 전", + "hr ago": "시간 전", + "hrs ago": "시간 전", + "min ago": "분 전", + "mins ago": "분 전", + "day ago": "일 전", + "days ago": "일 전", + "long ago": "기간 전", + "Clean": "Clean", + "Light": "Light", + "Medium": "보통", + "Heavy": "심한", + "Treatment type": "Treatment 타입", + "Raw BG": "Raw 혈당", + "Device": "기기", + "Noise": "노이즈", + "Calibration": "보정", + "Show Plugins": "플러그인 보기", + "About": "정보", + "Value in": "값", + "Carb Time": "탄수화물 시간", + "Language": "언어", + "Add new": "새입력", + "g": "g", + "ml": "ml", + "pcs": "조각 바로 가기(pieces shortcut)", + "Drag&drop food here": "음식을 여기에 드래그&드랍 해주세요.", + "Care Portal": "Care Portal", + "Medium/Unknown": "보통/알려지지 않은", + "IN THE FUTURE": "미래", + "Order": "순서", + "oldest on top": "오래된 것 부터", + "newest on top": "새로운 것 부터", + "All sensor events": "모든 센서 이벤트", + "Remove future items from mongo database": "mongo DB에서 미래 항목들을 지우세요.", + "Find and remove treatments in the future": "미래에 treatments를 검색하고 지우세요.", + "This task find and remove treatments in the future.": "이 작업은 미래에 treatments를 검색하고 지우는 것입니다.", + "Remove treatments in the future": "미래 treatments 지우기", + "Find and remove entries in the future": "미래에 입력을 검색하고 지우세요.", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "이 작업은 잘못된 날짜/시간으로 업로드 되어 생성된 미래의 CGM 데이터를 검색하고 지우는 것입니다.", + "Remove entries in the future": "미래의 입력 지우기", + "Loading database ...": "데이터베이스 로딩", + "Database contains %1 future records": "데이터베이스는 미래 기록을 %1 포함하고 있습니다.", + "Remove %1 selected records?": "선택된 기록 %1를 지우시겠습니까?", + "Error loading database": "데이터베이스 로딩 에러", + "Record %1 removed ...": "기록 %1가 삭제되었습니다.", + "Error removing record %1": "기록 %1을 삭제하는 중에 에러가 발생했습니다.", + "Deleting records ...": "기록 삭제 중", + "%1 records deleted": "%1 records deleted", + "Clean Mongo status database": "Mongo 상태 데이터베이스를 지우세요.", + "Delete all documents from devicestatus collection": "devicestatus 수집에서 모든 문서들을 지우세요", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "이 작업은 모든 문서를 devicestatus 수집에서 지웁니다. 업로더 배터리 상태가 적절하게 업데이트 되지 않을 때 유용합니다.", + "Delete all documents": "모든 문서들을 지우세요", + "Delete all documents from devicestatus collection?": "devicestatus 수집의 모든 문서들을 지우세요.", + "Database contains %1 records": "데이터베이스는 %1 기록을 포함합니다.", + "All records removed ...": "모든 기록들이 지워졌습니다.", + "Delete all documents from devicestatus collection older than 30 days": "Delete all documents from devicestatus collection older than 30 days", + "Number of Days to Keep:": "Number of Days to Keep:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from devicestatus collection?": "Delete old documents from devicestatus collection?", + "Clean Mongo entries (glucose entries) database": "Clean Mongo entries (glucose entries) database", + "Delete all documents from entries collection older than 180 days": "Delete all documents from entries collection older than 180 days", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents": "Delete old documents", + "Delete old documents from entries collection?": "Delete old documents from entries collection?", + "%1 is not a valid number": "%1 is not a valid number", + "%1 is not a valid number - must be more than 2": "%1 is not a valid number - must be more than 2", + "Clean Mongo treatments database": "Clean Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Delete all documents from treatments collection older than 180 days", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "관리 도구", + "Nightscout reporting": "Nightscout 보고서", + "Cancel": "취소", + "Edit treatment": "Treatments 편집", + "Duration": "기간", + "Duration in minutes": "분당 지속 기간", + "Temp Basal": "임시 basal", + "Temp Basal Start": "임시 basal 시작", + "Temp Basal End": "임시 basal 종료", + "Percent": "퍼센트", + "Basal change in %": "% 이내의 basal 변경", + "Basal value": "Basal", + "Absolute basal value": "절대적인 basal", + "Announcement": "공지", + "Loading temp basal data": "임시 basal 로딩", + "Save current record before changing to new?": "새 데이터로 변경하기 전에 현재의 기록을 저장하시겠습니까?", + "Profile Switch": "프로파일 변경", + "Profile": "프로파일", + "General profile settings": "일반 프로파일 설정", + "Title": "제목", + "Database records": "데이터베이스 기록", + "Add new record": "새 기록 추가", + "Remove this record": "이 기록 삭", + "Clone this record to new": "이 기록을 새기록으로 복제하기", + "Record valid from": "기록을 시작한 날짜", + "Stored profiles": "저장된 프로파일", + "Timezone": "타임존", + "Duration of Insulin Activity (DIA)": "활성 인슐린 지속 시간(DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "인슐린이 작용하는 지속시간을 나타냅니다. 사람마다 그리고 인슐린 종류에 따라 다르고 일반적으로 3~4시간간 동안 지속되며 인슐린 작용 시간(Insulin lifetime)이라고 불리기도 합니다.", + "Insulin to carb ratio (I:C)": "인슐린에 대한 탄수화물 비율(I:C)", + "Hours:": "시간:", + "hours": "시간", + "g/hour": "g/시간", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "인슐린 단위 당 탄수화물 g. 인슐린 단위에 대한 탄수화물의 양의 비율을 나타냅니다.", + "Insulin Sensitivity Factor (ISF)": "인슐린 민감도(ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "인슐린 단위당 mg/dL 또는 mmol/L. 인슐린 단위당 얼마나 많은 혈당 변화가 있는지의 비율을 나타냅니다.", + "Carbs activity / absorption rate": "활성 탄수화물/흡수율", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "단위 시간당 그램. 시간당 작용하는 탄수화물의 총량 뿐 아니라 시간 단위당 COB의 변화를 나타냅니다. 탄수화물 흡수율/활성도 곡선은 인슐린 활성도보다 이해가 잘 되지는 않지만 지속적인 흡수율(g/hr)에 따른 초기 지연을 사용하여 근사치를 구할 수 있습니다.", + "Basal rates [unit/hour]": "Basal 비율[unit/hour]", + "Target BG range [mg/dL,mmol/L]": "목표 혈당 범위 [mg/dL,mmol/L]", + "Start of record validity": "기록 유효기간의 시작일", + "Icicle": "고드름 방향", + "Render Basal": "Basal 사용하기", + "Profile used": "프로파일이 사용됨", + "Calculation is in target range.": "계산은 목표 범위 안에 있습니다.", + "Loading profile records ...": "프로파일 기록 로딩", + "Values loaded.": "값이 로드됨", + "Default values used.": "초기 설정 값이 사용됨", + "Error. Default values used.": "에러. 초기 설정 값이 사용됨", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "설정한 저혈당과 고혈당의 시간 범위와 일치하지 않습니다. 값은 초기 설정값으로 다시 저장 될 것입니다.", + "Valid from:": "유효", + "Save current record before switching to new?": "새 데이터로 변환하기 전에 현재의 기록을 저장하겠습니까?", + "Add new interval before": "새로운 구간을 추가하세요", + "Delete interval": "구간을 지우세요.", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Combo Bolus", + "Difference": "차이", + "New time": "새로운 시간", + "Edit Mode": "편집 모드", + "When enabled icon to start edit mode is visible": "편집모드를 시작하기 위해 아이콘을 활성화하면 볼 수 있습니다.", + "Operation": "동작", + "Move": "이동", + "Delete": "삭제", + "Move insulin": "인슐린을 이동하세요.", + "Move carbs": "탄수화물을 이동하세요.", + "Remove insulin": "인슐린을 지우세요.", + "Remove carbs": "탄수화물을 지우세요.", + "Change treatment time to %1 ?": "%1 treatment을 변경하세요.", + "Change carbs time to %1 ?": "%1로 탄수화물 시간을 변경하세요.", + "Change insulin time to %1 ?": "%1로 인슐린 시간을 변경하세요.", + "Remove treatment ?": "Treatment를 지우세요.", + "Remove insulin from treatment ?": "Treatment에서 인슐린을 지우세요.", + "Remove carbs from treatment ?": "Treatment에서 탄수화물을 지우세요.", + "Rendering": "랜더링", + "Loading OpenAPS data of": "OpenAPS 데이터 로딩", + "Loading profile switch data": "프로파일 변환 데이터 로딩", + "Redirecting you to the Profile Editor to create a new profile.": "잘못된 프로파일 설정. \n프로파일이 없어서 표시된 시간으로 정의됩니다. 새 프로파일을 생성하기 위해 프로파일 편집기로 리다이렉팅", + "Pump": "펌프", + "Sensor Age": "센서 사용 기간", + "Insulin Age": "인슐린 사용 기간", + "Temporary target": "임시 목표", + "Reason": "근거", + "Eating soon": "편집 중", + "Top": "최고", + "Bottom": "최저", + "Activity": "활성도", + "Targets": "목표", + "Bolus insulin:": "Bolus 인슐린", + "Base basal insulin:": "기본 basal 인슐린", + "Positive temp basal insulin:": "초과된 임시 basal 인슐린", + "Negative temp basal insulin:": "남은 임시 basal 인슐린", + "Total basal insulin:": "전체 basal 인슐린", + "Total daily insulin:": "하루 인슐린 총량", + "Unable to save Role": "Unable to save Role", + "Unable to delete Role": "삭제로 비활성", + "Database contains %1 roles": "데이터베이스가 %1 포함", + "Edit Role": "편집 모드", + "admin, school, family, etc": "관리자, 학교, 가족 등", + "Permissions": "허가", + "Are you sure you want to delete: ": "정말로 삭제하시겠습니까: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "각각은 1 또는 그 이상의 허가를 가지고 있습니다. 허가는 예측이 안되고 구분자로 : 사용한 계층이 있습니다", + "Add new Role": "새 역할 추가", + "Roles - Groups of People, Devices, etc": "역할 - 그룹, 기기, 등", + "Edit this role": "이 역할 편집", + "Admin authorized": "인증된 관리자", + "Subjects - People, Devices, etc": "주제 - 사람, 기기 등", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "각 주제는 유일한 access token을 가지고 1 또는 그 이상의 역할을 가질 것이다. 선택된 주제와 새로운 뷰를 열기 위해 access token을 클릭하세요. 이 비밀 링크는 공유되어질 수 있다.", + "Add new Subject": "새 주제 추가", + "Unable to save Subject": "Unable to save Subject", + "Unable to delete Subject": "주제를 지우기 위해 비활성화", + "Database contains %1 subjects": "데이터베이스는 %1 주제를 포함", + "Edit Subject": "주제 편집", + "person, device, etc": "사람, 기기 등", + "role1, role2": "역할1, 역할2", + "Edit this subject": "이 주제 편집", + "Delete this subject": "이 주제 삭제", + "Roles": "역할", + "Access Token": "Access Token", + "hour ago": "시간 후", + "hours ago": "시간 후", + "Silence for %1 minutes": "%1 분 동안 무음", + "Check BG": "혈당 체크", + "BASAL": "BASAL", + "Current basal": "현재 basal", + "Sensitivity": "인슐린 민감도(ISF)", + "Current Carb Ratio": "현재 탄수화물 비율(ICR)", + "Basal timezone": "Basal 타임존", + "Active profile": "활성 프로파일", + "Active temp basal": "활성 임시 basal", + "Active temp basal start": "활성 임시 basal 시작", + "Active temp basal duration": "활성 임시 basal 시간", + "Active temp basal remaining": "남아 있는 활성 임시 basal", + "Basal profile value": "Basal 프로파일 값", + "Active combo bolus": "활성 콤보 bolus", + "Active combo bolus start": "활성 콤보 bolus 시작", + "Active combo bolus duration": "활성 콤보 bolus 기간", + "Active combo bolus remaining": "남아 있는 활성 콤보 bolus", + "BG Delta": "혈당 차이", + "Elapsed Time": "경과 시간", + "Absolute Delta": "절대적인 차이", + "Interpolated": "삽입됨", + "BWP": "BWP", + "Urgent": "긴급", + "Warning": "경고", + "Info": "정보", + "Lowest": "가장 낮은", + "Snoozing high alarm since there is enough IOB": "충분한 IOB가 남아 있기 때문에 고혈당 알람을 스누즈", + "Check BG, time to bolus?": "Bolus를 주입할 시간입니다. 혈당 체크 하시겠습니까?", + "Notice": "알림", + "required info missing": "요청한 정보 손실", + "Insulin on Board": "활성 인슐린(IOB)", + "Current target": "현재 목표", + "Expected effect": "예상 효과", + "Expected outcome": "예상 결과", + "Carb Equivalent": "탄수화물 양", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "낮은 혈당 목표에 도달하기 위해 필요한 인슐린양보다 %1U의 인슐린 양이 초과 되었고 탄수화물 양이 초과되지 않았습니다.", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "낮은 혈당 목표에 도달하기 위해 필요한 인슐린양보다 %1U의 인슐린 양이 초과 되었습니다. 탄수화물로 IOB를 커버하세요.", + "%1U reduction needed in active insulin to reach low target, too much basal?": "낮은 혈당 목표에 도달하기 위해 활성 인슐린 양을 %1U 줄일 필요가 있습니다. basal양이 너무 많습니까?", + "basal adjustment out of range, give carbs?": "적정 basal 양의 범위를 초과했습니다. 탄수화물 보충 하시겠습니까?", + "basal adjustment out of range, give bolus?": "적정 basal 양의 범위를 초과했습니다. bolus를 추가하시겠습니까?", + "above high": "고혈당 초과", + "below low": "저혈당 미만", + "Projected BG %1 target": "목표된 혈당 %1", + "aiming at": "목표", + "Bolus %1 units": "Bolus %1 단위", + "or adjust basal": "또는 조절 basal", + "Check BG using glucometer before correcting!": "수정하기 전에 혈당체크기를 사용하여 혈당을 체크하세요!", + "Basal reduction to account %1 units:": "%1 단위로 계산하기 위해 Basal 감소", + "30m temp basal": "30분 임시 basal", + "1h temp basal": "1시간 임시 basal", + "Cannula change overdue!": "주입세트(cannula) 기한이 지났습니다. 변경하세요!", + "Time to change cannula": "주입세트(cannula)를 변경할 시간", + "Change cannula soon": "주입세트(cannula)를 곧 변경하세요.", + "Cannula age %1 hours": "주입세트(cannula) %1시간 사용", + "Inserted": "삽입된", + "CAGE": "주입세트사용기간", + "COB": "COB", + "Last Carbs": "마지막 탄수화물", + "IAGE": "인슐린사용기간", + "Insulin reservoir change overdue!": "레저보(펌프 주사기)의 사용기한이 지났습니다. 변경하세요!", + "Time to change insulin reservoir": "레저보(펌프 주사기)를 변경할 시간", + "Change insulin reservoir soon": "레저보(펌프 주사기)안의 인슐린을 곧 변경하세요.", + "Insulin reservoir age %1 hours": "레저보(펌프 주사기) %1시간 사용", + "Changed": "변경됨", + "IOB": "IOB", + "Careportal IOB": "케어포털 IOB", + "Last Bolus": "마지막 Bolus", + "Basal IOB": "Basal IOB", + "Source": "출처", + "Stale data, check rig?": "오래된 데이터입니다. 확인해 보시겠습니까?", + "Last received:": "마지막 수신", + "%1m ago": "%1분 전", + "%1h ago": "%1시간 전", + "%1d ago": "%1일 전", + "RETRO": "RETRO", + "SAGE": "센서사용기간", + "Sensor change/restart overdue!": "센서 사용기한이 지났습니다. 센서를 교체/재시작 하세요!", + "Time to change/restart sensor": "센서 교체/재시작 시간", + "Change/restart sensor soon": "센서를 곧 교체/재시작 하세요", + "Sensor age %1 days %2 hours": "센서사용기간 %1일 %2시간", + "Sensor Insert": "센서삽입", + "Sensor Start": "센서시작", + "days": "일", + "Insulin distribution": "인슐린주입", + "To see this report, press SHOW while in this view": "이 보고서를 보려면 \"확인\"을 누르세요", + "AR2 Forecast": "AR2 예측", + "OpenAPS Forecasts": "OpenAPS 예측", + "Temporary Target": "임시목표", + "Temporary Target Cancel": "임시목표취소", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "프로파일", + "Time in fluctuation": "변동시간", + "Time in rapid fluctuation": "빠른변동시간", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "이것은 대충 예측한 것이기 때문에 부정확할 수 있고 실제 혈당으로 대체되지 않습니다. 사용된 공식:", + "Filter by hours": "시간으로 정렬", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "변동시간과 빠른 변동시간은 조사된 기간 동안 %의 시간으로 측정되었습니다.혈당은 비교적 빠르게 변화되었습니다. 낮을수록 좋습니다.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "전체 일일 변동 평균은 조사된 기간동안 전체 혈당 절대값의 합을 전체 일수로 나눈 값입니다. 낮을수록 좋습니다.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "시간당 변동 평균은 조사된 기간 동안 전체 혈당 절대값의 합을 기간의 시간으로 나눈 값입니다.낮을수록 좋습니다.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">here.", + "Mean Total Daily Change": "전체 일일 변동 평균", + "Mean Hourly Change": "시간당 변동 평균", + "FortyFiveDown": "slightly dropping", + "FortyFiveUp": "slightly rising", + "Flat": "holding", + "SingleUp": "rising", + "SingleDown": "dropping", + "DoubleDown": "rapidly dropping", + "DoubleUp": "rapidly rising", + "virtAsstUnknown": "That value is unknown at the moment. Please see your Nightscout site for more details.", + "virtAsstTitleAR2Forecast": "AR2 Forecast", + "virtAsstTitleCurrentBasal": "Current Basal", + "virtAsstTitleCurrentCOB": "Current COB", + "virtAsstTitleCurrentIOB": "Current IOB", + "virtAsstTitleLaunch": "Welcome to Nightscout", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Forecast", + "virtAsstTitlePumpReservoir": "Insulin Remaining", + "virtAsstTitlePumpBattery": "Pump Battery", + "virtAsstTitleRawBG": "Current Raw BG", + "virtAsstTitleUploaderBattery": "Uploader Battery", + "virtAsstTitleCurrentBG": "Current BG", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "CGM Mode", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM Transmitter Age", + "virtAsstTitleCGMNoise": "CGM Noise", + "virtAsstTitleDelta": "Blood Glucose Delta", + "virtAsstStatus": "%1 and %2 as of %3.", + "virtAsstBasal": "%1 current basal is %2 units per hour", + "virtAsstBasalTemp": "%1 temp basal of %2 units per hour will end %3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "You have %1 insulin on board", + "virtAsstIobUnits": "%1 units of", + "virtAsstLaunch": "What would you like to check on Nightscout?", + "virtAsstPreamble": "Your", + "virtAsstPreamble3person": "%1 has a ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "Your pump battery is at %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "I'm sorry, I don't know what you're asking for.", + "Fat [g]": "Fat [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energy [kJ]", + "Clock Views:": "시계 보기", + "Clock": "시계모드", + "Color": "색상모드", + "Simple": "간편 모드", + "TDD average": "TDD average", + "Bolus average": "Bolus average", + "Basal average": "Basal average", + "Base basal average:": "Base basal average:", + "Carbs average": "Carbs average", + "Eating Soon": "Eating Soon", + "Last entry {0} minutes ago": "Last entry {0} minutes ago", + "change": "change", + "Speech": "Speech", + "Target Top": "Target Top", + "Target Bottom": "Target Bottom", + "Canceled": "Canceled", + "Meter BG": "Meter BG", + "predicted": "predicted", + "future": "future", + "ago": "ago", + "Last data received": "Last data received", + "Clock View": "Clock View", + "Protein": "Protein", + "Fat": "Fat", + "Protein average": "Protein average", + "Fat average": "Fat average", + "Total carbs": "Total carbs", + "Total protein": "Total protein", + "Total fat": "Total fat", + "Database Size": "Database Size", + "Database Size near its limits!": "Database Size near its limits!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "Database file size", + "%1 MiB of %2 MiB (%3%)": "%1 MiB of %2 MiB (%3%)", + "Data size": "Data size", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "Database file size", + "Carbs/Food/Time": "Carbs/Food/Time", + "You have administration messages": "You have administration messages", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "Data reads enabled", + "Data writes enabled": "Data writes enabled", + "Data writes not enabled": "Data writes not enabled", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "Check for Updates", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "Previous", + "Previous day": "Previous day", + "Next day": "Next day", + "Next": "Next", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/translations/nb_NO.json b/translations/nb_NO.json new file mode 100644 index 00000000000..9cdab4d5bd2 --- /dev/null +++ b/translations/nb_NO.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Lytter på port", + "Mo": "Man", + "Tu": "Tir", + "We": "Ons", + "Th": "Tors", + "Fr": "Fre", + "Sa": "Lør", + "Su": "Søn", + "Monday": "Mandag", + "Tuesday": "Tirsdag", + "Wednesday": "Onsdag", + "Thursday": "Torsdag", + "Friday": "Fredag", + "Saturday": "Lørdag", + "Sunday": "Søndag", + "Category": "Kategori", + "Subcategory": "Underkategori", + "Name": "Navn", + "Today": "I dag", + "Last 2 days": "Siste 2 dager", + "Last 3 days": "Siste 3 dager", + "Last week": "Siste uke", + "Last 2 weeks": "Siste 2 uker", + "Last month": "Siste måned", + "Last 3 months": "Siste 3 måneder", + "From": "Fra", + "To": "Til", + "Notes": "Notater", + "Food": "Mat", + "Insulin": "Insulin", + "Carbs": "Karbohydrater", + "Notes contain": "Notater inneholder", + "Target BG range bottom": "Nedre grense for blodsukkerverdier", + "top": "topp", + "Show": "Vis", + "Display": "Vis", + "Loading": "Leser inn", + "Loading profile": "Leser inn profil", + "Loading status": "Leser inn status", + "Loading food database": "Leser inn matdatabase", + "not displayed": "vises ikke", + "Loading CGM data of": "Leser CGM-data for", + "Loading treatments data of": "Leser behandlingsdata for", + "Processing data of": "Behandler data for", + "Portion": "Porsjon", + "Size": "Størrelse", + "(none)": "(ingen)", + "None": "Ingen", + "": "", + "Result is empty": "Tomt resultat", + "Day to day": "Dag for dag", + "Week to week": "Uke for uke", + "Daily Stats": "Daglig statistikk", + "Percentile Chart": "Persentildiagram", + "Distribution": "Distribusjon", + "Hourly stats": "Timestatistikk", + "netIOB stats": "netIOB statistikk", + "temp basals must be rendered to display this report": "midlertidig basal må være synlig for å vise denne rapporten", + "No data available": "Ingen data tilgjengelig", + "Low": "Lav", + "In Range": "Innenfor intervallet", + "Period": "Periode", + "High": "Høy", + "Average": "Gjennomsnitt", + "Low Quartile": "Nedre kvartil", + "Upper Quartile": "Øvre kvartil", + "Quartile": "Kvartil", + "Date": "Dato", + "Normal": "Normal", + "Median": "Median", + "Readings": "Avlesninger", + "StDev": "Standardavvik", + "Daily stats report": "Daglig statistikkrapport", + "Glucose Percentile report": "Persentildiagram for blodsukker", + "Glucose distribution": "Glukosefordeling", + "days total": "dager", + "Total per day": "Totalt per dag", + "Overall": "Totalt", + "Range": "Intervall", + "% of Readings": "% av avlesningene", + "# of Readings": "Antall avlesninger", + "Mean": "Gjennomsnitt", + "Standard Deviation": "Standardavvik", + "Max": "Maks", + "Min": "Minimum", + "A1c estimation*": "Estimert HbA1c", + "Weekly Success": "Ukentlig suksess", + "There is not sufficient data to run this report. Select more days.": "Det er ikke nok data til å lage rapporten. Velg flere dager.", + "Using stored API secret hash": "Bruker lagret tilgangsnøkkel", + "No API secret hash stored yet. You need to enter API secret.": "Mangler tilgangsnøkkel. Du må skrive inn API_SECRET.", + "Database loaded": "Database lest", + "Error: Database failed to load": "Feil: Database kunne ikke leses", + "Error": "Feil", + "Create new record": "Opprett ny registrering", + "Save record": "Lagre registrering", + "Portions": "Porsjoner", + "Unit": "Enhet", + "GI": "GI", + "Edit record": "Editere registrering", + "Delete record": "Slette registrering", + "Move to the top": "Gå til toppen", + "Hidden": "Skjult", + "Hide after use": "Skjul etter bruk", + "Your API secret must be at least 12 characters long": "API_SECRET må være minst 12 tegn", + "Bad API secret": "Ugyldig API-hemmelighet", + "API secret hash stored": "Tilgangsnøkkel lagret", + "Status": "Status", + "Not loaded": "Ikke lest", + "Food Editor": "Mat-editor", + "Your database": "Din database", + "Filter": "Filter", + "Save": "Lagre", + "Clear": "Tøm", + "Record": "Registrering", + "Quick picks": "Hurtigvalg", + "Show hidden": "Vis skjulte", + "Your API secret or token": "API-hemmelighet eller tilgangsnøkkel", + "Remember this device. (Do not enable this on public computers.)": "Husk denne enheten (Ikke velg dette på offentlige eller delte datamaskiner)", + "Treatments": "Behandlinger", + "Time": "Tid", + "Event Type": "Hendelsestype", + "Blood Glucose": "Blodsukker", + "Entered By": "Lagt inn av", + "Delete this treatment?": "Slett denne hendelsen?", + "Carbs Given": "Karbohydrat", + "Insulin Given": "Insulin", + "Event Time": "Tidspunkt", + "Please verify that the data entered is correct": "Vennligst verifiser at inntastet data er korrekt", + "BG": "BS", + "Use BG correction in calculation": "Bruk blodsukkerkorrigering i beregningen", + "BG from CGM (autoupdated)": "BS fra CGM (automatisk)", + "BG from meter": "BS fra blodsukkerapparat", + "Manual BG": "Manuelt BS", + "Quickpick": "Hurtigvalg", + "or": "eller", + "Add from database": "Legg til fra database", + "Use carbs correction in calculation": "Bruk karbohydratkorrigering i beregningen", + "Use COB correction in calculation": "Bruk aktive karbohydrater i beregningen", + "Use IOB in calculation": "Bruk aktivt insulin i beregningen", + "Other correction": "Annen korrigering", + "Rounding": "Avrunding", + "Enter insulin correction in treatment": "Angi insulinkorrigering", + "Insulin needed": "Insulin nødvendig", + "Carbs needed": "Karbohydrater nødvendig", + "Carbs needed if Insulin total is negative value": "Karbohydrater er nødvendige når total insulinmengde er negativ", + "Basal rate": "Basal", + "60 minutes earlier": "60 min tidligere", + "45 minutes earlier": "45 min tidligere", + "30 minutes earlier": "30 min tidigere", + "20 minutes earlier": "20 min tidligere", + "15 minutes earlier": "15 min tidligere", + "Time in minutes": "Tid i minutter", + "15 minutes later": "15 min senere", + "20 minutes later": "20 min senere", + "30 minutes later": "30 min senere", + "45 minutes later": "45 min senere", + "60 minutes later": "60 min senere", + "Additional Notes, Comments": "Notater", + "RETRO MODE": "Retro-modus", + "Now": "Nå", + "Other": "Annet", + "Submit Form": "Lagre", + "Profile Editor": "Profileditor", + "Reports": "Rapporter", + "Add food from your database": "Legg til mat fra din database", + "Reload database": "Last inn databasen på nytt", + "Add": "Legg til", + "Unauthorized": "Uautorisert", + "Entering record failed": "Lagring feilet", + "Device authenticated": "Enhet godkjent", + "Device not authenticated": "Enhet ikke godkjent", + "Authentication status": "Autentiseringsstatus", + "Authenticate": "Autentiser", + "Remove": "Slett", + "Your device is not authenticated yet": "Din enhet er ikke godkjent enda", + "Sensor": "Sensor", + "Finger": "Finger", + "Manual": "Manuell", + "Scale": "Skala", + "Linear": "Lineær", + "Logarithmic": "Logaritmisk", + "Logarithmic (Dynamic)": "Logaritmisk (Dynamisk)", + "Insulin-on-Board": "Aktivt insulin (iob)", + "Carbs-on-Board": "Aktive karbo (cob)", + "Bolus Wizard Preview": "Boluskalk (bwp)", + "Value Loaded": "Verdi lastet", + "Cannula Age": "Nålalder (cage)", + "Basal Profile": "Basalprofil (basal)", + "Silence for 30 minutes": "Stille i 30 min", + "Silence for 60 minutes": "Stille i 60 min", + "Silence for 90 minutes": "Stille i 90 min", + "Silence for 120 minutes": "Stile i 120 min", + "Settings": "Innstillinger", + "Units": "Enheter", + "Date format": "Datoformat", + "12 hours": "12 timer", + "24 hours": "24 timer", + "Log a Treatment": "Logg en hendelse", + "BG Check": "Blodsukkerkontroll", + "Meal Bolus": "Måltidsbolus", + "Snack Bolus": "Mellommåltidsbolus", + "Correction Bolus": "Korreksjonsbolus", + "Carb Correction": "Karbohydratkorrigering", + "Note": "Notat", + "Question": "Spørsmål", + "Exercise": "Trening", + "Pump Site Change": "Pumpebytte", + "CGM Sensor Start": "Sensorstart", + "CGM Sensor Stop": "CGM Sensor Stopp", + "CGM Sensor Insert": "Sensorbytte", + "Sensor Code": "Sensorkode", + "Transmitter ID": "Sender-ID", + "Dexcom Sensor Start": "Dexcom sensor start", + "Dexcom Sensor Change": "Dexcom sensor bytte", + "Insulin Cartridge Change": "Skifte insulin beholder", + "D.A.D. Alert": "Diabeteshundalarm", + "Glucose Reading": "Blodsukkermåling", + "Measurement Method": "Målemetode", + "Meter": "Måleapparat", + "Amount in grams": "Antall gram", + "Amount in units": "Antall enheter", + "View all treatments": "Vis behandlinger", + "Enable Alarms": "Aktiver alarmer", + "Pump Battery Change": "Bytte av pumpebatteri", + "Pump Battery Age": "Pumpebatteri alder", + "Pump Battery Low Alarm": "Pumpebatteri: Lav alarm", + "Pump Battery change overdue!": "Pumpebatteriet må byttes!", + "When enabled an alarm may sound.": "Når aktivert kan en alarm lyde.", + "Urgent High Alarm": "Kritisk høy alarm", + "High Alarm": "Høy alarm", + "Low Alarm": "Lav alarm", + "Urgent Low Alarm": "Kritisk lav alarm", + "Stale Data: Warn": "Gamle data: Advarsel etter", + "Stale Data: Urgent": "Kritisk gamle data: Advarsel etter", + "mins": "min", + "Night Mode": "Nattmodus", + "When enabled the page will be dimmed from 10pm - 6am.": "Når aktivert vil denne siden nedtones fra 22:00-06:00", + "Enable": "Aktiver", + "Show Raw BG Data": "Vis rådata", + "Never": "Aldri", + "Always": "Alltid", + "When there is noise": "Når det er støy", + "When enabled small white dots will be displayed for raw BG data": "Ved aktivering vil små hvite prikker bli vist for ubehandlede BG-data", + "Custom Title": "Egen tittel", + "Theme": "Tema", + "Default": "Standard", + "Colors": "Farger", + "Colorblind-friendly colors": "Fargeblindvennlige farger", + "Reset, and use defaults": "Gjenopprett standardinnstillinger", + "Calibrations": "Kalibreringer", + "Alarm Test / Smartphone Enable": "Alarmtest / Smartphone aktivering", + "Bolus Wizard": "Boluskalkulator", + "in the future": "fremtiden", + "time ago": "tid siden", + "hr ago": "t siden", + "hrs ago": "t siden", + "min ago": "min siden", + "mins ago": "min siden", + "day ago": "dag siden", + "days ago": "dager siden", + "long ago": "lenge siden", + "Clean": "Rydd", + "Light": "Lite", + "Medium": "Middels", + "Heavy": "Mye", + "Treatment type": "Behandlingstype", + "Raw BG": "RAW-BS", + "Device": "Enhet", + "Noise": "Støy", + "Calibration": "Kalibrering", + "Show Plugins": "Vis plugins", + "About": "Om", + "Value in": "Verdi i", + "Carb Time": "Karbohydrattid", + "Language": "Språk", + "Add new": "Legg til ny", + "g": "g", + "ml": "mL", + "pcs": "stk", + "Drag&drop food here": "Dra og slipp mat her", + "Care Portal": "Omsorgsportal (careportal)", + "Medium/Unknown": "Medium/ukjent", + "IN THE FUTURE": "I fremtiden", + "Order": "Sortering", + "oldest on top": "Eldste først", + "newest on top": "Nyeste først", + "All sensor events": "Alle sensorhendelser", + "Remove future items from mongo database": "Slett fremtidige elementer fra Mongo databasen", + "Find and remove treatments in the future": "Finn og slett fremtidige behandlinger", + "This task find and remove treatments in the future.": "Denne funksjonen finner og sletter fremtidige behandlinger.", + "Remove treatments in the future": "Slett fremtidige behandlinger", + "Find and remove entries in the future": "Finn og slett fremtidige hendelser", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Denne funksjonen finner og sletter fremtidige CGM data som er lastet opp med feil dato/tid.", + "Remove entries in the future": "Slett fremtidige hendelser", + "Loading database ...": "Leser database ...", + "Database contains %1 future records": "Databasen inneholder %1 fremtidige hendelser", + "Remove %1 selected records?": "Slette %1 valgte elementer?", + "Error loading database": "Feil under lasting av database", + "Record %1 removed ...": "Element %1 slettet ...", + "Error removing record %1": "Feil under sletting av registrering %1", + "Deleting records ...": "Sletter registreringer...", + "%1 records deleted": "%1 registreringer slettet", + "Clean Mongo status database": "Rydd i Mongo-databasen for \"device status\"", + "Delete all documents from devicestatus collection": "Slett alle dokumenter fra \"devicestatus\" samlingen", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Denne funksjonen sletter alle dokumenter fra \"devicestatus\" samlingen. Nyttig når status for opplaster-batteri ikke blir oppdatert.", + "Delete all documents": "Slett alle dokumenter", + "Delete all documents from devicestatus collection?": "Slette alle dokumenter fra \"devicestatus\" samlingen?", + "Database contains %1 records": "Databasen inneholder %1 registreringer", + "All records removed ...": "Alle elementer er slettet ...", + "Delete all documents from devicestatus collection older than 30 days": "Slett alle dokumenter fra \"devicestatus\" samlingen som er eldre enn 30 dager", + "Number of Days to Keep:": "Antall dager å beholde:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Denne funksjonen sletter alle dokumenter fra \"devicestatus\" samlingen som er eldre enn 30 dager. Nyttig når status for opplaster-batteri ikke blir oppdatert.", + "Delete old documents from devicestatus collection?": "Slett gamle dokumenter fra \"devicestatus\" samlingen?", + "Clean Mongo entries (glucose entries) database": "Rydd i Mongo-databasen for blodsukkerregistreringer", + "Delete all documents from entries collection older than 180 days": "Slett alle dokumenter fra \"entries\" samlingen som er eldre enn 180 dager", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Denne funksjonen sletter alle dokumenter fra \"entries\" samlingen som er eldre enn 180 dager. Nyttig når status for opplaster-batteri ikke blir oppdatert.", + "Delete old documents": "Slett eldre dokumenter", + "Delete old documents from entries collection?": "Slett gamle dokumenter fra \"entries\" samlingen?", + "%1 is not a valid number": "%1 er ikke et gyldig tall", + "%1 is not a valid number - must be more than 2": "%1 er ikke et gyldig tall - må være mer enn 2", + "Clean Mongo treatments database": "Rydd i Mongo-databasen for behandlinger", + "Delete all documents from treatments collection older than 180 days": "Slett alle dokumenter fra \"treatments\" samlingen som er eldre enn 180 dager", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Denne funksjonen sletter alle dokumenter fra \"treatments\" samlingen som er eldre enn 180 dager. Nyttig når status for opplaster-batteri ikke blir oppdatert.", + "Delete old documents from treatments collection?": "Slett gamle dokumenter fra \"treatments\" samlingen?", + "Admin Tools": "Administrasjonsverktøy", + "Nightscout reporting": "Rapporter", + "Cancel": "Avbryt", + "Edit treatment": "Editer behandling", + "Duration": "Varighet", + "Duration in minutes": "Varighet i minutter", + "Temp Basal": "Midlertidig basal", + "Temp Basal Start": "Midlertidig basal start", + "Temp Basal End": "Midlertidig basal stopp", + "Percent": "Prosent", + "Basal change in %": "Basal-endring i %", + "Basal value": "Basalverdi", + "Absolute basal value": "Absolutt basalverdi", + "Announcement": "Kunngjøring", + "Loading temp basal data": "Laster verdier for midlertidig basal", + "Save current record before changing to new?": "Lagre før bytte til ny?", + "Profile Switch": "Bytt profil", + "Profile": "Profil", + "General profile settings": "Profilinstillinger", + "Title": "Tittel", + "Database records": "Databaseverdier", + "Add new record": "Legg til ny rad", + "Remove this record": "Fjern denne raden", + "Clone this record to new": "Kopier til ny rad", + "Record valid from": "Oppføring gyldig fra", + "Stored profiles": "Lagrede profiler", + "Timezone": "Tidssone", + "Duration of Insulin Activity (DIA)": "Insulin-varighet (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Representerer typisk insulinvarighet. Varierer per pasient og per insulintype. Vanligvis 3-4 timer for de fleste typer insulin og de fleste pasientene. Noen ganger også kalt insulin-levetid eller Duration of Insulin Activity, DIA.", + "Insulin to carb ratio (I:C)": "IKH forhold", + "Hours:": "Timer:", + "hours": "timer", + "g/hour": "g/time", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g karbohydrater per enhet insulin. Beskriver hvor mange gram karbohydrater som hånderes av en enhet insulin.", + "Insulin Sensitivity Factor (ISF)": "Insulinfølsomhetsfaktor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dl eller mmol/L per enhet insulin. Beskriver hvor mye blodsukkeret senkes per enhet insulin.", + "Carbs activity / absorption rate": "Karbohydrattid", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gram per tidsenhet. Representerer både endringen i COB per tidsenhet, såvel som mengden av karbohydrater som blir tatt opp i løpet av den tiden. Carb absorpsjon / virkningskurver er mindre forstått enn insulinaktivitet, men kan tilnærmes ved hjelp av en forsinkelse fulgt av en konstant hastighet av absorpsjon ( g / time ) .", + "Basal rates [unit/hour]": "Basal [enhet/time]", + "Target BG range [mg/dL,mmol/L]": "Ønsket blodsukkerintervall [mg/dL, mmol/L]", + "Start of record validity": "Starttidspunkt for gyldighet", + "Icicle": "Istapp", + "Render Basal": "Basalgraf", + "Profile used": "Brukt profil", + "Calculation is in target range.": "Innenfor målområde", + "Loading profile records ...": "Leser profiler", + "Values loaded.": "Verdier lastet", + "Default values used.": "Standardverdier brukt", + "Error. Default values used.": "Feil. Standardverdier brukt.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Tidsintervall for målområde lav og høy stemmer ikke. Standardverdier er brukt.", + "Valid from:": "Gyldig fra:", + "Save current record before switching to new?": "Lagre før bytte til ny?", + "Add new interval before": "Legg til nytt intervall før", + "Delete interval": "Slett intervall", + "I:C": "IK", + "ISF": "ISF", + "Combo Bolus": "Kombinasjonsbolus", + "Difference": "Forskjell", + "New time": "Ny tid", + "Edit Mode": "Editeringsmodus", + "When enabled icon to start edit mode is visible": "Ikon vises når editeringsmodus er aktivert", + "Operation": "Operasjon", + "Move": "Flytt", + "Delete": "Slett", + "Move insulin": "Flytt insulin", + "Move carbs": "Flytt karbohydrater", + "Remove insulin": "Slett insulin", + "Remove carbs": "Slett karbohydrater", + "Change treatment time to %1 ?": "Endre behandlingstid til %1 ?", + "Change carbs time to %1 ?": "Endre karbohydrattid til %1 ?", + "Change insulin time to %1 ?": "Endre insulintid til %1 ?", + "Remove treatment ?": "Slette behandling ?", + "Remove insulin from treatment ?": "Slette insulin fra behandling?", + "Remove carbs from treatment ?": "Slette karbohydrater fra behandling ?", + "Rendering": "Tegner grafikk", + "Loading OpenAPS data of": "Laster OpenAPS data for", + "Loading profile switch data": "Laster nye profildata", + "Redirecting you to the Profile Editor to create a new profile.": "Feil profilinstilling.\nIngen profil valgt for valgt tid.\nVideresender for å lage ny profil.", + "Pump": "Pumpe", + "Sensor Age": "Sensoralder (sage)", + "Insulin Age": "Insulinalder (iage)", + "Temporary target": "Mildertidig mål", + "Reason": "Årsak", + "Eating soon": "Snart tid for mat", + "Top": "Øverst", + "Bottom": "Nederst", + "Activity": "Aktivitet", + "Targets": "Mål", + "Bolus insulin:": "Bolusinsulin:", + "Base basal insulin:": "Programmert basalinsulin:", + "Positive temp basal insulin:": "Positiv midlertidig basalinsulin:", + "Negative temp basal insulin:": "Negativ midlertidig basalinsulin:", + "Total basal insulin:": "Total daglig basalinsulin:", + "Total daily insulin:": "Total daglig insulin", + "Unable to save Role": "Kan ikke lagre rolle", + "Unable to delete Role": "Kan ikke ta bort rolle", + "Database contains %1 roles": "Databasen inneholder %1 roller", + "Edit Role": "Editer rolle", + "admin, school, family, etc": "Administrator, skole, familie osv", + "Permissions": "Rettigheter", + "Are you sure you want to delete: ": "Er du sikker på at du vil slette:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Hver enkelt rolle vil ha en eller flere rettigheter. *-rettigheten er wildcard. Rettigheter settes hierarkisk med : som separator.", + "Add new Role": "Legg til ny rolle", + "Roles - Groups of People, Devices, etc": "Roller - Grupper av brukere, enheter osv", + "Edit this role": "Editer denne rollen", + "Admin authorized": "Administratorgodkjent", + "Subjects - People, Devices, etc": "Ressurser - Brukere, enheter osv", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Hver enkelt ressurs får en unik tilgangsnøkkel og en eller flere roller. Klikk på tilgangsnøkkelen for å åpne valgte ressurs, denne hemmelige lenken kan så deles.", + "Add new Subject": "Legg til ny ressurs", + "Unable to save Subject": "Kan ikke lagre emne", + "Unable to delete Subject": "Kan ikke slette ressurs", + "Database contains %1 subjects": "Databasen inneholder %1 ressurser", + "Edit Subject": "Editer ressurs", + "person, device, etc": "person, enhet osv", + "role1, role2": "rolle1, rolle2", + "Edit this subject": "Editer ressurs", + "Delete this subject": "Slett ressurs", + "Roles": "Roller", + "Access Token": "Tilgangsnøkkel", + "hour ago": "time siden", + "hours ago": "timer siden", + "Silence for %1 minutes": "Stille i %1 minutter", + "Check BG": "Sjekk blodsukker", + "BASAL": "BASAL", + "Current basal": "Gjeldende basal", + "Sensitivity": "Insulinsensitivitet (ISF)", + "Current Carb Ratio": "Gjeldende karbohydratforhold", + "Basal timezone": "Basal tidssone", + "Active profile": "Aktiv profil", + "Active temp basal": "Aktiv midlertidig basal", + "Active temp basal start": "Aktiv midlertidig basal start", + "Active temp basal duration": "Aktiv midlertidig basal varighet", + "Active temp basal remaining": "Gjenstående tid for midlertidig basal", + "Basal profile value": "Basalprofil verdi", + "Active combo bolus": "Kombinasjonsbolus", + "Active combo bolus start": "Kombinasjonsbolus start", + "Active combo bolus duration": "Kombinasjonsbolus varighet", + "Active combo bolus remaining": "Gjenstående kombinasjonsbolus", + "BG Delta": "BS deltaverdi", + "Elapsed Time": "Forløpt tid", + "Absolute Delta": "Absolutt forskjell", + "Interpolated": "Interpolert", + "BWP": "Boluskalk", + "Urgent": "Akutt", + "Warning": "Advarsel", + "Info": "Informasjon", + "Lowest": "Laveste", + "Snoozing high alarm since there is enough IOB": "Utsetter høy alarm siden det er nok aktivt insulin", + "Check BG, time to bolus?": "Sjekk blodsukker, på tide med bolus?", + "Notice": "NB", + "required info missing": "Nødvendig informasjon mangler", + "Insulin on Board": "Aktivt insulin (IOB)", + "Current target": "Gjeldende mål", + "Expected effect": "Forventet effekt", + "Expected outcome": "Forventet resultat", + "Carb Equivalent": "Karbohydratekvivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Insulin tilsvarende %1U mer enn det trengs for å nå lavt mål, karbohydrater ikke medregnet", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Insulin tilsvarende %1U mer enn det trengs for å nå lavt mål, PASS PÅ AT AKTIVT INSULIN ER DEKKET OPP MED KARBOHYDRATER", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U reduksjon trengs i aktivt insulin for å nå lavt mål, for høy basal?", + "basal adjustment out of range, give carbs?": "basaljustering utenfor tillatt område, gi karbohydrater?", + "basal adjustment out of range, give bolus?": "basaljustering utenfor tillatt område, gi bolus?", + "above high": "over høy grense", + "below low": "under lav grense", + "Projected BG %1 target": "Predikert BS %1 mål", + "aiming at": "sikter på", + "Bolus %1 units": "Bolus %1 enheter", + "or adjust basal": "eller justere basal", + "Check BG using glucometer before correcting!": "Sjekk blodsukker før korrigering!", + "Basal reduction to account %1 units:": "Basalredusering for å nå %1 enheter", + "30m temp basal": "30 minutters midlertidig basal", + "1h temp basal": "60 minutters midlertidig basal", + "Cannula change overdue!": "Byttetid for infusjonssett overskredet", + "Time to change cannula": "På tide å bytte infusjonssett", + "Change cannula soon": "Bytt infusjonssett snart", + "Cannula age %1 hours": "Nålalder %1 timer", + "Inserted": "Satt inn", + "CAGE": "Nålalder", + "COB": "Aktive karbo", + "Last Carbs": "Siste karbohydrater", + "IAGE": "Insulinalder", + "Insulin reservoir change overdue!": "Insulinbyttetid overskrevet", + "Time to change insulin reservoir": "På tide å bytte insulinreservoar", + "Change insulin reservoir soon": "Bytt insulinreservoar snart", + "Insulin reservoir age %1 hours": "Insulinreservoaralder %1 timer", + "Changed": "Byttet", + "IOB": "Aktivt insulin", + "Careportal IOB": "Aktivt insulin i Careportal", + "Last Bolus": "Siste Bolus", + "Basal IOB": "Basal Aktivt Insulin", + "Source": "Kilde", + "Stale data, check rig?": "Gamle data, sjekk rigg?", + "Last received:": "Sist mottatt:", + "%1m ago": "%1m siden", + "%1h ago": "%1t siden", + "%1d ago": "%1d siden", + "RETRO": "GAMMELT", + "SAGE": "Sensoralder", + "Sensor change/restart overdue!": "Sensor bytte/omstart overskredet!", + "Time to change/restart sensor": "På tide å bytte/restarte sensoren", + "Change/restart sensor soon": "Bytt/restart sensoren snart", + "Sensor age %1 days %2 hours": "Sensoralder %1 dager %2 timer", + "Sensor Insert": "Sensor satt inn", + "Sensor Start": "Sensorstart", + "days": "dager", + "Insulin distribution": "Insulinfordeling", + "To see this report, press SHOW while in this view": "Klikk på \"VIS\" for å se denne rapporten", + "AR2 Forecast": "AR2-prediksjon", + "OpenAPS Forecasts": "OpenAPS-prediksjon", + "Temporary Target": "Midlertidig mål", + "Temporary Target Cancel": "Avslutt midlertidig mål", + "OpenAPS Offline": "OpenAPS offline", + "Profiles": "Profiler", + "Time in fluctuation": "Tid i fluktuasjon", + "Time in rapid fluctuation": "Tid i hurtig fluktuasjon", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Dette er kun et grovt estimat som kan være misvisende. Det erstatter ikke en blodprøve. Formelen er hentet fra:", + "Filter by hours": "Filtrer per time", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Tid i fluktuasjon og tid i hurtig fluktuasjon måler % av tiden i den aktuelle perioden der blodsukkeret har endret seg relativt raskt. Lave verdier er best.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Gjennomsnitt total daglig endring er summen av absolutverdier av alle glukoseendringer i den aktuelle perioden, divideret med antall dager. Lave verdier er best.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Gjennomsnitt endring per time er summen av absoluttverdier av alle glukoseendringer i den aktuelle perioden divideret med antall timer. Lave verdier er best.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS er beregnet ved å ta kvadratet av avstanden utenfor målområdet for alle blodsukkerverdier i den aktuelle perioden, summere dem, dele på antallet, og ta kvadratroten av resultatet. Dette målet er lignende som prodentvis tid i målområdet, men vekter målinger som ligger langt utenfor målområdet høyere. Lave verdier er best.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">her.", + "Mean Total Daily Change": "Gjennomsnitt total daglig endring", + "Mean Hourly Change": "Gjennomsnitt endring per time", + "FortyFiveDown": "svakt fallende", + "FortyFiveUp": "svakt stigende", + "Flat": "stabilt", + "SingleUp": "stigende", + "SingleDown": "fallende", + "DoubleDown": "hurtig fallende", + "DoubleUp": "hurtig stigende", + "virtAsstUnknown": "Den verdien er ukjent for øyeblikket. Vennligst se Nightscout siden for mer informasjon.", + "virtAsstTitleAR2Forecast": "AR2 Prognose", + "virtAsstTitleCurrentBasal": "Gjeldende Basal", + "virtAsstTitleCurrentCOB": "Nåværende COB", + "virtAsstTitleCurrentIOB": "Nåværende IOB", + "virtAsstTitleLaunch": "Velkommen til Nightscout", + "virtAsstTitleLoopForecast": "Loop Prognose", + "virtAsstTitleLastLoop": "Siste Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Prognose", + "virtAsstTitlePumpReservoir": "Insulin som gjenstår", + "virtAsstTitlePumpBattery": "Pumpebatteri", + "virtAsstTitleRawBG": "Nåværende Raw BG", + "virtAsstTitleUploaderBattery": "Opplaster-batteri", + "virtAsstTitleCurrentBG": "Nåværende BS", + "virtAsstTitleFullStatus": "Fullstendig status", + "virtAsstTitleCGMMode": "CGM modus", + "virtAsstTitleCGMStatus": "CGM status", + "virtAsstTitleCGMSessionAge": "CGM øktalder", + "virtAsstTitleCGMTxStatus": "CGM sender-status", + "virtAsstTitleCGMTxAge": "CMG Alder på sender", + "virtAsstTitleCGMNoise": "CGM støy", + "virtAsstTitleDelta": "Blodglukose Delta", + "virtAsstStatus": "%1 og %2 fra %3.", + "virtAsstBasal": "%1 gjeldende basalenhet er %2 enheter per time", + "virtAsstBasalTemp": "%1 midlertidig basal av %2 enheter per time vil avslutte %3", + "virtAsstIob": "og du har %1 insulin i kroppen.", + "virtAsstIobIntent": "Og du har %1 insulin i kroppen", + "virtAsstIobUnits": "%1 enheter av", + "virtAsstLaunch": "Hva vil du sjekke på Nightscout?", + "virtAsstPreamble": "Din", + "virtAsstPreamble3person": "%1 har en ", + "virtAsstNoInsulin": "nei", + "virtAsstUploadBattery": "Opplaster-batteriet er %1", + "virtAsstReservoir": "Du har %1 enheter igjen", + "virtAsstPumpBattery": "Pumpebatteriet er %1 %2", + "virtAsstUploaderBattery": "Opplaster-batteriet er %1", + "virtAsstLastLoop": "Siste vellykkede loop var %1", + "virtAsstLoopNotAvailable": "Programtillegg for Loop ser ikke ut til å være aktivert", + "virtAsstLoopForecastAround": "Ifølge loop-prognosen forventes du å være rundt %1 i løpet av den neste %2", + "virtAsstLoopForecastBetween": "I følge loop-prognosen forventes du å være mellom %1 og %2 i løpet av den neste %3", + "virtAsstAR2ForecastAround": "I følge prognose fra AR2 forventes du å være rundt %1 i løpet av den neste %2", + "virtAsstAR2ForecastBetween": "I følge AR2-prognosen forventes du å være mellom %1 og %2 i løpet av de neste %3", + "virtAsstForecastUnavailable": "Kan ikke forutsi med dataene som er tilgjengelig", + "virtAsstRawBG": "Din raw bg er %1", + "virtAsstOpenAPSForecast": "OpenAPS forventer at blodsukkeret er %1", + "virtAsstCob3person": "%1 har %2 karbohydrater i bruk", + "virtAsstCob": "Du har %1 karbohydrater i bruk", + "virtAsstCGMMode": "CGM-modusen din var %1 fra %2.", + "virtAsstCGMStatus": "CGM statusen din var %1 fra %2.", + "virtAsstCGMSessAge": "CGM økten har vært aktiv i %1 dager og %2 timer.", + "virtAsstCGMSessNotStarted": "Det er ingen aktiv CGM økt for øyeblikket.", + "virtAsstCGMTxStatus": "CGM sender status var %1 fra %2.", + "virtAsstCGMTxAge": "CGM-sender er %1 dager gammel.", + "virtAsstCGMNoise": "CGM statusen din var %1 fra %2.", + "virtAsstCGMBattOne": "CGM-batteriet ditt var %1 volt som av %2.", + "virtAsstCGMBattTwo": "CGM batterinivåene dine var %1 volt og %2 volt som av %3.", + "virtAsstDelta": "Din delta var %1 mellom %2 og %3.", + "virtAsstDeltaEstimated": "Din estimerte delta var %1 mellom %2 og %3.", + "virtAsstUnknownIntentTitle": "Ukjent hensikt", + "virtAsstUnknownIntentText": "Beklager, jeg vet ikke hva du ber om.", + "Fat [g]": "Fett [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energi [kJ]", + "Clock Views:": "Klokkevisning:", + "Clock": "Klokke", + "Color": "Farge", + "Simple": "Enkel", + "TDD average": "Gjennomsnitt TDD", + "Bolus average": "Gjennomsnitt bolus", + "Basal average": "Gjennomsnitt basal", + "Base basal average:": "Programmert gj.snitt basal", + "Carbs average": "Gjennomsnitt karbohydrater", + "Eating Soon": "Spiser snart", + "Last entry {0} minutes ago": "Siste registrering var for {0} minutter siden", + "change": "endre", + "Speech": "Tale", + "Target Top": "Høyt mål", + "Target Bottom": "Lavt mål", + "Canceled": "Avbrutt", + "Meter BG": "Blodsukkermåler BS", + "predicted": "predikert", + "future": "Fremtid", + "ago": "Siden", + "Last data received": "Siste data mottatt", + "Clock View": "Klokkevisning", + "Protein": "Protein", + "Fat": "Fett", + "Protein average": "Gjennomsnitt protein", + "Fat average": "Gjennomsnitt fett", + "Total carbs": "Totale karbohydrater", + "Total protein": "Totalt protein", + "Total fat": "Totalt fett", + "Database Size": "Databasestørrelse (dbsize)", + "Database Size near its limits!": "Databasestørrelsen nærmer seg grensene!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Databasestørrelsen er %1 MiB av %2 MiB. Vennligst ta backup og rydd i databasen!", + "Database file size": "Database filstørrelse", + "%1 MiB of %2 MiB (%3%)": "%1 MiB av %2 MiB (%3%)", + "Data size": "Datastørrelse", + "virtAsstDatabaseSize": "%1 MiB. Dette er %2% av tilgjengelig databaseplass.", + "virtAsstTitleDatabaseSize": "Database filstørrelse", + "Carbs/Food/Time": "Karbo/Mat/Abs.tid", + "You have administration messages": "Du har administrasjonsmeldinger", + "Admin messages in queue": "Admin meldinger i køen", + "Queue empty": "Tom kø", + "There are no admin messages in queue": "Det er ingen admin meldinger i køen", + "Please sign in using the API_SECRET to see your administration messages": "Vennligst logg inn med API_SECRET for å se administrasjonsmeldingene dine", + "Reads enabled in default permissions": "Lesetilgang er aktivert i innstillingene", + "Data reads enabled": "Lesetilgang aktivert", + "Data writes enabled": "Skrivetilgang aktivert", + "Data writes not enabled": "Skrivetilgang ikke aktivert", + "Color prediction lines": "Fargede prediksjonslinjer", + "Release Notes": "Utgivelsesnotater", + "Check for Updates": "Se etter oppdateringer", + "Open Source": "Åpen kildekode", + "Nightscout Info": "Dokumentasjon for Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Det primære formålet med Loopalyzer er å visualisere hvordan Loop opererer i lukket loop. Den kan også virke med andre oppsett, både lukket og åpen loop, og uten loop. Men avhengig av hvilken opplaster du bruker, hvor ofte den kan samle og laste opp data, og hvordan det kan laste opp manglende data i ettertid, kan noen grafer ha tomrom eller til og med være helt fraværende. Sørg alltid for at grafene ser rimelige ut. For å sjekke at dataene er komplette, er det best å se på én dag om gangen, og bla gjennom noen dager slik at mangler kan oppdages.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer har en tidsforskyvnings-funksjon. Hvis du for eksempel spiser frokost kl. 07:00 én dag og kl 08:00 dagen etter, vil den gjennomsnittlige blodsukkerkurven for disse to dagene mest sannsynlig se utflatet ut, og skjule den faktiske responsen av måltidet. Tidsforskyvningen beregner det gjennomsnittlige starttidspunktet for måltidene, og forskyver deretter alle data (karbohydrat, insulin, basaldata osv.) for begge dagene slik at måltidene sammenfaller med gjennomsnittlig starttid.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "I dette eksemplet skyves alle data fra første dag 30 minutter fremover i tid, og alle data fra andre dag 30 minutter bakover i tid. Det vil da fremstå som om du spiste frokost klokken 07:30 begge dager. Dette gjør at du kan se din faktiske gjennomsnittlige blodglukoserespons fra et måltid.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Tidsforskyvningen blir uthevet med grått for perioden etter gjennomsnittlig starttid for måltidet, med varighet lik DIA (Duration of Insulin Action). Siden alle datapunktene er forskjøvet for hele dagen, kan kurvene utenfor det grå området være unøyaktige.", + "Note that time shift is available only when viewing multiple days.": "Merk at tidsforskyvning kun er tilgjengelig når du ser på flere dager.", + "Please select a maximum of two weeks duration and click Show again.": "Velg maksimalt to ukers varighet og klikk på Vis igjen.", + "Show profiles table": "Vis profiler som tabell", + "Show predictions": "Vis prediksjoner", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Tidsforskyvning for måltider som er større enn %1 g karbohydrater mellom kl %2 og kl %3", + "Previous": "Forrige", + "Previous day": "Forrige dag", + "Next day": "Neste dag", + "Next": "Neste", + "Temp basal delta": "Midlertidig basal delta", + "Authorized by token": "Autorisert med tilgangsnøkkel", + "Auth role": "Autorisert", + "view without token": "vis uten tilgangsnøkkel", + "Remove stored token": "Fjern lagret tilgangsnøkkel", + "Weekly Distribution": "Ukentlige resultat", + "Failed authentication": "Autentisering mislykket", + "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?": "En enhet med IP-adresse %1 forsøkte å autentisere Nightscout med feil legitimasjon. Sjekk om du har en opplaster-app med feil API_SECRET eller tilgangsnøkkel.", + "Default (with leading zero and U)": "Standard (med foranstilt null og E)", + "Concise (with U, without leading zero)": "Konsis (med E, uten foranstilt null)", + "Minimal (without leading zero and U)": "Minimal (uten foranstilt null og E)", + "Small Bolus Display": "Liten markør for bolus", + "Large Bolus Display": "Stor markør for bolus", + "Bolus Display Threshold": "Vis bolus større enn eller lik", + "%1 U and Over": "%1 E", + "Event repeated %1 times.": "Hendelse gjentatt %1 ganger.", + "minutes": "minutt", + "Last recorded %1 %2 ago.": "Sist registrert %1 %2 siden.", + "Security issue": "Sikkerhetsproblem", + "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.": "Svakt API_SECRET oppdaget. Vennligst bruk en blanding av små og STORE bokstaver, tall og ikke-alfanumeriske tegn, som !#%&/ for å redusere risikoen for uautorisert tilgang. Minimumslengden på API_SECRET er 12 tegn.", + "less than 1": "mindre enn 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB passord og API_SECRET samsvarer. Dette er en veldig dårlig idé. Vennligst endre begge og ikke gjenbruk samme passord over alt i hele systemet." +} diff --git a/translations/nl_NL.json b/translations/nl_NL.json new file mode 100644 index 00000000000..ef0094d9a2b --- /dev/null +++ b/translations/nl_NL.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Luistert op poort", + "Mo": "Ma", + "Tu": "Di", + "We": "Wo", + "Th": "Do", + "Fr": "Vr", + "Sa": "Za", + "Su": "Zo", + "Monday": "Maandag", + "Tuesday": "Dinsdag", + "Wednesday": "Woensdag", + "Thursday": "Donderdag", + "Friday": "Vrijdag", + "Saturday": "Zaterdag", + "Sunday": "Zondag", + "Category": "Categorie", + "Subcategory": "Subcategorie", + "Name": "Naam", + "Today": "Vandaag", + "Last 2 days": "Afgelopen 2 dagen", + "Last 3 days": "Afgelopen 3 dagen", + "Last week": "Afgelopen week", + "Last 2 weeks": "Afgelopen 2 weken", + "Last month": "Afgelopen maand", + "Last 3 months": "Afgelopen 3 maanden", + "From": "Van", + "To": "Tot", + "Notes": "Notities", + "Food": "Voeding", + "Insulin": "Insuline", + "Carbs": "Koolhydraten", + "Notes contain": "Notities bevatten", + "Target BG range bottom": "Ondergrens doelbereik glucose", + "top": "Top", + "Show": "Laat zien", + "Display": "Weergeven", + "Loading": "Laden", + "Loading profile": "Profiel laden", + "Loading status": "Laadstatus", + "Loading food database": "Voeding database laden", + "not displayed": "Niet weergegeven", + "Loading CGM data of": "CGM data laden van", + "Loading treatments data of": "Behandel gegevens laden van", + "Processing data of": "Gegevens verwerken van", + "Portion": "Portie", + "Size": "Grootte", + "(none)": "(geen)", + "None": "geen", + "": "", + "Result is empty": "Geen resultaat", + "Day to day": "Dag tot Dag", + "Week to week": "Week tot week", + "Daily Stats": "Dagelijkse statistiek", + "Percentile Chart": "Procentuele grafiek", + "Distribution": "Verdeling", + "Hourly stats": "Statistieken per uur", + "netIOB stats": "netIOB statistieken", + "temp basals must be rendered to display this report": "tijdelijk basaal moet zichtbaar zijn voor dit rapport", + "No data available": "Geen gegevens beschikbaar", + "Low": "Laag", + "In Range": "Binnen bereik", + "Period": "Periode", + "High": "Hoog", + "Average": "Gemiddeld", + "Low Quartile": "Eerste kwartiel", + "Upper Quartile": "Derde kwartiel", + "Quartile": "Kwartiel", + "Date": "Datum", + "Normal": "Normaal", + "Median": "Mediaan", + "Readings": "Metingen", + "StDev": "Std. deviatie", + "Daily stats report": "Dagelijkse statistieken", + "Glucose Percentile report": "Glucose percentiel rapport", + "Glucose distribution": "Glucose verdeling", + "days total": "Totaal dagen", + "Total per day": "Totaal dagen", + "Overall": "Totaal", + "Range": "Bereik", + "% of Readings": "% metingen", + "# of Readings": "Aantal metingen", + "Mean": "Gemiddeld", + "Standard Deviation": "Standaard deviatie", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "Geschatte HbA1C", + "Weekly Success": "Wekelijks succes", + "There is not sufficient data to run this report. Select more days.": "Er zijn niet genoeg gegevens voor dit rapport, selecteer meer dagen.", + "Using stored API secret hash": "Gebruik opgeslagen geheime API Hash", + "No API secret hash stored yet. You need to enter API secret.": "Er is nog geen geheime API Hash opgeslagen. U moet eerst een geheime API code invoeren.", + "Database loaded": "Database geladen", + "Error: Database failed to load": "FOUT: Database niet geladen", + "Error": "Fout", + "Create new record": "Opslaan", + "Save record": "Sla op", + "Portions": "Porties", + "Unit": "Eenheid", + "GI": "Glycemische index ", + "Edit record": "Bewerk invoer", + "Delete record": "Verwijder invoer", + "Move to the top": "Ga naar boven", + "Hidden": "Verborgen", + "Hide after use": "Verberg na gebruik", + "Your API secret must be at least 12 characters long": "Uw API wachtwoord dient tenminste 12 karakters lang te zijn", + "Bad API secret": "Onjuist API wachtwoord", + "API secret hash stored": "API wachtwoord opgeslagen", + "Status": "Status", + "Not loaded": "Niet geladen", + "Food Editor": "Voeding beheer", + "Your database": "Uw database", + "Filter": "Filter", + "Save": "Opslaan", + "Clear": "Leeg maken", + "Record": "Toevoegen", + "Quick picks": "Snelkeuze", + "Show hidden": "Laat verborgen zien", + "Your API secret or token": "Je API geheim of token", + "Remember this device. (Do not enable this on public computers.)": "Onthoud dit apparaat. (Dit niet inschakelen op openbare computers.)", + "Treatments": "Behandelingen", + "Time": "Tijd", + "Event Type": "Soort", + "Blood Glucose": "Bloed glucose", + "Entered By": "Ingevoerd door", + "Delete this treatment?": "Verwijder", + "Carbs Given": "Aantal koolhydraten", + "Insulin Given": "Toegediende insuline", + "Event Time": "Tijdstip", + "Please verify that the data entered is correct": "Controleer uw invoer", + "BG": "BG", + "Use BG correction in calculation": "Gebruik BG in berekeningen", + "BG from CGM (autoupdated)": "BG van CGM (automatische invoer)", + "BG from meter": "BG van meter", + "Manual BG": "Handmatige BG", + "Quickpick": "Snelkeuze", + "or": "of", + "Add from database": "Toevoegen uit database", + "Use carbs correction in calculation": "Gebruik KH correctie in berekening", + "Use COB correction in calculation": "Gebruik ingenomen KH in berekening", + "Use IOB in calculation": "Gebruik IOB in berekening", + "Other correction": "Andere correctie", + "Rounding": "Afgerond", + "Enter insulin correction in treatment": "Voer insuline correctie toe aan behandeling", + "Insulin needed": "Benodigde insuline", + "Carbs needed": "Benodigde koolhydraten", + "Carbs needed if Insulin total is negative value": "Benodigde KH als insuline een negatieve waarde geeft", + "Basal rate": "Basaal snelheid", + "60 minutes earlier": "60 minuten eerder", + "45 minutes earlier": "45 minuten eerder", + "30 minutes earlier": "30 minuten eerder", + "20 minutes earlier": "20 minuten eerder", + "15 minutes earlier": "15 minuten eerder", + "Time in minutes": "Tijd in minuten", + "15 minutes later": "15 minuten later", + "20 minutes later": "20 minuten later", + "30 minutes later": "30 minuten later", + "45 minutes later": "45 minuten later", + "60 minutes later": "60 minuten later", + "Additional Notes, Comments": "Extra aantekeningen", + "RETRO MODE": "Retro mode", + "Now": "Nu", + "Other": "Andere", + "Submit Form": "Formulier opslaan", + "Profile Editor": "Profiel beheer", + "Reports": "Rapporten", + "Add food from your database": "Voeg voeding toe uit uw database", + "Reload database": "Database opnieuw laden", + "Add": "Toevoegen", + "Unauthorized": "Ongeauthoriseerd", + "Entering record failed": "Toevoegen mislukt", + "Device authenticated": "Apparaat geauthenticeerd", + "Device not authenticated": "Apparaat niet geauthenticeerd", + "Authentication status": "Authenticatie status", + "Authenticate": "Authenticatie", + "Remove": "Verwijder", + "Your device is not authenticated yet": "Uw apparaat is nog niet geauthenticeerd", + "Sensor": "Sensor", + "Finger": "Vinger", + "Manual": "Handmatig", + "Scale": "Schaal", + "Linear": "Lineair", + "Logarithmic": "Logaritmisch", + "Logarithmic (Dynamic)": "Logaritmisch (Dynamisch)", + "Insulin-on-Board": "Actieve insuline (IOB)", + "Carbs-on-Board": "Actieve koolhydraten (COB)", + "Bolus Wizard Preview": "Bolus Wizard Preview (BWP)", + "Value Loaded": "Waarde geladen", + "Cannula Age": "Canule leeftijd (CAGE)", + "Basal Profile": "Basaal profiel", + "Silence for 30 minutes": "Sluimer 30 minuten", + "Silence for 60 minutes": "Sluimer 60 minuten", + "Silence for 90 minutes": "Sluimer 90 minuten", + "Silence for 120 minutes": "Sluimer 2 uur", + "Settings": "Instellingen", + "Units": "Eenheden", + "Date format": "Datum formaat", + "12 hours": "12 uur", + "24 hours": "24 uur", + "Log a Treatment": "Registreer een behandeling", + "BG Check": "Bloedglucose check", + "Meal Bolus": "Maaltijd bolus", + "Snack Bolus": "Snack bolus", + "Correction Bolus": "Correctie bolus", + "Carb Correction": "Koolhydraat correctie", + "Note": "Notitie", + "Question": "Vraag", + "Exercise": "Beweging / sport", + "Pump Site Change": "Nieuwe pomp infuus", + "CGM Sensor Start": "CGM Sensor start", + "CGM Sensor Stop": "CGM Sensor Stop", + "CGM Sensor Insert": "CGM sensor wissel", + "Sensor Code": "Sensor Code", + "Transmitter ID": "Zender ID", + "Dexcom Sensor Start": "Dexcom sensor start", + "Dexcom Sensor Change": "Dexcom sensor wissel", + "Insulin Cartridge Change": "Insuline cartridge wissel", + "D.A.D. Alert": "Hulphond waarschuwing", + "Glucose Reading": "Glucose meting", + "Measurement Method": "Meetmethode", + "Meter": "Glucosemeter", + "Amount in grams": "Hoeveelheid in gram", + "Amount in units": "Aantal in eenheden", + "View all treatments": "Bekijk alle behandelingen", + "Enable Alarms": "Alarmen aan!", + "Pump Battery Change": "Pompbatterij vervangen", + "Pump Battery Age": "Pomp Batterij Leeftijd", + "Pump Battery Low Alarm": "Pompbatterij bijna leeg Alarm", + "Pump Battery change overdue!": "Pompbatterij moet vervangen worden!", + "When enabled an alarm may sound.": "Als ingeschakeld kan alarm klinken", + "Urgent High Alarm": "Urgent Alarm Hoge BG", + "High Alarm": "Alarm hoge BG", + "Low Alarm": "Alarm lage BG", + "Urgent Low Alarm": "Urgent Alarm lage BG", + "Stale Data: Warn": "Waarschuwing Oude gegevens na", + "Stale Data: Urgent": "Urgente Waarschuwing Oude gegevens na", + "mins": "minuten", + "Night Mode": "Nachtstand", + "When enabled the page will be dimmed from 10pm - 6am.": "Scherm dimmen tussen 22:00 en 06:00", + "Enable": "Activeren", + "Show Raw BG Data": "Laat ruwe data zien", + "Never": "Nooit", + "Always": "Altijd", + "When there is noise": "Bij ruis", + "When enabled small white dots will be displayed for raw BG data": "Indien geactiveerd is ruwe data zichtbaar als witte punten", + "Custom Title": "Eigen titel", + "Theme": "Thema", + "Default": "Standaard", + "Colors": "Kleuren", + "Colorblind-friendly colors": "Kleurenblind vriendelijke kleuren", + "Reset, and use defaults": "Herstel standaard waardes", + "Calibrations": "Kalibraties", + "Alarm Test / Smartphone Enable": "Alarm test / activeer Smartphone", + "Bolus Wizard": "Bolus calculator", + "in the future": "In de toekomst", + "time ago": "tijd geleden", + "hr ago": "uur geleden", + "hrs ago": "uren geleden", + "min ago": "minuut geleden", + "mins ago": "minuten geleden", + "day ago": "dag geleden", + "days ago": "dagen geleden", + "long ago": "lang geleden", + "Clean": "Schoon", + "Light": "Licht", + "Medium": "Gemiddeld", + "Heavy": "Zwaar", + "Treatment type": "Type behandeling", + "Raw BG": "Ruwe BG data", + "Device": "Apparaat", + "Noise": "Ruis", + "Calibration": "Kalibratie", + "Show Plugins": "Laat Plug-Ins zien", + "About": "Over", + "Value in": "Waarde in", + "Carb Time": "Koolhydraten tijd", + "Language": "Taal", + "Add new": "Voeg toe", + "g": "g", + "ml": "ml", + "pcs": "stk", + "Drag&drop food here": "Maaltijd naar hier verplaatsen", + "Care Portal": "Zorgportaal", + "Medium/Unknown": "Medium/Onbekend", + "IN THE FUTURE": "IN DE TOEKOMST", + "Order": "Sortering", + "oldest on top": "Oudste boven", + "newest on top": "Nieuwste boven", + "All sensor events": "Alle sensor gegevens", + "Remove future items from mongo database": "Verwijder items die in de toekomst liggen uit database", + "Find and remove treatments in the future": "Zoek en verwijder behandelingen met datum in de toekomst", + "This task find and remove treatments in the future.": "Dit commando zoekt en verwijdert behandelingen met datum in de toekomst", + "Remove treatments in the future": "Verwijder behandelingen met datum in de toekomst", + "Find and remove entries in the future": "Zoek en verwijder behandelingen met datum in de toekomst", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Dit commando zoekt en verwijdert behandelingen met datum in de toekomst", + "Remove entries in the future": "Verwijder invoer met datum in de toekomst", + "Loading database ...": "Database laden ....", + "Database contains %1 future records": "Database bevat %1 toekomstige data", + "Remove %1 selected records?": "Verwijder %1 geselecteerde data?", + "Error loading database": "Fout bij het laden van database", + "Record %1 removed ...": "Data %1 verwijderd ", + "Error removing record %1": "Fout bij het verwijderen van %1 data", + "Deleting records ...": "Verwijderen van data .....", + "%1 records deleted": "%1 records verwijderd", + "Clean Mongo status database": "Ruim Mongo database status op", + "Delete all documents from devicestatus collection": "Verwijder alle documenten uit \"devicestatus\" database", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Dit commando verwijdert alle documenten uit \"devicestatus\" database. Handig wanneer de batterij status niet correct wordt geupload.", + "Delete all documents": "Verwijder alle documenten", + "Delete all documents from devicestatus collection?": "Wil je alle data van \"devicestatus\" database verwijderen?", + "Database contains %1 records": "Database bevat %1 gegevens", + "All records removed ...": "Alle gegevens verwijderd", + "Delete all documents from devicestatus collection older than 30 days": "Verwijder alle documenten uit \"devicestatus\" database ouder dan 30 dagen", + "Number of Days to Keep:": "Aantal dagen te bewaren:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Dit commando verwijdert alle documenten uit de \"devicestatus\" database. Handig wanneer de batterij status niet correct wordt geüpload.", + "Delete old documents from devicestatus collection?": "Wil je oude data uit de \"devicestatus\" database verwijderen?", + "Clean Mongo entries (glucose entries) database": "Mongo gegevens (glucose waarden) opschonen", + "Delete all documents from entries collection older than 180 days": "Verwijder alle documenten uit \"entries\" database ouder dan 180 dagen", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Dit commando verwijdert alle documenten uit de \"entries\" database. Handig wanneer de batterij status niet correct wordt geüpload.", + "Delete old documents": "Verwijder oude documenten", + "Delete old documents from entries collection?": "Wil je oude data uit de \"entries\" database verwijderen?", + "%1 is not a valid number": "%1 is geen geldig nummer", + "%1 is not a valid number - must be more than 2": "%1 is geen geldig nummer - moet meer zijn dan 2", + "Clean Mongo treatments database": "Ruim Mongo behandel database op", + "Delete all documents from treatments collection older than 180 days": "Verwijder alle documenten uit \"treatments\" database ouder dan 180 dagen", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Dit commando verwijdert alle documenten uit de \"treatments\" database. Handig wanneer de batterij status niet correct wordt geüpload.", + "Delete old documents from treatments collection?": "Wil je oude data uit de \"treatments\" database verwijderen?", + "Admin Tools": "Admin tools", + "Nightscout reporting": "Nightscout rapportages", + "Cancel": "Annuleer", + "Edit treatment": "Bewerkt behandeling", + "Duration": "Duur", + "Duration in minutes": "Duur in minuten", + "Temp Basal": "Tijdelijke basaal", + "Temp Basal Start": "Start tijdelijke basaal", + "Temp Basal End": "Einde tijdelijke basaal", + "Percent": "Procent", + "Basal change in %": "Basaal aanpassing in %", + "Basal value": "Basaal snelheid", + "Absolute basal value": "Absolute basaal waarde", + "Announcement": "Mededeling", + "Loading temp basal data": "Laden tijdelijke basaal gegevens", + "Save current record before changing to new?": "Opslaan voor verder te gaan?", + "Profile Switch": "Profiel wissel", + "Profile": "Profiel", + "General profile settings": "Profiel instellingen", + "Title": "Titel", + "Database records": "Database gegevens", + "Add new record": "Toevoegen", + "Remove this record": "Verwijder", + "Clone this record to new": "Kopieer deze invoer naar nieuwe", + "Record valid from": "Geldig van", + "Stored profiles": "Opgeslagen profielen", + "Timezone": "Tijdzone", + "Duration of Insulin Activity (DIA)": "Werkingsduur insuline (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Geeft de werkingsduur van de insuline in het lichaam aan. Dit verschilt van patient tot patient er per soort insuline, algemeen gemiddelde is 3 tot 4 uur. ", + "Insulin to carb ratio (I:C)": "Insuline - Koolhydraat ratio (I:C)", + "Hours:": "Uren:", + "hours": "Uren", + "g/hour": "g/uur", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "G KH per Eh insuline. De verhouding tussen hoeveel grammen koohlhydraten er verwerkt kunnen worden per eenheid insuline.", + "Insulin Sensitivity Factor (ISF)": "Insuline gevoeligheid (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL of mmol/L per E insuline. Ratio daling BG waarde per eenheid correctie", + "Carbs activity / absorption rate": "Koolhydraat opname snelheid", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grammen per tijdseenheid. Geeft de wijzigingen in COB per tijdseenheid alsookde hoeveelheid KH dat impact zou moeten hebben over deze tijdspanne. KH absorbtie / activiteits curveszijn minder eenvoudig uitzetbaar, maar kunnen geschat worden door gebruik te maken van een startvertreging en daarna een constante curve van absorbtie (g/u).", + "Basal rates [unit/hour]": "Basaal snelheid [eenheden/uur]", + "Target BG range [mg/dL,mmol/L]": "Doel BG waarde in [mg/dl,mmol/L", + "Start of record validity": "Start geldigheid", + "Icicle": "Ijspegel", + "Render Basal": "Toon basaal", + "Profile used": "Gebruikt profiel", + "Calculation is in target range.": "Berekening valt binnen doelwaards", + "Loading profile records ...": "Laden profiel gegevens", + "Values loaded.": "Gegevens geladen", + "Default values used.": "Standaard waardes gebruikt", + "Error. Default values used.": "FOUT: Standaard waardes gebruikt", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Tijdspanne van laag en hoog doel zijn niet correct. Standaard waarden worden gebruikt", + "Valid from:": "Geldig van:", + "Save current record before switching to new?": "Opslaan voor verder te gaan?", + "Add new interval before": "Voeg interval toe voor", + "Delete interval": "Verwijder interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Pizza Bolus", + "Difference": "Verschil", + "New time": "Nieuwe tijd", + "Edit Mode": "Wijzigen uitvoeren", + "When enabled icon to start edit mode is visible": "Als geactiveerd is Wijzigen modus beschikbaar", + "Operation": "Operatie", + "Move": "Verplaats", + "Delete": "Verwijder", + "Move insulin": "Verplaats insuline", + "Move carbs": "Verplaats KH", + "Remove insulin": "Verwijder insuline", + "Remove carbs": "Verwijder KH", + "Change treatment time to %1 ?": "Wijzig behandel tijdstip naar %1", + "Change carbs time to %1 ?": "Wijzig KH tijdstip naar %1", + "Change insulin time to %1 ?": "Wijzig insuline tijdstip naar %1", + "Remove treatment ?": "Verwijder behandeling", + "Remove insulin from treatment ?": "Verwijder insuline van behandeling", + "Remove carbs from treatment ?": "Verwijder KH van behandeling", + "Rendering": "Renderen", + "Loading OpenAPS data of": "OpenAPS gegevens opladen van", + "Loading profile switch data": "Ophalen van data om profiel te wisselen", + "Redirecting you to the Profile Editor to create a new profile.": "Verkeerde profiel instellingen.\ngeen profiel beschibaar voor getoonde tijd.\nVerder naar profiel editor om een profiel te maken.", + "Pump": "Pomp", + "Sensor Age": "Sensor leeftijd", + "Insulin Age": "Insuline ouderdom (IAGE)", + "Temporary target": "Tijdelijk doel", + "Reason": "Reden", + "Eating soon": "Binnenkort eten", + "Top": "Boven", + "Bottom": "Beneden", + "Activity": "Activiteit", + "Targets": "Doelen", + "Bolus insulin:": "Bolus insuline", + "Base basal insulin:": "Basis basaal insuline", + "Positive temp basal insulin:": "Extra tijdelijke basaal insuline", + "Negative temp basal insulin:": "Negatieve tijdelijke basaal insuline", + "Total basal insulin:": "Totaal basaal insuline", + "Total daily insulin:": "Totaal dagelijkse insuline", + "Unable to save Role": "Kan rol niet opslaan", + "Unable to delete Role": "Niet mogelijk rol te verwijderen", + "Database contains %1 roles": "Database bevat %1 rollen", + "Edit Role": "Pas rol aan", + "admin, school, family, etc": "admin, school, familie, etc", + "Permissions": "Rechten", + "Are you sure you want to delete: ": "Weet u het zeker dat u wilt verwijderen?", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Elke rol heeft mintens 1 machtiging. De * machtiging is een wildcard, machtigingen hebben een hyrarchie door gebruik te maken van : als scheidingsteken.", + "Add new Role": "Voeg rol toe", + "Roles - Groups of People, Devices, etc": "Rollen - Groepen mensen apparaten etc", + "Edit this role": "Pas deze rol aan", + "Admin authorized": "Admin geauthoriseerd", + "Subjects - People, Devices, etc": "Onderwerpen - Mensen, apparaten etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Elk onderwerp heeft een unieke toegangscode en 1 of meer rollen. Klik op de toegangscode om een nieu venster te openen om het onderwerp te bekijken, deze verborgen link kan gedeeld worden.", + "Add new Subject": "Voeg onderwerp toe", + "Unable to save Subject": "Kan onderwerp niet opslaan", + "Unable to delete Subject": "Kan onderwerp niet verwijderen", + "Database contains %1 subjects": "Database bevat %1 onderwerpen", + "Edit Subject": "Pas onderwerp aan", + "person, device, etc": "persoon, apparaat etc", + "role1, role2": "rol1, rol2", + "Edit this subject": "pas dit onderwerp aan", + "Delete this subject": "verwijder dit onderwerp", + "Roles": "Rollen", + "Access Token": "toegangscode", + "hour ago": "uur geleden", + "hours ago": "uren geleden", + "Silence for %1 minutes": "Sluimer %1 minuten", + "Check BG": "Controleer BG", + "BASAL": "Basaal", + "Current basal": "Huidige basaal", + "Sensitivity": "Gevoeligheid", + "Current Carb Ratio": "Huidige KH ratio", + "Basal timezone": "Basaal tijdzone", + "Active profile": "Actief profiel", + "Active temp basal": "Actieve tijdelijke basaal", + "Active temp basal start": "Actieve tijdelijke basaal start", + "Active temp basal duration": "Actieve tijdelijk basaal duur", + "Active temp basal remaining": "Actieve tijdelijke basaal resteert", + "Basal profile value": "Basaal profiel waarde", + "Active combo bolus": "Actieve combo bolus", + "Active combo bolus start": "Actieve combo bolus start", + "Active combo bolus duration": "Actieve combo bolus duur", + "Active combo bolus remaining": "Actieve combo bolus resteert", + "BG Delta": "BG Delta", + "Elapsed Time": "Verstreken tijd", + "Absolute Delta": "Abslute delta", + "Interpolated": "Geinterpoleerd", + "BWP": "BWP", + "Urgent": "Urgent", + "Warning": "Waarschuwing", + "Info": "Info", + "Lowest": "Laagste", + "Snoozing high alarm since there is enough IOB": "Snooze hoog alarm, er is genoeg IOB", + "Check BG, time to bolus?": "Controleer BG, tijd om te bolussen?", + "Notice": "Notitie", + "required info missing": "vereiste gegevens ontbreken", + "Insulin on Board": "IOB - Inuline on board", + "Current target": "huidig doel", + "Expected effect": "verwacht effect", + "Expected outcome": "Veracht resultaat", + "Carb Equivalent": "Koolhydraat equivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Insulineoverschot van %1U om laag doel te behalen (excl. koolhydraten)", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Insulineoverschot van %1U om laag doel te behalen, ZORG VOOR VOLDOENDE KOOLHYDRATEN VOOR DE ACTIEVE INSULINE", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U reductie vereist in actieve insuline om laag doel te bereiken, teveel basaal?", + "basal adjustment out of range, give carbs?": "basaal aanpassing buiten bereik, geef KH?", + "basal adjustment out of range, give bolus?": "basaal aanpassing buiten bereik, bolus?", + "above high": "boven hoog", + "below low": "onder laag", + "Projected BG %1 target": "Verwachte BG %1 doel", + "aiming at": "doel is", + "Bolus %1 units": "Bolus %1 eenheden", + "or adjust basal": "of pas basaal aan", + "Check BG using glucometer before correcting!": "Controleer BG met bloeddruppel voor correctie!", + "Basal reduction to account %1 units:": "Basaal verlaagd voor %1 eenheden", + "30m temp basal": "30 minuten tijdelijke basaal", + "1h temp basal": "1 uur tijdelijke basaal", + "Cannula change overdue!": "Cannule te oud!", + "Time to change cannula": "Verwissel canule", + "Change cannula soon": "Verwissel canule binnenkort", + "Cannula age %1 hours": "Canule leeftijd %1 uren", + "Inserted": "Ingezet", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Laatse KH", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Verwissel insulinereservoir nu!", + "Time to change insulin reservoir": "Verwissel insuline reservoir", + "Change insulin reservoir soon": "Verwissel insuline reservoir binnenkort", + "Insulin reservoir age %1 hours": "Insuline reservoir leeftijd %1 uren", + "Changed": "veranderd", + "IOB": "IOB", + "Careportal IOB": "Careportal tabblad", + "Last Bolus": "Laatste bolus", + "Basal IOB": "Basaal IOB", + "Source": "bron", + "Stale data, check rig?": "Geen data, controleer uploader", + "Last received:": "laatste ontvangen", + "%1m ago": "%1m geleden", + "%1h ago": "%1u geleden", + "%1d ago": "%1d geleden", + "RETRO": "RETRO", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Sensor vevang/hertstart tijd gepasseerd", + "Time to change/restart sensor": "Sensor vervangen of herstarten", + "Change/restart sensor soon": "Herstart of vervang sensor binnenkort", + "Sensor age %1 days %2 hours": "Sensor leeftijd %1 dag(en) en %2 uur", + "Sensor Insert": "Sensor ingebracht", + "Sensor Start": "Sensor start", + "days": "dagen", + "Insulin distribution": "Insuline verdeling", + "To see this report, press SHOW while in this view": "Om dit rapport te zien, druk op \"Laat zien\"", + "AR2 Forecast": "AR2 Voorspelling", + "OpenAPS Forecasts": "OpenAPS Voorspelling", + "Temporary Target": "Tijdelijk doel", + "Temporary Target Cancel": "Annuleer tijdelijk doel", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profielen", + "Time in fluctuation": "Tijd met fluctuaties", + "Time in rapid fluctuation": "Tijd met grote fluctuaties", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Dit is enkel een grove schatting die onjuist kan zijn welke geen bloedtest vervangt. De gebruikte formule is afkomstig van:", + "Filter by hours": "Filter op uren", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Tijd met fluctuaties of grote fluctuaties in % van de geevalueerde periode, waarbij de bloed glucose relatief snel wijzigde.Lagere waarden zijn beter.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Gemiddelde veranderingen per dag is een som van alle waardes die uitschieten over de bekeken periode, gedeeld door het aantal dagen in deze periode. Lager is beter.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Gemiddelde veranderingen per uur is een som van alle waardes die uitschieten over de bekeken periode, gedeeld door het aantal uur in deze periode. Lager is beter.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Buiten het bereik van RMS wordt berekend door de afstand buiten het bereik van alle glucosewaardes voor de vastgestelde periode te nemen, deze te kwadrateren, sommeren, delen door het aantal metingen en daar de wortel van te nemen. Deze metriek is vergelijkbaar met het in-range percentage, maar metingen die verder buiten bereik zijn tellen zwaarder. Lagere waarden zijn beter.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">is hier te vinden.", + "Mean Total Daily Change": "Gemiddelde veranderingen per dag", + "Mean Hourly Change": "Gemiddelde veranderingen per uur", + "FortyFiveDown": "licht dalend", + "FortyFiveUp": "licht stijgend", + "Flat": "vlak", + "SingleUp": "stijgend", + "SingleDown": "dalend", + "DoubleDown": "snel dalend", + "DoubleUp": "snel stijgend", + "virtAsstUnknown": "Die waarde is op dit moment onbekend. Zie je Nightscout site voor meer informatie.", + "virtAsstTitleAR2Forecast": "AR2 Voorspelling", + "virtAsstTitleCurrentBasal": "Huidig basaal", + "virtAsstTitleCurrentCOB": "Huidig COB", + "virtAsstTitleCurrentIOB": "Huidig IOB", + "virtAsstTitleLaunch": "Welkom bij Nightscout", + "virtAsstTitleLoopForecast": "Loop Voorbeschouwing", + "virtAsstTitleLastLoop": "Laatste Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Voorspelling", + "virtAsstTitlePumpReservoir": "Resterende insuline", + "virtAsstTitlePumpBattery": "Pomp batterij", + "virtAsstTitleRawBG": "Huidige Ruw BG", + "virtAsstTitleUploaderBattery": "Uploader batterij", + "virtAsstTitleCurrentBG": "Huidig BG", + "virtAsstTitleFullStatus": "Volledige status", + "virtAsstTitleCGMMode": "CGM modus", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Sessie Leeftijd", + "virtAsstTitleCGMTxStatus": "CGM zender status", + "virtAsstTitleCGMTxAge": "CGM zender leeftijd", + "virtAsstTitleCGMNoise": "CGM ruis", + "virtAsstTitleDelta": "Bloedglucose delta", + "virtAsstStatus": "%1 en %2 sinds %3.", + "virtAsstBasal": "%1 huidig basaal is %2 eenheden per uur", + "virtAsstBasalTemp": "%1 tijdelijk basaal van %2 eenheden per uur zal eindigen %3", + "virtAsstIob": "en je hebt %1 insuline aan boord.", + "virtAsstIobIntent": "Je hebt %1 insuline aan boord", + "virtAsstIobUnits": "%1 eenheden van", + "virtAsstLaunch": "Wat wil je bekijken op Nightscout?", + "virtAsstPreamble": "Jouw", + "virtAsstPreamble3person": "%1 heeft een ", + "virtAsstNoInsulin": "geen", + "virtAsstUploadBattery": "De batterij van je mobiel is bij %l", + "virtAsstReservoir": "Je hebt nog %l eenheden in je reservoir", + "virtAsstPumpBattery": "Je pomp batterij is bij %1 %2", + "virtAsstUploaderBattery": "Je uploader batterij is bij %1", + "virtAsstLastLoop": "De meest recente goede loop was %1", + "virtAsstLoopNotAvailable": "De Loop plugin is niet geactiveerd", + "virtAsstLoopForecastAround": "Volgens de Loop voorspelling is je waarde around %1 over de volgnede %2", + "virtAsstLoopForecastBetween": "Volgens de Loop voorspelling is je waarde between %1 and %2 over de volgnede %3", + "virtAsstAR2ForecastAround": "Volgens de AR2 voorspelling wordt verwacht dat u ongeveer op %1 zal zitten in de volgende %2", + "virtAsstAR2ForecastBetween": "Volgens de AR2 voorspelling wordt verwacht dat u ongeveer tussen %1 en %2 zal zitten in de volgende %3", + "virtAsstForecastUnavailable": "Niet mogelijk om een voorspelling te doen met de data die beschikbaar is", + "virtAsstRawBG": "Je raw bloedwaarde is %1", + "virtAsstOpenAPSForecast": "OpenAPS uiteindelijke bloedglucose van %1", + "virtAsstCob3person": "%1 heeft %2 koolhydraten aan boord", + "virtAsstCob": "Je hebt %1 koolhydraten aan boord", + "virtAsstCGMMode": "Uw CGM modus was %1 sinds %2.", + "virtAsstCGMStatus": "Uw CGM status was %1 sinds %2.", + "virtAsstCGMSessAge": "Uw CGM sessie is actief gedurende %1 dagen en %2 uur.", + "virtAsstCGMSessNotStarted": "Er is op dit moment geen actieve CGM-sessie.", + "virtAsstCGMTxStatus": "Uw CGM zender status was %1 sinds %2.", + "virtAsstCGMTxAge": "Uw CGM-zender is %1 dagen oud.", + "virtAsstCGMNoise": "Uw CGM-ruis was %1 sinds %2.", + "virtAsstCGMBattOne": "Uw CGM batterij was %1 volt sinds %2.", + "virtAsstCGMBattTwo": "Uw CGM batterijniveau was %1 volt en %2 volt sinds %3.", + "virtAsstDelta": "Je delta was %1 tussen %2 en %3.", + "virtAsstDeltaEstimated": "Je geschatte delta was %1 tussen %2 en %3.", + "virtAsstUnknownIntentTitle": "Onbekende Intentie", + "virtAsstUnknownIntentText": "Het spijt me, ik weet niet wat je bedoelt.", + "Fat [g]": "Vet [g]", + "Protein [g]": "Proteine [g]", + "Energy [kJ]": "Energie [kJ]", + "Clock Views:": "Klokweergave:", + "Clock": "Klok", + "Color": "Kleur", + "Simple": "Simpel", + "TDD average": "Gemiddelde dagelijkse insuline (TDD)", + "Bolus average": "Gemiddelde bolus", + "Basal average": "Gemiddelde basaal", + "Base basal average:": "Basis basaal gemiddeld:", + "Carbs average": "Gemiddelde koolhydraten per dag", + "Eating Soon": "Pre-maaltijd modus", + "Last entry {0} minutes ago": "Laatste waarde {0} minuten geleden", + "change": "wijziging", + "Speech": "Spraak", + "Target Top": "Hoog tijdelijk doel", + "Target Bottom": "Laag tijdelijk doel", + "Canceled": "Geannuleerd", + "Meter BG": "Bloedglucosemeter waarde", + "predicted": "verwachting", + "future": "toekomstig", + "ago": "geleden", + "Last data received": "Laatste gegevens ontvangen", + "Clock View": "Klokweergave", + "Protein": "Eiwit", + "Fat": "Vet", + "Protein average": "eiwitgemiddelde", + "Fat average": "Vetgemiddelde", + "Total carbs": "Totaal koolhydraten", + "Total protein": "Totaal eiwitten", + "Total fat": "Totaal vetten", + "Database Size": "Grootte database", + "Database Size near its limits!": "Database grootte nadert limiet!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database grootte is %1 MiB van de %2 MiB. Maak een backup en verwijder oude data", + "Database file size": "Database bestandsgrootte", + "%1 MiB of %2 MiB (%3%)": "%1 MiB van de %2 MiB (%3%)", + "Data size": "Datagrootte", + "virtAsstDatabaseSize": "%1 MiB dat is %2% van de beschikbaare database ruimte", + "virtAsstTitleDatabaseSize": "Database bestandsgrootte", + "Carbs/Food/Time": "Koolhydraten/Voedsel/Tijd", + "You have administration messages": "U heeft beheerberichten", + "Admin messages in queue": "Administratie berichten in wachtrij", + "Queue empty": "Wachtrij leeg", + "There are no admin messages in queue": "Er staan geen administratie berichten in de wachtrij", + "Please sign in using the API_SECRET to see your administration messages": "Log in met behulp van uw API_SECRET om de administratie berichten te zien", + "Reads enabled in default permissions": "Lezen ingeschakeld in standaard toestemmingen", + "Data reads enabled": "Gegevens lezen ingeschakeld", + "Data writes enabled": "Gegevens schrijven ingeschakeld", + "Data writes not enabled": "Gegevens schrijven uitgeschakeld", + "Color prediction lines": "Gekleurde voorspellingslijnen", + "Release Notes": "Versie opmerkingen", + "Check for Updates": "Zoek naar updates", + "Open Source": "Open source", + "Nightscout Info": "Nightscout Informatie", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Het primaire doel van Loopalyzer is om te visualiseren hoe het Loop closed loop systeem presteert. Het kan ook werken met andere opstellingen, zowel in de closed en open loop als in de niet-loop. Echter afhankelijk van welke uploader u gebruikt, hoe vaak het in staat is om uw gegevens vast te leggen en te uploaden, en hoe het in staat is om ontbrekende gegevens op te vullen, kunnen sommige grafieken gaten hebben of zelfs volledig leeg zijn. Zorg er altijd voor dat de grafieken er redelijk uitzien. Het beste is om één dag tegelijk te bekijken en scroll door een aantal dagen eerst om te zien.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer bevat een functie voor tijdverschuiving. Als je bijvoorbeeld het ontbijt eet om 07:00 en om 08:00 op de dag erna, zal je gemiddelde bloedglucosecurve van deze twee dagen er zeer waarschijnlijk vlak uitzien en niet de werkelijke reactie laten zien na het ontbijt. Tijdverschuiving berekent de gemiddelde tijd dat deze maaltijden werden gegeten en verplaatst vervolgens alle data (koolhydraten, insuline, basaal, etc) zodat het lijkt of beide maaltijden op dezelfde tijd zijn gegeten.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In dit voorbeeld worden alle gegevens van de eerste dag 30 minuten naar voren geschoven en alle gegevens vanaf de tweede dag 30 minuten naar achter geschoven, dus het lijkt alsof je om 7:30 beide dagen hebt ontbeten. Hiermee kunt u uw gemiddelde bloedglucosereactie van een maaltijd zien.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Tijdverschuiving benadrukt de periode na de gemiddelde begintijd in het grijs, voor de duur van de DIA (duur van insuline actie). Omdat alle gegevens van de hele dag verschoven zijn kunnen de gegevens buiten het grijze gebied mogelijk niet accuraat zijn.", + "Note that time shift is available only when viewing multiple days.": "Merk op dat tijdverschuiving alleen beschikbaar is bij het bekijken van meerdere dagen.", + "Please select a maximum of two weeks duration and click Show again.": "Selecteer maximaal twee weken en klik opnieuw op Laat Zien.", + "Show profiles table": "Profieltabel weergeven", + "Show predictions": "Toon voorspellingen", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift op maaltijden groter dan %1 g koolhydraten gegeten tussen %2 en %3", + "Previous": "Vorige", + "Previous day": "Vorige dag", + "Next day": "Volgende dag", + "Next": "Volgende", + "Temp basal delta": "Tijdelijk basaal delta", + "Authorized by token": "Geautoriseerd met token", + "Auth role": "Auth rol", + "view without token": "bekijken zonder token", + "Remove stored token": "Verwijder opgeslagen token", + "Weekly Distribution": "Wekelijkse spreiding", + "Failed authentication": "Mislukte authenticatie", + "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?": "Een apparaat met IP adres %1 heeft een poging gedaan om te authenticeren met Nightscout met verkeerde inloggegevens. Controleer of je een uploader hebt ingesteld met een verkeerd API_SECRET of token?", + "Default (with leading zero and U)": "Standaard (met 0 aan het begin en U)", + "Concise (with U, without leading zero)": "Beknopt (met U, zonder 0 aan het begin)", + "Minimal (without leading zero and U)": "Minimaal (zonder 0 aan het begin en U)", + "Small Bolus Display": "Beknopte Bolus Weergave", + "Large Bolus Display": "Grote Bolus Weergave", + "Bolus Display Threshold": "Bolus Weergave Drempel", + "%1 U and Over": "%1 U en meer", + "Event repeated %1 times.": "Gebeurtenis is %1 keer herhaald.", + "minutes": "minuten", + "Last recorded %1 %2 ago.": "Voor het laatst opgeslagen %1 %2 geleden.", + "Security issue": "Beveiligingsprobleem", + "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.": "Zwak API_SECRET gevonden. Gebruik een mix van kleine en hoofdletters, cijfers en niet-alfanumerieke tekens !#%&/ om het risico op ongeoorloofde toegang te beperken. De minimale lengte van de API_SECRET is 12 tekens.", + "less than 1": "minder dan 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB wachtwoord en API_SECRET zijn hetzelfde. Dit is geen goed idee. Verander beide en hergebruik geen wachtwoorden in het hele systeem." +} diff --git a/translations/pl_PL.json b/translations/pl_PL.json new file mode 100644 index 00000000000..2991995b81c --- /dev/null +++ b/translations/pl_PL.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Słucham na porcie", + "Mo": "Po", + "Tu": "Wt", + "We": "Śr", + "Th": "Cz", + "Fr": "Pt", + "Sa": "So", + "Su": "Nd", + "Monday": "Poniedziałek", + "Tuesday": "Wtorek", + "Wednesday": "Środa", + "Thursday": "Czwartek", + "Friday": "Piątek", + "Saturday": "Sobota", + "Sunday": "Niedziela", + "Category": "Kategoria", + "Subcategory": "Podkategoria", + "Name": "Nazwa", + "Today": "Dziś", + "Last 2 days": "Ostatnie 2 dni", + "Last 3 days": "Ostatnie 3 dni", + "Last week": "Ostatni tydzień", + "Last 2 weeks": "Ostatnie 2 tygodnie", + "Last month": "Ostatni miesiąc", + "Last 3 months": "Ostatnie 3 miesiące", + "From": "Od", + "To": "Do", + "Notes": "Uwagi", + "Food": "Jedzenie", + "Insulin": "Insulina", + "Carbs": "Węglowodany", + "Notes contain": "Zawierają uwagi", + "Target BG range bottom": "Docelowy zakres glikemii, dolny", + "top": "górny", + "Show": "Pokaż", + "Display": "Wyświetl", + "Loading": "Ładowanie", + "Loading profile": "Ładowanie profilu", + "Loading status": "Status załadowania", + "Loading food database": "Ładowanie bazy posiłków", + "not displayed": "Nie jest wyświetlany", + "Loading CGM data of": "Ładowanie danych CGM z", + "Loading treatments data of": "Ładowanie danych leczenia z", + "Processing data of": "Przetwarzanie danych z", + "Portion": "Porcja", + "Size": "Rozmiar", + "(none)": "(brak)", + "None": "brak", + "": "", + "Result is empty": "Brak wyniku", + "Day to day": "Dzień po dniu", + "Week to week": "Tydzień po tygodniu", + "Daily Stats": "Statystyki dzienne", + "Percentile Chart": "Wykres percentylowy", + "Distribution": "Dystrybucja", + "Hourly stats": "Statystyki godzinowe", + "netIOB stats": "Statystyki netIOP", + "temp basals must be rendered to display this report": "Tymczasowa dawka podstawowa jest wymagana aby wyświetlić ten raport", + "No data available": "Brak danych", + "Low": "Niski", + "In Range": "W zakresie", + "Period": "Okres", + "High": "Wysoki", + "Average": "Średnia", + "Low Quartile": "Dolny kwartyl", + "Upper Quartile": "Górny kwartyl", + "Quartile": "Kwartyl", + "Date": "Data", + "Normal": "Normalny", + "Median": "Mediana", + "Readings": "Odczyty", + "StDev": "Stand. odchylenie", + "Daily stats report": "Dzienne statystyki", + "Glucose Percentile report": "Tabela centylowa glikemii", + "Glucose distribution": "Rozkład glikemii", + "days total": "dni łącznie", + "Total per day": "Dobowa suma insuliny z bazy", + "Overall": "Ogółem", + "Range": "Zakres", + "% of Readings": "% Odczytów", + "# of Readings": "Ilość Odczytów", + "Mean": "Wartość średnia", + "Standard Deviation": "Standardowe odchylenie", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "HbA1c przewidywany", + "Weekly Success": "Wyniki tygodniowe", + "There is not sufficient data to run this report. Select more days.": "Nie ma wystarczających danych dla tego raportu. Wybierz więcej dni.", + "Using stored API secret hash": "Korzystając z zapisanego poufnego hasha API", + "No API secret hash stored yet. You need to enter API secret.": "Nie ma żadnego poufnego hasha API zapisanego. Należy wprowadzić poufny hash API.", + "Database loaded": "Baza danych załadowana", + "Error: Database failed to load": "Błąd, baza danych nie może być załadowana", + "Error": "Błąd", + "Create new record": "Tworzenie nowego wpisu", + "Save record": "Zapisz wpis", + "Portions": "Porcja", + "Unit": "Jednostka", + "GI": "IG", + "Edit record": "Edycja wpisu", + "Delete record": "Usuń wpis", + "Move to the top": "Przejdź do góry", + "Hidden": "Ukryte", + "Hide after use": "Ukryj po użyciu", + "Your API secret must be at least 12 characters long": "Twój poufny klucz API musi zawierać co majmniej 12 znaków", + "Bad API secret": "Błędny klucz API", + "API secret hash stored": "Poufne klucz API zapisane", + "Status": "Status", + "Not loaded": "Nie załadowany", + "Food Editor": "Edytor posiłków", + "Your database": "Twoja baza danych", + "Filter": "Filtr", + "Save": "Zapisz", + "Clear": "Wyczyść", + "Record": "Wpis", + "Quick picks": "Szybki wybór", + "Show hidden": "Pokaż ukryte", + "Your API secret or token": "Twój hash API lub token", + "Remember this device. (Do not enable this on public computers.)": "Zapamiętaj to urządzenie (Nie używaj tej opcji korzystając z publicznych komputerów.)", + "Treatments": "Leczenie", + "Time": "Czas", + "Event Type": "Typ zdarzenia", + "Blood Glucose": "Glikemia z krwi", + "Entered By": "Wprowadzono przez", + "Delete this treatment?": "Usunąć te leczenie?", + "Carbs Given": "Węglowodany spożyte", + "Insulin Given": "Podana insulina", + "Event Time": "Czas zdarzenia", + "Please verify that the data entered is correct": "Proszę sprawdzić, czy wprowadzone dane są prawidłowe", + "BG": "BG", + "Use BG correction in calculation": "Użyj BG w obliczeniach korekty", + "BG from CGM (autoupdated)": "Wartość BG z CGM (automatycznie)", + "BG from meter": "Wartość BG z glukometru", + "Manual BG": "Ręczne wprowadzenie BG", + "Quickpick": "Szybki wybór", + "or": "lub", + "Add from database": "Dodaj z bazy danych", + "Use carbs correction in calculation": "Użyj wartość węglowodanów w obliczeniach korekty", + "Use COB correction in calculation": "Użyj COB do obliczenia korekty", + "Use IOB in calculation": "Użyj IOB w obliczeniach", + "Other correction": "Inna korekta", + "Rounding": "Zaokrąglanie", + "Enter insulin correction in treatment": "Wprowadź wartość korekty w leczeniu", + "Insulin needed": "Wymagana insulina", + "Carbs needed": "Wymagane węglowodany", + "Carbs needed if Insulin total is negative value": "Wymagane węglowodany, jeśli łączna wartość Insuliny jest ujemna", + "Basal rate": "Dawka bazowa", + "60 minutes earlier": "60 minut wcześniej", + "45 minutes earlier": "45 minut wcześniej", + "30 minutes earlier": "30 minut wcześniej", + "20 minutes earlier": "20 minut wcześniej", + "15 minutes earlier": "15 minut wcześniej", + "Time in minutes": "Czas w minutach", + "15 minutes later": "15 minut później", + "20 minutes later": "20 minut później", + "30 minutes later": "30 minut później", + "45 minutes later": "45 minut później", + "60 minutes later": "60 minut później", + "Additional Notes, Comments": "Uwagi dodatkowe", + "RETRO MODE": "Tryb RETRO", + "Now": "Teraz", + "Other": "Inny", + "Submit Form": "Zatwierdź", + "Profile Editor": "Edytor profilu", + "Reports": "Raporty", + "Add food from your database": "Dodaj posiłek z twojej bazy danych", + "Reload database": "Odśwież bazę danych", + "Add": "Dodaj", + "Unauthorized": "Nieuwierzytelniono", + "Entering record failed": "Wprowadzenie wpisu nie powiodło się", + "Device authenticated": "Urządzenie uwierzytelnione", + "Device not authenticated": "Urządzenie nieuwierzytelnione", + "Authentication status": "Status uwierzytelnienia", + "Authenticate": "Uwierzytelnienie", + "Remove": "Usuń", + "Your device is not authenticated yet": "Twoje urządzenie nie jest jeszcze uwierzytelnione", + "Sensor": "Sensor", + "Finger": "Glukometr", + "Manual": "Ręcznie", + "Scale": "Skala", + "Linear": "Liniowa", + "Logarithmic": "Logarytmiczna", + "Logarithmic (Dynamic)": "Logarytmiczna (Dynamiczna)", + "Insulin-on-Board": "Aktywna insulina (IOB)", + "Carbs-on-Board": "Aktywne wglowodany (COB)", + "Bolus Wizard Preview": "Kalkulator Bolusa (BWP)", + "Value Loaded": "Wartości wczytane", + "Cannula Age": "Czas wkłucia (CAGE)", + "Basal Profile": "Profil dawki bazowej", + "Silence for 30 minutes": "Wycisz na 30 minut", + "Silence for 60 minutes": "Wycisz na 60 minut", + "Silence for 90 minutes": "Wycisz na 90 minut", + "Silence for 120 minutes": "Wycisz na 120 minut", + "Settings": "Ustawienia", + "Units": "Jednostki", + "Date format": "Format daty", + "12 hours": "12 godzinny", + "24 hours": "24 godzinny", + "Log a Treatment": "Wprowadź leczenie", + "BG Check": "Pomiar glikemii", + "Meal Bolus": "Bolus do posiłku", + "Snack Bolus": "Bolus przekąskowy", + "Correction Bolus": "Bolus korekcyjny", + "Carb Correction": "Węglowodany korekcyjne", + "Note": "Uwagi", + "Question": "Pytanie", + "Exercise": "Wysiłek", + "Pump Site Change": "Zmiana miejsca wkłucia pompy", + "CGM Sensor Start": "Start nowego sensora", + "CGM Sensor Stop": "Stop Sensora CGM", + "CGM Sensor Insert": "Zmiana sensora", + "Sensor Code": "Kod sensora", + "Transmitter ID": "ID nadajnika", + "Dexcom Sensor Start": "Start sensora DEXCOM", + "Dexcom Sensor Change": "Zmiana sensora DEXCOM", + "Insulin Cartridge Change": "Zmiana zbiornika z insuliną", + "D.A.D. Alert": "Psi Alarm cukrzycowy", + "Glucose Reading": "Odczyt glikemii", + "Measurement Method": "Sposób pomiaru", + "Meter": "Glukometr", + "Amount in grams": "Wartość w gramach", + "Amount in units": "Wartość w jednostkach", + "View all treatments": "Pokaż całość leczenia", + "Enable Alarms": "Włącz alarmy", + "Pump Battery Change": "Zmiana baterii w pompie", + "Pump Battery Age": "Wiek baterii pompy", + "Pump Battery Low Alarm": "Alarm! Niski poziom baterii w pompie", + "Pump Battery change overdue!": "Bateria pompy musi być wymieniona!", + "When enabled an alarm may sound.": "Sygnalizacja dzwiękowa przy włączonym alarmie", + "Urgent High Alarm": "Uwaga: Alarm hiperglikemii", + "High Alarm": "Alarm hiperglikemii", + "Low Alarm": "Alarm hipoglikemii", + "Urgent Low Alarm": "Uwaga: Alarm hipoglikemii", + "Stale Data: Warn": "Ostrzeżenie: brak odczytów", + "Stale Data: Urgent": "Uwaga: brak odczytów", + "mins": "min", + "Night Mode": "Tryb nocny", + "When enabled the page will be dimmed from 10pm - 6am.": "Po włączeniu strona będzie przyciemniona od 22 wieczorem do 6 nad ranem.", + "Enable": "Włącz", + "Show Raw BG Data": "Wyświetl surowe dane BG", + "Never": "Nigdy", + "Always": "Zawsze", + "When there is noise": "Gdy sygnał jest zakłócony", + "When enabled small white dots will be displayed for raw BG data": "Gdy funkcja jest włączona, małe białe kropki pojawią się na surowych danych BG", + "Custom Title": "Własny tytuł strony", + "Theme": "Wygląd", + "Default": "Domyślny", + "Colors": "Kolorowy", + "Colorblind-friendly colors": "Kolory dla niedowidzących", + "Reset, and use defaults": "Reset i powrót do domyślnych ustawień", + "Calibrations": "Kalibracja", + "Alarm Test / Smartphone Enable": "Test alarmu / Smartphone aktywny", + "Bolus Wizard": "Kalkulator bolusa", + "in the future": "w przyszłości", + "time ago": "czas temu", + "hr ago": "godzina temu", + "hrs ago": "godzin temu", + "min ago": "minuta temu", + "mins ago": "minut temu", + "day ago": "dzień temu", + "days ago": "dni temu", + "long ago": "dawno", + "Clean": "Czysty", + "Light": "Niski", + "Medium": "Średni", + "Heavy": "Wysoki", + "Treatment type": "Rodzaj leczenia", + "Raw BG": "Surowe BG", + "Device": "Urządzenie", + "Noise": "Szum", + "Calibration": "Kalibracja", + "Show Plugins": "Pokaż rozszerzenia", + "About": "O aplikacji", + "Value in": "Wartości w", + "Carb Time": "Czas posiłku", + "Language": "Język", + "Add new": "Dodaj nowy", + "g": "g", + "ml": "ml", + "pcs": "cz.", + "Drag&drop food here": "Tutaj przesuń/upuść jedzenie", + "Care Portal": "Portal Opieki", + "Medium/Unknown": "Średni/nieznany", + "IN THE FUTURE": "W PRZYSZŁOŚCI", + "Order": "Kolejność", + "oldest on top": "Najstarszy na górze", + "newest on top": "Najnowszy na górze", + "All sensor events": "Wszystkie zdarzenia sensora", + "Remove future items from mongo database": "Usuń przyszłe/błędne wpisy z bazy mongo", + "Find and remove treatments in the future": "Znajdź i usuń przyszłe/błędne zabiegi", + "This task find and remove treatments in the future.": "To narzędzie znajduje i usuwa przyszłe/błędne zabiegi", + "Remove treatments in the future": "Usuń zabiegi w przyszłości", + "Find and remove entries in the future": "Znajdź i usuń wpisy z przyszłości", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "To narzędzie odnajduje i usuwa dane CGM utworzone przez uploader w przyszłości - ze złą datą/czasem.", + "Remove entries in the future": "Usuń wpisy w przyszłości", + "Loading database ...": "Wczytuje baze danych", + "Database contains %1 future records": "Baza danych zawiera %1 przyszłych rekordów", + "Remove %1 selected records?": "Usunąć %1 wybranych rekordów?", + "Error loading database": "Błąd wczytywania bazy danych", + "Record %1 removed ...": "%1 rekordów usunięto ...", + "Error removing record %1": "Błąd przy usuwaniu rekordu %1", + "Deleting records ...": "Usuwanie rekordów ...", + "%1 records deleted": "%1 rekordów zostało usuniętych", + "Clean Mongo status database": "Oczyść status bazy danych Mongo", + "Delete all documents from devicestatus collection": "Usuń wszystkie dokumenty z kolekcji devicestatus", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "To narzędzie usuwa wszystkie dokumenty z kolekcji devicestatus. Przydatne, gdy status baterii uploadera nie jest aktualizowany.", + "Delete all documents": "Usuń wszystkie dokumenty", + "Delete all documents from devicestatus collection?": "Czy na pewno usunąć wszystkie dokumenty z kolekcji devicestatus?", + "Database contains %1 records": "Baza danych zawiera %1 rekordów", + "All records removed ...": "Wszystkie rekordy usunięto", + "Delete all documents from devicestatus collection older than 30 days": "Usuń wszystkie dokumenty z kolekcji devicestatus starsze niż 30 dni", + "Number of Days to Keep:": "Ilość dni do zachowania:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "To narzędzie usuwa wszystkie dokumenty z kolekcji devicestatus starsze niż 30 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.", + "Delete old documents from devicestatus collection?": "Czy na pewno chcesz usunąć stare dokumenty z kolekcji devicestatus?", + "Clean Mongo entries (glucose entries) database": "Wyczyść bazę wpisów (wpisy glukozy) Mongo", + "Delete all documents from entries collection older than 180 days": "Usuń wszystkie dokumenty z kolekcji wpisów starsze niż 180 dni", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "To narzędzie usuwa wszystkie dokumenty z kolekcji wpisów starsze niż 180 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.", + "Delete old documents": "Usuń stare dokumenty", + "Delete old documents from entries collection?": "Czy na pewno chcesz usunąć stare dokumenty z kolekcji wpisów?", + "%1 is not a valid number": "%1 nie jest poprawną liczbą", + "%1 is not a valid number - must be more than 2": "%1 nie jest poprawną liczbą - musi być większe od 2", + "Clean Mongo treatments database": "Wyczyść bazę leczenia Mongo", + "Delete all documents from treatments collection older than 180 days": "Usuń wszystkie dokumenty z kolekcji leczenia starsze niż 180 dni", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "To narzędzie usuwa wszystkie dokumenty z kolekcji leczenia starsze niż 180 dni. Przydatne, gdy status baterii uploadera nie jest aktualizowany.", + "Delete old documents from treatments collection?": "Czy na pewno chcesz usunąć stare dokumenty z kolekcji leczenia?", + "Admin Tools": "Narzędzia administratora", + "Nightscout reporting": "Nightscout - raporty", + "Cancel": "Anuluj", + "Edit treatment": "Edytuj zabieg", + "Duration": "Czas trwania", + "Duration in minutes": "Czas trwania w minutach", + "Temp Basal": "Tymczasowa dawka podstawowa", + "Temp Basal Start": "Początek tymczasowej dawki podstawowej", + "Temp Basal End": "Koniec tymczasowej dawki podstawowej", + "Percent": "Procent", + "Basal change in %": "Zmiana dawki podstawowej w %", + "Basal value": "Dawka podstawowa", + "Absolute basal value": "Bezwględna wartość dawki podstawowej", + "Announcement": "Powiadomienie", + "Loading temp basal data": "Wczytuje dane tymczasowej dawki podstawowej", + "Save current record before changing to new?": "Zapisać bieżący rekord przed zamianą na nowy?", + "Profile Switch": "Przełączenie profilu", + "Profile": "Profil", + "General profile settings": "Ogólne ustawienia profilu", + "Title": "Nazwa", + "Database records": "Rekordy bazy danych", + "Add new record": "Dodaj nowy rekord", + "Remove this record": "Usuń ten rekord", + "Clone this record to new": "Powiel ten rekord na nowy", + "Record valid from": "Rekord ważny od", + "Stored profiles": "Zachowane profile", + "Timezone": "Strefa czasowa", + "Duration of Insulin Activity (DIA)": "Czas trwania aktywnej insuliny (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Odzwierciedla czas działania insuliny. Może różnić się w zależności od chorego i rodzaju insuliny. Zwykle są to 3-4 godziny dla insuliny podawanej pompą u większości chorych. Inna nazwa to czas trwania insuliny.", + "Insulin to carb ratio (I:C)": "Współczynnik insulina/węglowodany (I:C)", + "Hours:": "Godziny:", + "hours": "godziny", + "g/hour": "g/godzine", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g węglowodanów na 1j insuliny. Współczynnik ilości gram weglowodanów równoważonych przez 1j insuliny.", + "Insulin Sensitivity Factor (ISF)": "Współczynnik wrażliwosci na insulinę (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dl lub mmol/L na 1j insuliny. Współczynnik obniżenia poziomu glukozy przez 1j insuliny", + "Carbs activity / absorption rate": "Aktywność węglowodanów / współczynnik absorpcji", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "g na jednostkę czasu. Odzwierciedla zmianę COB na jednostkę czasu oraz ilość węglowodanów mających przynieść efekt w czasie. Krzywe absorpcji / aktywnosci węglowodanów są mniej poznane niż aktywności insuliny ale mogą być oszacowane przez ocenę opóźnienia wchłaniania przy stałym współczynniku absorpcji (g/h).", + "Basal rates [unit/hour]": "Dawka podstawowa [j/h]", + "Target BG range [mg/dL,mmol/L]": "Docelowy przedział glikemii [mg/dl, mmol/L])", + "Start of record validity": "Początek ważnych rekordów", + "Icicle": "Odwrotność", + "Render Basal": "Zmiana dawki bazowej", + "Profile used": "Profil wykorzystywany", + "Calculation is in target range.": "Obliczenie mieści się w zakresie docelowym", + "Loading profile records ...": "Wczytywanie rekordów profilu", + "Values loaded.": "Wartości wczytane.", + "Default values used.": "Używane domyślne wartości.", + "Error. Default values used.": "Błąd. Używane domyślne wartości.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Zakres czasu w docelowo niskim i wysokim przedziale nie są dopasowane. Przywrócono wartości domyślne", + "Valid from:": "Ważne od:", + "Save current record before switching to new?": "Nagrać bieżący rekord przed przełączeniem na nowy?", + "Add new interval before": "Dodaj nowy przedział przed", + "Delete interval": "Usuń przedział", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Bolus złożony", + "Difference": "Różnica", + "New time": "Nowy czas", + "Edit Mode": "Tryb edycji", + "When enabled icon to start edit mode is visible": "Po aktywacji, widoczne ikony, aby uruchomić tryb edycji", + "Operation": "Operacja", + "Move": "Przesuń", + "Delete": "Skasuj", + "Move insulin": "Przesuń insulinę", + "Move carbs": "Przesuń węglowodany", + "Remove insulin": "Usuń insulinę", + "Remove carbs": "Usuń węglowodany", + "Change treatment time to %1 ?": "Zmień czas zdarzenia na %1 ?", + "Change carbs time to %1 ?": "Zmień czas węglowodanów na %1 ?", + "Change insulin time to %1 ?": "Zmień czas insuliny na %1 ?", + "Remove treatment ?": "Usunąć wydarzenie?", + "Remove insulin from treatment ?": "Usunąć insulinę z wydarzenia?", + "Remove carbs from treatment ?": "Usunąć węglowodany z wydarzenia?", + "Rendering": "Rysuję", + "Loading OpenAPS data of": "Ładuję dane OpenAPS z", + "Loading profile switch data": "Ładuję dane zmienionego profilu", + "Redirecting you to the Profile Editor to create a new profile.": "Złe ustawienia profilu.\nDla podanego czasu nie zdefiniowano profilu.\nPrzekierowuję do edytora profili aby utworzyć nowy.", + "Pump": "Pompa", + "Sensor Age": "Wiek sensora", + "Insulin Age": "Wiek insuliny", + "Temporary target": "Tymczasowy cel", + "Reason": "Powód", + "Eating soon": "Zjedz wkrótce", + "Top": "Góra", + "Bottom": "Dół", + "Activity": "Aktywność", + "Targets": "Cele", + "Bolus insulin:": "Bolus insuliny", + "Base basal insulin:": "Bazowa dawka insuliny", + "Positive temp basal insulin:": "Zwiększona bazowa dawka insuliny", + "Negative temp basal insulin:": "Zmniejszona bazowa dawka insuliny", + "Total basal insulin:": "Całkowita ilość bazowej dawki insuliny", + "Total daily insulin:": "Całkowita dzienna ilość insuliny", + "Unable to save Role": "Nie można zapisać roli", + "Unable to delete Role": "Nie można usunąć roli", + "Database contains %1 roles": "Baza danych zawiera %1 ról", + "Edit Role": "Edycja roli", + "admin, school, family, etc": "administrator, szkoła, rodzina, itp", + "Permissions": "Uprawnienia", + "Are you sure you want to delete: ": "Jesteś pewien, że chcesz usunąć:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Każda rola będzie mieć 1 lub więcej uprawnień. Symbol * uprawnia do wszystkiego. Uprawnienia są hierarchiczne, używając : jako separatora.", + "Add new Role": "Dodaj nową rolę", + "Roles - Groups of People, Devices, etc": "Role - Grupy ludzi, urządzeń, itp", + "Edit this role": "Edytuj rolę", + "Admin authorized": "Administrator autoryzowany", + "Subjects - People, Devices, etc": "Obiekty - ludzie, urządzenia itp", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Każdy obiekt będzie miał unikalny token dostępu i jedną lub więcej ról. Kliknij token dostępu, aby otworzyć nowy widok z wybranym obiektem, ten tajny link może być następnie udostępniony", + "Add new Subject": "Dodaj obiekt", + "Unable to save Subject": "Nie można zapisać podmiotu", + "Unable to delete Subject": "Nie można usunąć obiektu", + "Database contains %1 subjects": "Baza danych zawiera %1 obiektów", + "Edit Subject": "Edytuj obiekt", + "person, device, etc": "osoba, urządzenie, itp", + "role1, role2": "rola1, rola2", + "Edit this subject": "Edytuj ten obiekt", + "Delete this subject": "Usuń ten obiekt", + "Roles": "Role", + "Access Token": "Token dostępu", + "hour ago": "Godzię temu", + "hours ago": "Godzin temu", + "Silence for %1 minutes": "Wycisz na %1 minut", + "Check BG": "Sprawdź glukozę z krwi", + "BASAL": "BAZA", + "Current basal": "Dawka podstawowa", + "Sensitivity": "Wrażliwość", + "Current Carb Ratio": "Obecny przelicznik węglowodanowy", + "Basal timezone": "Strefa czasowa dawki podstawowej", + "Active profile": "Profil aktywny", + "Active temp basal": "Aktywa tymczasowa dawka podstawowa", + "Active temp basal start": "Start aktywnej tymczasowej dawki podstawowej", + "Active temp basal duration": "Czas trwania aktywnej tymczasowej dawki podstawowej", + "Active temp basal remaining": "Pozostała aktywna tymczasowa dawka podstawowa", + "Basal profile value": "Wartość profilu podstawowego", + "Active combo bolus": "Aktywny bolus złożony", + "Active combo bolus start": "Start aktywnego bolusa złożonego", + "Active combo bolus duration": "Czas trwania aktywnego bolusa złożonego", + "Active combo bolus remaining": "Pozostały aktywny bolus złożony", + "BG Delta": "Zmiana glikemii", + "Elapsed Time": "Upłynął czas", + "Absolute Delta": "różnica absolutna", + "Interpolated": "Interpolowany", + "BWP": "Kalkulator bolusa", + "Urgent": "Pilny", + "Warning": "Ostrzeżenie", + "Info": "Informacja", + "Lowest": "Niski", + "Snoozing high alarm since there is enough IOB": "Wycisz alarm wysokiej glikemi, jest wystarczająco dużo aktywnej insuliny", + "Check BG, time to bolus?": "Sprawdź glikemię, czas na bolusa ?", + "Notice": "Uwaga", + "required info missing": "brak wymaganych informacji", + "Insulin on Board": "Aktywna insulina", + "Current target": "Aktualny cel", + "Expected effect": "Oczekiwany efekt", + "Expected outcome": "Oczekowany resultat", + "Carb Equivalent": "Odpowiednik w węglowodanach", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Nadmiar insuliny, %1J więcej niż potrzeba, aby osiągnąć cel dolnej granicy, nie biorąc pod uwagę węglowodanów", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Nadmiar insuliny: o %1J więcej niż potrzeba, aby osiągnąć cel dolnej granicy. UPEWNIJ SIĘ, ŻE AKTYWNA INSULINA JEST POKRYTA WĘGLOWODANAMI", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1J potrzebnej redukcji w aktywnej insulinie, aby osiągnąć niski cel dolnej granicy, Za duża dawka podstawowa ?", + "basal adjustment out of range, give carbs?": "dawka podstawowa poza zakresem, podać węglowodany?", + "basal adjustment out of range, give bolus?": "dawka podstawowa poza zakresem, podać insulinę?", + "above high": "powyżej wysokiego", + "below low": "poniżej niskiego", + "Projected BG %1 target": "Oczekiwany poziom glikemii %1", + "aiming at": "pożądany wynik", + "Bolus %1 units": "Bolus %1 jednostek", + "or adjust basal": "lub dostosuj dawkę bazową", + "Check BG using glucometer before correcting!": "Sprawdź glikemię z krwi przed podaniem korekty!", + "Basal reduction to account %1 units:": "Dawka bazowa zredukowana do 1% J", + "30m temp basal": "30 minut tymczasowej dawki bazowej", + "1h temp basal": "1 godzina tymczasowej dawki bazowej", + "Cannula change overdue!": "Przekroczono czas wymiany wkłucia!", + "Time to change cannula": "Czas do wymiany wkłucia", + "Change cannula soon": "Wkrótce wymiana wkłucia", + "Cannula age %1 hours": "Czas od wymiany wkłucia %1 godzin", + "Inserted": "Zamontowano", + "CAGE": "Wiek wkłucia", + "COB": "Aktywne węglowodany", + "Last Carbs": "Ostatnie węglowodany", + "IAGE": "Wiek insuliny", + "Insulin reservoir change overdue!": "Przekroczono czas wymiany zbiornika na insulinę!", + "Time to change insulin reservoir": "Czas do zmiany zbiornika na insulinę!", + "Change insulin reservoir soon": "Wkrótce wymiana zbiornika na insulinę!", + "Insulin reservoir age %1 hours": "Wiek zbiornika na insulinę %1 godzin", + "Changed": "Wymieniono", + "IOB": "Aktywna insulina", + "Careportal IOB": "Aktywna insulina z portalu", + "Last Bolus": "Ostatni bolus", + "Basal IOB": "Aktywna insulina z dawki bazowej", + "Source": "Źródło", + "Stale data, check rig?": "Dane są nieaktualne, sprawdź urządzenie transmisyjne.", + "Last received:": "Ostatnio odebrane:", + "%1m ago": "%1 minut temu", + "%1h ago": "%1 godzin temu", + "%1d ago": "%1 dni temu", + "RETRO": "DAWNE", + "SAGE": "Wiek sensora", + "Sensor change/restart overdue!": "Przekroczono czas wymiany/restartu sensora!", + "Time to change/restart sensor": "Czas do wymiany/restartu sensora", + "Change/restart sensor soon": "Wkrótce czas wymiany/restartu sensora", + "Sensor age %1 days %2 hours": "Wiek sensora: %1 dni %2 godzin", + "Sensor Insert": "Zamontuj sensor", + "Sensor Start": "Uruchomienie sensora", + "days": "dni", + "Insulin distribution": "podawanie insuliny", + "To see this report, press SHOW while in this view": "Aby wyświetlić ten raport, naciśnij przycisk POKAŻ w tym widoku", + "AR2 Forecast": "Prognoza AR2", + "OpenAPS Forecasts": "Prognoza OpenAPS", + "Temporary Target": "Cel tymczasowy", + "Temporary Target Cancel": "Cel tymczasowy - anuluj", + "OpenAPS Offline": "OpenAPS nieaktywny", + "Profiles": "Profile", + "Time in fluctuation": "Czas fluaktacji (odchyleń)", + "Time in rapid fluctuation": "Czas szybkich fluaktacji (odchyleń)", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "To tylko przybliżona ocena, która może być bardzo niedokładna i nie może zastąpić faktycznego poziomu cukru we krwi. Zastosowano formułę:", + "Filter by hours": "Filtruj po godzinach", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Czas fluktuacji i szybki czas fluktuacji mierzą % czasu w badanym okresie, w którym poziom glukozy we krwi zmieniał się szybko lub bardzo szybko. Preferowane są wolniejsze zmiany", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Średnia całkowita dziennych zmian jest sumą wszystkich zmian glikemii w badanym okresie, podzielona przez liczbę dni. Mniejsze wartości są lepsze.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Sednia całkowita godzinnych zmian jest sumą wszystkich zmian glikemii w badanym okresie, podzielonym przez liczbę godzin. Mniejsze są lepsze", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Średnią kwadratową (RMS) wartości poza zakresem oblicza się poprzez podniesienie do kwadratu wartości poza zakresem dla wszystkich odczytów glukozy w badanym okresie, zsumowanie ich, podzielenie przez liczbę odczytów i wyliczenie pierwiastka kwadratowego. Wskaźnik ten jest podobny do procentu w przebywania w zakresie, ale mają na niego wpływ w dużo większym stopniu odczyty daleko poza zakresem. Niższe wartości są lepsze.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">tutaj.", + "Mean Total Daily Change": "Średnia całkowita dziennych zmian", + "Mean Hourly Change": "Średnia całkowita godzinnych zmian", + "FortyFiveDown": "niewielki spadek", + "FortyFiveUp": "niewielki wzrost", + "Flat": "stabilny", + "SingleUp": "wzrost", + "SingleDown": "spada", + "DoubleDown": "szybko spada", + "DoubleUp": "szybko rośnie", + "virtAsstUnknown": "Ta wartość jest obecnie nieznana. Aby uzyskać więcej informacji, zajżyj na swoją stronę Nightscout.", + "virtAsstTitleAR2Forecast": "Prognoza AR2", + "virtAsstTitleCurrentBasal": "Bieżąca baza", + "virtAsstTitleCurrentCOB": "Bieżąca ilość węglowodanów na pokładzie", + "virtAsstTitleCurrentIOB": "Bieżąca ilość insuliny na pokładzie", + "virtAsstTitleLaunch": "Witamy w Nightscout", + "virtAsstTitleLoopForecast": "Prognoza pętli", + "virtAsstTitleLastLoop": "Ostatnia pętla", + "virtAsstTitleOpenAPSForecast": "Prognoza OpenAPS", + "virtAsstTitlePumpReservoir": "Pozostała insulina", + "virtAsstTitlePumpBattery": "Bateria Pompy", + "virtAsstTitleRawBG": "Bieżąca surowa glikemia", + "virtAsstTitleUploaderBattery": "Bateria uploadera", + "virtAsstTitleCurrentBG": "Bieżący poziom cukru", + "virtAsstTitleFullStatus": "Pełny status", + "virtAsstTitleCGMMode": "Tryb CGM", + "virtAsstTitleCGMStatus": "Status CGM", + "virtAsstTitleCGMSessionAge": "Wiek sesji CGM", + "virtAsstTitleCGMTxStatus": "Status nadajnika CGM", + "virtAsstTitleCGMTxAge": "Wiek nadajnika CGM", + "virtAsstTitleCGMNoise": "Szum CGM", + "virtAsstTitleDelta": "Poziom glukozy we krwi", + "virtAsstStatus": "%1 i %2 rozpoczęte od %3.", + "virtAsstBasal": "%1 obecna dawka bazalna %2 J na godzinę", + "virtAsstBasalTemp": "%1 tymczasowa dawka bazalna %2 J na godzinę zakoczy się o %3", + "virtAsstIob": "i masz %1 aktywnej insuliny.", + "virtAsstIobIntent": "Masz %1 aktywnej insuliny", + "virtAsstIobUnits": "%1 jednostek", + "virtAsstLaunch": "Co chcesz sprawdzić na Nightscout?", + "virtAsstPreamble": "twój", + "virtAsstPreamble3person": "%1 ma ", + "virtAsstNoInsulin": "nie", + "virtAsstUploadBattery": "Twoja bateria ma %1", + "virtAsstReservoir": "W zbiorniku pozostało %1 jednostek", + "virtAsstPumpBattery": "Bateria pompy jest w %1 %2", + "virtAsstUploaderBattery": "Twoja bateria ma %1", + "virtAsstLastLoop": "Ostatnia pomyślna pętla była %1", + "virtAsstLoopNotAvailable": "Plugin Loop prawdopodobnie nie jest włączona", + "virtAsstLoopForecastAround": "Zgodnie z prognozą pętli, glikemia around %1 będzie podczas następnego %2", + "virtAsstLoopForecastBetween": "Zgodnie z prognozą pętli, glikemia between %1 and %2 będzie podczas następnego %3", + "virtAsstAR2ForecastAround": "Zgodnie z prognozą AR2 oczekuje się, że w ciągu następnych %2 będziesz miał około %1", + "virtAsstAR2ForecastBetween": "Zgodnie z prognozą AR2 oczekuje się, że w ciągu następnych %3 będziesz miał między %1 a %2", + "virtAsstForecastUnavailable": "Prognoza pętli nie jest możliwa, z dostępnymi danymi.", + "virtAsstRawBG": "Glikemia RAW wynosi %1", + "virtAsstOpenAPSForecast": "Glikemia prognozowana przez OpenAPS wynosi %1", + "virtAsstCob3person": "%1 ma %2 węglowodanów na pokładzie", + "virtAsstCob": "Masz %1 węglowodanów na pokładzie", + "virtAsstCGMMode": "Twój tryb CGM jest %1 z %2.", + "virtAsstCGMStatus": "Twój status CGM jest %1 z %2.", + "virtAsstCGMSessAge": "Twoja sesja CGM była aktywna przez %1 dni i %2 godzin.", + "virtAsstCGMSessNotStarted": "W tej chwili nie ma aktywnej sesji CGM.", + "virtAsstCGMTxStatus": "Twój nadajnik CGM, według stanu na %2, był %1.", + "virtAsstCGMTxAge": "Twój nadajnik CGM działa %1 dni.", + "virtAsstCGMNoise": "Szum CGM, według stanu na %2, wynosił %1.", + "virtAsstCGMBattOne": "Bateria CGM ma %1 V od %2.", + "virtAsstCGMBattTwo": "Poziom baterii CGM, według stanu na %3, wynosił %1 i %2 voltów.", + "virtAsstDelta": "Twoja delta %1 wynosiła międy %2 a %3.", + "virtAsstDeltaEstimated": "Twoja szacowana delta %1 wynosiła międy %2 a %3.", + "virtAsstUnknownIntentTitle": "Nieznany zamiar", + "virtAsstUnknownIntentText": "Przykro mi, nie wiem, o co prosisz.", + "Fat [g]": "Tłuszcz [g]", + "Protein [g]": "Białko [g]", + "Energy [kJ]": "Energia [kJ}", + "Clock Views:": "Widoki zegarów", + "Clock": "Zegar", + "Color": "Kolor", + "Simple": "Prosty", + "TDD average": "Średnia dawka dzienna", + "Bolus average": "Udział bolusa w średniej", + "Basal average": "Udział bazy w średniej", + "Base basal average:": "Udział podstawowej bazy w średniej:", + "Carbs average": "Średnia ilość węglowodanów", + "Eating Soon": "Przed jedzeniem", + "Last entry {0} minutes ago": "Ostatni wpis przed {0} minutami", + "change": "zmiana", + "Speech": "Głos", + "Target Top": "Górny limit", + "Target Bottom": "Dolny limit", + "Canceled": "Anulowane", + "Meter BG": "Glikemia z krwi", + "predicted": "prognoza", + "future": "przyszłość", + "ago": "temu", + "Last data received": "Ostatnie otrzymane dane", + "Clock View": "Widok zegara", + "Protein": "Białko", + "Fat": "Tłuszcz", + "Protein average": "Średnia białka", + "Fat average": "Średnia tłuszczu", + "Total carbs": "Węglowodany ogółem", + "Total protein": "Białko ogółem", + "Total fat": "Tłuszcz ogółem", + "Database Size": "Rozmiar Bazy Danych", + "Database Size near its limits!": "Rozmiar bazy danych zbliża się do limitu!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Baza danych zajmuje %1 MiB z dozwolonych %2 MiB. Proszę zrób kopię zapasową i oczyść bazę danych!", + "Database file size": "Rozmiar pliku bazy danych", + "%1 MiB of %2 MiB (%3%)": "%1 MiB z %2 MiB (%3%)", + "Data size": "Rozmiar danych", + "virtAsstDatabaseSize": "%1 MiB co stanowi %2% przestrzeni dostępnej dla bazy danych", + "virtAsstTitleDatabaseSize": "Rozmiar pliku bazy danych", + "Carbs/Food/Time": "Węglowodory/Jedzenie/Czas", + "You have administration messages": "Masz wiadomości administracyjne", + "Admin messages in queue": "Oczekujące wiadomości administratora", + "Queue empty": "Kolejka pusta", + "There are no admin messages in queue": "Brak oczekujących wiadomości administratora", + "Please sign in using the API_SECRET to see your administration messages": "Zaloguj się za pomocą API_SECRET aby zobaczyć komunikaty administracyjne", + "Reads enabled in default permissions": "Odczyty włączone w uprawnieniach domyślnych", + "Data reads enabled": "Odczyt danych włączone", + "Data writes enabled": "Zapis danych włączony", + "Data writes not enabled": "Zapis danych wyłączony", + "Color prediction lines": "Kolorowe linie prognozy", + "Release Notes": "Informacje o wydaniu", + "Check for Updates": "Sprawdź aktualizacje", + "Open Source": "Otwartoźródłowe", + "Nightscout Info": "Nazwa strony Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Głównym celem Loopalyzera jest wizualizacja działania systemu zamkniętej pętli. Może działać również z innymi konfiguracjami, zarówno zamkniętych jak i otwartych pętli, jak i poza pętlą. Jednak w zależności od tego, którego źródła danych (uploadera) używasz, jak często jest on w stanie przechwytywać Twoje dane i przesyłać do NS, i w jaki sposób uzupełnia i dosyła brakujące dane, niektóre wykresy mogą zawierać luki, lub być całkowicie puste. Zawsze upewnij się, że wykresy wyglądają rozsądnie. Najlepiej przeglądać jeden dzień na raz, po czym przejżeć kolejne dni, aby upewnić się co do poprawności analizy.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalizator zawiera funkcję przesunięcia czasu. Jeśli na przykład pacjent je śniadanie o godzinie 07:00 jednego dnia i o godzinie 08:00 następnego dnia, to średnia krzywa stężenia glukozy we krwi z tych dwóch dni najprawdopodobniej będzie wyglądać na wyrównaną i nie będzie pokazywać rzeczywistej odpowiedzi glikemicznej po śniadaniu. Przesunięcie czasu obliczy średni czas spożywania tych posiłków, a następnie przesunie wszystkie dane (węglowodany, insulina, baza itp.). w obu dniach o odpowiednie przesunięcie czasowe, tak aby oba posiłki wypadały w średniej porze posiłku.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "W tym przykładzie wszystkie dane z pierwszego dnia są przesuwane w przód o 30 minut i wszystkie dane z drugiego dnia wstecz o 30 minut, tak aby wglądało na to, że śniadanie miało miejsce o godzinie 07:30 w obu dniach. Pozwala to zaobserwować rzeczywistą średnią odpowiedź glikemi po posiłku.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Przesunięcie czasu podświetla okres po średnim czasie rozpoczęcia posiłku na szaro, przez czas trwania DIA (Czas działania insuliny). Ponieważ wszystkie punkty danych dla całego dnia ulegają przesunięciu, krzywe poza szarym obszarem mogą być niedokładne.", + "Note that time shift is available only when viewing multiple days.": "Pamiętaj, że zmiana czasu jest dostępna tylko podczas przeglądania wielu dni.", + "Please select a maximum of two weeks duration and click Show again.": "Wybierz okres maksymalnie dwóch tygodni i kliknij przycisk Pokaż ponownie.", + "Show profiles table": "Pokaż tabelę profili", + "Show predictions": "Pokaż prognozy", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Przesunięcie czasu na posiłkach większych niż %1 g węglowodanów spożytych między %2 a %3", + "Previous": "Poprzedni", + "Previous day": "Poprzedni dzień", + "Next day": "Następny dzień", + "Next": "Dalej", + "Temp basal delta": "Baza tymczasowa", + "Authorized by token": "Autoryzowane przez token", + "Auth role": "Rola autoryzacji", + "view without token": "zobacz bez tokena", + "Remove stored token": "Usuń zapisany token", + "Weekly Distribution": "Dystrybucja tygodniowa", + "Failed authentication": "Nieudane uwierzytelnianie", + "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?": "Urządzenie o adresie IP %1 próbowało uwierzytelnić się w Nightscout używając błędnych danych uwierzytelniających. Sprawdź konfigurację swoich urządzeń i uploaderów, czy aby nie używają błędnego API_SECRET lub tokena?", + "Default (with leading zero and U)": "Domyślne (zero na początku i U)", + "Concise (with U, without leading zero)": "Krótki (z U, bez zera na początku)", + "Minimal (without leading zero and U)": "Minimalny (bez zera na początku i bez U)", + "Small Bolus Display": "Wyświetlanie małych bolusów", + "Large Bolus Display": "Wyświetlanie dużych bolusów", + "Bolus Display Threshold": "Próg dużego bolusa", + "%1 U and Over": "%1 U i więcej", + "Event repeated %1 times.": "Zdarzenie powtórzone %1 razy.", + "minutes": "min", + "Last recorded %1 %2 ago.": "Ostatnio odnotowano %1 %2 temu.", + "Security issue": "Problem zabezpieczeń", + "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.": "Wykryto słaby API_SECRET. Proszę użyć kombinacji małych i DUZYCH liter, cyfr i znaków niealfanumerycznych, takich jak !#%&/ aby zmniejszyć ryzyko nieautoryzowanego dostępu. Nie używać polskich znaków diakrytycznych (\"ogonki\"). Minimalna długość API_SECRET to 12 znaków.", + "less than 1": "mniej niż 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Hasło MongoDB i API_SECRET są takie same. To naprawdę zły pomysł. Zmień oba hasła i nie używaj ich ponownie w systemie." +} diff --git a/translations/pt_BR.json b/translations/pt_BR.json new file mode 100644 index 00000000000..6fc061284af --- /dev/null +++ b/translations/pt_BR.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Ouvindo na porta", + "Mo": "Seg", + "Tu": "Ter", + "We": "Qua", + "Th": "Qui", + "Fr": "Sex", + "Sa": "Sab", + "Su": "Dom", + "Monday": "Segunda", + "Tuesday": "Terça", + "Wednesday": "Quarta", + "Thursday": "Quinta", + "Friday": "Sexta", + "Saturday": "Sábado", + "Sunday": "Domingo", + "Category": "Categoria", + "Subcategory": "Subcategoria", + "Name": "Nome", + "Today": "Hoje", + "Last 2 days": "Últimos 2 dias", + "Last 3 days": "Últimos 3 dias", + "Last week": "Semana passada", + "Last 2 weeks": "Últimas 2 semanas", + "Last month": "Mês passado", + "Last 3 months": "Últimos 3 meses", + "From": "De", + "To": "a", + "Notes": "Notas", + "Food": "Comida", + "Insulin": "Insulina", + "Carbs": "Carboidrato", + "Notes contain": "Notas contém", + "Target BG range bottom": "Limite inferior de glicemia", + "top": "superior", + "Show": "Mostrar", + "Display": "Visualizar", + "Loading": "Carregando", + "Loading profile": "Carregando perfil", + "Loading status": "Carregando status", + "Loading food database": "Carregando dados de alimentos", + "not displayed": "não exibido", + "Loading CGM data of": "Carregando dados de CGM de", + "Loading treatments data of": "Carregando dados de tratamento de", + "Processing data of": "Processando dados de", + "Portion": "Porção", + "Size": "Tamanho", + "(none)": "(nenhum)", + "None": "nenhum", + "": "", + "Result is empty": "Resultado vazio", + "Day to day": "Dia a dia", + "Week to week": "Semana a semana", + "Daily Stats": "Estatísticas diárias", + "Percentile Chart": "Percentis", + "Distribution": "Distribuição", + "Hourly stats": "Estatísticas por hora", + "netIOB stats": "Estatísticas de netIOB", + "temp basals must be rendered to display this report": "basais temporárias devem ser renderizadas para exibir este relatório", + "No data available": "Não há dados", + "Low": "Baixo", + "In Range": "Na meta", + "Period": "Período", + "High": "Alto", + "Average": "Média", + "Low Quartile": "Quartil inferior", + "Upper Quartile": "Quartil superior", + "Quartile": "Quartil", + "Date": "Data", + "Normal": "Normal", + "Median": "Mediana", + "Readings": "Valores", + "StDev": "DesvPadr", + "Daily stats report": "Relatório diário", + "Glucose Percentile report": "Relatório de Percentis de Glicemia", + "Glucose distribution": "Distribuição de glicemias", + "days total": "dias no total", + "Total per day": "dias no total", + "Overall": "Geral", + "Range": "intervalo", + "% of Readings": "% de valores", + "# of Readings": "N° de valores", + "Mean": "Média", + "Standard Deviation": "Desvio padrão", + "Max": "Máx", + "Min": "Mín", + "A1c estimation*": "HbA1c estimada*", + "Weekly Success": "Resultados semanais", + "There is not sufficient data to run this report. Select more days.": "Não há dados suficientes para executar este relatório. Selecione mais dias.", + "Using stored API secret hash": "Usando o hash de API existente", + "No API secret hash stored yet. You need to enter API secret.": "Nenhum hash secreto da API armazenado ainda. Você precisa digitar o segredo da API.", + "Database loaded": "Banco de dados carregado", + "Error: Database failed to load": "Erro: Banco de dados não carregado", + "Error": "Erro", + "Create new record": "Criar novo registro", + "Save record": "Salvar registro", + "Portions": "Porções", + "Unit": "Unidade", + "GI": "IG", + "Edit record": "Editar registro", + "Delete record": "Apagar registro", + "Move to the top": "Mover para o topo", + "Hidden": "Oculto", + "Hide after use": "Ocultar após uso", + "Your API secret must be at least 12 characters long": "Seu segredo de API deve conter no mínimo 12 caracteres", + "Bad API secret": "Segredo de API incorreto", + "API secret hash stored": "Segredo de API guardado", + "Status": "Estado", + "Not loaded": "Não carregado", + "Food Editor": "Editor de alimentos", + "Your database": "Seu banco de dados", + "Filter": "Filtro", + "Save": "Salvar", + "Clear": "Apagar", + "Record": "Gravar", + "Quick picks": "Seleção rápida", + "Show hidden": "Mostrar ocultos", + "Your API secret or token": "Suq senha do API ou token", + "Remember this device. (Do not enable this on public computers.)": "Lembrar deste dispositivo. (Não ative isso em computadores de acesso público.)", + "Treatments": "Procedimentos", + "Time": "Hora", + "Event Type": "Tipo de evento", + "Blood Glucose": "Glicemia", + "Entered By": "Inserido por", + "Delete this treatment?": "Apagar este procedimento?", + "Carbs Given": "Carboidratos", + "Insulin Given": "Insulina", + "Event Time": "Hora do evento", + "Please verify that the data entered is correct": "Favor verificar se os dados estão corretos", + "BG": "Glicemia", + "Use BG correction in calculation": "Usar correção de glicemia nos cálculos", + "BG from CGM (autoupdated)": "Glicemia do sensor (Automático)", + "BG from meter": "Glicemia do glicosímetro", + "Manual BG": "Glicemia Manual", + "Quickpick": "Seleção rápida", + "or": "ou", + "Add from database": "Adicionar do banco de dados", + "Use carbs correction in calculation": "Usar correção com carboidratos no cálculo", + "Use COB correction in calculation": "Usar correção de COB no cálculo", + "Use IOB in calculation": "Usar IOB no cálculo", + "Other correction": "Outra correção", + "Rounding": "Arredondamento", + "Enter insulin correction in treatment": "Inserir correção com insulina no tratamento", + "Insulin needed": "Insulina necessária", + "Carbs needed": "Carboidratos necessários", + "Carbs needed if Insulin total is negative value": "Carboidratos necessários se Insulina total for negativa", + "Basal rate": "Taxa basal", + "60 minutes earlier": "60 min antes", + "45 minutes earlier": "45 min antes", + "30 minutes earlier": "30 min antes", + "20 minutes earlier": "20 min antes", + "15 minutes earlier": "15 min antes", + "Time in minutes": "Tempo em minutos", + "15 minutes later": "15 min depois", + "20 minutes later": "20 min depois", + "30 minutes later": "30 min depois", + "45 minutes later": "45 min depois", + "60 minutes later": "60 min depois", + "Additional Notes, Comments": "Notas adicionais e comentários", + "RETRO MODE": "Modo Retrospectivo", + "Now": "Agora", + "Other": "Outro", + "Submit Form": "Enviar formulário", + "Profile Editor": "Editor de perfil", + "Reports": "Relatórios", + "Add food from your database": "Incluir alimento do seu banco de dados", + "Reload database": "Recarregar banco de dados", + "Add": "Adicionar", + "Unauthorized": "Não autorizado", + "Entering record failed": "Entrada de registro falhou", + "Device authenticated": "Dispositivo autenticado", + "Device not authenticated": "Dispositivo não autenticado", + "Authentication status": "Status de autenticação", + "Authenticate": "Autenticar", + "Remove": "Remover", + "Your device is not authenticated yet": "Seu dispositivo ainda não foi autenticado", + "Sensor": "Sensor", + "Finger": "Ponta de dedo", + "Manual": "Manual", + "Scale": "Escala", + "Linear": "Linear", + "Logarithmic": "Logarítmica", + "Logarithmic (Dynamic)": "Logarítmica (Dinâmica)", + "Insulin-on-Board": "Insulina ativa", + "Carbs-on-Board": "Carboidratos ativos", + "Bolus Wizard Preview": "Ajuda de bolus", + "Value Loaded": "Valores carregados", + "Cannula Age": "Idade da Cânula (ICAT)", + "Basal Profile": "Perfil de Basal", + "Silence for 30 minutes": "Silenciar por 30 minutos", + "Silence for 60 minutes": "Silenciar por 60 minutos", + "Silence for 90 minutes": "Silenciar por 90 minutos", + "Silence for 120 minutes": "Silenciar por 120 minutos", + "Settings": "Ajustes", + "Units": "Unidades", + "Date format": "Formato de data", + "12 hours": "12 horas", + "24 hours": "24 horas", + "Log a Treatment": "Registre um tratamento", + "BG Check": "Medida de glicemia", + "Meal Bolus": "Bolus de refeição", + "Snack Bolus": "Bolus de lanche", + "Correction Bolus": "Bolus de correção", + "Carb Correction": "Correção com carboidrato", + "Note": "Nota", + "Question": "Pergunta", + "Exercise": "Exercício", + "Pump Site Change": "Troca de catéter", + "CGM Sensor Start": "Início de sensor", + "CGM Sensor Stop": "Parar sensor de glicose", + "CGM Sensor Insert": "Troca de sensor", + "Sensor Code": "Código de Sensor", + "Transmitter ID": "ID do transmissor", + "Dexcom Sensor Start": "Início de sensor", + "Dexcom Sensor Change": "Troca de sensor", + "Insulin Cartridge Change": "Troca de reservatório de insulina", + "D.A.D. Alert": "Alerta de cão sentinela de diabetes", + "Glucose Reading": "Valor de glicemia", + "Measurement Method": "Método de medida", + "Meter": "Glicosímetro", + "Amount in grams": "Quantidade em gramas", + "Amount in units": "Quantidade em unidades", + "View all treatments": "Visualizar todos os procedimentos", + "Enable Alarms": "Ativar alarmes", + "Pump Battery Change": "Bateria da bomba de infusão de insulina descarregada", + "Pump Battery Age": "Idade da bateria", + "Pump Battery Low Alarm": "Alarme de bateria baixa da bomba de infusão de insulina", + "Pump Battery change overdue!": "Mudança de bateria da bomba atrasada!", + "When enabled an alarm may sound.": "Quando ativado, um alarme poderá soar", + "Urgent High Alarm": "URGENTE: Alarme de glicemia alta", + "High Alarm": "Alarme de glicemia alta", + "Low Alarm": "Alarme de glicemia baixa", + "Urgent Low Alarm": "URGENTE: Alarme de glicemia baixa", + "Stale Data: Warn": "Dados antigos: alerta", + "Stale Data: Urgent": "Dados antigos: Urgente", + "mins": "min", + "Night Mode": "Modo noturno", + "When enabled the page will be dimmed from 10pm - 6am.": "Se ativado, a página será escurecida entre 22h e 6h", + "Enable": "Ativar", + "Show Raw BG Data": "Mostrar dados de glicemia não processados", + "Never": "Nunca", + "Always": "Sempre", + "When there is noise": "Quando houver ruído", + "When enabled small white dots will be displayed for raw BG data": "Se ativado, pontos brancos representarão os dados de glicemia não processados", + "Custom Title": "Customizar Título", + "Theme": "tema", + "Default": "Padrão", + "Colors": "Colorido", + "Colorblind-friendly colors": "Cores para daltônicos", + "Reset, and use defaults": "Zerar e usar padrões", + "Calibrations": "Calibraçôes", + "Alarm Test / Smartphone Enable": "Testar Alarme / Ativar Smartphone", + "Bolus Wizard": "Ajuda de bolus", + "in the future": "no futuro", + "time ago": "tempo atrás", + "hr ago": "h atrás", + "hrs ago": "h atrás", + "min ago": "min atrás", + "mins ago": "min atrás", + "day ago": "dia atrás", + "days ago": "dias atrás", + "long ago": "muito tempo atrás", + "Clean": "Limpo", + "Light": "Leve", + "Medium": "Médio", + "Heavy": "Pesado", + "Treatment type": "Tipo de tratamento", + "Raw BG": "Glicemia sem processamento", + "Device": "Dispositivo", + "Noise": "Ruído", + "Calibration": "Calibração", + "Show Plugins": "Mostrar les Plugins", + "About": "Sobre", + "Value in": "Valor em", + "Carb Time": "Hora do carboidrato", + "Language": "Língua", + "Add new": "Adicionar novo", + "g": "g", + "ml": "mL", + "pcs": "pçs", + "Drag&drop food here": "Arraste e coloque alimentos aqui", + "Care Portal": "Portal de Atendimento", + "Medium/Unknown": "Médio/Desconhecido", + "IN THE FUTURE": "NO FUTURO", + "Order": "Ordenar", + "oldest on top": "mais antigos no topo", + "newest on top": "Mais recentes no topo", + "All sensor events": "Todos os eventos de sensor", + "Remove future items from mongo database": "Remover itens futuro da base de dados mongo", + "Find and remove treatments in the future": "Encontrar e remover tratamentos futuros", + "This task find and remove treatments in the future.": "Este comando encontra e remove tratamentos futuros", + "Remove treatments in the future": "Remover tratamentos futuros", + "Find and remove entries in the future": "Encontrar e remover entradas futuras", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Este comando procura e remove dados de sensor futuros criados por um uploader com data ou horário errados.", + "Remove entries in the future": "Remover entradas futuras", + "Loading database ...": "Carregando banco de dados ...", + "Database contains %1 future records": "O banco de dados contém %1 registros futuros", + "Remove %1 selected records?": "Remover os %1 registros selecionados?", + "Error loading database": "Erro ao carregar danco de dados", + "Record %1 removed ...": "Registro %1 removido ...", + "Error removing record %1": "Erro ao remover registro %1", + "Deleting records ...": "Apagando registros ...", + "%1 records deleted": "%1 Registros excluídos", + "Clean Mongo status database": "Limpar banco de dados de status no Mongo", + "Delete all documents from devicestatus collection": "Apagar todos os documentos da coleção devicestatus", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Este comando remove todos os documentos da coleção devicestatus. Útil quando o status da bateria do uploader não é atualizado corretamente.", + "Delete all documents": "Apagar todos os documentos", + "Delete all documents from devicestatus collection?": "Apagar todos os documentos da coleção devicestatus?", + "Database contains %1 records": "O banco de dados contém %1 registros", + "All records removed ...": "Todos os registros foram removidos ...", + "Delete all documents from devicestatus collection older than 30 days": "Excluir todos os documentos da coleção de dispositivos mais antigos que 30 dias", + "Number of Days to Keep:": "Numero de dias mantidos:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Esta tarefa remove todos os documentos da coleção do dispositivo que são mais antigos do que 30 dias. Útil quando o status da bateria do carregador não é atualizado corretamente.", + "Delete old documents from devicestatus collection?": "Excluir documentos antigos da coleção do dispositivo?", + "Clean Mongo entries (glucose entries) database": "Limpar entradas (entradas de glicose) do banco de dados Mongo", + "Delete all documents from entries collection older than 180 days": "Excluir todos os documentos da coleção de entradas mais antigos que 180 dias", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Esta tarefa remove todos os documentos da coleção de entradas que são mais antigos do que 180 dias. Útil quando o status da bateria não é atualizado corretamente.", + "Delete old documents": "Apagar documentos antigos", + "Delete old documents from entries collection?": "Excluir documentos antigos da coleção de entradas?", + "%1 is not a valid number": "%1 não é um número válido", + "%1 is not a valid number - must be more than 2": "%1 não é um número válido - numero deve ser maior que 2", + "Clean Mongo treatments database": "Limpar banco de dados de tratamentos no Mongo", + "Delete all documents from treatments collection older than 180 days": "Excluir todos os documentos da coleção de tratamentos mais antigos que 180 dias", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Esta tarefa remove todos os documentos da coleção de tratamentos que são mais antigos do que 180 dias. Útil quando o status da bateria não é atualizado corretamente.", + "Delete old documents from treatments collection?": "Apagar todos os documentos da coleção devicestatus?", + "Admin Tools": "Ferramentas de administração", + "Nightscout reporting": "Relatórios do Nightscout", + "Cancel": "Cancelar", + "Edit treatment": "Editar tratamento", + "Duration": "Duração", + "Duration in minutes": "Duração em minutos", + "Temp Basal": "Basal Temporária", + "Temp Basal Start": "Início da Basal Temporária", + "Temp Basal End": "Fim de Basal Temporária", + "Percent": "Porcento", + "Basal change in %": "Mudança de basal em %", + "Basal value": "Valor da basal", + "Absolute basal value": "Valor absoluto da basal", + "Announcement": "Aviso", + "Loading temp basal data": "Carregando os dados de basal temporária", + "Save current record before changing to new?": "Salvar o registro atual antes de mudar para um novo?", + "Profile Switch": "Troca de perfil", + "Profile": "Perfil", + "General profile settings": "Configurações de perfil gerais", + "Title": "Título", + "Database records": "Registros do banco de dados", + "Add new record": "Adicionar novo registro", + "Remove this record": "Remover este registro", + "Clone this record to new": "Duplicar este registro como novo", + "Record valid from": "Registro válido desde", + "Stored profiles": "Perfis guardados", + "Timezone": "Fuso horário", + "Duration of Insulin Activity (DIA)": "Duração da Atividade da Insulina (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Representa a tempo típico durante o qual a insulina tem efeito. Varia de acordo com o paciente e tipo de insulina. Tipicamente 3-4 horas para a maioria das insulinas usadas em bombas e dos pacientes. Algumas vezes chamada de tempo de vida da insulina", + "Insulin to carb ratio (I:C)": "Relação Insulina-Carboidrato (I:C)", + "Hours:": "Horas:", + "hours": "horas", + "g/hour": "g/hora", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g de carboidrato por unidade de insulina. A razão de quantos gramas de carboidrato são processados por cada unidade de insulina.", + "Insulin Sensitivity Factor (ISF)": "Fator de Sensibilidade da Insulina (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL ou mmol/L por unidade de insulina. A razão entre queda glicêmica e cada unidade de insulina de correção administrada.", + "Carbs activity / absorption rate": "Atividade dos carboidratos / taxa de absorção", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "Gramas por unidade de tempo. Representa a mudança em COB por unidade de tempo, bem como a quantidade de carboidratos que deve ter efeito durante esse período de tempo. Absorção de carboidrato / curvas de atividade são menos conhecidas que atividade de insulina, mas podem ser aproximadas usando um atraso inicial seguido de uma taxa de absorção constante (g/h). ", + "Basal rates [unit/hour]": "Taxas de basal [unidades/hora]", + "Target BG range [mg/dL,mmol/L]": "Meta de glicemia [mg/dL, mmol/L]", + "Start of record validity": "Início da validade dos dados", + "Icicle": "Inverso", + "Render Basal": "Renderizar basal", + "Profile used": "Perfil utilizado", + "Calculation is in target range.": "O cálculo está dentro da meta", + "Loading profile records ...": "Carregando dados do perfil ...", + "Values loaded.": "Valores carregados.", + "Default values used.": "Valores padrão em uso.", + "Error. Default values used.": "Erro. Valores padrão em uso.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Os intervalos de tempo da meta inferior e da meta superior não conferem. Os valores padrão serão restaurados.", + "Valid from:": "Válido desde:", + "Save current record before switching to new?": "Salvar os dados atuais antes de mudar para um novo?", + "Add new interval before": "Adicionar novo intervalo antes de", + "Delete interval": "Apagar intervalo", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Bolus duplo", + "Difference": "Diferença", + "New time": "Novo horário", + "Edit Mode": "Modo de edição", + "When enabled icon to start edit mode is visible": "Quando ativado, o ícone iniciar modo de edição estará visível", + "Operation": "Operação", + "Move": "Mover", + "Delete": "Apagar", + "Move insulin": "Mover insulina", + "Move carbs": "Mover carboidratos", + "Remove insulin": "Remover insulina", + "Remove carbs": "Remover carboidratos", + "Change treatment time to %1 ?": "Alterar horário do tratamento para %1 ?", + "Change carbs time to %1 ?": "Alterar horário do carboidrato para %1 ?", + "Change insulin time to %1 ?": "Alterar horário da insulina para %1 ?", + "Remove treatment ?": "Remover tratamento?", + "Remove insulin from treatment ?": "Remover insulina do tratamento?", + "Remove carbs from treatment ?": "Remover carboidratos do tratamento?", + "Rendering": "Renderizando", + "Loading OpenAPS data of": "Carregando dados de OpenAPS de", + "Loading profile switch data": "Carregando dados de troca de perfil", + "Redirecting you to the Profile Editor to create a new profile.": "Configuração de perfil incorreta. \nNão há perfil definido para mostrar o horário. \nRedirecionando para o editor de perfil para criar um perfil novo.", + "Pump": "Bomba", + "Sensor Age": "Idade do sensor", + "Insulin Age": "Idade da insulina", + "Temporary target": "Meta temporária", + "Reason": "Razão", + "Eating soon": "Refeição em breve", + "Top": "Superior", + "Bottom": "Inferior", + "Activity": "Atividade", + "Targets": "Metas", + "Bolus insulin:": "Insulina de bolus", + "Base basal insulin:": "Insulina basal programada:", + "Positive temp basal insulin:": "Insulina basal temporária positiva:", + "Negative temp basal insulin:": "Insulina basal temporária negativa:", + "Total basal insulin:": "Insulina basal total:", + "Total daily insulin:": "Insulina diária total:", + "Unable to save Role": "Não foi possível salvar a função", + "Unable to delete Role": "Não foi possível apagar a Função", + "Database contains %1 roles": "Banco de dados contém %1 Funções", + "Edit Role": "Editar Função", + "admin, school, family, etc": "Administrador, escola, família, etc", + "Permissions": "Permissões", + "Are you sure you want to delete: ": "Tem certeza de que deseja apagar:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Cada função terá uma ou mais permissões. A permissão * é um wildcard, permissões são uma hierarquia utilizando * como um separador.", + "Add new Role": "Adicionar novo Papel", + "Roles - Groups of People, Devices, etc": "Funções - grupos de pessoas, dispositivos, etc", + "Edit this role": "Editar esta Função", + "Admin authorized": "Administrador autorizado", + "Subjects - People, Devices, etc": "Assunto - Pessoas, dispositivos, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Cada assunto terá um único token de acesso e uma ou mais Funções. Clique no token de acesso para abrir uma nova visualização com o assunto selecionado. Este link secreto poderá então ser compartilhado", + "Add new Subject": "Adicionar novo assunto", + "Unable to save Subject": "Não foi possível salvar o assunto", + "Unable to delete Subject": "Impossível apagar assunto", + "Database contains %1 subjects": "Banco de dados contém %1 assuntos", + "Edit Subject": "Editar assunto", + "person, device, etc": "Pessoa, dispositivo, etc", + "role1, role2": "papel1, papel2", + "Edit this subject": "Editar esse assunto", + "Delete this subject": "Apagar esse assunto", + "Roles": "Papéis", + "Access Token": "Token de acesso", + "hour ago": "hora atrás", + "hours ago": "horas atrás", + "Silence for %1 minutes": "Silencir por %1 minutos", + "Check BG": "Verifique a glicemia", + "BASAL": "BASAL", + "Current basal": "Basal atual", + "Sensitivity": "Fator de sensibilidade", + "Current Carb Ratio": "Relação insulina:carboidrato atual", + "Basal timezone": "Fuso horário da basal", + "Active profile": "Perfil ativo", + "Active temp basal": "Basal temporária ativa", + "Active temp basal start": "Início da basal temporária ativa", + "Active temp basal duration": "Duração de basal temporária ativa", + "Active temp basal remaining": "Basal temporária ativa restante", + "Basal profile value": "Valor do perfil basal", + "Active combo bolus": "Bolus duplo em atividade", + "Active combo bolus start": "Início do bolus duplo em atividade", + "Active combo bolus duration": "Duração de bolus duplo em atividade", + "Active combo bolus remaining": "Restante de bolus duplo em atividade", + "BG Delta": "Diferença de glicemia", + "Elapsed Time": "Tempo transcorrido", + "Absolute Delta": "Diferença absoluta", + "Interpolated": "Interpolado", + "BWP": "Ajuda de bolus", + "Urgent": "Urgente", + "Warning": "Aviso", + "Info": "Informações", + "Lowest": "Mais baixo", + "Snoozing high alarm since there is enough IOB": "Ignorar alarme de hiper em função de IOB suficiente", + "Check BG, time to bolus?": "Meça a glicemia, hora de bolus de correção?", + "Notice": "Nota", + "required info missing": "Informação essencial faltando", + "Insulin on Board": "Insulina ativa", + "Current target": "Meta atual", + "Expected effect": "Efeito esperado", + "Expected outcome": "Resultado esperado", + "Carb Equivalent": "Equivalente em carboidratos", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Excesso de insulina equivalente a %1U além do necessário para atingir a meta inferior, sem levar em conta carboidratos", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Excesso de insulina equivalente a %1U além do necessário para atingir a meta inferior. ASSEGURE-SE DE QUE A IOB ESTEJA COBERTA POR CARBOIDRATOS", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Necessária redução de %1U na insulina ativa para atingir a meta inferior, excesso de basal?", + "basal adjustment out of range, give carbs?": "ajuste de basal fora da meta, dar carboidrato?", + "basal adjustment out of range, give bolus?": "ajuste de basal fora da meta, dar bolus de correção?", + "above high": "acima do limite superior", + "below low": "abaixo do limite inferior", + "Projected BG %1 target": "Meta de glicemia estimada %1", + "aiming at": "meta", + "Bolus %1 units": "Bolus %1 unidades", + "or adjust basal": "ou ajuste basal", + "Check BG using glucometer before correcting!": "Verifique glicemia de ponta de dedo antes de corrigir!", + "Basal reduction to account %1 units:": "Redução de basal para compensar %1 unidades:", + "30m temp basal": "Basal temp 30m", + "1h temp basal": "Basal temp 1h", + "Cannula change overdue!": "Substituição de catéter vencida!", + "Time to change cannula": "Hora de subistituir catéter", + "Change cannula soon": "Substituir catéter em breve", + "Cannula age %1 hours": "Idade do catéter %1 horas", + "Inserted": "Inserido", + "CAGE": "ICAT", + "COB": "COB", + "Last Carbs": "Último carboidrato", + "IAGE": "IddI", + "Insulin reservoir change overdue!": "Substituição de reservatório vencida!", + "Time to change insulin reservoir": "Hora de substituir reservatório", + "Change insulin reservoir soon": "Substituir reservatório em brave", + "Insulin reservoir age %1 hours": "Idade do reservatório %1 horas", + "Changed": "Substituído", + "IOB": "IOB", + "Careportal IOB": "IOB do Careportal", + "Last Bolus": "Último bolus", + "Basal IOB": "IOB basal", + "Source": "Fonte", + "Stale data, check rig?": "Dados antigos, verificar uploader?", + "Last received:": "Último recebido:", + "%1m ago": "%1m atrás", + "%1h ago": "%1h atrás", + "%1d ago": "%1d atrás", + "RETRO": "RETRO", + "SAGE": "IddS", + "Sensor change/restart overdue!": "Substituição/reinício de sensor vencido", + "Time to change/restart sensor": "Hora de substituir/reiniciar sensor", + "Change/restart sensor soon": "Mudar/reiniciar sensor em breve", + "Sensor age %1 days %2 hours": "Idade do sensor %1 dias %2 horas", + "Sensor Insert": "Inserção de sensor", + "Sensor Start": "Início de sensor", + "days": "dias", + "Insulin distribution": "Distribuição de insulina", + "To see this report, press SHOW while in this view": "Para ver este relatório, pressione MOSTRAR neste modo de exibição", + "AR2 Forecast": "Previsão de AR2", + "OpenAPS Forecasts": "Previsões do OpenAPS", + "Temporary Target": "Alvo temporário", + "Temporary Target Cancel": "Cancelar alvo temporário", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Perfis", + "Time in fluctuation": "Tempo em flutuação", + "Time in rapid fluctuation": "Tempo em flutuação rápida", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Esta é apenas uma estimativa grosseira que pode ser muito imprecisa e não substitui o verdadeiro exame de sangue. A fórmula utilizada é retirada de:", + "Filter by hours": "Filtrar por horas", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Tempo de flutuação e tempo em flutuação rápida medem o percentual de tempo durante o período examinado, durante o qual a glicose do sangue tem mudado relativamente rápida ou rapidamente. Valores mais baixos são melhores.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Mudança Diária Média é uma soma do valor absoluto de todas as medicões de glicose para o período examinado, dividido pelo número de dias. Mais baixo é melhor.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Mudança Diária Média é uma soma do valor absoluto de todas as medicões de glicose para o período examinado, dividido pelo número de dias. Mais baixo é melhor.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "O RMS fora do intervalo é calculado pela raiz quadrada da distância fora do intervalo para todas as leituras de glicemia para o período examinado. somando as leituras, dividindo pela contagem e pegando a raiz quadrada. Esta métrica é semelhante à porcentagem dentro do intervalo, mas os pesos nas leituras mais altas. Os valores mais baixos são melhores.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">pode ser encontrado aqui.", + "Mean Total Daily Change": "Mudança Total Diária Média", + "Mean Hourly Change": "Mudança média de hora", + "FortyFiveDown": "ligeira descida", + "FortyFiveUp": "ligeira subida", + "Flat": "em espera", + "SingleUp": "subindo", + "SingleDown": "descendo", + "DoubleDown": "queda rápida", + "DoubleUp": "subida rápida", + "virtAsstUnknown": "Esse valor é desconhecido no momento. Por favor, veja o site do Nightscout para mais detalhes.", + "virtAsstTitleAR2Forecast": "Previsão de AR2", + "virtAsstTitleCurrentBasal": "Basal Atual", + "virtAsstTitleCurrentCOB": "COB Atual", + "virtAsstTitleCurrentIOB": "IOB Atual", + "virtAsstTitleLaunch": "Bem-vindo ao Nightscout", + "virtAsstTitleLoopForecast": "Probabilidade do loop", + "virtAsstTitleLastLoop": "Ultimo Loop", + "virtAsstTitleOpenAPSForecast": "Previsões do OpenAPS", + "virtAsstTitlePumpReservoir": "Insulina Restante", + "virtAsstTitlePumpBattery": "Bateria da Bomba", + "virtAsstTitleRawBG": "Glicemia atual", + "virtAsstTitleUploaderBattery": "Carregador de informacao da Bateria", + "virtAsstTitleCurrentBG": "Glicemia atual", + "virtAsstTitleFullStatus": "Status completo", + "virtAsstTitleCGMMode": "Modo CGM", + "virtAsstTitleCGMStatus": "Status do CGM", + "virtAsstTitleCGMSessionAge": "Idade da Sessão do CGM", + "virtAsstTitleCGMTxStatus": "Status do Transmissor CGM", + "virtAsstTitleCGMTxAge": "Idade do Transmissor CGM", + "virtAsstTitleCGMNoise": "Ruído de CGM", + "virtAsstTitleDelta": "Diferenca no nivel de glicemia", + "virtAsstStatus": "%1 e %2 a partir de %3.", + "virtAsstBasal": "%1 basal atual é de %2 unidades por hora", + "virtAsstBasalTemp": "%1 basal temp de %2 unidades por hora terminará %3", + "virtAsstIob": "e você tem %1 insulina a bordo.", + "virtAsstIobIntent": "Você tem %1 insulina a bordo", + "virtAsstIobUnits": "%1 unidades de", + "virtAsstLaunch": "O que você gostaria de verificar no Nightscout?", + "virtAsstPreamble": "Seu/Sua", + "virtAsstPreamble3person": "%1 possui uma ", + "virtAsstNoInsulin": "não", + "virtAsstUploadBattery": "Sua carga de bateria está às %1", + "virtAsstReservoir": "Você tem %1 unidades restantes", + "virtAsstPumpBattery": "Sua bateria de bomba está às %1 %2", + "virtAsstUploaderBattery": "A bateria do seu celular está em %1", + "virtAsstLastLoop": "O último loop de sucesso foi %1", + "virtAsstLoopNotAvailable": "O plugin Loop parece não estar ativado", + "virtAsstLoopForecastAround": "De acordo com a previsão do loop você deverá estar em torno de %1 durante o próximo %2", + "virtAsstLoopForecastBetween": "De acordo com a previsão do loop você deverá estar entre %1 e %2 durante o próximo %3", + "virtAsstAR2ForecastAround": "De acordo com a previsão do AR2, você deverá estar em torno de %1 durante o próximo %2", + "virtAsstAR2ForecastBetween": "De acordo com a previsão do AR2, você deverá estar entre %1 e %2 durante os próximos %3", + "virtAsstForecastUnavailable": "Não é possível fazer previsão com os dados disponíveis", + "virtAsstRawBG": "Seu BG é %1", + "virtAsstOpenAPSForecast": "O BG Eventual do OpenAPS é %1", + "virtAsstCob3person": "%1 tem %2 carboidratos ativos", + "virtAsstCob": "Voce tem %1 carboidratos ativos", + "virtAsstCGMMode": "Seu modo CGM era %1 a partir de %2.", + "virtAsstCGMStatus": "Seu status do CGM era %1 a partir de %2.", + "virtAsstCGMSessAge": "Sua sessão do CGM está ativa por %1 dias e %2 horas.", + "virtAsstCGMSessNotStarted": "Neste momento não existe uma sessão activa do CGM.", + "virtAsstCGMTxStatus": "Seu status do transmissor CGM era de %1 a partir de %2.", + "virtAsstCGMTxAge": "Seu transmissor CGM tem %1 dias.", + "virtAsstCGMNoise": "Seu ruído CGM era de %1 a partir de %2.", + "virtAsstCGMBattOne": "Sua bateria do CGM estava com %1 volts a partir de %2.", + "virtAsstCGMBattTwo": "Os seus níveis de bateria do CGM foram de %1 volts e %2 volts as %3.", + "virtAsstDelta": "Seu delta estava %1 entre %2 e %3.", + "virtAsstDeltaEstimated": "Sua variação estimada foi %1 entre %2 e %3.", + "virtAsstUnknownIntentTitle": "Intenção desconhecida", + "virtAsstUnknownIntentText": "Sinto muito, eu não sei o que você está pedindo.", + "Fat [g]": "Gorduras [g]", + "Protein [g]": "Proteínas [g]", + "Energy [kJ]": "Energia [kJ]", + "Clock Views:": "Relógios:", + "Clock": "Relógio", + "Color": "Cor", + "Simple": "Simples", + "TDD average": "TDD média", + "Bolus average": "Média de bólus", + "Basal average": "Basal média", + "Base basal average:": "Basal média:", + "Carbs average": "Média de carbs", + "Eating Soon": "Comer em breve", + "Last entry {0} minutes ago": "Última entrada há {0} minutos", + "change": "alterar", + "Speech": "Voz", + "Target Top": "Alvo Topo", + "Target Bottom": "Alvo Inferior", + "Canceled": "Cancelado", + "Meter BG": "Medidor Glicose", + "predicted": "prevista", + "future": "futuro", + "ago": "atrás", + "Last data received": "Últimos dados recebidos", + "Clock View": "Relógios", + "Protein": "Proteínas", + "Fat": "Gorduras", + "Protein average": "Média de proteínas", + "Fat average": "Média de gordura", + "Total carbs": "Total de carboidratos", + "Total protein": "Total de proteínas", + "Total fat": "Total de gorduras", + "Database Size": "Tamanho do Banco de Dados", + "Database Size near its limits!": "Banco de dados próximo do limite de espaço!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "O tamanho da base de dados é %1 MiB de %2 MiB. Por favor, faça backup e limpe o banco de dados!", + "Database file size": "Tamanho do banco de dados", + "%1 MiB of %2 MiB (%3%)": "%1 MiB de %2 MiB (%3%)", + "Data size": "Tamanho dos dados", + "virtAsstDatabaseSize": "%1 MiB. Isso é %2% do espaço de banco de dados disponível.", + "virtAsstTitleDatabaseSize": "Tamanho do arquivo de dados", + "Carbs/Food/Time": "Carbos/Comida/Tempo", + "You have administration messages": "Você tem mensagens administrativas", + "Admin messages in queue": "Mensagens administrativas na fila", + "Queue empty": "Fila vazia", + "There are no admin messages in queue": "Não há mensagens administrativas na fila", + "Please sign in using the API_SECRET to see your administration messages": "Por favor, entre usando a API_SECRET para ver suas mensagens de administração", + "Reads enabled in default permissions": "Leituras habilitadas nas permissões por padrão", + "Data reads enabled": "Leituras de dados ativadas", + "Data writes enabled": "Gravação de dados habilitada", + "Data writes not enabled": "Gravação de dados não habilitada", + "Color prediction lines": "Colorir linhas de previsão", + "Release Notes": "Notas de Versão", + "Check for Updates": "Verifique as Atualizações", + "Open Source": "Código Aberto", + "Nightscout Info": "Informação Do NIghtscount", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "A principal finalidade do Loopalyzer é visualizar como o sistema de ciclo fechado do Loop funciona. Ele também pode funcionar com outras configurações, tanto o ciclo fechado como o ciclo aberto, e sem ciclo. No entanto, dependendo do transmissor que você usa, e o quão frequente são capturados e transmitidos seus dados, e de como será possível preencher os dados que faltam, alguns gráficos podem ter lacunas ou até mesmo estar completamente vazios. Certifique-se sempre que os gráficos parecem razoáveis. O melhor é ver um dia de cada vez e percorrer vários dias antes.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "O Loopalyzer inclui um recurso de mudança de tempo. Se você, por exemplo, tomar café da manhã às 07:00 um dia e às 08:00 no dia seguinte à sua curva média de glicose sanguínea nestes dois dias provavelmente ficará nivelada e não mostrará a resposta real depois de um café da manhã. A mudança de tempo calculará o tempo médio que essas refeições foram ingeridas e depois mudará todos os dados (carbos, insulina, basal, etc.) durante ambos os dias a diferença de tempo correspondente para que ambas as refeições se alinha com o tempo médio de início da refeição.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "Neste exemplo, todos os dados do primeiro dia são empurrados para frente 30 minutos e todos os dados do segundo dia 30 minutos para trás, por isso parece que você teve café da manhã às 07h30 dos dois dias. Isso permite que você veja sua resposta média de glicose no sangue de uma refeição.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Tempo deslocado destaca o período após o tempo médio de início da refeição em cinza, pela duração da DIA (Duração da Ação da Insulina). Uma vez que todos os dados indicam que o dia inteiro é deslocado as curvas para fora da área cinza podem não ser exatas.", + "Note that time shift is available only when viewing multiple days.": "Note que o deslocamento do tempo só está disponível quando estiver visualizando vários dias.", + "Please select a maximum of two weeks duration and click Show again.": "Por favor, selecione no máximo duas semanas e clique em Mostrar novamente.", + "Show profiles table": "Mostrar tabela de perfis", + "Show predictions": "Mostrar as previsões", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Deslocamento de tempo nas refeições maiores que %1 g carboidratos consumidos entre %2 e %3", + "Previous": "Anterior", + "Previous day": "Dia anterior", + "Next day": "Próximo Dia", + "Next": "Próximo", + "Temp basal delta": "Variacao basal temporária", + "Authorized by token": "Autorizado por token", + "Auth role": "Código de autenticação", + "view without token": "ver sem token", + "Remove stored token": "Remover token armazenado", + "Weekly Distribution": "Distribuição Semanal", + "Failed authentication": "Falha Na Autenticação ", + "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?": "Um dispositivo no endereço IP %1 tentou autenticar com o Nightscout com credenciais erradas. Verifique se você tem um transmissor com configuração de API_SECRET ou token errados?", + "Default (with leading zero and U)": "Padrão (com zero a esquerda e U)", + "Concise (with U, without leading zero)": "Conciso (com U, sem zero à esquerda)", + "Minimal (without leading zero and U)": "Mínimo (sem zero a esquerda e U)", + "Small Bolus Display": "Tela Pequena de Bolus", + "Large Bolus Display": "Tela Grande de Bolus", + "Bolus Display Threshold": "Limite de exibição de Bolus", + "%1 U and Over": "%1 U e acima", + "Event repeated %1 times.": "Evento repetido %1 vezes.", + "minutes": "minutos", + "Last recorded %1 %2 ago.": "Última gravação há %1 %2 atrás.", + "Security issue": "Problema de segurança", + "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.": "API_SECRET fraco detectado. Por favor, use uma mistura de letras minúsculas e MAIÚSCULAS, números e caracteres não-alfanuméricos como !#%&/ para reduzir o risco de acesso não autorizado. O comprimento mínimo da API_SECRET é de 12 caracteres.", + "less than 1": "menos de 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Senha MongoDB e a interface API_SECRET coincidem. Esta é uma ideia muito ruim. Por favor, altere ambos e não reutilize senhas em todo o sistema." +} diff --git a/translations/pt_PT.json b/translations/pt_PT.json new file mode 100644 index 00000000000..fe86f000c2c --- /dev/null +++ b/translations/pt_PT.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "À escuta no porto", + "Mo": "Seg", + "Tu": "Ter", + "We": "Qua", + "Th": "Qui", + "Fr": "Sex", + "Sa": "Sáb", + "Su": "Dom", + "Monday": "Segunda-feira", + "Tuesday": "Terça-feira", + "Wednesday": "Quarta-feira", + "Thursday": "Quinta-feira", + "Friday": "Sexta-feira", + "Saturday": "Sábado", + "Sunday": "Domingo", + "Category": "Categoria", + "Subcategory": "Subcategoria", + "Name": "Nome", + "Today": "Hoje", + "Last 2 days": "Últimos 2 dias", + "Last 3 days": "Últimos 3 dias", + "Last week": "Última semana", + "Last 2 weeks": "Últimas 2 semanas", + "Last month": "Último mês", + "Last 3 months": "Últimos 3 meses", + "From": "De", + "To": "Para", + "Notes": "Notas", + "Food": "Alimentos", + "Insulin": "Insulina", + "Carbs": "Hidratos", + "Notes contain": "Notas contêm", + "Target BG range bottom": "Alvo Inferior Glicose", + "top": "topo", + "Show": "Mostrar", + "Display": "Visualização", + "Loading": "A Carregar", + "Loading profile": "A Carregar Perfil", + "Loading status": "A Carregar Estado", + "Loading food database": "A Carregar Base de Dados de Alimentos", + "not displayed": "não mostrado", + "Loading CGM data of": "A Carregar Dados CGM de", + "Loading treatments data of": "A Carregar Dados Alimentos de", + "Processing data of": "A Processar Dados de", + "Portion": "Porção", + "Size": "Tamanho", + "(none)": "(nenhum)", + "None": "Nenhum", + "": "", + "Result is empty": "Resultado está vazio", + "Day to day": "Dia a dia", + "Week to week": "Semana a semana", + "Daily Stats": "Estatísticas Diárias", + "Percentile Chart": "Gráfico Percentil", + "Distribution": "Distribuição", + "Hourly stats": "Estatísticas por hora", + "netIOB stats": "Estatísticas netIOB", + "temp basals must be rendered to display this report": "basais temporárias devem ser renderizadas para apresentar este relatório", + "No data available": "Não há dados disponíveis", + "Low": "Hipo", + "In Range": "No Intervalo", + "Period": "Período", + "High": "Hiper", + "Average": "Média", + "Low Quartile": "Quartil Inferior", + "Upper Quartile": "Quartil Superior", + "Quartile": "Quartil", + "Date": "Data", + "Normal": "Normal", + "Median": "Mediana", + "Readings": "Leituras", + "StDev": "Desvio-Padrão", + "Daily stats report": "Relatório Estatísticas Diárias", + "Glucose Percentile report": "Relatório do Percentil da Glicose", + "Glucose distribution": "Distribuição da Glicose", + "days total": "total de dias", + "Total per day": "Total por dia", + "Overall": "Em Geral", + "Range": "Intervalo", + "% of Readings": "% de Leituras", + "# of Readings": "# de Leituras", + "Mean": "Média", + "Standard Deviation": "Desvio Padrão", + "Max": "Máx", + "Min": "Min", + "A1c estimation*": "Estimativa A1c*", + "Weekly Success": "Sucesso Semanal", + "There is not sufficient data to run this report. Select more days.": "Não há dados suficientes para executar este relatório. Seleccione mais dias.", + "Using stored API secret hash": "A usar o Hash API Secret armazenado", + "No API secret hash stored yet. You need to enter API secret.": "Nenhum hash API Secret foi armazenado ainda. Precisa introduzir o API Secret.", + "Database loaded": "Base de Dados foi carregada", + "Error: Database failed to load": "Erro: falha ao carregar Base de Dados", + "Error": "Erro", + "Create new record": "Criar novo registo", + "Save record": "Guardar registo", + "Portions": "Porções", + "Unit": "Unidade", + "GI": "IG", + "Edit record": "Editar registo", + "Delete record": "Eliminar registo", + "Move to the top": "Mover para o topo", + "Hidden": "Oculto", + "Hide after use": "Ocultar após uso", + "Your API secret must be at least 12 characters long": "A sua API Secret tem de ter pelo menos 12 caracteres", + "Bad API secret": "API Secret incorrecta", + "API secret hash stored": "Hash da API Secret armazenado", + "Status": "Estado", + "Not loaded": "Não carregado", + "Food Editor": "Editor de Alimentos", + "Your database": "Sua Base de Dados", + "Filter": "Filtro", + "Save": "Guardar", + "Clear": "Limpar", + "Record": "Gravar", + "Quick picks": "Escolhas rápidas", + "Show hidden": "Mostrar ocultos", + "Your API secret or token": "Sua API Secret ou token", + "Remember this device. (Do not enable this on public computers.)": "Lembrar este dispositivo. (Não active isto em computadores públicos.)", + "Treatments": "Tratamentos", + "Time": "Hora", + "Event Type": "Tipo de Evento", + "Blood Glucose": "Glicose no Sangue", + "Entered By": "Inserido Por", + "Delete this treatment?": "Eliminar este tratamento?", + "Carbs Given": "Hidratos Dados", + "Insulin Given": "Insulina Dada", + "Event Time": "Hora do Evento", + "Please verify that the data entered is correct": "Por favor, verifique se os dados inseridos estão correctos", + "BG": "Glicose", + "Use BG correction in calculation": "Usar Correcção Glicemia no cálculo", + "BG from CGM (autoupdated)": "Glicose do CGM (actualizado auto.)", + "BG from meter": "Glicose do medidor", + "Manual BG": "Glicose Manual", + "Quickpick": "Escolha Rápida", + "or": "ou", + "Add from database": "Adicionar da base de dados", + "Use carbs correction in calculation": "Usar Correcção Hidratos no Cálculo", + "Use COB correction in calculation": "Usar Correção COB no cálculo", + "Use IOB in calculation": "Usar IA no cálculo", + "Other correction": "Outra correcção", + "Rounding": "Arredondamento", + "Enter insulin correction in treatment": "Inserir correcção de insulina no tratamento", + "Insulin needed": "Insulina necessária", + "Carbs needed": "Hidratos necessários", + "Carbs needed if Insulin total is negative value": "Hidratos necessários se o total da Insulina é negativo", + "Basal rate": "Taxa Basal", + "60 minutes earlier": "60 minutos antes", + "45 minutes earlier": "45 minutos antes", + "30 minutes earlier": "30 minutos antes", + "20 minutes earlier": "20 minutos antes", + "15 minutes earlier": "15 minutos antes", + "Time in minutes": "Tempo em minutos", + "15 minutes later": "15 minutos depois", + "20 minutes later": "20 minutos depois", + "30 minutes later": "30 minutos depois", + "45 minutes later": "45 minutos depois", + "60 minutes later": "60 minutos depois", + "Additional Notes, Comments": "Notas adicionais, Comentários", + "RETRO MODE": "MODO RETRO", + "Now": "Agora", + "Other": "Outro", + "Submit Form": "Enviar Formulário", + "Profile Editor": "Editor de Perfil", + "Reports": "Relatórios", + "Add food from your database": "Adicionar alimentos da sua base de dados", + "Reload database": "Recarregar base de dados", + "Add": "Adicionar", + "Unauthorized": "Não autorizado", + "Entering record failed": "Falha ao Inserir Registo", + "Device authenticated": "Dispositivo Autenticado", + "Device not authenticated": "Dispositivo Não Autenticado", + "Authentication status": "Estado de Autenticação", + "Authenticate": "Autenticar", + "Remove": "Remover", + "Your device is not authenticated yet": "Dispositivo ainda não está autenticado", + "Sensor": "Sensor", + "Finger": "Capilar", + "Manual": "Manual", + "Scale": "Escala", + "Linear": "Linear", + "Logarithmic": "Logarítmico", + "Logarithmic (Dynamic)": "Logarítmico (Dinâmico)", + "Insulin-on-Board": "Insulina-a-Bordo (IOB)", + "Carbs-on-Board": "Hidratos-a-Bordo (COB)", + "Bolus Wizard Preview": "Pré-Visualização do Assistente de Bólus", + "Value Loaded": "Valor Carregado", + "Cannula Age": "Idade Cânula", + "Basal Profile": "Perfil Basal", + "Silence for 30 minutes": "Silenciar por 30 minutos", + "Silence for 60 minutes": "Silenciar por 60 minutos", + "Silence for 90 minutes": "Silenciar por 90 minutos", + "Silence for 120 minutes": "Silenciar por 120 minutos", + "Settings": "Definições", + "Units": "Unidades", + "Date format": "Formato da Data", + "12 hours": "12 horas", + "24 hours": "24 horas", + "Log a Treatment": "Registar um Tratamento", + "BG Check": "Verificar Glicose", + "Meal Bolus": "Bólus Refeição", + "Snack Bolus": "Bólus Lanche", + "Correction Bolus": "Bólus Correcção", + "Carb Correction": "Correcção Hidratos", + "Note": "Nota", + "Question": "Questão", + "Exercise": "Exercício", + "Pump Site Change": "Mudança de Cateter", + "CGM Sensor Start": "Iniciar Sensor CGM", + "CGM Sensor Stop": "Parar Sensor CGM", + "CGM Sensor Insert": "Colocação Sensor CGM", + "Sensor Code": "Código do Sensor", + "Transmitter ID": "ID do Transmissor", + "Dexcom Sensor Start": "Iniciar Sensor Dexcom", + "Dexcom Sensor Change": "Troca do Sensor Dexcom", + "Insulin Cartridge Change": "Mudança do Cartucho de Insulina", + "D.A.D. Alert": "Alerta D.A.D. (Diabete Dog Alert)", + "Glucose Reading": "Leitura Glicose", + "Measurement Method": "Método de Medição", + "Meter": "Medidor", + "Amount in grams": "Quantidade em gramas", + "Amount in units": "Quantidade em unidades", + "View all treatments": "Ver todos os tratamentos", + "Enable Alarms": "Activar Alarmes", + "Pump Battery Change": "Troca de Bateria da Bomba", + "Pump Battery Age": "Idade Bateria da Bomba", + "Pump Battery Low Alarm": "Alarme de Bateria Fraca da Bomba", + "Pump Battery change overdue!": "Troca de Bateria da Bomba atrasada!", + "When enabled an alarm may sound.": "Quando activado um alarme pode soar.", + "Urgent High Alarm": "Alarme Muito Alto", + "High Alarm": "Alarme Hiper", + "Low Alarm": "Alarme Hipo", + "Urgent Low Alarm": "Alarme Muito Baixo", + "Stale Data: Warn": "Dados Antigos: Aviso", + "Stale Data: Urgent": "Dados Antigos: Urgente", + "mins": " mins", + "Night Mode": "Modo Nocturno", + "When enabled the page will be dimmed from 10pm - 6am.": "Quando activado, a página será escurecida das 22h - 7h.", + "Enable": "Activar", + "Show Raw BG Data": "Mostrar Dados em Bruto da Glicose", + "Never": "Nunca", + "Always": "Sempre", + "When there is noise": "Quando houver ruído", + "When enabled small white dots will be displayed for raw BG data": "Quando activado, pequenos pontos brancos serão exibidos para dados em bruto da glicose", + "Custom Title": "Título Personalizado", + "Theme": "Tema", + "Default": "Padrão", + "Colors": "Cores", + "Colorblind-friendly colors": "Cores para Daltónicos", + "Reset, and use defaults": "Repor, e usar os padrões", + "Calibrations": "Calibrações", + "Alarm Test / Smartphone Enable": "Alarme Teste / Smartphone Activado", + "Bolus Wizard": "Assistente de Bólus", + "in the future": "no futuro", + "time ago": "tempo atrás", + "hr ago": "hr atrás", + "hrs ago": "hrs atrás", + "min ago": "min atrás", + "mins ago": "mins atrás", + "day ago": "dia atrás", + "days ago": "dias atrás", + "long ago": "há muito tempo", + "Clean": "Limpar", + "Light": "Leve", + "Medium": "Médio", + "Heavy": "Forte", + "Treatment type": "Tipo de Tratamento", + "Raw BG": "Glicose em Bruto", + "Device": "Dispositivo", + "Noise": "Ruído", + "Calibration": "Calibração", + "Show Plugins": "Mostrar Plugins", + "About": "Acerca de", + "Value in": "Valor em", + "Carb Time": "Hora Hidrato", + "Language": "Idioma", + "Add new": "Adicionar novo", + "g": "g", + "ml": "ml", + "pcs": "pçs", + "Drag&drop food here": "Arrastar alimento para aqui", + "Care Portal": "Care Portal", + "Medium/Unknown": "Médio/Desconhecido", + "IN THE FUTURE": "NO FUTURO", + "Order": "Ordenar", + "oldest on top": "mais antigo no topo", + "newest on top": "mais novo no topo", + "All sensor events": "Todos os eventos do sensor", + "Remove future items from mongo database": "Remover itens futuros da base de dados mongo", + "Find and remove treatments in the future": "Localizar e remover tratamentos no futuro", + "This task find and remove treatments in the future.": "Esta tarefa localiza e remove tratamentos no futuro.", + "Remove treatments in the future": "Remover tratamentos no futuro", + "Find and remove entries in the future": "Localizar e remover entradas no futuro", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Esta tarefa localiza e remove dados do CGM no futuro criados pelo emissor com data/hora errada.", + "Remove entries in the future": "Remover entradas no futuro", + "Loading database ...": "A carregar base de dados ...", + "Database contains %1 future records": "Base de Dados contém %1 registos futuros", + "Remove %1 selected records?": "Remover %1 registos seleccionados?", + "Error loading database": "Erro ao carregar base de dados", + "Record %1 removed ...": "Registo %1 removido...", + "Error removing record %1": "Erro ao remover registo %1", + "Deleting records ...": "A eliminar registos ...", + "%1 records deleted": "%1 registos eliminados", + "Clean Mongo status database": "Limpar Estado Base de Dados Mongo", + "Delete all documents from devicestatus collection": "Eliminar todos os documentos da colecção devicestatus", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Esta tarefa remove todos os documentos da colecção do devicestatus. Útil quando o estado da bateria do emissor não é actualizado correctamente.", + "Delete all documents": "Eliminar todos os documentos", + "Delete all documents from devicestatus collection?": "Excluir todos os documentos da coleção devicestatus?", + "Database contains %1 records": "Base de dados contém %1 registos", + "All records removed ...": "Todos os registos removidos...", + "Delete all documents from devicestatus collection older than 30 days": "Eliminar todos os documentos da colecção devicestatus com mais de 30 dias", + "Number of Days to Keep:": "Número de Dias a Manter:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Esta tarefa remove todos os documentos da colecção devicestatus que são mais antigos do que 30 dias. Útil quando o estado da bateria do emissor não é actualizado correctamente.", + "Delete old documents from devicestatus collection?": "Eliminar documentos antigos da colecção devicestatus?", + "Clean Mongo entries (glucose entries) database": "Limpar entradas (entradas de glicose) na base de dados Mongo", + "Delete all documents from entries collection older than 180 days": "Eliminar todos os documentos da colecção de entradas com mais de 180 dias", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Esta tarefa remove todos os documentos da colecção de entradas que são mais antigos do que 180 dias. Útil quando o estado da bateria do emissor não é actualizado correctamente.", + "Delete old documents": "Eliminar documentos antigos", + "Delete old documents from entries collection?": "Eliminar documentos antigos da colecção de entradas?", + "%1 is not a valid number": "%1 não é um número válido", + "%1 is not a valid number - must be more than 2": "%1 não é um número válido - deve ser maior que 2", + "Clean Mongo treatments database": "Limpar base de dados de tratamentos Mongo", + "Delete all documents from treatments collection older than 180 days": "Eliminar todos os documentos da colecção de tratamentos com mais de 180 dias", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Esta tarefa remove todos os documentos da colecção de tratamentos que são mais antigos do que 180 dias. Útil quando o estado da bateria do emissor não é actualizado correctamente.", + "Delete old documents from treatments collection?": "Eliminar documentos antigos da colecção de tratamentos?", + "Admin Tools": "Ferramentas Admin", + "Nightscout reporting": "Relatório Nightscout", + "Cancel": "Cancelar", + "Edit treatment": "Editar tratamento", + "Duration": "Duração", + "Duration in minutes": "Duração em minutos", + "Temp Basal": "Basal Temp", + "Temp Basal Start": "Início Basal Temp", + "Temp Basal End": "Fim Basal Temp", + "Percent": "Percentagem", + "Basal change in %": "Mudança de Nasal em %", + "Basal value": "Valor da Basal", + "Absolute basal value": "Valor absoluto Basal", + "Announcement": "Anúncio", + "Loading temp basal data": "A carregar dados Basal Temp", + "Save current record before changing to new?": "Gravar o registo actual antes de alterar para novo?", + "Profile Switch": "Troca de Perfil", + "Profile": "Perfil", + "General profile settings": "Definições Gerais do Perfil", + "Title": "Título", + "Database records": "Registos Base de Dados", + "Add new record": "Adicionar novo registo", + "Remove this record": "Remover este registo", + "Clone this record to new": "Clonar este registo para o novo", + "Record valid from": "Registo válido de", + "Stored profiles": "Perfis armazenados", + "Timezone": "Fuso Horário", + "Duration of Insulin Activity (DIA)": "Duração de Insulina Activa (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Representa a duração típica sobre a qual a insulina tem efeito. Varia por paciente e por tipo de insulina. Normalmente 3-4 horas para a maioria das injecções de insulina e a maioria dos pacientes. Às vezes também chamado de duração da insulina activa.", + "Insulin to carb ratio (I:C)": "Insulina por Hidratos (I:HC)", + "Hours:": "Horas:", + "hours": "horas", + "g/hour": "g/hora", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g hidratos por U insulina. A proporção de quantos gramas de hidratos são cobertos por cada U de insulina.", + "Insulin Sensitivity Factor (ISF)": "Factor Sensibilidade à Insulina (FSI)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL ou mmol/L por U insulina. A proporção de quanta Glicemia altera com cada U de insulina de correcção.", + "Carbs activity / absorption rate": "Hidratos actividade / taxa de absorção", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gramas por unidade de tempo. Representa quer a alteração do COB por unidade de tempo, bem como a quantidade de hidratos de carbono que devem ter efeito ao longo do tempo. As curvas de absorção de hidratos/actividade são menos bem compreendidas do que as de actuação da insulina, mas podem ser aproximadas usando um atraso inicial seguido de uma taxa constante de absorção (g/hr).", + "Basal rates [unit/hour]": "Taxas de Basal [uni/hora]", + "Target BG range [mg/dL,mmol/L]": "Intervalo Alvo Glicose [mg/dL,mmol/L]", + "Start of record validity": "Início da validade do registo", + "Icicle": "Inverso", + "Render Basal": "Mostrar Basal", + "Profile used": "Perfil usado", + "Calculation is in target range.": "O cálculo está no intervalo alvo.", + "Loading profile records ...": "A carregar registos de perfil ...", + "Values loaded.": "Valores carregados.", + "Default values used.": "Valores padrão usados.", + "Error. Default values used.": "Erro. Valores padrão utilizados.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Os intervalos de tempo do alvo_mínimo e alvo_máximo não coincidem. Valores são restaurados para os valores padrão.", + "Valid from:": "Válido desde:", + "Save current record before switching to new?": "Guardar o registo atual antes de mudar para novo?", + "Add new interval before": "Adicionar novo intervalo antes", + "Delete interval": "Eliminar intervalo", + "I:C": "I:HC", + "ISF": "FSI", + "Combo Bolus": "Bólus Combo", + "Difference": "Diferença", + "New time": "Nova hora", + "Edit Mode": "Modo de Edição", + "When enabled icon to start edit mode is visible": "Quando activado o ícone para iniciar o modo de edição fica visível", + "Operation": "Operação", + "Move": "Mover", + "Delete": "Eliminar", + "Move insulin": "Mover insulina", + "Move carbs": "Mover hidratos", + "Remove insulin": "Remover insulina", + "Remove carbs": "Remover hidratos", + "Change treatment time to %1 ?": "Alterar o tempo de tratamento para %1?", + "Change carbs time to %1 ?": "Alterar o tempo dos hidratos para %1?", + "Change insulin time to %1 ?": "Alterar o tempo da insulina para %1?", + "Remove treatment ?": "Remover tratamento?", + "Remove insulin from treatment ?": "Remover insulina do tratamento?", + "Remove carbs from treatment ?": "Remover hidratos do tratamento?", + "Rendering": "A processar", + "Loading OpenAPS data of": "A carregar dados OpenAPS de", + "Loading profile switch data": "A carregar dados da troca de perfil", + "Redirecting you to the Profile Editor to create a new profile.": "A redireccionar para o Editor de Perfil para criar um novo perfil.", + "Pump": "Bomba", + "Sensor Age": "Idade do Sensor", + "Insulin Age": "Idade Insulina", + "Temporary target": "Alvo Temporário", + "Reason": "Motivo", + "Eating soon": "Comer Brevemente", + "Top": "Topo", + "Bottom": "Fundo", + "Activity": "Actividade", + "Targets": "Alvos", + "Bolus insulin:": "Insulina Bólus:", + "Base basal insulin:": "Insulina Basal Base:", + "Positive temp basal insulin:": "Insulina Basal Temp positiva:", + "Negative temp basal insulin:": "Insulina Basal Temp negativa:", + "Total basal insulin:": "Total Insulina Basal:", + "Total daily insulin:": "Total Diário Insulina (TDI):", + "Unable to save Role": "Não foi possível guardar a Função", + "Unable to delete Role": "Não foi possível eliminar a Função", + "Database contains %1 roles": "Base de Dados contém %1 funções", + "Edit Role": "Editar Função", + "admin, school, family, etc": "admin, escola, família, etc", + "Permissions": "Permissões", + "Are you sure you want to delete: ": "Tem a certeza que quer eliminar: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Cada função terá 1 ou mais permissões. A permissão * é uma carta curinga, as permissões são uma hierarquia usando : como separador.", + "Add new Role": "Adicionar nova Função", + "Roles - Groups of People, Devices, etc": "Funções - Grupos de Pessoas, Dispositivos, etc", + "Edit this role": "Editar esta função", + "Admin authorized": "Admin autorizado", + "Subjects - People, Devices, etc": "Assuntos - Pessoas, Dispositivos, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Cada assunto terá um token de acesso único e uma ou mais funções. Clique no token de acesso para abrir uma nova vista com o assunto selecionado, este link secreto pode então ser partilhado.", + "Add new Subject": "Adicionar novo Assunto", + "Unable to save Subject": "Não é possível guardar o Assunto", + "Unable to delete Subject": "Não é possível eliminar o Assunto", + "Database contains %1 subjects": "Base de Dados contém %1 assuntos", + "Edit Subject": "Editar Assunto", + "person, device, etc": "pessoa, dispositivo, etc", + "role1, role2": "função1, função2", + "Edit this subject": "Editar este assunto", + "Delete this subject": "Eliminar este assunto", + "Roles": "Funções", + "Access Token": "Token de Acesso", + "hour ago": "hora atrás", + "hours ago": "horas atrás", + "Silence for %1 minutes": "Silenciar por %1 minutos", + "Check BG": "Verificar Glicose", + "BASAL": "BASAL", + "Current basal": "Basal atual", + "Sensitivity": "Sensibilidade", + "Current Carb Ratio": "Rácio Actual Hidratos", + "Basal timezone": "Fuso Horário Basal", + "Active profile": "Perfil Activo", + "Active temp basal": "Basal Temp Activa", + "Active temp basal start": "Início da basal temporária activa", + "Active temp basal duration": "Duração da basal temporária activa", + "Active temp basal remaining": "Restante basal temporária activa", + "Basal profile value": "Valor do Perfil Basal", + "Active combo bolus": "Bólus Combo Activo", + "Active combo bolus start": "Início Bolús Combo Activo", + "Active combo bolus duration": "Duração Bólus Combo Activo", + "Active combo bolus remaining": "Restant Bólus Combo Activo", + "BG Delta": "Delta Glicose", + "Elapsed Time": "Tempo Decorrido", + "Absolute Delta": "Delta Absoluto", + "Interpolated": "Interpolado", + "BWP": "ASSBOLUS", + "Urgent": "Urgente", + "Warning": "Aviso", + "Info": "Info", + "Lowest": "Mínimo", + "Snoozing high alarm since there is enough IOB": "Silenciar Alarme Hiper, já que há IA suficiente", + "Check BG, time to bolus?": "Verificar Glicose, tempo para bólus?", + "Notice": "Observação", + "required info missing": "informação necessária em falta", + "Insulin on Board": "Insulina a Bordo", + "Current target": "Alvo actual", + "Expected effect": "Efeito Esperado", + "Expected outcome": "Resultado esperado", + "Carb Equivalent": "Equivalente Hidratos", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Excesso de insulina equivalente %1U mais do que o necessário para atingir um alvo baixo, sem contabilizar os hidratos de carbono", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Excesso de insulina equivalente %1U é mais do que o necessário para atingir o alvo baixo, GARANTIR QUE A INSULINA ACTIVA É COBERTA POR HIDRATOS DE CARBONO", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U de redução necessária na insulina activa para atingir um alvo baixo, muita basal?", + "basal adjustment out of range, give carbs?": "ajuste da basal fora do intervalo, ingerir hidratos de carbono?", + "basal adjustment out of range, give bolus?": "ajuste da basal fora do intervalo, corrigir com bolus?", + "above high": "acima hiper", + "below low": "abaixo hipo", + "Projected BG %1 target": "Glicose projectada %1 alvo", + "aiming at": "visando", + "Bolus %1 units": "Bólus %1 unidades", + "or adjust basal": "ou ajuste basal", + "Check BG using glucometer before correcting!": "Verificar Glicose com a capilar antes de corrigir!", + "Basal reduction to account %1 units:": "Redução da basal tem em conta %1 unidades:", + "30m temp basal": "30m basal temp", + "1h temp basal": "1h basal temp", + "Cannula change overdue!": "Troca de cânula atrasada!", + "Time to change cannula": "Hora de trocar a canula", + "Change cannula soon": "Mude a cânula em breve", + "Cannula age %1 hours": "Cânula com idade %1 horas", + "Inserted": "Inserido", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Últimos Hidratos", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Troca de reservatório de insulina em atraso!", + "Time to change insulin reservoir": "Hora de trocar reservatório de insulina", + "Change insulin reservoir soon": "Troque o reservatório de insulina em breve", + "Insulin reservoir age %1 hours": "Idade Reservatório Insulina %1 horas", + "Changed": "Alterado", + "IOB": "IA", + "Careportal IOB": "IA Careportal", + "Last Bolus": "Último Bólus", + "Basal IOB": "IA Basal", + "Source": "Fonte", + "Stale data, check rig?": "Dados obsoletos, verificar plataforma?", + "Last received:": "Último recebido:", + "%1m ago": "%1min atrás", + "%1h ago": "%1h atrás", + "%1d ago": "%1d atrás", + "RETRO": "RETRO", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Mudança/reinício do sensor atrasado!", + "Time to change/restart sensor": "Altura para mudar/reiniciar sensor", + "Change/restart sensor soon": "Mudar/reiniciar o sensor em breve", + "Sensor age %1 days %2 hours": "Idade Sensor %1 dias %2 horas", + "Sensor Insert": "Colocação Sensor", + "Sensor Start": "Início de Sensor", + "days": "dias", + "Insulin distribution": "Distribuição de Insulina", + "To see this report, press SHOW while in this view": "Para ver este relatório, pressione MOSTRAR neste modo de visualização", + "AR2 Forecast": "Previsão AR2", + "OpenAPS Forecasts": "Previsões OpenAPS", + "Temporary Target": "Alvo Temporário", + "Temporary Target Cancel": "Cancelar Alvo Temporário", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Perfis", + "Time in fluctuation": "Tempo em flutuação", + "Time in rapid fluctuation": "Tempo em flutuação rápida", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Esta é apenas uma estimativa aproximada que pode ser muito imprecisa e não substitui o teste da capilar. A fórmula utilizada é retirada de:", + "Filter by hours": "Filtrar por horas", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Tempo em flutuação e Tempo em flutuação rápida medem a percentagem de tempo durante o período examinado, durante o qual a glicemia tem mudado relativamente rápida ou rapidamente. Valores mais baixos são melhores.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Média Troca Total Diária é uma soma do valor absoluto de todas as excursões de glicose para o período examinado, dividido pelo número de dias. Menor é melhor.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Mudança Média por Hora é a soma dos valores absolutos de todas as entradas de glicemia para o período em análise, dividido pelo número de horas nesse periodo. Um valor baixo é melhor.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "RMS Fora do Ontervalo é calculado interligando a distância fora do alvo para todas as leituras de glicemia para o período examinado. somando-os, dividindo pela contagem e obtendo a raiz quadrada. Esta métrica é semelhante à percentagem dentro do intervalo, mas os pesos nas leituras mais altas. Valores mais baixos são melhores.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">pode ser encontrado aqui.", + "Mean Total Daily Change": "Média Troca Total Diária", + "Mean Hourly Change": "Média Troca Por Hora", + "FortyFiveDown": "a descer lento", + "FortyFiveUp": "a subir lento", + "Flat": "estavél", + "SingleUp": "a subir", + "SingleDown": "a descer", + "DoubleDown": "a descer rápido", + "DoubleUp": "a subir rápido", + "virtAsstUnknown": "Esse valor é desconhecido no momento. Por favor, veja o site do Nightscout para mais detalhes.", + "virtAsstTitleAR2Forecast": "Previsão AR2", + "virtAsstTitleCurrentBasal": "Basal Actual", + "virtAsstTitleCurrentCOB": "COB Actual", + "virtAsstTitleCurrentIOB": "IOB Actual", + "virtAsstTitleLaunch": "Bem-vindo(a) ao Nightscout", + "virtAsstTitleLoopForecast": "Previsão Loop", + "virtAsstTitleLastLoop": "Último Loop", + "virtAsstTitleOpenAPSForecast": "Previsão OpenAPS", + "virtAsstTitlePumpReservoir": "Insulina Restante", + "virtAsstTitlePumpBattery": "Bateria da Bomba", + "virtAsstTitleRawBG": "Glicose Bruta em Bruto", + "virtAsstTitleUploaderBattery": "Bateria Emissor", + "virtAsstTitleCurrentBG": "Glicose Actual", + "virtAsstTitleFullStatus": "Estado Completo", + "virtAsstTitleCGMMode": "Modo CGM", + "virtAsstTitleCGMStatus": "Estado CGM", + "virtAsstTitleCGMSessionAge": "Idade Sessão CGM", + "virtAsstTitleCGMTxStatus": "Estado Transmissor CGM", + "virtAsstTitleCGMTxAge": "Idade Transmissor CGM", + "virtAsstTitleCGMNoise": "Ruído CGM", + "virtAsstTitleDelta": "Delta Glicemia", + "virtAsstStatus": "%1 e %2 a partir de %3.", + "virtAsstBasal": "%1 basal actual é de %2 unidades por hora", + "virtAsstBasalTemp": "%1 basal temp de %2 unidades por hora terminará %3", + "virtAsstIob": "e tem %1 insulina a bordo.", + "virtAsstIobIntent": "Tem %1 insulina a bordo", + "virtAsstIobUnits": "%1 unidades de", + "virtAsstLaunch": "O que gostaria de verificar no Nightscout?", + "virtAsstPreamble": "Seu", + "virtAsstPreamble3person": "%1 tem uma ", + "virtAsstNoInsulin": "não", + "virtAsstUploadBattery": "Bateria do emissor está a %1", + "virtAsstReservoir": "Tem %1 unidades restantes", + "virtAsstPumpBattery": "Bateria da bomba está a %1 %2", + "virtAsstUploaderBattery": "Bateria do emissor está a %1", + "virtAsstLastLoop": "O último loop com sucesso foi %1", + "virtAsstLoopNotAvailable": "O plugin Loop parece não estar activado", + "virtAsstLoopForecastAround": "De acordo com a previsão do loop deverá estar perto de %1 durante o próximo %2", + "virtAsstLoopForecastBetween": "De acordo com a previsão do loop deverá estar entre %1 e %2 ao longo dos próximos %3", + "virtAsstAR2ForecastAround": "De acordo com a previsão do AR2 deverá estar perto de %1 durante o próximo %2", + "virtAsstAR2ForecastBetween": "De acordo com a previsão de AR2, você deverá estar entre %1 e %2 ao longo dos próximos %3", + "virtAsstForecastUnavailable": "Não é possível prever com os dados disponíveis", + "virtAsstRawBG": "Glicose em Bruto é %1", + "virtAsstOpenAPSForecast": "Glicose Eventual do OpenAPS é %1", + "virtAsstCob3person": "%1 tem %2 hidratos a bordo", + "virtAsstCob": "Tem %1 hidratos a bordo", + "virtAsstCGMMode": "Modo CGM era %1 a partir de %2.", + "virtAsstCGMStatus": "Estado do CGM era %1 a partir de %2.", + "virtAsstCGMSessAge": "Sessão do CGM está activa por %1 dias e %2 horas.", + "virtAsstCGMSessNotStarted": "Neste momento não existe uma sessão activa do CGM.", + "virtAsstCGMTxStatus": "Estado do transmissor CGM era de %1 a partir de %2.", + "virtAsstCGMTxAge": "Transmissor CGM tem %1 dias.", + "virtAsstCGMNoise": "Ruído CGM era de %1 a partir de %2.", + "virtAsstCGMBattOne": "Bateria CGM estava com %1 volts a partir de %2.", + "virtAsstCGMBattTwo": "Níveis bateria CGM eram de %1 volts e %2 volts de %3.", + "virtAsstDelta": "Delta era %1 entre %2 e %3.", + "virtAsstDeltaEstimated": "Delta estimado era %1 entre %2 e %3.", + "virtAsstUnknownIntentTitle": "Intenção Desconhecida", + "virtAsstUnknownIntentText": "Sinto muito, não sei o que está a pedir.", + "Fat [g]": "Gordura [g]", + "Protein [g]": "Proteína [g]", + "Energy [kJ]": "Energia [kJ]", + "Clock Views:": "Vistas Relógio:", + "Clock": "Relógio", + "Color": "Cor", + "Simple": "Simples", + "TDD average": "Média TDI", + "Bolus average": "Média Bólus", + "Basal average": "Média Basal", + "Base basal average:": "Média Basal Base:", + "Carbs average": "Média Hidratos", + "Eating Soon": "Comer Brevemente", + "Last entry {0} minutes ago": "Última entrada há {0} minutos", + "change": "alterar", + "Speech": "Fala", + "Target Top": "Alvo Superior", + "Target Bottom": "Alvo Inferior", + "Canceled": "Cancelado", + "Meter BG": "Medidor Glicose", + "predicted": "previsto", + "future": "futuro", + "ago": "atrás", + "Last data received": "Últimos dados recebidos", + "Clock View": "Vista do Relógio", + "Protein": "Proteína", + "Fat": "Gordura", + "Protein average": "Média Proteína", + "Fat average": "Média Gordura", + "Total carbs": "Total de Hidratos", + "Total protein": "Total Proteína", + "Total fat": "Total Gordura", + "Database Size": "Tamanho da Base de Dados", + "Database Size near its limits!": "Tamanho da Base de Dados próximo do seu limite!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "O tamanho da Dase de Dados é %1 MiB de %2 MiB. Por favor, faça copiar de segurança e limpe a base de dados!", + "Database file size": "Tamanho do ficheiro Base de Dados", + "%1 MiB of %2 MiB (%3%)": "%1 MiB de %2 MiB (%3%)", + "Data size": "Tamanho dos dados", + "virtAsstDatabaseSize": "%1 MiB. Isso é %2% do espaço da base de dados disponível.", + "virtAsstTitleDatabaseSize": "Tamanho do ficheiro da Base de Dados", + "Carbs/Food/Time": "Hidratos/Alimentos/Hora", + "You have administration messages": "Tem mensagens administrativas", + "Admin messages in queue": "Mensagens de Administrador na fila", + "Queue empty": "Fila vazia", + "There are no admin messages in queue": "Não há mensagens de admin na fila", + "Please sign in using the API_SECRET to see your administration messages": "Por favor, entre com a API_SECRET para ver mensagens de administração", + "Reads enabled in default permissions": "Leituras activadas nas permissões padrão", + "Data reads enabled": "Leitura de Dados Activada", + "Data writes enabled": "Escrita de Dados Activada", + "Data writes not enabled": "Escrita de Dados desactivada", + "Color prediction lines": "Cor Linhas de Previsão", + "Release Notes": "Notas de Lançamento", + "Check for Updates": "Verificar Acualizações", + "Open Source": "Código Aberto", + "Nightscout Info": "Info Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "A principal finalidade do Loopalyzer é visualizar como o sistema de Loop fechado funciona. Também pode funcionar com outras configurações, tanto com loop fechado como loop aberto, e o non-loop. No entanto, dependendo do uploader que é usado, a frequência com que os dados são recolhidos e é feito o upload, e a capacidade em preencher dados em falta, alguns gráficos podem ter espaços em branco ou até mesmo estar completamente vazios. Certifique-se sempre que a aparência dos gráficos é razoável. O melhor é verificar um dia de cada vez e percorrer vários dias primeiro.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "O Loopalyzer inclui uma função de mudança de tempo. Se, por exemplo, tomar o pequeno-almoço às 07h00 um dia e às 08h00 no dia seguinte, a curva média de glicose no sangue nestes dois dias provavelmente ficará achatada e não mostrará a resposta real depois de um pequeno-almoço. A função de mudança de tempo calculará o tempo médio em que essas refeições foram comidas e depois mudará todos os dados (hidratos de carbono, insulina, basal, etc.) de ambos os dias, fazendo corresponder a diferença de tempo, para que ambas as refeições se alinhem com o tempo médio de início da refeição.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "Neste exemplo, todos os dados do primeiro dia são empurrados para frente 30 minutos e todos os dados do segundo dia são puxados 30 minutos para trás no tempo, por isso parece que tomou o pequeno-almoço às 07h30 nos dois dias. Isto permite que consiga ver a resposta média da glicose no sangue numa refeição.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Mudança de tempo destaca o período após o tempo médio de início da refeição em cinza, pela duração da DIA (Duração de Insulina Activa). Uma vez que todos os dados indicam que o dia inteiro são deslocados, as curvas fora área cinza podem não ser exactas.", + "Note that time shift is available only when viewing multiple days.": "Note que a mudança de tempo só está disponível quando estiver a visualizar vários dias.", + "Please select a maximum of two weeks duration and click Show again.": "Por favor, selecione no máximo duas semanas de duração e clique em Mostrar novamente.", + "Show profiles table": "Mostrar tabela de perfis", + "Show predictions": "Mostrar previsões", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Mudança de tempo nas refeições maiores que %1 g hidratos consumidos entre %2 e %3", + "Previous": "Anterior", + "Previous day": "Dia Anterior", + "Next day": "Dia seguinte", + "Next": "Seguinte", + "Temp basal delta": "Delta Basal Temp", + "Authorized by token": "Autorizado pelo token", + "Auth role": "Função de Autenticação", + "view without token": "ver sem token", + "Remove stored token": "Remover token armazenado", + "Weekly Distribution": "Distribuição Semanal", + "Failed authentication": "Autenticação Falhou", + "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?": "Um dispositivo no endereço IP %1 tentou autenticar com o Nightscout com credenciais erradas. Verifique se tem uma configuração de um emissor com API_SECRET ou token errados?", + "Default (with leading zero and U)": "Padrão (com zero à esquerda e U)", + "Concise (with U, without leading zero)": "Conciso (com U, sem zero à esquerda)", + "Minimal (without leading zero and U)": "Mínimo (sem zero à esquerda e U)", + "Small Bolus Display": "Expositor Pequeno Bólus", + "Large Bolus Display": "Expositor Grande Bólus", + "Bolus Display Threshold": "Mostrador Limite Bólus", + "%1 U and Over": "%1 U e Acima", + "Event repeated %1 times.": "Evento repetido %1 vezes.", + "minutes": "minutos", + "Last recorded %1 %2 ago.": "Último registo %1 %2 atrás.", + "Security issue": "Problema de Segurança", + "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.": "API_SECRET fraco foi detectado. Por favor, use uma mistura de letras minúsculas e MAIÚSCULAS, números e caracteres não-alfanuméricos como !#%&/ para reduzir o risco de acesso não autorizado. O comprimento mínimo da API_SECRET é de 12 caracteres.", + "less than 1": "menos de 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Senha MongoDB e a API_SECRET coincidem. Esta é uma má ideia. Por favor, altere ambas e não reutilize senhas em todo o sistema." +} diff --git a/translations/ro_RO.json b/translations/ro_RO.json new file mode 100644 index 00000000000..e83b60efacf --- /dev/null +++ b/translations/ro_RO.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Activ pe portul", + "Mo": "Lu", + "Tu": "Ma", + "We": "Mie", + "Th": "Jo", + "Fr": "Vi", + "Sa": "Sa", + "Su": "Du", + "Monday": "Luni", + "Tuesday": "Marți", + "Wednesday": "Miercuri", + "Thursday": "Joi", + "Friday": "Vineri", + "Saturday": "Sâmbătă", + "Sunday": "Duminică", + "Category": "Categorie", + "Subcategory": "Subcategorie", + "Name": "Nume", + "Today": "Astăzi", + "Last 2 days": "Ultimele 2 zile", + "Last 3 days": "Ultimele 3 zile", + "Last week": "Săptămâna trecută", + "Last 2 weeks": "Ultimele 2 săptămâni", + "Last month": "Ultima lună", + "Last 3 months": "Ultimele 3 luni", + "From": "De la", + "To": "La", + "Notes": "Note", + "Food": "Mâncare", + "Insulin": "Insulină", + "Carbs": "Carbohidrați", + "Notes contain": "Conținut note", + "Target BG range bottom": "Limita de jos a glicemiei", + "top": "Sus", + "Show": "Arată", + "Display": "Afișează", + "Loading": "Se încarcă", + "Loading profile": "Se încarcă profilul", + "Loading status": "Se încarcă starea", + "Loading food database": "Se încarcă baza de date de alimente", + "not displayed": "neafișat", + "Loading CGM data of": "Încarc datele CGM pentru", + "Loading treatments data of": "Încarc datele despre tratament pentru", + "Processing data of": "Procesez datele pentru", + "Portion": "Porție", + "Size": "Mărime", + "(none)": "(fără)", + "None": "Niciunul", + "": "", + "Result is empty": "Fără rezultat", + "Day to day": "Zi cu zi", + "Week to week": "Săptămâna la săptămână", + "Daily Stats": "Statistici zilnice", + "Percentile Chart": "Grafic percentile", + "Distribution": "Distribuție", + "Hourly stats": "Statistici orare", + "netIOB stats": "Statistici netIOB", + "temp basals must be rendered to display this report": "bazalele temporare trebuie redate pentru a afișa acest raport", + "No data available": "Nu există date disponibile", + "Low": "Prea jos", + "In Range": "În interval", + "Period": "Perioada", + "High": "Prea sus", + "Average": "Media", + "Low Quartile": "Pătrime inferioară", + "Upper Quartile": "Pătrime superioară", + "Quartile": "Pătrime", + "Date": "Data", + "Normal": "Normal", + "Median": "Mediană", + "Readings": "Valori", + "StDev": "Deviație standard", + "Daily stats report": "Raport statistică zilnică", + "Glucose Percentile report": "Raport percentile glicemii", + "Glucose distribution": "Distribuție glicemie", + "days total": "total zile", + "Total per day": "Total pe zi", + "Overall": "General", + "Range": "Interval", + "% of Readings": "% de valori", + "# of Readings": "# de valori", + "Mean": "Medie", + "Standard Deviation": "Deviație standard", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "HbA1C estimată*", + "Weekly Success": "Rezultate Săptămânale", + "There is not sufficient data to run this report. Select more days.": "Nu sunt suficiente date pentru acest raport. Selectați mai multe zile.", + "Using stored API secret hash": "Folosind cheia API secretă stocată", + "No API secret hash stored yet. You need to enter API secret.": "Încă nu există o cheie API secretă stocată. Trebuie să introduci cheia API secretă.", + "Database loaded": "Baza de date încărcată", + "Error: Database failed to load": "Eroare: Nu s-a încărcat baza de date", + "Error": "Eroare", + "Create new record": "Crează înregistrare nouă", + "Save record": "Salvează înregistrarea", + "Portions": "Porții", + "Unit": "Unități", + "GI": "GI", + "Edit record": "Editează înregistrarea", + "Delete record": "Șterge înregistrarea", + "Move to the top": "Mergi la început", + "Hidden": "Ascuns", + "Hide after use": "Ascunde după folosire", + "Your API secret must be at least 12 characters long": "Cheia API trebuie să aibă minim 12 caractere", + "Bad API secret": "Cheie API greșită", + "API secret hash stored": "Cheie API înregistrată", + "Status": "Stare", + "Not loaded": "Neîncărcat", + "Food Editor": "Editor alimente", + "Your database": "Baza ta de date", + "Filter": "Filtru", + "Save": "Salvează", + "Clear": "Inițializare", + "Record": "Înregistrare", + "Quick picks": "Selecție rapidă", + "Show hidden": "Arată înregistrările ascunse", + "Your API secret or token": "API secret sau cheie de acces", + "Remember this device. (Do not enable this on public computers.)": "Tine minte acest dispozitiv. (Nu activaţi pe calculatoarele publice.)", + "Treatments": "Tratamente", + "Time": "Ora", + "Event Type": "Tip eveniment", + "Blood Glucose": "Glicemie", + "Entered By": "Introdus de", + "Delete this treatment?": "Șterg acest eveniment?", + "Carbs Given": "Carbohidrați administrați", + "Insulin Given": "Insulină administrată", + "Event Time": "Ora evenimentului", + "Please verify that the data entered is correct": "Verificaţi dacă datele introduse sunt corecte", + "BG": "Glicemie", + "Use BG correction in calculation": "Folosește corecția de glicemie în calcule", + "BG from CGM (autoupdated)": "Glicemie din senzor (automat)", + "BG from meter": "Glicemie din glucometru", + "Manual BG": "Glicemie manuală", + "Quickpick": "Selecție rapidă", + "or": "sau", + "Add from database": "Adaugă din baza de date", + "Use carbs correction in calculation": "Folosește corecția de carbohidrați în calcule", + "Use COB correction in calculation": "Folosește corectia COB în calcule", + "Use IOB in calculation": "Folosește IOB în calcule", + "Other correction": "Alte corecții", + "Rounding": "Rotunjire", + "Enter insulin correction in treatment": "Introdu corecția de insulină în tratament", + "Insulin needed": "Necesar insulină", + "Carbs needed": "Necesar carbohidrați", + "Carbs needed if Insulin total is negative value": "Carbohidrați necesari dacă insulina totală are valoare negativă", + "Basal rate": "Rata bazală", + "60 minutes earlier": "acum 60 min", + "45 minutes earlier": "acum 45 min", + "30 minutes earlier": "acum 30 min", + "20 minutes earlier": "acum 20 min", + "15 minutes earlier": "acum 15 min", + "Time in minutes": "Timp în minute", + "15 minutes later": "după 15 min", + "20 minutes later": "după 20 min", + "30 minutes later": "după 30 min", + "45 minutes later": "după 45 min", + "60 minutes later": "după 60 min", + "Additional Notes, Comments": "Note adiționale, comentarii", + "RETRO MODE": "MOD RETROSPECTIV", + "Now": "Acum", + "Other": "Altul", + "Submit Form": "Trimite formularul", + "Profile Editor": "Editare profil", + "Reports": "Rapoarte", + "Add food from your database": "Adaugă alimente din baza ta de date", + "Reload database": "Reîncarcă baza de date", + "Add": "Adaugă", + "Unauthorized": "Neautorizat", + "Entering record failed": "Înregistrare eșuată", + "Device authenticated": "Dispozitiv autentificat", + "Device not authenticated": "Dispozitiv neautentificat", + "Authentication status": "Starea autentificării", + "Authenticate": "Autentificare", + "Remove": "Șterge", + "Your device is not authenticated yet": "Dispozitivul nu este autentificat încă", + "Sensor": "Senzor", + "Finger": "Deget", + "Manual": "Manual", + "Scale": "Scală", + "Linear": "Liniar", + "Logarithmic": "Logaritmic", + "Logarithmic (Dynamic)": "Logaritmic (Dinamic)", + "Insulin-on-Board": "IOB-Insulină activă la bord", + "Carbs-on-Board": "COB-Carbohidrați activi la bord", + "Bolus Wizard Preview": "BWP-Vizualizare asistent de bolusare", + "Value Loaded": "Valoare încărcată", + "Cannula Age": "CAGE-Vechime canulă", + "Basal Profile": "Profil bazală", + "Silence for 30 minutes": "Silențios pentru 30 minute", + "Silence for 60 minutes": "Silențios pentru 60 minute", + "Silence for 90 minutes": "Silențios pentru 90 minute", + "Silence for 120 minutes": "Silențios pentru 120 minute", + "Settings": "Setări", + "Units": "Unități", + "Date format": "Formatul datei", + "12 hours": "12 ore", + "24 hours": "24 ore", + "Log a Treatment": "Înregistrează un eveniment", + "BG Check": "Verificare glicemie", + "Meal Bolus": "Bolus masă", + "Snack Bolus": "Bolus gustare", + "Correction Bolus": "Bolus corecție", + "Carb Correction": "Corecție de carbohidrați", + "Note": "Notă", + "Question": "Întrebare", + "Exercise": "Activitate fizică", + "Pump Site Change": "Schimbare loc pompă", + "CGM Sensor Start": "Start senzor CGM", + "CGM Sensor Stop": "Stop senzor CGM", + "CGM Sensor Insert": "Inserare senzor CGM", + "Sensor Code": "Cod senzor", + "Transmitter ID": "Serie transmițător", + "Dexcom Sensor Start": "Pornire senzor Dexcom", + "Dexcom Sensor Change": "Schimbare senzor Dexcom", + "Insulin Cartridge Change": "Schimbare cartuș insulină", + "D.A.D. Alert": "Alertă câine special antrenat ca însoțitor", + "Glucose Reading": "Valoare glicemie", + "Measurement Method": "Metoda de măsurare", + "Meter": "Glucometru", + "Amount in grams": "Cantitate în grame", + "Amount in units": "Cantitate în unități", + "View all treatments": "Vezi toate evenimentele", + "Enable Alarms": "Activează alarmele", + "Pump Battery Change": "Schimbare baterie pompă", + "Pump Battery Age": "Vechime Baterie Pompă", + "Pump Battery Low Alarm": "Alarmă Baterie Scăzută la pompă", + "Pump Battery change overdue!": "Intârziere la schimbarea bateriei de la pompă!", + "When enabled an alarm may sound.": "Când este activ, poate suna o alarmă.", + "Urgent High Alarm": "Alarmă urgentă hiper", + "High Alarm": "Alarmă hiper", + "Low Alarm": "Alarmă hipo", + "Urgent Low Alarm": "Alarmă urgentă hipo", + "Stale Data: Warn": "Date învechite: alertă", + "Stale Data: Urgent": "Date învechite: urgent", + "mins": "min", + "Night Mode": "Mod nocturn", + "When enabled the page will be dimmed from 10pm - 6am.": "La activare va scădea iluminarea paginii între 22 și 6.", + "Enable": "Activează", + "Show Raw BG Data": "Afișează date primare glicemie", + "Never": "Niciodată", + "Always": "Întotdeauna", + "When there is noise": "Atunci când este zgomot", + "When enabled small white dots will be displayed for raw BG data": "La activare vor apărea puncte albe reprezentând valorile primare ale glicemiei", + "Custom Title": "Titlu personalizat", + "Theme": "Temă", + "Default": "Implicit", + "Colors": "Culori", + "Colorblind-friendly colors": "Culori pentru cei cu deficiențe de vedere", + "Reset, and use defaults": "Resetează și folosește setările implicite", + "Calibrations": "Calibrări", + "Alarm Test / Smartphone Enable": "Testare alarmă / Activare pe smartphone", + "Bolus Wizard": "Asistent bolusare", + "in the future": "în viitor", + "time ago": "în trecut", + "hr ago": "oră în trecut", + "hrs ago": "ore în trecut", + "min ago": "min în trecut", + "mins ago": "minute în trecut", + "day ago": "zi în trecut", + "days ago": "zile în trecut", + "long ago": "in urma cu mult timp", + "Clean": "Curat", + "Light": "Ușor", + "Medium": "Mediu", + "Heavy": "Puternic", + "Treatment type": "Tip tratament", + "Raw BG": "Valorile primară a glicemiei", + "Device": "Dispozitiv", + "Noise": "Zgomot", + "Calibration": "Calibrare", + "Show Plugins": "Arată plugin-urile", + "About": "Despre", + "Value in": "Valoare în", + "Carb Time": "Ora carbohidrați", + "Language": "Limba", + "Add new": "Adaugă intrare noua", + "g": "gr", + "ml": "ml", + "pcs": "buc", + "Drag&drop food here": "Apasă&trage aliment aici", + "Care Portal": "Care Portal", + "Medium/Unknown": "Mediu/Necunoscut", + "IN THE FUTURE": "ÎN VIITOR", + "Order": "Sortare", + "oldest on top": "cele mai vechi primele de sus", + "newest on top": "cele mai noi primele de sus", + "All sensor events": "Toate evenimentele legate de senzor", + "Remove future items from mongo database": "Șterge date din viitor din baza de date mongo", + "Find and remove treatments in the future": "Caută și elimină tratamente din viitor", + "This task find and remove treatments in the future.": "Acest instrument curăță tratamentele din viitor.", + "Remove treatments in the future": "Șterge tratamentele din viitor", + "Find and remove entries in the future": "Caută și elimină valorile din viitor", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Acest instrument caută și elimină datele CGM din viitor, create de uploader cu ora setată greșit.", + "Remove entries in the future": "Elimină înregistrările din viitor", + "Loading database ...": "Se încarcă baza de date...", + "Database contains %1 future records": "Baza de date conține %1 înregistrări din viitor", + "Remove %1 selected records?": "Șterg %1 înregistrări selectate?", + "Error loading database": "Eroare la încărcarea bazei de date", + "Record %1 removed ...": "Înregistrarea %1 a fost ștearsă...", + "Error removing record %1": "Eroare la ștergerea înregistrării %1", + "Deleting records ...": "Se șterg înregistrările...", + "%1 records deleted": "%1 înregistrări șterse", + "Clean Mongo status database": "Curăță tabela despre status din Mongo", + "Delete all documents from devicestatus collection": "Șterge toate documentele din colecția de status dispozitiv", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Acest instrument șterge toate documentele din colecția devicestatus. Se folosește când încărcarea bateriei nu se afișează corect.", + "Delete all documents": "Șterge toate documentele", + "Delete all documents from devicestatus collection?": "Șterg toate documentele din colecția status dispozitiv?", + "Database contains %1 records": "Baza de date conține %1 înregistrări", + "All records removed ...": "Toate înregistrările au fost șterse...", + "Delete all documents from devicestatus collection older than 30 days": "Șterge toate documentele mai vechi de 30 de zile din starea dispozitivului", + "Number of Days to Keep:": "Numărul de zile de păstrat:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Acest instrument șterge toate documentele mai vechi de 30 de zile din starea dispozitivului. Se folosește când încărcarea bateriei nu se afișează corect.", + "Delete old documents from devicestatus collection?": "Șterg toate documentele din colecția starea dispozitivului?", + "Clean Mongo entries (glucose entries) database": "Curătă intrarile in baza de date Mongo (intrări de glicemie)", + "Delete all documents from entries collection older than 180 days": "Șterge toate documentele mai vechi de 180 de zile din colectia intrari valori", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Acest instrument șterge toate documentele mai vechi de 180 de zile din colectia intrari valori. Se folosește când încărcarea bateriei nu se afișează corect.", + "Delete old documents": "Șterge documentele vechi", + "Delete old documents from entries collection?": "Șterg toate documentele vechi din colecția intrari valori?", + "%1 is not a valid number": "%1 nu este un număr valid", + "%1 is not a valid number - must be more than 2": "%1 nu este un număr valid - trebuie să fie mai mare decât 2", + "Clean Mongo treatments database": "Curăță baza de date Mongo de tratamente", + "Delete all documents from treatments collection older than 180 days": "Șterge toate documentele mai vechi de 180 de zile din colecția de tratamente", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Acest instrument șterge toate documentele mai vechi de 180 de zile din colecția de tratamente. Se folosește când încărcarea bateriei nu se afișează corect.", + "Delete old documents from treatments collection?": "Șterg documentele vechi din colecția de tratamente?", + "Admin Tools": "Instrumente de administrare", + "Nightscout reporting": "Rapoarte Nightscout", + "Cancel": "Renunță", + "Edit treatment": "Modifică tratament", + "Duration": "Durata", + "Duration in minutes": "Durata în minute", + "Temp Basal": "Bazală temporară", + "Temp Basal Start": "Start bazală temporară", + "Temp Basal End": "Sfârșit bazală temporară", + "Percent": "Procent", + "Basal change in %": "Bazală schimbată în %", + "Basal value": "Valoare bazală", + "Absolute basal value": "Valoare absolută bazală", + "Announcement": "Anunț", + "Loading temp basal data": "Se încarcă date bazală temporară", + "Save current record before changing to new?": "Salvez înregistrarea curentă înainte de a trece mai departe?", + "Profile Switch": "Schimbă profilul", + "Profile": "Profil", + "General profile settings": "Setări generale profil", + "Title": "Titlu", + "Database records": "Baza de date cu înregistrări", + "Add new record": "Adaugă înregistrare nouă", + "Remove this record": "Șterge această înregistrare", + "Clone this record to new": "Dublează această înregistrare", + "Record valid from": "Înregistare validă de la", + "Stored profiles": "Profile salvate", + "Timezone": "Fus orar", + "Duration of Insulin Activity (DIA)": "Durata de acțiune a insulinei (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Reprezintă durata tipică pentru care insulina are efect. Este diferită la fiecare pacient și pentru fiecare tip de insulină. De obicei 3-4 ore pentru marea majoritate a insulinelor si pacienților. Uneori se numește și durata de viață a insulinei.", + "Insulin to carb ratio (I:C)": "Rată insulină la carbohidrați (ICR)", + "Hours:": "Ore:", + "hours": "ore", + "g/hour": "g/oră", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g carbohidrați pentru o unitate de insulină. Câte grame de carbohidrați sunt acoperite de fiecare unitate de insulină.", + "Insulin Sensitivity Factor (ISF)": "Factor de sensilibtate la insulină (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL sau mmol/L pentru 1U insulină. Cât de mult influențează glicemia o corecție de o unitate de insulină.", + "Carbs activity / absorption rate": "Rata de absorbție", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grame pe unitatea de timp. Reprezintă atât schimbarea COB pe unitatea de timp, cât și cantitatea de carbohidrați care ar influența în perioada de timp. Graficele ratei de absorbție sunt mai puțin înțelese decât senzitivitatea la insulină, dar se poate aproxima folosind o întârziere implicită și apoi o rată constantă de absorbție (gr/oră).", + "Basal rates [unit/hour]": "Rate bazale [unitati/oră]", + "Target BG range [mg/dL,mmol/L]": "Intervalul țintă al glicemiei [mg/dL, mmol/L]", + "Start of record validity": "De când este valabilă înregistrarea", + "Icicle": "Icicle", + "Render Basal": "Afișează bazala", + "Profile used": "Profil folosit", + "Calculation is in target range.": "Calculul este în intervalul țintă.", + "Loading profile records ...": "Se încarcă datele profilului...", + "Values loaded.": "Valorile au fost încărcate.", + "Default values used.": "Se folosesc valorile implicite.", + "Error. Default values used.": "Eroare. Se folosesc valorile implicite.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Intervalele temporale pentru țintă_inferioară și țintă_superioară nu se potrivesc. Se folosesc valorile implicite.", + "Valid from:": "Valid de la:", + "Save current record before switching to new?": "Salvez valoarea curentă înainte de a trece la una nouă?", + "Add new interval before": "Adaugă un nou interval înainte", + "Delete interval": "Șterge interval", + "I:C": "I:C", + "ISF": "ISF (factor de sensibilitate la insulina)", + "Combo Bolus": "Bolus extins", + "Difference": "Diferență", + "New time": "Oră nouă", + "Edit Mode": "Mod editare", + "When enabled icon to start edit mode is visible": "La activare, butonul de editare este vizibil", + "Operation": "Operațiune", + "Move": "Mutați", + "Delete": "Ștergeți", + "Move insulin": "Mutați insulina", + "Move carbs": "Mutați carbohidrații", + "Remove insulin": "Ștergeți insulina", + "Remove carbs": "Ștergeți carbohidrații", + "Change treatment time to %1 ?": "Schimbați ora tratamentului la %1 ?", + "Change carbs time to %1 ?": "Schimbați ora carbohidraților la %1 ?", + "Change insulin time to %1 ?": "Schimbați ora insulinei la %1 ?", + "Remove treatment ?": "Ștergeți tratamentul?", + "Remove insulin from treatment ?": "Ștergeți insulina din tratament?", + "Remove carbs from treatment ?": "Ștergeți carbohidrații din tratament?", + "Rendering": "Se desenează", + "Loading OpenAPS data of": "Se încarcă datele OpenAPS pentru", + "Loading profile switch data": "Se încarcă datele de schimbare profil", + "Redirecting you to the Profile Editor to create a new profile.": "Redirecționare către editorul de profil pentru a crea un profil nou.", + "Pump": "Pompă", + "Sensor Age": "Vechimea senzorului", + "Insulin Age": "Vechimea insulinei", + "Temporary target": "Țintă temporară", + "Reason": "Motiv", + "Eating soon": "Mâncare în curând", + "Top": "Deasupra", + "Bottom": "Sub", + "Activity": "Activitate", + "Targets": "Ținte", + "Bolus insulin:": "Insulină bolusată:", + "Base basal insulin:": "Bazala obișnuită:", + "Positive temp basal insulin:": "Bazală temporară marită:", + "Negative temp basal insulin:": "Bazală temporară micșorată:", + "Total basal insulin:": "Bazală totală:", + "Total daily insulin:": "Insulină totală zilnică:", + "Unable to save Role": "Rolul nu poate fi salvat", + "Unable to delete Role": "Imposibil de șters Rolul", + "Database contains %1 roles": "Baza de date conține %1 rol(uri)", + "Edit Role": "Editează Rolul", + "admin, school, family, etc": "administrator, școală, familie, etc", + "Permissions": "Permisiuni", + "Are you sure you want to delete: ": "Confirmați ștergerea: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Fiecare rol va avea cel puțin o permisiune. Permisiunea * este totală, permisiunile sunt ierarhice utilizând : pe post de separator.", + "Add new Role": "Adaugă un Rol nou", + "Roles - Groups of People, Devices, etc": "Roluri - Grupuri de persoane, dispozitive, etc", + "Edit this role": "Editează acest rol", + "Admin authorized": "Autorizat de admin", + "Subjects - People, Devices, etc": "Subiecte - Persoane, dispozitive, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Fiecare subiect va avea o cheie unică de acces și unul sau mai multe roluri. Apăsați pe cheia de acces pentru a vizualiza subiectul selectat, acest link poate fi distribuit.", + "Add new Subject": "Adaugă un Subiect nou", + "Unable to save Subject": "Imposibil de salvat Subiectul", + "Unable to delete Subject": "Imposibil de șters Subiectul", + "Database contains %1 subjects": "Baza de date are %1 subiecți", + "Edit Subject": "Editează Subiectul", + "person, device, etc": "persoană, dispozitiv, etc", + "role1, role2": "Rol1, Rol2", + "Edit this subject": "Editează acest subiect", + "Delete this subject": "Șterge acest subiect", + "Roles": "Roluri", + "Access Token": "Cheie de acces", + "hour ago": "oră în trecut", + "hours ago": "ore în trecut", + "Silence for %1 minutes": "Silențios pentru %1 minute", + "Check BG": "Verificați glicemia", + "BASAL": "BAZALĂ", + "Current basal": "Bazală curentă", + "Sensitivity": "Sensibilitate la insulină", + "Current Carb Ratio": "Raport Insulină:Carbohidrați (ICR)", + "Basal timezone": "Fus orar pentru bazală", + "Active profile": "Profilul activ", + "Active temp basal": "Bazală temporară activă", + "Active temp basal start": "Start bazală temporară activă", + "Active temp basal duration": "Durata bazalei temporare active", + "Active temp basal remaining": "Rest de bazală temporară activă", + "Basal profile value": "Valoarea profilului bazalei", + "Active combo bolus": "Bolus combinat activ", + "Active combo bolus start": "Start bolus combinat activ", + "Active combo bolus duration": "Durată bolus combinat activ", + "Active combo bolus remaining": "Rest de bolus combinat activ", + "BG Delta": "Variația glicemiei", + "Elapsed Time": "Timp scurs", + "Absolute Delta": "Variația absolută", + "Interpolated": "Interpolat", + "BWP": "BWP", + "Urgent": "Urgent", + "Warning": "Atenție", + "Info": "Informație", + "Lowest": "Cea mai mică", + "Snoozing high alarm since there is enough IOB": "Amână alarma de hiper deoarece este suficientă insulină activă IOB", + "Check BG, time to bolus?": "Verifică glicemia, poate este necesar un bolus?", + "Notice": "Notificare", + "required info missing": "lipsă informații necesare", + "Insulin on Board": "Insulină activă (IOB)", + "Current target": "Țintă curentă", + "Expected effect": "Efect presupus", + "Expected outcome": "Rezultat așteptat", + "Carb Equivalent": "Echivalență în carbohidrați", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Insulină în exces %1U mai mult decât este necesar pentru a atinge ținta inferioară, fără a ține cont de carbohidrați", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Insulină în exces %1U mai mult decât este necesar pentru a atinge ținta inferioară, ASIGURAȚI-VĂ CĂ INSULINA ACTIVĂ ESTE ACOPERITĂ DE CARBOHIDRAȚI", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U de insulină activă e în plus pentru a atinge ținta inferioară, bazala este prea mare?", + "basal adjustment out of range, give carbs?": "ajustarea bazalei duce în afara intervalului țintă. Suplimentați carbohirații?", + "basal adjustment out of range, give bolus?": "ajustarea bazalei duce în afara intervalului țintă. Suplimentați insulina?", + "above high": "peste ținta superioară", + "below low": "sub ținta inferioară", + "Projected BG %1 target": "Glicemie estimată %1 în țintă", + "aiming at": "ținta este", + "Bolus %1 units": "Bolus de %1 unități", + "or adjust basal": "sau ajustează bazala", + "Check BG using glucometer before correcting!": "Verifică glicemia cu glucometrul înainte de a face o corecție!", + "Basal reduction to account %1 units:": "Reducere bazală pentru a compensa %1 unități:", + "30m temp basal": "bazală temporară de 30 minute", + "1h temp basal": "bazală temporară de 1 oră", + "Cannula change overdue!": "Depășire termen schimbare canulă!", + "Time to change cannula": "Este vremea să schimbați canula", + "Change cannula soon": "În curând trebuie să schimbați canula", + "Cannula age %1 hours": "Vechimea canulei este %1 ore", + "Inserted": "Inserat", + "CAGE": "VC", + "COB": "COB", + "Last Carbs": "Ultimii carbohidrați", + "IAGE": "VI", + "Insulin reservoir change overdue!": "Termenul de schimbare a rezervorului de insulină a fost depășit!", + "Time to change insulin reservoir": "Este timpul pentru schimbarea rezervorului de insulină", + "Change insulin reservoir soon": "Rezervorul de insulină trebuie schimbat în curând", + "Insulin reservoir age %1 hours": "Vârsta reservorului de insulină %1 ore", + "Changed": "Schimbat", + "IOB": "IOB", + "Careportal IOB": "IOB în Careportal", + "Last Bolus": "Ultimul bolus", + "Basal IOB": "IOB bazală", + "Source": "Sursă", + "Stale data, check rig?": "Date învechite, verificați uploaderul!", + "Last received:": "Ultimile date:", + "%1m ago": "%1 min in urmă", + "%1h ago": "%1 ore in urmă", + "%1d ago": "%1d zile in urmă", + "RETRO": "VECHI", + "SAGE": "VS", + "Sensor change/restart overdue!": "Depășire termen schimbare/restart senzor!", + "Time to change/restart sensor": "Este timpul pentru schimbarea/repornirea senzorului", + "Change/restart sensor soon": "În curând trebuie să schimbați/restartați senzorul", + "Sensor age %1 days %2 hours": "Senzor vechi de %1 zile și %2 ore", + "Sensor Insert": "Inserția senzorului", + "Sensor Start": "Pornirea senzorului", + "days": "zile", + "Insulin distribution": "Distribuția de insulină", + "To see this report, press SHOW while in this view": "Pentru a vedea acest raport, apăsați butonul Arată", + "AR2 Forecast": "Predicție AR2", + "OpenAPS Forecasts": "Predicții OpenAPS", + "Temporary Target": "Țintă Temporară", + "Temporary Target Cancel": "Renunțare la Ținta Temporară", + "OpenAPS Offline": "OpenAPS deconectat", + "Profiles": "Profile", + "Time in fluctuation": "Timp în fluctuație", + "Time in rapid fluctuation": "Timp în fluctuație rapidă", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Aceasta este doar o aproximare brută, care poate fi foarte imprecisă și nu ține loc de testare capilară. Formula matematică folosită este luată din:", + "Filter by hours": "Filtrare pe ore", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Timpul în fluctuație și timpul în fluctuație rapidă măsoară procentul de timp, din perioada examinată, în care glicemia din sânge a avut o variație relativ rapidă sau rapidă. Valorile mici sunt de preferat.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Schimbarea medie totală zilnică este suma valorilor absolute ale tuturor excursiilor glicemice din perioada examinată, împărțite la numărul de zile. Valorile mici sunt de preferat.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Variația media orară este suma valorilor absolute ale tuturor excursiilor glicemice din perioada examinată, împărțite la numărul de ore din aceeași perioadă. Valorile mici sunt de preferat.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "RMS în afara tintelor se calculează prin insumarea tuturor ridicărilor la pătrat a distanțelor din afara intervalului pentru toate valorile de glicemie din perioada examinată, care se impart apoi la numarul lor si ulterior se calculează rădăcina pătrată a acestui rezultat. Această măsurătoare este similară cu procentajul în interval, dar cu greutate mai mare pe valorile mai mari. Valorile mai mici sunt mai bune.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">poate fi găsit aici.", + "Mean Total Daily Change": "Variația medie totală zilnică", + "Mean Hourly Change": "Variația medie orară", + "FortyFiveDown": "scădere ușoară", + "FortyFiveUp": "creștere ușoară", + "Flat": "stabil", + "SingleUp": "creștere", + "SingleDown": "scădere", + "DoubleDown": "scădere rapidă", + "DoubleUp": "creștere rapidă", + "virtAsstUnknown": "Acea valoare este necunoscută în acest moment. Te rugăm să consulți site-ul Nightscout pentru mai multe detalii.", + "virtAsstTitleAR2Forecast": "Predicție AR2", + "virtAsstTitleCurrentBasal": "Bazala actuală", + "virtAsstTitleCurrentCOB": "COB actuali", + "virtAsstTitleCurrentIOB": "IOB actuală", + "virtAsstTitleLaunch": "Bine ați venit la Nightscout", + "virtAsstTitleLoopForecast": "Prognoză buclă", + "virtAsstTitleLastLoop": "Ultima buclă", + "virtAsstTitleOpenAPSForecast": "Predicție OpenAPS", + "virtAsstTitlePumpReservoir": "Insulină rămasă", + "virtAsstTitlePumpBattery": "Baterie pompă", + "virtAsstTitleRawBG": "Valorile primară a glicemiei actuale", + "virtAsstTitleUploaderBattery": "Baterie telefon uploader", + "virtAsstTitleCurrentBG": "Glicemie actuală", + "virtAsstTitleFullStatus": "Stare completă", + "virtAsstTitleCGMMode": "Mod CGM", + "virtAsstTitleCGMStatus": "Stare CGM", + "virtAsstTitleCGMSessionAge": "Vechime sesiune CGM", + "virtAsstTitleCGMTxStatus": "Stare transmițător CGM", + "virtAsstTitleCGMTxAge": "Vârsta transmițător CGM", + "virtAsstTitleCGMNoise": "Zgomot CGM", + "virtAsstTitleDelta": "Variația glicemiei", + "virtAsstStatus": "%1 și %2 din %3.", + "virtAsstBasal": "%1 bazala curentă este %2 unități pe oră", + "virtAsstBasalTemp": "%1 bazala temporară de %2 unități pe oră se va termina la %3", + "virtAsstIob": "și mai aveți %1 insulină activă.", + "virtAsstIobIntent": "Aveți %1 insulină activă", + "virtAsstIobUnits": "%1 unități", + "virtAsstLaunch": "Ce doriți să verificați pe Nightscout?", + "virtAsstPreamble": "Al tau", + "virtAsstPreamble3person": "%1 are ", + "virtAsstNoInsulin": "fără", + "virtAsstUploadBattery": "Bateria uploaderului este la %1", + "virtAsstReservoir": "Mai aveți %1 unități rămase", + "virtAsstPumpBattery": "Bateria pompei este la %1 %2", + "virtAsstUploaderBattery": "Bateria telefonului este la %1", + "virtAsstLastLoop": "Ultima decizie loop implementată cu succes a fost %1", + "virtAsstLoopNotAvailable": "Extensia loop pare a fi dezactivată", + "virtAsstLoopForecastAround": "Potrivit previziunii date de loop se estimează că vei fi pe la %1 pentru următoarele %2", + "virtAsstLoopForecastBetween": "Potrivit previziunii date de loop se estimează că vei fi între %1 și %2 pentru următoarele %3", + "virtAsstAR2ForecastAround": "Potrivit previziunii AR2 se estimează că vei fi pe la %1 in următoarele %2", + "virtAsstAR2ForecastBetween": "Potrivit previziunii AR2 se estimează că vei fi intre %1 și %2 in următoarele %3", + "virtAsstForecastUnavailable": "Estimarea este imposibilă pe baza datelor disponibile", + "virtAsstRawBG": "Valorile primară a glicemiei este %1", + "virtAsstOpenAPSForecast": "Glicemia estimată de OpenAPS este %1", + "virtAsstCob3person": "%1 are %2 carbohidrați la bord", + "virtAsstCob": "Ai %1 carbohidrați la bord", + "virtAsstCGMMode": "Modul tău CGM a fost %1 din %2.", + "virtAsstCGMStatus": "Starea CGM-ului tău a fost de %1 din %2.", + "virtAsstCGMSessAge": "Sesiunea dvs. CGM a fost activă de %1 zile și %2 ore.", + "virtAsstCGMSessNotStarted": "Nu există nici o sesiune activă CGM în acest moment.", + "virtAsstCGMTxStatus": "Starea transmițătorului CGM a fost %1 din %2.", + "virtAsstCGMTxAge": "Transmițătorul dvs. CGM are %1 zile vechime.", + "virtAsstCGMNoise": "Zgomotul CGM a fost %1 din %2.", + "virtAsstCGMBattOne": "Bateria CGM-ului a fost de %1 volți din %2.", + "virtAsstCGMBattTwo": "Nivelurile bateriei CGM au fost de %1 volți și %2 volți din %3.", + "virtAsstDelta": "Variatia a fost %1 între %2 și %3.", + "virtAsstDeltaEstimated": "Variația estimată a fost %1 intre %2 si %3.", + "virtAsstUnknownIntentTitle": "Intenție necunoscută", + "virtAsstUnknownIntentText": "Îmi pare rău, nu ştiu ce doriţi.", + "Fat [g]": "Grăsimi [g]", + "Protein [g]": "Proteine [g]", + "Energy [kJ]": "Energie [kJ]", + "Clock Views:": "Vizualizări ceas:", + "Clock": "Ceas", + "Color": "Culoare", + "Simple": "Simplu", + "TDD average": "Media Dozei Zilnice Totale de insulină (TDD)", + "Bolus average": "Media bolusului", + "Basal average": "Media bazalei", + "Base basal average:": "Media bazalei de bază:", + "Carbs average": "Media carbohidraților", + "Eating Soon": "Mâncare în curând", + "Last entry {0} minutes ago": "Ultima înregistrare acum {0} minute", + "change": "schimbare", + "Speech": "Vorbire", + "Target Top": "Țintă superioară", + "Target Bottom": "Țintă inferioară", + "Canceled": "Anulat", + "Meter BG": "Glicemie din glucometru", + "predicted": "estimat", + "future": "viitor", + "ago": "în trecut", + "Last data received": "Ultimele date primite", + "Clock View": "Vedere tip ceas", + "Protein": "Proteine", + "Fat": "Grăsimi", + "Protein average": "Media proteinelor", + "Fat average": "Media grăsimilor", + "Total carbs": "Total carbohidrați", + "Total protein": "Total proteine", + "Total fat": "Total grăsimi", + "Database Size": "Dimensiunea bazei de date", + "Database Size near its limits!": "Dimensiunea bazei de date se apropie de limita!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Dimensiunea bazei de date este %1 MB din %2 MB. Vă rugăm să faceți o copie de rezervă și să curățați baza de date!", + "Database file size": "Dimensiunea bazei de date", + "%1 MiB of %2 MiB (%3%)": "%1 MB din %2 MB (%3%)", + "Data size": "Dimensiune date", + "virtAsstDatabaseSize": "%1 MB. Aceasta este %2% din spațiul disponibil pentru baza de date.", + "virtAsstTitleDatabaseSize": "Dimensiunea bazei de date", + "Carbs/Food/Time": "Carbohidrati/Mâncare/Timp", + "You have administration messages": "Aveți mesaje de administrare", + "Admin messages in queue": "Mesajele administratorului în așteptare", + "Queue empty": "Coadă goală", + "There are no admin messages in queue": "Nu există mesaje de administrator în coadă", + "Please sign in using the API_SECRET to see your administration messages": "Te rugăm să te autentifici folosind API_SECRET pentru a vedea mesajele de administrare", + "Reads enabled in default permissions": "Citiri activate în permisiunile implicite", + "Data reads enabled": "Citire date activată", + "Data writes enabled": "Scriere date activată", + "Data writes not enabled": "Scriere date neactivată", + "Color prediction lines": "Culoare linii de predicție", + "Release Notes": "Note asupra ediției", + "Check for Updates": "Verificați dacă există actualizări", + "Open Source": "Cod sursă public", + "Nightscout Info": "Informații Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Scopul principal al Loopalyzer este de a vizualiza modul de funcționare a sistemului Loop în buclă închisă. Poate funcționa și cu alte configurații, atât cu buclă închisă, cât și deschisă, și non-buclă. Cu toate acestea, în funcție de uploaderul pe care îl folosiți, cât de frecvent poate captura datele și încărca, și modul în care este capabil să completeze datele care lipsesc, unele grafice pot avea lipsuri sau chiar să fie complet goale. Asigură-te întotdeauna că graficele arată rezonabil. Cel mai bine este să vizualizezi câte o zi pe rand si apoi sa derulezi cu un numar de zile.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer include o funcție de schimbare de timp (Time Shift). Dacă aveți, de exemplu, micul dejun la ora 07:00 într-o zi și la ora 08:00 în ziua următoare, curba glicemiei medii pe aceste două zile va apărea cel mai probabil aplatizata și nu va arăta răspunsul real după un mic dejun. Functia Time Shift va calcula timpul mediu în care aceste mese au fost mâncate şi apoi va schimba toate datele (carbohidraţi, insulină, bazală etc.) în ambele zile, diferenţa de timp corespunzătoare, astfel încât ambele mese să se alinieze cu ora medie de începere a mesei.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "În acest exemplu, toate datele din prima zi sunt împinse cu 30 de minute înainte şi toate datele din a doua zi cu 30 de minute înapoi în timp, astfel încât sa apară că aţi avut micul dejun la ora 07:30 în ambele zile. Acest lucru vă permite să vedeţi răspunsul mediu real al glicemiei după o masă.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Deplasarea timpului (Time Shift) evidenţiază cu gri perioada de după ora medie de incepere a mesei, pe durata DIA (Durata de Acţiune a Insulinei). Deoarece toate valorile de date din toată ziua sunt deplasate, curbele din afara zonei gri pot să nu fie prea exacte.", + "Note that time shift is available only when viewing multiple days.": "Reţineţi că deplasarea timpului (Time Shift) este disponibilă doar când vizualizaţi mai multe zile.", + "Please select a maximum of two weeks duration and click Show again.": "Te rugăm să selectezi maxim două săptămâni și să apeși din nou pe Arată.", + "Show profiles table": "Arată tabelul profilelor", + "Show predictions": "Arată predicții", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timp intre mese mai mari de %1 g de carbohidrați consumați între %2 și %3", + "Previous": "Precedent", + "Previous day": "Ziua precedentă", + "Next day": "Ziua următoare", + "Next": "Următorul", + "Temp basal delta": "Variația bazalei temporare", + "Authorized by token": "Autorizat prin cheie de acces", + "Auth role": "Rolul de autentificare", + "view without token": "vizualizare fără cheie de acces", + "Remove stored token": "Elimină cheia de acces stocată", + "Weekly Distribution": "Distribuție săptămânală", + "Failed authentication": "Autentificare eșuată", + "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?": "Un dispozitiv cu adresa IP %1 a încercat autentificarea la Nightscout cu datele de autentificare greșite. Verificați dacă o instanta de încărcare a fost configurata cu API_SECRET sau token greșit?", + "Default (with leading zero and U)": "Implicit (zero-uri şi U)", + "Concise (with U, without leading zero)": "Concis (cu U, fără zero-uri)", + "Minimal (without leading zero and U)": "Minimal (fără zero-uri și U)", + "Small Bolus Display": "Afișaj bolus mic", + "Large Bolus Display": "Afișaj bolus mare", + "Bolus Display Threshold": "Prag de afişare bolus", + "%1 U and Over": "%1 U și peste", + "Event repeated %1 times.": "Eveniment repetat de %1 ori.", + "minutes": "minute", + "Last recorded %1 %2 ago.": "Ultima înregistrare %1 %2 în urmă.", + "Security issue": "Problemă de securitate", + "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.": "API_SECRET slab detectat. Te rugăm să folosești un amestec de litere mari și litere mici, numere și caractere non-alfanumerice, cum ar fi !#%&/ pentru a reduce riscul accesului neautorizat. Lungimea minimă a API_SECRET este de 12 caractere.", + "less than 1": "mai puțin de 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Parola MongoDB și API_SECRET se potrivesc. Acesta este un lucru foarte rău. Vă rugăm să le schimbați pe ambele și să nu reutilizați parolele în sistem." +} diff --git a/translations/ru_RU.json b/translations/ru_RU.json new file mode 100644 index 00000000000..c1e9e51ce74 --- /dev/null +++ b/translations/ru_RU.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Прослушивание порта", + "Mo": "Пон", + "Tu": "Вт", + "We": "Ср", + "Th": "Чт", + "Fr": "Пт", + "Sa": "Сб", + "Su": "Вс", + "Monday": "Понедельник", + "Tuesday": "Вторник", + "Wednesday": "Среда", + "Thursday": "Четверг", + "Friday": "Пятница", + "Saturday": "Суббота", + "Sunday": "Воскресенье", + "Category": "Категория", + "Subcategory": "Подкатегория", + "Name": "Имя", + "Today": "Сегодня", + "Last 2 days": "Прошедшие 2 дня", + "Last 3 days": "Прошедшие 3 дня", + "Last week": "Прошедшая неделя", + "Last 2 weeks": "Прошедшие 2 недели", + "Last month": "Прошедший месяц", + "Last 3 months": "Прошедшие 3 месяца", + "From": "С", + "To": "По", + "Notes": "Примечания", + "Food": "Еда", + "Insulin": "Инсулин", + "Carbs": "Углеводы", + "Notes contain": "Примечания содержат", + "Target BG range bottom": "Нижний порог целевых значений СК", + "top": "Верхний", + "Show": "Показать", + "Display": "Показывать", + "Loading": "Загрузка", + "Loading profile": "Загрузка профиля", + "Loading status": "Загрузка состояния", + "Loading food database": "Загрузка БД продуктов", + "not displayed": "Не отображено", + "Loading CGM data of": "Загрузка данных мониторинга", + "Loading treatments data of": "Загрузка данных терапии", + "Processing data of": "Обработка данных от", + "Portion": "Порция", + "Size": "Объем", + "(none)": "(нет)", + "None": "Нет", + "": "<нет>", + "Result is empty": "Результата нет ", + "Day to day": "По дням", + "Week to week": "По неделям", + "Daily Stats": "Ежедневная статистика", + "Percentile Chart": "Процентильная диаграмма", + "Distribution": "Распределение", + "Hourly stats": "Почасовая статистика", + "netIOB stats": "статистика нетто активн инс netIOB", + "temp basals must be rendered to display this report": "для этого отчета требуется прорисовка врем базала", + "No data available": "Нет данных", + "Low": "Низкая ГК", + "In Range": "В диапазоне", + "Period": "Период", + "High": "Высокая ГК", + "Average": "Cреднее значение", + "Low Quartile": "Нижняя четверть", + "Upper Quartile": "Верхняя четверть", + "Quartile": "Четверть", + "Date": "Дата", + "Normal": "Норма", + "Median": "Усредненный", + "Readings": "Значения", + "StDev": "Стандартное отклонение", + "Daily stats report": "Суточная статистика", + "Glucose Percentile report": "Процентильная ГК", + "Glucose distribution": "Распределение ГК", + "days total": "всего дней", + "Total per day": "всего за сутки", + "Overall": "Суммарно", + "Range": "Диапазон", + "% of Readings": "% измерений", + "# of Readings": "кол-во измерений", + "Mean": "Среднее значение", + "Standard Deviation": "Стандартное отклонение", + "Max": "Макс", + "Min": "Мин", + "A1c estimation*": "Ожидаемый HbA1c*", + "Weekly Success": "Итоги недели", + "There is not sufficient data to run this report. Select more days.": "Для этого отчета недостаточно данных. Выберите больше дней.", + "Using stored API secret hash": "Применение сохраненного пароля API", + "No API secret hash stored yet. You need to enter API secret.": "Пароля API нет в памяти. Введите пароль API", + "Database loaded": "База данных загружена", + "Error: Database failed to load": "Ошибка: Не удалось загрузить базу данных", + "Error": "Ошибка", + "Create new record": "Создайте новую запись", + "Save record": "Сохраните запись", + "Portions": "Порции", + "Unit": "Единица", + "GI": "гл индекс ГИ", + "Edit record": "Редактировать запись", + "Delete record": "Стереть запись", + "Move to the top": "Переместить наверх", + "Hidden": "Скрыт", + "Hide after use": "Скрыть после использования", + "Your API secret must be at least 12 characters long": "Ваш пароль API должен иметь не менее 12 знаков", + "Bad API secret": "Плохой пароль API", + "API secret hash stored": "Хэш пароля API сохранен", + "Status": "Статус", + "Not loaded": "Не загружено", + "Food Editor": "Редактор продуктов", + "Your database": "Ваша база данных ", + "Filter": "Фильтр", + "Save": "Сохранить", + "Clear": "Очистить", + "Record": "Запись", + "Quick picks": "Быстрый отбор", + "Show hidden": "Показать скрытые", + "Your API secret or token": "Ваш пароль API или код доступа ", + "Remember this device. (Do not enable this on public computers.)": "Запомнить это устройство (Не применяйте в общем доступе)", + "Treatments": "Терапия", + "Time": "Время", + "Event Type": "Тип события", + "Blood Glucose": "Гликемия", + "Entered By": "Внесено через", + "Delete this treatment?": "Удалить это событие?", + "Carbs Given": "Дано углеводов", + "Insulin Given": "Введено инсулина", + "Event Time": "Время события", + "Please verify that the data entered is correct": "Проверьте правильность вводимых данных", + "BG": "ГК", + "Use BG correction in calculation": "При расчете учитывать коррекцию ГК", + "BG from CGM (autoupdated)": "ГК с сенсора (автообновление)", + "BG from meter": "ГК по глюкометру", + "Manual BG": "ручной ввод ГК", + "Quickpick": "Быстрый отбор", + "or": "или", + "Add from database": "Добавить из базы данных", + "Use carbs correction in calculation": "Пользоваться коррекцией на углеводы при расчете", + "Use COB correction in calculation": "Учитывать активные углеводы COB при расчете", + "Use IOB in calculation": "Учитывать активный инсулин IOB при расчете", + "Other correction": "Иная коррекция", + "Rounding": "Округление", + "Enter insulin correction in treatment": "Внести коррекцию инсулина в терапию", + "Insulin needed": "Требуется инсулин", + "Carbs needed": "Требуются углеводы", + "Carbs needed if Insulin total is negative value": "Требуются углеводы если суммарный инсулин отрицательная величина", + "Basal rate": "Скорость базала", + "60 minutes earlier": "на 60 минут ранее", + "45 minutes earlier": "на 45 минут ранее", + "30 minutes earlier": "на 30 минут ранее", + "20 minutes earlier": "на 20 минут ранее", + "15 minutes earlier": "на 15 минут ранее", + "Time in minutes": "Время в минутах", + "15 minutes later": "на 15 мин позже", + "20 minutes later": "на 20 мин позже", + "30 minutes later": "на 30 мин позже", + "45 minutes later": "на 45 мин позже", + "60 minutes later": "на 60 мин позже", + "Additional Notes, Comments": "Дополнительные примечания", + "RETRO MODE": "режим РЕТРО", + "Now": "Сейчас", + "Other": "Другое", + "Submit Form": "Ввести форму", + "Profile Editor": "Редактор профиля", + "Reports": "Отчеты", + "Add food from your database": "Добавить продукт из вашей базы данных", + "Reload database": "Перезагрузить базу данных", + "Add": "Добавить", + "Unauthorized": "Не авторизовано", + "Entering record failed": "Ввод записи не состоялся", + "Device authenticated": "Устройство авторизовано", + "Device not authenticated": "Устройство не авторизовано", + "Authentication status": "Статус авторизации", + "Authenticate": "Авторизуйте", + "Remove": "Удалите", + "Your device is not authenticated yet": "Ваше устройство еще не авторизовано ", + "Sensor": "Сенсор", + "Finger": "Палец", + "Manual": "Вручную", + "Scale": "Масштаб", + "Linear": "Линейный", + "Logarithmic": "Логарифмический", + "Logarithmic (Dynamic)": "Логарифмический (Динамический)", + "Insulin-on-Board": "Активный инсулин IOB", + "Carbs-on-Board": "Активные углеводы COB", + "Bolus Wizard Preview": "Калькулятор болюса", + "Value Loaded": "Величина загружена", + "Cannula Age": "Катетеру", + "Basal Profile": "Профиль Базала", + "Silence for 30 minutes": "Тишина на 30 минут", + "Silence for 60 minutes": "Тишина 60 минут", + "Silence for 90 minutes": "Тишина 90 минут", + "Silence for 120 minutes": "Тишина 120 минут", + "Settings": "Настройки", + "Units": "Единицы", + "Date format": "Формат даты", + "12 hours": "12 часов", + "24 hours": "24 часа", + "Log a Treatment": "Записать терапию", + "BG Check": "Контроль ГК", + "Meal Bolus": "Болюс на еду", + "Snack Bolus": "Болюс на перекус", + "Correction Bolus": "Болюс на коррекцию", + "Carb Correction": "Углеводы на коррекцию", + "Note": "Примечания", + "Question": "Вопрос", + "Exercise": "Нагрузка", + "Pump Site Change": "Смена места катетера помпы", + "CGM Sensor Start": "Старт сенсора мониторинга", + "CGM Sensor Stop": "Останов сенсора мониторинга", + "CGM Sensor Insert": "Установка сенсора мониторинга", + "Sensor Code": "Код сенсора", + "Transmitter ID": "ID трансмиттера", + "Dexcom Sensor Start": "Старт сенсора Dexcom", + "Dexcom Sensor Change": "Замена сенсора Декском", + "Insulin Cartridge Change": "Замена картриджа инсулина", + "D.A.D. Alert": "Сигнал служебной собаки", + "Glucose Reading": "Значение ГК", + "Measurement Method": "Способ замера", + "Meter": "Глюкометр", + "Amount in grams": "Количество в граммах", + "Amount in units": "Количество в ед.", + "View all treatments": "Показать все события", + "Enable Alarms": "Активировать сигналы", + "Pump Battery Change": "Замена батареи помпы", + "Pump Battery Age": "Батарея помпы работает", + "Pump Battery Low Alarm": "Внимание! низкий заряд батареи помпы", + "Pump Battery change overdue!": "Пропущен срок замены батареи!", + "When enabled an alarm may sound.": "При активации может звучать сигнал", + "Urgent High Alarm": "Внимание: высокая гликемия ", + "High Alarm": "Высокая гликемия", + "Low Alarm": "Низкая гликемия", + "Urgent Low Alarm": "Внимание: низкая гликемия", + "Stale Data: Warn": "Предупреждение: старые данные", + "Stale Data: Urgent": "Внимание: старые данные", + "mins": "мин", + "Night Mode": "Режим: ночь", + "When enabled the page will be dimmed from 10pm - 6am.": "При активации страница затемнена с 22.00 до 06.00", + "Enable": "Активировать", + "Show Raw BG Data": "Показывать необработанные данные RAW", + "Never": "Никогда", + "Always": "Всегда", + "When there is noise": "Когда есть шумовой фон", + "When enabled small white dots will be displayed for raw BG data": "При активации данные RAW будут видны как мелкие белые точки", + "Custom Title": "Произвольное название", + "Theme": "Тема", + "Default": "По умолчанию", + "Colors": "Цвета", + "Colorblind-friendly colors": "Цветовая гамма для людей с нарушениями восприятия цвета", + "Reset, and use defaults": "Сбросить и использовать настройки по умолчанию", + "Calibrations": "Калибровки", + "Alarm Test / Smartphone Enable": "Проверка зв. уведомлений / смартфон активен", + "Bolus Wizard": "Калькулятор болюса", + "in the future": "в будущем", + "time ago": "времени назад", + "hr ago": "час назад", + "hrs ago": "часов назад", + "min ago": "мин назад", + "mins ago": "минут назад", + "day ago": "дн назад", + "days ago": "дней назад", + "long ago": "давно", + "Clean": "Чисто", + "Light": "Слабый", + "Medium": "Средний", + "Heavy": "Сильный", + "Treatment type": "Вид события", + "Raw BG": "необработанные данные ГК", + "Device": "Устройство", + "Noise": "Шумовой фон", + "Calibration": "Калибровка", + "Show Plugins": "Показать расширения", + "About": "О приложении", + "Value in": "Значения в", + "Carb Time": "Времени до приема углеводов", + "Language": "Язык", + "Add new": "Добавить новый", + "g": "г", + "ml": "мл", + "pcs": "шт", + "Drag&drop food here": "Перетащите еду сюда", + "Care Portal": "Портал терапии", + "Medium/Unknown": "Средний/Неизвестный", + "IN THE FUTURE": "В БУДУЩЕМ", + "Order": "Сортировать", + "oldest on top": "Старые наверху", + "newest on top": "Новые наверху", + "All sensor events": "Все события сенсора", + "Remove future items from mongo database": "Удалить будущие данные из базы Монго", + "Find and remove treatments in the future": "Найти и удалить будущие назначения", + "This task find and remove treatments in the future.": "Эта опция найдет и удалит данные из будущего", + "Remove treatments in the future": "Удалить назначения из будущего", + "Find and remove entries in the future": "Найти и удалить записи в будущем", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Эта опция найдет и удалит данные сенсора созданные загрузчиком с неверными датой/временем", + "Remove entries in the future": "Удалить данные из будущего", + "Loading database ...": "Загрузка базы данных", + "Database contains %1 future records": "База данных содержит %1 записей в будущем", + "Remove %1 selected records?": "Удалить %1 выбранных записей?", + "Error loading database": "Ошибка загрузки базы данных", + "Record %1 removed ...": "запись %1 удалена", + "Error removing record %1": "Ошибка удаления записи %1", + "Deleting records ...": "Записи удаляются", + "%1 records deleted": "%1 записей удалено", + "Clean Mongo status database": "Очистить статус базы данных Mongo", + "Delete all documents from devicestatus collection": "Стереть все документы из коллекции статус устройства", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Эта опция удаляет все документы из коллекции статус устройства. Полезно когда состояние батареи загрузчика не обновляется.", + "Delete all documents": "Удалить все документы", + "Delete all documents from devicestatus collection?": "Удалить все документы коллекции статус устройства?", + "Database contains %1 records": "База данных содержит %1 записей", + "All records removed ...": "Все записи удалены", + "Delete all documents from devicestatus collection older than 30 days": "Удалить все записи коллекции devicestatus старше 30 дней", + "Number of Days to Keep:": "Оставить дней", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Это удалит все документы коллекции devicestatus которым более 30 дней. Полезно, когда статус батареи не обновляется или обновляется неверно.", + "Delete old documents from devicestatus collection?": "Удалить старыые документы коллекции devicestatus?", + "Clean Mongo entries (glucose entries) database": "Очистить записи данных ГК в базе Mongo", + "Delete all documents from entries collection older than 180 days": "Удалить все документы коллекции entries старше 180 дней ", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Это удалит все документы коллекции entries старше 180 дней. Полезно, когда статус батареи загрузчика должным образом не обновляется", + "Delete old documents": "Удалить старые документы", + "Delete old documents from entries collection?": "Удалить старые документы из коллекции entries?", + "%1 is not a valid number": "%1 не является допустимым значением", + "%1 is not a valid number - must be more than 2": "%1 не является допустимым значением - должно быть больше 2", + "Clean Mongo treatments database": "Очистить базу терапии Mongo", + "Delete all documents from treatments collection older than 180 days": "Удалить все документы коллекции терапии treatments старше 180 дней", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Это удалит все документы коллекции терапии treatments старше 180 дней. Полезно, когда статус батареи загрузчика не обновляется должным образом.", + "Delete old documents from treatments collection?": "Удалить старые документы из коллекции терапии treatments?", + "Admin Tools": "Инструменты администратора", + "Nightscout reporting": "Статистика Nightscout", + "Cancel": "Отмена", + "Edit treatment": "Редактировать событие", + "Duration": "Продолжительность", + "Duration in minutes": "Продолжительность в мин", + "Temp Basal": "Временный базал", + "Temp Basal Start": "Начало временного базала", + "Temp Basal End": "Окончание временного базала", + "Percent": "Процент", + "Basal change in %": "Изменение базала в %", + "Basal value": "Величина базала", + "Absolute basal value": "Абсолютная величина базала", + "Announcement": "Оповещение", + "Loading temp basal data": "Загрузка данных временного базала", + "Save current record before changing to new?": "Сохранить текущие данные перед переходом к новым?", + "Profile Switch": "Изменить профиль", + "Profile": "Профиль", + "General profile settings": "Общие настройки профиля", + "Title": "Название", + "Database records": "Записи в базе данных", + "Add new record": "Добавить новую запись", + "Remove this record": "Удалить эту запись", + "Clone this record to new": "Клонировать эту запись в новую", + "Record valid from": "Запись действительна от", + "Stored profiles": "Сохраненные профили", + "Timezone": "Часовой пояс", + "Duration of Insulin Activity (DIA)": "Время действия инсулина (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Отражает типичную продолжительность действия инсулина. Зависит от пациента и от типа инсулина. Обычно 3-4 часа для большинства помповых инсулинов и большинства пациентов", + "Insulin to carb ratio (I:C)": "Соотношение инсулин/углеводы I:C", + "Hours:": "час:", + "hours": "час", + "g/hour": "г/час", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "г углеводов на ед инсулина. Соотношение показывает количество гр углеводов компенсируемого каждой единицей инсулина.", + "Insulin Sensitivity Factor (ISF)": "Фактор чувствительности к инсулину ISF", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "мг/дл или ммол/л на единицу инсулина. Насколько меняется СК с каждой единицей инсулина на коррекцию.", + "Carbs activity / absorption rate": "Активность углеводов / скорость усвоения", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "грамм на ед времени. Представляет изменение кол-ва углеводов в организме COB за единицу времени a также количество активных углеводов. Кривая усвоения углеводов менее понятна чем активность инсулина но приблизительно может быть представлена в виде первоначальной задержки с последующей постоянной скоростью поглощения (грамм/час).", + "Basal rates [unit/hour]": "Скорость базала [ед./час]", + "Target BG range [mg/dL,mmol/L]": "Целевой диапазон ГК [mg/dL,mmol/L]", + "Start of record validity": "Начало валидности записей", + "Icicle": "Силуэт сосульки", + "Render Basal": "Отображать базал", + "Profile used": "Используемый профиль", + "Calculation is in target range.": "Расчетная величина в целевом диапазоне.", + "Loading profile records ...": "Загрузка записей профиля", + "Values loaded.": "Данные загружены", + "Default values used.": "Используются значения по умолчанию", + "Error. Default values used.": "Ошибка. Используются значения по умолчанию", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Диапазоны нижних и верхних целевых значений не совпадают. Восстановлены значения по умолчанию.", + "Valid from:": "Действительно с", + "Save current record before switching to new?": "Сохранить текущие записи перед переходом к новым?", + "Add new interval before": "Добавить новый интервал перед этим", + "Delete interval": "Удалить интервал", + "I:C": "Инс:Углев", + "ISF": "Чувств к инс ISF", + "Combo Bolus": "Комбинир болюс", + "Difference": "Разность", + "New time": "Новое время", + "Edit Mode": "Режим редактирования", + "When enabled icon to start edit mode is visible": "При активации видна пиктограмма начать режим редактирования", + "Operation": "Операция", + "Move": "Переместить", + "Delete": "Удалить", + "Move insulin": "Переместить инсулин", + "Move carbs": "Переместить углеводы", + "Remove insulin": "Удалить инсулин", + "Remove carbs": "Удалить углеводы", + "Change treatment time to %1 ?": "Изменить время события на %1 ?", + "Change carbs time to %1 ?": "Изменить время приема углеводов на %1 ?", + "Change insulin time to %1 ?": "Изменить время подачи инсулина на %1 ?", + "Remove treatment ?": "Удалить событие ?", + "Remove insulin from treatment ?": "Удалить инсулин из событий ?", + "Remove carbs from treatment ?": "Удалить углеводы из событий ?", + "Rendering": "Построение графика", + "Loading OpenAPS data of": "Загрузка данных OpenAPS от", + "Loading profile switch data": "Загрузка данных нового профиля", + "Redirecting you to the Profile Editor to create a new profile.": "Переход к редактору профиля для создания нового", + "Pump": "Помпа", + "Sensor Age": "Сенсору", + "Insulin Age": "Инсулину", + "Temporary target": "Временная цель", + "Reason": "Основание", + "Eating soon": "Ожидаемый прием пищи", + "Top": "Верх", + "Bottom": "Низ", + "Activity": "Нагрузка", + "Targets": "Цели", + "Bolus insulin:": "Болюсный инсулин", + "Base basal insulin:": "Профильный базальный инсулин", + "Positive temp basal insulin:": "Положит знач временн базал инс ", + "Negative temp basal insulin:": "Отриц знач временн базал инс", + "Total basal insulin:": "Всего базал инсулина", + "Total daily insulin:": "Всего суточн базал инсулина", + "Unable to save Role": "Невозможно сохранить Роль", + "Unable to delete Role": "Невозможно удалить Роль", + "Database contains %1 roles": "База данных содержит %1 Ролей", + "Edit Role": "Редактировать Роль", + "admin, school, family, etc": "админ, школа, семья и т д", + "Permissions": "Разрешения", + "Are you sure you want to delete: ": "Подтвердите удаление", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Каждая роль имеет 1 или более разрешений. Разрешение * это подстановочный символ, разрешения это иерархия, использующая : как разделитель.", + "Add new Role": "Добавить новую Роль", + "Roles - Groups of People, Devices, etc": "Роли- группы людей, устройств и т п", + "Edit this role": "Редактировать эту роль", + "Admin authorized": "Разрешено администратором", + "Subjects - People, Devices, etc": "Субъекты - Люди, устройства и т п", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Каждый субъект получает уникальный код доступа и 1 или более ролей. Нажмите на иконку кода доступа чтобы открыть новое окно с выбранным субъектом, эту секретную ссылку можно переслать.", + "Add new Subject": "Добавить нового субъекта", + "Unable to save Subject": "Невозможно сохранить Субъект", + "Unable to delete Subject": "Невозможно удалить Субъект ", + "Database contains %1 subjects": "База данных содержит %1 субъекта/ов", + "Edit Subject": "Редактировать Субъект", + "person, device, etc": "лицо, устройство и т п", + "role1, role2": "Роль1, Роль2", + "Edit this subject": "Редактировать этот субъект", + "Delete this subject": "Удалить этот субъект", + "Roles": "Роли", + "Access Token": "Код доступа", + "hour ago": "час назад", + "hours ago": "часов назад", + "Silence for %1 minutes": "Молчание %1 минут", + "Check BG": "Проверьте гликемию", + "BASAL": "Базал", + "Current basal": "Актуальный базал", + "Sensitivity": "Чувствительность", + "Current Carb Ratio": "Актуальное соотношение инсулин:углеводы I:C", + "Basal timezone": "Часовой пояс базала", + "Active profile": "Действующий профиль", + "Active temp basal": "Активный временный базал", + "Active temp basal start": "Старт актуального временного базала", + "Active temp basal duration": "Длительность актуального временного базала", + "Active temp basal remaining": "Остается актуального временного базала", + "Basal profile value": "Величина базала профиля", + "Active combo bolus": "Активный комбинированный болюс", + "Active combo bolus start": "Старт активного комбинир болюса", + "Active combo bolus duration": "Длительность активного комбинир болюса", + "Active combo bolus remaining": "Остается активного комбинир болюса", + "BG Delta": "Изменение гликемии", + "Elapsed Time": "Прошло времени", + "Absolute Delta": "Абсолютное изменение", + "Interpolated": "Интерполировано", + "BWP": "Калькулятор болюса", + "Urgent": "Внимание", + "Warning": "Осторожно", + "Info": "Информация", + "Lowest": "Самое нижнее", + "Snoozing high alarm since there is enough IOB": "Отключение предупреждение о высоком СК ввиду достаточности инсулина в организме", + "Check BG, time to bolus?": "Проверьте ГК, дать болюс?", + "Notice": "Заметка", + "required info missing": "Отсутствует необходимая информация", + "Insulin on Board": "Активный инсулин (IOB)", + "Current target": "Актуальное целевое значение", + "Expected effect": "Ожидаемый эффект", + "Expected outcome": "Ожидаемый результат", + "Carb Equivalent": "Эквивалент в углеводах", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Избыток инсулина равного %1ед, необходимого для достижения нижнего целевого значения, углеводы не приняты в расчет", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Избыток инсулина, равного %1ед, необходимого для достижения нижнего целевого значения, ПОКРОЙТЕ АКТИВН IOB ИНСУЛИН УГЛЕВОДАМИ", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Для достижения нижнего целевого значения необходимо понизить величину активного инсулина на %1ед, велика база?", + "basal adjustment out of range, give carbs?": "Корректировка базала вне диапазона, добавить углеводов?", + "basal adjustment out of range, give bolus?": "Корректировка базала вне диапазона, добавить болюс?", + "above high": "Выше верхней границы", + "below low": "Ниже нижней границы", + "Projected BG %1 target": "Расчетная целевая гликемия %1", + "aiming at": "цель на", + "Bolus %1 units": "Болюс %1 единиц", + "or adjust basal": "или корректировать базал", + "Check BG using glucometer before correcting!": "Перед корректировкой сверьте ГК с глюкометром", + "Basal reduction to account %1 units:": "Снижение базы из-за %1 единиц болюса", + "30m temp basal": "30 мин врем базал", + "1h temp basal": "1 час врем базал", + "Cannula change overdue!": "Срок замены катетера помпы истек", + "Time to change cannula": "Пора заменить катетер помпы", + "Change cannula soon": "Приближается время замены катетера помпы", + "Cannula age %1 hours": "Катетер помпы проработал %1 час", + "Inserted": "Установлен", + "CAGE": "КатПомп", + "COB": "АктУгл COB", + "Last Carbs": "Прошлые углеводы", + "IAGE": "Инсулину", + "Insulin reservoir change overdue!": "Срок замены резервуара инсулина истек", + "Time to change insulin reservoir": "Наступил срок замены резервуара инсулина", + "Change insulin reservoir soon": "Наступает срок замены резервуара инсулина", + "Insulin reservoir age %1 hours": "Картриджу инсулина %1часов", + "Changed": "Замена произведена", + "IOB": "АктИнс IOB", + "Careportal IOB": "АктИнс на портале терапии", + "Last Bolus": "Прошлый болюс", + "Basal IOB": "АктуальнБазал IOB", + "Source": "Источник", + "Stale data, check rig?": "Старые данные, проверьте загрузчик", + "Last received:": "Получено:", + "%1m ago": "%1 мин назад", + "%1h ago": "%1 час назад", + "%1d ago": "%1d назад", + "RETRO": "ПРОШЛОЕ", + "SAGE": "Сенсор работает", + "Sensor change/restart overdue!": "Рестарт сенсора пропущен", + "Time to change/restart sensor": "Время замены/рестарта сенсора", + "Change/restart sensor soon": "Приближается срок замены/рестарта сенсора", + "Sensor age %1 days %2 hours": "Сенсору %1 дн %2 час", + "Sensor Insert": "Сенсор установлен", + "Sensor Start": "Старт сенсора", + "days": "дней", + "Insulin distribution": "распределение инсулина", + "To see this report, press SHOW while in this view": "чтобы увидеть отчет, нажмите show/показать", + "AR2 Forecast": "прогноз AR2", + "OpenAPS Forecasts": "прогнозы OpenAPS", + "Temporary Target": "Временная цель", + "Temporary Target Cancel": "Отмена временной цели", + "OpenAPS Offline": "OpenAPS вне сети", + "Profiles": "Профили", + "Time in fluctuation": "Время флуктуаций", + "Time in rapid fluctuation": "Время быстрых флуктуаций", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Это приблизительная оценка не заменяющая фактический анализ крови. Используемая формула взята из:", + "Filter by hours": "Почасовая фильтрация", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Время флуктуаций и время быстрых флуктуаций означает % времени в рассматриваемый период в течение которого ГК менялась относительно быстро или просто быстро. Более низкие значения предпочтительней", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Усредненное ежедневное изменение это сумма абсолютных величин всех отклонений ГК в рассматриваемый период, деленная на количество дней. Меньшая величина предпочтительней", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Усредненное часовое изменение это сумма абсолютных величин всех отклонений ГК в рассматриваемый период, деленная на количество часов в этот период. Более низкое предпочтительней", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Среднее квадратичное вне диапазона рассчитывается путем возведения в квадрат дистанции замеров вне диапазона для всех значений гликемии за рассматриваемый период, их суммирования, деления на общее количество и извлечения квадратные корни. Эта методика схожа с расчётом процента значений в диапазоне, но значения за пределами диапазона оказываются весомее. Чем ниже значение, тем лучше.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">здесь.", + "Mean Total Daily Change": "Усредненное изменение за день", + "Mean Hourly Change": "Усредненное изменение за час", + "FortyFiveDown": "незначительное падение", + "FortyFiveUp": "незначительный подъем", + "Flat": "ровный", + "SingleUp": "растет", + "SingleDown": "падает", + "DoubleDown": "быстро падает", + "DoubleUp": "быстрый рост", + "virtAsstUnknown": "В настоящий момент величина неизвестна. Зайдите на сайт Nightscout.", + "virtAsstTitleAR2Forecast": "Прогноз AR2", + "virtAsstTitleCurrentBasal": "Актуальный Базал", + "virtAsstTitleCurrentCOB": "АктивнУгл COB", + "virtAsstTitleCurrentIOB": "АктИнс IOB", + "virtAsstTitleLaunch": "Добро пожаловать в Nightscout", + "virtAsstTitleLoopForecast": "Прогноз Loop", + "virtAsstTitleLastLoop": "Прошлый Loop", + "virtAsstTitleOpenAPSForecast": "Прогноз OpenAPS", + "virtAsstTitlePumpReservoir": "Осталось Инсулина", + "virtAsstTitlePumpBattery": "Батарея помпы", + "virtAsstTitleRawBG": "Актуальн RAW ГК ", + "virtAsstTitleUploaderBattery": "Батарея загрузчика", + "virtAsstTitleCurrentBG": "Актуальная ГК", + "virtAsstTitleFullStatus": "Полная информация о статусе", + "virtAsstTitleCGMMode": "Режим непрерывного мониторинга", + "virtAsstTitleCGMStatus": "Состояние непрерывного мониторинга", + "virtAsstTitleCGMSessionAge": "Текущей сессии мониторинга", + "virtAsstTitleCGMTxStatus": "Статус трансмиттера", + "virtAsstTitleCGMTxAge": "Трансмиттеру", + "virtAsstTitleCGMNoise": "Зашумленность мониторинга", + "virtAsstTitleDelta": "Дельта ГК", + "virtAsstStatus": "%1 и %2 начиная с %3.", + "virtAsstBasal": "%1 текущий базал %2 ед в час", + "virtAsstBasalTemp": "%1 временный базал %2 ед в час закончится через %3", + "virtAsstIob": "и вы имеете %1 инсулина в организме.", + "virtAsstIobIntent": "вы имеете %1 инсулина в организме", + "virtAsstIobUnits": "%1 единиц", + "virtAsstLaunch": "Что проверить в Nightscout?", + "virtAsstPreamble": "Ваш", + "virtAsstPreamble3person": "%1 имеет ", + "virtAsstNoInsulin": "нет", + "virtAsstUploadBattery": "батарея загрузчика %1", + "virtAsstReservoir": "остается %1 ед", + "virtAsstPumpBattery": "батарея помпы %1 %2", + "virtAsstUploaderBattery": "Батарея загрузчика %1", + "virtAsstLastLoop": "недавний успешный цикл был %1", + "virtAsstLoopNotAvailable": "Расширение ЗЦ Loop не активировано", + "virtAsstLoopForecastAround": "По прогнозу алгоритма ЗЦ ожидаемая ГК около %1 в следующие %2", + "virtAsstLoopForecastBetween": "По прогнозу алгоритма ЗЦ ожидаемая ГК между %1 и %2 в следующие %3", + "virtAsstAR2ForecastAround": "По прогнозу AR2 ГК ожидается в районе %1 в следующие %2", + "virtAsstAR2ForecastBetween": "По прогнозу AR2 ГК ожидается между %1 и %2 в следующие %3", + "virtAsstForecastUnavailable": "Прогноз при таких данных невозможен", + "virtAsstRawBG": "Ваши необработанные данные RAW %1", + "virtAsstOpenAPSForecast": "Прогнозируемая OpenAPS ГК равна %1", + "virtAsstCob3person": "%1 имеет %2 активных углеводов", + "virtAsstCob": "У вас %1 активных углеводов", + "virtAsstCGMMode": "Режим непрерывного мониторинга был %1 по состоянию на %2.", + "virtAsstCGMStatus": "Состояние непрерывного мониторинга было %1 по состоянию на %2.", + "virtAsstCGMSessAge": "Сессия мониторинга была активна %1 дн и %2 час.", + "virtAsstCGMSessNotStarted": "Сейчас нет активных сессий мониторинга.", + "virtAsstCGMTxStatus": "Состояние трансмиттера мониторинга было %1 по состоянию на %2.", + "virtAsstCGMTxAge": "Трансмиттеру мониторинга %1 дн.", + "virtAsstCGMNoise": "Зашумленность мониторинга была %1 по состоянию на %2.", + "virtAsstCGMBattOne": "Батарея мониторинга была %1 вольт по состоянию на %2.", + "virtAsstCGMBattTwo": "Уровни заряда аккумулятора были %1 вольт и %2 вольт по состоянию на %3.", + "virtAsstDelta": "Дельта была %1 между %2 и %3.", + "virtAsstDeltaEstimated": "Ваша ожидаемая дельта была %1 между %2 и %3.", + "virtAsstUnknownIntentTitle": "Неизвестное намерение", + "virtAsstUnknownIntentText": "Ваш запрос непонятен", + "Fat [g]": "жиры [g]", + "Protein [g]": "белки [g]", + "Energy [kJ]": "энергия [kJ]", + "Clock Views:": "цифры крупно:", + "Clock": "часы", + "Color": "цвет", + "Simple": "простой", + "TDD average": "средняя суточная доза инсулина", + "Bolus average": "Среднее значение болюсов", + "Basal average": "Среднее значение базала", + "Base basal average:": "Среднее значение базового базала:", + "Carbs average": "среднее кол-во углеводов за сутки", + "Eating Soon": "Ожидаемый прием пищи", + "Last entry {0} minutes ago": "предыдущая запись {0} минут назад", + "change": "замена", + "Speech": "речь", + "Target Top": "верхняя граница цели", + "Target Bottom": "нижняя граница цели", + "Canceled": "отменено", + "Meter BG": "ГК по глюкометру", + "predicted": "прогноз", + "future": "будущее", + "ago": "назад", + "Last data received": "прошлые данные получены", + "Clock View": "вид циферблата", + "Protein": "Белки", + "Fat": "Жиры", + "Protein average": "Средний белок", + "Fat average": "Средний жир", + "Total carbs": "Всего углеводов", + "Total protein": "Всего белков", + "Total fat": "Всего жиров", + "Database Size": "Размер базы данных", + "Database Size near its limits!": "Размер базы данных приближается к лимиту!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Размер базы данных %1 MiB из %2 MiB. Сделайте резервную копию и очистите базу данных!", + "Database file size": "Размер файла базы данных", + "%1 MiB of %2 MiB (%3%)": "%1 MiB из %2 MiB (%3%)", + "Data size": "Объем данных", + "virtAsstDatabaseSize": "%1 MiB Это %2% доступного места в базе данных.", + "virtAsstTitleDatabaseSize": "Размер файла базы данных", + "Carbs/Food/Time": "Углеводы/Еда/Время", + "You have administration messages": "У вас есть сообщения администрирования", + "Admin messages in queue": "Сообщения администрирования в очереди", + "Queue empty": "Очередь пуста", + "There are no admin messages in queue": "В очереди нет сообщений администрирования", + "Please sign in using the API_SECRET to see your administration messages": "Пожалуйста, войдите, используя API_SECRET, чтобы увидеть ваши сообщения администрирования", + "Reads enabled in default permissions": "Чтение включено в разрешениях по умолчанию", + "Data reads enabled": "Чтение данных включено", + "Data writes enabled": "Запись данных включена", + "Data writes not enabled": "Запись данных не включена", + "Color prediction lines": "Цветные линии прогнозов", + "Release Notes": "Заметки к релизу", + "Check for Updates": "Проверить наличие обновлений", + "Open Source": "Открытый код", + "Nightscout Info": "Информация о Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Основной целью Loopalyzer является визуализировать работу системы замкнутого цикла Loop. Он может работать и с другими установками, как с закрытыми и открытыми циклами, так и вне циклов. Однако, в зависимости от того, какой загрузчик вы используете, как часто он может получать и загружать данные, как способен заполнять недостающие. Всегда убедитесь, что графики выглядят разумно. Лучше всего просматривать по одному дню и пролистывать через несколько дней.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer включает в себя функцию сдвига по времени. Если вы, например, завтракаете в 07:00 один день и в 08:00 в другой, то средняя кривая глюкозы за эти два дня скорее всего выглядят сглаженной и не показывают фактический ответ после завтрака. Сдвиг по времени рассчитает среднее время приема этих блюд и затем переведет все данные (углеводы, инсулин, базал и т.д.) таким образом, что оба приема пищи совпадут со средним временем начала питания.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "В данном примере все данные первого дня сдвинуты на 30 мин вперед, а данные второго - на 30 минут назад, так что создается впечатление, что вы завтракали оба раза в 7:30. Это позволяет точнее увидеть вашу настоящую реакцию ГК на прием пищи.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Сдвиг по времени выделяет среднее время начала приема пищи серым на продолжительность действия инсулина.", + "Note that time shift is available only when viewing multiple days.": "Обратите внимание, что сдвиг по времени доступен только при просмотре нескольких дней.", + "Please select a maximum of two weeks duration and click Show again.": "Пожалуйста, выберите не более двух недель и нажмите Показать снова.", + "Show profiles table": "Показать таблицу профилей", + "Show predictions": "Показывать прогнозы", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Интервал приемов пищи больше %1 гу между %2 и%3", + "Previous": "Предыдущие", + "Previous day": "Предыдущий день", + "Next day": "Следующий день", + "Next": "Следующий", + "Temp basal delta": "Дельта временной базальной скорости", + "Authorized by token": "Авторизован по токену", + "Auth role": "Роль авторизующего", + "view without token": "просмотр без токена", + "Remove stored token": "Удалить сохраненный токен", + "Weekly Distribution": "Недельное распределение", + "Failed authentication": "Ошибка аутентификации", + "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?": "Устройство с IP-адресом %1 попыталось авторизоваться в Nightscout с неверными учетными данными. Проверьте, есть ли у вас загрузчик, настроенный с неверным API_SECRET или токеном.", + "Default (with leading zero and U)": "По умолчанию (с нулем впереди и словом ед.)", + "Concise (with U, without leading zero)": "Полный (со словом ед., без нуля впереди)", + "Minimal (without leading zero and U)": "Минимальный (без нуля и слова ед.)", + "Small Bolus Display": "Отображать болюсы мелкими значками", + "Large Bolus Display": "Отображать болюсы крупными значками", + "Bolus Display Threshold": "Порог отображения болюса", + "%1 U and Over": "%1 ед. и более", + "Event repeated %1 times.": "Событие повторяется %1 раз.", + "minutes": "минут", + "Last recorded %1 %2 ago.": "Записано %1 %2 назад.", + "Security issue": "Проблема безопасности", + "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.": "Обнаружен слабый API_SECRET. Примените сочетание маленьких и CAPITAL букв, цифр и небуквенно-цифровых символов, таких как !#%&/ для уменьшения риска несанкционированного доступа. Минимальная длина API_SECRET составляет 12 символов.", + "less than 1": "менее 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Пароли MongoDB и API_SECRET совпадают. Неудачное решение. Измените оба и не используйте пароли повторно в системе." +} diff --git a/translations/sk_SK.json b/translations/sk_SK.json new file mode 100644 index 00000000000..ee55b3bfe96 --- /dev/null +++ b/translations/sk_SK.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Načúvam na porte", + "Mo": "Po", + "Tu": "Ut", + "We": "St", + "Th": "Št", + "Fr": "Pi", + "Sa": "So", + "Su": "Ne", + "Monday": "Pondelok", + "Tuesday": "Utorok", + "Wednesday": "Streda", + "Thursday": "Štvrtok", + "Friday": "Piatok", + "Saturday": "Sobota", + "Sunday": "Nedeľa", + "Category": "Kategória", + "Subcategory": "Podkategória", + "Name": "Meno", + "Today": "Dnes", + "Last 2 days": "Posledné 2 dni", + "Last 3 days": "Posledné 3 dni", + "Last week": "Posledný týždeň", + "Last 2 weeks": "Posledné 2 týždne", + "Last month": "Posledný mesiac", + "Last 3 months": "Posledné 3 mesiace", + "From": "Od", + "To": "Do", + "Notes": "Poznámky", + "Food": "Jedlo", + "Insulin": "Inzulín", + "Carbs": "Sacharidy", + "Notes contain": "Poznámky obsahujú", + "Target BG range bottom": "Cieľová glykémia spodná", + "top": "horná", + "Show": "Ukáž", + "Display": "Zobraz", + "Loading": "Nahrávam", + "Loading profile": "Nahrávam profil", + "Loading status": "Nahrávam status", + "Loading food database": "Nahrávam databázu jedál", + "not displayed": "Nie je zobrazené", + "Loading CGM data of": "Nahrávam CGM dáta", + "Loading treatments data of": "Nahrávam dáta ošetrenia", + "Processing data of": "Spracovávam dáta", + "Portion": "Porcia", + "Size": "Veľkosť", + "(none)": "(žiadny)", + "None": "Žiadny", + "": "<žiadny>", + "Result is empty": "Prázdny výsledok", + "Day to day": "Deň po dni", + "Week to week": "Od tedna do tedna", + "Daily Stats": "Denné štatistiky", + "Percentile Chart": "Percentil", + "Distribution": "Distribúcia", + "Hourly stats": "Hodinové štatistiky", + "netIOB stats": "netIOB statistika", + "temp basals must be rendered to display this report": "zacasni basal mora biti prikazan za pregled tega porocila", + "No data available": "Žiadne dostupné dáta", + "Low": "Nízka", + "In Range": "V rozsahu", + "Period": "Obdobie", + "High": "Vysoká", + "Average": "Priemer", + "Low Quartile": "Nizky kvartil", + "Upper Quartile": "Vysoký kvartil", + "Quartile": "Kvartil", + "Date": "Dátum", + "Normal": "Normálny", + "Median": "Medián", + "Readings": "Záznamy", + "StDev": "Štand. odch.", + "Daily stats report": "Denné štatistiky", + "Glucose Percentile report": "Report percentilu glykémií", + "Glucose distribution": "Rozloženie glykémie", + "days total": "dní celkom", + "Total per day": "dní celkom", + "Overall": "Súhrn", + "Range": "Rozsah", + "% of Readings": "% záznamov", + "# of Readings": "Počet záznamov", + "Mean": "Stred", + "Standard Deviation": "Štandardná odchylka", + "Max": "Maksimalno", + "Min": "Minimalno", + "A1c estimation*": "Odhadované HbA1C*", + "Weekly Success": "Týždenná úspešnosť", + "There is not sufficient data to run this report. Select more days.": "Nedostatok dát. Vyberte dlhšie časové obdobie.", + "Using stored API secret hash": "Používam uložený API hash heslo", + "No API secret hash stored yet. You need to enter API secret.": "Nieje uložené žiadne API hash heslo. Musíte zadať API heslo.", + "Database loaded": "Databáza načítaná", + "Error: Database failed to load": "Chyba pri načítaní databázy", + "Error": "Napaka", + "Create new record": "Vytovriť nový záznam", + "Save record": "Uložiť záznam", + "Portions": "Porcií", + "Unit": "Jednot.", + "GI": "GI", + "Edit record": "Upraviť záznam", + "Delete record": "Zmazať záznam", + "Move to the top": "Presunúť na začiatok", + "Hidden": "Skrytý", + "Hide after use": "Skryť po použití", + "Your API secret must be at least 12 characters long": "Vaše API heslo musí mať najmenej 12 znakov", + "Bad API secret": "Nesprávne API heslo", + "API secret hash stored": "Hash API hesla uložený", + "Status": "Stanje", + "Not loaded": "Nenačítaný", + "Food Editor": "Editor jedál", + "Your database": "Vaša databáza", + "Filter": "Filtriraj", + "Save": "Uložiť", + "Clear": "Vymazať", + "Record": "Záznam", + "Quick picks": "Rýchly výber", + "Show hidden": "Zobraziť skryté", + "Your API secret or token": "Tvoja API koda ali token", + "Remember this device. (Do not enable this on public computers.)": "Zapomni si to napravo (ne omogoci tega na javnih racunalnikih)", + "Treatments": "Ošetrenie", + "Time": "Čas", + "Event Type": "Typ udalosti", + "Blood Glucose": "Glykémia", + "Entered By": "Zadal", + "Delete this treatment?": "Vymazať toto ošetrenie?", + "Carbs Given": "Sacharidov", + "Insulin Given": "Podaný inzulín", + "Event Time": "Čas udalosti", + "Please verify that the data entered is correct": "Prosím, skontrolujte správnosť zadaných údajov", + "BG": "Glykémia", + "Use BG correction in calculation": "Použite korekciu na glykémiu", + "BG from CGM (autoupdated)": "Glykémia z CGM (automatická aktualizácia) ", + "BG from meter": "Glykémia z glukomeru", + "Manual BG": "Ručne zadaná glykémia", + "Quickpick": "Rýchly výber", + "or": "alebo", + "Add from database": "Pridať z databázy", + "Use carbs correction in calculation": "Použite korekciu na sacharidy", + "Use COB correction in calculation": "Použite korekciu na COB", + "Use IOB in calculation": "Použite IOB vo výpočte", + "Other correction": "Iná korekcia", + "Rounding": "Zaokrúhlenie", + "Enter insulin correction in treatment": "Zadajte korekciu inzulínu do ošetrenia", + "Insulin needed": "Potrebný inzulín", + "Carbs needed": "Potrebné sacharidy", + "Carbs needed if Insulin total is negative value": "Potrebné sacharidy, ak je celkový inzulín záporná hodnota", + "Basal rate": "Bazál", + "60 minutes earlier": "60 min. pred", + "45 minutes earlier": "45 min. pred", + "30 minutes earlier": "30 min. pred", + "20 minutes earlier": "20 min. pred", + "15 minutes earlier": "15 min. pred", + "Time in minutes": "Čas v minútach", + "15 minutes later": "15 min. po", + "20 minutes later": "20 min. po", + "30 minutes later": "30 min. po", + "45 minutes later": "45 min. po", + "60 minutes later": "60 min. po", + "Additional Notes, Comments": "Ďalšie poznámky, komentáre", + "RETRO MODE": "V MINULOSTI", + "Now": "Teraz", + "Other": "Iný", + "Submit Form": "Odoslať formulár", + "Profile Editor": "Editor profilu", + "Reports": "Správy", + "Add food from your database": "Pridať jedlo z Vašej databázy", + "Reload database": "Obnoviť databázu", + "Add": "Pridať", + "Unauthorized": "Neautorizované", + "Entering record failed": "Zadanie záznamu zlyhalo", + "Device authenticated": "Zariadenie overené", + "Device not authenticated": "Zariadenie nieje overené", + "Authentication status": "Stav overenia", + "Authenticate": "Overiť", + "Remove": "Odstrániť", + "Your device is not authenticated yet": "Toto zariadenie zatiaľ nebolo overené", + "Sensor": "Senzor", + "Finger": "Glukomer", + "Manual": "Ručne", + "Scale": "Mierka", + "Linear": "Lineárne", + "Logarithmic": "Logaritmické", + "Logarithmic (Dynamic)": "Logaritmické (Dynamické)", + "Insulin-on-Board": "Aktívny inzulín (IOB)", + "Carbs-on-Board": "Aktívne sacharidy (COB)", + "Bolus Wizard Preview": "Bolus Wizard", + "Value Loaded": "Hodnoty načítané", + "Cannula Age": "Zavedenie kanyly (CAGE)", + "Basal Profile": "Bazál", + "Silence for 30 minutes": "Stíšiť na 30 minút", + "Silence for 60 minutes": "Stíšiť na 60 minút", + "Silence for 90 minutes": "Stíšiť na 90 minút", + "Silence for 120 minutes": "Stíšiť na 120 minút", + "Settings": "Nastavenia", + "Units": "Jednotky", + "Date format": "Formát času", + "12 hours": "12 hodín", + "24 hours": "24 hodín", + "Log a Treatment": "Záznam ošetrenia", + "BG Check": "Kontrola glykémie", + "Meal Bolus": "Bolus na jedlo", + "Snack Bolus": "Bolus na desiatu/olovrant", + "Correction Bolus": "Korekčný bolus", + "Carb Correction": "Prídavok sacharidov", + "Note": "Poznámka", + "Question": "Otázka", + "Exercise": "Cvičenie", + "Pump Site Change": "Výmena setu", + "CGM Sensor Start": "Spustenie senzoru", + "CGM Sensor Stop": "CGM Senzor Stop", + "CGM Sensor Insert": "Výmena senzoru", + "Sensor Code": "Sensor Code", + "Transmitter ID": "ID Vysielača", + "Dexcom Sensor Start": "Spustenie senzoru DEXCOM", + "Dexcom Sensor Change": "Výmena senzoru DEXCOM", + "Insulin Cartridge Change": "Výmena inzulínu", + "D.A.D. Alert": "Upozornenie signálneho psa", + "Glucose Reading": "Hodnota glykémie", + "Measurement Method": "Metóda merania", + "Meter": "Glukomer", + "Amount in grams": "Množstvo v gramoch", + "Amount in units": "Množstvo v jednotkách", + "View all treatments": "Zobraziť všetky ošetrenia", + "Enable Alarms": "Aktivovať alarmy", + "Pump Battery Change": "Zamenjaj baterijo crpalke", + "Pump Battery Age": "Vek batérie pumpy", + "Pump Battery Low Alarm": "Baterija crpalke nizka Alarm", + "Pump Battery change overdue!": "Pump Battery change overdue!", + "When enabled an alarm may sound.": "Pri aktivovanom alarme znie zvuk ", + "Urgent High Alarm": "Naliehavý alarm vysokej glykémie", + "High Alarm": "Alarm vysokej glykémie", + "Low Alarm": "Alarm nízkej glykémie", + "Urgent Low Alarm": "Naliehavý alarm nízkej glykémie", + "Stale Data: Warn": "Varovanie: Zastaralé dáta", + "Stale Data: Urgent": "Naliehavé: Zastaralé dáta", + "mins": "min.", + "Night Mode": "Nočný mód", + "When enabled the page will be dimmed from 10pm - 6am.": "Keď je povolený, obrazovka bude stlmená od 22:00 do 6:00.", + "Enable": "Povoliť", + "Show Raw BG Data": "Zobraziť RAW dáta", + "Never": "Nikdy", + "Always": "Vždy", + "When there is noise": "Pri šume", + "When enabled small white dots will be displayed for raw BG data": "Keď je povolené, malé bodky budú zobrazovať RAW dáta.", + "Custom Title": "Vlastný názov stránky", + "Theme": "Vzhľad", + "Default": "Predvolený", + "Colors": "Farebný", + "Colorblind-friendly colors": "Farby vhodné pre farboslepých", + "Reset, and use defaults": "Resetovať do pôvodného nastavenia", + "Calibrations": "Kalibrácie", + "Alarm Test / Smartphone Enable": "Test alarmu", + "Bolus Wizard": "Bolusový kalkulátor", + "in the future": "v budúcnosti", + "time ago": "čas pred", + "hr ago": "hod. pred", + "hrs ago": "hod. pred", + "min ago": "min. pred", + "mins ago": "min. pred", + "day ago": "deň pred", + "days ago": "dni pred", + "long ago": "veľmi dávno", + "Clean": "Čistý", + "Light": "Nízky", + "Medium": "Stredný", + "Heavy": "Veľký", + "Treatment type": "Typ ošetrenia", + "Raw BG": "RAW dáta glykémie", + "Device": "Zariadenie", + "Noise": "Šum", + "Calibration": "Kalibrácia", + "Show Plugins": "Zobraziť pluginy", + "About": "O aplikácii", + "Value in": "Hodnota v", + "Carb Time": "Čas jedla", + "Language": "Jazyk", + "Add new": "Pridať nový", + "g": "g", + "ml": "ml", + "pcs": "ks", + "Drag&drop food here": "Potiahni a pusti jedlo sem", + "Care Portal": "Portál starostlivosti", + "Medium/Unknown": "Stredný/Neznámi", + "IN THE FUTURE": "V BUDÚCNOSTI", + "Order": "Usporiadať", + "oldest on top": "najstaršie hore", + "newest on top": "najnovšie hore", + "All sensor events": "Všetky udalosti senzoru", + "Remove future items from mongo database": "Odobrať budúce položky z Mongo databázy", + "Find and remove treatments in the future": "Nájsť a odstrániť záznamy ošetrenia v budúcnosti", + "This task find and remove treatments in the future.": "Táto úloha nájde a odstáni záznamy ošetrenia v budúcnosti.", + "Remove treatments in the future": "Odstrániť záznamy ošetrenia v budúcnosti", + "Find and remove entries in the future": "Nájsť a odstrániť CGM dáta v budúcnosti", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Táto úloha nájde a odstráni CGM dáta v budúcnosti vzniknuté zle nastaveným časom uploaderu.", + "Remove entries in the future": "Odstrániť CGM dáta v budúcnosti", + "Loading database ...": "Nahrávam databázu...", + "Database contains %1 future records": "Databáza obsahuje %1 záznamov v budúcnosti", + "Remove %1 selected records?": "Odstrániť %1 vybraných záznamov", + "Error loading database": "Chyba pri nahrávanií databázy", + "Record %1 removed ...": "%1 záznamov bolo odstránených...", + "Error removing record %1": "Chyba pri odstraňovaní záznamu %1", + "Deleting records ...": "Odstraňovanie záznamov...", + "%1 records deleted": "%1 záznamov vymazaných", + "Clean Mongo status database": "Vyčistiť Mongo databázu statusov", + "Delete all documents from devicestatus collection": "Odstránenie všetkých záznamov z kolekcie \"devicestatus\"", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Táto úloha vymaže všetky záznamy z kolekcie \"devicestatus\". Je to vhodné keď sa stav batérie nezobrazuje správne.", + "Delete all documents": "Zmazať všetky záznamy", + "Delete all documents from devicestatus collection?": "Zmazať všetky záznamy z kolekcie \"devicestatus\"?", + "Database contains %1 records": "Databáza obsahuje %1 záznamov", + "All records removed ...": "Všetky záznamy boli zmazané...", + "Delete all documents from devicestatus collection older than 30 days": "Delete all documents from devicestatus collection older than 30 days", + "Number of Days to Keep:": "Number of Days to Keep:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from devicestatus collection?": "Delete old documents from devicestatus collection?", + "Clean Mongo entries (glucose entries) database": "Clean Mongo entries (glucose entries) database", + "Delete all documents from entries collection older than 180 days": "Delete all documents from entries collection older than 180 days", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents": "Delete old documents", + "Delete old documents from entries collection?": "Delete old documents from entries collection?", + "%1 is not a valid number": "%1 is not a valid number", + "%1 is not a valid number - must be more than 2": "%1 is not a valid number - must be more than 2", + "Clean Mongo treatments database": "Clean Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Delete all documents from treatments collection older than 180 days", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "Nástroje pre správu", + "Nightscout reporting": "Nightscout výkazy", + "Cancel": "Zrušiť", + "Edit treatment": "Upraviť ošetrenie", + "Duration": "Trvanie", + "Duration in minutes": "Trvanie v minútach", + "Temp Basal": "Dočasný bazál", + "Temp Basal Start": "Začiatok dočasného bazálu", + "Temp Basal End": "Koniec dočasného bazálu", + "Percent": "Percent", + "Basal change in %": "Zmena bazálu v %", + "Basal value": "Hodnota bazálu", + "Absolute basal value": "Absolútna hodnota bazálu", + "Announcement": "Oznámenia", + "Loading temp basal data": "Nahrávam dáta dočasného bazálu", + "Save current record before changing to new?": "Uložiť súčastny záznam pred zmenou na nový?", + "Profile Switch": "Zmena profilu", + "Profile": "Profil", + "General profile settings": "Obecné nastavenia profilu", + "Title": "Názov", + "Database records": "Záznamy databázi", + "Add new record": "Pridať nový záznam", + "Remove this record": "Zmazať záznam", + "Clone this record to new": "Skopírovať záznam do nového", + "Record valid from": "Záznam platný od", + "Stored profiles": "Uložené profily", + "Timezone": "Časové pásmo", + "Duration of Insulin Activity (DIA)": "Doba pôsobenia inzulínu (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Predstavuje typickú dobu počas ktorej inzulín pôsobí. Býva rôzna od pacienta a od typu inzulínu. Zvyčajne sa pohybuje medzi 3-4 hodinami u pacienta s pumpou.", + "Insulin to carb ratio (I:C)": "Inzulín-sacharidový pomer (I:C)", + "Hours:": "Hodiny:", + "hours": "hodiny", + "g/hour": "g/hod", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "gramy sacharidov na jednotku inzulínu. Pomer udáva aké množstvo sacharidov pokryje jednotka inzulínu.", + "Insulin Sensitivity Factor (ISF)": "Citlivosť na inzulín (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL alebo mmol/L na jednotku inzulínu. Pomer udáva o koľko sa zmení hodnota glykémie po podaní jednotky inzulínu.", + "Carbs activity / absorption rate": "Rýchlosť vstrebávania sacharidov", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gramy za jednotku času. Reprezentuje súčasne zmenu COB za jednotku času, ako aj množstvo sacharidov ktoré sa za tú dobu prejavili. Krivka vstrebávania sacharidov je omnoho menej pochopiteľná ako pôsobenie inzulínu (IOB), ale môže byť približne s použitím počiatočného oneskorenia a následne s konštantným vstrebávaním (g/hod). ", + "Basal rates [unit/hour]": "Bazál [U/hod]", + "Target BG range [mg/dL,mmol/L]": "Rozsah cieľovej glykémie [mg/dL,mmol/L]", + "Start of record validity": "Začiatok platnosti záznamu", + "Icicle": "Inverzne", + "Render Basal": "Zobrazenie bazálu", + "Profile used": "Použitý profil", + "Calculation is in target range.": "Výpočet je v cieľovom rozsahu.", + "Loading profile records ...": "Nahrávam záznamy profilov", + "Values loaded.": "Hodnoty načítané.", + "Default values used.": "Použité východzie hodnoty.", + "Error. Default values used.": "CHYBA! Použité východzie hodnoty.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Časové rozsahy pre cieľové glykémie sa nezhodujú. Hodnoty nastavené na východzie.", + "Valid from:": "Platné od:", + "Save current record before switching to new?": "Uložiť súčastný záznam pred prepnutím na nový?", + "Add new interval before": "Pridať nový interval pred", + "Delete interval": "Zmazať interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Kombinovaný bolus", + "Difference": "Rozdiel", + "New time": "Nový čas", + "Edit Mode": "Editačný mód", + "When enabled icon to start edit mode is visible": "Keď je povolený, je zobrazená ikona editačného módu", + "Operation": "Operácia", + "Move": "Presunúť", + "Delete": "Zmazať", + "Move insulin": "Presunúť inzulín", + "Move carbs": "Presunúť sacharidy", + "Remove insulin": "Odstrániť inzulín", + "Remove carbs": "Odstrániť sacharidy", + "Change treatment time to %1 ?": "Zmeniť čas ošetrenia na %1 ?", + "Change carbs time to %1 ?": "Zmeniť čas sacharidov na %1 ?", + "Change insulin time to %1 ?": "Zmeniť čas inzulínu na %1 ?", + "Remove treatment ?": "Odstrániť ošetrenie?", + "Remove insulin from treatment ?": "Odstrániť inzulín z ošetrenia?", + "Remove carbs from treatment ?": "Odstrániť sacharidy z ošetrenia?", + "Rendering": "Vykresľujem", + "Loading OpenAPS data of": "Nahrávam OpenAPS dáta z", + "Loading profile switch data": "Nahrávam dáta prepnutia profilu", + "Redirecting you to the Profile Editor to create a new profile.": "Zle nastavený profil.\nK zobrazenému času nieje definovaný žiadny profil.\nPresmerovávam na vytvorenie profilu.", + "Pump": "Pumpa", + "Sensor Age": "Zavedenie senzoru (SAGE)", + "Insulin Age": "Výmena inzulínu (IAGE)", + "Temporary target": "Dočasný cieľ", + "Reason": "Dôvod", + "Eating soon": "Jesť čoskoro", + "Top": "Vrchná", + "Bottom": "Spodná", + "Activity": "Aktivita", + "Targets": "Ciele", + "Bolus insulin:": "Bolusový inzulín:", + "Base basal insulin:": "Základný bazálny inzulín:", + "Positive temp basal insulin:": "Pozitívny dočasný bazálny inzulín:", + "Negative temp basal insulin:": "Negatívny dočasný bazálny inzulín:", + "Total basal insulin:": "Celkový bazálny inzulín:", + "Total daily insulin:": "Celkový denný inzulín:", + "Unable to save Role": "Unable to save Role", + "Unable to delete Role": "Rola sa nedá zmazať", + "Database contains %1 roles": "Databáza obsahuje %1 rolí", + "Edit Role": "Editovať rolu", + "admin, school, family, etc": "administrátor, škola, rodina atď...", + "Permissions": "Oprávnenia", + "Are you sure you want to delete: ": "Naozaj zmazať:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Každá rola má 1 alebo viac oprávnení. Oprávnenie * je zástupný znak, oprávnenia sú hierarchie používajúce : ako oddelovač.", + "Add new Role": "Pridať novú rolu", + "Roles - Groups of People, Devices, etc": "Role - skupiny ľudí, zariadení atď...", + "Edit this role": "Editovať túto rolu", + "Admin authorized": "Admin autorizovaný", + "Subjects - People, Devices, etc": "Subjekty - ľudia, zariadenia atď...", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Každý objekt má svoj unikátny prístupový token a 1 alebo viac rolí. Klikni na prístupový token pre otvorenie nového okna pre tento subjekt. Tento link je možné zdielať.", + "Add new Subject": "Pridať nový subjekt", + "Unable to save Subject": "Unable to save Subject", + "Unable to delete Subject": "Subjekt sa nedá odstrániť", + "Database contains %1 subjects": "Databáza obsahuje %1 subjektov", + "Edit Subject": "Editovať subjekt", + "person, device, etc": "osoba, zariadenie atď...", + "role1, role2": "rola1, rola2", + "Edit this subject": "Editovať tento subjekt", + "Delete this subject": "Zmazať tento subjekt", + "Roles": "Rola", + "Access Token": "Prístupový Token", + "hour ago": "pred hodinou", + "hours ago": "hodín pred", + "Silence for %1 minutes": "Stíšiť na %1 minút", + "Check BG": "Skontrolovať glykémiu", + "BASAL": "BAZÁL", + "Current basal": "Aktuálny bazál", + "Sensitivity": "Citlivosť (ISF)", + "Current Carb Ratio": "Aktuálny sacharidový pomer (I\"C)", + "Basal timezone": "Časová zóna pre bazál", + "Active profile": "Aktívny profil", + "Active temp basal": "Aktívny dočasný bazál", + "Active temp basal start": "Štart dočasného bazálu", + "Active temp basal duration": "Trvanie dočasného bazálu", + "Active temp basal remaining": "Zostatok dočasného bazálu", + "Basal profile value": "Základná hodnota bazálu", + "Active combo bolus": "Aktívny kombinovaný bolus", + "Active combo bolus start": "Štart kombinovaného bolusu", + "Active combo bolus duration": "Trvanie kombinovaného bolusu", + "Active combo bolus remaining": "Zostávajúci kombinovaný bolus", + "BG Delta": "Zmena glykémie", + "Elapsed Time": "Uplynutý čas", + "Absolute Delta": "Absolútny rozdiel", + "Interpolated": "Interpolované", + "BWP": "BK", + "Urgent": "Urgentné", + "Warning": "Varovanie", + "Info": "Info", + "Lowest": "Najnižsie", + "Snoozing high alarm since there is enough IOB": "Odloženie alarmu vysokej glykémie, pretože je dostatok IOB", + "Check BG, time to bolus?": "Skontrolovať glykémiu, čas na bolus?", + "Notice": "Poznámka", + "required info missing": "chýbajúca informácia", + "Insulin on Board": "Aktívny inzulín (IOB)", + "Current target": "Aktuálny cieľ", + "Expected effect": "Očakávaný efekt", + "Expected outcome": "Očakávaný výsledok", + "Carb Equivalent": "Sacharidový ekvivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Nadbytok inzulínu o %1U viac ako je potrebné na dosiahnutie spodnej cieľovej hranice. Neráta sa so sacharidmi.", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Nadbytok inzulínu o %1U viac ako je potrebné na dosiahnutie spodnej cieľovej hranice. UISTITE SA, ŽE JE TO POKRYTÉ SACHARIDMI", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Nutné zníženie aktívneho inzulínu o %1U pre dosiahnutie spodnej cieľovej hranice. Príliš veľa bazálu?", + "basal adjustment out of range, give carbs?": "úprava pomocou zmeny bazálu nie je možná. Podať sacharidy?", + "basal adjustment out of range, give bolus?": "úprava pomocou zmeny bazálu nie je možná. Podať bolus?", + "above high": "nad horným", + "below low": "pod spodným", + "Projected BG %1 target": "Predpokladaná glykémia %1 cieľ", + "aiming at": "cieľom", + "Bolus %1 units": "Bolus %1 jednotiek", + "or adjust basal": "alebo úprava bazálu", + "Check BG using glucometer before correcting!": "Pred korekciou skontrolujte glykémiu glukometrom!", + "Basal reduction to account %1 units:": "Úprava bazálu pre výpočet %1 jednotiek:", + "30m temp basal": "30 minutový dočasný bazál", + "1h temp basal": "hodinový dočasný bazál", + "Cannula change overdue!": "Výmena kanyli po lehote!", + "Time to change cannula": "Čas na výmenu kanyli", + "Change cannula soon": "Čoskoro bude potrebné vymeniť kanylu", + "Cannula age %1 hours": "Vek kanyli %1 hodín", + "Inserted": "Zavedený", + "CAGE": "SET", + "COB": "SACH", + "Last Carbs": "Posledné sacharidy", + "IAGE": "INZ", + "Insulin reservoir change overdue!": "Čas na výmenu inzulínu po lehote!", + "Time to change insulin reservoir": "Čas na výmenu inzulínu", + "Change insulin reservoir soon": "Čoskoro bude potrebné vymeniť inzulín", + "Insulin reservoir age %1 hours": "Vek inzulínu %1 hodín", + "Changed": "Vymenený", + "IOB": "IOB", + "Careportal IOB": "IOB z portálu starostlivosti", + "Last Bolus": "Posledný bolus", + "Basal IOB": "Bazálny IOB", + "Source": "Zdroj", + "Stale data, check rig?": "Zastaralé dáta, skontrolujte uploader", + "Last received:": "Naposledy prijaté:", + "%1m ago": "pred %1m", + "%1h ago": "pred %1h", + "%1d ago": "pred %1d", + "RETRO": "RETRO", + "SAGE": "SENZ", + "Sensor change/restart overdue!": "Čas na výmenu/reštart sensoru uplynul!", + "Time to change/restart sensor": "Čas na výmenu/reštart senzoru", + "Change/restart sensor soon": "Čoskoro bude potrebné vymeniť/reštartovať senzor", + "Sensor age %1 days %2 hours": "Vek senzoru %1 dní %2 hodín", + "Sensor Insert": "Výmena senzoru", + "Sensor Start": "Štart senzoru", + "days": "dní", + "Insulin distribution": "Insulin distribution", + "To see this report, press SHOW while in this view": "To see this report, press SHOW while in this view", + "AR2 Forecast": "AR2 Forecast", + "OpenAPS Forecasts": "OpenAPS Forecasts", + "Temporary Target": "Temporary Target", + "Temporary Target Cancel": "Temporary Target Cancel", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profiles", + "Time in fluctuation": "Time in fluctuation", + "Time in rapid fluctuation": "Time in rapid fluctuation", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:", + "Filter by hours": "Filter by hours", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">can be found here.", + "Mean Total Daily Change": "Mean Total Daily Change", + "Mean Hourly Change": "Mean Hourly Change", + "FortyFiveDown": "slightly dropping", + "FortyFiveUp": "slightly rising", + "Flat": "holding", + "SingleUp": "rising", + "SingleDown": "dropping", + "DoubleDown": "rapidly dropping", + "DoubleUp": "rapidly rising", + "virtAsstUnknown": "That value is unknown at the moment. Please see your Nightscout site for more details.", + "virtAsstTitleAR2Forecast": "AR2 Forecast", + "virtAsstTitleCurrentBasal": "Current Basal", + "virtAsstTitleCurrentCOB": "Current COB", + "virtAsstTitleCurrentIOB": "Current IOB", + "virtAsstTitleLaunch": "Welcome to Nightscout", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Forecast", + "virtAsstTitlePumpReservoir": "Insulin Remaining", + "virtAsstTitlePumpBattery": "Pump Battery", + "virtAsstTitleRawBG": "Current Raw BG", + "virtAsstTitleUploaderBattery": "Uploader Battery", + "virtAsstTitleCurrentBG": "Current BG", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "CGM Mode", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM Transmitter Age", + "virtAsstTitleCGMNoise": "CGM Noise", + "virtAsstTitleDelta": "Blood Glucose Delta", + "virtAsstStatus": "%1 and %2 as of %3.", + "virtAsstBasal": "%1 current basal is %2 units per hour", + "virtAsstBasalTemp": "%1 temp basal of %2 units per hour will end %3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "You have %1 insulin on board", + "virtAsstIobUnits": "%1 units of", + "virtAsstLaunch": "What would you like to check on Nightscout?", + "virtAsstPreamble": "Your", + "virtAsstPreamble3person": "%1 has a ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "Your pump battery is at %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "I'm sorry, I don't know what you're asking for.", + "Fat [g]": "Fat [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energy [kJ]", + "Clock Views:": "Clock Views:", + "Clock": "Clock", + "Color": "Color", + "Simple": "Simple", + "TDD average": "TDD average", + "Bolus average": "Bolus average", + "Basal average": "Basal average", + "Base basal average:": "Base basal average:", + "Carbs average": "Carbs average", + "Eating Soon": "Eating Soon", + "Last entry {0} minutes ago": "Last entry {0} minutes ago", + "change": "change", + "Speech": "Speech", + "Target Top": "Target Top", + "Target Bottom": "Target Bottom", + "Canceled": "Canceled", + "Meter BG": "Meter BG", + "predicted": "predicted", + "future": "future", + "ago": "ago", + "Last data received": "Last data received", + "Clock View": "Clock View", + "Protein": "Protein", + "Fat": "Fat", + "Protein average": "Protein average", + "Fat average": "Fat average", + "Total carbs": "Total carbs", + "Total protein": "Total protein", + "Total fat": "Total fat", + "Database Size": "Database Size", + "Database Size near its limits!": "Database Size near its limits!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "Database file size", + "%1 MiB of %2 MiB (%3%)": "%1 MiB of %2 MiB (%3%)", + "Data size": "Data size", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "Database file size", + "Carbs/Food/Time": "Carbs/Food/Time", + "You have administration messages": "You have administration messages", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "Data reads enabled", + "Data writes enabled": "Data writes enabled", + "Data writes not enabled": "Data writes not enabled", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "Check for Updates", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "Previous", + "Previous day": "Previous day", + "Next day": "Next day", + "Next": "Next", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/translations/sl_SI.json b/translations/sl_SI.json new file mode 100644 index 00000000000..cbbf98d2c59 --- /dev/null +++ b/translations/sl_SI.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Poslušam na vratih", + "Mo": "Pon", + "Tu": "Tor", + "We": "Sre", + "Th": "Čet", + "Fr": "Pet", + "Sa": "Sob", + "Su": "Ned", + "Monday": "Ponedeljek", + "Tuesday": "Torek", + "Wednesday": "Sreda", + "Thursday": "Četrtek", + "Friday": "Petek", + "Saturday": "Sobota", + "Sunday": "Nedelja", + "Category": "Kategorija", + "Subcategory": "Podkategorija", + "Name": "Ime", + "Today": "Danes", + "Last 2 days": "Zadnje 2 dni", + "Last 3 days": "Zadnje 3 dni", + "Last week": "Zadnji teden", + "Last 2 weeks": "Zadnja 2 tedna", + "Last month": "Zadnji mesec", + "Last 3 months": "Zadnje 3 mesece", + "From": "Od", + "To": "Do", + "Notes": "Opombe", + "Food": "Hrana", + "Insulin": "Insulin", + "Carbs": "OH", + "Notes contain": "Opombe vsebujejo", + "Target BG range bottom": "Ciljna GK spodnja", + "top": "zgornja", + "Show": "Pokaži", + "Display": "Prikaži", + "Loading": "Nalaganje", + "Loading profile": "Nalaganje profila", + "Loading status": "Nalaganje statusa", + "Loading food database": "Nalaganje baze podatkov jedi", + "not displayed": "ni prikazano", + "Loading CGM data of": "Nalaganje CGM podatkov od", + "Loading treatments data of": "Nalaganje podatkov o negi od", + "Processing data of": "Obdelovanje podatkov od", + "Portion": "Obrok", + "Size": "Velikost", + "(none)": "(brez)", + "None": "Brez", + "": "", + "Result is empty": "Prazen rezultat", + "Day to day": "Od dneva do dne", + "Week to week": "Od tedna do tedna", + "Daily Stats": "Dnevna statistika", + "Percentile Chart": "Procentni Graf", + "Distribution": "Porazdelitev", + "Hourly stats": "Urna statistika", + "netIOB stats": "netoIOB statistika", + "temp basals must be rendered to display this report": "začasni basal mora biti prikazan za pregled tega poročila", + "No data available": "Podatki niso na voljo", + "Low": "Nizki", + "In Range": "V mejah", + "Period": "Obdobje", + "High": "Visoki", + "Average": "Povprečje", + "Low Quartile": "Nizki Kvartil", + "Upper Quartile": "Visoki Kvartil", + "Quartile": "Kvartil", + "Date": "Datum", + "Normal": "Normalno", + "Median": "Median", + "Readings": "Meritev", + "StDev": "St. Dev", + "Daily stats report": "Dnevno statistično poročilo", + "Glucose Percentile report": "Procentno poročilo glukoze", + "Glucose distribution": "Porazdelitev glukoze", + "days total": "dni", + "Total per day": "Skupno na dan", + "Overall": "Skupno", + "Range": "Področje", + "% of Readings": "% meritev", + "# of Readings": "Število meritev", + "Mean": "Srednja vrednost", + "Standard Deviation": "Standardna Deviacija", + "Max": "Maks", + "Min": "Min", + "A1c estimation*": "Ocena A1c*", + "Weekly Success": "Tedenska Uspešnost", + "There is not sufficient data to run this report. Select more days.": "Ni dovolj podatkov za prikaz tega poročila. Izberi več dni.", + "Using stored API secret hash": "Uporaba shranjene API hash skrivne kode", + "No API secret hash stored yet. You need to enter API secret.": "API hash skrivna koda ni shranjena. Vnesi API hash skrivno kodo.", + "Database loaded": "Baza podatkov naložena", + "Error: Database failed to load": "Napaka: Baze podatkov ni bilo mogoče naložiti", + "Error": "Napaka", + "Create new record": "Ustvari nov zapis", + "Save record": "Shrani zapis", + "Portions": "Obroki", + "Unit": "Enot", + "GI": "GI", + "Edit record": "Uredi zapis", + "Delete record": "Izbriši zapis", + "Move to the top": "Premakni na vrh", + "Hidden": "Skrit", + "Hide after use": "Skrij po uporabi", + "Your API secret must be at least 12 characters long": "Vaše API skrivno geslo mora biti dolgo vsaj 12 znakov", + "Bad API secret": "Nepravilno API skrivno geslo", + "API secret hash stored": "API hash skrivno geslo je shranjeno", + "Status": "Stanje", + "Not loaded": "Ni naloženo", + "Food Editor": "Urednik Hrane", + "Your database": "Vaša baza podatkov", + "Filter": "Filter", + "Save": "Shrani", + "Clear": "Počisti", + "Record": "Zapis", + "Quick picks": "Hitre izbire", + "Show hidden": "Pokaži skrito", + "Your API secret or token": "Tvoja API skrivna koda ali token", + "Remember this device. (Do not enable this on public computers.)": "Zapomni si to napravo (Ne omogoci tega na javnih racunalnikih)", + "Treatments": "Nega", + "Time": "Čas", + "Event Type": "Tip dogodka", + "Blood Glucose": "Glukoza v krvi", + "Entered By": "Vpisal", + "Delete this treatment?": "Izbriši to nego?", + "Carbs Given": "Ogljikovi Hidrati", + "Insulin Given": "Insulin", + "Event Time": "Čas", + "Please verify that the data entered is correct": "Preverite, ali so vnešeni podatki pravilni", + "BG": "GK", + "Use BG correction in calculation": "Uporabi GK korekcijo v izračunu", + "BG from CGM (autoupdated)": "GK iz CGM (samodejno posodobljen)", + "BG from meter": "GK iz merilnika", + "Manual BG": "Ročni GK", + "Quickpick": "Hitri izbor", + "or": "ali", + "Add from database": "Dodaj iz zbirke podatkov", + "Use carbs correction in calculation": "Uporabi korekcijo OH v izračunu", + "Use COB correction in calculation": "Uporabi korekcijo COB v izračunu", + "Use IOB in calculation": "Uporabi IOB v izračunu", + "Other correction": "Ostale korekcije", + "Rounding": "Zaokroženo", + "Enter insulin correction in treatment": "Uporabi korekcijo Insulina v izračunu", + "Insulin needed": "Potreben Insulin", + "Carbs needed": "Potrebné sacharidy", + "Carbs needed if Insulin total is negative value": "Potrebni ogljikovi hidrati, če ima skupni Insulin negativno vrednost", + "Basal rate": "Bazalni odmerek", + "60 minutes earlier": "pred 60 min", + "45 minutes earlier": "pred 45 min", + "30 minutes earlier": "pred 30 min", + "20 minutes earlier": "pred 20 min", + "15 minutes earlier": "pred 15 min", + "Time in minutes": "Čas v minutah", + "15 minutes later": "15 min kasneje", + "20 minutes later": "20 min kasneje", + "30 minutes later": "30 min kasneje", + "45 minutes later": "45 min kasneje", + "60 minutes later": "60 min kasneje", + "Additional Notes, Comments": "Dodatne opombe, Komentarji", + "RETRO MODE": "Retro način", + "Now": "Zdaj", + "Other": "Ostalo", + "Submit Form": "Potrdi Vnos", + "Profile Editor": "Urejevalnik profilov", + "Reports": "Poročila", + "Add food from your database": "Dodaj jedi v bazo podatkov", + "Reload database": "Obnovi bazo podatkov", + "Add": "Dodaj", + "Unauthorized": "Nepooblaščen", + "Entering record failed": "Vnos zapisa ni uspel", + "Device authenticated": "Naprava je overjena", + "Device not authenticated": "Naprava ni overjena", + "Authentication status": "Stanje overjanja", + "Authenticate": "Preveri pristnost", + "Remove": "Odstrani", + "Your device is not authenticated yet": "Vaša naprave še ni overjena", + "Sensor": "Senzor", + "Finger": "Prst", + "Manual": "Ročno", + "Scale": "Merilo", + "Linear": "Linearno", + "Logarithmic": "Logaritmično", + "Logarithmic (Dynamic)": "Logaritmično (dinamično)", + "Insulin-on-Board": "Aktivni Insulin (IOB)", + "Carbs-on-Board": "Aktivni OH (COB)", + "Bolus Wizard Preview": "Predogled čarovnika za Bolus", + "Value Loaded": "Vrednost naložena", + "Cannula Age": "Starost Kanule (CAGE)", + "Basal Profile": "Bazalni profil", + "Silence for 30 minutes": "Utišaj za 30 min", + "Silence for 60 minutes": "Utišaj za 60 min", + "Silence for 90 minutes": "Utišaj za 90 min", + "Silence for 120 minutes": "Utišaj za 120 min", + "Settings": "Nastavitve", + "Units": "Enote", + "Date format": "Oblika datuma", + "12 hours": "12 ur", + "24 hours": "24 ur", + "Log a Treatment": "Zabeleži nego", + "BG Check": "Preverjanje GK", + "Meal Bolus": "Bolus za obrok", + "Snack Bolus": "Bolus za prigrizek", + "Correction Bolus": "Bolus za korekcijo", + "Carb Correction": "OH za korekcijo", + "Note": "Opomba", + "Question": "Vprašanje", + "Exercise": "Vadba", + "Pump Site Change": "Menjava mesta kanule", + "CGM Sensor Start": "Start CGM senzorja", + "CGM Sensor Stop": "Stop CGM senzorja", + "CGM Sensor Insert": "Zamenjava CGM senzorja", + "Sensor Code": "Koda senzorja", + "Transmitter ID": "Koda oddajnika", + "Dexcom Sensor Start": "Start Dexcom senzorja", + "Dexcom Sensor Change": "Menjava Dexcom senzorja", + "Insulin Cartridge Change": "Menjava Insulina", + "D.A.D. Alert": "D.A.D. Opozorilo", + "Glucose Reading": "Meritev glukoze v krvi", + "Measurement Method": "Metoda merjenja", + "Meter": "Merilec", + "Amount in grams": "Količina v gramih", + "Amount in units": "Količina v enotah", + "View all treatments": "Pregled vseh neg", + "Enable Alarms": "Omogoči Alarm", + "Pump Battery Change": "Menjava baterije črpalke", + "Pump Battery Age": "Starost baterije črpalke", + "Pump Battery Low Alarm": "Alarm baterija črpalke nizka", + "Pump Battery change overdue!": "Nujna zamenjava baterije črpalke!", + "When enabled an alarm may sound.": "Če omogočeno, se lahko oglasi alarm.", + "Urgent High Alarm": "Urgetno visoki GS", + "High Alarm": "Visoki GS", + "Low Alarm": "Nizki GS", + "Urgent Low Alarm": "Urgentno nizki GS", + "Stale Data: Warn": "Zastareli podatki", + "Stale Data: Urgent": "Urgentno zastareli podatki", + "mins": "min", + "Night Mode": "Nočni način", + "When enabled the page will be dimmed from 10pm - 6am.": "Če omogočeno, bo stran zatemnjena od 22. do 6. ure.", + "Enable": "Omogoči", + "Show Raw BG Data": "Pokaži Raw GK podatke", + "Never": "Nikoli", + "Always": "Vedno", + "When there is noise": "Ko je šum", + "When enabled small white dots will be displayed for raw BG data": "Če omogočeno, bodo za Raw GK podatke prikazane majhne bele pike", + "Custom Title": "Naslov po meri", + "Theme": "Videz", + "Default": "Privzeto", + "Colors": "Barve", + "Colorblind-friendly colors": "Visoko-kontrastne barve", + "Reset, and use defaults": "Ponastavi, uporabi privzeto", + "Calibrations": "Kalibracija", + "Alarm Test / Smartphone Enable": "Preizkus Alarma / Pametni telefon omogočen", + "Bolus Wizard": "Bolus Čarovnik", + "in the future": "v prihodnosti", + "time ago": "pred časom", + "hr ago": "uro nazaj", + "hrs ago": "ur nazaj", + "min ago": "min nazaj", + "mins ago": "min nazaj", + "day ago": "dan nazaj", + "days ago": "dni nazaj", + "long ago": "dolgo časa nazaj", + "Clean": "Čisto", + "Light": "Nizki", + "Medium": "Srednji", + "Heavy": "Visok", + "Treatment type": "Tip nege", + "Raw BG": "RAW GK", + "Device": "Naprava", + "Noise": "Šum", + "Calibration": "Kalibracija", + "Show Plugins": "Pokaži vtičnike", + "About": "Vizitka", + "Value in": "Vrednost v", + "Carb Time": "Čas obroka", + "Language": "Jezik", + "Add new": "Dodaj novo", + "g": "g", + "ml": "ml", + "pcs": "kos", + "Drag&drop food here": "Potegni&spusti hrano tukaj", + "Care Portal": "Portal ze nego", + "Medium/Unknown": "Srednji/Neznani", + "IN THE FUTURE": "V PRIHODNOSTI", + "Order": "Vrstni red", + "oldest on top": "starejši najprej", + "newest on top": "novejši najprej", + "All sensor events": "Vsi dogodki senzorja", + "Remove future items from mongo database": "Odstrani prihodnje vnose iz mongo zbirke podatkov", + "Find and remove treatments in the future": "Poišči in odstrani nege v prihodnosti", + "This task find and remove treatments in the future.": "Ta funkcija poišče in odstrani nege v prihodnosti", + "Remove treatments in the future": "Odstrani nege v prihodnosti", + "Find and remove entries in the future": "Najdi in odstrani vnose v prihodnosti", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Ta funkcija bo poiskala in odstranila CGM podatke v pihodnosti, ki jih je ustvaril uporabnik z napačnim datumom/uro.", + "Remove entries in the future": "Odstrani vnose v prihodnosti", + "Loading database ...": "Nalaganje baze podatkov ...", + "Database contains %1 future records": "Baza podatkov vsebuje %1 zapisov v prihodnosti", + "Remove %1 selected records?": "Odstrani %1 označenih zapisov?", + "Error loading database": "Napaka pri nalaganju baze podatkov", + "Record %1 removed ...": "%1 zapisov odstranjenih ...", + "Error removing record %1": "Napaka pri odstranjevanju zapisa %1", + "Deleting records ...": "Brisanje zapisov ...", + "%1 records deleted": "%1 zapisov izbrisanih", + "Clean Mongo status database": "Izbriši vnose iz status Mongo baze podatkov", + "Delete all documents from devicestatus collection": "Izbriši vse dokumente iz zbirke \"devicestatus\" ", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Ta funkcija izbriše vse zapise iz zbirke \"devicestatus\". Uporabno, v primeru ko stanje baterije uploader-ja ni pravilno osveženo.", + "Delete all documents": "Izbriši vse dokumente", + "Delete all documents from devicestatus collection?": "Izbriši vse dokumente iz zbirke \"devicestatus\"?", + "Database contains %1 records": "Baza podatkov vsebuje %1 zapisov", + "All records removed ...": "Vsi zapisi so odstranjeni ...", + "Delete all documents from devicestatus collection older than 30 days": "Izbriši vse dokumente iz zbirke \"devicestatus\" starejše od 30 dni", + "Number of Days to Keep:": "Število dni, ki jih želite obdržati:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Ta funkcija izbriše vse dokumente iz zbirke \"devicestatus\" starejše od 30 dni. Uporabno, v primeru ko stanje baterije uploader-ja ni pravilno osveženo.", + "Delete old documents from devicestatus collection?": "Izbriši vse stare dokumente iz zbirke \"devicestatus\"?", + "Clean Mongo entries (glucose entries) database": "Izbriši vse Mongo vnose (meritve glukoze) iz baze podatkov", + "Delete all documents from entries collection older than 180 days": "Izbriši vse dokumente vnosov starejših od 180 dni", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Ta funkcija izbriše vse dokumente vnosov starejših od 180 dni. Uporabno, v primeru ko stanje baterije uploader-ja ni pravilno osveženo.", + "Delete old documents": "Izbriši stare dokumente", + "Delete old documents from entries collection?": "Izbriši stare dokumente vnosov?", + "%1 is not a valid number": "%1 ni veljavna številka", + "%1 is not a valid number - must be more than 2": "%1 ni veljavna številka - mora biti večja od 2", + "Clean Mongo treatments database": "Izbriši vse nege iz Mongo baze podatkov", + "Delete all documents from treatments collection older than 180 days": "Izbriši vse dokumente neg starejših od 180 dni", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Ta funkcija izbriše vse dokumente neg starejših od 180 dni. Uporabno, v primeru ko stanje baterije uploader-ja ni pravilno osveženo.", + "Delete old documents from treatments collection?": "Izbriši stare dokumente neg?", + "Admin Tools": "Skrbniška orodja", + "Nightscout reporting": "Nightscout poročanje", + "Cancel": "Prekliči", + "Edit treatment": "Uredi nego", + "Duration": "Trajanje", + "Duration in minutes": "Trajanje v minutah", + "Temp Basal": "Začasni Bazal", + "Temp Basal Start": "Začetek Začasni Bazal", + "Temp Basal End": "Konec Začasni Bazal", + "Percent": "Procent", + "Basal change in %": "Bazalna sprememba v %", + "Basal value": "Bazalna vrednost", + "Absolute basal value": "Absolutna Bazalna vrednost", + "Announcement": "Obvestilo", + "Loading temp basal data": "Nalaganje podatkov Začasni Bazal", + "Save current record before changing to new?": "Shrani trenutni zapis, preden ga spremenite v novega?", + "Profile Switch": "Izbira Profila", + "Profile": "Profil", + "General profile settings": "Osnovne nastavitve profila", + "Title": "Naslov", + "Database records": "Zapisi v podatkovni bazi", + "Add new record": "Dodaj nov zapis", + "Remove this record": "Odstrani ta zapis", + "Clone this record to new": "Kloniraj ta zapis v novega", + "Record valid from": "Zapis veljaven od", + "Stored profiles": "Shranjeni profili", + "Timezone": "Časovni pas", + "Duration of Insulin Activity (DIA)": "Čas delovanja insulina (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Predstavlja tipično trajanje, v katerem insulin učinkuje. Spreminja se v odvisnosti od pacienta in vrste insulina. Običajno 3-4 ure za večino insulina preko črpalke in večino bolnikov. Včasih se imenuje tudi življenjska doba insulina.", + "Insulin to carb ratio (I:C)": "Razmerje Insulin:OH (I:C)", + "Hours:": "Ur:", + "hours": "ur", + "g/hour": "g/uro", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g OH na U inzulina. Razmerje med številom gramov ogljikovih hidratov na U (enoto) insulina.", + "Insulin Sensitivity Factor (ISF)": "Faktor občutljivosti za insulin (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dl ali mmol/l na U insulina. Razmerje med tem, koliko se GK spremeni z vsako U (enoto) korekcijskega insulina.", + "Carbs activity / absorption rate": "Aktivnost / stopnja absorpcije ogljikovih hidratov", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gramov na enoto časa. Predstavlja tako spremembo COB na enoto časa kot tudi količino ogljikovih hidratov, ki naj bi začeli delovati v tem času. Krivulje absorpcije / aktivnosti ogljikovih hidratov so slabše razumljene kot aktivnost insulina, vendar jih je mogoče aproksimirati z uporabo začetne zakasnitvije, ki ji sledi konstantna hitrost absorpcije (g/h).", + "Basal rates [unit/hour]": "Bazalni odmerek [U/uro]", + "Target BG range [mg/dL,mmol/L]": "Ciljne vrednosti GK [mg/dL,mmol/L]", + "Start of record validity": "Začetek veljavnosti zapisa", + "Icicle": "Inverzno", + "Render Basal": "Bazalni prikaz", + "Profile used": "Uporabljeni profil", + "Calculation is in target range.": "Izračun je znotraj ciljnega področja.", + "Loading profile records ...": "Nalaganje profilnih zapisov ...", + "Values loaded.": "Vrednosti naložene.", + "Default values used.": "Uporabljene privzete vrednosti.", + "Error. Default values used.": "Napaka. Uporabljene privzete vrednosti.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Časovna področja ciljni_nizki in ciljni_visoki se ne ujemata. Vrednosti so obnovljene na privzete.", + "Valid from:": "Veljavno od:", + "Save current record before switching to new?": "Shranite trenutni zapis, preden ga spremenite v novega?", + "Add new interval before": "Dodaj nov interval pred", + "Delete interval": "Izbriši interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Kombinirani Bolus", + "Difference": "Razlika", + "New time": "Nov čas", + "Edit Mode": "Način urejanja", + "When enabled icon to start edit mode is visible": "Ko omogočeno, je ikona za zagon načina urejanja vidna", + "Operation": "Postopek", + "Move": "Premakni", + "Delete": "Izbriši", + "Move insulin": "Premakni Insulin", + "Move carbs": "Premakni Ogljikove Hidrate", + "Remove insulin": "Odstrani insulin", + "Remove carbs": "Odstrani ogljikove hidrate", + "Change treatment time to %1 ?": "Spremeni čas nege na %1 ?", + "Change carbs time to %1 ?": "Spremeni čas OH na %1 ?", + "Change insulin time to %1 ?": "Spremeni čas insulina na %1 ?", + "Remove treatment ?": "Odstrani nego?", + "Remove insulin from treatment ?": "Odstrani insulin iz nege?", + "Remove carbs from treatment ?": "Odstrani OH is nege?", + "Rendering": "Vizualizacija", + "Loading OpenAPS data of": "Nalaganje OpenAPS podatkov za", + "Loading profile switch data": "Nalaganje podatkov izbire profila", + "Redirecting you to the Profile Editor to create a new profile.": "Preusmeritev v urejevalnik profilov, da ustvarite nov profil.", + "Pump": "Črpalka", + "Sensor Age": "Starost senzorja (SAGE)", + "Insulin Age": "Starost insulina (IAGE)", + "Temporary target": "Začasni Cilj", + "Reason": "Razlog", + "Eating soon": "Obrok kmalu", + "Top": "Visoki", + "Bottom": "Nizki", + "Activity": "Aktivnost", + "Targets": "Cilji", + "Bolus insulin:": "Bolusni insulin:", + "Base basal insulin:": "Osnovni bazalni insulin:", + "Positive temp basal insulin:": "Pozitivni začasni bazalni insulin:", + "Negative temp basal insulin:": "Negativni začasni bazalni insulin:", + "Total basal insulin:": "Skupni bazalni insulin:", + "Total daily insulin:": "Skupni dnevni insulin:", + "Unable to save Role": "Napaka pri shranjevanju Vloge!", + "Unable to delete Role": "Napaka pri odstranjevanju Vloge!", + "Database contains %1 roles": "Baza podatkov vsebuje %1 vlog", + "Edit Role": "Uredi Vlogo", + "admin, school, family, etc": "admin, šola, družina itd", + "Permissions": "Pravice", + "Are you sure you want to delete: ": "Ali ste prepričani da želite izbrisati: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Vsaka vloga bo imela 1 ali več dovoljenj. Dovoljenje * je nadomestni znak, dovoljenja so hierarhija, ki uporablja : kot ločilo.", + "Add new Role": "Dodaj novo Vlogo", + "Roles - Groups of People, Devices, etc": "Vloge - Skupine ljudi, Naprave itd", + "Edit this role": "Uredi to vlogo", + "Admin authorized": "Skrbnik pooblaščen", + "Subjects - People, Devices, etc": "Subjekti - Ljudje, Naprave itd", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Vsak subjekt bo imel edinstven žeton za dostop in 1 ali več vlog. Kliknite na dostopni žeton, da odprete nov pogled z izbranim subjektom, to skrivno povezavo lahko nato delite.", + "Add new Subject": "Dodaj nov Subjekt", + "Unable to save Subject": "Napaka pri shranjevanju Subjekta", + "Unable to delete Subject": "Napaka pri izbrisu Subjekta", + "Database contains %1 subjects": "Baza podatkov vsebuje %1 subjektov", + "Edit Subject": "Uredi Subjekt", + "person, device, etc": "oseba, naprava itd", + "role1, role2": "vloga1, vloga2", + "Edit this subject": "Uredi ta subjekt", + "Delete this subject": "Izbriši ta subjekt", + "Roles": "Vloga", + "Access Token": "Žeton za dostop", + "hour ago": "uro nazaj", + "hours ago": "ur nazaj", + "Silence for %1 minutes": "Utišaj za %1 min", + "Check BG": "Preveri GK", + "BASAL": "BAZAL", + "Current basal": "Trenutni bazal", + "Sensitivity": "Občutljivost (ISF)", + "Current Carb Ratio": "Trenutno razmerje I/OH (I/C)", + "Basal timezone": "Bazalni časovni pas", + "Active profile": "Aktivni profil", + "Active temp basal": "Aktivni začasni bazal", + "Active temp basal start": "Začetek aktivnega začasnega bazala", + "Active temp basal duration": "Trajanje aktivnega začasnega bazala", + "Active temp basal remaining": "Preostanek aktivnega začasnega bazala", + "Basal profile value": "Vrednost bazalnega profile", + "Active combo bolus": "Aktivni kombinirani bolus", + "Active combo bolus start": "Začetek aktivnega kombinirana bolusa", + "Active combo bolus duration": "Trajanje aktivnega kombinirana bolusa", + "Active combo bolus remaining": "Preostanek aktivnega kombinirana bolusa", + "BG Delta": "GK Sprememba", + "Elapsed Time": "Pretečen čas", + "Absolute Delta": "Absolutna sprememba", + "Interpolated": "Interpolirano", + "BWP": "BWP", + "Urgent": "Urgentno", + "Warning": "Opozorilo", + "Info": "Info", + "Lowest": "Najnižji", + "Snoozing high alarm since there is enough IOB": "Odloženi visoki alarm, ker je IOB dovolj visok", + "Check BG, time to bolus?": "Preveri GK, čas za bolus?", + "Notice": "Obvestilo", + "required info missing": "manjkajoče informacije", + "Insulin on Board": "Aktivni Insulin (IOB)", + "Current target": "Trenutni cilj", + "Expected effect": "Pričakovani učinek", + "Expected outcome": "Pričakovani izid", + "Carb Equivalent": "Ekvivalent OH", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Presežek ekvivalenta insulina %1U več, kot je potrebno za dosego nizkega cilja, ne da bi se upoštevali ogljikove hidrate", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Presežek ekvivalenta insulina %1U več, kot je potrebno za doseganje nizkega cilja, PREPRIČAJTE SE, DA IOB POKRIVAJO OGLJIKOVI HIDRATI", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Potrebno zmanjšanje %1U aktivnega insulina, da dosežemo nizek cilj. Preveč bazalnega insulina?", + "basal adjustment out of range, give carbs?": "bazalna prilagoditev izven dosega. Potrebujete ogljikove hidrate?", + "basal adjustment out of range, give bolus?": "bazalna nastavitev izven dosega. Potrebujete bolus?", + "above high": "nad visokim", + "below low": "pod nizkim", + "Projected BG %1 target": "Predvideni GK %1 ciljnega", + "aiming at": "cilj na", + "Bolus %1 units": "Bolus %1 enot", + "or adjust basal": "ali prilagodite bazalni insulin", + "Check BG using glucometer before correcting!": "Pred korekcijo preverite GK z merilnikom!", + "Basal reduction to account %1 units:": "Bazalno znižanje upoštevajoč %1 enot:", + "30m temp basal": "30min začasni bazal", + "1h temp basal": "1h začasni bazal", + "Cannula change overdue!": "Nujna zamenjava kanule!", + "Time to change cannula": "Čas za zamenjavo kanule", + "Change cannula soon": "Kmalu zamenjava kanule", + "Cannula age %1 hours": "Starost kanule %1 ur", + "Inserted": "Vstavljeno", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Zadnji OH", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Nujna zamenjava rezervoarja za inzulin!", + "Time to change insulin reservoir": "Čas za zamenjavo rezervoarja za inzulin", + "Change insulin reservoir soon": "Kmalu zamenjava rezervoarja za inzulin", + "Insulin reservoir age %1 hours": "Starost rezervoarja za inzulin %1 ur", + "Changed": "Zamenjan", + "IOB": "IOB", + "Careportal IOB": "IOB v portalu za nego", + "Last Bolus": "Zadnji bolus", + "Basal IOB": "Bazalni IOB", + "Source": "Vir", + "Stale data, check rig?": "Zastareli podatki, preverite uploader?", + "Last received:": "Zadnji prejeti podatek:", + "%1m ago": "pred %1 min", + "%1h ago": "pred %1 h", + "%1d ago": "pred %1d", + "RETRO": "RETRO", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Nujna zamenjava/ponovni zagon senzorja!", + "Time to change/restart sensor": "Čas za zamenjavo/ponovni zagon senzorja", + "Change/restart sensor soon": "Kmalu čas za zamenjavo/ponovni zagon senzorja", + "Sensor age %1 days %2 hours": "Starost senzorja %1 dni %2 ur", + "Sensor Insert": "Zamenjava CGM senzorja", + "Sensor Start": "Start CGM senzorja", + "days": "dni", + "Insulin distribution": "Porazdelitev insulina", + "To see this report, press SHOW while in this view": "Za pregled tega poročila, klikni SHOW v temu oknu", + "AR2 Forecast": "AR2 Napoved", + "OpenAPS Forecasts": "OpenAPS Napovedi", + "Temporary Target": "Začasni Cilj", + "Temporary Target Cancel": "Prekliči Začasni Cilj", + "OpenAPS Offline": "OpenAPS brez povezave", + "Profiles": "Profili", + "Time in fluctuation": "Čas v nihanju", + "Time in rapid fluctuation": "Čas v hitrem nihanju", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Tole je samo groba ocena, ki je lahko zelo nenatančna in ne nadomesti merjenja na krvi. Formula, ki je uporabljena je izpeljana iz:", + "Filter by hours": "Filtriraj po urah", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Čas v nihanju in čas v hitrem nihanju predstavljajo % časa izbranega časovnega intervala, v katerem se je glukoza spreminjala relativno hitro. Manjše vrednosti so boljše.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Povprečna Skupna Dnevna Sprememba je vsota vseh meritev glukoze v izbranem obdobju deljena s številom dni v izbranem obdobju. Nizka številka je boljša.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Povprečna Urna Sprememba je vsota vseh meritev glukoze v izbranem obdobju deljena s številom ur izbranega obdobja. Manjša številka je boljša.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "\"RMS meritev izven izbranega področja\" je izračunana kot kvadratni koren vsote vseh kvadratnih vrednosti presežkov meritev izven izbranega ciljnega področja deljene s številom meritev. Ta metoda je podobna metodi za procent meritev, ki so v obsegu izbranega ciljnega področja, vendar daje večjo težo meritvam, ki so izven ciljnega področja. Manjše vrednosti so boljše.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">najdete tukaj.", + "Mean Total Daily Change": "Povprečna Skupna Dnevna Sprememba", + "Mean Hourly Change": "Povprečna Urna Sprememba", + "FortyFiveDown": "malce pada", + "FortyFiveUp": "malce narašča", + "Flat": "stabilno", + "SingleUp": "narašča", + "SingleDown": "pada", + "DoubleDown": "hitro pada", + "DoubleUp": "hitro narašča", + "virtAsstUnknown": "Ta vrednost je trenutno neznana. Prosim poglejte si Nightscout stran za več informacij.", + "virtAsstTitleAR2Forecast": "AR2 Napoved", + "virtAsstTitleCurrentBasal": "Trenutni Basal", + "virtAsstTitleCurrentCOB": "Trenutni COB", + "virtAsstTitleCurrentIOB": "Trenutni IOB", + "virtAsstTitleLaunch": "Dobrodošli na Nightscout", + "virtAsstTitleLoopForecast": "Loop Napoved", + "virtAsstTitleLastLoop": "Zadnji Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Napoved", + "virtAsstTitlePumpReservoir": "Preostali Insulin", + "virtAsstTitlePumpBattery": "Baterija Črpalke", + "virtAsstTitleRawBG": "Trenutni RAW GK", + "virtAsstTitleUploaderBattery": "Baterija Uploader-ja", + "virtAsstTitleCurrentBG": "Trenutni GK", + "virtAsstTitleFullStatus": "Popolni Pregled", + "virtAsstTitleCGMMode": "CGM Način", + "virtAsstTitleCGMStatus": "CGM Stanje", + "virtAsstTitleCGMSessionAge": "CGM Dolžina Seje", + "virtAsstTitleCGMTxStatus": "CGM Stanje Oddajnika", + "virtAsstTitleCGMTxAge": "CGM Starost Oddajnika", + "virtAsstTitleCGMNoise": "CGM Šum", + "virtAsstTitleDelta": "Sprememba Glukoze v krvi", + "virtAsstStatus": "%1 in %2 od %3.", + "virtAsstBasal": "%1 trenutni bazal je %2 enot na uro", + "virtAsstBasalTemp": "%1 začasni bazal od %2 enot na uro bo končal %3", + "virtAsstIob": "imaš %1 insulina v telesu.", + "virtAsstIobIntent": "Imaš %1 insulina v telesu", + "virtAsstIobUnits": "%1 enot od", + "virtAsstLaunch": "Kaj želite preveriti na Nightscout?", + "virtAsstPreamble": "Vaš", + "virtAsstPreamble3person": "%1 ima ", + "virtAsstNoInsulin": "ne", + "virtAsstUploadBattery": "Baterija tvojega uploader-ja je na %1", + "virtAsstReservoir": "Imaš %1 preostalih enot", + "virtAsstPumpBattery": "Baterija črpalke je na %1 %2", + "virtAsstUploaderBattery": "Baterija tvojega uploader-ja je na %1", + "virtAsstLastLoop": "Zadnji uspešen loop je bil %1", + "virtAsstLoopNotAvailable": "Kaže da Loop plugin ni omogočen", + "virtAsstLoopForecastAround": "Glede na loop napoved se pričakuje da bo približno %1 čez naslednjih %2", + "virtAsstLoopForecastBetween": "Glede na loop napoved se pričakuje da bo med %1 in %2 čez naslednjih %3", + "virtAsstAR2ForecastAround": "Glede na AR2 napoved se pričakuje da bo približno %1 čez naslednjih %2", + "virtAsstAR2ForecastBetween": "Glede na AR2 napoved se pričakuje da bo med %1 in %2 čez naslednjih %3", + "virtAsstForecastUnavailable": "Napoved ni možna na podlagi podatkov, ki so na voljo", + "virtAsstRawBG": "Tvoj raw gk je %1", + "virtAsstOpenAPSForecast": "OpenAPS Morebitni BG je %1", + "virtAsstCob3person": "%1 ima %2 ogljikovih hidratov v telesu", + "virtAsstCob": "Ti imaš %1 ogljikovih hidratov v telesu", + "virtAsstCGMMode": "Tvoj CGM način je bil %1 od %2.", + "virtAsstCGMStatus": "Tvoje CGM stanje je bilo %1 od %2.", + "virtAsstCGMSessAge": "Tvoja CGM seja je bila aktivna %1 dni in %2 ur.", + "virtAsstCGMSessNotStarted": "Trenutno ni aktivne CGM seje.", + "virtAsstCGMTxStatus": "Tvoj CGM oddajnik je bil %1 od %2.", + "virtAsstCGMTxAge": "Tvoj CGM oddajnik je star %1 dni.", + "virtAsstCGMNoise": "CGM motnje so bile %1 od %2.", + "virtAsstCGMBattOne": "Baterija CGM senzorja je bila %1V ob %2.", + "virtAsstCGMBattTwo": "Nivo Baterije CGM Senzorja je bil %1V in %2V ob %3.", + "virtAsstDelta": "Tvoj delta je bil %1 med %2 in %3.", + "virtAsstDeltaEstimated": "Tvoj ocenjeni delta je bil %1 med %2 in %3.", + "virtAsstUnknownIntentTitle": "Neznan namen", + "virtAsstUnknownIntentText": "Se opravičujem, ampak ne razumem kaj želite.", + "Fat [g]": "Maščoba [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energijska vrednost [kJ]", + "Clock Views:": "Ura-Pogled:", + "Clock": "Ura", + "Color": "Barva", + "Simple": "Enostavna", + "TDD average": "TDD povprečje", + "Bolus average": "Bolus povprečje", + "Basal average": "Basal povprečje", + "Base basal average:": "Osnovni Basal povprečje:", + "Carbs average": "Ogljikovi Hidrati povprečje", + "Eating Soon": "Obrok Kmalu", + "Last entry {0} minutes ago": "Zadnji zapis pred {0} minutami", + "change": "sprememba", + "Speech": "Govor", + "Target Top": "Ciljna Visoka Vrednost", + "Target Bottom": "Ciljna Nizka Vrednost", + "Canceled": "Preklicano", + "Meter BG": "Merilnik GK", + "predicted": "napovedan", + "future": "prihodnost", + "ago": "nazaj", + "Last data received": "Zadnji prejeti podatek", + "Clock View": "Ura-Pogled", + "Protein": "Protein", + "Fat": "Maščoba", + "Protein average": "Protein povprečje", + "Fat average": "Maščoba povprečje", + "Total carbs": "Skupna količina ogljikovih hidratov", + "Total protein": "Skupna količina proteinov", + "Total fat": "Skupna količina maščobe", + "Database Size": "Velikost Baze Podatkov", + "Database Size near its limits!": "Velikost Baze Podatkov blizu omejitve!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Velikost baze podatkov je %1 MiB od %2 MiB. Prosim, naredite kopijo podatkov in očistite bazo podatkov!", + "Database file size": "Velikost datoteke v bazi podatkov", + "%1 MiB of %2 MiB (%3%)": "%1 MiB od %2 MiB (%3%)", + "Data size": "Velikost podatkov", + "virtAsstDatabaseSize": "%1 MiB. To je %2% od razpoložljivega prostora baze podatkov.", + "virtAsstTitleDatabaseSize": "Velikost datoteke v bazi podatkov", + "Carbs/Food/Time": "Ogljikovi hidrati/Hrana/Čas", + "You have administration messages": "Imate skrbniška sporočila", + "Admin messages in queue": "Skrbniška sporočila v čakalni vrsti", + "Queue empty": "Vrsta je prazna", + "There are no admin messages in queue": "Ni sporočil za administratorja v čakalni vrsti", + "Please sign in using the API_SECRET to see your administration messages": "Prosim prijavi se s pomočjo API_SECRET kode za pregled sporočil za administratorja", + "Reads enabled in default permissions": "Branje omogočeno v privzetih pravicah", + "Data reads enabled": "Branje podatkov omogočeno", + "Data writes enabled": "Pisanje podatkov omogočeno", + "Data writes not enabled": "Pisanje podatkov onemogočeno", + "Color prediction lines": "Barve črt za napovedi", + "Release Notes": "Opombe Izdaje", + "Check for Updates": "Preveri za posodobitve", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Informacije", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Primarni namen Loopalyzer-ja je grafično prikazati delovanje Loop zaprte zanke. Mogoče bo deloval tudi z drugimi zaprtimi zankami, odprtimi zankami in drugi sistemi. Vendar odvisno od uploader-ja, ki ga uporabljate in s kako frekvenco shranjuje podatke in kako zapolnjuje manjkajoče podatke bodo nekateri grafi imeli luknje ali pa bodo popolnoma prazni. Vedno pazi na to, da so grafi smiselni. Najboljše je pregledati grafe po dnevih in se prepričati da so ustrezni.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer vključuje funkcijo časovnega zamika. Na primer, če imate zajtrk of 07:00 en dan in 08:00 naslednji dan bo krivulja povprečne glukoze teh dveh dni verjetno izgledala sploščena ne bo kazala dejanskega odziva po zajtrku. Funkcija časovnega zamika bo izračunala povprečni čas teh obrokov in zamaknila podatke (oglikovi hidrati, insulin, basal itd) tako, da čas teh obrokov poravnana s povprečnim časom teh obrokov.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "V temu primeru so vsi podatki prvega dne premaknjeni za 30 minut naprej v času in vsi podatki naslednjega dne za 30 minut nazaj v času, tako da izgleda kot da bi imeli zajtrk ob 07:30 dva dni zapored. To omogoča pregled dejanskega odziva povprečnega krvnega sladkorja na obroke.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Časovni zamik poudari čas po povprečnem času začetka obroka v sivi barvi za čas DIA (Duration of Insulin Action - Čas delovanja insulina). Ker so podatki celotnega dne zamaknjeni, pomeni da so lahko podatki izven sivega področja nenatančni.", + "Note that time shift is available only when viewing multiple days.": "Upoštevajte, da je časovni zamik omogočen samo pri pregledu več dni.", + "Please select a maximum of two weeks duration and click Show again.": "Prosim izberi časovni termin največ dveh tednov in klikni Show again.", + "Show profiles table": "Pokaži tabelo profilov", + "Show predictions": "Pokaži napovedi", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Časovni zamik jedi večjih od %1 g ogljikovih hidratov zaužitih med %2 in %3", + "Previous": "Prejšnji", + "Previous day": "Prejšnji dan", + "Next day": "Naslednji dan", + "Next": "Naslednji", + "Temp basal delta": "Sprememba začasni basal", + "Authorized by token": "Pooblaščen s žetonom", + "Auth role": "Pooblaščena vloga", + "view without token": "pregled brez žetona", + "Remove stored token": "Odstrani shranjeni žeton", + "Weekly Distribution": "Tedenska Distribucija", + "Failed authentication": "Neuspešno preverjanje pristnosti", + "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?": "Naprava z IP naslovom %1 je poskusila odpreti Nightscout z napačnimi dostopnimi podatki. Preveri, če imaš uploader z napačnim API_SECRET ali žetonom?", + "Default (with leading zero and U)": "Privzeto (vodilna ničla in U)", + "Concise (with U, without leading zero)": "Kratko (z U, brez vodilne ničle)", + "Minimal (without leading zero and U)": "Minimalno (brez vodilne ničle in U)", + "Small Bolus Display": "Mali Bolus Zaslon", + "Large Bolus Display": "Velik Bolus Zaslon", + "Bolus Display Threshold": "Meja za Bolus Zaslon", + "%1 U and Over": "%1 U in več", + "Event repeated %1 times.": "Dogodek ponovljen %1 - krat.", + "minutes": "minut", + "Last recorded %1 %2 ago.": "Zadnji zapis %1 pred %2.", + "Security issue": "Varnostna težava", + "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.": "Zaznana je šibka API_SECRET koda. Za zmanjšanje tveganja nepooblaščenega dostopa uporabite kombinacijo malih in VELIKIH črk, številk in ne-alfanumeričnih znakov, kot so ! #% & /. Najmanjša dolžina API_SECRET kode je 12 znakov.", + "less than 1": "manj kot 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Geslo MongoDB in API_SECRET se ujemata. To je res slaba ideja. Spremenite oboje in ne uporabite ponovno tega gesla." +} diff --git a/translations/sv_SE.json b/translations/sv_SE.json new file mode 100644 index 00000000000..c6817eb61ae --- /dev/null +++ b/translations/sv_SE.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Lyssnar på port", + "Mo": "Mån", + "Tu": "Tis", + "We": "Ons", + "Th": "Tor", + "Fr": "Fre", + "Sa": "Lör", + "Su": "Sön", + "Monday": "Måndag", + "Tuesday": "Tisdag", + "Wednesday": "Onsdag", + "Thursday": "Torsdag", + "Friday": "Fredag", + "Saturday": "Lördag", + "Sunday": "Söndag", + "Category": "Kategori", + "Subcategory": "Underkategori", + "Name": "Namn", + "Today": "Idag", + "Last 2 days": "Senaste 2 dagarna", + "Last 3 days": "Senaste 3 dagarna", + "Last week": "Senaste veckan", + "Last 2 weeks": "Senaste 2 veckorna", + "Last month": "Senaste månaden", + "Last 3 months": "Senaste 3 månaderna", + "From": "Från", + "To": "Till", + "Notes": "Anteckningar", + "Food": "Mat", + "Insulin": "Insulin", + "Carbs": "Kolhydrater", + "Notes contain": "Anteckningar innehåller", + "Target BG range bottom": "Gräns för nedre blodsockervärde", + "top": "topp", + "Show": "Visa", + "Display": "Visa", + "Loading": "Laddar", + "Loading profile": "Laddar profil", + "Loading status": "Laddar status", + "Loading food database": "Laddar livsmedelsdatabas", + "not displayed": "visas inte", + "Loading CGM data of": "Laddar CGM-data för", + "Loading treatments data of": "Laddar behandlingsdata för", + "Processing data of": "Behandlar data för", + "Portion": "Portion", + "Size": "Storlek", + "(none)": "(ingen)", + "None": "Ingen", + "": "", + "Result is empty": "Resultat saknas", + "Day to day": "Dag för dag", + "Week to week": "Vecka till vecka", + "Daily Stats": "Dygnsstatistik", + "Percentile Chart": "Percentilgraf", + "Distribution": "Distribution", + "Hourly stats": "Timstatistik", + "netIOB stats": "Netto IOB statistik", + "temp basals must be rendered to display this report": "tempbasal måste visas för att visa denna rapport", + "No data available": "Data saknas", + "Low": "Låg", + "In Range": "Inom intervallet", + "Period": "Period", + "High": "Hög", + "Average": "Genomsnittligt", + "Low Quartile": "Nedre kvartil", + "Upper Quartile": "Övre kvartil", + "Quartile": "Kvartil", + "Date": "Datum", + "Normal": "Normal", + "Median": "Median", + "Readings": "Avläsningar", + "StDev": "Standardavvikelse", + "Daily stats report": "Dygnsstatistik", + "Glucose Percentile report": "Glukosrapport i procent", + "Glucose distribution": "Glukosdistribution", + "days total": "antal dagar", + "Total per day": "Totalt per dag", + "Overall": "Genomsnitt", + "Range": "Intervall", + "% of Readings": "% av avläsningar", + "# of Readings": "# av avläsningar", + "Mean": "Genomsnitt", + "Standard Deviation": "Standardavvikelse", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "Beräknad A1c*", + "Weekly Success": "Veckoresultat", + "There is not sufficient data to run this report. Select more days.": "Inte tillräckling med data för att visa rapporten. Välj fler dagar.", + "Using stored API secret hash": "Använd hemlig API-nyckel", + "No API secret hash stored yet. You need to enter API secret.": "Hemlig API-nyckel saknas. Du måste ange API hemlighet.", + "Database loaded": "Databas laddad", + "Error: Database failed to load": "Fel: Databasen kunde inte läsas in", + "Error": "Fel", + "Create new record": "Skapa ny post", + "Save record": "Spara post", + "Portions": "Portion", + "Unit": "Enhet", + "GI": "GI", + "Edit record": "Editera post", + "Delete record": "Radera post", + "Move to the top": "Gå till toppen", + "Hidden": "Dold", + "Hide after use": "Dölj efter användning", + "Your API secret must be at least 12 characters long": "Hemlig API-nyckel måsta innehålla 12 tecken", + "Bad API secret": "Felaktig API-nyckel", + "API secret hash stored": "Lagrad hemlig API-hash", + "Status": "Status", + "Not loaded": "Ej laddad", + "Food Editor": "Födoämneseditor", + "Your database": "Din databas", + "Filter": "Filter", + "Save": "Spara", + "Clear": "Rensa", + "Record": "Post", + "Quick picks": "Snabbval", + "Show hidden": "Visa dolda", + "Your API secret or token": "Din API hemlighet eller token", + "Remember this device. (Do not enable this on public computers.)": "Kom ihåg den här enheten. (Aktivera inte detta på offentliga datorer.)", + "Treatments": "Behandlingar", + "Time": "Tid", + "Event Type": "Händelsetyp", + "Blood Glucose": "Blodsocker", + "Entered By": "Inlagt av", + "Delete this treatment?": "Ta bort händelse?", + "Carbs Given": "Antal kolhydrater", + "Insulin Given": "Insulindos", + "Event Time": "Klockslag", + "Please verify that the data entered is correct": "Vänligen verifiera att inlagd data är korrekt", + "BG": "BS", + "Use BG correction in calculation": "Använd BS för beräkning av korrigering", + "BG from CGM (autoupdated)": "BS från CGM (automatiskt)", + "BG from meter": "BS från blodsockermätare", + "Manual BG": "Manuellt BS", + "Quickpick": "Snabbval", + "or": "eller", + "Add from database": "Lägg till från databas", + "Use carbs correction in calculation": "Använd kolhydratkorrektion för beräkning", + "Use COB correction in calculation": "Använd aktiva kolhydrater för beräkning", + "Use IOB in calculation": "Använd aktivt insulin för beräkning", + "Other correction": "Övrig korrektion", + "Rounding": "Avrundning", + "Enter insulin correction in treatment": "Ange insulinkorrektion för händelse", + "Insulin needed": "Beräknad insulinmängd", + "Carbs needed": "Beräknad kolhydratmängd", + "Carbs needed if Insulin total is negative value": "Nödvändig kolhydratmängd för angiven insulinmängd", + "Basal rate": "Basaldos", + "60 minutes earlier": "60 min tidigare", + "45 minutes earlier": "45 min tidigare", + "30 minutes earlier": "30 min tidigare", + "20 minutes earlier": "20 min tidigare", + "15 minutes earlier": "15 min tidigare", + "Time in minutes": "Tid i minuter", + "15 minutes later": "15 min senare", + "20 minutes later": "20 min senare", + "30 minutes later": "30 min senare", + "45 minutes later": "45 min senare", + "60 minutes later": "60 min senare", + "Additional Notes, Comments": "Notering, övrigt", + "RETRO MODE": "Retroläge", + "Now": "Nu", + "Other": "Övrigt", + "Submit Form": "Överför händelse", + "Profile Editor": "Editera profil", + "Reports": "Rapportverktyg", + "Add food from your database": "Lägg till livsmedel från databas", + "Reload database": "Ladda om databas", + "Add": "Lägg till", + "Unauthorized": "Ej behörig", + "Entering record failed": "Lägga till post nekas", + "Device authenticated": "Enhet autentiserad", + "Device not authenticated": "Enhet EJ autentiserad", + "Authentication status": "Autentiseringsstatus", + "Authenticate": "Autentisera", + "Remove": "Ta bort", + "Your device is not authenticated yet": "Din enhet är ej autentiserad", + "Sensor": "Sensor", + "Finger": "Finger", + "Manual": "Manuell", + "Scale": "Skala", + "Linear": "Linjär", + "Logarithmic": "Logaritmisk", + "Logarithmic (Dynamic)": "Logaritmisk (Dynamisk)", + "Insulin-on-Board": "Aktivt insulin (IOB)", + "Carbs-on-Board": "Aktiva kolhydrater (COB)", + "Bolus Wizard Preview": "Boluskalkylator (BWP)", + "Value Loaded": "Laddat värde", + "Cannula Age": "Kanylålder (CAGE)", + "Basal Profile": "Basalprofil", + "Silence for 30 minutes": "Tyst i 30 min", + "Silence for 60 minutes": "Tyst i 60 min", + "Silence for 90 minutes": "Tyst i 90 min", + "Silence for 120 minutes": "Tyst i 120 min", + "Settings": "Inställningar", + "Units": "Enheter", + "Date format": "Datumformat", + "12 hours": "12-timmars", + "24 hours": "24-timmars", + "Log a Treatment": "Ange händelse", + "BG Check": "Blodsockerkontroll", + "Meal Bolus": "Måltidsbolus", + "Snack Bolus": "Mellanmålsbolus", + "Correction Bolus": "Korrektionsbolus", + "Carb Correction": "Kolhydratskorrektion", + "Note": "Notering", + "Question": "Fråga", + "Exercise": "Aktivitet", + "Pump Site Change": "Pump/nålbyte", + "CGM Sensor Start": "Sensorstart", + "CGM Sensor Stop": "CGM Sensor Stoppa", + "CGM Sensor Insert": "Sensorbyte", + "Sensor Code": "Sensor Kod", + "Transmitter ID": "Sändare ID", + "Dexcom Sensor Start": "Dexcom sensorstart", + "Dexcom Sensor Change": "Dexcom sensorbyte", + "Insulin Cartridge Change": "Insulinreservoarbyte", + "D.A.D. Alert": "Diabeteshundlarm (Duktig vovve!)", + "Glucose Reading": "Glukosvärde", + "Measurement Method": "Mätmetod", + "Meter": "Mätare", + "Amount in grams": "Antal gram", + "Amount in units": "Antal enheter", + "View all treatments": "Visa behandlingar", + "Enable Alarms": "Aktivera larm", + "Pump Battery Change": "Byte av pumpbatteri", + "Pump Battery Age": "Pump Batteriets ålder", + "Pump Battery Low Alarm": "Pumpbatteri lågt Alarm", + "Pump Battery change overdue!": "Pumpbatteriet måste bytas!", + "When enabled an alarm may sound.": "När markerad är ljudlarm aktivt", + "Urgent High Alarm": "Brådskande högt larmvärde", + "High Alarm": "Högt larmvärde", + "Low Alarm": "Lågt larmvärde", + "Urgent Low Alarm": "Brådskande lågt larmvärde", + "Stale Data: Warn": "Förfluten data: Varning!", + "Stale Data: Urgent": "Brådskande varning, inaktuell data", + "mins": "min", + "Night Mode": "Nattläge", + "When enabled the page will be dimmed from 10pm - 6am.": "När aktiverad dimmas sidan mellan 22:00 - 06:00", + "Enable": "Aktivera", + "Show Raw BG Data": "Visa RAW-data", + "Never": "Aldrig", + "Always": "Alltid", + "When there is noise": "När det är brus", + "When enabled small white dots will be displayed for raw BG data": "När aktiverad visar de vita punkterna RAW-blodglukosevärden", + "Custom Title": "Egen titel", + "Theme": "Tema", + "Default": "Standard", + "Colors": "Färg", + "Colorblind-friendly colors": "Högkontrastfärger", + "Reset, and use defaults": "Återställ standardvärden", + "Calibrations": "Kalibreringar", + "Alarm Test / Smartphone Enable": "Testa alarm / Aktivera Smatphone", + "Bolus Wizard": "Boluskalkylator", + "in the future": "framtida", + "time ago": "förfluten tid", + "hr ago": "timmar sedan", + "hrs ago": "Timmar sedan", + "min ago": "minut sedan", + "mins ago": "minuter sedan", + "day ago": "dag sedan", + "days ago": "dagar sedan", + "long ago": "länge sedan", + "Clean": "Rent", + "Light": "Lätt", + "Medium": "Måttligt", + "Heavy": "Rikligt", + "Treatment type": "Behandlingstyp", + "Raw BG": "RAW-BS", + "Device": "Enhet", + "Noise": "Brus", + "Calibration": "Kalibrering", + "Show Plugins": "Visa tillägg", + "About": "Om", + "Value in": "Värde om", + "Carb Time": "Kolhydratstid", + "Language": "Språk", + "Add new": "Lägg till ny", + "g": "g", + "ml": "ml", + "pcs": "st", + "Drag&drop food here": "Dra&Släpp mat här", + "Care Portal": "Vård Portal", + "Medium/Unknown": "Medium/Okänt", + "IN THE FUTURE": "Framtida", + "Order": "Sortering", + "oldest on top": "Äldst först", + "newest on top": "Nyast först", + "All sensor events": "Alla sensorhändelser", + "Remove future items from mongo database": "Ta bort framtida händelser från mongodatabasen", + "Find and remove treatments in the future": "Hitta och ta bort framtida behandlingar", + "This task find and remove treatments in the future.": "Denna uppgift hittar och rensar framtida händelser", + "Remove treatments in the future": "Ta bort framtida händelser", + "Find and remove entries in the future": "Hitta och ta bort framtida händelser", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Denna uppgift hittar och tar bort framtida CGM-data skapad vid felaktig tidsinställning", + "Remove entries in the future": "Ta bort framtida händelser", + "Loading database ...": "Laddar databas ...", + "Database contains %1 future records": "Databas innehåller %1 framtida händelser", + "Remove %1 selected records?": "Ta bort %1 valda händelser", + "Error loading database": "Fel vid laddning av databas", + "Record %1 removed ...": "Händelse %1 borttagen ...", + "Error removing record %1": "Fel vid borttagning av %1", + "Deleting records ...": "Tar bort händelser ...", + "%1 records deleted": "%1 poster raderade", + "Clean Mongo status database": "Rensa Mongo status databas", + "Delete all documents from devicestatus collection": "Ta bort alla dokument i devicestatus-samlingen", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Denna uppgift tar bort alla dokument från devicestatus-samlingen. Användbar när uppladdarens batteristatus inte uppdateras ordentligt.", + "Delete all documents": "Ta bort alla dokument", + "Delete all documents from devicestatus collection?": "Ta bort alla dokument från devicestatus-samlingen?", + "Database contains %1 records": "Databasen innehåller %1 händelser", + "All records removed ...": "Alla händelser raderade ...", + "Delete all documents from devicestatus collection older than 30 days": "Ta bort alla dokument från devicestatus-samling äldre än 30 dagar", + "Number of Days to Keep:": "Antal dagar att behålla:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Denna uppgift tar bort alla dokument från devicestatus-samling som är äldre än 30 dagar. Användbart när uppladdarens batteristatus inte är korrekt uppdaterad.", + "Delete old documents from devicestatus collection?": "Ta bort gamla dokument från devicestatus-samling?", + "Clean Mongo entries (glucose entries) database": "Rensa Mongo poster (glukos-poster) databas", + "Delete all documents from entries collection older than 180 days": "Ta bort alla dokument från insamling äldre än 180 dagar", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Denna uppgift tar bort alla dokument från insamlingsuppgifter som är äldre än 180 dagar. Användbar när uppladdarens batteristatus inte uppdateras ordentligt.", + "Delete old documents": "Ta bort gamla dokument", + "Delete old documents from entries collection?": "Ta bort gamla dokument från postsamlingen?", + "%1 is not a valid number": "%1 är inte ett giltigt nummer", + "%1 is not a valid number - must be more than 2": "%1 är inte ett giltigt tal - måste vara mer än 2", + "Clean Mongo treatments database": "Clean Mongo behandlingar databas", + "Delete all documents from treatments collection older than 180 days": "Ta bort alla dokument från behandlingar som är äldre än 180 dagar", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Denna uppgift tar bort alla dokument från behandlingar som är äldre än 180 dagar. Användbar när uppladdarens batteristatus inte är korrekt uppdaterad.", + "Delete old documents from treatments collection?": "Ta bort gamla dokument från samlingen av behandlingar?", + "Admin Tools": "Adminverktyg", + "Nightscout reporting": "Nightscout - Statistik", + "Cancel": "Avbryt", + "Edit treatment": "Redigera behandling", + "Duration": "Varaktighet", + "Duration in minutes": "Varaktighet i minuter", + "Temp Basal": "Temporär basal", + "Temp Basal Start": "Temporär basalstart", + "Temp Basal End": "Temporär basalavslut", + "Percent": "Procent", + "Basal change in %": "Basaländring i %", + "Basal value": "Basalvärde", + "Absolute basal value": "Absolut basalvärde", + "Announcement": "Avisering", + "Loading temp basal data": "Laddar temporär basaldata", + "Save current record before changing to new?": "Spara aktuell data innan skifte till ny?", + "Profile Switch": "Ny profil", + "Profile": "Profil", + "General profile settings": "Allmän profilinställning", + "Title": "Titel", + "Database records": "Databashändelser", + "Add new record": "Lägg till ny händelse", + "Remove this record": "Ta bort denna händelse", + "Clone this record to new": "Kopiera denna händelse till ny", + "Record valid from": "Händelse giltig från", + "Stored profiles": "Lagrad profil", + "Timezone": "Tidszon", + "Duration of Insulin Activity (DIA)": "Verkningstid för insulin (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Beskriver under hur lång tid insulinet verkar. Varierar mellan patienter. Typisk varaktighet 3-4 timmar.", + "Insulin to carb ratio (I:C)": "Insulin-Kolhydratskvot", + "Hours:": "Timmar:", + "hours": "timmar", + "g/hour": "g/timme", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "gram kolhydrater per enhet insulin. Antal gram kolhydrater varje enhet insulin sänker", + "Insulin Sensitivity Factor (ISF)": "Insulinkänslighetsfaktor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dl eller mmol per enhet insulin. Hur varje enhet insulin sänker blodsockret", + "Carbs activity / absorption rate": "Kolhydratstid", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "gram per tidsenhet. Representerar både ändring i aktiva kolhydrater per tidsenhet som mängden kolhydrater som tas upp under denna tid. Kolhydratsupptag / aktivitetskurvor är svårare att förutspå än aktivt insulin men kan beräknas genom att använda en startfördröjning följd av en konstant absorbtionsgrad (g/timme) ", + "Basal rates [unit/hour]": "Basal [enhet/t]", + "Target BG range [mg/dL,mmol/L]": "Önskvärt blodsockerintervall [mg/dl,mmol]", + "Start of record validity": "Starttid för händelse", + "Icicle": "Istapp", + "Render Basal": "Generera Basal", + "Profile used": "Vald profil", + "Calculation is in target range.": "Inom intervallområde", + "Loading profile records ...": "Laddar profildata ...", + "Values loaded.": "Värden laddas", + "Default values used.": "Standardvärden valda", + "Error. Default values used.": "Error. Standardvärden valda.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Tidsintervall för målområde låg och hög stämmer ej", + "Valid from:": "Giltig från:", + "Save current record before switching to new?": "Spara före byte till nytt?", + "Add new interval before": "Lägg till nytt intervall före", + "Delete interval": "Ta bort intervall", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Combo-bolus", + "Difference": "Skillnad", + "New time": "Ny tid", + "Edit Mode": "Editeringsläge", + "When enabled icon to start edit mode is visible": "Ikon visas när editeringsläge är aktivt", + "Operation": "Åtgärd", + "Move": "Flytta", + "Delete": "Ta bort", + "Move insulin": "Flytta insulin", + "Move carbs": "Flytta kolhydrater", + "Remove insulin": "Ta bort insulin", + "Remove carbs": "Ta bort kolhydrater", + "Change treatment time to %1 ?": "Ändra behandlingstid till %1 ?", + "Change carbs time to %1 ?": "Ändra kolhydratstid till %1 ?", + "Change insulin time to %1 ?": "Ändra insulintid till %1 ?", + "Remove treatment ?": "Ta bort behandling ?", + "Remove insulin from treatment ?": "Ta bort insulin från behandling ?", + "Remove carbs from treatment ?": "Ta bort kolhydrater från behandling ?", + "Rendering": "Renderar", + "Loading OpenAPS data of": "Laddar OpenAPS data för", + "Loading profile switch data": "Laddar ny profildata", + "Redirecting you to the Profile Editor to create a new profile.": "Fel profilinställning.\nIngen profil vald för vald tid.\nOmdirigerar för att skapa ny profil.", + "Pump": "Pump", + "Sensor Age": "Sensorålder (SAGE)", + "Insulin Age": "Insulinålder (IAGE)", + "Temporary target": "Tillfälligt mål", + "Reason": "Orsak", + "Eating soon": "Snart matdags", + "Top": "Toppen", + "Bottom": "Botten", + "Activity": "Aktivitet", + "Targets": "Mål", + "Bolus insulin:": "Bolusinsulin:", + "Base basal insulin:": "Basalinsulin:", + "Positive temp basal insulin:": "Positiv tempbasal insulin:", + "Negative temp basal insulin:": "Negativ tempbasal för insulin:", + "Total basal insulin:": "Total dagsdos basalinsulin:", + "Total daily insulin:": "Total dagsdos insulin", + "Unable to save Role": "Det går inte att spara Roll", + "Unable to delete Role": "Kan ej ta bort roll", + "Database contains %1 roles": "Databasen innehåller %1 roller", + "Edit Role": "Editera roll", + "admin, school, family, etc": "Administratör, skola, familj, etc", + "Permissions": "Rättigheter", + "Are you sure you want to delete: ": "Är du säker att du vill ta bort:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Varje roll kommer få en eller flera rättigheter. * är en wildcard, rättigheter sätts hirarkiskt med : som avgränsare.", + "Add new Role": "Lägg till roll", + "Roles - Groups of People, Devices, etc": "Roller - Grupp av användare, Enheter, etc", + "Edit this role": "Editera denna roll", + "Admin authorized": "Administratorgodkänt", + "Subjects - People, Devices, etc": "Ämnen - Användare, Enheter, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Varje ämne får en unik säkerhetsnyckel och en eller flera roller. Klicka på accessnyckeln för att öppna en ny vy med det valda ämnet, denna hemliga länk kan sedan delas.", + "Add new Subject": "Lägg till nytt ämne", + "Unable to save Subject": "Det gick inte att spara Ämnet", + "Unable to delete Subject": "Kan ej ta bort ämne", + "Database contains %1 subjects": "Databasen innehåller %1 ämnen", + "Edit Subject": "Editera ämne", + "person, device, etc": "person,enhet,etc", + "role1, role2": "Roll1, Roll2", + "Edit this subject": "Editera ämnet", + "Delete this subject": "Ta bort ämnet", + "Roles": "Roller", + "Access Token": "Åtkomstnyckel", + "hour ago": "timme sedan", + "hours ago": "timmar sedan", + "Silence for %1 minutes": "Tyst i %1 minuter", + "Check BG": "Kontrollera blodglukos", + "BASAL": "BASAL", + "Current basal": "Nuvarande basal", + "Sensitivity": "Insulinkönslighet (ISF)", + "Current Carb Ratio": "Gällande kolhydratkvot", + "Basal timezone": "Basal tidszon", + "Active profile": "Aktiv profil", + "Active temp basal": "Aktiv tempbasal", + "Active temp basal start": "Aktiv tempbasal start", + "Active temp basal duration": "Aktiv tempbasal varaktighetstid", + "Active temp basal remaining": "Återstående tempbasaltid", + "Basal profile value": "Basalprofil värde", + "Active combo bolus": "Aktiv kombobolus", + "Active combo bolus start": "Aktiv kombobolus start", + "Active combo bolus duration": "Aktiv kombibolus varaktighet", + "Active combo bolus remaining": "Återstående aktiv kombibolus", + "BG Delta": "BS deltavärde", + "Elapsed Time": "Förfluten tid", + "Absolute Delta": "Absolut deltavärde", + "Interpolated": "Interpolerad", + "BWP": "Boluskalkylator", + "Urgent": "Akut", + "Warning": "Varning", + "Info": "Information", + "Lowest": "Lägsta", + "Snoozing high alarm since there is enough IOB": "Snoozar höglarm då aktivt insulin är tillräckligt", + "Check BG, time to bolus?": "Kontrollera BS, dags att ge bolus?", + "Notice": "Notering", + "required info missing": "Nödvändig information saknas", + "Insulin on Board": "Aktivt insulin (IOB)", + "Current target": "Aktuellt mål", + "Expected effect": "Förväntad effekt", + "Expected outcome": "Förväntat resultat", + "Carb Equivalent": "Kolhydratsinnehåll", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Överskott av insulin motsvarande %1U mer än nödvändigt för att nå lågt målvärde, kolhydrater ej medräknade", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Överskott av insulin motsvarande %1U mer än nödvändigt för att nå lågt målvärde, SÄKERSTÄLL ATT IOB TÄCKS AV KOLHYDRATER", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U minskning nödvändig i aktivt insulin för att nå lågt målvärde, för hög basal?", + "basal adjustment out of range, give carbs?": "basaländring utanför gräns, ge kolhydrater?", + "basal adjustment out of range, give bolus?": "basaländring utanför gräns, ge bolus?", + "above high": "över hög nivå", + "below low": "under låg nivå", + "Projected BG %1 target": "Önskat BS %1 mål", + "aiming at": "önskad utgång", + "Bolus %1 units": "Bolus %1 enheter", + "or adjust basal": "eller justera basal", + "Check BG using glucometer before correcting!": "Kontrollera blodglukos med fingerstick före korrigering!", + "Basal reduction to account %1 units:": "Basalsänkning för att nå %1 enheter", + "30m temp basal": "30 minuters temporär basal", + "1h temp basal": "60 minuters temporär basal", + "Cannula change overdue!": "Infusionsset, bytestid överskriden", + "Time to change cannula": "Dags att byta infusionsset", + "Change cannula soon": "Byt infusionsset snart", + "Cannula age %1 hours": "Infusionsset tid %1 timmar", + "Inserted": "Applicerad", + "CAGE": "Nål", + "COB": "COB", + "Last Carbs": "Senaste kolhydrater", + "IAGE": "Insulinålder", + "Insulin reservoir change overdue!": "Insulinbytestid överskriden", + "Time to change insulin reservoir": "Dags att byta insulinreservoar", + "Change insulin reservoir soon": "Byt insulinreservoar snart", + "Insulin reservoir age %1 hours": "Insulinreservoarsålder %1 timmar", + "Changed": "Bytt", + "IOB": "IOB", + "Careportal IOB": "IOB i Careportal", + "Last Bolus": "Senaste Bolus", + "Basal IOB": "Basal IOB", + "Source": "Källa", + "Stale data, check rig?": "Gammal data, kontrollera rigg?", + "Last received:": "Senast mottagen:", + "%1m ago": "%1m sedan", + "%1h ago": "%1h sedan", + "%1d ago": "%1d sedan", + "RETRO": "RETRO", + "SAGE": "Sensor", + "Sensor change/restart overdue!": "Sensor byte/omstart överskriden!", + "Time to change/restart sensor": "Dags att byta/starta om sensorn", + "Change/restart sensor soon": "Byt/starta om sensorn snart", + "Sensor age %1 days %2 hours": "Sensorålder %1 dagar %2 timmar", + "Sensor Insert": "Sensor insättning", + "Sensor Start": "Sensorstart", + "days": "dagar", + "Insulin distribution": "Insulindistribution", + "To see this report, press SHOW while in this view": "För att se denna rapport, klicka på \"Visa\"", + "AR2 Forecast": "AR2 Förutsägelse", + "OpenAPS Forecasts": "OpenAPS Förutsägelse", + "Temporary Target": "Tillfälligt mål", + "Temporary Target Cancel": "Avsluta tillfälligt mål", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profiler", + "Time in fluctuation": "Tid i fluktation", + "Time in rapid fluctuation": "Tid i snabb fluktation", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Detta är en grov uppskattning som kan vara missvisande. Det ersätter inte blodprov. Formeln är hämtad från:", + "Filter by hours": "Filtrera per timme", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Tid i fluktuation och tid i snabb fluktuation mäter% av tiden under den undersökta perioden, under vilken blodsockret har förändrats relativt snabbt eller snabbt. Lägre värden är bättre", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Medel Total Daglig Förändring är summan av absolutvärdet av alla glukosförändringar under den undersökta perioden, dividerat med antalet dagar. Lägre är bättre.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Medelvärde per timme är summan av absolutvärdet av alla glukosförändringar under den undersökta perioden dividerat med antalet timmar under perioden. Lägre är bättre.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Utanför intervall RMS beräknas genom att kvadrera avståndet utanför intervallet för alla glukosavläsningar under den undersökta perioden, summera dem, dividera med räkningen och ta kvadratroten. Denna mätning liknar procentandelen inom intervallet men väger avläsningar långt utanför området högre. Lägre värden är bättre.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">här.", + "Mean Total Daily Change": "Medel Total Daglig Förändring", + "Mean Hourly Change": "Medelvärde per timme", + "FortyFiveDown": "något sjunkande", + "FortyFiveUp": "något stigande", + "Flat": "håller", + "SingleUp": "stiger", + "SingleDown": "sjunker", + "DoubleDown": "snabbt sjunkande", + "DoubleUp": "snabbt stigande", + "virtAsstUnknown": "Det värdet är okänt just nu. Se din Nightscout sida för mer information.", + "virtAsstTitleAR2Forecast": "AR2 Prognos", + "virtAsstTitleCurrentBasal": "Nuvarande basal", + "virtAsstTitleCurrentCOB": "Nuvarande COB", + "virtAsstTitleCurrentIOB": "Nuvarande IOB", + "virtAsstTitleLaunch": "Välkommen till Nightscout", + "virtAsstTitleLoopForecast": "Loop prognos", + "virtAsstTitleLastLoop": "Senaste Loopen", + "virtAsstTitleOpenAPSForecast": "OpenAPS Prognos", + "virtAsstTitlePumpReservoir": "Återstående insulin", + "virtAsstTitlePumpBattery": "Pump Batteri", + "virtAsstTitleRawBG": "Nuvarande Rå BG", + "virtAsstTitleUploaderBattery": "Batteri uppladdare", + "virtAsstTitleCurrentBG": "Nuvarande BG", + "virtAsstTitleFullStatus": "Fullständig status", + "virtAsstTitleCGMMode": "CGM läge", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM session ålder", + "virtAsstTitleCGMTxStatus": "CGM Sändarstatus", + "virtAsstTitleCGMTxAge": "CGM Sändare Ålder", + "virtAsstTitleCGMNoise": "CGM Brus", + "virtAsstTitleDelta": "Blodsocker Delta", + "virtAsstStatus": "%1 och %2 från och med %3.", + "virtAsstBasal": "%1 nuvarande basal är %2 enheter per timme", + "virtAsstBasalTemp": "%1 temp basal av %2 enheter per timme kommer att sluta %3", + "virtAsstIob": "och du har %1 insulin ombord.", + "virtAsstIobIntent": "Du har %1 insulin ombord", + "virtAsstIobUnits": "%1 enheter av", + "virtAsstLaunch": "Vad skulle du vilja kolla på Nightscout?", + "virtAsstPreamble": "Din", + "virtAsstPreamble3person": "%1 har en ", + "virtAsstNoInsulin": "nej", + "virtAsstUploadBattery": "Din uppladdares batteri är %1", + "virtAsstReservoir": "Du har %1 enheter kvar", + "virtAsstPumpBattery": "Din pumps batteri är %1 %2", + "virtAsstUploaderBattery": "Din uppladdares batteri är %1", + "virtAsstLastLoop": "Senaste lyckade loop var %1", + "virtAsstLoopNotAvailable": "Loop plugin verkar inte vara aktiverad", + "virtAsstLoopForecastAround": "Enligt Loops förutsägelse förväntas du bli around %1 inom %2", + "virtAsstLoopForecastBetween": "Enligt Loops förutsägelse förväntas du bli between %1 and %2 inom %3", + "virtAsstAR2ForecastAround": "Enligt AR2 prognosen förväntas du vara runt %1 under nästa %2", + "virtAsstAR2ForecastBetween": "Enligt AR2 prognosen förväntas du vara mellan %1 och %2 under de kommande %3", + "virtAsstForecastUnavailable": "Förutsägelse ej möjlig med tillgänlig data", + "virtAsstRawBG": "Ditt raw blodsocker är %1", + "virtAsstOpenAPSForecast": "OpenAPS slutgiltigt blodsocker är %1", + "virtAsstCob3person": "%1 har %2 kolhydrater ombord", + "virtAsstCob": "Du har %1 kolhydrater ombord", + "virtAsstCGMMode": "Ditt CGM läge var %1 från och med %2.", + "virtAsstCGMStatus": "Din CGM status var %1 från och med %2.", + "virtAsstCGMSessAge": "Din CGM session har varit aktiv i %1 dagar och %2 timmar.", + "virtAsstCGMSessNotStarted": "Det finns för närvarande ingen aktiv GMO-session.", + "virtAsstCGMTxStatus": "Din CGM sändarstatus var %1 från och med %2.", + "virtAsstCGMTxAge": "Din CGM sändare är %1 dagar gammal.", + "virtAsstCGMNoise": "Ditt CGM brus var %1 från och med %2.", + "virtAsstCGMBattOne": "Ditt CGM batteri var %1 volt från och med %2.", + "virtAsstCGMBattTwo": "Dina CGM batterinivåer var %1 volt och %2 volt från och med %3.", + "virtAsstDelta": "Ditt delta var %1 mellan %2 och %3.", + "virtAsstDeltaEstimated": "Ditt uppskattade delta var %1 mellan %2 och %3.", + "virtAsstUnknownIntentTitle": "Okänd Avsikt", + "virtAsstUnknownIntentText": "Jag är ledsen, jag vet inte vad du ber om.", + "Fat [g]": "Fett [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energi [kJ]", + "Clock Views:": "Visa klocka:", + "Clock": "Klocka", + "Color": "Färg", + "Simple": "Simpel", + "TDD average": "Genomsnittlig daglig mängd insulin", + "Bolus average": "Bolus genomsnitt", + "Basal average": "Basal genomsnitt", + "Base basal average:": "Bas basal genomsnitt:", + "Carbs average": "Genomsnittlig mängd kolhydrater per dag", + "Eating Soon": "Äter snart", + "Last entry {0} minutes ago": "Senaste värde {0} minuter sedan", + "change": "byta", + "Speech": "Tal", + "Target Top": "Högt målvärde", + "Target Bottom": "Lågt målvärde", + "Canceled": "Avbruten", + "Meter BG": "Blodsockermätare BG", + "predicted": "prognos", + "future": "framtida", + "ago": "förfluten", + "Last data received": "Data senast mottagen", + "Clock View": "Visa klocka", + "Protein": "Protein", + "Fat": "Fett", + "Protein average": "Protein genomsnitt", + "Fat average": "Fett genomsnitt", + "Total carbs": "Totalt kolhydrater", + "Total protein": "Totalt protein", + "Total fat": "Totalt fett", + "Database Size": "Databasens Storlek", + "Database Size near its limits!": "Databasens Storlek nära dess gränser!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Databasens storlek är %1 MiB av %2 MiB. Säkerhetskopiera och rensa upp databasen!", + "Database file size": "Databasens filstorlek", + "%1 MiB of %2 MiB (%3%)": "%1 MiB av %2 MiB (%3%)", + "Data size": "Datastorlek", + "virtAsstDatabaseSize": "%1 MiB. Det är %2% av tillgänglig databasutrymme.", + "virtAsstTitleDatabaseSize": "Databasens filstorlek", + "Carbs/Food/Time": "Kolhydrater/Mat/Tid", + "You have administration messages": "Du har administrationsmeddelanden", + "Admin messages in queue": "Admin meddelanden i kö", + "Queue empty": "Kön är tom", + "There are no admin messages in queue": "Det finns inga administratörsmeddelanden i kön", + "Please sign in using the API_SECRET to see your administration messages": "Logga in med API_SECRET för att se dina administrationsmeddelanden", + "Reads enabled in default permissions": "Läser aktiverade i standardbehörigheter", + "Data reads enabled": "Dataläsningar aktiverade", + "Data writes enabled": "Dataskrivningar aktiverade", + "Data writes not enabled": "Dataskrivningar är inte aktiverade", + "Color prediction lines": "Färgsätt prognosrader", + "Release Notes": "Ändringslogg", + "Check for Updates": "Sök efter uppdateringar", + "Open Source": "Öppen källkod", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Det primära syftet med Loopalyzer är att visualisera hur Loop slutna loop-systemet fungerar. Det kan fungera med andra installationer också, både stängda och öppna loop, och icke loop. Men beroende på vilken uppladdare du använder, hur ofta den kan samla in dina data och ladda upp, och hur den kan återfylla saknade data vissa grafer kan ha luckor eller till och med vara helt tom. Se alltid till att graferna ser rimliga ut. Bäst är att se en dag i taget och bläddra igenom ett antal dagar först att se.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer innehåller en time shift-funktion. Om du till exempel äter frukost kl 07:00 en dag och kl 08:00 dagen efter din genomsnittliga blodsockerkurva dessa två dagar kommer sannolikt att se tillplattad ut och inte visa det faktiska svaret efter en frukost. Tidsskiftet kommer att beräkna den genomsnittliga tiden dessa måltider äts och sedan flytta alla data (kolhydrater, insulin, basal etc.) under båda dagarna motsvarande tidsskillnad så att båda måltiderna överensstämmer med den genomsnittliga måltiden starttid.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "I detta exempel skjuts all data från första dagen 30 minuter framåt i tid och all data från andra dagen 30 minuter bakåt i tiden så det verkar som om du hade haft frukost klockan 07:30 båda dagarna. Detta gör att du kan se din faktiska genomsnittliga blodsockersvar från en måltid.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Tidsskiftet belyser perioden efter den genomsnittliga måltidens starttid i grått, under tiden för DIA (Varaktighet av Insulin Action). Eftersom alla datapunkter hela dagen skiftas kurvorna utanför det gråa området kanske inte är korrekta.", + "Note that time shift is available only when viewing multiple days.": "Observera att tidsförskjutning endast är tillgänglig vid visning av flera dagar.", + "Please select a maximum of two weeks duration and click Show again.": "Välj högst två veckor varaktighet och klicka på Visa igen.", + "Show profiles table": "Visa profiltabellen", + "Show predictions": "Visa förutsägelser", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift på måltider större än %1 g kolhydrater konsumeras mellan %2 och %3", + "Previous": "Föregående", + "Previous day": "Föregående dag", + "Next day": "Nästa dag", + "Next": "Nästa", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Auktoriserad av token", + "Auth role": "Auth roll", + "view without token": "visa utan token", + "Remove stored token": "Ta bort sparad token", + "Weekly Distribution": "Fördelning veckovis", + "Failed authentication": "Misslyckad autentisering", + "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?": "En enhet på IP-adress %1 försökte autentisera med Nightscout med fel uppgifter. Kontrollera om du har en uppladdare med fel API_SECRET eller token?", + "Default (with leading zero and U)": "Standard (med ledande nolla och U)", + "Concise (with U, without leading zero)": "Kortfattad (med U, utan ledande nolla)", + "Minimal (without leading zero and U)": "Minimal (utan ledande nolla och U)", + "Small Bolus Display": "Liten bolusskärm", + "Large Bolus Display": "Stor bolusskärm", + "Bolus Display Threshold": "Tröskelvärde för bolusdisplay", + "%1 U and Over": "%1 U och Över", + "Event repeated %1 times.": "Händelsen upprepas %1 gånger.", + "minutes": "minuter", + "Last recorded %1 %2 ago.": "Senast inspelad för %1 %2 sedan.", + "Security issue": "Säkerhetsproblem", + "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.": "Svag API_SECRET upptäckt. Använd en blandning av små och KAPITALA bokstäver, siffror och icke-alfanumeriska tecken som !#%&/ för att minska risken för obehörig åtkomst. Den minsta längden på API_SECRET är 12 tecken.", + "less than 1": "mindre än 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB lösenordet och API_SECRET matchar. Detta är en riktigt dålig idé. Vänligen ändra båda och återanvänd inte lösenorden i systemet." +} diff --git a/translations/ta_IN.json b/translations/ta_IN.json new file mode 100644 index 00000000000..37e1d47e97d --- /dev/null +++ b/translations/ta_IN.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Listening on port", + "Mo": "Mo", + "Tu": "Tu", + "We": "We", + "Th": "Th", + "Fr": "Fr", + "Sa": "Sa", + "Su": "Su", + "Monday": "Monday", + "Tuesday": "Tuesday", + "Wednesday": "Wednesday", + "Thursday": "Thursday", + "Friday": "Friday", + "Saturday": "Saturday", + "Sunday": "Sunday", + "Category": "Category", + "Subcategory": "Subcategory", + "Name": "Name", + "Today": "Today", + "Last 2 days": "Last 2 days", + "Last 3 days": "Last 3 days", + "Last week": "Last week", + "Last 2 weeks": "Last 2 weeks", + "Last month": "Last month", + "Last 3 months": "Last 3 months", + "From": "From", + "To": "To", + "Notes": "Notes", + "Food": "Food", + "Insulin": "Insulin", + "Carbs": "Carbs", + "Notes contain": "Notes contain", + "Target BG range bottom": "Target BG range bottom", + "top": "top", + "Show": "Show", + "Display": "Display", + "Loading": "Loading", + "Loading profile": "Loading profile", + "Loading status": "Loading status", + "Loading food database": "Loading food database", + "not displayed": "not displayed", + "Loading CGM data of": "Loading CGM data of", + "Loading treatments data of": "Loading treatments data of", + "Processing data of": "Processing data of", + "Portion": "Portion", + "Size": "Size", + "(none)": "(none)", + "None": "None", + "": "", + "Result is empty": "Result is empty", + "Day to day": "Day to day", + "Week to week": "Week to week", + "Daily Stats": "Daily Stats", + "Percentile Chart": "Percentile Chart", + "Distribution": "Distribution", + "Hourly stats": "Hourly stats", + "netIOB stats": "netIOB stats", + "temp basals must be rendered to display this report": "temp basals must be rendered to display this report", + "No data available": "No data available", + "Low": "Low", + "In Range": "In Range", + "Period": "Period", + "High": "High", + "Average": "Average", + "Low Quartile": "Low Quartile", + "Upper Quartile": "Upper Quartile", + "Quartile": "Quartile", + "Date": "Date", + "Normal": "Normal", + "Median": "Median", + "Readings": "Readings", + "StDev": "StDev", + "Daily stats report": "Daily stats report", + "Glucose Percentile report": "Glucose Percentile report", + "Glucose distribution": "Glucose distribution", + "days total": "days total", + "Total per day": "Total per day", + "Overall": "Overall", + "Range": "Range", + "% of Readings": "% of Readings", + "# of Readings": "# of Readings", + "Mean": "Mean", + "Standard Deviation": "Standard Deviation", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "A1c estimation*", + "Weekly Success": "Weekly Success", + "There is not sufficient data to run this report. Select more days.": "There is not sufficient data to run this report. Select more days.", + "Using stored API secret hash": "Using stored API secret hash", + "No API secret hash stored yet. You need to enter API secret.": "No API secret hash stored yet. You need to enter API secret.", + "Database loaded": "Database loaded", + "Error: Database failed to load": "Error: Database failed to load", + "Error": "Error", + "Create new record": "Create new record", + "Save record": "Save record", + "Portions": "Portions", + "Unit": "Unit", + "GI": "GI", + "Edit record": "Edit record", + "Delete record": "Delete record", + "Move to the top": "Move to the top", + "Hidden": "Hidden", + "Hide after use": "Hide after use", + "Your API secret must be at least 12 characters long": "Your API secret must be at least 12 characters long", + "Bad API secret": "Bad API secret", + "API secret hash stored": "API secret hash stored", + "Status": "Status", + "Not loaded": "Not loaded", + "Food Editor": "Food Editor", + "Your database": "Your database", + "Filter": "Filter", + "Save": "Save", + "Clear": "Clear", + "Record": "Record", + "Quick picks": "Quick picks", + "Show hidden": "Show hidden", + "Your API secret or token": "Your API secret or token", + "Remember this device. (Do not enable this on public computers.)": "Remember this device. (Do not enable this on public computers.)", + "Treatments": "Treatments", + "Time": "Time", + "Event Type": "Event Type", + "Blood Glucose": "Blood Glucose", + "Entered By": "Entered By", + "Delete this treatment?": "Delete this treatment?", + "Carbs Given": "Carbs Given", + "Insulin Given": "Insulin Given", + "Event Time": "Event Time", + "Please verify that the data entered is correct": "Please verify that the data entered is correct", + "BG": "BG", + "Use BG correction in calculation": "Use BG correction in calculation", + "BG from CGM (autoupdated)": "BG from CGM (autoupdated)", + "BG from meter": "BG from meter", + "Manual BG": "Manual BG", + "Quickpick": "Quickpick", + "or": "or", + "Add from database": "Add from database", + "Use carbs correction in calculation": "Use carbs correction in calculation", + "Use COB correction in calculation": "Use COB correction in calculation", + "Use IOB in calculation": "Use IOB in calculation", + "Other correction": "Other correction", + "Rounding": "Rounding", + "Enter insulin correction in treatment": "Enter insulin correction in treatment", + "Insulin needed": "Insulin needed", + "Carbs needed": "Carbs needed", + "Carbs needed if Insulin total is negative value": "Carbs needed if Insulin total is negative value", + "Basal rate": "Basal rate", + "60 minutes earlier": "60 minutes earlier", + "45 minutes earlier": "45 minutes earlier", + "30 minutes earlier": "30 minutes earlier", + "20 minutes earlier": "20 minutes earlier", + "15 minutes earlier": "15 minutes earlier", + "Time in minutes": "Time in minutes", + "15 minutes later": "15 minutes later", + "20 minutes later": "20 minutes later", + "30 minutes later": "30 minutes later", + "45 minutes later": "45 minutes later", + "60 minutes later": "60 minutes later", + "Additional Notes, Comments": "Additional Notes, Comments", + "RETRO MODE": "RETRO MODE", + "Now": "Now", + "Other": "Other", + "Submit Form": "Submit Form", + "Profile Editor": "Profile Editor", + "Reports": "Reports", + "Add food from your database": "Add food from your database", + "Reload database": "Reload database", + "Add": "Add", + "Unauthorized": "Unauthorized", + "Entering record failed": "Entering record failed", + "Device authenticated": "Device authenticated", + "Device not authenticated": "Device not authenticated", + "Authentication status": "Authentication status", + "Authenticate": "Authenticate", + "Remove": "Remove", + "Your device is not authenticated yet": "Your device is not authenticated yet", + "Sensor": "Sensor", + "Finger": "Finger", + "Manual": "Manual", + "Scale": "Scale", + "Linear": "Linear", + "Logarithmic": "Logarithmic", + "Logarithmic (Dynamic)": "Logarithmic (Dynamic)", + "Insulin-on-Board": "Insulin-on-Board", + "Carbs-on-Board": "Carbs-on-Board", + "Bolus Wizard Preview": "Bolus Wizard Preview", + "Value Loaded": "Value Loaded", + "Cannula Age": "Cannula Age", + "Basal Profile": "Basal Profile", + "Silence for 30 minutes": "Silence for 30 minutes", + "Silence for 60 minutes": "Silence for 60 minutes", + "Silence for 90 minutes": "Silence for 90 minutes", + "Silence for 120 minutes": "Silence for 120 minutes", + "Settings": "Settings", + "Units": "Units", + "Date format": "Date format", + "12 hours": "12 hours", + "24 hours": "24 hours", + "Log a Treatment": "Log a Treatment", + "BG Check": "BG Check", + "Meal Bolus": "Meal Bolus", + "Snack Bolus": "Snack Bolus", + "Correction Bolus": "Correction Bolus", + "Carb Correction": "Carb Correction", + "Note": "Note", + "Question": "Question", + "Exercise": "Exercise", + "Pump Site Change": "Pump Site Change", + "CGM Sensor Start": "CGM Sensor Start", + "CGM Sensor Stop": "CGM Sensor Stop", + "CGM Sensor Insert": "CGM Sensor Insert", + "Sensor Code": "Sensor Code", + "Transmitter ID": "Transmitter ID", + "Dexcom Sensor Start": "Dexcom Sensor Start", + "Dexcom Sensor Change": "Dexcom Sensor Change", + "Insulin Cartridge Change": "Insulin Cartridge Change", + "D.A.D. Alert": "D.A.D. Alert", + "Glucose Reading": "Glucose Reading", + "Measurement Method": "Measurement Method", + "Meter": "Meter", + "Amount in grams": "Amount in grams", + "Amount in units": "Amount in units", + "View all treatments": "View all treatments", + "Enable Alarms": "Enable Alarms", + "Pump Battery Change": "Pump Battery Change", + "Pump Battery Age": "Pump Battery Age", + "Pump Battery Low Alarm": "Pump Battery Low Alarm", + "Pump Battery change overdue!": "Pump Battery change overdue!", + "When enabled an alarm may sound.": "When enabled an alarm may sound.", + "Urgent High Alarm": "Urgent High Alarm", + "High Alarm": "High Alarm", + "Low Alarm": "Low Alarm", + "Urgent Low Alarm": "Urgent Low Alarm", + "Stale Data: Warn": "Stale Data: Warn", + "Stale Data: Urgent": "Stale Data: Urgent", + "mins": "mins", + "Night Mode": "Night Mode", + "When enabled the page will be dimmed from 10pm - 6am.": "When enabled the page will be dimmed from 10pm - 6am.", + "Enable": "Enable", + "Show Raw BG Data": "Show Raw BG Data", + "Never": "Never", + "Always": "Always", + "When there is noise": "When there is noise", + "When enabled small white dots will be displayed for raw BG data": "When enabled small white dots will be displayed for raw BG data", + "Custom Title": "Custom Title", + "Theme": "Theme", + "Default": "Default", + "Colors": "Colors", + "Colorblind-friendly colors": "Colorblind-friendly colors", + "Reset, and use defaults": "Reset, and use defaults", + "Calibrations": "Calibrations", + "Alarm Test / Smartphone Enable": "Alarm Test / Smartphone Enable", + "Bolus Wizard": "Bolus Wizard", + "in the future": "in the future", + "time ago": "time ago", + "hr ago": "hr ago", + "hrs ago": "hrs ago", + "min ago": "min ago", + "mins ago": "mins ago", + "day ago": "day ago", + "days ago": "days ago", + "long ago": "long ago", + "Clean": "Clean", + "Light": "Light", + "Medium": "Medium", + "Heavy": "Heavy", + "Treatment type": "Treatment type", + "Raw BG": "Raw BG", + "Device": "Device", + "Noise": "Noise", + "Calibration": "Calibration", + "Show Plugins": "Show Plugins", + "About": "About", + "Value in": "Value in", + "Carb Time": "Carb Time", + "Language": "Language", + "Add new": "Add new", + "g": "g", + "ml": "ml", + "pcs": "pcs", + "Drag&drop food here": "Drag&drop food here", + "Care Portal": "Care Portal", + "Medium/Unknown": "Medium/Unknown", + "IN THE FUTURE": "IN THE FUTURE", + "Order": "Order", + "oldest on top": "oldest on top", + "newest on top": "newest on top", + "All sensor events": "All sensor events", + "Remove future items from mongo database": "Remove future items from mongo database", + "Find and remove treatments in the future": "Find and remove treatments in the future", + "This task find and remove treatments in the future.": "This task find and remove treatments in the future.", + "Remove treatments in the future": "Remove treatments in the future", + "Find and remove entries in the future": "Find and remove entries in the future", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "This task find and remove CGM data in the future created by uploader with wrong date/time.", + "Remove entries in the future": "Remove entries in the future", + "Loading database ...": "Loading database ...", + "Database contains %1 future records": "Database contains %1 future records", + "Remove %1 selected records?": "Remove %1 selected records?", + "Error loading database": "Error loading database", + "Record %1 removed ...": "Record %1 removed ...", + "Error removing record %1": "Error removing record %1", + "Deleting records ...": "Deleting records ...", + "%1 records deleted": "%1 records deleted", + "Clean Mongo status database": "Clean Mongo status database", + "Delete all documents from devicestatus collection": "Delete all documents from devicestatus collection", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.", + "Delete all documents": "Delete all documents", + "Delete all documents from devicestatus collection?": "Delete all documents from devicestatus collection?", + "Database contains %1 records": "Database contains %1 records", + "All records removed ...": "All records removed ...", + "Delete all documents from devicestatus collection older than 30 days": "Delete all documents from devicestatus collection older than 30 days", + "Number of Days to Keep:": "Number of Days to Keep:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from devicestatus collection?": "Delete old documents from devicestatus collection?", + "Clean Mongo entries (glucose entries) database": "Clean Mongo entries (glucose entries) database", + "Delete all documents from entries collection older than 180 days": "Delete all documents from entries collection older than 180 days", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents": "Delete old documents", + "Delete old documents from entries collection?": "Delete old documents from entries collection?", + "%1 is not a valid number": "%1 is not a valid number", + "%1 is not a valid number - must be more than 2": "%1 is not a valid number - must be more than 2", + "Clean Mongo treatments database": "Clean Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Delete all documents from treatments collection older than 180 days", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "Admin Tools", + "Nightscout reporting": "Nightscout reporting", + "Cancel": "Cancel", + "Edit treatment": "Edit treatment", + "Duration": "Duration", + "Duration in minutes": "Duration in minutes", + "Temp Basal": "Temp Basal", + "Temp Basal Start": "Temp Basal Start", + "Temp Basal End": "Temp Basal End", + "Percent": "Percent", + "Basal change in %": "Basal change in %", + "Basal value": "Basal value", + "Absolute basal value": "Absolute basal value", + "Announcement": "Announcement", + "Loading temp basal data": "Loading temp basal data", + "Save current record before changing to new?": "Save current record before changing to new?", + "Profile Switch": "Profile Switch", + "Profile": "Profile", + "General profile settings": "General profile settings", + "Title": "Title", + "Database records": "Database records", + "Add new record": "Add new record", + "Remove this record": "Remove this record", + "Clone this record to new": "Clone this record to new", + "Record valid from": "Record valid from", + "Stored profiles": "Stored profiles", + "Timezone": "Timezone", + "Duration of Insulin Activity (DIA)": "Duration of Insulin Activity (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.", + "Insulin to carb ratio (I:C)": "Insulin to carb ratio (I:C)", + "Hours:": "Hours:", + "hours": "hours", + "g/hour": "g/hour", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.", + "Insulin Sensitivity Factor (ISF)": "Insulin Sensitivity Factor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.", + "Carbs activity / absorption rate": "Carbs activity / absorption rate", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).", + "Basal rates [unit/hour]": "Basal rates [unit/hour]", + "Target BG range [mg/dL,mmol/L]": "Target BG range [mg/dL,mmol/L]", + "Start of record validity": "Start of record validity", + "Icicle": "Icicle", + "Render Basal": "Render Basal", + "Profile used": "Profile used", + "Calculation is in target range.": "Calculation is in target range.", + "Loading profile records ...": "Loading profile records ...", + "Values loaded.": "Values loaded.", + "Default values used.": "Default values used.", + "Error. Default values used.": "Error. Default values used.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Time ranges of target_low and target_high don't match. Values are restored to defaults.", + "Valid from:": "Valid from:", + "Save current record before switching to new?": "Save current record before switching to new?", + "Add new interval before": "Add new interval before", + "Delete interval": "Delete interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Combo Bolus", + "Difference": "Difference", + "New time": "New time", + "Edit Mode": "Edit Mode", + "When enabled icon to start edit mode is visible": "When enabled icon to start edit mode is visible", + "Operation": "Operation", + "Move": "Move", + "Delete": "Delete", + "Move insulin": "Move insulin", + "Move carbs": "Move carbs", + "Remove insulin": "Remove insulin", + "Remove carbs": "Remove carbs", + "Change treatment time to %1 ?": "Change treatment time to %1 ?", + "Change carbs time to %1 ?": "Change carbs time to %1 ?", + "Change insulin time to %1 ?": "Change insulin time to %1 ?", + "Remove treatment ?": "Remove treatment ?", + "Remove insulin from treatment ?": "Remove insulin from treatment ?", + "Remove carbs from treatment ?": "Remove carbs from treatment ?", + "Rendering": "Rendering", + "Loading OpenAPS data of": "Loading OpenAPS data of", + "Loading profile switch data": "Loading profile switch data", + "Redirecting you to the Profile Editor to create a new profile.": "Redirecting you to the Profile Editor to create a new profile.", + "Pump": "Pump", + "Sensor Age": "Sensor Age", + "Insulin Age": "Insulin Age", + "Temporary target": "Temporary target", + "Reason": "Reason", + "Eating soon": "Eating soon", + "Top": "Top", + "Bottom": "Bottom", + "Activity": "Activity", + "Targets": "Targets", + "Bolus insulin:": "Bolus insulin:", + "Base basal insulin:": "Base basal insulin:", + "Positive temp basal insulin:": "Positive temp basal insulin:", + "Negative temp basal insulin:": "Negative temp basal insulin:", + "Total basal insulin:": "Total basal insulin:", + "Total daily insulin:": "Total daily insulin:", + "Unable to save Role": "Unable to save Role", + "Unable to delete Role": "Unable to delete Role", + "Database contains %1 roles": "Database contains %1 roles", + "Edit Role": "Edit Role", + "admin, school, family, etc": "admin, school, family, etc", + "Permissions": "Permissions", + "Are you sure you want to delete: ": "Are you sure you want to delete: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.", + "Add new Role": "Add new Role", + "Roles - Groups of People, Devices, etc": "Roles - Groups of People, Devices, etc", + "Edit this role": "Edit this role", + "Admin authorized": "Admin authorized", + "Subjects - People, Devices, etc": "Subjects - People, Devices, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.", + "Add new Subject": "Add new Subject", + "Unable to save Subject": "Unable to save Subject", + "Unable to delete Subject": "Unable to delete Subject", + "Database contains %1 subjects": "Database contains %1 subjects", + "Edit Subject": "Edit Subject", + "person, device, etc": "person, device, etc", + "role1, role2": "role1, role2", + "Edit this subject": "Edit this subject", + "Delete this subject": "Delete this subject", + "Roles": "Roles", + "Access Token": "Access Token", + "hour ago": "hour ago", + "hours ago": "hours ago", + "Silence for %1 minutes": "Silence for %1 minutes", + "Check BG": "Check BG", + "BASAL": "BASAL", + "Current basal": "Current basal", + "Sensitivity": "Sensitivity", + "Current Carb Ratio": "Current Carb Ratio", + "Basal timezone": "Basal timezone", + "Active profile": "Active profile", + "Active temp basal": "Active temp basal", + "Active temp basal start": "Active temp basal start", + "Active temp basal duration": "Active temp basal duration", + "Active temp basal remaining": "Active temp basal remaining", + "Basal profile value": "Basal profile value", + "Active combo bolus": "Active combo bolus", + "Active combo bolus start": "Active combo bolus start", + "Active combo bolus duration": "Active combo bolus duration", + "Active combo bolus remaining": "Active combo bolus remaining", + "BG Delta": "BG Delta", + "Elapsed Time": "Elapsed Time", + "Absolute Delta": "Absolute Delta", + "Interpolated": "Interpolated", + "BWP": "BWP", + "Urgent": "Urgent", + "Warning": "Warning", + "Info": "Info", + "Lowest": "Lowest", + "Snoozing high alarm since there is enough IOB": "Snoozing high alarm since there is enough IOB", + "Check BG, time to bolus?": "Check BG, time to bolus?", + "Notice": "Notice", + "required info missing": "required info missing", + "Insulin on Board": "Insulin on Board", + "Current target": "Current target", + "Expected effect": "Expected effect", + "Expected outcome": "Expected outcome", + "Carb Equivalent": "Carb Equivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U reduction needed in active insulin to reach low target, too much basal?", + "basal adjustment out of range, give carbs?": "basal adjustment out of range, give carbs?", + "basal adjustment out of range, give bolus?": "basal adjustment out of range, give bolus?", + "above high": "above high", + "below low": "below low", + "Projected BG %1 target": "Projected BG %1 target", + "aiming at": "aiming at", + "Bolus %1 units": "Bolus %1 units", + "or adjust basal": "or adjust basal", + "Check BG using glucometer before correcting!": "Check BG using glucometer before correcting!", + "Basal reduction to account %1 units:": "Basal reduction to account %1 units:", + "30m temp basal": "30m temp basal", + "1h temp basal": "1h temp basal", + "Cannula change overdue!": "Cannula change overdue!", + "Time to change cannula": "Time to change cannula", + "Change cannula soon": "Change cannula soon", + "Cannula age %1 hours": "Cannula age %1 hours", + "Inserted": "Inserted", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Last Carbs", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Insulin reservoir change overdue!", + "Time to change insulin reservoir": "Time to change insulin reservoir", + "Change insulin reservoir soon": "Change insulin reservoir soon", + "Insulin reservoir age %1 hours": "Insulin reservoir age %1 hours", + "Changed": "Changed", + "IOB": "IOB", + "Careportal IOB": "Careportal IOB", + "Last Bolus": "Last Bolus", + "Basal IOB": "Basal IOB", + "Source": "Source", + "Stale data, check rig?": "Stale data, check rig?", + "Last received:": "Last received:", + "%1m ago": "%1m ago", + "%1h ago": "%1h ago", + "%1d ago": "%1d ago", + "RETRO": "RETRO", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Sensor change/restart overdue!", + "Time to change/restart sensor": "Time to change/restart sensor", + "Change/restart sensor soon": "Change/restart sensor soon", + "Sensor age %1 days %2 hours": "Sensor age %1 days %2 hours", + "Sensor Insert": "Sensor Insert", + "Sensor Start": "Sensor Start", + "days": "days", + "Insulin distribution": "Insulin distribution", + "To see this report, press SHOW while in this view": "To see this report, press SHOW while in this view", + "AR2 Forecast": "AR2 Forecast", + "OpenAPS Forecasts": "OpenAPS Forecasts", + "Temporary Target": "Temporary Target", + "Temporary Target Cancel": "Temporary Target Cancel", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profiles", + "Time in fluctuation": "Time in fluctuation", + "Time in rapid fluctuation": "Time in rapid fluctuation", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:", + "Filter by hours": "Filter by hours", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">can be found here.", + "Mean Total Daily Change": "Mean Total Daily Change", + "Mean Hourly Change": "Mean Hourly Change", + "FortyFiveDown": "slightly dropping", + "FortyFiveUp": "slightly rising", + "Flat": "holding", + "SingleUp": "rising", + "SingleDown": "dropping", + "DoubleDown": "rapidly dropping", + "DoubleUp": "rapidly rising", + "virtAsstUnknown": "That value is unknown at the moment. Please see your Nightscout site for more details.", + "virtAsstTitleAR2Forecast": "AR2 Forecast", + "virtAsstTitleCurrentBasal": "Current Basal", + "virtAsstTitleCurrentCOB": "Current COB", + "virtAsstTitleCurrentIOB": "Current IOB", + "virtAsstTitleLaunch": "Welcome to Nightscout", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Forecast", + "virtAsstTitlePumpReservoir": "Insulin Remaining", + "virtAsstTitlePumpBattery": "Pump Battery", + "virtAsstTitleRawBG": "Current Raw BG", + "virtAsstTitleUploaderBattery": "Uploader Battery", + "virtAsstTitleCurrentBG": "Current BG", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "CGM Mode", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM Transmitter Age", + "virtAsstTitleCGMNoise": "CGM Noise", + "virtAsstTitleDelta": "Blood Glucose Delta", + "virtAsstStatus": "%1 and %2 as of %3.", + "virtAsstBasal": "%1 current basal is %2 units per hour", + "virtAsstBasalTemp": "%1 temp basal of %2 units per hour will end %3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "You have %1 insulin on board", + "virtAsstIobUnits": "%1 units of", + "virtAsstLaunch": "What would you like to check on Nightscout?", + "virtAsstPreamble": "Your", + "virtAsstPreamble3person": "%1 has a ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "Your pump battery is at %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "I'm sorry, I don't know what you're asking for.", + "Fat [g]": "Fat [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energy [kJ]", + "Clock Views:": "Clock Views:", + "Clock": "Clock", + "Color": "Color", + "Simple": "Simple", + "TDD average": "TDD average", + "Bolus average": "Bolus average", + "Basal average": "Basal average", + "Base basal average:": "Base basal average:", + "Carbs average": "Carbs average", + "Eating Soon": "Eating Soon", + "Last entry {0} minutes ago": "Last entry {0} minutes ago", + "change": "change", + "Speech": "Speech", + "Target Top": "Target Top", + "Target Bottom": "Target Bottom", + "Canceled": "Canceled", + "Meter BG": "Meter BG", + "predicted": "predicted", + "future": "future", + "ago": "ago", + "Last data received": "Last data received", + "Clock View": "Clock View", + "Protein": "Protein", + "Fat": "Fat", + "Protein average": "Protein average", + "Fat average": "Fat average", + "Total carbs": "Total carbs", + "Total protein": "Total protein", + "Total fat": "Total fat", + "Database Size": "Database Size", + "Database Size near its limits!": "Database Size near its limits!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "Database file size", + "%1 MiB of %2 MiB (%3%)": "%1 MiB of %2 MiB (%3%)", + "Data size": "Data size", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "Database file size", + "Carbs/Food/Time": "Carbs/Food/Time", + "You have administration messages": "You have administration messages", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "Data reads enabled", + "Data writes enabled": "Data writes enabled", + "Data writes not enabled": "Data writes not enabled", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "Check for Updates", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "Previous", + "Previous day": "Previous day", + "Next day": "Next day", + "Next": "Next", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/translations/tr_TR.json b/translations/tr_TR.json new file mode 100644 index 00000000000..ed33cfa831c --- /dev/null +++ b/translations/tr_TR.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Port dinleniyor", + "Mo": "Pzt", + "Tu": "Sal", + "We": "Çar", + "Th": "Per", + "Fr": "Cum", + "Sa": "Cmt", + "Su": "Paz", + "Monday": "Pazartesi", + "Tuesday": "Salı", + "Wednesday": "Çarşamba", + "Thursday": "Perşembe", + "Friday": "Cuma", + "Saturday": "Cumartesi", + "Sunday": "Pazar", + "Category": "Kategori", + "Subcategory": "Altkategori", + "Name": "İsim", + "Today": "Bugün", + "Last 2 days": "Son 2 gün", + "Last 3 days": "Son 3 gün", + "Last week": "Geçen Hafta", + "Last 2 weeks": "Son 2 hafta", + "Last month": "Geçen Ay", + "Last 3 months": "Son 3 ay", + "From": "Başlangıç", + "To": "Bitiş", + "Notes": "Not", + "Food": "Gıda", + "Insulin": "İnsülin", + "Carbs": "Karbonhidrat", + "Notes contain": "Notlar içerir", + "Target BG range bottom": "Hedef KŞ aralığı düşük", + "top": "yüksek", + "Show": "Göster", + "Display": "Görüntüle", + "Loading": "Yükleniyor", + "Loading profile": "Profil yükleniyor", + "Loading status": "Durum Yükleniyor", + "Loading food database": "Gıda veritabanı yükleniyor", + "not displayed": "görüntülenmedi", + "Loading CGM data of": "den CGM veriler yükleniyor", + "Loading treatments data of": "dan Tedavi verilerini yükle", + "Processing data of": "dan Veri işleme", + "Portion": "Porsiyon", + "Size": "Boyut", + "(none)": "(yok)", + "None": "Yok", + "": "", + "Result is empty": "Sonuç boş", + "Day to day": "Günden Güne", + "Week to week": "Haftalık", + "Daily Stats": "Günlük İstatistikler", + "Percentile Chart": "Yüzdelik Grafiği", + "Distribution": "Dağılım", + "Hourly stats": "Saatlik istatistikler", + "netIOB stats": "netIOB istatistikleri", + "temp basals must be rendered to display this report": "Bu raporu görüntülemek için geçici bazal oluşturulmalıdır", + "No data available": "Veri yok", + "Low": "Düşük", + "In Range": "Hedef alanında", + "Period": "Periyot", + "High": "Yüksek", + "Average": "Ortalama", + "Low Quartile": "Alt Çeyrek", + "Upper Quartile": "Üst Çeyrek", + "Quartile": "Çeyrek", + "Date": "Tarih", + "Normal": "Normal", + "Median": "Ortanca", + "Readings": "Ölçümler", + "StDev": "Standart Sapma", + "Daily stats report": "Günlük istatistikler raporu", + "Glucose Percentile report": "Glikoz Yüzdelik raporu", + "Glucose distribution": "Glikoz dağılımı", + "days total": "gün toplamı", + "Total per day": "Günlük toplam", + "Overall": "Tümü", + "Range": "Aralık", + "% of Readings": "% Okumaların", + "# of Readings": "# Okumaların", + "Mean": "Ortalama", + "Standard Deviation": "Standart Sapma", + "Max": "Maks", + "Min": "Min", + "A1c estimation*": "Tahmini A1c *", + "Weekly Success": "Haftalık Başarı", + "There is not sufficient data to run this report. Select more days.": "Bu raporu çalıştırmak için yeterli veri yok. Daha fazla gün seçin.", + "Using stored API secret hash": "Kaydedilmiş API secret hash kullan", + "No API secret hash stored yet. You need to enter API secret.": "Henüz bir API secret hash saklanmadı. API parolasını girmeniz gerekiyor.", + "Database loaded": "Veritabanı yüklendi", + "Error: Database failed to load": "Hata: Veritabanı yüklenemedi", + "Error": "Hata", + "Create new record": "Yeni kayıt oluştur", + "Save record": "Kayıtları kaydet", + "Portions": "Porsiyonlar", + "Unit": "Birim", + "GI": "GI-Glisemik İndeks", + "Edit record": "Kaydı düzenle", + "Delete record": "Kaydı sil", + "Move to the top": "En üste taşı", + "Hidden": "Gizli", + "Hide after use": "Kullandıktan sonra gizle", + "Your API secret must be at least 12 characters long": "API parolanız en az 12 karakter uzunluğunda olmalıdır", + "Bad API secret": "Hatalı API parolası", + "API secret hash stored": "API secret hash parolası saklandı", + "Status": "Durum", + "Not loaded": "Yüklü değil", + "Food Editor": "Yemek Editörü", + "Your database": "Veritabanınız", + "Filter": "Filtre", + "Save": "Kaydet", + "Clear": "Temizle", + "Record": "Kayıt", + "Quick picks": "Hızlı seçim", + "Show hidden": "Gizleneni göster", + "Your API secret or token": "API parolan yada anahtarın", + "Remember this device. (Do not enable this on public computers.)": "Bu cihazı hatırla. (Ortak kullanılan bilgisayarlarda seçmeyiniz.)", + "Treatments": "Tedaviler", + "Time": "Zaman", + "Event Type": "Etkinlik tipi", + "Blood Glucose": "Kan Şekeri", + "Entered By": "Tarafından girildi", + "Delete this treatment?": "Bu tedaviyi sil?", + "Carbs Given": "Verilen Karbonhidrat", + "Insulin Given": "Verilen İnsülin", + "Event Time": "Etkinliğin zamanı", + "Please verify that the data entered is correct": "Lütfen girilen verilerin doğru olduğunu kontrol edin.", + "BG": "KŞ", + "Use BG correction in calculation": "Hesaplamada KŞ düzeltmesini kullan", + "BG from CGM (autoupdated)": "CGM den KŞ (otomatik güncelleme)", + "BG from meter": "Glikometre KŞ", + "Manual BG": "Manuel KŞ", + "Quickpick": "Hızlı seçim", + "or": "veya", + "Add from database": "Veritabanından ekle", + "Use carbs correction in calculation": "Hesaplamada karbonhidrat düzeltmesini kullan", + "Use COB correction in calculation": "Hesaplamada AKRB (Aktif Karbonhidrat) düzeltmesini kullan", + "Use IOB in calculation": "Hesaplamada Aktif İnsülin (AİNS) düzeltmesini kullan", + "Other correction": "Diğer düzeltme", + "Rounding": "Yuvarlama", + "Enter insulin correction in treatment": "Tedavide insülin düzeltmesini girin", + "Insulin needed": "İnsülin gerekli", + "Carbs needed": "Karbonhidrat gerekli", + "Carbs needed if Insulin total is negative value": "Toplam insülin negatif değer olduğunda karbonhidrat gereklidir", + "Basal rate": "Basal oranı", + "60 minutes earlier": "60 dak. önce", + "45 minutes earlier": "45 dak. önce", + "30 minutes earlier": "30 dak. önce", + "20 minutes earlier": "20 dak. önce", + "15 minutes earlier": "15 dak. önce", + "Time in minutes": "Dakika cinsinden süre", + "15 minutes later": "15 dak. sonra", + "20 minutes later": "20 dak. sonra", + "30 minutes later": "30 dak. sonra", + "45 minutes later": "45 dak. sonra", + "60 minutes later": "60 dak. sonra", + "Additional Notes, Comments": "Ek Notlar, Yorumlar", + "RETRO MODE": "Retro Mod", + "Now": "Şimdi", + "Other": "Diğer", + "Submit Form": "Formu gönder", + "Profile Editor": "Profil Düzenleyicisi", + "Reports": "Raporlar", + "Add food from your database": "Veritabanınızdan yemek ekleyin", + "Reload database": "Veritabanını yeniden yükle", + "Add": "Ekle", + "Unauthorized": "Yetkisiz", + "Entering record failed": "Kayıt girişi başarısız oldu", + "Device authenticated": "Cihaz kimliği doğrulandı", + "Device not authenticated": "Cihaz kimliği doğrulanmamış", + "Authentication status": "Kimlik doğrulama durumu", + "Authenticate": "Kimlik doğrulaması", + "Remove": "Kaldır", + "Your device is not authenticated yet": "Cihazınız henüz doğrulanmamış", + "Sensor": "Sensör", + "Finger": "Parmak", + "Manual": "Elle", + "Scale": "Ölçek", + "Linear": "Doğrusal", + "Logarithmic": "Logaritmik", + "Logarithmic (Dynamic)": "Logaritmik (Dinamik)", + "Insulin-on-Board": "Aktif-İnsülin (AİNS)", + "Carbs-on-Board": "Aktif-Karbonhidrat (AKRB)", + "Bolus Wizard Preview": "Bolus Sihirbazı Önizlemesi", + "Value Loaded": "Değer Yüklendi", + "Cannula Age": "Kanül yaşı", + "Basal Profile": "Bazal Profil", + "Silence for 30 minutes": "30 dakika sessizlik", + "Silence for 60 minutes": "60 dakika sessizlik", + "Silence for 90 minutes": "90 dakika sessizlik", + "Silence for 120 minutes": "120 dakika sessizlik", + "Settings": "Ayarlar", + "Units": "Birim", + "Date format": "Veri formatı", + "12 hours": "12 saat", + "24 hours": "24 saat", + "Log a Treatment": "Tedaviyi günlüğe kaydet", + "BG Check": "KŞ Kontol", + "Meal Bolus": "Yemek bolus", + "Snack Bolus": "Aperatif (Snack) Bolus", + "Correction Bolus": "Düzeltme Bolusu", + "Carb Correction": "Karbonhidrat Düzeltme", + "Note": "Not", + "Question": "Soru", + "Exercise": "Egzersiz", + "Pump Site Change": "Pompa Kanül değişimi", + "CGM Sensor Start": "CGM Sensörü Başlat", + "CGM Sensor Stop": "CGM Sensör Durdur", + "CGM Sensor Insert": "CGM Sensor yerleştir", + "Sensor Code": "Sensör Kodu", + "Transmitter ID": "Verici ID", + "Dexcom Sensor Start": "Dexcom Sensör Başlat", + "Dexcom Sensor Change": "Dexcom Sensör değiştir", + "Insulin Cartridge Change": "İnsülin rezervuar değişimi", + "D.A.D. Alert": "D.A.D Alarmı", + "Glucose Reading": "Glikoz Değeri", + "Measurement Method": "Ölçüm Metodu", + "Meter": "Glikometre", + "Amount in grams": "Gram cinsinden miktar", + "Amount in units": "Ünite", + "View all treatments": "Tüm tedavileri görüntüle", + "Enable Alarms": "Alarmları Etkinleştir", + "Pump Battery Change": "Pompa pil değişimi", + "Pump Battery Age": "Pompa pil yaşı", + "Pump Battery Low Alarm": "Pompa Düşük pil alarmı", + "Pump Battery change overdue!": "Pompa pil değişimi gecikti!", + "When enabled an alarm may sound.": "Etkinleştirildiğinde alarm çalabilir.", + "Urgent High Alarm": "Acil Yüksek Alarmı", + "High Alarm": "Yüksek Alarmı", + "Low Alarm": "Düşük Alarmı", + "Urgent Low Alarm": "Acil Düşük Alarmı", + "Stale Data: Warn": "Eski Veri: Uyarı", + "Stale Data: Urgent": "Eski Veri: Acil", + "mins": "dk.", + "Night Mode": "Gece Modu", + "When enabled the page will be dimmed from 10pm - 6am.": "Etkinleştirildiğinde, ekran akşam 22'den sabah 6'ya kadar kararır.", + "Enable": "Etkinleştir", + "Show Raw BG Data": "Ham KŞ verilerini göster", + "Never": "Hiçbir zaman", + "Always": "Her zaman", + "When there is noise": "Gürültü olduğunda", + "When enabled small white dots will be displayed for raw BG data": "Etkinleştirildiğinde, ham KŞ verileri için küçük beyaz noktalar görüntülenecektir.", + "Custom Title": "Özel Başlık", + "Theme": "Tema", + "Default": "Varsayılan", + "Colors": "Renkler", + "Colorblind-friendly colors": "Renk körü dostu görünüm", + "Reset, and use defaults": "Sıfırla ve varsayılanları kullan", + "Calibrations": "Kalibrasyon", + "Alarm Test / Smartphone Enable": "Alarm Testi / Akıllı Telefon için Etkin", + "Bolus Wizard": "Bolus Hesaplayıcısı", + "in the future": "gelecekte", + "time ago": "süre önce", + "hr ago": "saat önce", + "hrs ago": "saat önce", + "min ago": "dk. önce", + "mins ago": "dakika önce", + "day ago": "gün önce", + "days ago": "günler önce", + "long ago": "uzun zaman önce", + "Clean": "Temiz", + "Light": "Hafif", + "Medium": "Orta", + "Heavy": "Ağır", + "Treatment type": "Tedavi tipi", + "Raw BG": "Ham KŞ", + "Device": "Cihaz", + "Noise": "Gürültü", + "Calibration": "Kalibrasyon", + "Show Plugins": "Eklentileri Göster", + "About": "Hakkında", + "Value in": "Değer cinsinden", + "Carb Time": "Karb. zamanı", + "Language": "Dil", + "Add new": "Yeni ekle", + "g": "g", + "ml": "ml", + "pcs": "adet", + "Drag&drop food here": "Yiyecekleri buraya sürükle bırak", + "Care Portal": "Bakım portalı", + "Medium/Unknown": "Orta/Bilinmeyen", + "IN THE FUTURE": "GELECEKTE", + "Order": "Sıra", + "oldest on top": "en eski üste", + "newest on top": "en yeni üste", + "All sensor events": "Tüm sensör olayları", + "Remove future items from mongo database": "İleri tarihli öğeleri mongo veritabanından kaldır", + "Find and remove treatments in the future": "İleri tarihli tedavileri bulun ve kaldırın", + "This task find and remove treatments in the future.": "Bu görev, ileri tarihli tedavileri bulur ve kaldırır.", + "Remove treatments in the future": "İleri tarihli tedavileri kaldırın", + "Find and remove entries in the future": "İleri tarihli girdileri bul ve kaldır", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Bu görev, yükleyici tarafından yanlış tarih/saatle oluşturulan ileri tarihli CGM verilerini bulur ve kaldırır.", + "Remove entries in the future": "İleri Tarihli girdileri kaldır", + "Loading database ...": "Veritabanı yükleniyor ...", + "Database contains %1 future records": "Veritabanı %1 ileri tarihli girdi içeriyor", + "Remove %1 selected records?": "Seçilen %1 kayıtlar kaldırılsın mı?", + "Error loading database": "Veritabanını yüklerken hata oluştu", + "Record %1 removed ...": "%1 kayıt silindi ...", + "Error removing record %1": "%1 kayıt kaldırılırken hata oluştu", + "Deleting records ...": "Kayıtlar siliniyor ...", + "%1 records deleted": "%1 kayıt silindi", + "Clean Mongo status database": "Mongo durum veritabanını temizle", + "Delete all documents from devicestatus collection": "Cihazdurumu koleksiyonundan tüm dokümanları sil", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Bu görev tüm belgeleri Cihazdurumu koleksiyonundan kaldırır. Yükleyici pil durumunu düzgün güncellemiyorsa kullanışlıdır.", + "Delete all documents": "Tüm Belgeleri sil", + "Delete all documents from devicestatus collection?": "Cihazdurumu koleksiyonundan tüm dosyalar silinsin mi?", + "Database contains %1 records": "Veritabanı %1 kayıt içeriyor", + "All records removed ...": "Tüm kayıtlar kaldırıldı ...", + "Delete all documents from devicestatus collection older than 30 days": "Cihazdurumu koleksiyonundaki 30 günden eski tüm dosyaları sil", + "Number of Days to Keep:": "Saklanacak gün sayısı:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Bu görev 30 günden eski tüm dosyaları Cihazdurumu koleksiyonundan kaldırır. Yükleyici pil durumu doğru güncellenmiyorsa kullanışlıdır.", + "Delete old documents from devicestatus collection?": "Cihazdurumu koleksiyonundan eski belgeler silinsin mi?", + "Clean Mongo entries (glucose entries) database": "Mongo veritabanı girişlerini (kan şekeri girişleri) temizle", + "Delete all documents from entries collection older than 180 days": "180 günden eski olan tüm girişleri koleksiyondan silin", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Bu görev 180 günden eski tüm dosyaları koleksiyondan kaldırır. Yükleyici pil durumu doğru güncellenmiyorsa kullanışlıdır.", + "Delete old documents": "Eski belgeleri sil", + "Delete old documents from entries collection?": "Eski belgeleri giriş koleksiyonundan silin?", + "%1 is not a valid number": "%1 geçerli bir sayı değil", + "%1 is not a valid number - must be more than 2": "%1 geçerli bir sayı değil - 2'den büyük olmalı", + "Clean Mongo treatments database": "Mongo tedavi veritabanını temizle", + "Delete all documents from treatments collection older than 180 days": "Tedaviler koleksiyonundan 180 günden daha eski tüm kayıtları sil", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Bu görev 180 günden eski tüm dosyaları tedavi koleksiyonundan kaldırır. Yükleyici pil durumu doğru güncellenmiyorsa kullanışlıdır.", + "Delete old documents from treatments collection?": "Tüm eski tedavi belgeleri silinsin mi?", + "Admin Tools": "Yönetici araçları", + "Nightscout reporting": "NightScout raporları", + "Cancel": "İptal", + "Edit treatment": "Tedaviyi düzenle", + "Duration": "Süre", + "Duration in minutes": "Dakika cinsinden süre", + "Temp Basal": "Geçici Bazal", + "Temp Basal Start": "Geçici Bazal Başlangıç", + "Temp Basal End": "Geçici Bazal Bitiş", + "Percent": "Yüzde", + "Basal change in %": "Bazal değişimi % cinsinden", + "Basal value": "Bazal değer", + "Absolute basal value": "Mutlak bazal değer", + "Announcement": "Duyuru", + "Loading temp basal data": "Geçici bazal verileri yükleniyor", + "Save current record before changing to new?": "Yenisiyle değiştirmeden önce mevcut kayıt kaydedilsin mi?", + "Profile Switch": "Profil Değiştir", + "Profile": "Profil", + "General profile settings": "Genel profil ayarları", + "Title": "Başlık", + "Database records": "Veritabanı kayıtları", + "Add new record": "Yeni kayıt ekle", + "Remove this record": "Bu kaydı kaldır", + "Clone this record to new": "Bu kaydı yeniden kopyala", + "Record valid from": "Kayıt şu tarihten itibaren geçerli", + "Stored profiles": "Kaydedilmiş profiller", + "Timezone": "Saat dilimi", + "Duration of Insulin Activity (DIA)": "İnsülin Etki Süresi (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "İnsülinin etki ettiği tipik süreye karşılık gelir. Hastaya ve insülin tipine göre değişir. Çoğu pompa insülini ve çoğu hasta için genellikle 3-4 saattir. Bazen de insülin etki süresi de denir.", + "Insulin to carb ratio (I:C)": "İnsülin/Karbonhidrat oranı (I:C)", + "Hours:": "Saat:", + "hours": "saat", + "g/hour": "g/saat", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "1 ünite insülin başına gr karbonhidrat. 1 ünite insülin başına kaç gram karbonhidrat tüketildiği oranıdır.", + "Insulin Sensitivity Factor (ISF)": "İnsülin Duyarlılık Faktörü (İDF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "Ünite insülin başına mg/dL veya mmol/L. Her bir Ünite düzeltme insülin ile KŞ'nin ne kadar değiştiğini gösteren orandır.", + "Carbs activity / absorption rate": "Karbonhidrat aktivitesi / emilim oranı", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "birim zaman başına gram. Birim zamanda (AKRB) Aktif Krabonhidratdaki değişimin yanı sıra o zaman üzerinde etki etmesi gereken karbonhidrat miktarını ifade eder. Karbonhidrat emme/aktivite eğrileri, insülin aktivitesinden daha zor anlaşılmaktadır, ancak bir başlangıç gecikmesi ve ardından sabit bir emilim oranı (g/st) kullanılarak yaklaşık olarak tahmin edilebilmektedir.", + "Basal rates [unit/hour]": "Bazal oranı [ünite/saat]", + "Target BG range [mg/dL,mmol/L]": "Hedef KŞ aralığı [mg / dL, mmol / L]", + "Start of record validity": "Kayıt geçerliliği başlangıcı", + "Icicle": "Buzsaçağı", + "Render Basal": "Bazal Grafik", + "Profile used": "Kullanılan profil", + "Calculation is in target range.": "Hesaplama hedef aralıktadır.", + "Loading profile records ...": "Profil kayıtları yükleniyor ...", + "Values loaded.": "Değerler yüklendi.", + "Default values used.": "Varsayılan değerler kullanıldı.", + "Error. Default values used.": "Hata. Varsayılan değerler kullanıldı.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Düşük_hedef ve yüksek_hedef öğelerinin zaman aralıkları eşleşmiyor. Değerler varsayılanlara geri yüklenir.", + "Valid from:": "Tarihinden itibaren geçerli", + "Save current record before switching to new?": "Yenisine geçmeden önce mevcut kaydı kaydet", + "Add new interval before": "Önce yeni aralık ekle", + "Delete interval": "Aralığı sil", + "I:C": "İ:K", + "ISF": "İDF", + "Combo Bolus": "Kombo (Yayma) Bolus", + "Difference": "fark", + "New time": "Yeni zaman", + "Edit Mode": "Düzenleme Modu", + "When enabled icon to start edit mode is visible": "Etkinleştirildiğinde düzenleme modunun başında simgesi görünecektir.", + "Operation": "İşlem", + "Move": "Taşı", + "Delete": "Sil", + "Move insulin": "İnsülini taşı", + "Move carbs": "Karbonhidratları taşı", + "Remove insulin": "İnsülini kaldır", + "Remove carbs": "Karbonhidratları kaldır", + "Change treatment time to %1 ?": "Tedavi zamanı %1 e değiştirilsin mi?", + "Change carbs time to %1 ?": "Karbonhidrat zamanı %1 e değiştirilsin mi?", + "Change insulin time to %1 ?": "İnsülin zamanı %1 e değiştirilsin mi?", + "Remove treatment ?": "Tedavi kaldırılsın mı? ", + "Remove insulin from treatment ?": "İnsülin tedaviden çıkartılsın mı?", + "Remove carbs from treatment ?": "Karbonhidratlar tedaviden çıkartılsın mı ?", + "Rendering": "Grafik oluşturuluyor...", + "Loading OpenAPS data of": "OpenAPS verileri yükleniyor", + "Loading profile switch data": "Profil değişimi verileri yükleniyor", + "Redirecting you to the Profile Editor to create a new profile.": "Yeni bir profil oluşturmak için Profil Düzenleyicisine yönlendiriliyor.", + "Pump": "Pompa", + "Sensor Age": "Sensör Yaşı", + "Insulin Age": "İnsülin yaşı", + "Temporary target": "Geçici hedef", + "Reason": "Sebep", + "Eating soon": "Yakında yemek", + "Top": "Üst", + "Bottom": "Alt", + "Activity": "Aktivite", + "Targets": "Hedefler", + "Bolus insulin:": "Bolus insülin:", + "Base basal insulin:": "Temel bazal insülin", + "Positive temp basal insulin:": "Pozitif geçici bazal insülin:", + "Negative temp basal insulin:": "Negatif geçici bazal insülin:", + "Total basal insulin:": "Toplam bazal insülin:", + "Total daily insulin:": "Günlük toplam insülin:", + "Unable to save Role": "Rol kaydedilemedi", + "Unable to delete Role": "Rol silinemedi", + "Database contains %1 roles": "Veritabanı %1 rol içerir", + "Edit Role": "Rolü düzenle", + "admin, school, family, etc": "yönetici, okul, aile, vb", + "Permissions": "İzinler", + "Are you sure you want to delete: ": "Silmek istediğinizden emin misiniz:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Her rolün bir veya daha fazla izni vardır.*izni bir yer tutucudur ve izinler ayırıcı olarak : ile hiyerarşiktir.", + "Add new Role": "Yeni Rol ekle", + "Roles - Groups of People, Devices, etc": "Roller - İnsan grupları, Cihazlar vb.", + "Edit this role": "Bu rolü düzenle", + "Admin authorized": "Yönetici yetkilendirildi", + "Subjects - People, Devices, etc": "Konular - İnsanlar, Cihazlar, vb.", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Her konu benzersiz bir erişim anahtarı ve bir veya daha fazla rol alır. Seçilen konuyla ilgili yeni bir görünüm elde etmek için erişim tuşuna tıklayın. Bu gizli bağlantı paylaşılabilinir.", + "Add new Subject": "Yeni konu ekle", + "Unable to save Subject": "Konu kaydedilemedi", + "Unable to delete Subject": "Konu silinemedi", + "Database contains %1 subjects": "Veritabanı %1 konu içeriyor", + "Edit Subject": "Konuyu düzenle", + "person, device, etc": "kişi, cihaz, vb", + "role1, role2": "rol1, rol2", + "Edit this subject": "Bu konuyu düzenle", + "Delete this subject": "Bu konuyu sil", + "Roles": "Roller", + "Access Token": "Erişim Simgesi (Access Token)", + "hour ago": "saat önce", + "hours ago": "saat önce", + "Silence for %1 minutes": "%1 dakika sessize al", + "Check BG": "KŞ'ini kontrol et", + "BASAL": "BAZAL", + "Current basal": "Geçerli Bazal", + "Sensitivity": "Duyarlılık", + "Current Carb Ratio": "Geçerli Karbonhidrat oranı I/C (ICR)", + "Basal timezone": "Bazal saat dilimi", + "Active profile": "Aktif profil", + "Active temp basal": "Aktif geçici bazal", + "Active temp basal start": "Aktif geçici bazal başlangıcı", + "Active temp basal duration": "Aktif geçici bazal süresi", + "Active temp basal remaining": "Kalan aktif geçici bazal", + "Basal profile value": "Bazal profil değeri", + "Active combo bolus": "Aktif kombo bolus", + "Active combo bolus start": "Aktif kombo bolus başlatma", + "Active combo bolus duration": "Aktif kombo bolus süresi", + "Active combo bolus remaining": "Kalan aktif kombo bolus", + "BG Delta": "KŞ Değişimi (Delta)", + "Elapsed Time": "Geçen zaman", + "Absolute Delta": "Mutlak Değişim (Delta)", + "Interpolated": "Enterpolasyonlu", + "BWP": "Bolus Sihirbazı Önizlemesi", + "Urgent": "Acil", + "Warning": "Uyarı", + "Info": "Bilgi", + "Lowest": "En düşük değer", + "Snoozing high alarm since there is enough IOB": "Yeterli AİNS (Aktif İnsülin) olduğundan yüksek alarmı erteleniyor", + "Check BG, time to bolus?": "KŞine bakın, gerekirse bolus verin?", + "Notice": "Not", + "required info missing": "gerekli bilgi eksik", + "Insulin on Board": "(AİNS) Aktif İnsülin", + "Current target": "Mevcut hedef", + "Expected effect": "Beklenen etki", + "Expected outcome": "Beklenen sonuç", + "Carb Equivalent": "Karbonhidrat eşdeğeri", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Fazla insülin: Karbonhidratlar dikkate alınmadan, alt hedefe ulaşmak için gerekenden %1Ü fazla", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Alt KŞ hedefine ulaşmak için gerekenden %1 daha fazla insülin. (AİNS) Aktif İnsülin'ün karbonhidratlar tarafından karşılandığından emin olun.", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Alt KŞ hedefi için %1U aktif insülin azaltılmalı, bazal oranı çok mu yüksek?", + "basal adjustment out of range, give carbs?": "bazal ayarlaması limit dışı, karbonhidrat alınsın mı?", + "basal adjustment out of range, give bolus?": "bazal ayarlaması limit dışı, bolus alınsın mı?", + "above high": "yüksek hedefin üzerinde", + "below low": "düşük hedefin altında", + "Projected BG %1 target": "Beklenen KŞ %1", + "aiming at": "hedeflenen", + "Bolus %1 units": "Bolus %1 Ünite", + "or adjust basal": "ya da bazalı ayarla", + "Check BG using glucometer before correcting!": "Düzeltme bolusu öncesi glukometreyle parmaktan KŞ'ni kontrol edin!", + "Basal reduction to account %1 units:": "%1 birimi telafi etmek için bazal azaltma:", + "30m temp basal": "30 dk. geçici bazal", + "1h temp basal": "1 sa. geçici bazal", + "Cannula change overdue!": "Kanül değişimi gecikmiş!", + "Time to change cannula": "Kanül değiştirme zamanı", + "Change cannula soon": "Yakında kanül değiştirin", + "Cannula age %1 hours": "Kanül yaşı %1 saat", + "Inserted": "Yerleştirilmiş", + "CAGE": "İnfüzyon seti yaşı", + "COB": "AKRB", + "Last Carbs": "Son Karbonhidratlar", + "IAGE": "İns. Yaşı", + "Insulin reservoir change overdue!": "İnsülin rezervuarı değişimi gecikmiş!", + "Time to change insulin reservoir": "İnsülin rezervuarını değiştirme zamanı!", + "Change insulin reservoir soon": "Yakında insülin rezervuarını değiştirin", + "Insulin reservoir age %1 hours": "İnsülin rezervuar yaşı %1 saat", + "Changed": "Değişmiş", + "IOB": "AİNS", + "Careportal IOB": "Careportal AİNS (Aktif İnsülin)", + "Last Bolus": "Son Bolus", + "Basal IOB": "Bazal AİNS", + "Source": "Kaynak", + "Stale data, check rig?": "Veri güncel değil, vericiyi kontrol et?", + "Last received:": "Son alınan:", + "%1m ago": "%1 dk. önce", + "%1h ago": "%1 sa. önce", + "%1d ago": "%1 gün önce", + "RETRO": "RETRO Geçmiş", + "SAGE": "SYAŞ", + "Sensor change/restart overdue!": "Sensör değişimi/yeniden başlatma gecikti!", + "Time to change/restart sensor": "Sensörü değiştirme/yeniden başlatma zamanı", + "Change/restart sensor soon": "Sensörü yakında değiştir/yeniden başlat", + "Sensor age %1 days %2 hours": "Sensör yaşı %1 gün %2 saat", + "Sensor Insert": "Sensor yerleştirme", + "Sensor Start": "Sensör başlatma", + "days": "gün", + "Insulin distribution": "İnsülin dağılımı", + "To see this report, press SHOW while in this view": "Bu raporu görmek için bu görünümde GÖSTER düğmesine basın.", + "AR2 Forecast": "AR2 Tahmini", + "OpenAPS Forecasts": "OpenAPS Tahminleri", + "Temporary Target": "Geçici Hedef", + "Temporary Target Cancel": "Geçici Hedef İptal", + "OpenAPS Offline": "OpenAPS çevrimdışı", + "Profiles": "Profiller", + "Time in fluctuation": "Dalgalanmada geçen süre", + "Time in rapid fluctuation": "Hızlı dalgalanmalarda geçen süre", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Bu bir kaba tahmindir ve çok hata içerebilir gerçek kan şekeri testlerinin yerini tutmayacaktır. Kullanılan formülde buradandır:", + "Filter by hours": "Saatlere göre filtrele", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Dalgalanmadaki zaman ve Hızlı dalgalanmadaki zaman, kan şekerinin nispeten hızlı veya çok hızlı bir şekilde değiştiği, incelenen dönemdeki zamanın %'sini ölçer. Düşük değerler daha iyidir.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Toplam Günlük Değişim, incelenen süre için, gün sayısına bölünen tüm glukoz değerlerinin mutlak değerinin toplamıdır. Düşük değer daha iyidir.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Saat başına ortalama değişim, gözlem periyodu üzerindeki tüm glikoz değişikliklerinin mutlak değerlerinin saat sayısına bölünmesiyle elde edilen toplam değerdir. Düşük değerler daha iyidir.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Aralık Dışı RMS, incelenen dönem içindeki tüm glikoz okumaları için aralık dışı mesafenin karesi alınarak, bunların toplanması, sayıma bölünmesi ve karekök alınmasıyla hesaplanır. Bu metrik, aralık içi yüzdeye benzer, ancak ölçümlerin ağırlıkları aralık dışında daha yüksektir. Daha düşük değerler daha iyidir.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">burada bulabilirsiniz.", + "Mean Total Daily Change": "Ortalama Toplam Günlük Değişim", + "Mean Hourly Change": "Ortalama Saatlik Değişim", + "FortyFiveDown": "biraz düşen", + "FortyFiveUp": "biraz yükselen", + "Flat": "sabit", + "SingleUp": "yükseliyor", + "SingleDown": "düşüyor", + "DoubleDown": "hızlı düşen", + "DoubleUp": "hızla yükselen", + "virtAsstUnknown": "Bu değer şu anda bilinmiyor. Daha fazla ayrıntı için lütfen Nightscout sitenize bakın.", + "virtAsstTitleAR2Forecast": "AR2 Tahmini", + "virtAsstTitleCurrentBasal": "Geçerli Bazal", + "virtAsstTitleCurrentCOB": "Geçerli AKRB", + "virtAsstTitleCurrentIOB": "Geçerli AİNS", + "virtAsstTitleLaunch": "Nightscout'a hoş geldiniz", + "virtAsstTitleLoopForecast": "Döngü Tahmini", + "virtAsstTitleLastLoop": "Son Döngü", + "virtAsstTitleOpenAPSForecast": "OpenAPS Tahmini", + "virtAsstTitlePumpReservoir": "Kalan İnsülin", + "virtAsstTitlePumpBattery": "Pompa Pili", + "virtAsstTitleRawBG": "Geçerli ham KŞ değeri", + "virtAsstTitleUploaderBattery": "Yükleyici Pili", + "virtAsstTitleCurrentBG": "Geçerli KŞ", + "virtAsstTitleFullStatus": "Tam Durum", + "virtAsstTitleCGMMode": "CGM Modu", + "virtAsstTitleCGMStatus": "CGM Durumu", + "virtAsstTitleCGMSessionAge": "CGM Oturum Yaşı", + "virtAsstTitleCGMTxStatus": "CGM Verici Durumu", + "virtAsstTitleCGMTxAge": "CGM Verici Yaşı", + "virtAsstTitleCGMNoise": "CGM Gürültüsü", + "virtAsstTitleDelta": "Kan Şekeri Delta", + "virtAsstStatus": "%3 itibariyle %1 ve %2.", + "virtAsstBasal": "%1 geçerli bazal oranı saatte %2 ünite", + "virtAsstBasalTemp": "%1 geçici bazal %2 ünite %3 sona eriyor", + "virtAsstIob": "ve %1 aktif insulin var.", + "virtAsstIobIntent": "%1 aktif insülin var", + "virtAsstIobUnits": "%1 ünite", + "virtAsstLaunch": "Nightscout'ta neyi kontrol etmek istersiniz?", + "virtAsstPreamble": "Senin", + "virtAsstPreamble3person": "%1 ", + "virtAsstNoInsulin": "yok", + "virtAsstUploadBattery": "Yükleyici piliniz %1", + "virtAsstReservoir": "%1 ünite kaldı", + "virtAsstPumpBattery": "Pompa piliniz %1 %2", + "virtAsstUploaderBattery": "Yükleyicinizin pili %1 seviyesinde", + "virtAsstLastLoop": "Son başarılı döngü %1 oldu", + "virtAsstLoopNotAvailable": "Döngü eklentisi etkin görünmüyor", + "virtAsstLoopForecastAround": "Döngü tahminine göre, sonraki %2'de %1 civarında olmanız bekleniyor", + "virtAsstLoopForecastBetween": "Döngü tahminine göre, sonraki %3'te %1 ile %2 arasında olmanız bekleniyor", + "virtAsstAR2ForecastAround": "AR2 tahminine göre, sonraki %2'de %1 civarında olmanız bekleniyor", + "virtAsstAR2ForecastBetween": "AR2 tahminine göre, sonraki %3'te %1 ile %2 arasında olmanız bekleniyor", + "virtAsstForecastUnavailable": "Mevcut verilerle tahmin edilemedi", + "virtAsstRawBG": "Ham kan şekeriniz %1", + "virtAsstOpenAPSForecast": "OpenAPS tarafından tahmin edilen kan şekeri %1", + "virtAsstCob3person": "%1'de %2 aktif karbonhidrat var", + "virtAsstCob": "%1 aktif karbonhidrat var", + "virtAsstCGMMode": "CGM modunuz %2 itibariyle %1 idi.", + "virtAsstCGMStatus": "CGM durumunuz %2 itibariyle %1 idi.", + "virtAsstCGMSessAge": "CGM oturumunuz %1 gün ve %2 saattir etkin.", + "virtAsstCGMSessNotStarted": "Şu anda aktif bir CGM oturumu bulunmuyor.", + "virtAsstCGMTxStatus": "CGM transmitter verici durumunuz %2 itibariyle %1 idi.", + "virtAsstCGMTxAge": "CGM transmitter vericiniz %1 günlük.", + "virtAsstCGMNoise": "CGM sensör gürültüsü %2 itibariyle %1 idi.", + "virtAsstCGMBattOne": "CGM piliniz %2 itibariyle %1 volttu.", + "virtAsstCGMBattTwo": "CGM pil seviyeleriniz %3 itibariyle %1 volt ve %2 volttur.", + "virtAsstDelta": "Deltanız %2 ile %3 arasında %1 idi.", + "virtAsstDeltaEstimated": "Tahmini deltanız %2 ile %3 arasında %1 idi.", + "virtAsstUnknownIntentTitle": "Niyet Bilinmiyor", + "virtAsstUnknownIntentText": "Üzgünüm, ne istediğinizi bilmiyorum.", + "Fat [g]": "Yağ [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Enerji [kJ]", + "Clock Views:": "Saat Görünümleri:", + "Clock": "Saat", + "Color": "Renk", + "Simple": "Sade", + "TDD average": "Ortalama günlük Toplam Doz (TDD)", + "Bolus average": "Bolus Ortalaması", + "Basal average": "Bazal ortalaması", + "Base basal average:": "Temel bazal ortalaması:", + "Carbs average": "Karbonhidrat ortalaması", + "Eating Soon": "Yakında Yenecek", + "Last entry {0} minutes ago": "Son giriş {0} dakika önce", + "change": "değişiklik", + "Speech": "Konuş", + "Target Top": "Üst Hedef", + "Target Bottom": "Alt Hedef", + "Canceled": "İptal edildi", + "Meter BG": "Glikometre KŞ", + "predicted": "tahmin", + "future": "gelecek", + "ago": "önce", + "Last data received": "Son veri alındı", + "Clock View": "Saat Görünümü", + "Protein": "Protein", + "Fat": "Yağ", + "Protein average": "Protein Ortalaması", + "Fat average": "Yağ Ortalaması", + "Total carbs": "Toplam Karbonhidrat", + "Total protein": "Toplam Protein", + "Total fat": "Toplam Yağ", + "Database Size": "Veritabanı Boyutu", + "Database Size near its limits!": "Veritabanı Boyutu limitlerine yakın!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Veritabanı boyutu %2 MiB üzerinden %1 MiB'dır. Lütfen veritabanını yedekleyin ve temizleyin!", + "Database file size": "Veritabanı dosya boyutu", + "%1 MiB of %2 MiB (%3%)": "%1 MiB den %2 MiB (%3%)", + "Data size": "Veri boyutu", + "virtAsstDatabaseSize": "%1 MiB. Bu kullanılabilir veritabanı alanının %2%'sidir.", + "virtAsstTitleDatabaseSize": "Veritabanı dosya boyutu", + "Carbs/Food/Time": "Karbonhidrat/Gıda/Zaman", + "You have administration messages": "Yönetim mesajlarınız var", + "Admin messages in queue": "Sıradaki yönetici mesajları", + "Queue empty": "Sıra boş", + "There are no admin messages in queue": "Sırada yönetici mesajı yok", + "Please sign in using the API_SECRET to see your administration messages": "Yönetim mesajlarınızı görmek için lütfen API_SECRET'i kullanarak oturum açın", + "Reads enabled in default permissions": "Varsayılan izinlerde okuma yetkisi etkin", + "Data reads enabled": "Veri okumaları etkinleştirildi", + "Data writes enabled": "Veri yazma etkin", + "Data writes not enabled": "Veri yazma etkin değil", + "Color prediction lines": "Renkli tahmin çizgileri", + "Release Notes": "Sürüm Notları", + "Check for Updates": "Güncellemeleri Kontrol Et", + "Open Source": "Açık Kaynaklı", + "Nightscout Info": "Nightscout Bilgisi", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Loopalyzer'ın birincil amacı, Loop kapalı döngü sisteminin nasıl performans gösterdiğini görselleştirmektir. Hem kapalı hem de açık döngü ve döngü olmayan diğer kurulumlarla da çalışabilir. Ancak hangi yükleyiciyi kullandığınıza, verilerinizi ne sıklıkta yakalayıp yükleyebildiğine ve eksik verileri nasıl geri doldurabildiğine bağlı olarak bazı grafiklerde boşluklar olabilir ve hatta tamamen boş olabilir. Grafiklerin her zaman makul göründüğünden emin olun. En iyisi, her seferinde bir günü görüntülemek ve ilk önce görmek için birkaç gün arasında gezinmektir.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer bir zaman kaydırma özelliği içerir. Örneğin, bir gün 07:00'de ve ortalama kan şekeri eğrinizden sonraki gün 08:00'de kahvaltı yaparsanız, bu iki gün büyük olasılıkla düzleşecek ve kahvaltıdan sonra gerçek yanıtı göstermeyecektir. Zaman kaydırma, bu öğünlerin yenildiği ortalama süreyi hesaplayacak ve ardından her iki gün boyunca tüm verileri (karbonhidrat, insülin, bazal vb.) karşılık gelen zaman farkını kaydıracak, böylece her iki öğün de ortalama öğün başlangıç zamanı ile aynı hizaya gelecektir.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "Bu örnekte, ilk güne ait tüm veriler zamanda 30 dakika ileri ve ikinci güne ait tüm veriler zamanda 30 dakika geriye alınır, böylece her iki gün de 07:30'da kahvaltı yapmışsınız gibi görünür. Bu, bir yemekten gerçek ortalama kan şekeri değişiminizi görmenizi sağlar.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Zaman kayması, DIA (İnsülin Etkinlik Süresi) süresi boyunca ortalama yemek başlama zamanından sonraki süreyi gri renkle vurgular. Tüm gün boyunca tüm veri noktaları kaydırıldığından, gri alanın dışındaki eğriler doğru olmayabilir.", + "Note that time shift is available only when viewing multiple days.": "Zaman kaymasının yalnızca birden fazla gün görüntülenirken kullanılabileceğini unutmayın.", + "Please select a maximum of two weeks duration and click Show again.": "Lütfen en fazla iki haftalık bir süre seçin ve tekrar Göster'e tıklayın.", + "Show profiles table": "Profil tablosunu göster", + "Show predictions": "Tahminleri göster", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "%2 ile %3 arasında tüketilen karbonhidrat içeriği %1 g'dan fazla olan öğünlerde saat farkı", + "Previous": "Önceki", + "Previous day": "Önceki gün", + "Next day": "Sonraki gün", + "Next": "Sonraki", + "Temp basal delta": "Geçici bazal delta", + "Authorized by token": "Token tarafından yetkilendirilmiş", + "Auth role": "Kimlik doğrulama rolü", + "view without token": "token'sız görüntüle", + "Remove stored token": "Depolanan token'ı kaldır", + "Weekly Distribution": "Haftalık Dağılım", + "Failed authentication": "Başarısız kimlik doğrulama", + "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?": "%1 IP adresindeki bir cihaz, yanlış kimlik bilgileriyle Nightscout ile kimlik doğrulamayı denedi. Yanlış API_SECRET veya tokenlu bir yükleyici kurulumunuz olup olmadığını kontrol ediniz?", + "Default (with leading zero and U)": "Varsayılan (başında sıfır ve U ile)", + "Concise (with U, without leading zero)": "Kısa (U ile, başında 0 olmadan)", + "Minimal (without leading zero and U)": "Minimal (başında sıfır ve U olmadan)", + "Small Bolus Display": "Küçük Bolus Ekranı", + "Large Bolus Display": "Büyük Bolus Ekranı", + "Bolus Display Threshold": "Bolus Görüntüleme Eşiği", + "%1 U and Over": "%1 U ve Üzeri", + "Event repeated %1 times.": "Etkinlik %1 kez tekrarlandı.", + "minutes": "dakika", + "Last recorded %1 %2 ago.": "En son %1 %2 önce kaydedildi.", + "Security issue": "Güvenlik Sorunu", + "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.": "Zayıf API_SECRET algılandı. Yetkisiz erişim riskini azaltmak için lütfen küçük ve BÜYÜK harf, sayı ve !#%&/ gibi alfasayısal olmayan karakterleri bir arada kullanın. API_SECRET'in minimum uzunluğu 12 karakterdir.", + "less than 1": "1'den az", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB şifresi ve API_SECRET eşleşmesi. Bu gerçekten kötü bir fikir. Lütfen her ikisini de değiştirin ve sistem genelinde parolaları yeniden kullanmayın." +} diff --git a/translations/uk_UA.json b/translations/uk_UA.json new file mode 100644 index 00000000000..9f816014c82 --- /dev/null +++ b/translations/uk_UA.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Прослуховування порту", + "Mo": "Пн", + "Tu": "Вт", + "We": "Ср", + "Th": "Чт", + "Fr": "Пт", + "Sa": "Сб", + "Su": "Нд", + "Monday": "Понеділок", + "Tuesday": "Вівторок", + "Wednesday": "Середа", + "Thursday": "Четвер", + "Friday": "П'ятниця", + "Saturday": "Субота", + "Sunday": "Неділя", + "Category": "Категорія", + "Subcategory": "Підкатегорія", + "Name": "Ім'я", + "Today": "Сьогодні", + "Last 2 days": "Минулі 2 дні", + "Last 3 days": "Минулі 3 дні", + "Last week": "Минулий тиждень", + "Last 2 weeks": "Минулих 2 тижні", + "Last month": "Минулий місяць", + "Last 3 months": "Минулих 3 місяці", + "From": "З", + "To": "До", + "Notes": "Примітки", + "Food": "Їжа", + "Insulin": "Інсулін", + "Carbs": "Вуглеводи", + "Notes contain": "Примітки містять", + "Target BG range bottom": "Нижній поріг цільових значень ЦК", + "top": "верх", + "Show": "Показати", + "Display": "Показувати", + "Loading": "Завантаження", + "Loading profile": "Завантаження профілю", + "Loading status": "Завантаження статусу", + "Loading food database": "Завантаження бази продуктів", + "not displayed": "не показано", + "Loading CGM data of": "Завантаження даних моніторингу", + "Loading treatments data of": "Завантаження даних терапії", + "Processing data of": "Обробка даних від", + "Portion": "Порція", + "Size": "Розмір", + "(none)": "(немає)", + "None": "Немає", + "": "<немає>", + "Result is empty": "Результату немає", + "Day to day": "День за день", + "Week to week": "Тиждень у тиждень", + "Daily Stats": "Щоденна статистика", + "Percentile Chart": "Відсоткова діаграма", + "Distribution": "Розподіл", + "Hourly stats": "Погодинна статистика", + "netIOB stats": "Статистика netIOB", + "temp basals must be rendered to display this report": "для звіту потрібно промалювати час базалу", + "No data available": "Немає даних", + "Low": "Низький", + "In Range": "В діапазоні", + "Period": "Перiод", + "High": "Високий", + "Average": "Середнє", + "Low Quartile": "Нижня четверть", + "Upper Quartile": "Верхня четверть", + "Quartile": "Четверть", + "Date": "Дата", + "Normal": "Нормальний", + "Median": "Медіана", + "Readings": "Читання", + "StDev": "Стандартне відхилення", + "Daily stats report": "Щоденна статистика", + "Glucose Percentile report": "Процентний звіт", + "Glucose distribution": "Розподіл глюкози", + "days total": "всього днів", + "Total per day": "Всього за добу", + "Overall": "Усього", + "Range": "Діапозон", + "% of Readings": "% вимірювань", + "# of Readings": "# вимірювань", + "Mean": "Середнє", + "Standard Deviation": "Стандартне відхилення", + "Max": "Макс", + "Min": "Мін", + "A1c estimation*": "Очікуваний HbA1c*", + "Weekly Success": "Підсумки тижня", + "There is not sufficient data to run this report. Select more days.": "Недостатньо даних для звіту. Виберіть більше днів.", + "Using stored API secret hash": "Використання збереженого хешу API", + "No API secret hash stored yet. You need to enter API secret.": "Немає збереженого хешу API. Вам потрібно ввести API.", + "Database loaded": "Базу даних завантажено", + "Error: Database failed to load": "Помилка: Не вдалося завантажити базу даних", + "Error": "Помилка", + "Create new record": "Створити новий запис", + "Save record": "Зберегти запис", + "Portions": "Порції", + "Unit": "Одиниця", + "GI": "GI", + "Edit record": "Редагувати запис", + "Delete record": "Видалити запис", + "Move to the top": "Перемістити на початок", + "Hidden": "Прихований", + "Hide after use": "Приховати після використання", + "Your API secret must be at least 12 characters long": "Ваш пароль API повинен мати щонайменше 12 символів", + "Bad API secret": "Не вірний пароль API", + "API secret hash stored": "Хеш пароля API збережений", + "Status": "Статус", + "Not loaded": "Не завантажено", + "Food Editor": "Редактор продуктів", + "Your database": "Ваша база даних", + "Filter": "Фільтр", + "Save": "Зберегти", + "Clear": "Очистити", + "Record": "Запис", + "Quick picks": "Швидкий вибір", + "Show hidden": "Показати приховане", + "Your API secret or token": "Ваш пароль API або токен", + "Remember this device. (Do not enable this on public computers.)": "Запам'ятати цей пристрій. (Не використовуйте у загальному доступі.)", + "Treatments": "Терапія", + "Time": "Час", + "Event Type": "Тип події", + "Blood Glucose": "Глюкоза крові", + "Entered By": "Введено", + "Delete this treatment?": "Видалити цю терапію?", + "Carbs Given": "Видано вуглеводів", + "Insulin Given": "Введено інсуліну", + "Event Time": "Час події", + "Please verify that the data entered is correct": "Будь ласка перевірте правильність введених даних", + "BG": "ГК", + "Use BG correction in calculation": "При розрахунку враховувати корекцію ГК", + "BG from CGM (autoupdated)": "ГК із моніторингу (автооновлення)", + "BG from meter": "ГК із глюкометра", + "Manual BG": "Ручне введення ГК", + "Quickpick": "Швидкий вибір", + "or": "або", + "Add from database": "Додати з бази даних", + "Use carbs correction in calculation": "Під час розрахунку використовувати вуглеводну корекцію", + "Use COB correction in calculation": "Під час розрахунку використовувати COB корекцію", + "Use IOB in calculation": "Під час розрахунку використовувати IOB", + "Other correction": "Інша корекція", + "Rounding": "Округлення", + "Enter insulin correction in treatment": "Введіть корекцію інсуліну в терапію", + "Insulin needed": "Потрібен інсулін", + "Carbs needed": "Потрібні вуглеводи", + "Carbs needed if Insulin total is negative value": "Потрібні вуглеводи, якщо сумарний інсулін є негативною величиною", + "Basal rate": "Норма базалу", + "60 minutes earlier": "60 хв раніше", + "45 minutes earlier": "45 хв раніше", + "30 minutes earlier": "30 хв раніше", + "20 minutes earlier": "20 хв раніше", + "15 minutes earlier": "На 15 хвилин раніше", + "Time in minutes": "Час у хвилинах", + "15 minutes later": "Через 15 хвилин", + "20 minutes later": "Через 20 хвилин", + "30 minutes later": "На 30 хв пізніше", + "45 minutes later": "На 45 хв пізніше", + "60 minutes later": "На 60 хв пізніше", + "Additional Notes, Comments": "Додаткові примітки", + "RETRO MODE": "Режим РЕТРО", + "Now": "Зараз", + "Other": "Інше", + "Submit Form": "Підтвердити форму", + "Profile Editor": "Редактор профілю", + "Reports": "Звіти", + "Add food from your database": "Додайте їжу з вашої бази даних", + "Reload database": "Перезавантажити базу даних", + "Add": "Додати", + "Unauthorized": "Не авторизовано", + "Entering record failed": "Введення запису не вдалося", + "Device authenticated": "Пристрій автентифіковано", + "Device not authenticated": "Пристрій не автентифіковано", + "Authentication status": "Статус автентифікації", + "Authenticate": "Автентифікувати", + "Remove": "Видаліть", + "Your device is not authenticated yet": "Ваш пристрій ще не автентифіковано", + "Sensor": "Датчик", + "Finger": "Палець", + "Manual": "Вручну", + "Scale": "Масштаб", + "Linear": "Лінійний", + "Logarithmic": "Логарифмічний", + "Logarithmic (Dynamic)": "Логарифмічна (Динамічна)", + "Insulin-on-Board": "Активний інсулін (IOB)", + "Carbs-on-Board": "Активні вуглеводи COB", + "Bolus Wizard Preview": "Калькулятор болюсу", + "Value Loaded": "Значення завантажено", + "Cannula Age": "Катетеру", + "Basal Profile": "Профіль базала", + "Silence for 30 minutes": "Тиша на 30 хвилин", + "Silence for 60 minutes": "Тиша на 60 хвилин", + "Silence for 90 minutes": "Тиша на 90 хвилин", + "Silence for 120 minutes": "Тиша на 120 хвилин", + "Settings": "Налаштування", + "Units": "Одиниці", + "Date format": "Формат дати", + "12 hours": "12 годин", + "24 hours": "24 години", + "Log a Treatment": "Записати терапію", + "BG Check": "Контроль ГК", + "Meal Bolus": "Болюс на їду", + "Snack Bolus": "Подати болюс", + "Correction Bolus": "Корекція болюсу", + "Carb Correction": "Корекція вуглеводів", + "Note": "Примітка", + "Question": "Питання", + "Exercise": "Навантаження", + "Pump Site Change": "Зміна місця катетера помпи", + "CGM Sensor Start": "Старт сенсора моніторингу", + "CGM Sensor Stop": "Зупинка сенсора моніторингу", + "CGM Sensor Insert": "Встановлення сенсора моніторингу", + "Sensor Code": "Код сенсора", + "Transmitter ID": "ID передавача", + "Dexcom Sensor Start": "Старт сенсора Dexcom", + "Dexcom Sensor Change": "Зміна сенсора Dexcom", + "Insulin Cartridge Change": "Зміна картриджа інсуліну", + "D.A.D. Alert": "Сигнал D.A.D.", + "Glucose Reading": "Зчитування глюкози", + "Measurement Method": "Метод вимірювання", + "Meter": "Вимірювач", + "Amount in grams": "Кількість в грамах", + "Amount in units": "Кількість в одиницях", + "View all treatments": "Переглянути всю терапію", + "Enable Alarms": "Увімкнути сигнали тривоги", + "Pump Battery Change": "Заміна батареї помпи", + "Pump Battery Age": "Батарея помпи працює", + "Pump Battery Low Alarm": "Низький заряд батареї помпи", + "Pump Battery change overdue!": "Пропущено термін заміни батареї помпи!", + "When enabled an alarm may sound.": "Коли ввімкнено, може звучати сигнал.", + "Urgent High Alarm": "Увага висока глікемія", + "High Alarm": "Висока глікемія", + "Low Alarm": "Низька глікемія", + "Urgent Low Alarm": "Увага низька глікемія", + "Stale Data: Warn": "Попереджати про застарілі дані", + "Stale Data: Urgent": "Увага застарілі дані", + "mins": "хв.", + "Night Mode": "Нічний режим", + "When enabled the page will be dimmed from 10pm - 6am.": "При активації сторінка буде затемнена з 22:00 до 06:00.", + "Enable": "Увімкнути", + "Show Raw BG Data": "Показувати необроблені дані", + "Never": "Ніколи", + "Always": "Завжди", + "When there is noise": "Коли є шум", + "When enabled small white dots will be displayed for raw BG data": "При ввімкнені необроблені дані будуть помітні як дрібні білі крапки", + "Custom Title": "Довільна назва", + "Theme": "Тема", + "Default": "За умовчанням", + "Colors": "Кольори", + "Colorblind-friendly colors": "Колірна гама для людей із порушеннями зору", + "Reset, and use defaults": "Скидання та використання за замовчуванням", + "Calibrations": "Калібрування", + "Alarm Test / Smartphone Enable": "Перевірка оповіщень / Смартфон активний", + "Bolus Wizard": "Калькулятор болюса", + "in the future": "у майбутньому", + "time ago": "тому", + "hr ago": " год тому", + "hrs ago": " год тому", + "min ago": "хв тому", + "mins ago": "хв тому", + "day ago": "день назад", + "days ago": "днів назад", + "long ago": "давно", + "Clean": "Чисто", + "Light": "Легкий", + "Medium": "Середній", + "Heavy": "Сильний", + "Treatment type": "Тип події", + "Raw BG": "RAW дані", + "Device": "Пристрій", + "Noise": "Шум", + "Calibration": "Калібрування", + "Show Plugins": "Показати плагіни", + "About": "Про застосунок", + "Value in": "Значення в", + "Carb Time": "Час прийому вуглеводів", + "Language": "Мова", + "Add new": "Додати новий", + "g": "г", + "ml": "мл", + "pcs": "шт", + "Drag&drop food here": "Перетягніть сюди їжу", + "Care Portal": "Портал терапії", + "Medium/Unknown": "Середній/Невідомий", + "IN THE FUTURE": "У МАЙБУТНЬОМУ", + "Order": "Сортувати", + "oldest on top": "Старі зверху", + "newest on top": "Нові зверху", + "All sensor events": "Усі події сенсора", + "Remove future items from mongo database": "Видалити майбутні дані з бази Mongo", + "Find and remove treatments in the future": "Знаходити та видаляти методи терапії в майбутньому", + "This task find and remove treatments in the future.": "Ця опція знайде та видалить терапію в майбутньому.", + "Remove treatments in the future": "Видаляйте терапії в майбутньому", + "Find and remove entries in the future": "Знайти та видалити записи в майбутньому", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "Ця опція знайде і видалить дані CGM у майбутньому, створені завантажувачем з неправильною датою/часом.", + "Remove entries in the future": "Видалити записи в майбутньому", + "Loading database ...": "Завантаження бази даних...", + "Database contains %1 future records": "База даних містить %1 майбутніх записів", + "Remove %1 selected records?": "Видалити %1 вибраних записів?", + "Error loading database": "Помилка завантаження бази даних", + "Record %1 removed ...": "Запис %1 видалено...", + "Error removing record %1": "Помилка при видаленні запису %1", + "Deleting records ...": "Видалення записів ...", + "%1 records deleted": "%1 записів видалено", + "Clean Mongo status database": "Очистити статус бази даних Mongo", + "Delete all documents from devicestatus collection": "Видалити всі документи з пристроїв збору", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "Ця опція видаляє всі документи з пристроїв збору. Корисно, коли стан батареї завантажується некоректно.", + "Delete all documents": "Видалити всі документи", + "Delete all documents from devicestatus collection?": "Видалити всі документи з пристроїв збору?", + "Database contains %1 records": "База даних містить %1 записів", + "All records removed ...": "Усі записи видалено ...", + "Delete all documents from devicestatus collection older than 30 days": "Видалити всі документи з пристроїв збору старші 30 днів", + "Number of Days to Keep:": "Кількість днів для зберігання:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "Ця опція видаляє всі документи пристроїв збору, яким понад 30 днів. Корисно, коли статус батареї оновлюється некоректно.", + "Delete old documents from devicestatus collection?": "Видалити старі документи з пристрою збору?", + "Clean Mongo entries (glucose entries) database": "Очистити записи глюкози у базі даних Mongo", + "Delete all documents from entries collection older than 180 days": "Видалити всі документи із записів старше 180 днів", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Ця опція видалить всі документи із записів старше 180 днів. Корисно, коли статус батареї оновлюється некоректно.", + "Delete old documents": "Видалити старі документи", + "Delete old documents from entries collection?": "Видалити старі документи із записів?", + "%1 is not a valid number": "%1 є некоректним числом", + "%1 is not a valid number - must be more than 2": "%1 є некоректним числом - має бути більше ніж 2", + "Clean Mongo treatments database": "Очистити базу даних терапії Mongo", + "Delete all documents from treatments collection older than 180 days": "Видалити всі документи з терапії старше 180 днів", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "Ця опція видалить всі документи з терапії старше 180 днів. Корисно, коли статус батареї оновлюється некоректно.", + "Delete old documents from treatments collection?": "Видалити старі документи з терапії?", + "Admin Tools": "Інструменти адміністратора", + "Nightscout reporting": "Звіт Nightscout", + "Cancel": "Скасувати", + "Edit treatment": "Редагувати терапію", + "Duration": "Тривалість", + "Duration in minutes": "Тривалість у хвилинах", + "Temp Basal": "Тимч. базал", + "Temp Basal Start": "Початок тимч. базала", + "Temp Basal End": "Закінчення тимч. базала", + "Percent": "Відсоток", + "Basal change in %": "Зміна базала в %", + "Basal value": "Величина базала", + "Absolute basal value": "Абсолютна величина базала", + "Announcement": "Оголошення", + "Loading temp basal data": "Завантаження даних тимч. базала", + "Save current record before changing to new?": "Зберегти поточний запис перед переходом до нових?", + "Profile Switch": "Перемикач профілю", + "Profile": "Профіль", + "General profile settings": "Загальні налаштування профілю", + "Title": "Назва", + "Database records": "Записи бази даних", + "Add new record": "Додати новий запис", + "Remove this record": "Видалити цей запис", + "Clone this record to new": "Клонувати цей запис у новий", + "Record valid from": "Запис дійсний від", + "Stored profiles": "Збережені профілі", + "Timezone": "Часовий пояс", + "Duration of Insulin Activity (DIA)": "Тривалість дії інсуліну (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Представляє типову тривалість, протягом якої інсулін набуває чинності. Залежить від пацієнта та типу інсуліну. Зазвичай 3-4 години для більшості помпових інсулінів і більшості пацієнтів.", + "Insulin to carb ratio (I:C)": "Співвідношення інсулін/вуглеводи (I:C)", + "Hours:": "Години:", + "hours": "години", + "g/hour": "г/год", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "г вуглеводів на одиницю інсуліну. Співвідношення показує кількість г вуглеводів, яке компенсується кожною одиницею інсуліну.", + "Insulin Sensitivity Factor (ISF)": "Чутливість до інсуліну (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL або mmol/L на одиницю інсуліну. Коефіцієнт того, скільки BG змінюється з кожною одиницею інсуліну на корекцію.", + "Carbs activity / absorption rate": "Активність вуглеводів / швидкість засвоєння", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "грам на одиницю часу. Представляє зміну кількості вуглеводів в організмі COB за одиницю часу і кількість активних вуглеводів. Крива засвоєння вуглеводів менш зрозуміла ніж активність інсуліну, але приблизно може бути представлена ​​у вигляді початкової затримки з наступною постійною швидкістю поглинання (г/год).", + "Basal rates [unit/hour]": "Швидкість базалу [од./час]", + "Target BG range [mg/dL,mmol/L]": "Цільовий діапазон [mg/dL, mmol/L]", + "Start of record validity": "Початок валідності записів", + "Icicle": "Бурулька", + "Render Basal": "Показувати базал", + "Profile used": "Використовуваний профіль", + "Calculation is in target range.": "Розрахункова величина у цільовому діапазоні.", + "Loading profile records ...": "Завантаження записів профілю...", + "Values loaded.": "Значення завантажено.", + "Default values used.": "Використовуються стандартні значення.", + "Error. Default values used.": "Помилка. Використовуються стандартні значення.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Діапазони нижніх та верхніх цільових значень не збігаються. Відновлено значення за промовчанням.", + "Valid from:": "Дійсний з:", + "Save current record before switching to new?": "Зберегти поточний запис перед переходом до нових?", + "Add new interval before": "Додати новий інтервал перед цим", + "Delete interval": "Видалити інтервал", + "I:C": "І:С", + "ISF": "ISF", + "Combo Bolus": "Комбінований болюс", + "Difference": "Різниця", + "New time": "Новий час", + "Edit Mode": "Режим редагування", + "When enabled icon to start edit mode is visible": "При активації видно піктограму почати режим редагування", + "Operation": "Операція", + "Move": "Перемістити", + "Delete": "Видалити", + "Move insulin": "Перемістити інсулін", + "Move carbs": "Перемістити вуглеводи", + "Remove insulin": "Видалити інсулін", + "Remove carbs": "Видалити вуглеводи", + "Change treatment time to %1 ?": "Змінити час події на %1?", + "Change carbs time to %1 ?": "Змінити час прийому вуглеводів на %1?", + "Change insulin time to %1 ?": "Змінити час подачі інсуліну на %1?", + "Remove treatment ?": "Видалити подію?", + "Remove insulin from treatment ?": "Видалити інсулін з події?", + "Remove carbs from treatment ?": "Видалити вуглеводи з події?", + "Rendering": "Побудова графіка", + "Loading OpenAPS data of": "Завантаження даних OpenAPS", + "Loading profile switch data": "Завантаження даних перемикання профілю", + "Redirecting you to the Profile Editor to create a new profile.": "Перенаправлення вас до редактора профілю, щоб створити новий профіль.", + "Pump": "Помпа", + "Sensor Age": "Термін сенсора", + "Insulin Age": "Термін інсуліну", + "Temporary target": "Тимчасова ціль", + "Reason": "Підстава", + "Eating soon": "Очікуваний прийом їжі", + "Top": "верх", + "Bottom": "низ", + "Activity": "Активність", + "Targets": "Цілі", + "Bolus insulin:": "Болюс інсулін:", + "Base basal insulin:": "Базовий базал інсулін:", + "Positive temp basal insulin:": "Позитивне значення базал інсуліну:", + "Negative temp basal insulin:": "Негативне значення базал інсуліну:", + "Total basal insulin:": "Загальний базал інсулін:", + "Total daily insulin:": "Загальний добовий інсулін:", + "Unable to save Role": "Неможливо зберегти Роль", + "Unable to delete Role": "Неможливо видалити Роль", + "Database contains %1 roles": "База даних містить %1 ролей", + "Edit Role": "Змінити Роль", + "admin, school, family, etc": "адмін, школа, сім'я тощо", + "Permissions": "Дозволи", + "Are you sure you want to delete: ": "Ви впевнені, що хочете видалити: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Кожна роль матиме 1 або більше дозволів. Дозвіл * це символ підставляння, дозволи це ієрархія, що використовує : як роздільник.", + "Add new Role": "Додати нову Роль", + "Roles - Groups of People, Devices, etc": "Ролі - групи людей, пристроїв тощо", + "Edit this role": "Редагувати дану Роль", + "Admin authorized": "Адміністратора авторизовано", + "Subjects - People, Devices, etc": "Суб'єкти - люди, пристрої тощо", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Кожна тема матиме унікальний токен доступу та 1 або більше ролей. Натисніть на маркер доступу, щоб відкрити новий вид з обраною темою, цим секретним посиланням потім можна поділитися.", + "Add new Subject": "Додати нового суб'єкта", + "Unable to save Subject": "Неможливо зберегти суб'єкт", + "Unable to delete Subject": "Неможливо видалити суб'єкт", + "Database contains %1 subjects": "База даних містить %1 суб'єктів", + "Edit Subject": "Редагувати суб'єкт", + "person, device, etc": "особа, пристрій тощо", + "role1, role2": "роль 1, роль 2", + "Edit this subject": "Редагувати цей суб'єкт", + "Delete this subject": "Видалити цей суб'єкт", + "Roles": "Ролі", + "Access Token": "Маркер доступу", + "hour ago": "година тому", + "hours ago": "години тому", + "Silence for %1 minutes": "Тиша %1 хвилин", + "Check BG": "Перевірте ЦК", + "BASAL": "Базал", + "Current basal": "Поточний базал", + "Sensitivity": "Чутливість", + "Current Carb Ratio": "Актуальне співвідношення інсуліну та вуглеводів", + "Basal timezone": "Часовий пояс базалу", + "Active profile": "Активований профіль", + "Active temp basal": "Активний тимчасовий базал", + "Active temp basal start": "Початок активного тимчасового базалу", + "Active temp basal duration": "Тривалість активного тимчасового базалу", + "Active temp basal remaining": "Залишок активного тимчасового базалу", + "Basal profile value": "Значення профілю базалу", + "Active combo bolus": "Активний комбінований болюс", + "Active combo bolus start": "Початок активного комбінованого болюсу", + "Active combo bolus duration": "Тривалість активного комбінованого болюсу", + "Active combo bolus remaining": "Залишок активного комбінованого болюсу", + "BG Delta": "Дельта ГК", + "Elapsed Time": "Минуло часу", + "Absolute Delta": "Абсолютна зміна", + "Interpolated": "Інтерпольовано", + "BWP": "Калькулятор болюсу", + "Urgent": "Увага", + "Warning": "Попередження", + "Info": "Інформація", + "Lowest": "Найнижче", + "Snoozing high alarm since there is enough IOB": "Відкладений сигналу високого СК через достатню кількість IOB", + "Check BG, time to bolus?": "Перевірте ГК, дати болюс?", + "Notice": "Примітка", + "required info missing": "необхідна інформація відсутня", + "Insulin on Board": "Активний інсулін (IOB)", + "Current target": "Актуальне цільове значення", + "Expected effect": "Очікуваний ефект", + "Expected outcome": "Очікуваний результат", + "Carb Equivalent": "Еквівалент у вуглеводах", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Вихід інсуліну рівного %1 од, необхідного для досягнення нижнього цільного значення, вуглеводи не прийняті до розрахунку", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Надлишок інсуліну, що дорівнює %1 од, необхідного для досягнення нижнього цільового значення, ПОКРИЙТЕ АКТИВНИЙ ІНСУЛІН ВУГЛЕВОДАМИ", + "%1U reduction needed in active insulin to reach low target, too much basal?": "Для досягнення нижнього цільового значення необхідно знизити величину активного інсуліну на %1 од, чи велика база?", + "basal adjustment out of range, give carbs?": "Коригування базалу поза діапазоном, додати вуглеводів?", + "basal adjustment out of range, give bolus?": "Коригування базалу поза діапазоном, додати болюс?", + "above high": "вище верхньої межі", + "below low": "нижче нижньої межі", + "Projected BG %1 target": "Прогнозована глікемія %1", + "aiming at": "ціль на", + "Bolus %1 units": "Болюс %1 од.", + "or adjust basal": "чи коректувати базал", + "Check BG using glucometer before correcting!": "Перед коригуванням звірте ГК із глюкометром", + "Basal reduction to account %1 units:": "Зниження бази через %1 од. болюса", + "30m temp basal": "30 хв тимчасового базалу", + "1h temp basal": "1 год тимчасового базалу", + "Cannula change overdue!": "Термін заміни катетера помпи минув", + "Time to change cannula": "Час змінити катетер помпи", + "Change cannula soon": "Наближається час заміни катетера помпи", + "Cannula age %1 hours": "Катетер помпи відпрацював %1 год", + "Inserted": "Вставлений", + "CAGE": "CAGE", + "COB": "COB", + "Last Carbs": "Остані вуглеводи", + "IAGE": "IAGE", + "Insulin reservoir change overdue!": "Термін заміни резервуару інсуліну минув!", + "Time to change insulin reservoir": "Настав термін заміни резервуару інсуліну", + "Change insulin reservoir soon": "Настає термін заміни резервуару інсуліну", + "Insulin reservoir age %1 hours": "Картриджу інсуліну %1 годин", + "Changed": "Змінено", + "IOB": "IOB", + "Careportal IOB": "IOB на порталі терапії", + "Last Bolus": "Минулий болюс", + "Basal IOB": "Базал IOB", + "Source": "Джерело", + "Stale data, check rig?": "Застарілі дані, перевірити пристрій?", + "Last received:": "Востаннє отримано:", + "%1m ago": "%1 хв тому", + "%1h ago": "%1 год тому", + "%1d ago": "%1 днів тому", + "RETRO": "RETRO", + "SAGE": "SAGE", + "Sensor change/restart overdue!": "Заміна/рестарт сенсора пропущений!", + "Time to change/restart sensor": "Час заміни/рестарта сенсора", + "Change/restart sensor soon": "Наближається заміна/рестарт сенсора", + "Sensor age %1 days %2 hours": "Термін сенсора %1 дн %2 годин", + "Sensor Insert": "Сенсор встановлено", + "Sensor Start": "Старт сенсора", + "days": "днів", + "Insulin distribution": "Розподіл інсуліну", + "To see this report, press SHOW while in this view": "Щоб побачити звіт, натисніть Показати", + "AR2 Forecast": "AR2 прогноз", + "OpenAPS Forecasts": "Прогноз OpenAPS", + "Temporary Target": "Тимчасова ціль", + "Temporary Target Cancel": "Скасування тимчасової цілі", + "OpenAPS Offline": "OpenAPS не в мережі", + "Profiles": "Профілі", + "Time in fluctuation": "Час флуктуацій", + "Time in rapid fluctuation": "Час швидких флуктуацій", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "Це лише грубі оцінки, які можуть бути неточними і не замінюють фактичного тестування крові. Звідси взята формула:", + "Filter by hours": "Фільтрувати за годинами", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Час флуктуацій і час швидких флуктуацій означає % часу в аналізований період, протягом якого ГК змінювалася відносно швидко або просто швидко. Нижчі значення краще.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Усереднена щоденна зміна це сума абсолютних величин всіх відхилень ЦК в аналізований період, поділена на кількість днів. Менша величина краща.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Усереднена годинна зміна це сума абсолютних величин всіх відхилень ЦК в аналізований період, поділена на кількість годин у цей період. Менша величина краща.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Середнє квадратичне поза діапазоном розраховується шляхом зведення в квадрат дистанції вимірів поза діапазоном для всіх значень глікемії за аналізований період, їх підсумовування, поділу на загальну кількість та вилучення квадратного коріння. Ця методика схожа з розрахунком відсотка значень у діапазоні, але значення за межами діапазону виявляються вагомішими. Чим нижче значення, тим краще.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">можна знайти тут.", + "Mean Total Daily Change": "Середня зміна за день", + "Mean Hourly Change": "Середня зміна за годину", + "FortyFiveDown": "незначне падіння", + "FortyFiveUp": "незначний ріст", + "Flat": "Рівний", + "SingleUp": "зростає", + "SingleDown": "падає", + "DoubleDown": "стрімко зростає", + "DoubleUp": "стрімко падає", + "virtAsstUnknown": "На даний час величина невідома. Зайдіть на сайт Nightscout.", + "virtAsstTitleAR2Forecast": "AR2 прогноз", + "virtAsstTitleCurrentBasal": "Актуальний базал", + "virtAsstTitleCurrentCOB": "Активні COB", + "virtAsstTitleCurrentIOB": "Активний IOB", + "virtAsstTitleLaunch": "Ласкаво просимо до Nightscout", + "virtAsstTitleLoopForecast": "Прогноз Loop", + "virtAsstTitleLastLoop": " Минулий Loop", + "virtAsstTitleOpenAPSForecast": "Прогноз OpenAPS", + "virtAsstTitlePumpReservoir": "Залишилося інсуліну", + "virtAsstTitlePumpBattery": "Батарея помпи", + "virtAsstTitleRawBG": "Актуальний RAW ГК", + "virtAsstTitleUploaderBattery": "Батарея завантажувача", + "virtAsstTitleCurrentBG": "Актуальна ГК", + "virtAsstTitleFullStatus": "Повний статус", + "virtAsstTitleCGMMode": "CGM режим", + "virtAsstTitleCGMStatus": "CGM статус", + "virtAsstTitleCGMSessionAge": "CGM вік сесії", + "virtAsstTitleCGMTxStatus": "CGM статус передавача", + "virtAsstTitleCGMTxAge": "CGM строк передавача", + "virtAsstTitleCGMNoise": "CGM шум", + "virtAsstTitleDelta": "Дельта ГК", + "virtAsstStatus": "%1 та %2 починаючи з %3.", + "virtAsstBasal": "%1 поточний базал %2 од за годину", + "virtAsstBasalTemp": "%1 тимчасовий базал %2 од за годину закінчиться %3", + "virtAsstIob": "і ви маєте %1 інсуліну в організмі.", + "virtAsstIobIntent": "ви маєте %1 інсуліну в організмі", + "virtAsstIobUnits": "%1 од", + "virtAsstLaunch": "Що б ви хотіли перевірити на Nightscout?", + "virtAsstPreamble": "Ваш", + "virtAsstPreamble3person": "%1 має", + "virtAsstNoInsulin": "немає", + "virtAsstUploadBattery": "Батарея завантажувача %1", + "virtAsstReservoir": "У вас залишилася %1 од", + "virtAsstPumpBattery": "Батарея помпи %1 %2", + "virtAsstUploaderBattery": "Батарея завантажувача %1", + "virtAsstLastLoop": "Останній успішний цикл був %1", + "virtAsstLoopNotAvailable": "Плагін циклу не є активним", + "virtAsstLoopForecastAround": "За прогнозом алгоритму , очікувана ГК близько %1 у наступні %2", + "virtAsstLoopForecastBetween": "За прогнозом алгоритму, очікувана ГК між %1 і %2 у наступні %3", + "virtAsstAR2ForecastAround": "За прогнозом AR2 ГК очікується в районі %1 у наступні %2", + "virtAsstAR2ForecastBetween": "За прогнозом AR2 ГК очікується між %1 і %2 у наступні %3", + "virtAsstForecastUnavailable": "Прогноз за таких даних неможливий", + "virtAsstRawBG": "Ваш RAW ГК %1", + "virtAsstOpenAPSForecast": "Прогнозована OpenAPS ГК дорівнює %1", + "virtAsstCob3person": "%1 має %2 вуглеводів", + "virtAsstCob": "У вас %1 вуглеводів", + "virtAsstCGMMode": "Ваш режим моніторингу був %1 як на %2.", + "virtAsstCGMStatus": "Ваш статус моніторингу був %1 як на %2.", + "virtAsstCGMSessAge": "Сесія моніторингу була активною протягом %1 днів та %2 годин.", + "virtAsstCGMSessNotStarted": "Зараз немає активних сеансів моніторингу.", + "virtAsstCGMTxStatus": "Ваш статус передавача моніторингу був %1 як на %2.", + "virtAsstCGMTxAge": "Ваш передавач моніторингу має %1 днів.", + "virtAsstCGMNoise": "Шум у моніторингу був %1 як на %2.", + "virtAsstCGMBattOne": "Ваш акумулятор моніторингу був %1 вольтів як на %2.", + "virtAsstCGMBattTwo": "Ваш рівень заряду батареї становив %1 вольт та %2 вольт як на %3.", + "virtAsstDelta": "Ваша різниця %1 була між %2 і %3.", + "virtAsstDeltaEstimated": "Ви оцінили дельту %1 між %2 і %3.", + "virtAsstUnknownIntentTitle": "Невідомі наміри", + "virtAsstUnknownIntentText": "Перепрошую, я не знаю про що ви запитуєте.", + "Fat [g]": "Жири (г)", + "Protein [g]": "Білок (г)", + "Energy [kJ]": "Енергетична цінність (kJ)", + "Clock Views:": "Вигляд годинника:", + "Clock": "Годинник", + "Color": "Колір", + "Simple": "Простий", + "TDD average": "Середня TDD", + "Bolus average": "Середній болюс", + "Basal average": "Середній базал", + "Base basal average:": "Середнє базового базалу:", + "Carbs average": "Середня кількість вуглеводів", + "Eating Soon": "Очікуваний прийом їжі", + "Last entry {0} minutes ago": "Останній запис {0} хвилин тому", + "change": "зміна", + "Speech": "Мова", + "Target Top": "Верхня межа", + "Target Bottom": "Нижня межа", + "Canceled": "Скасовано", + "Meter BG": "Вимірювач ГК", + "predicted": "прогнозовано", + "future": "майбутнє", + "ago": "тому", + "Last data received": "Останні дані отримані", + "Clock View": "Вигляд годинника", + "Protein": "Білки", + "Fat": "Жири", + "Protein average": "Середні білки", + "Fat average": "Середні жири", + "Total carbs": "Загальні вуглеводи", + "Total protein": "Загальні білки", + "Total fat": "Загальні жири", + "Database Size": "Розмір бази даних", + "Database Size near its limits!": "Розмір бази даних наближається до ліміту!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Розмір бази даних %1 MiB з %2 MiB. Зробіть резервну копію та очистіть базу даних!", + "Database file size": "Розмір файлу бази даних", + "%1 MiB of %2 MiB (%3%)": "%1 МіБ з %2 МіБ (%3%)", + "Data size": "Об'єм даних", + "virtAsstDatabaseSize": "%1 MiB Це %2% доступного місця в базі даних.", + "virtAsstTitleDatabaseSize": "Розмір файлу бази даних", + "Carbs/Food/Time": "Вуглеводи/Їжа/Час", + "You have administration messages": "У вас є повідомлення адміністрації", + "Admin messages in queue": "Адміністративні повідомлення в черзі", + "Queue empty": "Черга порожня", + "There are no admin messages in queue": "У черзі немає повідомлень адміністратора", + "Please sign in using the API_SECRET to see your administration messages": "Будь ласка, увійдіть за допомогою API_SECRET, щоб побачити ваші повідомлення адміністрування", + "Reads enabled in default permissions": "Читання увімкнено в стандартних дозволах", + "Data reads enabled": "Запис даних увімкнено", + "Data writes enabled": "Запис даних увімкнено", + "Data writes not enabled": "Запис даних не увімкнено", + "Color prediction lines": "Кольорові лінії прогнозів", + "Release Notes": "Примітки до релізу", + "Check for Updates": "Перевірити наявність оновлень", + "Open Source": "Відкритий вихідний код", + "Nightscout Info": "Інформація про Nightscout", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "Основною метою Loopalyzer є візуалізувати роботу системи замкнутого циклу Loop. Він може працювати і з іншими установками, як із закритими та відкритими циклами, так і поза циклами. Однак, залежно від того, який завантажувач ви використовуєте, як часто він може отримувати та завантажувати дані, як здатний заповнювати відсутні. Завжди переконайтесь, що графіки виглядають розумно. Найкраще переглядати по одному дню і перегортати через кілька днів.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer включає функцію зсуву за часом. Якщо ви, наприклад, снідаєте о 07:00 один день і о 08:00 в інший, то середня крива глюкози за ці два дні, швидше за все, виглядають згладженою і не показують фактичної відповіді після сніданку. Зсув часу розрахує середній час прийому цих страв і потім переведе всі дані (вуглеводи, інсулін, базал і т.д.) таким чином, що обидва прийоми їжі збігаються з середнім часом початку харчування.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "У даному прикладі всі дані першого дня зрушені на 30 хв вперед, а дані другого - на 30 хвилин тому, так що створюється враження, що ви снідали обидва рази о 7:30. Це дозволяє точніше побачити вашу справжню реакцію ГК на їду.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Зсув часу виділяє середній час початку приймання їжі сірим на тривалість дії інсуліну.", + "Note that time shift is available only when viewing multiple days.": "Зверніть увагу, що зсув часу доступний тільки при перегляді декількох днів.", + "Please select a maximum of two weeks duration and click Show again.": "Будь ласка, виберіть не більше двох тижнів і натисніть Показати знову.", + "Show profiles table": "Показати таблицю профілів", + "Show predictions": "Показати прогнози", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Інтервал приймання їжі більше %1 г вуглеводів між %2 і %3", + "Previous": "Попередні", + "Previous day": "Попередній день", + "Next day": "Наступний день", + "Next": "Наступний", + "Temp basal delta": "Дельта тимчасової базальної швидкості", + "Authorized by token": "Авторизований за токеном", + "Auth role": "Роль авторизації", + "view without token": "перегляд без токена", + "Remove stored token": "Видалити збережений токен", + "Weekly Distribution": "Щотижневий розподіл", + "Failed authentication": "Невдала автентифікація", + "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?": "Пристрій з IP-адресою %1 спробував авторизуватися в Nightscout з неправильними обліковими даними. Перевірте, чи є у вас завантажувач, налаштований з неправильним API_SECRET або токеном.", + "Default (with leading zero and U)": "За замовчуванням (з початковим нулем та од.)", + "Concise (with U, without leading zero)": "Повний (зі словом од., без нуля попереду)", + "Minimal (without leading zero and U)": "Мінімальний (без нуля та слова од.)", + "Small Bolus Display": "Показувати болюси дрібними значками", + "Large Bolus Display": "Відображати болюси великими значками", + "Bolus Display Threshold": "Поріг відображення болюса", + "%1 U and Over": "%1 од. і більше", + "Event repeated %1 times.": "Подія повторюється %1 разів.", + "minutes": "хвилин", + "Last recorded %1 %2 ago.": "Останній запис %1 %2 тому.", + "Security issue": "Проблема безпеки", + "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.": "Виявлено слабкий API_SECRET . Будь ласка, використовуйте поєднання малих і великих літер, цифр і неалфавітно-цифрових символів, таких як !#%&/ щоб зменшити ризик несанкціонованого доступу. Мінімальна довжина API_SECRET становить 12 символів.", + "less than 1": "менш ніж 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "Пароль MongoDB і API_SECRET збігається. Це дійсно погана ідея. Будь ласка, змініть обидва і не використовуйте паролі в системі." +} diff --git a/translations/zh_CN.json b/translations/zh_CN.json new file mode 100644 index 00000000000..5046dc1f426 --- /dev/null +++ b/translations/zh_CN.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "正在监听端口", + "Mo": "一", + "Tu": "二", + "We": "三", + "Th": "四", + "Fr": "五", + "Sa": "六", + "Su": "日", + "Monday": "星期一", + "Tuesday": "星期二", + "Wednesday": "星期三", + "Thursday": "星期四", + "Friday": "星期五", + "Saturday": "星期六", + "Sunday": "星期日", + "Category": "类别", + "Subcategory": "子类别", + "Name": "名称", + "Today": "今天", + "Last 2 days": "过去2天", + "Last 3 days": "过去3天", + "Last week": "上周", + "Last 2 weeks": "过去2周", + "Last month": "上个月", + "Last 3 months": "过去3个月", + "From": "从", + "To": "到", + "Notes": "记录", + "Food": "食物", + "Insulin": "胰岛素", + "Carbs": "碳水化合物", + "Notes contain": "记录包括", + "Target BG range bottom": "目标血糖范围 下限", + "top": "上限", + "Show": "生成", + "Display": "显示", + "Loading": "载入中", + "Loading profile": "载入配置文件", + "Loading status": "载入状态", + "Loading food database": "载入食物数据库", + "not displayed": "未显示", + "Loading CGM data of": "载入CGM(连续血糖监测)数据从", + "Loading treatments data of": "载入操作数据从", + "Processing data of": "处理数据从", + "Portion": "部分", + "Size": "大小", + "(none)": "(无)", + "None": "无", + "": "<无>", + "Result is empty": "结果为空", + "Day to day": "日到日", + "Week to week": "周到周", + "Daily Stats": "每日状态", + "Percentile Chart": "百分位图形", + "Distribution": "分布", + "Hourly stats": "每小时状态", + "netIOB stats": "IOB净值", + "temp basals must be rendered to display this report": "要显示本报表必须先生成“日到日报表”", + "No data available": "无可用数据", + "Low": "低血糖", + "In Range": "范围内", + "Period": "期间", + "High": "高血糖", + "Average": "平均", + "Low Quartile": "下四分位数", + "Upper Quartile": "上四分位数", + "Quartile": "四分位数", + "Date": "日期", + "Normal": "正常", + "Median": "中值", + "Readings": "读数", + "StDev": "标准偏差", + "Daily stats report": "每日状态报表", + "Glucose Percentile report": "血糖百分位报表", + "Glucose distribution": "血糖分布", + "days total": "天总计", + "Total per day": "天总计", + "Overall": "概览", + "Range": "范围", + "% of Readings": "%已读取", + "# of Readings": "#已读取", + "Mean": "平均", + "Standard Deviation": "标准偏差", + "Max": "最大值", + "Min": "最小值", + "A1c estimation*": "糖化血红蛋白估算", + "Weekly Success": "每周统计", + "There is not sufficient data to run this report. Select more days.": "没有足够的数据生成报表,请选择更长时间段。", + "Using stored API secret hash": "使用已存储的API密钥哈希值", + "No API secret hash stored yet. You need to enter API secret.": "没有已存储的API密钥,请输入API密钥。", + "Database loaded": "数据库已载入", + "Error: Database failed to load": "错误:数据库载入失败", + "Error": "出错了!", + "Create new record": "新增记录", + "Save record": "保存记录", + "Portions": "部分", + "Unit": "单位", + "GI": "GI(血糖生成指数)", + "Edit record": "编辑记录", + "Delete record": "删除记录", + "Move to the top": "移至顶端", + "Hidden": "隐藏", + "Hide after use": "使用后隐藏", + "Your API secret must be at least 12 characters long": "API密钥最少需要12个字符", + "Bad API secret": "API密钥错误", + "API secret hash stored": "API密钥已存储", + "Status": "状态", + "Not loaded": "未载入", + "Food Editor": "食物编辑器", + "Your database": "你的数据库", + "Filter": "过滤器", + "Save": "保存", + "Clear": "清除", + "Record": "记录", + "Quick picks": "快速选择", + "Show hidden": "显示隐藏值", + "Your API secret or token": "请输入您的密码", + "Remember this device. (Do not enable this on public computers.)": "记住该设备(请勿在公共电脑上启用该功能)", + "Treatments": "操作", + "Time": "时间", + "Event Type": "事件类型", + "Blood Glucose": "血糖值", + "Entered By": "输入人", + "Delete this treatment?": "删除这个操作?", + "Carbs Given": "碳水化合物量", + "Insulin Given": "胰岛素输注量", + "Event Time": "事件时间", + "Please verify that the data entered is correct": "请验证输入的数据是否正确", + "BG": "血糖", + "Use BG correction in calculation": "使用血糖值修正计算", + "BG from CGM (autoupdated)": "CGM(连续血糖监测)测量的血糖值(自动更新)", + "BG from meter": "血糖仪测量的血糖值", + "Manual BG": "手动输入的血糖值", + "Quickpick": "快速选择", + "or": "或", + "Add from database": "从数据库增加", + "Use carbs correction in calculation": "使用碳水化合物修正计算结果", + "Use COB correction in calculation": "使用COB(活性碳水化合物)修正计算结果", + "Use IOB in calculation": "使用IOB(活性胰岛素)修正计算结果", + "Other correction": "其它修正", + "Rounding": "取整", + "Enter insulin correction in treatment": "在操作中输入胰岛素修正", + "Insulin needed": "需要的胰岛素量", + "Carbs needed": "需要的碳水量", + "Carbs needed if Insulin total is negative value": "如果胰岛素总量为负时所需的碳水化合物量", + "Basal rate": "基础率", + "60 minutes earlier": "60分钟前", + "45 minutes earlier": "45分钟前", + "30 minutes earlier": "30分钟前", + "20 minutes earlier": "20分钟前", + "15 minutes earlier": "15分钟前", + "Time in minutes": "1分钟前", + "15 minutes later": "15分钟后", + "20 minutes later": "20分钟后", + "30 minutes later": "30分钟后", + "45 minutes later": "45分钟后", + "60 minutes later": "60分钟后", + "Additional Notes, Comments": "备注", + "RETRO MODE": "历史模式", + "Now": "现在", + "Other": "其它", + "Submit Form": "提交", + "Profile Editor": "配置文件编辑器", + "Reports": "生成报表", + "Add food from your database": "从数据库增加食物", + "Reload database": "重新载入数据库", + "Add": "增加", + "Unauthorized": "未授权", + "Entering record failed": "输入记录失败", + "Device authenticated": "设备已认证", + "Device not authenticated": "设备未认证", + "Authentication status": "认证状态", + "Authenticate": "认证", + "Remove": "取消", + "Your device is not authenticated yet": "此设备还未进行认证", + "Sensor": "CGM探头", + "Finger": "手指", + "Manual": "手动", + "Scale": "函数", + "Linear": "线性", + "Logarithmic": "对数", + "Logarithmic (Dynamic)": "对数(动态)", + "Insulin-on-Board": "活性胰岛素(IOB)", + "Carbs-on-Board": "活性碳水化合物(COB)", + "Bolus Wizard Preview": "大剂量向导预览(BWP)", + "Value Loaded": "数值已读取", + "Cannula Age": "管路使用时间(CAGE)", + "Basal Profile": "基础率配置文件", + "Silence for 30 minutes": "静音30分钟", + "Silence for 60 minutes": "静音60分钟", + "Silence for 90 minutes": "静音90分钟", + "Silence for 120 minutes": "静音2小时", + "Settings": "设置", + "Units": "计量单位", + "Date format": "时间格式", + "12 hours": "12小时制", + "24 hours": "24小时制", + "Log a Treatment": "记录操作", + "BG Check": "测量血糖", + "Meal Bolus": "正餐大剂量", + "Snack Bolus": "加餐大剂量", + "Correction Bolus": "临时大剂量", + "Carb Correction": "碳水修正", + "Note": "备忘", + "Question": "问题", + "Exercise": "运动", + "Pump Site Change": "更换胰岛素输注部位", + "CGM Sensor Start": "启动CGM(连续血糖监测)探头", + "CGM Sensor Stop": "CGM 传感器停止", + "CGM Sensor Insert": "植入CGM(连续血糖监测)探头", + "Sensor Code": "G6 传感器代码(code)", + "Transmitter ID": "发射器编号", + "Dexcom Sensor Start": "启动Dexcom探头", + "Dexcom Sensor Change": "更换Dexcom探头", + "Insulin Cartridge Change": "更换胰岛素储液器", + "D.A.D. Alert": "D.A.D(低血糖通报犬)警告", + "Glucose Reading": "血糖数值", + "Measurement Method": "测量方法", + "Meter": "血糖仪", + "Amount in grams": "总量(g)", + "Amount in units": "总量(U)", + "View all treatments": "查看所有操作", + "Enable Alarms": "启用报警", + "Pump Battery Change": "更换泵电池", + "Pump Battery Age": "泵电池使用时间", + "Pump Battery Low Alarm": "胰岛素泵电量低", + "Pump Battery change overdue!": "泵电池更换超时!", + "When enabled an alarm may sound.": "启用后可发出声音报警", + "Urgent High Alarm": "血糖过高报警", + "High Alarm": "高血糖报警", + "Low Alarm": "低血糖报警", + "Urgent Low Alarm": "血糖过低报警", + "Stale Data: Warn": "数据过期:提醒", + "Stale Data: Urgent": "数据过期:警告", + "mins": "分", + "Night Mode": "夜间模式", + "When enabled the page will be dimmed from 10pm - 6am.": "启用后将在夜间22点至早晨6点降低页面亮度", + "Enable": "启用", + "Show Raw BG Data": "显示原始血糖数据", + "Never": "不显示", + "Always": "一直显示", + "When there is noise": "当有噪声时显示", + "When enabled small white dots will be displayed for raw BG data": "启用后将使用小白点标注原始血糖数据", + "Custom Title": "自定义标题", + "Theme": "主题", + "Default": "默认", + "Colors": "彩色", + "Colorblind-friendly colors": "色盲患者可辨识的颜色", + "Reset, and use defaults": "使用默认值重置", + "Calibrations": "校准", + "Alarm Test / Smartphone Enable": "报警测试/智能手机启用", + "Bolus Wizard": "大剂量向导", + "in the future": "在未来", + "time ago": "在过去", + "hr ago": "小时前", + "hrs ago": "小时前", + "min ago": "分钟前", + "mins ago": "分钟前", + "day ago": "天前", + "days ago": "天前", + "long ago": "很长时间前", + "Clean": "无", + "Light": "轻度", + "Medium": "中度", + "Heavy": "重度", + "Treatment type": "操作类型", + "Raw BG": "原始血糖", + "Device": "设备", + "Noise": "噪声", + "Calibration": "校准", + "Show Plugins": "校准", + "About": "关于", + "Value in": "数值", + "Carb Time": "数值", + "Language": "语言", + "Add new": "新增", + "g": "克", + "ml": "毫升", + "pcs": "件", + "Drag&drop food here": "拖放食物到这", + "Care Portal": "服务面板", + "Medium/Unknown": "中等/不知道", + "IN THE FUTURE": "在未来", + "Order": "排序", + "oldest on top": "按时间升序排列", + "newest on top": "按时间降序排列", + "All sensor events": "所有探头事件", + "Remove future items from mongo database": "从数据库中清除所有未来条目", + "Find and remove treatments in the future": "查找并清除所有未来的操作", + "This task find and remove treatments in the future.": "此功能查找并清除所有未来的操作。", + "Remove treatments in the future": "清除未来操作", + "Find and remove entries in the future": "查找并清除所有的未来的记录", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "此功能查找并清除所有上传时日期时间错误导致生成在未来时间的CGM数据。", + "Remove entries in the future": "清除未来记录", + "Loading database ...": "载入数据库...", + "Database contains %1 future records": "数据库包含%1条未来记录", + "Remove %1 selected records?": "清除%1条选择的记录?", + "Error loading database": "载入数据库错误", + "Record %1 removed ...": "%1条记录已清除", + "Error removing record %1": "%1条记录清除出错", + "Deleting records ...": "正在删除记录...", + "%1 records deleted": "%1的记录已被删除", + "Clean Mongo status database": "清除状态数据库", + "Delete all documents from devicestatus collection": "从设备状态采集删除所有文档", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "此功能从设备状态采集中删除所有文档。适用于上传设备电量信息不能正常同步时使用。", + "Delete all documents": "删除所有文档", + "Delete all documents from devicestatus collection?": "从设备状态采集删除所有文档?", + "Database contains %1 records": "数据库包含%1条记录", + "All records removed ...": "所有记录已经被清除", + "Delete all documents from devicestatus collection older than 30 days": "删除30天以前的设备数据", + "Number of Days to Keep:": "保留天数:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "此功能将删除所有30天以前的设备状态采集数据。适用于上传设备的电池状态不能正常更新的情况。", + "Delete old documents from devicestatus collection?": "删除旧的设备状态数据?", + "Clean Mongo entries (glucose entries) database": "清空Mongo数据库的葡萄糖条目数据库。", + "Delete all documents from entries collection older than 180 days": "删除所有超过180天的条目数据", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "此功能将删除所有180天以前的设备状态采集数据。适用于上传设备的电池状态不能正常更新的情况。", + "Delete old documents": "删除历史数据", + "Delete old documents from entries collection?": "从血糖数据集合内删除旧数据?", + "%1 is not a valid number": "%1 不是有效的数字", + "%1 is not a valid number - must be more than 2": "%1 is not a valid number - must be more than 2", + "Clean Mongo treatments database": "Clean Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Delete all documents from treatments collection older than 180 days", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "管理工具", + "Nightscout reporting": "Nightscout报表生成器", + "Cancel": "取消", + "Edit treatment": "编辑操作", + "Duration": "持续", + "Duration in minutes": "持续时间(分钟)", + "Temp Basal": "临时基础率", + "Temp Basal Start": "临时基础率开始", + "Temp Basal End": "临时基础率结束", + "Percent": "百分比", + "Basal change in %": "基础率变化百分比", + "Basal value": "基础率值", + "Absolute basal value": "绝对基础率值", + "Announcement": "通告", + "Loading temp basal data": "载入临时基础率数据", + "Save current record before changing to new?": "在修改至新值前保存当前记录?", + "Profile Switch": "切换配置文件", + "Profile": "配置文件", + "General profile settings": "通用配置文件设置", + "Title": "标题", + "Database records": "数据库记录", + "Add new record": "新增记录", + "Remove this record": "删除记录", + "Clone this record to new": "复制记录", + "Record valid from": "有效记录,从", + "Stored profiles": "配置文件已存储", + "Timezone": "时区", + "Duration of Insulin Activity (DIA)": "胰岛素作用时间(DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "体现典型的胰岛素活性持续时间。 通过百分比和胰岛素类型体现。对于大多数胰岛素和患者来说是3至4个小时。也称为胰岛素生命周期。", + "Insulin to carb ratio (I:C)": "碳水化合物系数(ICR)", + "Hours:": "小时:", + "hours": "小时", + "g/hour": "g/小时", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "克碳水每单位胰岛素。每单位胰岛素可以抵消的碳水化合物克值比例。", + "Insulin Sensitivity Factor (ISF)": "胰岛素敏感系数(ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL或mmol/L每单位胰岛素。每单位输入胰岛素导致血糖变化的比例", + "Carbs activity / absorption rate": "碳水化合物活性/吸收率", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "克每单位时间。表示每单位时间COB(活性碳水化合物)的变化,以及在该时间应该生效的碳水化合物的量。碳水化合物活性/吸收曲线比胰岛素活性难理解,但可以使用初始延迟,接着恒定吸收速率(克/小时)来近似模拟。", + "Basal rates [unit/hour]": "基础率 [U/小时]", + "Target BG range [mg/dL,mmol/L]": "目标血糖范围 [mg/dL,mmol/L]", + "Start of record validity": "有效记录开始", + "Icicle": "倒立柱形", + "Render Basal": "使用基础率", + "Profile used": "配置文件已使用", + "Calculation is in target range.": "预计在目标范围内", + "Loading profile records ...": "载入配置文件记录...", + "Values loaded.": "已载入数值", + "Default values used.": "已使用默认值", + "Error. Default values used.": "错误,已使用默认值", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "时间范围内的目标高低血糖值不匹配。已恢复使用默认值。", + "Valid from:": "生效从:", + "Save current record before switching to new?": "切换至新记录前保存当前记录?", + "Add new interval before": "在此前新增区间", + "Delete interval": "删除区间", + "I:C": "ICR", + "ISF": "ISF胰岛素敏感系数", + "Combo Bolus": "双波", + "Difference": "差别", + "New time": "新时间", + "Edit Mode": "编辑模式", + "When enabled icon to start edit mode is visible": "启用后开始编辑模式图标可见", + "Operation": "操作", + "Move": "移动", + "Delete": "删除", + "Move insulin": "移动胰岛素", + "Move carbs": "移动碳水", + "Remove insulin": "去除胰岛素", + "Remove carbs": "去除碳水", + "Change treatment time to %1 ?": "修改操作时间到%1?", + "Change carbs time to %1 ?": "修改碳水时间到%1?", + "Change insulin time to %1 ?": "修改胰岛素时间到%1?", + "Remove treatment ?": "去除操作?", + "Remove insulin from treatment ?": "从操作中去除胰岛素?", + "Remove carbs from treatment ?": "从操作中去除碳水化合物?", + "Rendering": "渲染", + "Loading OpenAPS data of": "载入OpenAPS数据从", + "Loading profile switch data": "载入配置文件交换数据", + "Redirecting you to the Profile Editor to create a new profile.": "配置文件设置错误。\n没有配置文件定义为显示时间。\n返回配置文件编辑器以新建配置文件。", + "Pump": "胰岛素泵", + "Sensor Age": "探头使用时间(SAGE)", + "Insulin Age": "胰岛素使用时间(IAGE)", + "Temporary target": "临时目标", + "Reason": "原因", + "Eating soon": "接近用餐时间", + "Top": "顶部", + "Bottom": "底部", + "Activity": "有效的", + "Targets": "目标", + "Bolus insulin:": "大剂量胰岛素", + "Base basal insulin:": "基础率胰岛素", + "Positive temp basal insulin:": "实际临时基础率胰岛素", + "Negative temp basal insulin:": "其余临时基础率胰岛素", + "Total basal insulin:": "基础率胰岛素合计", + "Total daily insulin:": "每日胰岛素合计", + "Unable to save Role": "Unable to save Role", + "Unable to delete Role": "无法删除角色", + "Database contains %1 roles": "数据库包含%1个角色", + "Edit Role": "编辑角色", + "admin, school, family, etc": "政府、学校、家庭等", + "Permissions": "权限", + "Are you sure you want to delete: ": "你确定要删除:", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "每个角色都具有一个或多个权限。权限设置时使用*作为通配符,层次结构使用:作为分隔符。", + "Add new Role": "添加新角色", + "Roles - Groups of People, Devices, etc": "角色 - 一组人或设备等", + "Edit this role": "编辑角色", + "Admin authorized": "已授权", + "Subjects - People, Devices, etc": "用户 - 人、设备等", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "每个用户具有唯一的访问令牌和一个或多个角色。在访问令牌上单击打开新窗口查看已选择用户,此时该链接可分享。", + "Add new Subject": "添加新用户", + "Unable to save Subject": "无法保存标题", + "Unable to delete Subject": "无法删除用户", + "Database contains %1 subjects": "数据库包含%1个用户", + "Edit Subject": "编辑用户", + "person, device, etc": "人、设备等", + "role1, role2": "角色1、角色2", + "Edit this subject": "编辑此用户", + "Delete this subject": "删除此用户", + "Roles": "角色", + "Access Token": "访问令牌", + "hour ago": "小时前", + "hours ago": "小时前", + "Silence for %1 minutes": "静音%1分钟", + "Check BG": "测量血糖", + "BASAL": "基础率", + "Current basal": "当前基础率", + "Sensitivity": "胰岛素敏感系数", + "Current Carb Ratio": "当前碳水化合物系数", + "Basal timezone": "基础率时区", + "Active profile": "当前配置文件", + "Active temp basal": "当前临时基础率", + "Active temp basal start": "当前临时基础率开始", + "Active temp basal duration": "当前临时基础率期间", + "Active temp basal remaining": "当前临时基础率剩余", + "Basal profile value": "基础率配置文件值", + "Active combo bolus": "当前双波大剂量", + "Active combo bolus start": "当前双波大剂量开始", + "Active combo bolus duration": "当前双波大剂量期间", + "Active combo bolus remaining": "当前双波大剂量剩余", + "BG Delta": "血糖增量", + "Elapsed Time": "所需时间", + "Absolute Delta": "绝对增量", + "Interpolated": "插值", + "BWP": "BWP", + "Urgent": "紧急", + "Warning": "警告", + "Info": "信息", + "Lowest": "血糖极低", + "Snoozing high alarm since there is enough IOB": "有足够的IOB(活性胰岛素),暂停高血糖警报", + "Check BG, time to bolus?": "测量血糖,该输注大剂量了?", + "Notice": "提示", + "required info missing": "所需信息不全", + "Insulin on Board": "活性胰岛素(IOB)", + "Current target": "当前目标", + "Expected effect": "预期效果", + "Expected outcome": "预期结果", + "Carb Equivalent": "碳水当量", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "胰岛素超过至血糖下限目标所需剂量%1单位,不计算碳水化合物", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "胰岛素超过至血糖下限目标所需剂量%1单位,确认IOB(活性胰岛素)被碳水化合物覆盖", + "%1U reduction needed in active insulin to reach low target, too much basal?": "活性胰岛素已可至血糖下限目标,需减少%1单位,基础率过高?", + "basal adjustment out of range, give carbs?": "基础率调整在范围之外,需要碳水化合物?", + "basal adjustment out of range, give bolus?": "基础率调整在范围之外,需要大剂量?", + "above high": "血糖过高", + "below low": "血糖过低", + "Projected BG %1 target": "预计血糖%1目标", + "aiming at": "目标在", + "Bolus %1 units": "大剂量%1单位", + "or adjust basal": "或调整基础率", + "Check BG using glucometer before correcting!": "校正前请使用血糖仪测量血糖!", + "Basal reduction to account %1 units:": "基础率减少到%1单位", + "30m temp basal": "30分钟临时基础率", + "1h temp basal": "1小时临时基础率", + "Cannula change overdue!": "超过更换管路的时间", + "Time to change cannula": "已到更换管路的时间", + "Change cannula soon": "接近更换管路的时间", + "Cannula age %1 hours": "管路已使用%1小时", + "Inserted": "已植入", + "CAGE": "管路", + "COB": "活性碳水COB", + "Last Carbs": "上次碳水", + "IAGE": "胰岛素", + "Insulin reservoir change overdue!": "超过更换胰岛素储液器的时间", + "Time to change insulin reservoir": "已到更换胰岛素储液器的时间", + "Change insulin reservoir soon": "接近更换胰岛素储液器的时间", + "Insulin reservoir age %1 hours": "胰岛素储液器已使用%1小时", + "Changed": "已更换", + "IOB": "活性胰岛素IOB", + "Careportal IOB": "服务面板IOB(活性胰岛素)", + "Last Bolus": "上次大剂量", + "Basal IOB": "基础率IOB(活性胰岛素)", + "Source": "来源", + "Stale data, check rig?": "数据过期,检查一下设备?", + "Last received:": "上次接收:", + "%1m ago": "%1分钟前", + "%1h ago": "%1小时前", + "%1d ago": "%1天前", + "RETRO": "历史数据", + "SAGE": "探头", + "Sensor change/restart overdue!": "超过更换/重启探头的时间", + "Time to change/restart sensor": "已到更换/重启探头的时间", + "Change/restart sensor soon": "接近更换/重启探头的时间", + "Sensor age %1 days %2 hours": "探头使用了%1天%2小时", + "Sensor Insert": "植入探头", + "Sensor Start": "启动探头", + "days": "天", + "Insulin distribution": "胰岛素分布", + "To see this report, press SHOW while in this view": "要查看此报告,请在此视图中按生成", + "AR2 Forecast": "AR2 预测", + "OpenAPS Forecasts": "OpenAPS 预测", + "Temporary Target": "临时目标", + "Temporary Target Cancel": "临时目标取消", + "OpenAPS Offline": "OpenAPS 离线", + "Profiles": "配置文件", + "Time in fluctuation": "波动时间", + "Time in rapid fluctuation": "快速波动时间", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "这只是一个粗略的估计,可能非常不准确,并不能取代测指血", + "Filter by hours": "按小时过滤", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "在检查期间血糖波动时间和快速波动时间占的时间百分比,在此期间血糖相对快速或快速地变化。百分比值越低越好。", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "平均每日总变化是检查期间所有血糖偏移的绝对值之和除以天数。越低越好", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "平均每小时变化是检查期间所有血糖偏移的绝对值之和除以该期间的小时数。 越低越好", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">here.", + "Mean Total Daily Change": "平均每日总变化", + "Mean Hourly Change": "平均每小时变化", + "FortyFiveDown": "缓慢下降", + "FortyFiveUp": "缓慢上升", + "Flat": "平", + "SingleUp": "上升", + "SingleDown": "下降", + "DoubleDown": "快速下降", + "DoubleUp": "快速上升", + "virtAsstUnknown": "当前状态未知,请查看您的 Nightscout 站点了解更多详情。", + "virtAsstTitleAR2Forecast": "AR2 预测", + "virtAsstTitleCurrentBasal": "当前基础率", + "virtAsstTitleCurrentCOB": "当前COB", + "virtAsstTitleCurrentIOB": "当前IOB", + "virtAsstTitleLaunch": "欢迎使用 Nightscout", + "virtAsstTitleLoopForecast": "Loop预测", + "virtAsstTitleLastLoop": "最近一次闭环运行", + "virtAsstTitleOpenAPSForecast": "OpenAPS 预测", + "virtAsstTitlePumpReservoir": "胰岛素余量", + "virtAsstTitlePumpBattery": "泵电池", + "virtAsstTitleRawBG": "当前血糖原始值", + "virtAsstTitleUploaderBattery": "手机电量", + "virtAsstTitleCurrentBG": "当前血糖", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "动态血糖模式", + "virtAsstTitleCGMStatus": "动态血糖设备状态", + "virtAsstTitleCGMSessionAge": "传感器使用时间", + "virtAsstTitleCGMTxStatus": "发射器状态", + "virtAsstTitleCGMTxAge": "发射器使用时间", + "virtAsstTitleCGMNoise": "动态血糖值噪声", + "virtAsstTitleDelta": "血糖变化率", + "virtAsstStatus": "%1 和 %2 到 %3.", + "virtAsstBasal": "%1 当前基础率是 %2 U/小时", + "virtAsstBasalTemp": "%1 临时基础率 %2 U/小时将会在 %3结束", + "virtAsstIob": "并且你有 %1 的活性胰岛素.", + "virtAsstIobIntent": "你有 %1 的活性胰岛素", + "virtAsstIobUnits": "%1 单位", + "virtAsstLaunch": "What would you like to check on Nightscout?", + "virtAsstPreamble": "你的", + "virtAsstPreamble3person": "%1 有一个 ", + "virtAsstNoInsulin": "否", + "virtAsstUploadBattery": "你的手机电池电量是 %1 ", + "virtAsstReservoir": "你剩余%1 U的胰岛素", + "virtAsstPumpBattery": "你的泵电池电量是%1 %2", + "virtAsstUploaderBattery": "你的手机电池电量是 %1", + "virtAsstLastLoop": "最后一次成功闭环的是在%1", + "virtAsstLoopNotAvailable": "Loop插件看起来没有被启用", + "virtAsstLoopForecastAround": "根据loop的预测,在接下来的%2你的血糖将会是around %1", + "virtAsstLoopForecastBetween": "根据loop的预测,在接下来的%3你的血糖将会是between %1 and %2", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "血糖数据不可用,无法预测未来走势", + "virtAsstRawBG": "你的血糖是 %1", + "virtAsstOpenAPSForecast": "OpenAPS 预测最终血糖是 %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "您的发射器已使用了%1天", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "很抱歉,我不知道你想要什么。", + "Fat [g]": "脂肪[g]", + "Protein [g]": "蛋白质[g]", + "Energy [kJ]": "能量 [kJ]", + "Clock Views:": "时钟视图", + "Clock": "时钟", + "Color": "彩色", + "Simple": "简单", + "TDD average": "日胰岛素用量平均值", + "Bolus average": "大剂量均值", + "Basal average": "基础率均值", + "Base basal average:": "基础量平均值:", + "Carbs average": "碳水化合物平均值", + "Eating Soon": "过会吃饭", + "Last entry {0} minutes ago": "最后一个条目 {0} 分钟之前", + "change": "改变", + "Speech": "朗读", + "Target Top": "目标高值", + "Target Bottom": "目标低值", + "Canceled": "被取消了", + "Meter BG": "指血血糖值", + "predicted": "预测", + "future": "将来", + "ago": "之前", + "Last data received": "上次收到数据", + "Clock View": "时钟视图", + "Protein": "蛋白质", + "Fat": "脂肪", + "Protein average": "蛋白质平均摄入量", + "Fat average": "脂肪平均摄入量", + "Total carbs": "碳水化合物总摄入量", + "Total protein": "蛋白质总摄入量", + "Total fat": "脂肪总摄入量", + "Database Size": "数据库容量", + "Database Size near its limits!": "数据库可使用量即将不足", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "数据库文件大小", + "%1 MiB of %2 MiB (%3%)": "%1 MiB of %2 MiB (%3%)", + "Data size": "数量量", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "数据库文件大小", + "Carbs/Food/Time": "碳水化合物/饮食/时间", + "You have administration messages": "系统消息", + "Admin messages in queue": "队列内的管理消息", + "Queue empty": "空队列", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "请使用 API_SECRET 登录以查看您的管理信息", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "已启动数据只读", + "Data writes enabled": "已允许写入数据", + "Data writes not enabled": "禁用写入数据", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "检查更新", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "向前", + "Previous day": "前一天", + "Next day": "下一天", + "Next": "向后", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "删除存储的密码", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "分钟", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "安全问题", + "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.": "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.", + "less than 1": "小于1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "不要为MongoDB 和 API_SECRET设置相同的密码,请更改其中的一个或者全部,且不要在MongoDB中重复使用相同的密码" +} diff --git a/translations/zh_TW.json b/translations/zh_TW.json new file mode 100644 index 00000000000..eefd243fd67 --- /dev/null +++ b/translations/zh_TW.json @@ -0,0 +1,707 @@ +{ + "Listening on port": "Listening on port", + "Mo": "Mo", + "Tu": "Tu", + "We": "We", + "Th": "Th", + "Fr": "Fr", + "Sa": "Sa", + "Su": "Su", + "Monday": "Monday", + "Tuesday": "Tuesday", + "Wednesday": "Wednesday", + "Thursday": "Thursday", + "Friday": "Friday", + "Saturday": "Saturday", + "Sunday": "Sunday", + "Category": "Category", + "Subcategory": "Subcategory", + "Name": "Name", + "Today": "Today", + "Last 2 days": "Last 2 days", + "Last 3 days": "Last 3 days", + "Last week": "Last week", + "Last 2 weeks": "Last 2 weeks", + "Last month": "Last month", + "Last 3 months": "Last 3 months", + "From": "From", + "To": "To", + "Notes": "Notes", + "Food": "Food", + "Insulin": "Insulin", + "Carbs": "Carbs", + "Notes contain": "Notes contain", + "Target BG range bottom": "Target BG range bottom", + "top": "top", + "Show": "Show", + "Display": "Display", + "Loading": "Loading", + "Loading profile": "Loading profile", + "Loading status": "Loading status", + "Loading food database": "Loading food database", + "not displayed": "not displayed", + "Loading CGM data of": "Loading CGM data of", + "Loading treatments data of": "Loading treatments data of", + "Processing data of": "Processing data of", + "Portion": "Portion", + "Size": "Size", + "(none)": "(無)", + "None": "無", + "": "", + "Result is empty": "Result is empty", + "Day to day": "Day to day", + "Week to week": "Week to week", + "Daily Stats": "Daily Stats", + "Percentile Chart": "Percentile Chart", + "Distribution": "Distribution", + "Hourly stats": "Hourly stats", + "netIOB stats": "netIOB stats", + "temp basals must be rendered to display this report": "temp basals must be rendered to display this report", + "No data available": "No data available", + "Low": "Low", + "In Range": "In Range", + "Period": "Period", + "High": "High", + "Average": "Average", + "Low Quartile": "Low Quartile", + "Upper Quartile": "Upper Quartile", + "Quartile": "Quartile", + "Date": "Date", + "Normal": "Normal", + "Median": "Median", + "Readings": "Readings", + "StDev": "StDev", + "Daily stats report": "Daily stats report", + "Glucose Percentile report": "Glucose Percentile report", + "Glucose distribution": "Glucose distribution", + "days total": "days total", + "Total per day": "Total per day", + "Overall": "Overall", + "Range": "Range", + "% of Readings": "% of Readings", + "# of Readings": "# of Readings", + "Mean": "Mean", + "Standard Deviation": "Standard Deviation", + "Max": "Max", + "Min": "Min", + "A1c estimation*": "A1c estimation*", + "Weekly Success": "Weekly Success", + "There is not sufficient data to run this report. Select more days.": "There is not sufficient data to run this report. Select more days.", + "Using stored API secret hash": "使用已存儲的API密鑰哈希值", + "No API secret hash stored yet. You need to enter API secret.": "沒有已存儲的API密鑰,請輸入API密鑰。", + "Database loaded": "Database loaded", + "Error: Database failed to load": "Error: Database failed to load", + "Error": "Error", + "Create new record": "Create new record", + "Save record": "Save record", + "Portions": "Portions", + "Unit": "Unit", + "GI": "GI", + "Edit record": "Edit record", + "Delete record": "Delete record", + "Move to the top": "Move to the top", + "Hidden": "Hidden", + "Hide after use": "Hide after use", + "Your API secret must be at least 12 characters long": "API密鑰最少需要12個字符", + "Bad API secret": "API密鑰錯誤", + "API secret hash stored": "API密鑰已存儲", + "Status": "Status", + "Not loaded": "Not loaded", + "Food Editor": "Food Editor", + "Your database": "Your database", + "Filter": "Filter", + "Save": "保存", + "Clear": "Clear", + "Record": "Record", + "Quick picks": "Quick picks", + "Show hidden": "Show hidden", + "Your API secret or token": "Your API secret or token", + "Remember this device. (Do not enable this on public computers.)": "Remember this device. (Do not enable this on public computers.)", + "Treatments": "Treatments", + "Time": "時間", + "Event Type": "Event Type", + "Blood Glucose": "Blood Glucose", + "Entered By": "Entered By", + "Delete this treatment?": "Delete this treatment?", + "Carbs Given": "Carbs Given", + "Insulin Given": "Insulin Given", + "Event Time": "Event Time", + "Please verify that the data entered is correct": "Please verify that the data entered is correct", + "BG": "BG", + "Use BG correction in calculation": "Use BG correction in calculation", + "BG from CGM (autoupdated)": "BG from CGM (autoupdated)", + "BG from meter": "BG from meter", + "Manual BG": "Manual BG", + "Quickpick": "Quickpick", + "or": "or", + "Add from database": "Add from database", + "Use carbs correction in calculation": "Use carbs correction in calculation", + "Use COB correction in calculation": "Use COB correction in calculation", + "Use IOB in calculation": "Use IOB in calculation", + "Other correction": "Other correction", + "Rounding": "Rounding", + "Enter insulin correction in treatment": "Enter insulin correction in treatment", + "Insulin needed": "Insulin needed", + "Carbs needed": "Carbs needed", + "Carbs needed if Insulin total is negative value": "Carbs needed if Insulin total is negative value", + "Basal rate": "Basal rate", + "60 minutes earlier": "60 minutes earlier", + "45 minutes earlier": "45 minutes earlier", + "30 minutes earlier": "30 minutes earlier", + "20 minutes earlier": "20 minutes earlier", + "15 minutes earlier": "15 minutes earlier", + "Time in minutes": "Time in minutes", + "15 minutes later": "15 minutes later", + "20 minutes later": "20 minutes later", + "30 minutes later": "30 minutes later", + "45 minutes later": "45 minutes later", + "60 minutes later": "60 minutes later", + "Additional Notes, Comments": "Additional Notes, Comments", + "RETRO MODE": "RETRO MODE", + "Now": "Now", + "Other": "Other", + "Submit Form": "Submit Form", + "Profile Editor": "配置文件編輯器", + "Reports": "生成報表", + "Add food from your database": "Add food from your database", + "Reload database": "Reload database", + "Add": "Add", + "Unauthorized": "未授權", + "Entering record failed": "Entering record failed", + "Device authenticated": "設備已認證", + "Device not authenticated": "設備未認證", + "Authentication status": "認證狀態", + "Authenticate": "認證", + "Remove": "取消", + "Your device is not authenticated yet": "這個設備還未進行認證", + "Sensor": "Sensor", + "Finger": "Finger", + "Manual": "Manual", + "Scale": "函數", + "Linear": "線性", + "Logarithmic": "對數", + "Logarithmic (Dynamic)": "對數(動態)", + "Insulin-on-Board": "活性胰島素(IOB)", + "Carbs-on-Board": "活性碳水化合物(COB)", + "Bolus Wizard Preview": "大劑量嚮導預覽(BWP)", + "Value Loaded": "數值已讀取", + "Cannula Age": "管路使用時間(CAGE)", + "Basal Profile": "基礎率配置文件", + "Silence for 30 minutes": "靜音30分鐘", + "Silence for 60 minutes": "靜音60分鐘", + "Silence for 90 minutes": "靜音90分鐘", + "Silence for 120 minutes": "靜音2小時", + "Settings": "設置", + "Units": "计量單位", + "Date format": "時間格式", + "12 hours": "12小時制", + "24 hours": "24小時制", + "Log a Treatment": "Log a Treatment", + "BG Check": "BG Check", + "Meal Bolus": "Meal Bolus", + "Snack Bolus": "Snack Bolus", + "Correction Bolus": "Correction Bolus", + "Carb Correction": "Carb Correction", + "Note": "Note", + "Question": "Question", + "Exercise": "Exercise", + "Pump Site Change": "Pump Site Change", + "CGM Sensor Start": "CGM Sensor Start", + "CGM Sensor Stop": "CGM Sensor Stop", + "CGM Sensor Insert": "CGM Sensor Insert", + "Sensor Code": "Sensor Code", + "Transmitter ID": "Transmitter ID", + "Dexcom Sensor Start": "Dexcom Sensor Start", + "Dexcom Sensor Change": "Dexcom Sensor Change", + "Insulin Cartridge Change": "Insulin Cartridge Change", + "D.A.D. Alert": "D.A.D. Alert", + "Glucose Reading": "Glucose Reading", + "Measurement Method": "Measurement Method", + "Meter": "Meter", + "Amount in grams": "Amount in grams", + "Amount in units": "Amount in units", + "View all treatments": "View all treatments", + "Enable Alarms": "啟用報警", + "Pump Battery Change": "Pump Battery Change", + "Pump Battery Age": "Pump Battery Age", + "Pump Battery Low Alarm": "Pump Battery Low Alarm", + "Pump Battery change overdue!": "Pump Battery change overdue!", + "When enabled an alarm may sound.": "啟用後可發出聲音報警", + "Urgent High Alarm": "血糖過高報警", + "High Alarm": "高血糖報警", + "Low Alarm": "低血糖報警", + "Urgent Low Alarm": "血糖過低報警", + "Stale Data: Warn": "數據過期:提醒", + "Stale Data: Urgent": "數據過期:警告", + "mins": "分", + "Night Mode": "夜間模式", + "When enabled the page will be dimmed from 10pm - 6am.": "啟用後將在夜間22點至早晨6點降低頁面亮度", + "Enable": "啟用", + "Show Raw BG Data": "顯示原始血糖數據", + "Never": "不顯示", + "Always": "一直顯示", + "When there is noise": "當有噪聲時顯示", + "When enabled small white dots will be displayed for raw BG data": "啟用後將使用小白點標註原始血糖數據", + "Custom Title": "自定義標題", + "Theme": "主題", + "Default": "默認", + "Colors": "彩色", + "Colorblind-friendly colors": "色盲患者可辨識的顏色", + "Reset, and use defaults": "使用默認值重置", + "Calibrations": "Calibrations", + "Alarm Test / Smartphone Enable": "報警測試/智能手機啟用", + "Bolus Wizard": "大劑量嚮導", + "in the future": "在未來", + "time ago": "在過去", + "hr ago": "小時前", + "hrs ago": "小時前", + "min ago": "分鐘前", + "mins ago": "分鐘前", + "day ago": "天前", + "days ago": "天前", + "long ago": "很長時間前", + "Clean": "無", + "Light": "輕度", + "Medium": "中度", + "Heavy": "嚴重", + "Treatment type": "Treatment type", + "Raw BG": "Raw BG", + "Device": "Device", + "Noise": "Noise", + "Calibration": "Calibration", + "Show Plugins": "Show Plugins", + "About": "關於", + "Value in": "Value in", + "Carb Time": "Carb Time", + "Language": "語言", + "Add new": "Add new", + "g": "克", + "ml": "克", + "pcs": "件", + "Drag&drop food here": "Drag&drop food here", + "Care Portal": "服務面板", + "Medium/Unknown": "Medium/Unknown", + "IN THE FUTURE": "IN THE FUTURE", + "Order": "Order", + "oldest on top": "oldest on top", + "newest on top": "newest on top", + "All sensor events": "All sensor events", + "Remove future items from mongo database": "從數據庫中清除所有未來條目", + "Find and remove treatments in the future": "查找並清除所有未來的操作", + "This task find and remove treatments in the future.": "此功能查找並清除所有未來的操作。", + "Remove treatments in the future": "清除未來操作", + "Find and remove entries in the future": "查找並清除所有的未來的記錄", + "This task find and remove CGM data in the future created by uploader with wrong date/time.": "此功能查找並清除所有上傳時日期時間錯誤導致生成在未來時間的CGM數據。", + "Remove entries in the future": "清除未來記錄", + "Loading database ...": "載入數據庫...", + "Database contains %1 future records": "數據庫包含%1條未來記錄", + "Remove %1 selected records?": "清除%1條選擇的記錄?", + "Error loading database": "載入數據庫錯誤", + "Record %1 removed ...": "%1條記錄已清除", + "Error removing record %1": "%1條記錄清除出錯", + "Deleting records ...": "正在刪除記錄...", + "%1 records deleted": "%1 records deleted", + "Clean Mongo status database": "Clean Mongo status database", + "Delete all documents from devicestatus collection": "Delete all documents from devicestatus collection", + "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.", + "Delete all documents": "Delete all documents", + "Delete all documents from devicestatus collection?": "Delete all documents from devicestatus collection?", + "Database contains %1 records": "Database contains %1 records", + "All records removed ...": "All records removed ...", + "Delete all documents from devicestatus collection older than 30 days": "Delete all documents from devicestatus collection older than 30 days", + "Number of Days to Keep:": "Number of Days to Keep:", + "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from devicestatus collection that are older than 30 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from devicestatus collection?": "Delete old documents from devicestatus collection?", + "Clean Mongo entries (glucose entries) database": "Clean Mongo entries (glucose entries) database", + "Delete all documents from entries collection older than 180 days": "Delete all documents from entries collection older than 180 days", + "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from entries collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents": "Delete old documents", + "Delete old documents from entries collection?": "Delete old documents from entries collection?", + "%1 is not a valid number": "%1 is not a valid number", + "%1 is not a valid number - must be more than 2": "%1 is not a valid number - must be more than 2", + "Clean Mongo treatments database": "Clean Mongo treatments database", + "Delete all documents from treatments collection older than 180 days": "Delete all documents from treatments collection older than 180 days", + "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.": "This task removes all documents from treatments collection that are older than 180 days. Useful when uploader battery status is not properly updated.", + "Delete old documents from treatments collection?": "Delete old documents from treatments collection?", + "Admin Tools": "管理工具", + "Nightscout reporting": "Nightscout reporting", + "Cancel": "Cancel", + "Edit treatment": "Edit treatment", + "Duration": "Duration", + "Duration in minutes": "Duration in minutes", + "Temp Basal": "Temp Basal", + "Temp Basal Start": "Temp Basal Start", + "Temp Basal End": "Temp Basal End", + "Percent": "Percent", + "Basal change in %": "Basal change in %", + "Basal value": "Basal value", + "Absolute basal value": "Absolute basal value", + "Announcement": "Announcement", + "Loading temp basal data": "Loading temp basal data", + "Save current record before changing to new?": "Save current record before changing to new?", + "Profile Switch": "Profile Switch", + "Profile": "Profile", + "General profile settings": "General profile settings", + "Title": "Title", + "Database records": "Database records", + "Add new record": "Add new record", + "Remove this record": "Remove this record", + "Clone this record to new": "Clone this record to new", + "Record valid from": "Record valid from", + "Stored profiles": "Stored profiles", + "Timezone": "Timezone", + "Duration of Insulin Activity (DIA)": "Duration of Insulin Activity (DIA)", + "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.": "Represents the typical duration over which insulin takes effect. Varies per patient and per insulin type. Typically 3-4 hours for most pumped insulin and most patients. Sometimes also called insulin lifetime.", + "Insulin to carb ratio (I:C)": "Insulin to carb ratio (I:C)", + "Hours:": "Hours:", + "hours": "hours", + "g/hour": "g/hour", + "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.": "g carbs per U insulin. The ratio of how many grams of carbohydrates are offset by each U of insulin.", + "Insulin Sensitivity Factor (ISF)": "Insulin Sensitivity Factor (ISF)", + "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.": "mg/dL or mmol/L per U insulin. The ratio of how much BG changes with each U of corrective insulin.", + "Carbs activity / absorption rate": "Carbs activity / absorption rate", + "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).": "grams per unit time. Represents both the change in COB per unit of time, as well as the amount of carbs that should take effect over that time. Carb absorption / activity curves are less well understood than insulin activity, but can be approximated using an initial delay followed by a constant rate of absorption (g/hr).", + "Basal rates [unit/hour]": "Basal rates [unit/hour]", + "Target BG range [mg/dL,mmol/L]": "Target BG range [mg/dL,mmol/L]", + "Start of record validity": "Start of record validity", + "Icicle": "Icicle", + "Render Basal": "使用基礎率", + "Profile used": "Profile used", + "Calculation is in target range.": "Calculation is in target range.", + "Loading profile records ...": "Loading profile records ...", + "Values loaded.": "Values loaded.", + "Default values used.": "Default values used.", + "Error. Default values used.": "Error. Default values used.", + "Time ranges of target_low and target_high don't match. Values are restored to defaults.": "Time ranges of target_low and target_high don't match. Values are restored to defaults.", + "Valid from:": "Valid from:", + "Save current record before switching to new?": "Save current record before switching to new?", + "Add new interval before": "Add new interval before", + "Delete interval": "Delete interval", + "I:C": "I:C", + "ISF": "ISF", + "Combo Bolus": "Combo Bolus", + "Difference": "Difference", + "New time": "New time", + "Edit Mode": "編輯模式", + "When enabled icon to start edit mode is visible": "啟用後開始編輯模式圖標可見", + "Operation": "Operation", + "Move": "Move", + "Delete": "Delete", + "Move insulin": "Move insulin", + "Move carbs": "Move carbs", + "Remove insulin": "Remove insulin", + "Remove carbs": "Remove carbs", + "Change treatment time to %1 ?": "Change treatment time to %1 ?", + "Change carbs time to %1 ?": "Change carbs time to %1 ?", + "Change insulin time to %1 ?": "Change insulin time to %1 ?", + "Remove treatment ?": "Remove treatment ?", + "Remove insulin from treatment ?": "Remove insulin from treatment ?", + "Remove carbs from treatment ?": "Remove carbs from treatment ?", + "Rendering": "Rendering", + "Loading OpenAPS data of": "Loading OpenAPS data of", + "Loading profile switch data": "Loading profile switch data", + "Redirecting you to the Profile Editor to create a new profile.": "Redirecting you to the Profile Editor to create a new profile.", + "Pump": "Pump", + "Sensor Age": "探頭使用時間(SAGE)", + "Insulin Age": "胰島素使用時間(IAGE)", + "Temporary target": "Temporary target", + "Reason": "Reason", + "Eating soon": "Eating soon", + "Top": "Top", + "Bottom": "Bottom", + "Activity": "Activity", + "Targets": "Targets", + "Bolus insulin:": "Bolus insulin:", + "Base basal insulin:": "Base basal insulin:", + "Positive temp basal insulin:": "Positive temp basal insulin:", + "Negative temp basal insulin:": "Negative temp basal insulin:", + "Total basal insulin:": "Total basal insulin:", + "Total daily insulin:": "Total daily insulin:", + "Unable to save Role": "Unable to save Role", + "Unable to delete Role": "Unable to delete Role", + "Database contains %1 roles": "Database contains %1 roles", + "Edit Role": "Edit Role", + "admin, school, family, etc": "admin, school, family, etc", + "Permissions": "Permissions", + "Are you sure you want to delete: ": "Are you sure you want to delete: ", + "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.": "Each role will have a 1 or more permissions. The * permission is a wildcard, permissions are a hierarchy using : as a separator.", + "Add new Role": "Add new Role", + "Roles - Groups of People, Devices, etc": "Roles - Groups of People, Devices, etc", + "Edit this role": "Edit this role", + "Admin authorized": "已授權", + "Subjects - People, Devices, etc": "Subjects - People, Devices, etc", + "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.": "Each subject will have a unique access token and 1 or more roles. Click on the access token to open a new view with the selected subject, this secret link can then be shared.", + "Add new Subject": "Add new Subject", + "Unable to save Subject": "Unable to save Subject", + "Unable to delete Subject": "Unable to delete Subject", + "Database contains %1 subjects": "Database contains %1 subjects", + "Edit Subject": "Edit Subject", + "person, device, etc": "person, device, etc", + "role1, role2": "role1, role2", + "Edit this subject": "Edit this subject", + "Delete this subject": "Delete this subject", + "Roles": "Roles", + "Access Token": "Access Token", + "hour ago": "hour ago", + "hours ago": "hours ago", + "Silence for %1 minutes": "靜音%1分鐘", + "Check BG": "Check BG", + "BASAL": "基礎率", + "Current basal": "Current basal", + "Sensitivity": "Sensitivity", + "Current Carb Ratio": "Current Carb Ratio", + "Basal timezone": "Basal timezone", + "Active profile": "Active profile", + "Active temp basal": "Active temp basal", + "Active temp basal start": "Active temp basal start", + "Active temp basal duration": "Active temp basal duration", + "Active temp basal remaining": "Active temp basal remaining", + "Basal profile value": "Basal profile value", + "Active combo bolus": "Active combo bolus", + "Active combo bolus start": "Active combo bolus start", + "Active combo bolus duration": "Active combo bolus duration", + "Active combo bolus remaining": "Active combo bolus remaining", + "BG Delta": "血糖增量", + "Elapsed Time": "所需時間", + "Absolute Delta": "絕對增量", + "Interpolated": "插值", + "BWP": "BWP", + "Urgent": "緊急", + "Warning": "警告", + "Info": "資訊", + "Lowest": "血糖極低", + "Snoozing high alarm since there is enough IOB": "Snoozing high alarm since there is enough IOB", + "Check BG, time to bolus?": "Check BG, time to bolus?", + "Notice": "Notice", + "required info missing": "required info missing", + "Insulin on Board": "Insulin on Board", + "Current target": "Current target", + "Expected effect": "Expected effect", + "Expected outcome": "Expected outcome", + "Carb Equivalent": "Carb Equivalent", + "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs": "Excess insulin equivalent %1U more than needed to reach low target, not accounting for carbs", + "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS": "Excess insulin equivalent %1U more than needed to reach low target, MAKE SURE IOB IS COVERED BY CARBS", + "%1U reduction needed in active insulin to reach low target, too much basal?": "%1U reduction needed in active insulin to reach low target, too much basal?", + "basal adjustment out of range, give carbs?": "basal adjustment out of range, give carbs?", + "basal adjustment out of range, give bolus?": "basal adjustment out of range, give bolus?", + "above high": "血糖過高", + "below low": "血糖過低", + "Projected BG %1 target": "Projected BG %1 target", + "aiming at": "aiming at", + "Bolus %1 units": "Bolus %1 units", + "or adjust basal": "or adjust basal", + "Check BG using glucometer before correcting!": "Check BG using glucometer before correcting!", + "Basal reduction to account %1 units:": "Basal reduction to account %1 units:", + "30m temp basal": "30m temp basal", + "1h temp basal": "1h temp basal", + "Cannula change overdue!": "Cannula change overdue!", + "Time to change cannula": "Time to change cannula", + "Change cannula soon": "Change cannula soon", + "Cannula age %1 hours": "Cannula age %1 hours", + "Inserted": "已植入", + "CAGE": "管路", + "COB": "COB", + "Last Carbs": "Last Carbs", + "IAGE": "胰島素", + "Insulin reservoir change overdue!": "Insulin reservoir change overdue!", + "Time to change insulin reservoir": "Time to change insulin reservoir", + "Change insulin reservoir soon": "Change insulin reservoir soon", + "Insulin reservoir age %1 hours": "Insulin reservoir age %1 hours", + "Changed": "Changed", + "IOB": "IOB", + "Careportal IOB": "Careportal IOB", + "Last Bolus": "Last Bolus", + "Basal IOB": "Basal IOB", + "Source": "Source", + "Stale data, check rig?": "Stale data, check rig?", + "Last received:": "Last received:", + "%1m ago": "%1m ago", + "%1h ago": "%1h ago", + "%1d ago": "%1d ago", + "RETRO": "RETRO", + "SAGE": "探頭", + "Sensor change/restart overdue!": "Sensor change/restart overdue!", + "Time to change/restart sensor": "Time to change/restart sensor", + "Change/restart sensor soon": "Change/restart sensor soon", + "Sensor age %1 days %2 hours": "Sensor age %1 days %2 hours", + "Sensor Insert": "Sensor Insert", + "Sensor Start": "Sensor Start", + "days": "days", + "Insulin distribution": "Insulin distribution", + "To see this report, press SHOW while in this view": "To see this report, press SHOW while in this view", + "AR2 Forecast": "AR2 Forecast", + "OpenAPS Forecasts": "OpenAPS Forecasts", + "Temporary Target": "Temporary Target", + "Temporary Target Cancel": "Temporary Target Cancel", + "OpenAPS Offline": "OpenAPS Offline", + "Profiles": "Profiles", + "Time in fluctuation": "Time in fluctuation", + "Time in rapid fluctuation": "Time in rapid fluctuation", + "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:": "This is only a rough estimation that can be very inaccurate and does not replace actual blood testing. The formula used is taken from:", + "Filter by hours": "Filter by hours", + "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.": "Time in fluctuation and Time in rapid fluctuation measure the % of time during the examined period, during which the blood glucose has been changing relatively fast or rapidly. Lower values are better.", + "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.": "Mean Total Daily Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of days. Lower is better.", + "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.": "Mean Hourly Change is a sum of the absolute value of all glucose excursions for the examined period, divided by the number of hours in the period. Lower is better.", + "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.": "Out of Range RMS is calculated by squaring the distance out of range for all glucose readings for the examined period, summing them, dividing by the count and taking the square root. This metric is similar to in-range percentage but weights readings far out of range higher. Lower values are better.", + "GVI (Glycemic Variability Index) and PGS (Patient Glycemic Status) are measures developed by Dexcom, detailed can be found here.": "\">can be found here.", + "Mean Total Daily Change": "Mean Total Daily Change", + "Mean Hourly Change": "Mean Hourly Change", + "FortyFiveDown": "slightly dropping", + "FortyFiveUp": "slightly rising", + "Flat": "holding", + "SingleUp": "rising", + "SingleDown": "dropping", + "DoubleDown": "rapidly dropping", + "DoubleUp": "rapidly rising", + "virtAsstUnknown": "That value is unknown at the moment. Please see your Nightscout site for more details.", + "virtAsstTitleAR2Forecast": "AR2 Forecast", + "virtAsstTitleCurrentBasal": "Current Basal", + "virtAsstTitleCurrentCOB": "Current COB", + "virtAsstTitleCurrentIOB": "Current IOB", + "virtAsstTitleLaunch": "Welcome to Nightscout", + "virtAsstTitleLoopForecast": "Loop Forecast", + "virtAsstTitleLastLoop": "Last Loop", + "virtAsstTitleOpenAPSForecast": "OpenAPS Forecast", + "virtAsstTitlePumpReservoir": "Insulin Remaining", + "virtAsstTitlePumpBattery": "Pump Battery", + "virtAsstTitleRawBG": "Current Raw BG", + "virtAsstTitleUploaderBattery": "Current Raw BG", + "virtAsstTitleCurrentBG": "Current BG", + "virtAsstTitleFullStatus": "Full Status", + "virtAsstTitleCGMMode": "CGM Mode", + "virtAsstTitleCGMStatus": "CGM Status", + "virtAsstTitleCGMSessionAge": "CGM Session Age", + "virtAsstTitleCGMTxStatus": "CGM Transmitter Status", + "virtAsstTitleCGMTxAge": "CGM Transmitter Age", + "virtAsstTitleCGMNoise": "CGM Noise", + "virtAsstTitleDelta": "Blood Glucose Delta", + "virtAsstStatus": "%1 and %2 as of %3.", + "virtAsstBasal": "%1 current basal is %2 units per hour", + "virtAsstBasalTemp": "%1 temp basal of %2 units per hour will end %3", + "virtAsstIob": "and you have %1 insulin on board.", + "virtAsstIobIntent": "You have %1 insulin on board", + "virtAsstIobUnits": "%1 units of", + "virtAsstLaunch": "What would you like to check on Nightscout?", + "virtAsstPreamble": "Your", + "virtAsstPreamble3person": "%1 has a ", + "virtAsstNoInsulin": "no", + "virtAsstUploadBattery": "Your uploader battery is at %1", + "virtAsstReservoir": "You have %1 units remaining", + "virtAsstPumpBattery": "Your pump battery is at %1 %2", + "virtAsstUploaderBattery": "Your uploader battery is at %1", + "virtAsstLastLoop": "The last successful loop was %1", + "virtAsstLoopNotAvailable": "Loop plugin does not seem to be enabled", + "virtAsstLoopForecastAround": "According to the loop forecast you are expected to be around %1 over the next %2", + "virtAsstLoopForecastBetween": "According to the loop forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstAR2ForecastAround": "According to the AR2 forecast you are expected to be around %1 over the next %2", + "virtAsstAR2ForecastBetween": "According to the AR2 forecast you are expected to be between %1 and %2 over the next %3", + "virtAsstForecastUnavailable": "Unable to forecast with the data that is available", + "virtAsstRawBG": "Your raw bg is %1", + "virtAsstOpenAPSForecast": "The OpenAPS Eventual BG is %1", + "virtAsstCob3person": "%1 has %2 carbohydrates on board", + "virtAsstCob": "You have %1 carbohydrates on board", + "virtAsstCGMMode": "Your CGM mode was %1 as of %2.", + "virtAsstCGMStatus": "Your CGM status was %1 as of %2.", + "virtAsstCGMSessAge": "Your CGM session has been active for %1 days and %2 hours.", + "virtAsstCGMSessNotStarted": "There is no active CGM session at the moment.", + "virtAsstCGMTxStatus": "Your CGM transmitter status was %1 as of %2.", + "virtAsstCGMTxAge": "Your CGM transmitter is %1 days old.", + "virtAsstCGMNoise": "Your CGM noise was %1 as of %2.", + "virtAsstCGMBattOne": "Your CGM battery was %1 volts as of %2.", + "virtAsstCGMBattTwo": "Your CGM battery levels were %1 volts and %2 volts as of %3.", + "virtAsstDelta": "Your delta was %1 between %2 and %3.", + "virtAsstDeltaEstimated": "Your estimated delta was %1 between %2 and %3.", + "virtAsstUnknownIntentTitle": "Unknown Intent", + "virtAsstUnknownIntentText": "I'm sorry, I don't know what you're asking for.", + "Fat [g]": "Fat [g]", + "Protein [g]": "Protein [g]", + "Energy [kJ]": "Energy [kJ]", + "Clock Views:": "Clock Views:", + "Clock": "Clock", + "Color": "Color", + "Simple": "Simple", + "TDD average": "TDD average", + "Bolus average": "Bolus average", + "Basal average": "Basal average", + "Base basal average:": "Base basal average:", + "Carbs average": "Carbs average", + "Eating Soon": "Eating Soon", + "Last entry {0} minutes ago": "Last entry {0} minutes ago", + "change": "change", + "Speech": "Speech", + "Target Top": "Target Top", + "Target Bottom": "Target Bottom", + "Canceled": "Canceled", + "Meter BG": "Meter BG", + "predicted": "predicted", + "future": "future", + "ago": "ago", + "Last data received": "Last data received", + "Clock View": "Clock View", + "Protein": "Protein", + "Fat": "Fat", + "Protein average": "Protein average", + "Fat average": "Fat average", + "Total carbs": "Total carbs", + "Total protein": "Total protein", + "Total fat": "Total fat", + "Database Size": "Database Size", + "Database Size near its limits!": "Database Size near its limits!", + "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!": "Database size is %1 MiB out of %2 MiB. Please backup and clean up database!", + "Database file size": "Database file size", + "%1 MiB of %2 MiB (%3%)": "%1 MiB of %2 MiB (%3%)", + "Data size": "Data size", + "virtAsstDatabaseSize": "%1 MiB. That is %2% of available database space.", + "virtAsstTitleDatabaseSize": "Database file size", + "Carbs/Food/Time": "Carbs/Food/Time", + "You have administration messages": "You have administration messages", + "Admin messages in queue": "Admin messages in queue", + "Queue empty": "Queue empty", + "There are no admin messages in queue": "There are no admin messages in queue", + "Please sign in using the API_SECRET to see your administration messages": "Please sign in using the API_SECRET to see your administration messages", + "Reads enabled in default permissions": "Reads enabled in default permissions", + "Data reads enabled": "Data reads enabled", + "Data writes enabled": "Data writes enabled", + "Data writes not enabled": "Data writes not enabled", + "Color prediction lines": "Color prediction lines", + "Release Notes": "Release Notes", + "Check for Updates": "Check for Updates", + "Open Source": "Open Source", + "Nightscout Info": "Nightscout Info", + "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.": "The primary purpose of Loopalyzer is to visualise how the Loop closed loop system performs. It may work with other setups as well, both closed and open loop, and non loop. However depending on which uploader you use, how frequent it is able to capture your data and upload, and how it is able to backfill missing data some graphs may have gaps or even be completely empty. Always ensure the graphs look reasonable. Best is to view one day at a time and scroll through a number of days first to see.", + "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.": "Loopalyzer includes a time shift feature. If you for example have breakfast at 07:00 one day and at 08:00 the day after your average blood glucose curve these two days will most likely look flattened and not show the actual response after a breakfast. Time shift will compute the average time these meals were eaten and then shift all data (carbs, insulin, basal etc.) during both days the corresponding time difference so that both meals align with the average meal start time.", + "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.": "In this example all data from first day is pushed 30 minutes forward in time and all data from second day 30 minutes backward in time so it appears as if you had had breakfast at 07:30 both days. This allows you to see your actual average blood glucose response from a meal.", + "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.": "Time shift highlights the period after the average meal start time in gray, for the duration of the DIA (Duration of Insulin Action). As all data points the entire day are shifted the curves outside the gray area may not be accurate.", + "Note that time shift is available only when viewing multiple days.": "Note that time shift is available only when viewing multiple days.", + "Please select a maximum of two weeks duration and click Show again.": "Please select a maximum of two weeks duration and click Show again.", + "Show profiles table": "Show profiles table", + "Show predictions": "Show predictions", + "Timeshift on meals larger than %1 g carbs consumed between %2 and %3": "Timeshift on meals larger than %1 g carbs consumed between %2 and %3", + "Previous": "Previous", + "Previous day": "Previous day", + "Next day": "Next day", + "Next": "Next", + "Temp basal delta": "Temp basal delta", + "Authorized by token": "Authorized by token", + "Auth role": "Auth role", + "view without token": "view without token", + "Remove stored token": "Remove stored token", + "Weekly Distribution": "Weekly Distribution", + "Failed authentication": "Failed authentication", + "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?": "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?", + "Default (with leading zero and U)": "Default (with leading zero and U)", + "Concise (with U, without leading zero)": "Concise (with U, without leading zero)", + "Minimal (without leading zero and U)": "Minimal (without leading zero and U)", + "Small Bolus Display": "Small Bolus Display", + "Large Bolus Display": "Large Bolus Display", + "Bolus Display Threshold": "Bolus Display Threshold", + "%1 U and Over": "%1 U and Over", + "Event repeated %1 times.": "Event repeated %1 times.", + "minutes": "minutes", + "Last recorded %1 %2 ago.": "Last recorded %1 %2 ago.", + "Security issue": "Security issue", + "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.": "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.", + "less than 1": "less than 1", + "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.": "MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system." +} diff --git a/static/admin/index.html b/views/adminindex.html similarity index 67% rename from static/admin/index.html rename to views/adminindex.html index 7b040f71fec..c47b64b9323 100644 --- a/static/admin/index.html +++ b/views/adminindex.html @@ -25,32 +25,23 @@ - - - - - - -
    -

    Nightscout

    -
    -
    -

    Admin Tools

    -
    + + + + + + <%- include('preloadCSS')%> + + + <%- include('partials/toolbar') %>
    - -
    - Authentication status: - - - + <%- include('partials/authentication-status') %> + + - - - diff --git a/views/clockviews/clock-config.css b/views/clockviews/clock-config.css new file mode 100644 index 00000000000..0d5f2b8b912 --- /dev/null +++ b/views/clockviews/clock-config.css @@ -0,0 +1,39 @@ +#config-form { + position: fixed; + top: 10px; + left: 10px; + width: 250px; + min-width: 220px; + background: white; + color: black; + opacity: 0.8; + padding: 1%; + font-size: 10px; +} +#config-form p { + margin: 15px; + text-align: left; +} +input.elmt { + width: 120px; +} +select { + width: 100%; +} +#facename { + font-size: 7px; +} +#clocklink { + font-size: 18px; +} +#clocklink:link, #clocklink:visited { + background-color: #f44336; + color: white; + padding: 14px 25px; + text-align: center; + text-decoration: none; + display: inline-block; +} +#clocklink:hover, #clocklink:active { + background-color: red; +} \ No newline at end of file diff --git a/views/clockviews/clock-config.html b/views/clockviews/clock-config.html new file mode 100644 index 00000000000..fff92bb65cb --- /dev/null +++ b/views/clockviews/clock-config.html @@ -0,0 +1,65 @@ +
    +

    Clock view configurator

    +
    +

    + + +

    +

    + + +

    +

    SGV age threshold: minutes

    +

    +

    Size:

    +

    Size:

    +

    Size:

    +

    Size:

    +

    Size:

    +

    + Open my clock view! +
    cy10
    + +
    + \ No newline at end of file diff --git a/views/clockviews/clock-shared.css b/views/clockviews/clock-shared.css new file mode 100644 index 00000000000..fc624a52c00 --- /dev/null +++ b/views/clockviews/clock-shared.css @@ -0,0 +1,69 @@ +body { + text-align: center; + margin: 0 0; + padding: 0; + overflow: hidden; + font-family: 'Open Sans', Arial, Helvetica, sans-serif; + color: grey; + background-color: black; +} + +main { + height: 100vh; +} + +#inner { + display: -webkit-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + flex-flow: wrap; + height: 100%; + width: 100%; +} + +#inner div { + margin-right: 2vmin; + margin-left: 2vmin; + line-height: 1em; +} + +#inner div img { + height: 100%; +} + +#inner div.nl { + width: 100%; + margin: 0; + height: 3vmin; +} + +#errorMessage { + font-size: 25em; +} + +.stale { + text-decoration: line-through; +} + +.close { + color: white; + font: 4em 'Open Sans'; + position: absolute; + top: 0; + right: 20px; + text-decoration: none; + z-index: 10; +} + +.close:after { + content: '\00D7'; +} + +.hidden { + opacity: 0; + transition: opacity 0.5s linear; +} \ No newline at end of file diff --git a/views/clockviews/clock.html b/views/clockviews/clock.html new file mode 100644 index 00000000000..2893aec3612 --- /dev/null +++ b/views/clockviews/clock.html @@ -0,0 +1,102 @@ + + + + + + + + Nightscout + + + + + + + + + + + + + + + + +
    + +
    > +
    +
    + + + + <%if (face == 'config') { %> + <%- include('clock-config.html', {}); %> + <% } %> + + + diff --git a/views/error.html b/views/error.html new file mode 100644 index 00000000000..580618c4071 --- /dev/null +++ b/views/error.html @@ -0,0 +1,64 @@ + + + + + + + + Nightscout - Boot error + + + + + + + + + + + + + + + + + +
    ').append('' + name + ''))); + table.append($('
    ').append('' + translate('Units') + ': ' + record.units))); + table.append($('
    ').append('' + translate('DIA') + ': ' + record.dia))); + table.append($('
    ').append('' + translate('Timezone') + ': ' + record.timezone))); + table.append($('
    ').append('' + translate('Carbs activity / absorption rate') + ': ' + record.carbs_hr))); + table.append($('
    ').append('' + translate('Insulin to carb ratio (I:C)') + ': ' + '
    ' + displayRanges(record.carbratio)))); + table.append($('
    ').append('' + translate('Insulin Sensitivity Factor (ISF)') + ': ' + '
    ' + displayRanges(record.sens)))); + table.append($('
    ').append('' + translate('Basal rates [unit/hour]') + ': ' + '
    ' + displayRanges(record.basal)))); + table.append($('
    ').append('' + translate('Target BG range [mg/dL,mmol/L]') + ': ' + '
    ' + displayRanges(record.target_low, record.target_high)))); + + td.append(table); + return td; + } + + function displayRanges (array, array2) { + var text = ''; + + if (array && array2) { + for (let i = 0; i < array.length; i++) { + text += array[i].time + ' : ' + array[i].value + (array2 ? ' - ' + array2[i].value : '') + '
    '; + } + } else { + for (let i = 0; i < array.length; i++) { + text += array[i].time + ' : ' + array[i].value + '
    '; + } + } + return text; + } +}; diff --git a/lib/report_plugins/success.js b/lib/report_plugins/success.js index ed55c3687b5..62417ac0ef5 100644 --- a/lib/report_plugins/success.js +++ b/lib/report_plugins/success.js @@ -4,70 +4,64 @@ var times = require('../times'); var success = { name: 'success' - , label: 'Weekly success' + , label: 'Weekly Distribution' , pluginType: 'report' }; -function init() { +function init () { return success; } module.exports = init; -success.html = function html(client) { +success.html = function html (client) { var translate = client.translate; var ret = - '

    ' + translate('Weekly Success') + '

    ' - + '
    ' - ; + '

    ' + translate('Weekly Distribution') + '

    ' + + '
    '; return ret; }; -success.css = - '#success-placeholder td {'+ - ' border: 1px #ccc solid;'+ - ' margin: 0;'+ - ' padding: 1px;'+ - ' text-align:center;'+ - '}'+ - '#success-placeholder .bad {'+ - ' background-color: #fcc;'+ - '}'+ - - '#success-placeholder .good {'+ - ' background-color: #cfc;'+ - '}'+ - - '#success-placeholder th:first-child {'+ - ' width: 30%;'+ - '}'+ - '#success-placeholder th {'+ - ' width: 10%;'+ - '}'+ - '#success-placeholder table {'+ - ' width: 100%;'+ - '}' - ; - - - -success.report = function report_success(datastorage, sorteddaystoshow, options) { +success.css = + `#success-placeholder td { + border: 1px #ccc solid; + margin: 0; + padding: 1px; + text-align:center; + } + #success-placeholder .bad { + background-color: #fcc; + } + #success-placeholder .good { + background-color: #cfc; + } + #success-placeholder th:first-child { + width: 30%; + } + #success-placeholder th { + width: 10%; + } + #success-placeholder table { + width: 100%; + }`; + +success.report = function report_success (datastorage, sorteddaystoshow, options) { var Nightscout = window.Nightscout; var client = Nightscout.client; var translate = client.translate; var ss = require('simple-statistics'); - var low = options.targetLow, - high = options.targetHigh; + var low = options.targetLow + , high = options.targetHigh; var data = datastorage.allstatsrecords; - + var now = Date.now(); var period = 7 * times.hours(24).msecs; var firstDataPoint = data.reduce(function(min, record) { - return Math.min(min, record.displayTime); - }, Number.MAX_VALUE); + return Math.min(min, record.displayTime); + }, Number.MAX_VALUE); if (firstDataPoint < 1390000000000) { firstDataPoint = 1390000000000; } @@ -79,39 +73,39 @@ success.report = function report_success(datastorage, sorteddaystoshow, options) if (quarters === 0) { // insufficent data - grid.append('

    '+translate('There is not sufficient data to run this report. Select more days.')+'

    '); + grid.append('

    ' + translate('There is not sufficient data to run this report. Select more days.') + '

    '); return; } var dim = function(n) { var a = []; for (var i = 0; i < n; i++) { - a[i]=0; + a[i] = 0; } return a; }; var sum = function(a) { - return a.reduce(function(sum,v) { - return sum+v; + return a.reduce(function(sum, v) { + return sum + v; }, 0); }; var averages = { - percentLow: 0, - percentInRange: 0, - percentHigh: 0, - standardDeviation: 0, - lowerQuartile: 0, - upperQuartile: 0, - average: 0 + percentLow: 0 + , percentInRange: 0 + , percentHigh: 0 + , standardDeviation: 0 + , lowerQuartile: 0 + , upperQuartile: 0 + , average: 0 }; quarters = dim(quarters).map(function(blank, n) { - var starting = new Date(now - (n+1) * period), - ending = new Date(now - n * period); + var starting = new Date(now - (n + 1) * period) + , ending = new Date(now - n * period); return { - starting: starting, - ending: ending, - records: data.filter(function(record) { - return record.displayTime > starting && record.displayTime <= ending; + starting: starting + , ending: ending + , records: data.filter(function(record) { + return record.displayTime > starting && record.displayTime <= ending; }) }; }).filter(function(quarter) { @@ -121,8 +115,8 @@ success.report = function report_success(datastorage, sorteddaystoshow, options) return record.sgv; }); quarter.standardDeviation = ss.standard_deviation(bgValues); - quarter.average = bgValues.length > 0? (sum(bgValues) / bgValues.length): 'N/A'; - quarter.lowerQuartile = ss.quantile(bgValues, 0.25); + quarter.average = bgValues.length > 0 ? (sum(bgValues) / bgValues.length) : 'N/A'; + quarter.lowerQuartile = ss.quantile(bgValues, 0.25); quarter.upperQuartile = ss.quantile(bgValues, 0.75); quarter.numberLow = bgValues.filter(function(bg) { return bg < low; @@ -148,9 +142,9 @@ success.report = function report_success(datastorage, sorteddaystoshow, options) var lowComparison = function(quarter, averages, field, invert) { if (quarter[field] < averages[field] * 0.8) { - return (invert? 'bad': 'good'); + return (invert ? 'bad' : 'good'); } else if (quarter[field] > averages[field] * 1.2) { - return (invert? 'good': 'bad'); + return (invert ? 'good' : 'bad'); } else { return ''; } @@ -172,44 +166,44 @@ success.report = function report_success(datastorage, sorteddaystoshow, options) } }; - table.append('
    '+translate('Period')+''+translate('Low')+''+translate('In Range')+''+translate('High')+''+translate('Standard Deviation')+''+translate('Low Quartile')+''+translate('Average')+''+translate('Upper Quartile')+'
    ' + translate('Period') + '' + translate('Low') + '' + translate('In Range') + '' + translate('High') + '' + translate('Standard Deviation') + '' + translate('Low Quartile') + '' + translate('Average') + '' + translate('Upper Quartile') + '
    ' + v.text + '' + v.text + '' + v + '
    ').css('width','150px').attr('align','left').append(translate('Event Type'))) .append($('').css('width','150px').attr('align','left').append(translate('Blood Glucose'))) .append($('').css('width','50px').attr('align','left').append(translate('Insulin'))) - .append($('').css('width','50px').attr('align','left').append(translate('Carbs'))) + .append($('').css('width','100px').attr('align','left').append(translate('Carbs/Food/Time'))) + .append($('').css('width','50px').attr('align','left').append(translate('Protein'))) + .append($('').css('width','50px').attr('align','left').append(translate('Fat'))) .append($('').css('width','50px').attr('align','left').append(translate('Duration'))) .append($('').css('width','50px').attr('align','left').append(translate('Percent'))) .append($('').css('width','50px').attr('align','left').append(translate('Basal value'))) @@ -298,6 +316,25 @@ treatments.report = function report_treatments(datastorage, sorteddaystoshow, op } for (var t=0; t').addClass('border_bottom') .append($('') .append($('').addClass('deleteTreatment').css('cursor','pointer').attr('title',translate('Delete record')).attr('src',icon_remove).attr('data',JSON.stringify(tr)).attr('day',day)) @@ -305,13 +342,16 @@ treatments.report = function report_treatments(datastorage, sorteddaystoshow, op .append($('').addClass('editTreatment').css('cursor','pointer').attr('title',translate('Edit record')).attr('src',icon_edit).attr('data',JSON.stringify(tr)).attr('day',day)) ) .append($('').append(new Date(tr.created_at).toLocaleTimeString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'))) - .append($('').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) : '')) + .append($('').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) + (tr.reason ? '
    ' + tr.reason : '') + + (tr.insulinNeedsScaleFactor ? '
    ' + tr.insulinNeedsScaleFactor * 100 + '%' : '') + (tr.correctionRange ? ' ' + correctionRangeText : '') : '')) .append($('
    ').attr('align','center').append(tr.glucose ? tr.glucose + ' ('+translate(tr.glucoseType)+')' : '')) - .append($('').attr('align','center').append(tr.insulin ? tr.insulin : '')) - .append($('').attr('align','center').append(tr.carbs ? tr.carbs : '')) - .append($('').attr('align','center').append(tr.duration ? tr.duration : '')) + .append($('').attr('align','center').append(tr.insulin ? tr.insulin.toFixed(2) : '')) + .append($('').attr('align','center').append(carbs)) + .append($('').attr('align','center').append(tr.protein ? tr.protein : '')) + .append($('').attr('align','center').append(tr.fat ? tr.fat : '')) + .append($('').attr('align','center').append(tr.duration ? tr.duration.toFixed(0) : '')) .append($('').attr('align','center').append(tr.percent ? tr.percent : '')) - .append($('').attr('align','center').append(tr.absolute ? tr.absolute : '')) + .append($('').attr('align','center').append('absolute' in tr ? tr.absolute.toFixed(2) : '')) .append($('').attr('align','center').append(tr.profile ? tr.profile : '')) .append($('').append(tr.enteredBy ? tr.enteredBy : '')) .append($('').append(tr.notes ? tr.notes : '')) diff --git a/lib/report_plugins/utils.js b/lib/report_plugins/utils.js index 10daec32304..be6ba940003 100644 --- a/lib/report_plugins/utils.js +++ b/lib/report_plugins/utils.js @@ -1,5 +1,7 @@ 'use strict'; +var consts = require('../constants'); + var moment = window.moment; var utils = { }; @@ -69,7 +71,7 @@ utils.scaledTreatmentBG = function scaledTreatmentBG(treatment,data) { console.info('found mismatched glucose units, converting ' + treatment.units + ' into ' + client.settings.units, treatment); if (treatment.units === 'mmol') { //BG is in mmol and display in mg/dl - treatmentGlucose = Math.round(treatment.glucose * 18); + treatmentGlucose = Math.round(treatment.glucose * consts.MMOL_TO_MGDL); } else { //BG is in mg/dl and display in mmol treatmentGlucose = client.utils.scaleMgdl(treatment.glucose); diff --git a/lib/report_plugins/weektoweek.js b/lib/report_plugins/weektoweek.js new file mode 100644 index 00000000000..2639e4b634a --- /dev/null +++ b/lib/report_plugins/weektoweek.js @@ -0,0 +1,326 @@ +'use strict'; + +var _ = require('lodash'); +var moment = window.moment; +var d3 = (global && global.d3) || require('d3'); + +var dayColors = [ + 'rgb(73, 22, 153)' + , 'rgb(34, 201, 228)' + , 'rgb(0, 153, 123)' + , 'rgb(135, 135, 228)' + , 'rgb(135, 49, 204)' + , 'rgb(36, 36, 228)' + , 'rgb(0, 234, 188)' +]; + +var weektoweek = { + name: 'weektoweek' + , label: 'Week to week' + , pluginType: 'report' +}; + +function init (ctx) { + + weektoweek.html = function html (client) { + var translate = client.translate; + var ret = + '

    ' + translate('Week to week') + '

    ' + + '' + translate('To see this report, press SHOW while in this view') + '
    ' + + ' ' + translate('Size') + + ' ' + + '
    ' + + translate('Scale') + ': ' + + '' + + translate('Linear') + + '' + + translate('Logarithmic') + + '
    ' + + '
    ' + + '
    '; + return ret; + }; + + weektoweek.prepareHtml = function weektoweekPrepareHtml (weekstoshow) { + $('#weektoweekcharts').html(''); + + var translate = ctx.language.translate; + + var colorIdx = 0; + + var legend = ''; + + legend += ''; + legend += ''; + legend += ''; + legend += ''; + legend += ''; + legend += ''; + legend += ''; + legend += '
    ' + translate('Sunday') + '' + translate('Monday') + '' + translate('Tuesday') + '' + translate('Wednesday') + '
    ' + translate('Thursday') + '' + translate('Friday') + '' + translate('Saturday') + '
    '; + + $('#weektoweekcharts').append($(legend)); + + weekstoshow.forEach(function eachWeek (d) { + $('#weektoweekcharts').append($('
    ')); + }); + }; + + weektoweek.report = function report_weektoweek (datastorage, sorteddaystoshow, options) { + var Nightscout = window.Nightscout; + var client = Nightscout.client; + var report_plugins = Nightscout.report_plugins; + + var padding = { top: 15, right: 22, bottom: 30, left: 35 }; + + var weekstoshow = []; + + var startDay = moment(sorteddaystoshow[0] + ' 00:00:00'); + + sorteddaystoshow.forEach(function eachDay (day) { + var weekNum = Math.abs(moment(day + ' 00:00:00').diff(startDay, 'weeks')); + + if (typeof weekstoshow[weekNum] === 'undefined') { + weekstoshow[weekNum] = []; + } + + weekstoshow[weekNum].push(day); + }); + + weekstoshow = weekstoshow.map(function orderWeek (week) { + return _.sortBy(week); + }); + + weektoweek.prepareHtml(weekstoshow); + + weekstoshow.forEach(function eachWeek (week) { + var sgvData = []; + var weekStart = moment(week[0] + ' 00:00:00'); + + week.forEach(function eachDay (day) { + var dayNum = Math.abs(moment(day + ' 00:00:00').diff(weekStart, 'days')); + + datastorage[day].sgv.forEach(function eachSgv (sgv) { + var sgvWeekday = moment(sgv.date).day(); + var sgvColor = dayColors[sgvWeekday]; + + if (sgv.color === 'gray') { + sgvColor = sgv.color; + } + + sgvData.push({ + 'color': sgvColor + , 'date': moment(sgv.date).subtract(dayNum, 'days').toDate() + , 'filtered': sgv.filtered + , 'mills': sgv.mills - dayNum * 24 * 60 * 60000 + , 'noise': sgv.noise + , 'sgv': sgv.sgv + , 'type': sgv.type + , 'unfiltered': sgv.unfiltered + , 'y': sgv.y + }); + }); + }); + + drawChart(week, sgvData, options); + }); + + function timeTicks (n, i) { + var t12 = [ + '12am', '', '2am', '', '4am', '', '6am', '', '8am', '', '10am', '' + , '12pm', '', '2pm', '', '4pm', '', '6pm', '', '8pm', '', '10pm', '', '12am' + ]; + if (Nightscout.client.settings.timeFormat === 24) { + return ('00' + i).slice(-2); + } else { + return t12[i]; + } + } + + function drawChart (week, sgvData, options) { + var tickValues + , charts + , context + , xScale2, yScale2 + , xAxis2, yAxis2 + , dateFn = function(d) { return new Date(d.date); }; + + tickValues = client.ticks(client, { + scaleY: options.weekscale === report_plugins.consts.SCALE_LOG ? 'log' : 'linear' + , targetTop: options.targetHigh + , targetBottom: options.targetLow + }); + + // add defs for combo boluses + var dashWidth = 5; + d3.select('body').append('svg') + .append('defs') + .append('pattern') + .attr('id', 'hash') + .attr('patternUnits', 'userSpaceOnUse') + .attr('width', 6) + .attr('height', 6) + .attr('x', 0) + .attr('y', 0) + .append('g') + .style('fill', 'none') + .style('stroke', '#0099ff') + .style('stroke-width', 2) + .append('path').attr('d', 'M0,0 l' + dashWidth + ',' + dashWidth) + .append('path').attr('d', 'M' + dashWidth + ',0 l-' + dashWidth + ',' + dashWidth); + + // create svg and g to contain the chart contents + charts = d3.select('#weektoweekchart-' + week[0] + '-' + week[week.length - 1]).html( + '' + + report_plugins.utils.localeDate(week[0]) + + '-' + + report_plugins.utils.localeDate(week[week.length - 1]) + + '
    ' + ).append('svg'); + + charts.append('rect') + .attr('width', '100%') + .attr('height', '100%') + .attr('fill', 'WhiteSmoke'); + + context = charts.append('g'); + + // define the parts of the axis that aren't dependent on width or height + xScale2 = d3.scaleTime() + .domain(d3.extent(sgvData, dateFn)); + + if (options.weekscale === report_plugins.consts.SCALE_LOG) { + yScale2 = d3.scaleLog() + .domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]); + } else { + yScale2 = d3.scaleLinear() + .domain([client.utils.scaleMgdl(36), client.utils.scaleMgdl(420)]); + } + + xAxis2 = d3.axisBottom(xScale2) + .tickFormat(timeTicks) + .ticks(24); + + yAxis2 = d3.axisLeft(yScale2) + .tickFormat(d3.format('d')) + .tickValues(tickValues); + + // get current data range + var dataRange = d3.extent(sgvData, dateFn); + + // get the entire container height and width subtracting the padding + var chartWidth = options.weekwidth - padding.left - padding.right; + var chartHeight = options.weekheight - padding.top - padding.bottom; + + //set the width and height of the SVG element + charts.attr('width', options.weekwidth) + .attr('height', options.weekheight); + + // ranges are based on the width and height available so reset + xScale2.range([0, chartWidth]); + yScale2.range([chartHeight, 0]); + + // add target BG rect + context.append('rect') + .attr('x', xScale2(dataRange[0]) + padding.left) + .attr('y', yScale2(options.targetHigh) + padding.top) + .attr('width', xScale2(dataRange[1] - xScale2(dataRange[0]))) + .attr('height', yScale2(options.targetLow) - yScale2(options.targetHigh)) + .style('fill', '#D6FFD6') + .attr('stroke', 'grey'); + + // create the x axis container + context.append('g') + .attr('class', 'x axis'); + + // create the y axis container + context.append('g') + .attr('class', 'y axis'); + + context.select('.y') + .attr('transform', 'translate(' + (padding.left) + ',' + padding.top + ')') + .style('stroke', 'black') + .style('shape-rendering', 'crispEdges') + .style('fill', 'none') + .call(yAxis2); + + // if first run then just display axis with no transition + context.select('.x') + .attr('transform', 'translate(' + padding.left + ',' + (chartHeight + padding.top) + ')') + .style('stroke', 'black') + .style('shape-rendering', 'crispEdges') + .style('fill', 'none') + .call(xAxis2); + + _.each(tickValues, function(n, li) { + context.append('line') + .attr('class', 'high-line') + .attr('x1', xScale2(dataRange[0]) + padding.left) + .attr('y1', yScale2(tickValues[li]) + padding.top) + .attr('x2', xScale2(dataRange[1]) + padding.left) + .attr('y2', yScale2(tickValues[li]) + padding.top) + .style('stroke-dasharray', ('1, 5')) + .attr('stroke', 'grey'); + }); + + // bind up the context chart data to an array of circles + var contextCircles = context.selectAll('circle') + .data(sgvData); + + function prepareContextCircles (sel) { + var badData = []; + sel.attr('cx', function(d) { + return xScale2(d.date) + padding.left; + }) + .attr('cy', function(d) { + if (isNaN(d.sgv)) { + badData.push(d); + return yScale2(client.utils.scaleMgdl(450) + padding.top); + } else { + return yScale2(d.sgv) + padding.top; + } + }) + .attr('fill', function(d) { + if (d.color === 'gray') { + return 'transparent'; + } + return d.color; + }) + .style('opacity', function() { return 0.5 }) + .attr('stroke-width', function(d) { if (d.type === 'mbg') { return 2; } else { return 0; } }) + .attr('stroke', function() { return 'black'; }) + .attr('r', function(d) { + if (d.type === 'mbg') { + return 4; + } else { + return 2 + (options.weekwidth - 800) / 400; + } + }) + .on('mouseout', hideTooltip); + + if (badData.length > 0) { + console.warn('Bad Data: isNaN(sgv)', badData); + } + return sel; + } + + // if new circle then just display + prepareContextCircles(contextCircles.enter().append('circle')); + + contextCircles.exit() + .remove(); + } + + function hideTooltip () { + client.tooltip.style('opacity', 0); + } + }; + return weektoweek; +} + +module.exports = init; diff --git a/lib/sandbox.js b/lib/sandbox.js index 12f1e8ce6a8..6b338e7b1cc 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -4,26 +4,36 @@ var _ = require('lodash'); var units = require('./units')(); var times = require('./times'); -function init ( ) { +function init () { var sbx = {}; function reset () { - sbx.properties = []; + sbx.properties = {}; } function extend () { sbx.unitsLabel = unitsLabel(); sbx.data = sbx.data || {}; //default to prevent adding checks everywhere - sbx.extendedSettings = {empty: true}; + sbx.extendedSettings = { empty: true }; } - function withExtendedSettings(plugin, allExtendedSettings, sbx) { + function withExtendedSettings (plugin, allExtendedSettings, sbx) { var sbx2 = _.extend({}, sbx); sbx2.extendedSettings = allExtendedSettings && allExtendedSettings[plugin.name] || {}; return sbx2; } + /** + * A view into the safe notification functions for plugins + * + * @param ctx + * @returns {{notification}} + */ + function safeNotifications (ctx) { + return _.pick(ctx.notifications, ['requestNotify', 'requestSnooze', 'requestClear']); + } + /** * Initialize the sandbox using server state * @@ -34,20 +44,25 @@ function init ( ) { sbx.serverInit = function serverInit (env, ctx) { reset(); + sbx.runtimeEnvironment = 'server'; + sbx.runtimeState = ctx.runtimeState; sbx.time = Date.now(); sbx.settings = env.settings; - sbx.data = ctx.data.clone(); + sbx.data = ctx.ddata.clone(); + sbx.notifications = safeNotifications(ctx); - //don't expose all of notifications, ctx.notifications will decide what to do after all plugins chime in - sbx.notifications = _.pick(ctx.notifications, ['levels', 'requestNotify', 'requestSnooze', 'requestClear']); + sbx.levels = ctx.levels; + sbx.language = ctx.language; + sbx.translate = ctx.language.translate; - var profile = require('./profilefunctions')(); + var profile = require('./profilefunctions')(null, ctx); //Plugins will expect the right profile based on time - profile.loadData(ctx.data.profiles); + profile.loadData(_.cloneDeep(ctx.ddata.profiles)); + profile.updateTreatments(ctx.ddata.profileTreatments, ctx.ddata.tempbasalTreatments, ctx.ddata.combobolusTreatments); sbx.data.profile = profile; delete sbx.data.profiles; - sbx.properties = []; + sbx.properties = {}; sbx.withExtendedSettings = function getPluginExtendedSettingsOnly (plugin) { return withExtendedSettings(plugin, env.extendedSettings, sbx); @@ -67,22 +82,29 @@ function init ( ) { * @param data - svgs, treatments, profile, etc * @returns {{sbx}} */ - sbx.clientInit = function clientInit (settings, time, pluginBase, data) { + sbx.clientInit = function clientInit (ctx, time, data) { reset(); - sbx.settings = settings; - sbx.showPlugins = settings.showPlugins; + sbx.runtimeEnvironment = 'client'; + sbx.settings = ctx.settings; + sbx.showPlugins = ctx.settings.showPlugins; sbx.time = time; sbx.data = data; - sbx.pluginBase = pluginBase; + sbx.pluginBase = ctx.pluginBase; + sbx.notifications = safeNotifications(ctx); + + sbx.levels = ctx.levels; + sbx.language = ctx.language; + sbx.translate = ctx.language.translate; if (sbx.pluginBase) { - sbx.pluginBase.forecastPoints = []; + sbx.pluginBase.forecastInfos = []; + sbx.pluginBase.forecastPoints = {}; } - sbx.extendedSettings = {empty: true}; + sbx.extendedSettings = { empty: true }; sbx.withExtendedSettings = function getPluginExtendedSettingsOnly (plugin) { - return withExtendedSettings(plugin, settings.extendedSettings, sbx); + return withExtendedSettings(plugin, sbx.settings.extendedSettings, sbx); }; extend(); @@ -97,7 +119,7 @@ function init ( ) { * @param setter */ sbx.offerProperty = function offerProperty (name, setter) { - if (!sbx.properties.hasOwnProperty(name)) { + if (!Object.keys(sbx.properties).includes(name)) { var value = setter(); if (value) { sbx.properties[name] = value; @@ -105,7 +127,7 @@ function init ( ) { } }; - sbx.isCurrent = function isCurrent(entry) { + sbx.isCurrent = function isCurrent (entry) { return entry && sbx.time - entry.mills <= times.mins(15).msecs; }; @@ -115,32 +137,56 @@ function init ( ) { }); }; - sbx.lastSGVEntry = function lastSGVEntry ( ) { + sbx.lastNEntries = function lastNEntries (entries, n) { + var lastN = []; + + _.takeRightWhile(entries, function(entry) { + if (sbx.entryMills(entry) <= sbx.time) { + lastN.push(entry); + } + return lastN.length < n; + }); + + lastN.reverse(); + + return lastN; + }; + + sbx.prevEntry = function prevEntry (entries) { + var last2 = sbx.lastNEntries(entries, 2); + return _.first(last2); + }; + + sbx.prevSGVEntry = function prevSGVEntry () { + return sbx.prevEntry(sbx.data.sgvs); + }; + + sbx.lastSGVEntry = function lastSGVEntry () { return sbx.lastEntry(sbx.data.sgvs); }; - sbx.lastSGVMgdl = function lastSGVMgdl ( ) { + sbx.lastSGVMgdl = function lastSGVMgdl () { var last = sbx.lastSGVEntry(); return last && last.mgdl; }; - sbx.lastSGVMills = function lastSGVMills ( ) { + sbx.lastSGVMills = function lastSGVMills () { return sbx.entryMills(sbx.lastSGVEntry()); }; - sbx.entryMills = function entryMills(entry) { + sbx.entryMills = function entryMills (entry) { return entry && entry.mills; }; - sbx.lastScaledSGV = function lastScaledSVG ( ) { + sbx.lastScaledSGV = function lastScaledSVG () { return sbx.scaleEntry(sbx.lastSGVEntry()); }; - sbx.lastDisplaySVG = function lastDisplaySVG ( ) { + sbx.lastDisplaySVG = function lastDisplaySVG () { return sbx.displayBg(sbx.lastSGVEntry()); }; - sbx.buildBGNowLine = function buildBGNowLine ( ) { + sbx.buildBGNowLine = function buildBGNowLine () { var line = 'BG Now: ' + sbx.lastDisplaySVG(); var delta = sbx.properties.delta && sbx.properties.delta.display; @@ -173,7 +219,7 @@ function init ( ) { return lines; }; - sbx.prepareDefaultLines = function prepareDefaultLines() { + sbx.prepareDefaultLines = function prepareDefaultLines () { var lines = [sbx.buildBGNowLine()]; sbx.appendPropertyLine('rawbg', lines); sbx.appendPropertyLine('ar2', lines); @@ -184,15 +230,14 @@ function init ( ) { return lines; }; - sbx.buildDefaultMessage = function buildDefaultMessage() { + sbx.buildDefaultMessage = function buildDefaultMessage () { return sbx.prepareDefaultLines().join('\n'); }; sbx.displayBg = function displayBg (entry) { - var isDex = entry && (!entry.device || entry.device === 'dexcom'); - if (isDex && Number(entry.mgdl) === 39) { + if (Number(entry.mgdl) === 39) { return 'LOW'; - } else if (isDex && Number(entry.mgdl) === 401) { + } else if (Number(entry.mgdl) === 401) { return 'HIGH'; } else { return sbx.scaleEntry(entry); @@ -229,14 +274,10 @@ function init ( ) { if (sbx.properties.roundingStyle === 'medtronic') { var denominator = 0.1; var digits = 1; - if (insulin > 0.5 && iob < 1) { + if (insulin <= 0.5) { denominator = 0.05; digits = 2; } - if (insulin <= 0.5) { - denominator = 0.025; - digits = 3; - } return (Math.floor(insulin / denominator) * denominator).toFixed(digits); } @@ -244,7 +285,7 @@ function init ( ) { }; - function unitsLabel ( ) { + function unitsLabel () { return sbx.settings.units === 'mmol' ? 'mmol/L' : 'mg/dl'; } @@ -256,4 +297,3 @@ function init ( ) { } module.exports = init; - diff --git a/lib/server/activity.js b/lib/server/activity.js new file mode 100644 index 00000000000..45b77e60587 --- /dev/null +++ b/lib/server/activity.js @@ -0,0 +1,85 @@ +'use strict'; + +var find_options = require('./query'); + + +function storage (env, ctx) { + var ObjectID = require('mongodb').ObjectID; + + function create (obj, fn) { + obj.created_at = (new Date( )).toISOString( ); + api().insert(obj, function (err, doc) { + if (err != null && err.message) { + console.log('Activity data insertion error', err.message); + fn(err.message, null); + return; + } + fn(null, doc.ops); + }); + } + + function save (obj, fn) { + obj._id = new ObjectID(obj._id); + obj.created_at = (new Date( )).toISOString( ); + api().save(obj, function (err, doc) { + fn(err, doc); + }); + } + + function query_for (opts) { + return find_options(opts, storage.queryOpts); + } + + function list(opts, fn) { + // these functions, find, sort, and limit, are used to + // dynamically configure the request, based on the options we've + // been given + + // determine sort options + function sort ( ) { + return opts && opts.sort || {created_at: -1}; + } + + // configure the limit portion of the current query + function limit ( ) { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + // handle all the results + function toArray (err, entries) { + fn(err, entries); + } + + // now just stitch them all together + limit.call(api( ) + .find(query_for(opts)) + .sort(sort( )) + ).toArray(toArray); + } + + function remove (_id, fn) { + var objId = new ObjectID(_id); + return api( ).remove({ '_id': objId }, fn); + } + + function api ( ) { + return ctx.store.collection(env.activity_collection); + } + + api.list = list; + api.create = create; + api.query_for = query_for; + api.save = save; + api.remove = remove; + api.indexedFields = ['created_at']; + return api; +} + +module.exports = storage; + +storage.queryOpts = { + dateField: 'created_at' +}; diff --git a/lib/server/aggregate.js b/lib/server/aggregate.js new file mode 100644 index 00000000000..ff7bd3a1cfe --- /dev/null +++ b/lib/server/aggregate.js @@ -0,0 +1,32 @@ +var find_options = require('./query'); + +function create (conf, api) { + + var template = function ( ) { + return [ + { + $group: { + _id: null + , count: { $sum: 1 } + } + } + ]; + }; + + // var collection = api( ); + function aggregate (opts, done) { + var query = find_options(opts); + + var pipeline = (conf.pipeline || [ ]).concat(opts.pipeline || [ ]); + var groupBy = [ {$match: query } ].concat(pipeline).concat(template( )); + console.log('$match query', query); + console.log('AGGREGATE', groupBy); + api( ).aggregate(groupBy, done); + } + + return aggregate; + +} + +module.exports = create; + diff --git a/lib/server/app.js b/lib/server/app.js new file mode 100644 index 00000000000..8e9b967b7b9 --- /dev/null +++ b/lib/server/app.js @@ -0,0 +1,350 @@ +'use strict'; + +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. + var insecureUseHttp = env.insecureUseHttp; + var secureHstsHeader = env.secureHstsHeader; + if (!insecureUseHttp) { + console.info('Redirecting http traffic to https because INSECURE_USE_HTTP=', insecureUseHttp); + app.use((req, res, next) => { + if (req.header('x-forwarded-proto') === 'https' || req.secure) { + next(); + } else { + res.redirect(307, `https://${req.header('host')}${req.url}`); + } + }); + if (secureHstsHeader) { // Add HSTS (HTTP Strict Transport Security) header + + const enableCSP = env.secureCsp ? true : false; + + let cspPolicy = false; + + if (enableCSP) { + var secureCspReportOnly = env.secureCspReportOnly; + if (secureCspReportOnly) { + console.info('Enabled SECURE_CSP (Content Security Policy header). Not enforcing. Report only.'); + } else { + console.info('Enabled SECURE_CSP (Content Security Policy header). Enforcing.'); + } + + let frameAncestors = ["'self'"]; + + for (let i = 0; i <= 8; i++) { + let u = env.settings['frameUrl' + i]; + if (u) { + frameAncestors.push(u); + } + } + + cspPolicy = { //TODO make NS work without 'unsafe-inline' + directives: { + defaultSrc: ["'self'"] + , styleSrc: ["'self'", 'https://fonts.googleapis.com/', 'https://fonts.gstatic.com/', "'unsafe-inline'"] + , scriptSrc: ["'self'", "'unsafe-inline'"] + , fontSrc: ["'self'", 'https://fonts.googleapis.com/', 'https://fonts.gstatic.com/', 'data:'] + , imgSrc: ["'self'", 'data:'] + , objectSrc: ["'none'"] // Restricts , , and elements + , reportUri: '/report-violation' + , baseUri: ["'none'"] // Restricts use of the tag + , formAction: ["'self'"] // Restricts where
    contents may be submitted + , connectSrc: ["'self'", "ws:", "wss:", 'https://fonts.googleapis.com/', 'https://fonts.gstatic.com/'] + , frameSrc: ["'self'"] + , frameAncestors: frameAncestors + } + , reportOnly: secureCspReportOnly + }; + } + + + console.info('Enabled SECURE_HSTS_HEADER (HTTP Strict Transport Security)'); + const helmet = require('helmet'); + var includeSubDomainsValue = env.secureHstsHeaderIncludeSubdomains; + var preloadValue = env.secureHstsHeaderPreload; + app.use(helmet({ + hsts: { + maxAge: 31536000 + , includeSubDomains: includeSubDomainsValue + , preload: preloadValue + } + , frameguard: false + , contentSecurityPolicy: cspPolicy + })); + + if (enableCSP) { + + app.use(helmet.referrerPolicy({ policy: 'no-referrer' })); + app.use(bodyParser.json({ type: ['json', 'application/csp-report'] })); + app.post('/report-violation', (req, res) => { + if (req.body) { + console.log('CSP Violation: ', req.body); + } else { + console.log('CSP Violation: No data received!'); + } + res.status(204).end(); + }) + } + } + } else { + console.info('Security settings: INSECURE_USE_HTTP=', insecureUseHttp, ', SECURE_HSTS_HEADER=', secureHstsHeader); + } + + app.set('view engine', 'ejs'); + app.engine('html', require('ejs').renderFile); + app.set("views", resolvePath('/views')); + + 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(swcontent, { locals: app.locals} )); + }); + + // Allow static resources to be cached for week + var maxAge = 7 * 24 * 60 * 60 * 1000; + + if (process.env.NODE_ENV === 'development') { + maxAge = 1; + console.log('Development environment detected, setting static file cache age to 1 second'); + } + + var staticFiles = express.static(resolvePath(env.static_files), { + maxAge + }); + + // serve the static content + app.use(staticFiles); + + app.use('/translations', express.static(resolvePath('/translations'), { + maxAge + })); + + if (ctx.bootErrors && ctx.bootErrors.length > 0) { + const bootErrorView = require('./booterror')(env, ctx); + bootErrorView.setLocals(app.locals); + app.get('*', bootErrorView); + return app; + } + + if (env.settings.isEnabled('cors')) { + var allowOrigin = _get(env, 'extendedSettings.cors.allowOrigin') || '*'; + console.info('Enabled CORS, allow-origin:', allowOrigin); + app.use(function allowCrossDomain (req, res, next) { + res.header('Access-Control-Allow-Origin', allowOrigin); + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With'); + + // intercept OPTIONS method + if ('OPTIONS' === req.method) { + res.send(200); + } else { + next(); + } + }); + } + + /////////////////////////////////////////////////// + // api and json object variables + /////////////////////////////////////////////////// + const apiRoot = require('../api/root')(env, ctx); + var api = require('../api/')(env, ctx); + var api2 = require('../api2/')(env,ctx, api); + var api3 = require('../api3/')(env, ctx); + + app.use(compression({ + filter: function shouldCompress (req, res) { + //TODO: return false here if we find a condition where we don't want to compress + // fallback to standard filter function + return compression.filter(req, res); + } + })); + + var appPages = { + "/": { + file: "index.html" + , type: "index" + } + , "/admin": { + file: "adminindex.html" + , title: 'Admin Tools' + , type: 'admin' + } + , "/food": { + file: "foodindex.html" + , title: 'Food Editor' + , type: 'food' + } + , "/profile": { + file: "profileindex.html" + , title: 'Profile Editor' + , type: 'profile' + } + , "/report": { + file: "reportindex.html" + , title: 'Nightscout reporting' + , type: 'report' + } + , "/split": { + file: "frame.html" + , title: '8-user view' + , type: 'index' + } + }; + + Object.keys(appPages).forEach(function(page) { + app.get(page, (req, res) => { + res.render(appPages[page].file, { + locals: app.locals, + title: appPages[page].title ? appPages[page].title : '', + type: appPages[page].type ? appPages[page].type : '', + settings: env.settings + }); + }); + }); + + const clockviews = require('./clocks.js')(env, ctx); + clockviews.setLocals(app.locals); + + app.use("/clock", clockviews); + + app.use('/api', apiRoot); + app.use('/api/v1', api); + app.use('/api/v2', api2); + app.use('/api/v3', api3); + + // 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.setHeader("Content-Type", 'application/json'); + res.send(swaggerjson); + }); + + // expose swagger.yaml + app.get('/swagger.yaml', function(req, res) { + res.setHeader("Content-Type", 'text/vnd.yaml'); + res.send(swaggeryaml); + }); + + // API docs + + const swaggerUi = require('swagger-ui-express'); + const swaggerUseSchema = schema => (...args) => swaggerUi.setup(schema)(...args); + const swaggerDocument = require('./swagger.json'); + const swaggerDocumentApiV3 = require('../api3/swagger.json'); + + app.use('/api-docs', swaggerUi.serve, swaggerUseSchema(swaggerDocument)); + app.use('/api3-docs', swaggerUi.serve, swaggerUseSchema(swaggerDocumentApiV3)); + + app.use('/swagger-ui-dist', (req, res) => { + res.redirect(307, '/api-docs'); + }); + + // if this is dev environment, package scripts on the fly + // 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') { + + console.log('Development mode'); + + app.locals.mode = 'development'; + app.locals.bundle = '/devbundle'; + + const webpack = require('webpack'); + const webpack_conf = require('../../webpack/webpack.config'); + const middleware = require('webpack-dev-middleware'); + const compiler = webpack(webpack_conf); + + app.use( + middleware(compiler, { + // webpack-dev-middleware options + publicPath: webpack_conf.output.publicPath + }) + ); + + app.use(require("webpack-hot-middleware")(compiler, { + heartbeat: 1000 + })); + } + + // Production bundling + const tmpFiles = express.static(resolvePath('/node_modules/.cache/_ns_cache/public'), { + maxAge: maxAge + }); + + // serve the static content + app.use('/bundle', tmpFiles); + + if (process.env.NODE_ENV !== 'development') { + + console.log('Production environment detected, enabling Minify'); + + var minify = require('express-minify'); + var myCssmin = require('cssmin'); + + app.use(minify({ + js_match: /\.js/ + , css_match: /\.css/ + , sass_match: /scss/ + , less_match: /less/ + , stylus_match: /stylus/ + , coffee_match: /coffeescript/ + , json_match: /json/ + , cssmin: myCssmin + , cache: resolvePath('/node_modules/.cache/_ns_cache/public') + , onerror: undefined + , })); + + } + + // Handle errors with express's errorhandler, to display more readable error messages. + var errorhandler = require('errorhandler'); + //if (process.env.NODE_ENV === 'development') { + app.use(errorhandler()); + //} + return app; +} +module.exports = create; diff --git a/lib/server/booterror.js b/lib/server/booterror.js new file mode 100644 index 00000000000..92de6cbc81b --- /dev/null +++ b/lib/server/booterror.js @@ -0,0 +1,46 @@ +'use strict'; + +const express = require('express'); +const path = require('path'); +var _ = require('lodash'); + +function bootError(env, ctx) { + + const app = new express(); + let locals = {}; + + app.set('view engine', 'ejs'); + app.engine('html', require('ejs').renderFile); + app.set("views", path.join(__dirname, "../../views/")); + + app.get('*', (req, res, next) => { + + if (req.url.includes('images')) return next(); + + var errors = _.map(ctx.bootErrors, function (obj) { + + let message; + + if (typeof obj.err === 'string' || obj.err instanceof String) { + message = obj.err; + } else { + message = JSON.stringify(_.pick(obj.err, Object.getOwnPropertyNames(obj.err))); + } + return '
    ' + obj.desc + '
    ' + message.replace(/\\n/g, '
    ') + '
    '; + }).join(' '); + + res.render('error.html', { + errors, + locals + }); + + }); + + app.setLocals = function (_locals) { + locals = _locals; + } + + return app; +} + +module.exports = bootError; \ No newline at end of file diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js new file mode 100644 index 00000000000..b377abcce98 --- /dev/null +++ b/lib/server/bootevent.js @@ -0,0 +1,392 @@ +'use strict'; + +const _ = require('lodash'); +const UPDATE_THROTTLE = 5000; + +function boot (env, language) { + + function startBoot(ctx, next) { + + console.log('Executing startBoot'); + + ctx.bootErrors = [ ]; + ctx.moment = require('moment-timezone'); + ctx.runtimeState = 'booting'; + ctx.settings = env.settings; + 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 LTS releases are recommended and supported. + // Current Node releases MAY work, but are not recommended. Will be tested in CI + // Older Node versions or Node versions with known security issues will not work. + /////////////////////////////////////////////////// + function checkNodeVersion (ctx, next) { + + console.log('Executing checkNodeVersion'); + + var semver = require('semver'); + var nodeVersion = process.version; + + const isLTS = process.release.lts ? true : false; + + if (isLTS && (semver.satisfies(nodeVersion, '^20.0.0') || semver.satisfies(nodeVersion, '^18.0.0') || semver.satisfies(nodeVersion, '^16.0.0') || semver.satisfies(nodeVersion, '^14.0.0'))) { + //Latest Node 14 LTS and Node 16 LTS are recommended and supported. + //Require at least Node 14 without known security issues + console.debug('Node LTS version ' + nodeVersion + ' is supported'); + next(); + return; + } + + console.log( 'ERROR: Node version ' + nodeVersion + ' is not supported. Please use a secure LTS version or upgrade your Node'); + process.exit(1); + + } + + function checkEnv (ctx, next) { + + console.log('Executing checkEnv'); + + ctx.language = language; + if (env.err.length > 0) { + ctx.bootErrors = ctx.bootErrors || [ ]; + ctx.bootErrors.push({'desc': 'ENV Error', err: env.err}); + } + next(); + } + + function hasBootErrors(ctx) { + return ctx.bootErrors && ctx.bootErrors.length > 0; + } + + function augmentSettings (ctx, next) { + + console.log('Executing augmentSettings'); + + var configURL = env.IMPORT_CONFIG || null; + var url = require('url'); + var href = null; + + if (configURL) { + try { + href = url.parse(configURL).href; + } catch (e) { + console.error('Parsing config URL from IMPORT_CONFIG failed'); + } + } + + if(configURL && href) { + var axios_default = { headers: { 'Accept': 'application/json' } }; + var axios = require('axios').create(axios_default); + console.log('Getting settings from', href); + return axios.get(href).then(function (resp) { + var body = resp.data; + var settings = body.settings || body; + console.log('extending settings with', settings); + _.merge(env.settings, settings); + if (body.extendedSettings) { + console.log('extending extendedSettings with', body.extendedSettings); + _.merge(env.extendedSettings, body.extendedSettings); + } + next( ); + }).catch(function (err) { + var synopsis = ['Attempt to fetch config', href, 'failed.']; + console.log('Attempt to fetch config', href, 'failed.', err.response); + ctx.bootErrors.push({desc: synopsis.join(' '), err}); + next( ); + + }); + } else { + next( ); + } + } + + function checkSettings (ctx, next) { + + console.log('Executing checkSettings'); + + ctx.bootErrors = ctx.bootErrors || []; + + console.log('Checking settings'); + + if (!env.storageURI) { + ctx.bootErrors.push({'desc': 'Mandatory setting missing', + err: 'MONGODB_URI setting is missing, cannot connect to database'}); + } + + 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 Nightscout documentation." + ,persistent: true + }; + ctx.adminnotifies.addNotify(message); + } + + next(); + } + + function setupStorage (ctx, next) { + + console.log('Executing setupStorage'); + + if (hasBootErrors(ctx)) { + return next(); + } + + try { + if (_.startsWith(env.storageURI, 'openaps://')) { + require('../storage/openaps-storage')(env, function ready (err, store) { + if (err) { + throw err; + } + ctx.store = store; + console.log('OpenAPS Storage system ready'); + next(); + }); + } else { + //TODO assume mongo for now, when there are more storage options add a lookup + require('../storage/mongo-storage')(env, function ready(err, store) { + // FIXME, error is always null, if there is an error, the index.js will throw an exception + if (err) { + console.info('ERROR CONNECTING TO MONGO', err); + ctx.bootErrors = ctx.bootErrors || [ ]; + ctx.bootErrors.push({'desc': 'Unable to connect to Mongo', err: err.message}); + } + console.log('Mongo Storage system ready'); + ctx.store = store; + next(); + }); + } + } catch (err) { + console.info('ERROR CONNECTING TO MONGO', err); + ctx.bootErrors = ctx.bootErrors || [ ]; + ctx.bootErrors.push({'desc': 'Unable to connect to Mongo', err: err.message}); + next(); + } + } + + function setupAuthorization (ctx, next) { + + console.log('Executing setupAuthorization'); + + if (hasBootErrors(ctx)) { + return next(); + } + + ctx.authorization = require('../authorization')(env, ctx); + ctx.authorization.storage.ensureIndexes(); + ctx.authorization.storage.reload(function loaded (err) { + if (err) { + ctx.bootErrors = ctx.bootErrors || [ ]; + ctx.bootErrors.push({'desc': 'Unable to setup authorization', err: err}); + } + next(); + }); + } + + function setupInternals (ctx, next) { + + console.log('Executing setupInternals'); + + if (hasBootErrors(ctx)) { + return next(); + } + + ctx.levels = require('../levels'); + ctx.levels.translate = ctx.language.translate; + + /////////////////////////////////////////////////// + // api and json object variables + /////////////////////////////////////////////////// + ctx.plugins = require('../plugins')({ + settings: env.settings + , language: ctx.language + , levels: ctx.levels + , moment: ctx.moment + }).registerServerDefaults(); + + ctx.wares = require('../middleware/')(env); + + ctx.pushover = require('../plugins/pushover')(env, ctx); + ctx.maker = require('../plugins/maker')(env); + ctx.pushnotify = require('./pushnotify')(env, ctx); + ctx.loop = require('./loop')(env, ctx); + + ctx.activity = require('./activity')(env, ctx); + ctx.entries = require('./entries')(env, ctx); + ctx.treatments = require('./treatments')(env, ctx); + ctx.devicestatus = require('./devicestatus')(env.devicestatus_collection, ctx); + ctx.profile = require('./profile')(env.profile_collection, ctx); + ctx.food = require('./food')(env, ctx); + ctx.pebble = require('./pebble')(env, ctx); + ctx.properties = require('../api2/properties')(env, 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); + } + + if (env.settings.isEnabled('alexa')) { + ctx.alexa = require('../plugins/alexa')(env, ctx); + } + + if (env.settings.isEnabled('googlehome')) { + ctx.googleHome = require('../plugins/googlehome')(env, ctx); + } + + next( ); + } + + function ensureIndexes (ctx, next) { + + console.log('Executing ensureIndexes'); + + if (hasBootErrors(ctx)) { + return next(); + } + + console.info('Ensuring indexes'); + ctx.store.ensureIndexes(ctx.entries( ), ctx.entries.indexedFields); + ctx.store.ensureIndexes(ctx.treatments( ), ctx.treatments.indexedFields); + ctx.store.ensureIndexes(ctx.devicestatus( ), ctx.devicestatus.indexedFields); + ctx.store.ensureIndexes(ctx.profile( ), ctx.profile.indexedFields); + ctx.store.ensureIndexes(ctx.food( ), ctx.food.indexedFields); + ctx.store.ensureIndexes(ctx.activity( ), ctx.activity.indexedFields); + + next( ); + } + + function setupListeners (ctx, next) { + + console.log('Executing setupListeners'); + + if (hasBootErrors(ctx)) { + return next(); + } + + var updateData = _.debounce(function debouncedUpdateData ( ) { + ctx.dataloader.update(ctx.ddata, function dataUpdated () { + ctx.bus.emit('data-loaded'); + }); + }, UPDATE_THROTTLE); + + ctx.bus.on('tick', function timedReloadData (tick) { + console.info('tick', tick.now); + updateData(); + }); + + ctx.bus.on('data-received', function forceReloadData ( ) { + console.info('got data-received event, requesting reload'); + updateData(); + }); + + ctx.bus.on('data-loaded', function updatePlugins ( ) { + console.info('data loaded: reloading sandbox data and updating plugins'); + var sbx = require('../sandbox')().serverInit(env, ctx); + ctx.plugins.setProperties(sbx); + ctx.notifications.initRequests(); + ctx.plugins.checkNotifications(sbx); + ctx.notifications.process(sbx); + ctx.sbx = sbx; + ctx.bus.emit('data-processed', sbx); + }); + + ctx.bus.on('data-processed', function processed ( ) { + ctx.runtimeState = 'loaded'; + }); + + ctx.bus.on('notification', ctx.pushnotify.emitNotification); + + next( ); + } + + function setupConnect (ctx, next) { + console.log('Executing setupConnect'); + ctx.nightscoutConnect = require('nightscout-connect')(env, ctx) + // ctx.nightscoutConnect. + return next( ); + } + + function setupBridge (ctx, next) { + + console.log('Executing setupBridge'); + + if (hasBootErrors(ctx)) { + return next(); + } + + ctx.bridge = require('../plugins/bridge')(env, ctx.bus); + if (ctx.bridge) { + ctx.bridge.startEngine(ctx.entries); + console.log("DEPRECATION WARNING", "PLEASE CONSIDER nightscout-connect instead."); + } + next( ); + } + + function setupMMConnect (ctx, next) { + + console.log('Executing setupMMConnect'); + + if (hasBootErrors(ctx)) { + return next(); + } + + ctx.mmconnect = require('../plugins/mmconnect').init(env, ctx.entries, ctx.devicestatus, ctx.bus); + if (ctx.mmconnect) { + ctx.mmconnect.run(); + console.log("DEPRECATION WARNING", "PLEASE CONSIDER nightscout-connect instead."); + } + next( ); + } + + function finishBoot (ctx, next) { + + console.log('Executing finishBoot'); + + if (hasBootErrors(ctx)) { + return next(); + } + ctx.bus.emit('finishBoot'); + + ctx.runtimeState = 'booted'; + ctx.bus.uptime( ); + + next( ); + } + + return require('bootevent')( ) + .acquire(startBoot) + .acquire(checkNodeVersion) + .acquire(checkEnv) + .acquire(augmentSettings) + .acquire(checkSettings) + .acquire(setupStorage) + .acquire(setupAuthorization) + .acquire(setupInternals) + .acquire(ensureIndexes) + .acquire(setupListeners) + .acquire(setupConnect) + .acquire(setupBridge) + .acquire(setupMMConnect) + .acquire(finishBoot); +} + +module.exports = boot; diff --git a/lib/server/cache.js b/lib/server/cache.js new file mode 100644 index 00000000000..06e55c2a665 --- /dev/null +++ b/lib/server/cache.js @@ -0,0 +1,111 @@ +'use strict'; + +/* This is a simple cache intended to reduce the amount of load + * Nightscout puts on MongoDB. The cache is based on identifying + * elements based on the MongoDB _id field and implements simple + * semantics for adding data to the cache in the runtime, intended + * to be accessed by the persistence layer as data is inserted, updated + * or deleted, as well as the periodic dataloader, which polls Mongo + * for new inserts. + * + * Longer term, the cache is planned to allow skipping the Mongo polls + * altogether. + */ + +const _ = require('lodash'); +const constants = require('../constants'); + +function cache (env, ctx) { + + const data = { + treatments: [] + , devicestatus: [] + , entries: [] + }; + + const retentionPeriods = { + treatments: constants.ONE_HOUR * 60 + , devicestatus: env.extendedSettings.devicestatus && env.extendedSettings.devicestatus.days && env.extendedSettings.devicestatus.days == 2 ? constants.TWO_DAYS : constants.ONE_DAY + , entries: constants.TWO_DAYS + }; + + function getObjectAge(object) { + let age = object.mills || object.date; + if (isNaN(age) && object.created_at) age = Date.parse(object.created_at).valueOf(); + return age; + } + + function mergeCacheArrays (oldData, newData, retentionPeriod) { + + const ageLimit = Date.now() - retentionPeriod; + + var filteredOld = filterForAge(oldData, ageLimit); + var filteredNew = filterForAge(newData, ageLimit); + + const merged = ctx.ddata.idMergePreferNew(filteredOld, filteredNew); + + return _.sortBy(merged, function(item) { + const age = getObjectAge(item); + return -age; + }); + + function filterForAge(data, ageLimit) { + return _.filter(data, function hasId(object) { + const hasId = !_.isEmpty(object._id); + const age = getObjectAge(object); + const isFresh = age >= ageLimit; + return isFresh && hasId; + }); + } + + } + + data.isEmpty = (datatype) => { + return data[datatype].length < 20; + } + + data.getData = (datatype) => { + return _.cloneDeep(data[datatype]); + } + + data.insertData = (datatype, newData) => { + data[datatype] = mergeCacheArrays(data[datatype], newData, retentionPeriods[datatype]); + return data.getData(datatype); + } + + function dataChanged (operation) { + if (!data[operation.type]) return; + + if (operation.op == 'remove') { + // if multiple items were deleted, flush entire cache + if (!operation.changes) { + data.treatments = []; + data.devicestatus = []; + data.entries = []; + } else { + removeFromArray(data[operation.type], operation.changes); + } + } + + if (operation.op == 'update') { + data[operation.type] = mergeCacheArrays(data[operation.type], operation.changes, retentionPeriods[operation.type]); + } + } + + ctx.bus.on('data-update', dataChanged); + + function removeFromArray (array, id) { + for (let i = 0; i < array.length; i++) { + const o = array[i]; + if (o._id == id) { + //console.log('Deleting object from cache', id); + array.splice(i, 1); + break; + } + } + } + + return data; +} + +module.exports = cache; diff --git a/lib/server/clocks.js b/lib/server/clocks.js new file mode 100644 index 00000000000..9926eefcf82 --- /dev/null +++ b/lib/server/clocks.js @@ -0,0 +1,34 @@ +'use strict'; + +const express = require('express'); +const path = require('path'); + +function clockviews() { + + const app = new express(); + let locals = {}; + + app.set('view engine', 'ejs'); + app.engine('html', require('ejs').renderFile); + app.set("views", path.join(__dirname, "../../views/clockviews/")); + + app.get('/:face', (req, res) => { + + const face = req.params.face; + console.log('Clockface requested:', face); + + res.render('clock.html', { + face, + locals + }); + + }); + + app.setLocals = function (_locals) { + locals = _locals; + } + + return app; +} + +module.exports = clockviews; \ No newline at end of file diff --git a/lib/server/devicestatus.js b/lib/server/devicestatus.js new file mode 100644 index 00000000000..bf71437d646 --- /dev/null +++ b/lib/server/devicestatus.js @@ -0,0 +1,142 @@ +'use strict'; + +var moment = require('moment'); +var find_options = require('./query'); + +function storage (collection, ctx) { + + function create (statuses, fn) { + + if (!Array.isArray(statuses)) { statuses = [statuses]; } + + const r = []; + let errorOccurred = false; + + for (let i = 0; i < statuses.length; i++) { + + const obj = statuses[i]; + + if (errorOccurred) return; + + // Normalize all dates to UTC + const d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment(); + obj.created_at = d.toISOString(); + obj.utcOffset = d.utcOffset(); + + api().insertOne(obj, function(err, results) { + if (err !== null && err.message) { + console.log('Error inserting the device status object', err.message); + errorOccurred = true; + fn(err.message, null); + return; + } + + if (!err) { + + if (!obj._id) obj._id = results.insertedIds[0]._id; + r.push(obj); + + ctx.bus.emit('data-update', { + type: 'devicestatus' + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime([obj]) + }); + + // Last object! Return results + if (i == statuses.length - 1) { + fn(null, r); + ctx.bus.emit('data-received'); + } + } + }); + }; + } + + function last (fn) { + return list({ count: 1 }, function(err, entries) { + if (entries && entries.length > 0) { + fn(err, entries[0]); + } else { + fn(err, null); + } + }); + } + + function query_for (opts) { + return find_options(opts, storage.queryOpts); + } + + function list (opts, fn) { + // these functions, find, sort, and limit, are used to + // dynamically configure the request, based on the options we've + // been given + + // determine sort options + function sort () { + return opts && opts.sort || { created_at: -1 }; + } + + // configure the limit portion of the current query + function limit () { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + // handle all the results + function toArray (err, entries) { + fn(err, entries); + } + + // now just stitch them all together + limit.call(api() + .find(query_for(opts)) + .sort(sort()) + ).toArray(toArray); + } + + function remove (opts, fn) { + + function removed (err, stat) { + + ctx.bus.emit('data-update', { + type: 'devicestatus' + , op: 'remove' + , count: stat.result.n + , changes: opts.find._id + }); + + fn(err, stat); + } + + return api().remove( + query_for(opts), removed); + } + + function api () { + return ctx.store.collection(collection); + } + + api.list = list; + api.create = create; + api.query_for = query_for; + api.last = last; + api.remove = remove; + api.aggregate = require('./aggregate')({}, api); + api.indexedFields = [ + 'created_at' + + + + + , 'NSCLIENT_ID' + ]; + return api; +} + +storage.queryOpts = { + dateField: 'created_at' +}; + +module.exports = storage; diff --git a/lib/server/enclave.js b/lib/server/enclave.js new file mode 100644 index 00000000000..03dc8facf96 --- /dev/null +++ b/lib/server/enclave.js @@ -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 + '/../../node_modules/.cache/_ns_cache/' + 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').toLowerCase(); + } + + 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.toLowerCase() == 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').toLowerCase(); + } + + return enclave; +} + +module.exports = init; diff --git a/lib/server/entries.js b/lib/server/entries.js new file mode 100644 index 00000000000..7e6ac90d35f --- /dev/null +++ b/lib/server/entries.js @@ -0,0 +1,194 @@ +'use strict'; + +var es = require('event-stream'); +var find_options = require('./query'); +var ObjectID = require('mongodb').ObjectID; +var moment = require('moment'); + +/**********\ + * Entries + * Encapsulate persistent storage of sgv entries. +\**********/ + +function storage (env, ctx) { + + // TODO: Code is a little redundant. + + // query for entries from storage + function list (opts, fn) { + // these functions, find, sort, and limit, are used to + // dynamically configure the request, based on the options we've + // been given + + // determine sort options + function sort () { + return opts && opts.sort || { date: -1 }; + } + + // configure the limit portion of the current query + function limit () { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + // handle all the results + function toArray (err, entries) { + fn(err, entries); + } + + // now just stitch them all together + limit.call(api() + .find(query_for(opts)) + .sort(sort()) + ).toArray(toArray); + } + + function remove (opts, fn) { + api().remove(query_for(opts), function(err, stat) { + + ctx.bus.emit('data-update', { + type: 'entries' + , op: 'remove' + , count: stat.result.n + , changes: opts.find._id + }); + + //TODO: this is triggering a read from Mongo, we can do better + ctx.bus.emit('data-received'); + fn(err, stat); + }); + } + + // return writable stream to lint each sgv record passing through it + // TODO: get rid of this? not doing anything now + function map () { + return es.map(function iter (item, next) { + return next(null, item); + }); + } + + // writable stream that persists all records + // takes function to call when done + function persist (fn) { + // receives entire list at end of stream + function done (err, result) { + // report any errors + if (err) { return fn(err, result); } + // batch insert a list of records + create(result, fn); + } + // lint and store the entire list + return es.pipeline(map(), es.writeArray(done)); + } + + //TODO: implement + //function update (fn) { + //} + // + + // store new documents using the storage mechanism + function create (docs, fn) { + // potentially a batch insert + var firstErr = null + , numDocs = docs.length + , totalCreated = 0; + + docs.forEach(function(doc) { + + // Normalize dates to be in UTC, store offset in utcOffset + + var _sysTime; + + if (doc.dateString) { _sysTime = moment.parseZone(doc.dateString); } + if (!_sysTime && doc.date) { _sysTime = moment(doc.date); } + if (!_sysTime) _sysTime = moment(); + + doc.utcOffset = _sysTime.utcOffset(); + doc.sysTime = _sysTime.toISOString(); + if (doc.dateString) doc.dateString = doc.sysTime; + + var query = (doc.sysTime && doc.type) ? { sysTime: doc.sysTime, type: doc.type } : doc; + api().update(query, doc, { upsert: true }, function(err, updateResults) { + firstErr = firstErr || err; + + if (!err) { + if (updateResults.result.upserted) { + doc._id = updateResults.result.upserted[0]._id + } + + ctx.bus.emit('data-update', { + type: 'entries' + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime([doc]) + }); + } + + if (++totalCreated === numDocs) { + //TODO: this is triggering a read from Mongo, we can do better + ctx.bus.emit('data-received'); + fn(firstErr, docs); + } + }); + }); + } + + function getEntry (id, fn) { + api().findOne({ _id: ObjectID(id) }, function(err, entry) { + if (err) { + fn(err); + } else { + fn(null, entry); + } + }); + } + + function query_for (opts) { + return find_options(opts, storage.queryOpts); + } + + // closure to represent the API + function api () { + // obtain handle usable for querying the collection associated + // with these records + return ctx.store.collection(env.entries_collection); + } + + // Expose all the useful functions + api.list = list; + api.map = map; + api.create = create; + api.remove = remove; + api.persist = persist; + api.query_for = query_for; + api.getEntry = getEntry; + api.aggregate = require('./aggregate')({}, api); + api.indexedFields = [ + 'date' + , 'type' + , 'sgv' + , 'mbg' + , 'sysTime' + , 'dateString' + , { 'type': 1, 'date': -1, 'dateString': 1 } + ]; + return api; +} + +storage.queryOpts = { + walker: { + date: parseInt + , sgv: parseInt + , filtered: parseInt + , unfiltered: parseInt + , rssi: parseInt + , noise: parseInt + , mbg: parseInt + } + , useEpoch: true +}; + +// expose module +storage.storage = storage; +module.exports = storage; diff --git a/lib/server/env.js b/lib/server/env.js new file mode 100644 index 00000000000..afb53d51332 --- /dev/null +++ b/lib/server/env.js @@ -0,0 +1,222 @@ +'use strict'; + +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'); + +const mongoParser = require('mongo-url-parser'); + +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 () { + + // Assume users will typo whitespaces into keys and values + + shadowEnv = {}; + + Object.keys(process.env).forEach((key, index) => { + 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', '/static'); + env.debug = { + minify: readENVTruthy('DEBUG_MINIFY', true) + }; + + env.err = []; + env.notifies = []; + env.enclave = enclave(); + + setSSL(); + setStorage(); + setAPISecret(); + setVersion(); + updateSettings(); + + return env; +} + +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) + }; + if (env.SSL_CA) { + env.ca = fs.readFileSync(env.SSL_CA); + } + } + + 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.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 () { + var useSecret = (readENV('API_SECRET') && readENV('API_SECRET').length > 0); + //TODO: should we clear API_SECRET from process env? + env.api_secret = null; + // if a passphrase was provided, get the hex digest to mint a single token + if (useSecret) { + 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.push({ desc: msg }); + } else { + + 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'); + env.version = software.version; + env.name = software.name; +} + +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_'); + env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); + env.profile_collection = readENV('MONGO_PROFILE_COLLECTION', 'profile'); + env.settings_collection = readENV('MONGO_SETTINGS_COLLECTION', 'settings'); + env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); + env.food_collection = readENV('MONGO_FOOD_COLLECTION', 'food'); + env.activity_collection = readENV('MONGO_ACTIVITY_COLLECTION', 'activity'); + + var DB = { url: null, collection: null } + , 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 () { + + var envNameOverrides = { + UNITS: 'DISPLAY_UNITS' + }; + + var envDefaultOverrides = { + DISPLAY_UNITS: 'mg/dl' + }; + + env.settings.eachSettingAsEnv(function settingFromEnv (name) { + var envName = envNameOverrides[name] || name; + return readENV(envName, envDefaultOverrides[envName]); + }); + + //should always find extended settings last + env.extendedSettings = findExtendedSettings(shadowEnv); + + if (!readENVTruthy('TREATMENTS_AUTH', true)) { + env.settings.authDefaultRoles = env.settings.authDefaultRoles || ""; + env.settings.authDefaultRoles += ' careportal'; + } +} + +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()]; + + if (varName == 'DISPLAY_UNITS') { + if (value && value.toLowerCase().includes('mmol')) { + value = 'mmol'; + } else { + value = defaultValue; + } + } + + return value != null ? value : 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 } + return value; +} + +function findExtendedSettings (envs) { + var extended = {}; + + extended.devicestatus = {}; + extended.devicestatus.advanced = true; + 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) { + if (_trim(enable)) { + _forIn(envs, function eachEnvPair (value, key) { + var env = normalizeEnv(key); + if (_startsWith(env, enable.toUpperCase() + '_')) { + var split = env.indexOf('_'); + if (split > -1 && split <= env.length) { + var exts = extended[enable] || {}; + extended[enable] = exts; + var ext = _camelCase(env.substring(split + 1).toLowerCase()); + if (!isNaN(value)) { value = Number(value); } + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + exts[ext] = value; + } + } + }); + } + }); + return extended; +} + +module.exports = config; diff --git a/lib/food.js b/lib/server/food.js similarity index 71% rename from lib/food.js rename to lib/server/food.js index cfe45f9d85a..92c41843f7b 100644 --- a/lib/food.js +++ b/lib/server/food.js @@ -6,12 +6,22 @@ function storage (env, ctx) { function create (obj, fn) { obj.created_at = (new Date( )).toISOString( ); api().insert(obj, function (err, doc) { + if (err != null && err.message) { + console.log('Data insertion error', err.message); + fn(err.message, null); + return; + } fn(null, doc.ops); }); } function save (obj, fn) { - obj._id = new ObjectID(obj._id); + try { + obj._id = new ObjectID(obj._id); + } catch (err){ + console.error(err); + obj._id = new ObjectID(); + } obj.created_at = (new Date( )).toISOString( ); api().save(obj, function (err, doc) { fn(err, doc); @@ -31,13 +41,14 @@ function storage (env, ctx) { } function remove (_id, fn) { - return api( ).remove({ '_id': new ObjectID(_id) }, fn); + var objId = new ObjectID(_id); + return api( ).remove({ '_id': objId }, fn); } function api ( ) { - return ctx.store.db.collection(env.food_collection); + return ctx.store.collection(env.food_collection); } api.list = list; diff --git a/lib/server/loop.js b/lib/server/loop.js new file mode 100644 index 00000000000..3b31c0ec78c --- /dev/null +++ b/lib/server/loop.js @@ -0,0 +1,146 @@ +//'use strict'; + +const apn = require('@parse/node-apn'); + +function init (env, ctx) { + + function loop () { + return loop; + } + + loop.sendNotification = function sendNotification (data, remoteAddress, completion) { + + // console.info("JAP"); + // console.info(data); + + if (env.extendedSettings.loop.apnsKey === undefined || env.extendedSettings.loop.apnsKey.length == 0) { + completion("Loop notification failed: LOOP_APNS_KEY not set."); + return; + } + + if (env.extendedSettings.loop.apnsKeyId === undefined || env.extendedSettings.loop.apnsKeyId.length == 0) { + completion("Loop notification failed: LOOP_APNS_KEY_ID not set."); + return; + } + + if (env.extendedSettings.loop.developerTeamId === undefined || env.extendedSettings.loop.developerTeamId.length != 10) { + completion("Loop notification failed: LOOP_DEVELOPER_TEAM_ID not set."); + return; + } + + if (ctx.ddata.profiles === undefined || ctx.ddata.profiles.length < 1 || ctx.ddata.profiles[0].loopSettings === undefined) { + completion("Loop notification failed: Could not find loopSettings in profile."); + return; + } + + let loopSettings = ctx.ddata.profiles[0].loopSettings; + + if (loopSettings.deviceToken === undefined) { + completion("Loop notification failed: Could not find deviceToken in loopSettings."); + return; + } + + if (loopSettings.bundleIdentifier === undefined) { + completion("Loop notification failed: Could not find bundleIdentifier in loopSettings."); + return; + } + + var options = { + token: { + key: env.extendedSettings.loop.apnsKey + , keyId: env.extendedSettings.loop.apnsKeyId + , teamId: env.extendedSettings.loop.developerTeamId + }, + production: env.extendedSettings.loop.pushServerEnvironment === "production" + }; + + var provider = new apn.Provider(options); + + var payload = { + 'remote-address': remoteAddress, + 'notes': data.notes, + 'entered-by': data.enteredBy + }; + var alert; + if (data.eventType === 'Temporary Override Cancel') { + payload["cancel-temporary-override"] = "true"; + alert = "Cancel Temporary Override"; + } else if (data.eventType === 'Temporary Override') { + payload["override-name"] = data.reason; + if (data.duration !== undefined && parseInt(data.duration) > 0) { + payload["override-duration-minutes"] = parseInt(data.duration); + } + alert = data.reasonDisplay + " Temporary Override"; + } else if (data.eventType === 'Remote Carbs Entry') { + payload["carbs-entry"] = parseFloat(data.remoteCarbs); + if(payload["carbs-entry"] > 0.0 ) { + payload["absorption-time"] = 3.0; + if (data.remoteAbsorption !== undefined && parseFloat(data.remoteAbsorption) > 0.0) { + payload["absorption-time"] = parseFloat(data.remoteAbsorption); + } + if (data.otp !== undefined && data.otp.length > 0) { + payload["otp"] = ""+data.otp + } + if (data.created_at !== undefined) { + payload['start-time'] = data.created_at; + } + alert = "Remote Carbs Entry: "+payload["carbs-entry"]+" grams\n"; + alert += "Absorption Time: "+payload["absorption-time"]+" hours"; + } else { + completion("Loop remote carbs failed. Incorrect carbs entry: ", data.remoteCarbs); + return; + } + + } else if (data.eventType === 'Remote Bolus Entry') { + payload["bolus-entry"] = parseFloat(data.remoteBolus); + if(payload["bolus-entry"] > 0.0 ) { + alert = "Remote Bolus Entry: "+payload["bolus-entry"]+" U\n"; + if (data.otp !== undefined && data.otp.length > 0) { + payload["otp"] = ""+data.otp + } + } else { + completion("Loop remote bolus failed. Incorrect bolus entry: ", data.remoteBolus); + return; + } + } else { + completion("Loop notification failed: Unhandled event type:", data.eventType); + return; + } + + if (data.notes !== undefined && data.notes.length > 0) { + alert += " - " + data.notes + } + + if (data.enteredBy !== undefined && data.enteredBy.length > 0) { + alert += " - " + data.enteredBy + } + + // Track time notification was sent + let now = new Date() + payload['sent-at'] = now.toISOString(); + + // Expire after 5 minutes. + let expiration = new Date(now.getTime() + 5 * 60 * 1000) + payload['expiration'] = expiration.toISOString(); + + let notification = new apn.Notification(); + notification.alert = alert; + notification.topic = loopSettings.bundleIdentifier; + notification.contentAvailable = 1; + notification.payload = payload; + notification.interruptionLevel = "time-sensitive" + + provider.send(notification, [loopSettings.deviceToken]).then( (response) => { + if (response.sent && response.sent.length > 0) { + completion(); + } else { + console.log("APNs delivery failed:", response.failed) + completion("APNs delivery failed: " + response.failed[0].response.reason); + } + }); + }; + + return loop(); +} + +module.exports = init; diff --git a/lib/pebble.js b/lib/server/pebble.js similarity index 70% rename from lib/pebble.js rename to lib/server/pebble.js index 877c4764b30..97b756a9b84 100644 --- a/lib/pebble.js +++ b/lib/server/pebble.js @@ -2,11 +2,8 @@ var _ = require('lodash'); -var sandbox = require('./sandbox')(); -var units = require('./units')(); -var iob = require('./plugins/iob')(); -var bwp = require('./plugins/boluswizardpreview')(); -var delta = require('./plugins/delta')(); +var sandbox = require('../sandbox')(); +var units = require('../units')(); var DIRECTIONS = { NONE: 0 @@ -69,32 +66,36 @@ function addExtraData (first, req, sbx) { var data = sbx.data; function addDelta() { - var prev = data.sgvs.length >= 2 ? data.sgvs[data.sgvs.length - 2] : null; - var current = sbx.lastSGVEntry(); + var delta = sbx.properties.delta; //for legacy reasons we need to return a 0 for delta if it can't be calculated - var deltaResult = delta.calc(prev, current, sbx); - first.bgdelta = deltaResult && deltaResult.scaled || 0; + first.bgdelta = delta && delta.scaled || 0; if (req.mmol) { first.bgdelta = first.bgdelta.toFixed(1); } } function addBattery() { - if (data.devicestatus && data.devicestatus.uploaderBattery && data.devicestatus.uploaderBattery >= 0) { - first.battery = data.devicestatus.uploaderBattery.toString(); + var uploaderStatus = _.findLast(data.devicestatus, function (status) { + return ('uploader' in status); + }); + + var battery = uploaderStatus && uploaderStatus.uploader && uploaderStatus.uploader.battery; + + if (battery && battery >= 0) { + first.battery = battery.toString(); } } function addIOB() { if (req.iob) { - var iobResult = iob.calcTotal(data.treatments, data.profile, Date.now()); + var iobResult = req.ctx.plugins('iob').calcTotal(data.treatments, data.devicestatus, data.profile, Date.now()); if (iobResult) { - first.iob = iobResult.display; + first.iob = iobResult.display || 0; } sbx.properties.iob = iobResult; - var bwpResult = bwp.calc(sbx); + var bwpResult = req.ctx.plugins('bwp').calc(sbx); if (bwpResult) { first.bwp = bwpResult.bolusEstimateDisplay; @@ -104,9 +105,19 @@ function addExtraData (first, req, sbx) { } } + function addCOB() { + if (req.cob) { + var cobResult = req.ctx.plugins('cob').cobTotal(data.treatments, data.devicestatus, data.profile, Date.now()); + if (cobResult) { + first.cob = cobResult.display || 0; + } + } + } + addDelta(); addBattery(); addIOB(); + addCOB(); } function prepareBGs (req, sbx) { @@ -137,7 +148,11 @@ function prepareSandbox (req) { if (req.mmol) { clonedEnv.settings.units = 'mmol'; } - return sandbox.serverInit(clonedEnv, req.ctx); + + var sbx = sandbox.serverInit(clonedEnv, req.ctx); + req.ctx.plugins('bgnow').setProperties(sbx); + + return sbx; } function pebble (req, res) { @@ -154,17 +169,19 @@ function pebble (req, res) { } function configure (env, ctx) { + var wares = require('../middleware/')(env); function middle (req, res, next) { req.env = env; req.ctx = ctx; req.rawbg = env.settings.isEnabled('rawbg'); req.iob = env.settings.isEnabled('iob'); - req.mmol = (req.query.units || env.DISPLAY_UNITS) === 'mmol'; + req.cob = env.settings.isEnabled('cob'); + req.mmol = (req.query.units || env.settings.units) === 'mmol'; req.count = parseInt(req.query.count) || 1; next( ); } - return [middle, pebble]; + return [middle, wares.sendJSONStatus, ctx.authorization.isPermitted('api:pebble,entries:read'), pebble]; } configure.pebble = pebble; diff --git a/lib/server/profile.js b/lib/server/profile.js new file mode 100644 index 00000000000..e49e366d15a --- /dev/null +++ b/lib/server/profile.js @@ -0,0 +1,101 @@ +'use strict'; + +var find_options = require('./query'); +var consts = require('../constants'); + +function storage (collection, ctx) { + var ObjectID = require('mongodb').ObjectID; + + function create (obj, fn) { + obj.created_at = (new Date( )).toISOString( ); + api().insert(obj, function (err, doc) { + fn(null, doc); + }); + ctx.bus.emit('data-received'); + } + + function save (obj, fn) { + obj._id = new ObjectID(obj._id); + if (!obj.created_at) { + obj.created_at = (new Date( )).toISOString( ); + } + api().save(obj, function (err) { + //id should be added for new docs + fn(err, obj); + }); + ctx.bus.emit('data-received'); + } + + function list (fn, count) { + const limit = count !== null ? count : Number(consts.PROFILES_DEFAULT_COUNT); + return api( ).find({ }).limit(limit).sort({startDate: -1}).toArray(fn); + } + + function list_query (opts, fn) { + + storage.queryOpts = { + walker: {} + , dateField: 'startDate' + }; + + function limit () { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + return limit.call(api() + .find(query_for(opts)) + .sort(opts && opts.sort && query_sort(opts) || { startDate: -1 }), opts) + .toArray(fn); + } + + function query_for (opts) { + var retVal = find_options(opts, storage.queryOpts); + return retVal; + } + + function query_sort (opts) { + if (opts && opts.sort) { + var sortKeys = Object.keys(opts.sort); + + for (var i = 0; i < sortKeys.length; i++) { + if (opts.sort[sortKeys[i]] == '1') { + opts.sort[sortKeys[i]] = 1; + } + else { + opts.sort[sortKeys[i]] = -1; + } + } + return opts.sort; + } + } + + + function last (fn) { + return api().find().sort({startDate: -1}).limit(1).toArray(fn); + } + + function remove (_id, fn) { + var objId = new ObjectID(_id); + api( ).remove({ '_id': objId }, fn); + + ctx.bus.emit('data-received'); + } + + function api () { + return ctx.store.collection(collection); + } + + api.list = list; + api.list_query = list_query; + api.create = create; + api.save = save; + api.remove = remove; + api.last = last; + api.indexedFields = ['startDate']; + return api; +} + +module.exports = storage; diff --git a/lib/server/purifier.js b/lib/server/purifier.js new file mode 100644 index 00000000000..c9fe3c0230e --- /dev/null +++ b/lib/server/purifier.js @@ -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; diff --git a/lib/pushnotify.js b/lib/server/pushnotify.js similarity index 61% rename from lib/pushnotify.js rename to lib/server/pushnotify.js index 3cca136131f..4fd35ac03c4 100644 --- a/lib/pushnotify.js +++ b/lib/server/pushnotify.js @@ -1,18 +1,17 @@ 'use strict'; -var _ = require('lodash'); -var crypto = require('crypto'); -var NodeCache = require('node-cache'); +const _ = require('lodash'); +const crypto = require('crypto'); +const NodeCache = require('node-cache'); -var levels = require('./levels'); -var times = require('./times'); +const times = require('../times'); -function init(env, ctx) { +function init (env, ctx) { - function pushnotify() { + function pushnotify () { return pushnotify; } - + var levels = ctx.levels; var receipts = new NodeCache({ stdTTL: times.hour().secs, checkperiod: times.mins(5).secs }); var recentlySent = new NodeCache({ stdTTL: times.mins(15).secs, checkperiod: 20 }); @@ -23,17 +22,20 @@ function init(env, ctx) { return; } - var key = null; - if (notify.isAnnouncement) { - //Announcement notifications are sent if they are different from whats been recently sent - key = notifyToHash(notify); - } else if (levels.isAlarm(notify.level)) { - //Alarms can be snoozed - //for WARN and higher use the plugin name and notification level so that louder alarms aren't triggered too often - key = notify.plugin.name + '_' + notify.level; - } else { - //INFO and lower notifications should be sent as long as they are different from whats been recently sent - key = notifyToHash(notify); + var key = notify.notifyhash || false; + + if (!key) { + if (notify.isAnnouncement) { + //Announcement notifications are sent if they are different from whats been recently sent + key = notifyToHash(notify); + } else if (levels.isAlarm(notify.level)) { + //Alarms can be snoozed + //for WARN and higher use the plugin name and notification level so that louder alarms aren't triggered too often + key = notify.plugin.name + '_' + notify.level; + } else { + //INFO and lower notifications should be sent as long as they are different from whats been recently sent + key = notifyToHash(notify); + } } notify.key = key; @@ -57,28 +59,31 @@ function init(env, ctx) { if (!response.receipt) { return false; } var notify = receipts.get(response.receipt); - console.info('push ack, response: ', response, ', notify: ', notify); if (notify) { + console.info('push ack, response: ', response, ', notify: ', notify); var snoozeMins = env.settings.snoozeFirstMinsForAlarmEvent(notify); - ctx.notifications.ack(notify.level, times.mins(snoozeMins).msecs, true); + ctx.notifications.ack(notify.level, notify.group, times.mins(snoozeMins).msecs, true); receipts.del(response.receipt); + } else { + console.info('unable to find notify for pushover ack', response, receipts.keys()); } + return !!notify; }; - function cancelPushoverNotifications ( ) { + function cancelPushoverNotifications () { if (ctx.pushover) { var receiptKeys = receipts.keys(); - _.each(receiptKeys, function eachKey(receipt) { - ctx.pushover.cancelWithReceipt(receipt, function cancelCallback(err) { + _.each(receiptKeys, function eachKey (receipt) { + ctx.pushover.cancelWithReceipt(receipt, function cancelCallback (err) { if (err) { - console.error('error canceling receipt, err: ', err); + console.error('error canceling receipt:' + receipt + ', err: ', err); } else { - console.info('got a receipt cancel response'); + console.info('got a receipt cancel response for:' + receipt + ', removing from cache'); + receipts.del(receipt); } }); - receipts.del(receipt); }); } } @@ -86,9 +91,9 @@ function init(env, ctx) { function sendPushoverNotifications (notify) { if (ctx.pushover) { //add the key to the cache before sending, but with a short TTL - ctx.pushover.send(notify, function pushoverCallback(err, result) { + ctx.pushover.send(notify, function pushoverCallback (err, result) { if (err) { - console.warn('Unable to send pushover', err, notify); + console.warn('Unable to send pushover', notify, err); } else { //result comes back as a string here, so fix it result = JSON.parse(result); @@ -97,6 +102,7 @@ function init(env, ctx) { if (result.receipt) { //if this was an emergency alarm, also hold on to the receipt/notify mapping, for later acking + console.info('storing pushover receipt', result.receipt, notify); receipts.set(result.receipt, notify); } } @@ -108,9 +114,9 @@ function init(env, ctx) { if (ctx.maker) { ctx.maker.sendAllClear(notify, function makerCallback (err, result) { if (err) { - console.error('unable to send maker allclear', err); + console.error('unable to send maker allclear', notify, err); } else if (result && result.sent) { - console.info('sent maker allclear'); + console.info('sent maker allclear', notify); } }); } @@ -130,7 +136,7 @@ function init(env, ctx) { }; ctx.maker.sendEvent(event, function makerCallback (err) { if (err) { - console.error('unable to send maker event', err, event); + console.error('unable to send maker event', event, err); } else { console.info('sent maker event: ', event); recentlySent.ttl(notify.key, times.mins(15).secs); @@ -148,5 +154,4 @@ function init(env, ctx) { return pushnotify(); } - -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/query.js b/lib/server/query.js similarity index 84% rename from lib/query.js rename to lib/server/query.js index 8366af4844a..8279d5ad1e8 100644 --- a/lib/query.js +++ b/lib/server/query.js @@ -1,9 +1,10 @@ 'use strict'; -var traverse = require('traverse'); -var ObjectID = require('mongodb').ObjectID; +const traverse = require('traverse'); +const ObjectID = require('mongodb').ObjectID; +const moment = require('moment'); -var TWO_DAYS = 172800000; +const TWO_DAYS = 172800000; /** * @module query utilities * Assist in translating objects from query-string representation into @@ -56,7 +57,27 @@ function default_options (opts) { function enforceDateFilter (query, opts) { var dateValue = query[opts.dateField]; - if (!dateValue && !query.dateString) { + // rewrite dates to ISO UTC strings so queries work as expected + if (dateValue) { + Object.keys(dateValue).forEach(function(key) { + let dateString = dateValue[key]; + if (isNaN(dateString)) { + dateString = dateString.replace(' ', '+'); // some clients don't excape the plus + + const validDate = moment(dateString).isValid(); + + if (!validDate) { + console.error('API request using an invalid date:', dateString); + throw new Error('Cannot parse ' + dateString + ' as a valid ISO-8601 date'); + } + + const d = moment.parseZone(dateString); + dateValue[key] = d.toISOString(); + } + }); + } + + if (!dateValue && !query.dateString && true !== opts.noDateFilter) { var minDate = Date.now( ) - opts.deltaAgo; query[opts.dateField] = { $gte: opts.useEpoch ? minDate : new Date(minDate).toISOString() @@ -94,11 +115,15 @@ function create (params, opts) { var query = finder && finder.find ? finder.find : { }; // Ensure some kind of sane date constraint tied to an index is expressed in the query. - enforceDateFilter(query, opts); + // unless an ID is provided, in which case assume the user knows what they are doing. + if (! query._id ) { + enforceDateFilter(query, opts); + } + // Help queries for _id. updateIdQuery(query); - console.info('query:', query); + //console.info('query:', query); // Ready for mongodb.find( ) and friends. return query; } diff --git a/lib/server/server.js b/lib/server/server.js new file mode 100644 index 00000000000..49efaeb9394 --- /dev/null +++ b/lib/server/server.js @@ -0,0 +1,84 @@ +/* +* cgm-remote-monitor - web app to broadcast cgm readings +* Copyright (C) 2014 Nightscout contributors. See the COPYRIGHT file +* at the root directory of this distribution and at +* https://github.com/nightscout/cgm-remote-monitor/blob/master/COPYRIGHT +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published +* by the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +// Description: Basic web server to display data from Dexcom G4. Requires a database that contains +// the Dexcom SGV data. +'use strict'; + +/////////////////////////////////////////////////// +// DB Connection setup and utils +/////////////////////////////////////////////////// + +const fs = require('fs'); +const env = require('./env')( ); +const language = require('../language')(); +const translate = language.set(env.settings.language).translate; +language.loadLocalization(fs); + +/////////////////////////////////////////////////// +// setup http server +/////////////////////////////////////////////////// +var PORT = env.PORT; +var HOSTNAME = env.HOSTNAME; + +function create (app) { + var transport = (env.ssl + ? require('https') : require('http')); + if (env.ssl) { + return transport.createServer(env.ssl, app); + } + return transport.createServer(app); +} + +require('./bootevent')(env, language).boot(function booted (ctx) { + + console.log('Boot event processing completed'); + + var app = require('./app')(env, ctx); + var server = create(app).listen(PORT, HOSTNAME); + console.log(translate('Listening on port'), PORT, HOSTNAME); + + if (ctx.bootErrors && ctx.bootErrors.length > 0) { + return; + } + + ctx.bus.on('teardown', function serverTeardown () { + server.close(); + clearTimeout(sendStartupAllClearTimer); + ctx.store.client.close(); + }); + + /////////////////////////////////////////////////// + // setup socket io for data and message transmission + /////////////////////////////////////////////////// + var websocket = require('./websocket')(env, ctx, server); + + //after startup if there are no alarms send all clear + let sendStartupAllClearTimer = setTimeout(function sendStartupAllClear () { + var alarm = ctx.notifications.findHighestAlarm(); + if (!alarm) { + ctx.bus.emit('notification', { + clear: true + , title: 'All Clear' + , message: 'Server started without alarms' + }); + } + }, 20000); +}); diff --git a/lib/server/swagger.json b/lib/server/swagger.json new file mode 100755 index 00000000000..b041d5a2850 --- /dev/null +++ b/lib/server/swagger.json @@ -0,0 +1,1450 @@ +{ + "openapi": "3.0.0", + "servers": [ + { + "url": "/api/v1" + } + ], + "info": { + "title": "Nightscout API", + "description": "Own your DData with the Nightscout API", + "version": "14.2.3", + "license": { + "name": "AGPL 3", + "url": "https://www.gnu.org/licenses/agpl.txt" + } + }, + "security": [ + { + "api_secret": [], + "token_in_url": [], + "jwtoken": [] + } + ], + "paths": { + "/entries/{spec}": { + "get": { + "summary": "All Entries matching query", + "description": "The Entries endpoint returns information about the\nNightscout entries.\n", + "parameters": [ + { + "name": "spec", + "in": "path", + "description": "entry id, such as `55cf81bc436037528ec75fa5` or a type filter such\nas `sgv`, `mbg`, etc.\n", + "required": true, + "schema": { + "type": "string", + "default": "sgv" + } + }, + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for\nexample `find[dateString][$gte]=2015-08-27`. All find parameters\nare interpreted as strings.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "tags": [ + "Entries" + ], + "responses": { + "200": { + "description": "An array of entries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entries" + } + } + } + }, + "default": { + "description": "Entries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entries" + } + } + } + } + } + } + }, + "/slice/{storage}/{field}/{type}/{prefix}/{regex}": { + "get": { + "summary": "All Entries matching query", + "description": "The Entries endpoint returns information about the Nightscout entries.", + "parameters": [ + { + "name": "storage", + "in": "path", + "description": "Prefix to use in constructing a prefix-based regex, default is `entries`.", + "required": true, + "schema": { + "type": "string", + "default": "entries" + } + }, + { + "name": "field", + "in": "path", + "description": "Name of the field to use Regex against in query object, default is `dateString`.", + "required": true, + "schema": { + "type": "string", + "default": "dateString" + } + }, + { + "name": "type", + "in": "path", + "description": "The type field to search against, default is sgv.", + "required": true, + "schema": { + "type": "string", + "default": "sgv" + } + }, + { + "name": "prefix", + "in": "path", + "description": "Prefix to use in constructing a prefix-based regex.", + "required": true, + "schema": { + "type": "string", + "default": "2015" + } + }, + { + "name": "regex", + "in": "path", + "description": "Tail part of regexp to use in expanding/construccting a query object.\nRegexp also has bash-style brace and glob expansion applied to it,\ncreating ways to search for modal times of day, perhaps using\nsomething like this syntax: `T{15..17}:.*`, this would search for\nall records from 3pm to 5pm.\n", + "required": true, + "schema": { + "type": "string", + "default": ".*" + } + }, + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for\nexample `find[dateString][$gte]=2015-08-27`. All find parameters\nare interpreted as strings.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "tags": [ + "Entries" + ], + "responses": { + "200": { + "description": "An array of entries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entries" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/echo/{storage}/{spec}": { + "get": { + "summary": "View generated Mongo Query object", + "description": "Information about the mongo query object created by the query.\n", + "parameters": [ + { + "name": "storage", + "in": "path", + "description": "`entries`, or `treatments` to select the storage layer.\n", + "required": true, + "schema": { + "type": "string", + "default": "sgv" + } + }, + { + "name": "spec", + "in": "path", + "description": "entry id, such as `55cf81bc436037528ec75fa5` or a type filter such\nas `sgv`, `mbg`, etc.\nThis parameter is optional.\n", + "required": true, + "schema": { + "type": "string", + "default": "sgv" + } + }, + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for\nexample `find[dateString][$gte]=2015-08-27`. All find parameters\nare interpreted as strings.\n", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "tags": [ + "Entries", + "Debug" + ], + "responses": { + "200": { + "description": "An array of entries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MongoQuery" + } + } + } + } + } + } + }, + "/times/echo/{prefix}/{regex}": { + "get": { + "summary": "Echo the query object to be used.", + "description": "Echo debug information about the query object constructed.", + "parameters": [ + { + "name": "prefix", + "in": "path", + "description": "Prefix to use in constructing a prefix-based regex.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "regex", + "in": "path", + "description": "Tail part of regexp to use in expanding/construccting a query object.\nRegexp also has bash-style brace and glob expansion applied to it,\ncreating ways to search for modal times of day, perhaps using\nsomething like this syntax: `T{15..17}:.*`, this would search for\nall records from 3pm to 5pm.\n", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "tags": [ + "Entries", + "Debug" + ], + "responses": { + "200": { + "description": "An array of entries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MongoQuery" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/times/{prefix}/{regex}": { + "get": { + "summary": "All Entries matching query", + "description": "The Entries endpoint returns information about the Nightscout entries.", + "parameters": [ + { + "name": "prefix", + "in": "path", + "description": "Prefix to use in constructing a prefix-based regex.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "regex", + "in": "path", + "description": "Tail part of regexp to use in expanding/construccting a query object.\nRegexp also has bash-style brace and glob expansion applied to it,\ncreating ways to search for modal times of day, perhaps using\nsomething like this syntax: `T{15..17}:.*`, this would search for\nall records from 3pm to 5pm.\n", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "tags": [ + "Entries" + ], + "responses": { + "200": { + "description": "An array of entries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entries" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/entries": { + "get": { + "summary": "All Entries matching query", + "description": "The Entries endpoint returns information about the Nightscout entries.", + "parameters": [ + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "tags": [ + "Entries" + ], + "responses": { + "200": { + "description": "An array of entries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entries" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Entries" + ], + "summary": "Add new entries.", + "description": "", + "operationId": "addEntries", + "responses": { + "200": { + "description": "Rejected list of entries. Empty list is success." + }, + "405": { + "description": "Invalid input" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Entries" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Entries" + } + } + }, + "description": "Entries to be uploaded.", + "required": true + } + }, + "delete": { + "tags": [ + "Entries" + ], + "summary": "Delete entries matching query.", + "description": "Remove entries, same search syntax as GET.", + "operationId": "remove", + "parameters": [ + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Empty list is success." + } + } + } + }, + "/treatments": { + "get": { + "summary": "Treatments", + "description": "The Treatments endpoint returns information about the Nightscout treatments.", + "tags": [ + "Treatments" + ], + "parameters": [ + { + "name": "find", + "in": "query", + "description": "The query used to find entries, supports nested query syntax. Examples `find[insulin][$gte]=3` `find[carb][$gte]=100` `find[eventType]=Correction+Bolus` All find parameters are interpreted as strings.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "An array of treatments", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Treatments" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Treatments" + ], + "summary": "Add new treatments.", + "description": "", + "operationId": "addTreatments", + "responses": { + "200": { + "description": "Rejected list of treatments. Empty list is success." + }, + "405": { + "description": "Invalid input" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Treatments" + } + } + }, + "description": "Treatments to be uploaded.", + "required": true + } + }, + "delete": { + "tags": [ + "Treatments" + ], + "summary": "Delete treatments matching query.", + "description": "Remove treatments, same search syntax as GET.", + "operationId": "remove", + "parameters": [ + { + "name": "find", + "in": "query", + "description": "The query used to find treatments to delete, support nested query syntax, for example `find[insulin][$gte]=3`. All find parameters are interpreted as strings.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of entries to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Empty list is success." + } + } + } + }, + "/treatments/{spec}": { + "delete": { + "summary": "Delete treatments record with id provided in spec", + "description": "The Treatments endpoint returns information about the\nNightscout devicestatus records.\n", + "parameters": [ + { + "name": "spec", + "in": "path", + "description": "treatment id, such as `55cf81bc436037528ec75fa5`\n", + "required": true, + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Treatments" + ], + "responses": { + "200": { + "description": "A status record of the delete.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteStatus" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/profile": { + "get": { + "summary": "Profile", + "description": "The Profile endpoint returns information about the Nightscout Treatment Profiles.", + "tags": [ + "Profile" + ], + "responses": { + "200": { + "description": "An array of treatments", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Profile" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/status": { + "get": { + "summary": "Status", + "description": "Server side status, default settings and capabilities.", + "tags": [ + "Status" + ], + "responses": { + "200": { + "description": "Server capabilities and status.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Status" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/devicestatus/": { + "get": { + "summary": "All Devicestatuses matching query", + "description": "The Devicestatus endpoint returns information about the Nightscout devicestatus records.", + "parameters": [ + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "Number of devicestatus records to return.", + "required": false, + "schema": { + "type": "number" + } + } + ], + "tags": [ + "Devicestatus" + ], + "responses": { + "200": { + "description": "An array of devicestatus entries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Devicestatuses" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Devicestatus" + ], + "summary": "Add new devicestatus records.", + "description": "", + "operationId": "addDevicestatuses", + "responses": { + "200": { + "description": "Rejected list of device statuses. Empty list is success." + }, + "405": { + "description": "Invalid input" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Devicestatuses" + } + } + }, + "description": "Device statuses to be uploaded.", + "required": true + } + }, + "delete": { + "summary": "Delete all Devicestatus records matching query", + "description": "The Devicestatus endpoint returns information about the\nNightscout devicestatus records.\n", + "parameters": [ + { + "name": "find", + "in": "query", + "description": "The query used to find entries, support nested query syntax, for\nexample `find[created_at][$gte]=2015-08-27`. All find parameters\nare interpreted as strings.\n", + "required": false, + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Devicestatus" + ], + "responses": { + "200": { + "description": "A status record of the delete.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteStatus" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/devicestatus/{spec}": { + "delete": { + "summary": "Delete devicestatus record with id provided in spec", + "description": "The Devicestatus endpoint returns information about the\nNightscout devicestatus records.\n", + "parameters": [ + { + "name": "spec", + "in": "path", + "description": "entry id, such as `55cf81bc436037528ec75fa5`\n", + "required": true, + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Devicestatus" + ], + "responses": { + "200": { + "description": "A status record of the delete.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteStatus" + } + } + } + }, + "default": { + "description": "Unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "api_secret": { + "type": "apiKey", + "name": "api-secret", + "in": "header", + "description": "The hash of the API_SECRET env var" + }, + "token_in_url": { + "type": "apiKey", + "name": "token", + "in": "query", + "description": "Add token as query item in the URL. You can manage access Token in `/admin`. This uses json webtokens." + }, + "jwtoken": { + "type": "http", + "scheme": "bearer", + "description": "Use this if you know the temporary json webtoken.", + "bearerFormat": "JWT" + } + }, + "schemas": { + "Entry": { + "properties": { + "type": { + "type": "string", + "description": "sgv, mbg, cal, etc" + }, + "dateString": { + "type": "string", + "description": "dateString, MUST be ISO `8601` format date parseable by Javascript Date()" + }, + "date": { + "type": "number", + "description": "Epoch" + }, + "sgv": { + "type": "number", + "description": "The glucose reading. (only available for sgv types)" + }, + "direction": { + "type": "string", + "description": "Direction of glucose trend reported by CGM. (only available for sgv types)" + }, + "noise": { + "type": "number", + "description": "Noise level at time of reading. (only available for sgv types)" + }, + "filtered": { + "type": "number", + "description": "The raw filtered value directly from CGM transmitter. (only available for sgv types)" + }, + "unfiltered": { + "type": "number", + "description": "The raw unfiltered value directly from CGM transmitter. (only available for sgv types)" + }, + "rssi": { + "type": "number", + "description": "The signal strength from CGM transmitter. (only available for sgv types)" + } + } + }, + "Entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Entry" + } + }, + "Devicestatus": { + "required": [ + "device", + "created_at" + ], + "properties": { + "device": { + "type": "string", + "description": "Device type and hostname for example openaps://hostname" + }, + "created_at": { + "type": "string", + "description": "dateString, prefer ISO `8601`" + }, + "openaps": { + "type": "string", + "description": "OpenAPS devicestatus record - TODO: Fill Out Details" + }, + "loop": { + "type": "string", + "description": "Loop devicestatus record - TODO: Fill Out Details" + }, + "pump": { + "$ref": "#/components/schemas/pump" + }, + "uploader": { + "$ref": "#/components/schemas/uploader" + }, + "xdripjs": { + "$ref": "#/components/schemas/xdripjs" + } + } + }, + "Devicestatuses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Devicestatus" + } + }, + "pump": { + "properties": { + "clock": { + "type": "string", + "description": "dateString, prefer ISO `8601`" + }, + "battery": { + "$ref": "#/components/schemas/pumpbattery" + }, + "reservoir": { + "type": "number", + "description": "Amount of insulin remaining in pump reservoir" + }, + "status": { + "$ref": "#/components/schemas/pumpstatus" + } + } + }, + "pumpbattery": { + "properties": { + "status": { + "type": "string", + "description": "Pump Battery Status String" + }, + "voltage": { + "type": "number", + "description": "Pump Battery Voltage Level" + } + } + }, + "pumpstatus": { + "properties": { + "status": { + "type": "string", + "description": "Pump Status String" + }, + "bolusing": { + "type": "boolean", + "description": "Is Pump Bolusing" + }, + "suspended": { + "type": "boolean", + "description": "Is Pump Suspended" + }, + "timestamp": { + "type": "string", + "description": "dateString, prefer ISO `8601`" + } + } + }, + "uploader": { + "properties": { + "batteryVoltage": { + "type": "number", + "description": "Uploader Device Battery Voltage" + }, + "battery": { + "type": "number", + "description": "Uploader Device Battery Percentage Charge Remaining" + } + } + }, + "xdripjs": { + "properties": { + "state": { + "type": "number", + "description": "CGM Sensor Session State Code" + }, + "stateString": { + "type": "string", + "description": "CGM Sensor Session State String" + }, + "stateStringShort": { + "type": "string", + "description": "CGM Sensor Session State Short String" + }, + "txId": { + "type": "string", + "description": "CGM Transmitter ID" + }, + "txStatus": { + "type": "number", + "description": "CGM Transmitter Status" + }, + "txStatusString": { + "type": "string", + "description": "CGM Transmitter Status String" + }, + "txStatusStringShort": { + "type": "string", + "description": "CGM Transmitter Status Short String" + }, + "txActivation": { + "type": "number", + "description": "CGM Transmitter Activation Milliseconds After Epoch" + }, + "mode": { + "type": "string", + "description": "Mode xdrip-js Application Operationg: expired, not expired, etc." + }, + "timestamp": { + "type": "number", + "description": "Last Update Milliseconds After Epoch" + }, + "rssi": { + "type": "number", + "description": "Receive Signal Strength of Transmitter" + }, + "unfiltered": { + "type": "number", + "description": "Most Recent Raw Unfiltered Glucose" + }, + "filtered": { + "type": "number", + "description": "Most Recent Raw Filtered Glucose" + }, + "noise": { + "type": "number", + "description": "Calculated Noise Value - 1=Clean, 2=Light, 3=Medium, 4=Heavy" + }, + "noiseString": { + "type": "number", + "description": "Noise Value String" + }, + "slope": { + "type": "number", + "description": "Calibration Slope Value" + }, + "intercept": { + "type": "number", + "description": "Calibration Intercept Value" + }, + "calType": { + "type": "string", + "description": "Algorithm Used to Calculate Calibration Values" + }, + "lastCalibrationDate": { + "type": "number", + "description": "Most Recent Calibration Milliseconds After Epoch" + }, + "sessionStart": { + "type": "number", + "description": "Sensor Session Start Milliseconds After Epoch" + }, + "batteryTimestamp": { + "type": "number", + "description": "Most Recent Batter Status Read Milliseconds After Epoch" + }, + "voltagea": { + "type": "number", + "description": "Voltage of Battery A" + }, + "voltageb": { + "type": "number", + "description": "Voltage of Battery B" + }, + "temperature": { + "type": "number", + "description": "Transmitter Temperature" + }, + "resistance": { + "type": "number", + "description": "Sensor Resistance" + } + } + }, + "Treatment": { + "properties": { + "_id": { + "type": "string", + "description": "Internally assigned id." + }, + "eventType": { + "type": "string", + "description": "The type of treatment event." + }, + "created_at": { + "type": "string", + "description": "The date of the event, might be set retroactively ." + }, + "glucose": { + "type": "string", + "description": "Current glucose." + }, + "glucoseType": { + "type": "string", + "description": "Method used to obtain glucose, Finger or Sensor." + }, + "carbs": { + "type": "number", + "description": " Amount of carbs consumed in grams." + }, + "protein": { + "type": "number", + "description": " Amount of protein consumed in grams." + }, + "fat": { + "type": "number", + "description": " Amount of fat consumed in grams." + }, + "insulin": { + "type": "number", + "description": "Amount of insulin, if any." + }, + "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." + }, + "enteredBy": { + "type": "string", + "description": "Who entered the treatment." + } + } + }, + "Treatments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Treatment" + } + }, + "Profile": { + "properties": { + "sens": { + "type": "integer", + "description": "Internally assigned id" + }, + "dia": { + "type": "integer", + "description": "Internally assigned id" + }, + "carbratio": { + "type": "integer", + "description": "Internally assigned id" + }, + "carbs_hr": { + "type": "integer", + "description": "Internally assigned id" + }, + "_id": { + "type": "string", + "description": "Internally assigned id" + } + } + }, + "Status": { + "properties": { + "apiEnabled": { + "type": "boolean", + "description": "Whether or not the REST API is enabled." + }, + "careportalEnabled": { + "type": "boolean", + "description": "Whether or not the careportal is enabled in the API." + }, + "head": { + "type": "string", + "description": "The git identifier for the running instance of the app." + }, + "name": { + "type": "string", + "description": "Nightscout (static)" + }, + "version": { + "type": "string", + "description": "The version label of the app." + }, + "settings": { + "$ref": "#/components/schemas/Settings" + }, + "extendedSettings": { + "$ref": "#/components/schemas/ExtendedSettings" + } + } + }, + "Settings": { + "properties": { + "units": { + "type": "string", + "description": "Default units for glucose measurements across the server." + }, + "timeFormat": { + "type": "string", + "description": "Default time format", + "enum": [ + 12, + 24 + ] + }, + "customTitle": { + "type": "string", + "description": "Default custom title to be displayed system wide." + }, + "nightMode": { + "type": "boolean", + "description": "Should Night mode be enabled by default?" + }, + "theme": { + "type": "string", + "description": "Default theme to be displayed system wide, `default`, `colors`, `colorblindfriendly`." + }, + "language": { + "type": "string", + "description": "Default language code to be used system wide" + }, + "showPlugins": { + "type": "string", + "description": "Plugins that should be shown by default" + }, + "showRawbg": { + "type": "string", + "description": "If Raw BG is enabled when should it be shown? `never`, `always`, `noise`" + }, + "alarmTypes": { + "type": "array", + "items": { + "type": "string" + }, + "enum": [ + "simple", + "predict" + ], + "description": "Enabled alarm types, can be multiple" + }, + "alarmUrgentHigh": { + "type": "boolean", + "description": "Enable/Disable client-side Urgent High alarms by default, for use with `simple` alarms." + }, + "alarmHigh": { + "type": "boolean", + "description": "Enable/Disable client-side High alarms by default, for use with `simple` alarms." + }, + "alarmLow": { + "type": "boolean", + "description": "Enable/Disable client-side Low alarms by default, for use with `simple` alarms." + }, + "alarmUrgentLow": { + "type": "boolean", + "description": "Enable/Disable client-side Urgent Low alarms by default, for use with `simple` alarms." + }, + "alarmTimeagoWarn": { + "type": "boolean", + "description": "Enable/Disable client-side stale data alarms by default." + }, + "alarmTimeagoWarnMins": { + "type": "number", + "description": "Number of minutes before a stale data warning is generated." + }, + "alarmTimeagoUrgent": { + "type": "boolean", + "description": "Enable/Disable client-side urgent stale data alarms by default." + }, + "alarmTimeagoUrgentMins": { + "type": "number", + "description": "Number of minutes before a stale data warning is generated." + }, + "enable": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features" + }, + "thresholds": { + "$ref": "#/components/schemas/Threshold" + } + } + }, + "Threshold": { + "properties": { + "bg_high": { + "type": "integer", + "description": "High BG range." + }, + "bg_target_top": { + "type": "integer", + "description": "Top of target range." + }, + "bg_target_bottom": { + "type": "integer", + "description": "Bottom of target range." + }, + "bg_low": { + "type": "integer", + "description": "Low BG range." + } + } + }, + "ExtendedSettings": { + "description": "Extended settings of client side plugins" + }, + "MongoQuery": { + "description": "Mongo Query object." + }, + "Error": { + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "fields": { + "type": "object" + } + } + }, + "DeleteStatus": { + "properties": { + "n": { + "type": "integer", + "format": "int32", + "description": "Number of records deleted" + }, + "optime": { + "$ref": "#/components/schemas/optime" + }, + "electionId": { + "type": "string", + "description": "Election id of operation" + }, + "ok": { + "type": "integer", + "format": "int32", + "description": "Status of whether delete was successful" + }, + "operationTime": { + "type": "string", + "description": "Time delete operation was executed" + }, + "$clusterTime": { + "type": "string", + "description": "Information about execution time in cluster environment" + } + } + }, + "optime": { + "properties": { + "ts": { + "type": "string", + "description": "Time the operation started" + }, + "t": { + "type": "integer", + "format": "int32", + "description": "Time the operation took to complete" + } + } + } + } + } +} diff --git a/lib/server/swagger.yaml b/lib/server/swagger.yaml new file mode 100755 index 00000000000..ef20b655b68 --- /dev/null +++ b/lib/server/swagger.yaml @@ -0,0 +1,1084 @@ +openapi: 3.0.0 +servers: + - url: /api/v1 +info: + title: Nightscout API + description: Own your DData with the Nightscout API + version: 14.2.3 + license: + name: AGPL 3 + url: 'https://www.gnu.org/licenses/agpl.txt' +security: + - api_secret: [] + token_in_url: [] + jwtoken: [] +paths: + '/entries/{spec}': + get: + summary: All Entries matching query + description: | + The Entries endpoint returns information about the + Nightscout entries. + parameters: + - name: spec + in: path + description: | + entry id, such as `55cf81bc436037528ec75fa5` or a type filter such + as `sgv`, `mbg`, etc. + required: true + schema: + type: string + default: sgv + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + tags: + - Entries + responses: + '200': + description: An array of entries + content: + application/json: + schema: + $ref: '#/components/schemas/Entries' + default: + description: Entries + content: + application/json: + schema: + $ref: '#/components/schemas/Entries' + '/slice/{storage}/{field}/{type}/{prefix}/{regex}': + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: storage + in: path + description: >- + Prefix to use in constructing a prefix-based regex, default is + `entries`. + required: true + schema: + type: string + default: entries + - name: field + in: path + description: >- + Name of the field to use Regex against in query object, default is + `dateString`. + required: true + schema: + type: string + default: dateString + - name: type + in: path + description: 'The type field to search against, default is sgv.' + required: true + schema: + type: string + default: sgv + - name: prefix + in: path + description: Prefix to use in constructing a prefix-based regex. + required: true + schema: + type: string + default: '2015' + - name: regex + in: path + description: > + Tail part of regexp to use in expanding/construccting a query + object. + + Regexp also has bash-style brace and glob expansion applied to it, + + creating ways to search for modal times of day, perhaps using + + something like this syntax: `T{15..17}:.*`, this would search for + + all records from 3pm to 5pm. + required: true + schema: + type: string + default: .* + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + tags: + - Entries + responses: + '200': + description: An array of entries + content: + application/json: + schema: + $ref: '#/components/schemas/Entries' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '/echo/{storage}/{spec}': + get: + summary: View generated Mongo Query object + description: | + Information about the mongo query object created by the query. + parameters: + - name: storage + in: path + description: | + `entries`, or `treatments` to select the storage layer. + required: true + schema: + type: string + default: sgv + - name: spec + in: path + description: | + entry id, such as `55cf81bc436037528ec75fa5` or a type filter such + as `sgv`, `mbg`, etc. + This parameter is optional. + required: true + schema: + type: string + default: sgv + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + tags: + - Entries + - Debug + responses: + '200': + description: An array of entries + content: + application/json: + schema: + $ref: '#/components/schemas/MongoQuery' + '/times/echo/{prefix}/{regex}': + get: + summary: Echo the query object to be used. + description: Echo debug information about the query object constructed. + parameters: + - name: prefix + in: path + description: Prefix to use in constructing a prefix-based regex. + required: true + schema: + type: string + - name: regex + in: path + description: > + Tail part of regexp to use in expanding/construccting a query + object. + + Regexp also has bash-style brace and glob expansion applied to it, + + creating ways to search for modal times of day, perhaps using + + something like this syntax: `T{15..17}:.*`, this would search for + + all records from 3pm to 5pm. + required: true + schema: + type: string + - name: find + in: query + description: >- + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + tags: + - Entries + - Debug + responses: + '200': + description: An array of entries + content: + application/json: + schema: + $ref: '#/components/schemas/MongoQuery' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '/times/{prefix}/{regex}': + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: prefix + in: path + description: Prefix to use in constructing a prefix-based regex. + required: true + schema: + type: string + - name: regex + in: path + description: > + Tail part of regexp to use in expanding/construccting a query + object. + + Regexp also has bash-style brace and glob expansion applied to it, + + creating ways to search for modal times of day, perhaps using + + something like this syntax: `T{15..17}:.*`, this would search for + + all records from 3pm to 5pm. + required: true + schema: + type: string + - name: find + in: query + description: >- + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + tags: + - Entries + responses: + '200': + description: An array of entries + content: + application/json: + schema: + $ref: '#/components/schemas/Entries' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /entries: + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: find + in: query + description: >- + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + tags: + - Entries + responses: + '200': + description: An array of entries + content: + application/json: + schema: + $ref: '#/components/schemas/Entries' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + tags: + - Entries + summary: Add new entries. + description: '' + operationId: addEntries + responses: + '200': + description: Rejected list of entries. Empty list is success. + '405': + description: Invalid input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Entries' + text/plain: + schema: + $ref: '#/components/schemas/Entries' + description: Entries to be uploaded. + required: true + delete: + tags: + - Entries + summary: Delete entries matching query. + description: 'Remove entries, same search syntax as GET.' + operationId: remove + parameters: + - name: find + in: query + description: >- + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + responses: + '200': + description: Empty list is success. + /treatments: + get: + summary: Treatments + description: >- + The Treatments endpoint returns information about the Nightscout + treatments. + tags: + - Treatments + parameters: + - name: find + in: query + description: >- + The query used to find entries, supports nested query syntax. + Examples `find[insulin][$gte]=3` `find[carb][$gte]=100` + `find[eventType]=Correction+Bolus` All find parameters are + interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + responses: + '200': + description: An array of treatments + content: + application/json: + schema: + $ref: '#/components/schemas/Treatments' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + tags: + - Treatments + summary: Add new treatments. + description: '' + operationId: addTreatments + responses: + '200': + description: Rejected list of treatments. Empty list is success. + '405': + description: Invalid input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Treatments' + description: Treatments to be uploaded. + required: true + delete: + tags: + - Treatments + summary: Delete treatments matching query. + description: 'Remove treatments, same search syntax as GET.' + operationId: remove + parameters: + - name: find + in: query + description: >- + The query used to find treatments to delete, + support nested query syntax, for example `find[insulin][$gte]=3`. + All find parameters are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of entries to return. + required: false + schema: + type: number + responses: + '200': + description: Empty list is success. + '/treatments/{spec}': + delete: + summary: Delete treatments record with id provided in spec + description: | + The Treatments endpoint returns information about the + Nightscout devicestatus records. + parameters: + - name: spec + in: path + description: | + treatment id, such as `55cf81bc436037528ec75fa5` + required: true + schema: + type: string + tags: + - Treatments + responses: + '200': + description: A status record of the delete. + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteStatus' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /profile: + get: + summary: Profile + description: >- + The Profile endpoint returns information about the Nightscout Treatment + Profiles. + tags: + - Profile + responses: + '200': + description: An array of treatments + content: + application/json: + schema: + $ref: '#/components/schemas/Profile' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /status: + get: + summary: Status + description: 'Server side status, default settings and capabilities.' + tags: + - Status + responses: + '200': + description: Server capabilities and status. + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /devicestatus/: + get: + summary: All Devicestatuses matching query + description: >- + The Devicestatus endpoint returns information about the Nightscout + devicestatus records. + parameters: + - name: find + in: query + description: >- + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + - name: count + in: query + description: Number of devicestatus records to return. + required: false + schema: + type: number + tags: + - Devicestatus + responses: + '200': + description: An array of devicestatus entries + content: + application/json: + schema: + $ref: '#/components/schemas/Devicestatuses' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + tags: + - Devicestatus + summary: Add new devicestatus records. + description: '' + operationId: addDevicestatuses + responses: + '200': + description: Rejected list of device statuses. Empty list is success. + '405': + description: Invalid input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Devicestatuses' + description: Device statuses to be uploaded. + required: true + delete: + summary: Delete all Devicestatus records matching query + description: | + The Devicestatus endpoint returns information about the + Nightscout devicestatus records. + parameters: + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[created_at][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + schema: + type: string + tags: + - Devicestatus + responses: + '200': + description: A status record of the delete. + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteStatus' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '/devicestatus/{spec}': + delete: + summary: Delete devicestatus record with id provided in spec + description: | + The Devicestatus endpoint returns information about the + Nightscout devicestatus records. + parameters: + - name: spec + in: path + description: | + entry id, such as `55cf81bc436037528ec75fa5` + required: true + schema: + type: string + tags: + - Devicestatus + responses: + '200': + description: A status record of the delete. + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteStatus' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + securitySchemes: + api_secret: + type: apiKey + name: api-secret + in: header + description: The hash of the API_SECRET env var + token_in_url: + type: apiKey + name: token + in: query + description: >- + Add token as query item in the URL. You can manage access Token in + `/admin`. This uses json webtokens. + jwtoken: + type: http + scheme: bearer + description: Use this if you know the temporary json webtoken. + bearerFormat: JWT + schemas: + Entry: + properties: + type: + type: string + description: 'sgv, mbg, cal, etc' + dateString: + type: string + description: 'dateString, MUST be ISO `8601` format date parseable by Javascript Date()' + date: + type: number + description: Epoch + sgv: + type: number + description: The glucose reading. (only available for sgv types) + direction: + type: string + description: >- + Direction of glucose trend reported by CGM. (only available for sgv + types) + noise: + type: number + description: Noise level at time of reading. (only available for sgv types) + filtered: + type: number + description: >- + The raw filtered value directly from CGM transmitter. (only + available for sgv types) + unfiltered: + type: number + description: >- + The raw unfiltered value directly from CGM transmitter. (only + available for sgv types) + rssi: + type: number + description: >- + The signal strength from CGM transmitter. (only available for sgv + types) + Entries: + type: array + items: + $ref: '#/components/schemas/Entry' + Devicestatus: + required: + - device + - created_at + properties: + device: + type: string + description: 'Device type and hostname for example openaps://hostname' + created_at: + type: string + description: 'dateString, MUST be ISO `8601` format date parseable by Javascript Date()' + openaps: + type: string + description: 'OpenAPS devicestatus record - TODO: Fill Out Details' + loop: + type: string + description: 'Loop devicestatus record - TODO: Fill Out Details' + pump: + $ref: '#/components/schemas/pump' + uploader: + $ref: '#/components/schemas/uploader' + xdripjs: + $ref: '#/components/schemas/xdripjs' + Devicestatuses: + type: array + items: + $ref: '#/components/schemas/Devicestatus' + pump: + properties: + clock: + type: string + description: 'dateString, MUST be ISO `8601` format date parseable by Javascript Date()' + battery: + $ref: '#/components/schemas/pumpbattery' + reservoir: + type: number + description: Amount of insulin remaining in pump reservoir + status: + $ref: '#/components/schemas/pumpstatus' + pumpbattery: + properties: + status: + type: string + description: Pump Battery Status String + voltage: + type: number + description: Pump Battery Voltage Level + pumpstatus: + properties: + status: + type: string + description: Pump Status String + bolusing: + type: boolean + description: Is Pump Bolusing + suspended: + type: boolean + description: Is Pump Suspended + timestamp: + type: string + description: 'dateString, MUST be ISO `8601` format date parseable by Javascript Date()' + uploader: + properties: + batteryVoltage: + type: number + description: Uploader Device Battery Voltage + battery: + type: number + description: Uploader Device Battery Percentage Charge Remaining + xdripjs: + properties: + state: + type: number + description: CGM Sensor Session State Code + stateString: + type: string + description: CGM Sensor Session State String + stateStringShort: + type: string + description: CGM Sensor Session State Short String + txId: + type: string + description: CGM Transmitter ID + txStatus: + type: number + description: CGM Transmitter Status + txStatusString: + type: string + description: CGM Transmitter Status String + txStatusStringShort: + type: string + description: CGM Transmitter Status Short String + txActivation: + type: number + description: CGM Transmitter Activation Milliseconds After Epoch + mode: + type: string + description: 'Mode xdrip-js Application Operationg: expired, not expired, etc.' + timestamp: + type: number + description: Last Update Milliseconds After Epoch + rssi: + type: number + description: Receive Signal Strength of Transmitter + unfiltered: + type: number + description: Most Recent Raw Unfiltered Glucose + filtered: + type: number + description: Most Recent Raw Filtered Glucose + noise: + type: number + description: 'Calculated Noise Value - 1=Clean, 2=Light, 3=Medium, 4=Heavy' + noiseString: + type: number + description: Noise Value String + slope: + type: number + description: Calibration Slope Value + intercept: + type: number + description: Calibration Intercept Value + calType: + type: string + description: Algorithm Used to Calculate Calibration Values + lastCalibrationDate: + type: number + description: Most Recent Calibration Milliseconds After Epoch + sessionStart: + type: number + description: Sensor Session Start Milliseconds After Epoch + batteryTimestamp: + type: number + description: Most Recent Batter Status Read Milliseconds After Epoch + voltagea: + type: number + description: Voltage of Battery A + voltageb: + type: number + description: Voltage of Battery B + temperature: + type: number + description: Transmitter Temperature + resistance: + type: number + description: Sensor Resistance + Treatment: + properties: + _id: + type: string + description: Internally assigned id. + eventType: + type: string + description: The type of treatment event. + created_at: + type: string + description: 'The date of the event, might be set retroactively .' + glucose: + type: string + description: Current glucose. + glucoseType: + type: string + description: 'Method used to obtain glucose, Finger or Sensor.' + carbs: + type: number + description: Amount of carbs consumed in grams. + protein: + type: number + description: Amount of protein consumed in grams. + fat: + type: number + description: Amount of fat consumed in grams. + insulin: + type: number + description: 'Amount of insulin, if any.' + 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. + enteredBy: + type: string + description: Who entered the treatment. + Treatments: + type: array + items: + $ref: '#/components/schemas/Treatment' + Profile: + properties: + sens: + type: integer + description: Internally assigned id + dia: + type: integer + description: Internally assigned id + carbratio: + type: integer + description: Internally assigned id + carbs_hr: + type: integer + description: Internally assigned id + _id: + type: string + description: Internally assigned id + Status: + properties: + apiEnabled: + type: boolean + description: Whether or not the REST API is enabled. + careportalEnabled: + type: boolean + description: Whether or not the careportal is enabled in the API. + head: + type: string + description: The git identifier for the running instance of the app. + name: + type: string + description: Nightscout (static) + version: + type: string + description: The version label of the app. + settings: + $ref: '#/components/schemas/Settings' + extendedSettings: + $ref: '#/components/schemas/ExtendedSettings' + Settings: + properties: + units: + type: string + description: Default units for glucose measurements across the server. + timeFormat: + type: string + description: Default time format + enum: + - 12 + - 24 + customTitle: + type: string + description: Default custom title to be displayed system wide. + nightMode: + type: boolean + description: Should Night mode be enabled by default? + theme: + type: string + description: >- + Default theme to be displayed system wide, `default`, `colors`, + `colorblindfriendly`. + language: + type: string + description: Default language code to be used system wide + showPlugins: + type: string + description: Plugins that should be shown by default + showRawbg: + type: string + description: >- + If Raw BG is enabled when should it be shown? `never`, `always`, + `noise` + alarmTypes: + type: array + items: + type: string + enum: + - simple + - predict + description: 'Enabled alarm types, can be multiple' + alarmUrgentHigh: + type: boolean + description: >- + Enable/Disable client-side Urgent High alarms by default, for use + with `simple` alarms. + alarmHigh: + type: boolean + description: >- + Enable/Disable client-side High alarms by default, for use with + `simple` alarms. + alarmLow: + type: boolean + description: >- + Enable/Disable client-side Low alarms by default, for use with + `simple` alarms. + alarmUrgentLow: + type: boolean + description: >- + Enable/Disable client-side Urgent Low alarms by default, for use + with `simple` alarms. + alarmTimeagoWarn: + type: boolean + description: Enable/Disable client-side stale data alarms by default. + alarmTimeagoWarnMins: + type: number + description: Number of minutes before a stale data warning is generated. + alarmTimeagoUrgent: + type: boolean + description: Enable/Disable client-side urgent stale data alarms by default. + alarmTimeagoUrgentMins: + type: number + description: Number of minutes before a stale data warning is generated. + enable: + type: array + items: + type: string + description: Enabled features + thresholds: + $ref: '#/components/schemas/Threshold' + Threshold: + properties: + bg_high: + type: integer + description: High BG range. + bg_target_top: + type: integer + description: Top of target range. + bg_target_bottom: + type: integer + description: Bottom of target range. + bg_low: + type: integer + description: Low BG range. + ExtendedSettings: + description: Extended settings of client side plugins + MongoQuery: + description: Mongo Query object. + Error: + properties: + code: + type: integer + format: int32 + message: + type: string + fields: + type: object + DeleteStatus: + properties: + 'n': + type: integer + format: int32 + description: Number of records deleted + optime: + $ref: '#/components/schemas/optime' + electionId: + type: string + description: Election id of operation + ok: + type: integer + format: int32 + description: Status of whether delete was successful + operationTime: + type: string + description: Time delete operation was executed + $clusterTime: + type: string + description: Information about execution time in cluster environment + optime: + properties: + ts: + type: string + description: Time the operation started + t: + type: integer + format: int32 + description: Time the operation took to complete diff --git a/lib/server/treatments.js b/lib/server/treatments.js new file mode 100644 index 00000000000..a9107ea99b2 --- /dev/null +++ b/lib/server/treatments.js @@ -0,0 +1,291 @@ +'use strict'; + +var _ = require('lodash'); +var async = require('async'); +var moment = require('moment'); +var find_options = require('./query'); + +function storage (env, ctx) { + var ObjectID = require('mongodb').ObjectID; + + function create (objOrArray, fn) { + + function done (err, result) { + ctx.bus.emit('data-received'); + fn(err, result); + } + + if (_.isArray(objOrArray)) { + var allDocs = []; + var errs = []; + async.eachSeries(objOrArray, function (obj, callback) { + upsert(obj, function upserted (err, docs) { + allDocs = allDocs.concat(docs); + errs.push(err); + callback(err, docs) + }); + }, function () { + errs = _.compact(errs); + done(errs.length > 0 ? errs : null, allDocs); + }); + } else { + upsert(objOrArray, function upserted (err, docs) { + done(err, docs); + }); + } + + + } + + function upsert (obj, fn) { + + var results = prepareData(obj); + + var query = { + created_at: results.created_at + , eventType: obj.eventType + }; + + api( ).update(query, obj, {upsert: true}, function complete (err, updateResults) { + + if (err) console.error('Problem upserting treatment', err); + + if (!err) { + if (updateResults.result.upserted) { + obj._id = updateResults.result.upserted[0]._id + } + } + + // TODO document this feature + if (!err && obj.preBolus) { + //create a new object to insert copying only the needed fields + var pbTreat = { + created_at: (new Date(new Date(results.created_at).getTime() + (obj.preBolus * 60000))).toISOString(), + eventType: obj.eventType, + carbs: results.preBolusCarbs + }; + + if (obj.notes) { + pbTreat.notes = obj.notes; + } + + query.created_at = pbTreat.created_at; + api( ).update(query, pbTreat, {upsert: true}, function pbComplete (err, updateResults) { + + if (!err) { + if (updateResults.result.upserted) { + pbTreat._id = updateResults.result.upserted[0]._id + } + } + + var treatments = _.compact([obj, pbTreat]); + + ctx.bus.emit('data-update', { + type: 'treatments', + op: 'update', + changes: ctx.ddata.processRawDataForRuntime(treatments) + }); + + fn(err, treatments); + }); + } else { + + ctx.bus.emit('data-update', { + type: 'treatments', + op: 'update', + changes: ctx.ddata.processRawDataForRuntime([obj]) + }); + + fn(err, [obj]); + } + + }); + } + + function list (opts, fn) { + + function limit ( ) { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + return limit.call(api() + .find(query_for(opts)) + .sort(opts && opts.sort || {created_at: -1}), opts) + .toArray(fn); + } + + function query_for (opts) { + return find_options(opts, storage.queryOpts); + } + + function remove (opts, fn) { + return api( ).remove(query_for(opts), function (err, stat) { + //TODO: this is triggering a read from Mongo, we can do better + //console.log('Treatment removed', opts); // , stat); + + ctx.bus.emit('data-update', { + type: 'treatments', + op: 'remove', + count: stat.result.n, + changes: opts.find._id + }); + + ctx.bus.emit('data-received'); + fn(err, stat); + }); + } + + function save (obj, fn) { + obj._id = new ObjectID(obj._id); + prepareData(obj); + + function saved (err, created) { + if (!err) { + // console.log('Treatment updated', created); + + ctx.ddata.processRawDataForRuntime(obj); + + ctx.bus.emit('data-update', { + type: 'treatments', + op: 'update', + changes: ctx.ddata.processRawDataForRuntime([obj]) + }); + + } + if (err) console.error('Problem saving treating', err); + + fn(err, created); + } + + api().save(obj, saved); + + ctx.bus.emit('data-received'); + } + + function api ( ) { + return ctx.store.collection(env.treatments_collection); + } + + api.list = list; + api.create = create; + api.query_for = query_for; + api.indexedFields = [ + 'created_at' + , 'eventType' + , 'insulin' + , 'carbs' + , 'glucose' + , 'enteredBy' + , 'boluscalc.foods._id' + , 'notes' + , 'NSCLIENT_ID' + , 'percent' + , 'absolute' + , 'duration' + , { 'eventType' : 1, 'duration' : 1, 'created_at' : 1 } + ]; + + api.remove = remove; + api.save = save; + api.aggregate = require('./aggregate')({ }, api); + + return api; +} + +function prepareData(obj) { + + // Convert all dates to UTC dates + + // TODO remove this -> must not create new date if missing + const d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment(); + obj.created_at = d.toISOString(); + + var results = { + created_at: obj.created_at + , preBolusCarbs: '' + }; + + const offset = d.utcOffset(); + obj.utcOffset = offset; + results.offset = offset; + + obj.glucose = Number(obj.glucose); + obj.targetTop = Number(obj.targetTop); + obj.targetBottom = Number(obj.targetBottom); + obj.carbs = Number(obj.carbs); + obj.insulin = Number(obj.insulin); + obj.duration = Number(obj.duration); + obj.percent = Number(obj.percent); + obj.absolute = Number(obj.absolute); + obj.relative = Number(obj.relative); + obj.preBolus = Number(obj.preBolus); + + //NOTE: the eventTime is sent by the client, but deleted, we only store created_at + var eventTime; + if (obj.eventTime) { + eventTime = new Date(obj.eventTime).toISOString(); + results.created_at = eventTime; + } + + obj.created_at = results.created_at; + if (obj.preBolus && obj.preBolus !== 0 && obj.carbs) { + results.preBolusCarbs = obj.carbs; + delete obj.carbs; + } + + if (obj.eventType === 'Announcement') { + obj.isAnnouncement = true; + } + + // clean data + delete obj.eventTime; + + function deleteIfEmpty (field) { + if (!obj[field] || obj[field] === 0) { + delete obj[field]; + } + } + + function deleteIfNaN (field) { + if (isNaN(obj[field])) { + delete obj[field]; + } + } + + deleteIfEmpty('targetTop'); + deleteIfEmpty('targetBottom'); + deleteIfEmpty('carbs'); + deleteIfEmpty('insulin'); + deleteIfEmpty('percent'); + deleteIfEmpty('relative'); + deleteIfEmpty('notes'); + deleteIfEmpty('preBolus'); + + deleteIfNaN('absolute'); + deleteIfNaN('duration'); + + if (obj.glucose === 0 || isNaN(obj.glucose)) { + delete obj.glucose; + delete obj.glucoseType; + delete obj.units; + } + + return results; +} + +storage.queryOpts = { + walker: { + insulin: parseInt + , carbs: parseInt + , glucose: parseInt + , notes: find_options.parseRegEx + , eventType: find_options.parseRegEx + , enteredBy: find_options.parseRegEx + } + , dateField: 'created_at' +}; + +module.exports = storage; diff --git a/lib/server/websocket.js b/lib/server/websocket.js new file mode 100644 index 00000000000..95da7d906ce --- /dev/null +++ b/lib/server/websocket.js @@ -0,0 +1,603 @@ +'use strict'; + +var times = require('../times'); +var calcData = require('../data/calcdelta'); +var ObjectID = require('mongodb').ObjectID; +const forwarded = require('forwarded-for'); + +function getRemoteIP (req) { + const address = forwarded(req, req.headers); + return address.ip; +} + +function init (env, ctx, server) { + + function websocket () { + return websocket; + } + + //var log_yellow = '\x1B[33m'; + var log_green = '\x1B[32m'; + var log_magenta = '\x1B[35m'; + var log_reset = '\x1B[0m'; + var LOG_WS = log_green + 'WS: ' + log_reset; + var LOG_DEDUP = log_magenta + 'DEDUPE: ' + log_reset; + + var io; + var watchers = 0; + var lastData = {}; + var lastProfileSwitch = null; + + // TODO: this would be better to have somehow integrated/improved + var supportedCollections = { + 'treatments': env.treatments_collection + , 'entries': env.entries_collection + , 'devicestatus': env.devicestatus_collection + , 'profile': env.profile_collection + , 'food': env.food_collection + , 'activity': env.activity_collection + }; + + // 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; + const vString = '' + env.version; + const verParse = vString.split('.'); + if (verParse) { + versionNum = 10000 * Number(verParse[0]) + 100 * Number(verParse[1]) + 1 * Number(verParse[2]); + } + + var apiEnabled = env.enclave.isApiKeySet(); + + var activeProfile = ctx.ddata.lastProfileFromSwitch; + + var info = { + status: 'ok' + , name: env.name + , version: env.version + , versionNum: versionNum + , serverTime: new Date().toISOString() + , apiEnabled: apiEnabled + , careportalEnabled: apiEnabled && env.settings.enable.indexOf('careportal') > -1 + , boluscalcEnabled: apiEnabled && env.settings.enable.indexOf('boluscalc') > -1 + , settings: env.settings + , extendedSettings: ctx.plugins && ctx.plugins.extendedClientSettings ? ctx.plugins.extendedClientSettings(env.extendedSettings) : {} + }; + + if (activeProfile) { + info.activeProfile = activeProfile; + } + return info; + } + + function start () { + io = require('socket.io')({ + 'log level': 0 + }).listen(server, { + //these only effect the socket.io.js file that is sent to the client, but better than nothing + // compat with v2 client + allowEIO3: true + , 'browser client minification': true + , 'browser client etag': true + , 'browser client gzip': false + , 'perMessageDeflate': { + threshold: 512 + } + , transports: ["polling", "websocket"] + , httpCompression: { + threshold: 512 + } + }); + + ctx.bus.on('teardown', function serverTeardown () { + Object.keys(io.sockets.sockets).forEach(function(s) { + io.sockets.sockets[s].disconnect(true); + }); + io.close(); + }); + + ctx.bus.on('data-processed', function() { + update(); + }); + + } + + 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) { + return callback(err, { + read: false + , write: false + , write_treatment: false + , error: true + }); + } + + return callback(null, { + read: ctx.authorization.checkMultiple('api:*:read', result.shiros) + , write: ctx.authorization.checkMultiple('api:*:create,update,delete', result.shiros) + , write_treatment: ctx.authorization.checkMultiple('api:treatments:create,update,delete', result.shiros) + }); + }); + } + + function emitData (delta) { + if (lastData.cals) { + // console.log(LOG_WS + 'running websocket.emitData', ctx.ddata.lastUpdated); + if (lastProfileSwitch !== ctx.ddata.lastProfileFromSwitch) { + // console.log(LOG_WS + 'profile switch detected OLD: ' + lastProfileSwitch + ' NEW: ' + ctx.ddata.lastProfileFromSwitch); + delta.status = status(ctx.ddata.profiles); + lastProfileSwitch = ctx.ddata.lastProfileFromSwitch; + } + io.to('DataReceivers').compress(true).emit('dataUpdate', delta); + } + } + + function listeners () { + io.sockets.on('connection', function onConnection (socket) { + var socketAuthorization = null; + var clientType = null; + var timeDiff; + var history; + + const remoteIP = getRemoteIP(socket.request); + console.log(LOG_WS + 'Connection from client ID: ', socket.client.id, ' IP: ', remoteIP); + + io.emit('clients', ++watchers); + socket.on('disconnect', function onDisconnect () { + io.emit('clients', --watchers); + console.log(LOG_WS + 'Disconnected client ID: ', socket.client.id); + }); + + function checkConditions (action, data) { + var collection = supportedCollections[data.collection]; + if (!collection) { + console.log('WS dbUpdate/dbAdd call: ', 'Wrong collection', data); + return { result: 'Wrong collection' }; + } + + if (!socketAuthorization) { + console.log('WS dbUpdate/dbAdd call: ', 'Not authorized', data); + return { result: 'Not authorized' }; + } + + if (data.collection === 'treatments') { + if (!socketAuthorization.write_treatment) { + console.log('WS dbUpdate/dbAdd call: ', 'Not permitted', data); + return { result: 'Not permitted' }; + } + } else { + if (!socketAuthorization.write) { + console.log('WS dbUpdate call: ', 'Not permitted', data); + return { result: 'Not permitted' }; + } + } + + if (action === 'dbUpdate' && !data._id) { + console.log('WS dbUpdate/dbAddnot sure abou documentati call: ', 'Missing _id', data); + return { result: 'Missing _id' }; + } + + return null; + } + + socket.on('loadRetro', function loadRetro (opts, callback) { + if (callback) { + callback({ result: 'success' }); + } + //TODO: use opts to only send delta for retro data + socket.compress(true).emit('retroUpdate', { devicestatus: lastData.devicestatus }); + console.info('sent retroUpdate', opts); + }); + + // dbUpdate message + // { + // collection: treatments + // _id: 'some mongo record id' + // data: { + // field_1: new_value, + // field_2: another_value + // } + // } + socket.on('dbUpdate', function dbUpdate (data, callback) { + console.log(LOG_WS + 'dbUpdate client ID: ', socket.client.id, ' data: ', data); + var collection = supportedCollections[data.collection]; + + var check = checkConditions('dbUpdate', data); + if (check) { + if (callback) { + callback(check); + } + return; + } + var id; + try { + id = new ObjectID(data._id); + } catch (err) { + console.error(err); + id = new ObjectID(); + } + + ctx.store.collection(collection).update({ '_id': id } + , { $set: data.data } + , function(err, results) { + + if (!err) { + ctx.store.collection(collection).findOne({ '_id': id } + , function(err, results) { + console.log('Got results', results); + if (!err && results !== null) { + ctx.bus.emit('data-update', { + type: data.collection + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime([results]) + }); + } + }); + } + } + ); + + if (callback) { + callback({ result: 'success' }); + } + ctx.bus.emit('data-received'); + }); + + // dbUpdateUnset message + // { + // collection: treatments + // _id: 'some mongo record id' + // data: { + // field_1: 1, + // field_2: 1 + // } + // } + socket.on('dbUpdateUnset', function dbUpdateUnset (data, callback) { + console.log(LOG_WS + 'dbUpdateUnset client ID: ', socket.client.id, ' data: ', data); + var collection = supportedCollections[data.collection]; + + var check = checkConditions('dbUpdate', data); + if (check) { + if (callback) { + callback(check); + } + return; + } + + var objId = new ObjectID(data._id); + ctx.store.collection(collection).update({ '_id': objId }, { $unset: data.data } + , function(err, results) { + + if (!err) { + ctx.store.collection(collection).findOne({ '_id': objId } + , function(err, results) { + console.log('Got results', results); + if (!err && results !== null) { + ctx.bus.emit('data-update', { + type: data.collection + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime([results]) + }); + } + }); + } + }); + + if (callback) { + callback({ result: 'success' }); + } + ctx.bus.emit('data-received'); + }); + + // dbAdd message + // { + // collection: treatments + // data: { + // field_1: new_value, + // field_2: another_value + // } + // } + socket.on('dbAdd', function dbAdd (data, callback) { + console.log(LOG_WS + 'dbAdd client ID: ', socket.client.id, ' data: ', data); + var collection = supportedCollections[data.collection]; + var maxtimediff = times.secs(2).msecs; + + var check = checkConditions('dbAdd', data); + if (check) { + if (callback) { + callback(check); + } + return; + } + + if (data.collection === 'treatments' && !('eventType' in data.data)) { + data.data.eventType = ''; + } + if (!('created_at' in data.data)) { + data.data.created_at = new Date().toISOString(); + } + + // treatments deduping + if (data.collection === 'treatments') { + var query; + if (data.data.NSCLIENT_ID) { + query = { NSCLIENT_ID: data.data.NSCLIENT_ID }; + } else { + query = { + created_at: data.data.created_at + , eventType: data.data.eventType + }; + } + + // try to find exact match + ctx.store.collection(collection).find(query).toArray(function findResult (err, array) { + if (err) { + console.error(err); + callback([]); + return; + } + + if (array.length > 0) { + console.log(LOG_DEDUP + 'Exact match'); + if (callback) { + callback([array[0]]); + } + return; + } + + var selected = false; + var query_similiar = { + created_at: { $gte: new Date(new Date(data.data.created_at).getTime() - maxtimediff).toISOString(), $lte: new Date(new Date(data.data.created_at).getTime() + maxtimediff).toISOString() } + }; + if (data.data.insulin) { + query_similiar.insulin = data.data.insulin; + selected = true; + } + if (data.data.carbs) { + query_similiar.carbs = data.data.carbs; + selected = true; + } + if (data.data.percent) { + query_similiar.percent = data.data.percent; + selected = true; + } + if (data.data.absolute) { + query_similiar.absolute = data.data.absolute; + selected = true; + } + if (data.data.duration) { + query_similiar.duration = data.data.duration; + selected = true; + } + if (data.data.NSCLIENT_ID) { + query_similiar.NSCLIENT_ID = data.data.NSCLIENT_ID; + selected = true; + } + // if none assigned add at least eventType + if (!selected) { + query_similiar.eventType = data.data.eventType; + } + // 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) { + 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); + ctx.store.collection(collection).update({ '_id': objId }, { $set: { created_at: data.data.created_at } }); + if (callback) { + callback([array[0]]); + } + ctx.bus.emit('data-received'); + return; + } + // if not found create new record + console.log(LOG_DEDUP + 'Adding new record'); + ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) { + if (err != null && err.message) { + console.log('treatments data insertion error: ', err.message); + return; + } + + ctx.bus.emit('data-update', { + type: data.collection + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime(doc.ops) + }); + + if (callback) { + callback(doc.ops); + } + ctx.bus.emit('data-received'); + }); + }); + }); + // devicestatus deduping + } else if (data.collection === 'devicestatus') { + var queryDev; + if (data.data.NSCLIENT_ID) { + queryDev = { NSCLIENT_ID: data.data.NSCLIENT_ID }; + } else { + queryDev = { + created_at: data.data.created_at + }; + } + + // try to find exact match + ctx.store.collection(collection).find(queryDev).toArray(function findResult (err, array) { + 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); + return; + } + + ctx.bus.emit('data-update', { + type: 'devicestatus' + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime(doc.ops) + }); + + if (callback) { + callback(doc.ops); + } + ctx.bus.emit('data-received'); + }); + } else { + ctx.store.collection(collection).insert(data.data, function insertResult (err, doc) { + if (err != null && err.message) { + console.log(data.collection + ' insertion error: ', err.message); + return; + } + + ctx.bus.emit('data-update', { + type: data.collection + , op: 'update' + , changes: ctx.ddata.processRawDataForRuntime(doc.ops) + }); + + if (callback) { + callback(doc.ops); + } + ctx.bus.emit('data-received'); + }); + } + }); + // dbRemove message + // { + // collection: treatments + // _id: 'some mongo record id' + // } + socket.on('dbRemove', function dbRemove (data, callback) { + console.log(LOG_WS + 'dbRemove client ID: ', socket.client.id, ' data: ', data); + var collection = supportedCollections[data.collection]; + + var check = checkConditions('dbUpdate', data); + if (check) { + if (callback) { + callback(check); + } + return; + } + + var objId = new ObjectID(data._id); + ctx.store.collection(collection).remove({ '_id': objId } + , function(err, stat) { + + if (!err) { + ctx.bus.emit('data-update', { + type: data.collection + , op: 'remove' + , count: stat.result.n + , changes: data._id + }); + + } + }); + + if (callback) { + callback({ result: 'success' }); + } + ctx.bus.emit('data-received'); + }); + + // Authorization message + // { + // client: 'web' | 'phone' | 'pump' + // , secret: 'secret_hash' + // [, history : history_in_hours ] + // [, status : true ] + // } + socket.on('authorize', function authorize (message, callback) { + const remoteIP = getRemoteIP(socket.request); + 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 + + if (socketAuthorization.read) { + socket.join('DataReceivers'); + + if (lastData && lastData.dataWithRecentStatuses) { + let data = lastData.dataWithRecentStatuses(); + + if (message.status) { + data.status = status(data.profiles); + } + + socket.emit('dataUpdate', data); + } + } + // console.log(LOG_WS + 'Authetication ID: ', socket.client.id, ' client: ', clientType, ' history: ' + history); + if (callback) { + callback(socketAuthorization); + } + }); + }); + }); + } + + function update () { + // console.log(LOG_WS + 'running websocket.update'); + if (lastData.sgvs) { + var delta = calcData(lastData, ctx.ddata); + if (delta.delta) { + // console.log('lastData full size', JSON.stringify(lastData).length,'bytes'); + // if (delta.sgvs) { console.log('patientData update size', JSON.stringify(delta).length,'bytes'); } + emitData(delta); + }; // else { console.log('delta calculation indicates no new data is present'); } + } + lastData = ctx.ddata.clone(); + }; + + start(); + listeners(); + + if (ctx.storageSocket) { + ctx.storageSocket.init(io); + } + + if (ctx.alarmSocket) { + ctx.alarmSocket.init(io); + } + + return websocket(); +} + +module.exports = init; diff --git a/lib/settings.js b/lib/settings.js index c489fa48b54..8de44d64a73 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -1,14 +1,17 @@ 'use strict'; var _ = require('lodash'); -var levels = require('./levels'); +var constants = require('./constants.json'); -function init ( ) { +function init () { var settings = { - units: 'mg/dL' + units: 'mg/dl' , timeFormat: 12 + , dayStart: 7.0 + , dayEnd: 21.0 , nightMode: false + , editMode: true , showRawbg: 'never' , customTitle: 'Nightscout' , theme: 'default' @@ -26,29 +29,118 @@ function init ( ) { , alarmTimeagoWarnMins: 15 , alarmTimeagoUrgent: true , alarmTimeagoUrgentMins: 30 + , alarmPumpBatteryLow: false , language: 'en' , scaleY: 'log' - , showPlugins: '' + , showPlugins: 'dbsize' + , showForecast: 'ar2' + , focusHours: 3 , heartbeat: 60 , baseURL: '' + , authDefaultRoles: 'readable' , thresholds: { bgHigh: 260 , bgTargetTop: 180 , bgTargetBottom: 80 , bgLow: 55 } + , insecureUseHttp: false + , secureHstsHeader: true + , secureHstsHeaderIncludeSubdomains: false + , secureHstsHeaderPreload: false + , secureCsp: false + , deNormalizeDates: false + , showClockDelta: false + , showClockLastTime: false + , frameUrl1: '' + , frameUrl2: '' + , frameUrl3: '' + , frameUrl4: '' + , frameUrl5: '' + , frameUrl6: '' + , frameUrl7: '' + , frameUrl8: '' + , frameName1: '' + , frameName2: '' + , frameName3: '' + , frameName4: '' + , frameName5: '' + , frameName6: '' + , frameName7: '' + , frameName8: '' + , authFailDelay: 5000 + , adminNotifiesEnabled: true + , obscured: '' + , obscureDeviceProvenance: '' + , authenticationPromptOnLoad: false }; + var secureSettings = [ + 'apnsKey' + , 'apnsKeyId' + , 'developerTeamId' + , 'userName' + , 'password' + , 'obscured' + , 'obscureDeviceProvenance' + ]; + var valueMappers = { - alarmUrgentHighMins: mapNumberArray + nightMode: mapTruthy + , alarmUrgentHigh: mapTruthy + , alarmUrgentHighMins: mapNumberArray + , alarmHigh: mapTruthy , alarmHighMins: mapNumberArray + , alarmLow: mapTruthy , alarmLowMins: mapNumberArray + , alarmUrgentLow: mapTruthy , alarmUrgentLowMins: mapNumberArray , alarmUrgentMins: mapNumberArray + , alarmTimeagoWarn: mapTruthy + , alarmTimeagoUrgent: mapTruthy , alarmWarnMins: mapNumberArray , timeFormat: mapNumber + , insecureUseHttp: mapTruthy + , secureHstsHeader: mapTruthy + , secureCsp: mapTruthy + , deNormalizeDates: mapTruthy + , showClockDelta: mapTruthy + , showClockLastTime: mapTruthy + , bgHigh: mapNumber + , bgLow: mapNumber + , bgTargetTop: mapNumber + , bgTargetBottom: mapNumber + , authFailDelay: mapNumber + , adminNotifiesEnabled: mapTruthy + , authenticationPromptOnLoad: mapTruthy }; + function filterObj(obj, secureKeys) { + if (obj && typeof obj === 'object') { + var allKeys = Object.keys(obj); + for (var i = 0 ; i < allKeys.length ; i++) { + var k = allKeys[i]; + if (secureKeys.includes(k)) { + delete obj[k]; + } else { + var value = obj[k]; + if ( typeof value === 'object') { + filterObj(value, secureKeys); + } + } + } + } + return obj; + } + + function filteredSettings(settingsObject) { + let so = _.cloneDeep(settingsObject); + if (so.obscured) { + so.enable = _.difference(so.enable, so.obscured); + } + return filterObj(so, secureSettings); + } + function mapNumberArray (value) { if (!value || _.isArray(value)) { return value; @@ -56,11 +148,11 @@ function init ( ) { if (isNaN(value)) { var rawValues = value && value.split(' ') || []; - return _.map(rawValues, function (num) { + return _.map(rawValues, function(num) { return isNaN(num) ? null : Number(num); }); } else { - return value; + return [Number(value)]; } } @@ -69,6 +161,11 @@ function init ( ) { return value; } + if (typeof value === 'string' && isNaN(value)) { + const decommaed = value.replace(',','.'); + if (!isNaN(decommaed)) { value = decommaed; } + } + if (isNaN(value)) { return value; } else { @@ -76,8 +173,14 @@ function init ( ) { } } + function mapTruthy (value) { + if (typeof value === 'string' && (value.toLowerCase() === 'on' || value.toLowerCase() === 'true')) { value = true; } + if (typeof value === 'string' && (value.toLowerCase() === 'off' || value.toLowerCase() === 'false')) { value = false; } + return value; + } + //TODO: getting sent in status.json, shouldn't be - settings.DEFAULT_FEATURES = ['delta', 'direction', 'upbat', 'errorcodes']; + settings.DEFAULT_FEATURES = ['bgnow', 'delta', 'direction', 'timeago', 'devicestatus', 'upbat', 'errorcodes', 'profile', 'bolus', 'dbsize', 'runtimestate', 'basal', 'careportal']; var wasSet = []; @@ -116,7 +219,9 @@ function init ( ) { function getAndPrepare (key) { var raw = accessor(nameFromKey(key, nameType)) || ''; var cleaned = decodeURIComponent(raw).toLowerCase(); - return cleaned ? cleaned.split(' ') : []; + cleaned = cleaned ? cleaned.split(' ') : []; + cleaned = _.filter(cleaned, function(e) { return e !== ""; } ); + return cleaned; } function enableIf (feature, condition) { @@ -126,18 +231,18 @@ function init ( ) { } function anyEnabled (features) { - return _.findIndex(features, function (feature) { + return _.findIndex(features, function(feature) { return enable.indexOf(feature) > -1; }) > -1; } - function prepareAlarmTypes ( ) { + function prepareAlarmTypes () { var alarmTypes = _.filter(getAndPrepare('alarmTypes'), function onlyKnownTypes (type) { return type === 'predict' || type === 'simple'; }); if (alarmTypes.length === 0) { - var thresholdWasSet = _.findIndex(wasSet, function (name) { + var thresholdWasSet = _.findIndex(wasSet, function(name) { return name.indexOf('bg') === 0; }) > -1; alarmTypes = thresholdWasSet ? ['simple'] : ['predict']; @@ -148,6 +253,7 @@ function init ( ) { var enable = getAndPrepare('enable'); var disable = getAndPrepare('disable'); + var obscured = getAndPrepare('obscured'); settings.alarmTypes = prepareAlarmTypes(); @@ -170,6 +276,7 @@ function init ( ) { //all enabled feature, without any that have been disabled settings.enable = _.difference(enable, disable); + settings.obscured = obscured; var thresholds = settings.thresholds; @@ -178,11 +285,19 @@ function init ( ) { thresholds.bgTargetBottom = Number(thresholds.bgTargetBottom); thresholds.bgLow = Number(thresholds.bgLow); + // Do not convert for old installs that have these set in mg/dl + if (settings.units.toLowerCase().includes('mmol') && thresholds.bgHigh < 50) { + thresholds.bgHigh = Math.round(thresholds.bgHigh * constants.MMOL_TO_MGDL); + thresholds.bgTargetTop = Math.round(thresholds.bgTargetTop * constants.MMOL_TO_MGDL); + thresholds.bgTargetBottom = Math.round(thresholds.bgTargetBottom * constants.MMOL_TO_MGDL); + thresholds.bgLow = Math.round(thresholds.bgLow * constants.MMOL_TO_MGDL); + } + verifyThresholds(); adjustShownPlugins(); } - function verifyThresholds() { + function verifyThresholds () { var thresholds = settings.thresholds; if (thresholds.bgTargetBottom >= thresholds.bgTargetTop) { @@ -207,17 +322,23 @@ function init ( ) { } } - function adjustShownPlugins ( ) { - //TODO: figure out something for some plugins to have them shown by default + function adjustShownPlugins () { + var showPluginsUnset = settings.showPlugins && 0 === settings.showPlugins.length; + settings.showPlugins += ' delta direction upbat'; if (settings.showRawbg === 'always' || settings.showRawbg === 'noise') { settings.showPlugins += ' rawbg'; } - ['iob', 'cob', 'bwp', 'cage', 'basal', 'careportal' , 'boluscalc'].forEach( function showFeature (feature) { - if (isEnabled(feature)) { - settings.showPlugins += ' ' + feature; - } - }); + + if (showPluginsUnset) { + //assume all enabled features are plugins and they should be shown for now + //it would be better to use the registered plugins, but it's not loaded yet... + _.forEach(settings.enable, function showFeature (feature) { + if (isEnabled(feature)) { + settings.showPlugins += ' ' + feature; + } + }); + } } function isEnabled (feature) { @@ -234,36 +355,42 @@ function init ( ) { return enabled; } - function isAlarmEventEnabled (notify) { - var enabled = false; + function isUrgentHighAlarmEnabled(notify) { + return notify.eventName === 'high' && notify.level === constants.LEVEL_URGENT && settings.alarmUrgentHigh; + } - if ('high' !== notify.eventName && 'low' !== notify.eventName) { - enabled = true; - } else if (notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh) { - enabled = true; - } else if (notify.eventName === 'high' && settings.alarmHigh) { - enabled = true; - } else if (notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow) { - enabled = true; - } else if (notify.eventName === 'low' && settings.alarmLow) { - enabled = true; - } + function isHighAlarmEnabled(notify) { + return notify.eventName === 'high' && settings.alarmHigh; + } - return enabled; + function isUrgentLowAlarmEnabled(notify) { + return notify.eventName === 'low' && notify.level === constants.LEVEL_URGENT && settings.alarmUrgentLow; + } + + function isLowAlarmEnabled(notify) { + return notify.eventName === 'low' && settings.alarmLow; + } + + function isAlarmEventEnabled (notify) { + return ('high' !== notify.eventName && 'low' !== notify.eventName) + || isUrgentHighAlarmEnabled(notify) + || isHighAlarmEnabled(notify) + || isUrgentLowAlarmEnabled(notify) + || isLowAlarmEnabled(notify); } function snoozeMinsForAlarmEvent (notify) { var snoozeTime; - if (notify.eventName === 'high' && notify.level === levels.URGENT && settings.alarmUrgentHigh) { - snoozeTime = settings.alarmUrgentHighMins; - } else if (notify.eventName === 'high' && settings.alarmHigh) { + if (isUrgentHighAlarmEnabled(notify)) { + snoozeTime = settings.alarmUrgentHighMins; + } else if (isHighAlarmEnabled(notify)) { snoozeTime = settings.alarmHighMins; - } else if (notify.eventName === 'low' && notify.level === levels.URGENT && settings.alarmUrgentLow) { + } else if (isUrgentLowAlarmEnabled(notify)) { snoozeTime = settings.alarmUrgentLowMins; - } else if (notify.eventName === 'low' && settings.alarmLow) { + } else if (isLowAlarmEnabled(notify)) { snoozeTime = settings.alarmLowMins; - } else if (notify.level === levels.URGENT) { + } else if (notify.level === constants.LEVEL_URGENT) { snoozeTime = settings.alarmUrgentMins; } else { snoozeTime = settings.alarmWarnMins; @@ -282,9 +409,10 @@ function init ( ) { settings.isAlarmEventEnabled = isAlarmEventEnabled; settings.snoozeMinsForAlarmEvent = snoozeMinsForAlarmEvent; settings.snoozeFirstMinsForAlarmEvent = snoozeFirstMinsForAlarmEvent; + settings.filteredSettings = filteredSettings; return settings; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/storage.js b/lib/storage.js deleted file mode 100644 index e7830cbeecc..00000000000 --- a/lib/storage.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -var mongodb = require('mongodb'); - -var connection = null; - -function init (env, cb, forceNewConnection) { - var MongoClient = mongodb.MongoClient; - var mongo = {}; - - function maybe_connect (cb) { - - if (connection != null && !forceNewConnection) { - console.log('Reusing MongoDB connection handler'); - // If there is a valid callback, then return the Mongo-object - mongo.db = connection; - - if (cb && cb.call) { - cb(null, mongo); - } - } else { - if (!env.mongo) { - throw new Error('MongoDB connection string is missing'); - } - - console.log('Setting up new connection to MongoDB'); - var timeout = 30 * 1000; - var options = { replset: { socketOptions: { connectTimeoutMS : timeout, socketTimeoutMS : timeout }}}; - - MongoClient.connect(env.mongo, options, function connected(err, db) { - if (err) { - console.log('Error connecting to MongoDB, ERROR: %j', err); - throw err; - } else { - console.log('Successfully established a connected to MongoDB'); - } - - connection = db; - - mongo.db = connection; - - // If there is a valid callback, then invoke the function to perform the callback - if (cb && cb.call) { - cb(err, mongo); - } - }); - } - } - - mongo.collection = function get_collection (name) { - return connection.collection(name); - }; - - mongo.with_collection = function with_collection (name) { - return function use_collection(fn) { - fn(null, connection.collection(name)); - }; - }; - - mongo.limit = function limit (opts) { - if (opts && opts.count) { - return this.limit(parseInt(opts.count)); - } - return this; - }; - - mongo.ensureIndexes = function ensureIndexes (collection, fields) { - fields.forEach(function (field) { - console.info('ensuring index for: ' + field); - collection.ensureIndex(field, function (err) { - if (err) { - console.error('unable to ensureIndex for: ' + field + ' - ' + err); - } - }); - }); - }; - - return maybe_connect(cb); -} - -module.exports = init; diff --git a/lib/storage/mongo-storage.js b/lib/storage/mongo-storage.js new file mode 100644 index 00000000000..987e41ef67d --- /dev/null +++ b/lib/storage/mongo-storage.js @@ -0,0 +1,97 @@ +'use strict'; + +const MongoClient = require('mongodb').MongoClient; + +const mongo = { + client: null, + db: null, +}; + +function init(env, cb, forceNewConnection) { + + function maybe_connect(cb) { + + if (mongo.db != null && !forceNewConnection) { + console.log('Reusing MongoDB connection handler'); + // If there is a valid callback, then return the Mongo-object + + if (cb && cb.call) { + cb(null, mongo); + } + } else { + if (!env.storageURI) { + throw new Error('MongoDB connection string is missing. Please set MONGODB_URI environment variable'); + } + + console.log('Setting up new connection to MongoDB'); + const options = { + useNewUrlParser: true, + useUnifiedTopology: true, + }; + + const connect_with_retry = async function (i) { + + mongo.client = new MongoClient(env.storageURI, options); + try { + await mongo.client.connect(); + + console.log('Successfully established connection to MongoDB'); + + const dbName = mongo.client.s.options.dbName; + mongo.db = mongo.client.db(dbName); + + const result = await mongo.db.command({ connectionStatus: 1 }); + const roles = result.authInfo.authenticatedUserRoles; + if (roles.length > 0 && roles[0].role == 'readAnyDatabase') { + console.error('Mongo user is read only'); + cb(new Error('MongoDB connection is in read only mode! Go back to MongoDB configuration and check your database user has read and write access.'), null); + } + + console.log('Mongo user role seems ok:', roles); + + // If there is a valid callback, then invoke the function to perform the callback + if (cb && cb.call) { + cb(null, mongo); + } + } catch (err) { + if (err.message && err.message.includes('AuthenticationFailed')) { + console.log('Authentication to Mongo failed'); + cb(new Error('MongoDB authentication failed! Double check the URL has the right username and password in MONGODB_URI.'), null); + return; + } + + if (err.name && err.name === "MongoServerSelectionError") { + const timeout = (i > 15) ? 60000 : i * 3000; + console.log('Error connecting to MongoDB: %j - retrying in ' + timeout / 1000 + ' sec', err); + 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 seems invalid: ' + err.message)); + } + } + }; + + return connect_with_retry(1); + + } + } + + mongo.collection = function get_collection(name) { + return mongo.db.collection(name); + }; + + mongo.ensureIndexes = function ensureIndexes(collection, fields) { + fields.forEach(function (field) { + console.info('ensuring index for: ' + field); + collection.createIndex(field, { 'background': true }, function (err) { + if (err) { + console.error('unable to ensureIndex for: ' + field + ' - ' + err); + } + }); + }); + }; + + return maybe_connect(cb); +} + +module.exports = init; diff --git a/lib/storage/openaps-storage.js b/lib/storage/openaps-storage.js new file mode 100644 index 00000000000..0c9c238015e --- /dev/null +++ b/lib/storage/openaps-storage.js @@ -0,0 +1,191 @@ +'use strict'; + +var _ = require('lodash'); +var fs = require('fs'); +var crypto = require('crypto'); +var MongoMock = require('mongomock'); + +var config = { + collections: {} +}; + +function init (env, callback) { + + if (!env.storageURI || !_.isString(env.storageURI)) { + throw new Error('openaps config uri is missing or invalid'); + } + + var configPath = env.storageURI.split('openaps://').pop(); + + function addId (data) { + var shasum = crypto.createHash('sha1'); + shasum.update(JSON.stringify(data)); + data._id = shasum.digest('hex'); + } + + function loadData (path) { + + if (!path || !_.isString(path)) { + return [ ]; + } + + try { + purgeCache(path); + var inputData = require(path); + if (_.isArray(inputData)) { + //console.info('>>>input is an array', path); + _.forEach(inputData, addId); + } else if (!_.isEmpty(inputData) && _.isObject(inputData)) { + //console.info('>>>input is an object', path); + inputData.created_at = new Date(fs.statSync(path).mtime).toISOString(); + addId(inputData); + inputData = [ inputData ]; + } else { + //console.info('>>>input is something else', path, inputData); + inputData = [ ]; + } + + return inputData; + } catch (err) { + console.error('unable to find input data for', path, err); + return [ ]; + } + + } + + function reportAsCollection (name) { + var data = { }; + var input = _.get(config, 'collections.' + name + '.input'); + + if (_.isArray(input)) { + //console.info('>>>input is an array', input); + data[name] = _.flatten(_.map(input, loadData)); + } else { + data[name] = loadData(input); + } + + var mock = new MongoMock(data); + + var collection = mock.collection(name); + + var wrapper = { + findQuery: null + , sortQuery: null + , limitCount: null + , find: function find (query) { + query = _.cloneDeepWith(query, function booleanize (value) { + //TODO: for some reason we're getting {$exists: NaN} instead of true/false + if (value && _.isObject(value) && '$exists' in value) { + return {$exists: true}; + } + }); + wrapper.findQuery = query; + return wrapper; + } + , limit: function limit (count) { + wrapper.limitCount = count; + return wrapper; + } + , sort: function sort (query) { + wrapper.sortQuery = query; + return wrapper; + } + , toArray: function toArray(callback) { + collection.find(wrapper.findQuery).toArray(function intercept (err, results) { + if (err) { + return callback(err, results); + } + + if (wrapper.sortQuery) { + var field = _.keys(wrapper.sortQuery).pop(); + //console.info('>>>sortField', field); + if (field) { + results = _.sortBy(results, field); + if (-1 === wrapper.sortQuery[field]) { + //console.info('>>>sort reverse'); + results = _.reverse(results); + } + } + } + + if (wrapper.limitCount !== null && _.isNumber(wrapper.limitCount)) { + //console.info('>>>limit count', wrapper.limitCount); + results = _.take(results, wrapper.limitCount); + } + + //console.info('>>>toArray', name, wrapper.findQuery, wrapper.sortQuery, wrapper.limitCount, results.length); + + callback(null, results); + }); + return wrapper; + } + }; + + return wrapper; + + } + + try { + var customConfig = require(configPath); + + config = _.merge({}, customConfig, config); + + callback(null, { + collection: reportAsCollection + , ensureIndexes: _.noop + }); + } catch (err) { + callback(err); + } +} + +/** + * Removes a module from the cache + * + * see http://stackoverflow.com/a/14801711 + */ +function purgeCache(moduleName) { + // Traverse the cache looking for the files + // loaded by the specified module name + searchCache(moduleName, function (mod) { + delete require.cache[mod.id]; + }); + + // Remove cached paths to the module. + // Thanks to @bentael for pointing this out. + Object.keys(module.constructor._pathCache).forEach(function(cacheKey) { + if (cacheKey.indexOf(moduleName)>0) { + delete module.constructor._pathCache[cacheKey]; + } + }); +} + +/** + * Traverses the cache to search for all the cached + * files of the specified module name + * + * see http://stackoverflow.com/a/14801711 + */ +function searchCache(moduleName, callback) { + // Resolve the module identified by the specified name + var mod = require.resolve(moduleName); + + // Check if the module has been resolved and found within + // the cache + if (mod && ((mod = require.cache[mod]) !== undefined)) { + // Recursively go over the results + (function traverse(mod) { + // Go over each of the module's children and + // traverse them + mod.children.forEach(function (child) { + traverse(child); + }); + + // Call the specified callback providing the + // found cached module + callback(mod); + }(mod)); + } +} + +module.exports = init; diff --git a/lib/times.js b/lib/times.js index 9d37c533218..900155a8e47 100644 --- a/lib/times.js +++ b/lib/times.js @@ -1,11 +1,19 @@ 'use strict'; -var cache = { }; - var factories = { - hours: function hours(value) { + weeks: function weeks(value) { + return { + mins: value * 7 * 24 * 60, secs: value * 7 * 24 * 60 * 60, msecs: value * 7 * 24 * 60 * 60 * 1000 + }; + } + , days: function days(value) { + return { + hours: value * 24, mins: value * 24 * 60, secs: value * 24 * 60 * 60, msecs: value * 24 * 60 * 60 * 1000 + }; + } + , hours: function hours(value) { return { - days: value / 24 ,mins: value * 60, secs: value * 60 * 60, msecs: value * 60 * 60 * 1000 + mins: value * 60, secs: value * 60 * 60, msecs: value * 60 * 60 * 1000 }; } , mins: function mins(value) { @@ -25,27 +33,25 @@ var factories = { } }; -function getOrCreate (types) { +function create (types) { return function withValue (value) { - var key = types + value; - var obj = cache[key]; - if (!obj) { - obj = factories[types](value); - cache[key] = obj; - } - return obj; + return factories[types](value); }; } var times = { - hour: function ( ) { return getOrCreate('hours')(1); } - , hours: function (value) { return getOrCreate('hours')(value); } - , min: function ( ) { return getOrCreate('mins')(1); } - , mins: function (value) { return getOrCreate('mins')(value); } - , sec: function ( ) { return getOrCreate('secs')(1); } - , secs: function (value) { return getOrCreate('secs')(value); } - , msec: function ( ) { return getOrCreate('msecs')(1); } - , msecs: function (value) { return getOrCreate('msecs')(value); } + week: function ( ) { return create('weeks')(1); } + , weeks: function (value) { return create('weeks')(value); } + , day: function ( ) { return create('days')(1); } + , days: function (value) { return create('days')(value); } + , hour: function ( ) { return create('hours')(1); } + , hours: function (value) { return create('hours')(value); } + , min: function ( ) { return create('mins')(1); } + , mins: function (value) { return create('mins')(value); } + , sec: function ( ) { return create('secs')(1); } + , secs: function (value) { return create('secs')(value); } + , msec: function ( ) { return create('msecs')(1); } + , msecs: function (value) { return create('msecs')(value); } }; module.exports = times; \ No newline at end of file diff --git a/lib/treatments.js b/lib/treatments.js deleted file mode 100644 index 31d31ab4ab9..00000000000 --- a/lib/treatments.js +++ /dev/null @@ -1,164 +0,0 @@ -'use strict'; - -var find_options = require('./query'); - -function storage (env, ctx) { - var ObjectID = require('mongodb').ObjectID; - - function create (obj, fn) { - - var results = prepareData(obj); - - api( ).insert(obj, function (err, doc) { - fn(null, doc); - - if (obj.preBolus) { - //create a new object to insert copying only the needed fields - var pbTreat = { - created_at: (new Date(results.created_at.getTime() + (obj.preBolus * 60000))).toISOString(), - eventType: obj.eventType, - carbs: results.preBolusCarbs - }; - - if (obj.notes) { - pbTreat.notes = obj.notes; - } - - api( ).insert(pbTreat, function() { - //nothing to do here - }); - } - - //TODO: this is triggering a read from Mongo, we can do better - ctx.bus.emit('data-received'); - - }); - } - - function list (opts, fn) { - return ctx.store.limit.call(api() - .find(query_for(opts)) - .sort(opts && opts.sort || {created_at: -1}), opts) - .toArray(fn); - } - - function query_for (opts) { - return find_options(opts, storage.queryOpts); - } - - - function remove (_id, fn) { - api( ).remove({ '_id': new ObjectID(_id) }, fn); - - ctx.bus.emit('data-received'); - } - - function save (obj, fn) { - obj._id = new ObjectID(obj._id); - prepareData(obj); - api().save(obj, fn); - - ctx.bus.emit('data-received'); - } - - - function api ( ) { - return ctx.store.db.collection(env.treatments_collection); - } - - api.list = list; - api.create = create; - api.query_for = query_for; - api.indexedFields = [ - 'created_at' - , 'eventType' - , 'insulin' - , 'carbs' - , 'glucose' - , 'enteredBy' - , 'boluscalc.foods._id' - , 'notes' - ]; - - api.remove = remove; - api.save = save; - - return api; -} - -function prepareData(obj) { - - //NOTE: the eventTime is sent by the client, but deleted, we only store created_at right now - var results = { - created_at: new Date() - , preBolusCarbs: '' - }; - - obj.glucose = Number(obj.glucose); - obj.carbs = Number(obj.carbs); - obj.insulin = Number(obj.insulin); - obj.duration = Number(obj.duration); - obj.percent = Number(obj.percent); - obj.absolute = Number(obj.absolute); - obj.preBolus = Number(obj.preBolus); - - var eventTime; - if (obj.eventTime) { - eventTime = new Date(obj.eventTime); - results.created_at = eventTime; - } - - obj.created_at = results.created_at.toISOString(); - if (obj.preBolus && obj.preBolus !== 0 && obj.carbs) { - results.preBolusCarbs = obj.carbs; - delete obj.carbs; - } - - if (obj.eventType === 'Announcement') { - obj.isAnnouncement = true; - } - - // clean data - delete obj.eventTime; - - function deleteIfEmpty (field) { - if (!obj[field] || obj[field] === 0) { - delete obj[field]; - } - } - - deleteIfEmpty('carbs'); - deleteIfEmpty('insulin'); - deleteIfEmpty('duration'); - deleteIfEmpty('percent'); - deleteIfEmpty('notes'); - deleteIfEmpty('preBolus'); - - //special handling for absolute to support temp to 0 - if (isNaN(obj.absolute)) { - delete obj.absolute; - } - - if (obj.glucose === 0 || isNaN(obj.glucose)) { - delete obj.glucose; - delete obj.glucoseType; - delete obj.units; - } - - return results; -} - -storage.queryOpts = { - walker: { - insulin: parseInt - , carbs: parseInt - , glucose: parseInt - , notes: find_options.parseRegEx - , eventType: find_options.parseRegEx - , enteredBy: find_options.parseRegEx - } - , dateField: 'created_at' -}; - -module.exports = storage; - diff --git a/lib/units.js b/lib/units.js index f548d55744e..5eb36c10950 100644 --- a/lib/units.js +++ b/lib/units.js @@ -1,11 +1,13 @@ 'use strict'; +var consts = require('./constants'); + function mgdlToMMOL(mgdl) { - return (Math.round((mgdl / 18) * 10) / 10).toFixed(1); + return (Math.round((mgdl / consts.MMOL_TO_MGDL) * 10) / 10).toFixed(1); } function mmolToMgdl(mgdl) { - return Math.round(mgdl * 18); + return Math.round(mgdl * consts.MMOL_TO_MGDL); } function configure() { diff --git a/lib/utils.js b/lib/utils.js index 8d7a51397e0..8cd32bba529 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,17 +1,16 @@ 'use strict'; -var moment = require('moment-timezone'); +var _ = require('lodash'); var units = require('./units')(); -function init(settings) { +function init(ctx) { + var moment = ctx.moment; + var settings = ctx.settings; + var translate = ctx.language.translate; + var timeago = require('./plugins/timeago')(ctx); - var utils = { - }; - - var MINUTE_IN_SECS = 60 - , HOUR_IN_SECS = 3600 - , DAY_IN_SECS = 86400; + var utils = { }; utils.scaleMgdl = function scaleMgdl (mgdl) { if (settings.units === 'mmol' && mgdl) { @@ -26,7 +25,7 @@ function init(settings) { }; utils.toFixed = function toFixed(value) { - if (value === 0) { + if (!value) { return '0'; } else { var fixed = value.toFixed(2); @@ -34,52 +33,54 @@ function init(settings) { } }; - utils.timeAgo = function timeAgo(time) { - - var now = Date.now() - , offset = time === -1 ? -1 : (now - time) / 1000 - , parts = {}; - - if (offset < MINUTE_IN_SECS * -5) { - parts = { value: 'in the future' }; - } else if (offset === -1) { - parts = { label: 'time ago' }; - } else if (offset <= MINUTE_IN_SECS * 2) { - parts = { value: 1, label: 'min ago' }; - } else if (offset < (MINUTE_IN_SECS * 60)) { - parts = { value: Math.round(Math.abs(offset / MINUTE_IN_SECS)), label: 'mins ago' }; - } else if (offset < (HOUR_IN_SECS * 2)) { - parts = { value: 1, label: 'hr ago' }; - } else if (offset < (HOUR_IN_SECS * 24)) { - parts = { value: Math.round(Math.abs(offset / HOUR_IN_SECS)), label: 'hrs ago' }; - } else if (offset < DAY_IN_SECS) { - parts = { value: 1, label: 'day ago' }; - } else if (offset <= (DAY_IN_SECS * 7)) { - parts = { value: Math.round(Math.abs(offset / DAY_IN_SECS)), label: 'day ago' }; - } else { - parts = { value: 'long ago' }; + /** + * Round the number to maxDigits places, return a string + * that truncates trailing zeros + */ + utils.toRoundedStr = function toRoundedStr (value, maxDigits) { + if (!value) { + return '0'; } + 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); + }; - if (offset > DAY_IN_SECS * 7) { - parts.status = 'warn'; - } else if (offset < MINUTE_IN_SECS * -5 || offset > (MINUTE_IN_SECS * settings.alarmTimeagoUrgentMins)) { - parts.status = 'urgent'; - } else if (offset > (MINUTE_IN_SECS * settings.alarmTimeagoWarnMins)) { - parts.status = 'warn'; + // some helpers for input "date" + utils.mergeInputTime = function mergeInputTime(timestring, datestring) { + return moment(datestring + ' ' + timestring, 'YYYY-MM-D HH:mm'); + }; + + + utils.deviceName = function deviceName (device) { + var last = device ? _.last(device.split('://')) : 'unknown'; + return _.first(last.split('/')); + }; + + utils.timeFormat = function timeFormat (m, sbx) { + var when; + if (m && sbx.data.inRetroMode) { + when = m.format('LT'); + } else if (m) { + when = utils.formatAgo(m, sbx.time); } else { - parts.status = 'current'; + when = 'unknown'; } - return parts; + return when; + }; + utils.formatAgo = function formatAgo (m, nowMills) { + var ago = timeago.calcDisplay({mills: m.valueOf()}, nowMills); + return translate('%1' + ago.shortLabel + (ago.shortLabel.length === 1 ? ' ago' : ''), { params: [(ago.value ? ago.value : '')]}); }; - // some helpers for input "date" - utils.mergeInputTime = function mergeInputTime(timestring, datestring) { - return moment(datestring + ' ' + timestring, 'YYYY-MM-D HH:mm'); + utils.timeAt = function timeAt (prefix, sbx) { + return sbx.data.inRetroMode ? (prefix ? ' ' : '') + '@ ' : (prefix ? ', ' : ''); }; - return utils; + return utils; } -module.exports = init; \ No newline at end of file +module.exports = init; diff --git a/lib/websocket.js b/lib/websocket.js deleted file mode 100644 index 7af5f8e009e..00000000000 --- a/lib/websocket.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; - -var levels = require('./levels'); - -function init (env, ctx, server) { - - function websocket ( ) { - return websocket; - } - - var io; - var watchers = 0; - var lastData = {}; - - var alarmType2Level = { - urgent_alarm: levels.URGENT - , alarm: levels.WARN - }; - - function start ( ) { - io = require('socket.io')({ - 'transports': ['xhr-polling'], 'log level': 0 - }).listen(server, { - //these only effect the socket.io.js file that is sent to the client, but better than nothing - 'browser client minification': true, - 'browser client etag': true, - 'browser client gzip': false - }); - } - - function emitData (delta) { - if (lastData.cals) { - console.log('running websocket.emitData', ctx.data.lastUpdated); - io.emit('dataUpdate', delta); - } - } - - function listeners ( ) { - io.sockets.on('connection', function (socket) { - // send all data upon new connection - socket.emit('dataUpdate',lastData); - io.emit('clients', ++watchers); - socket.on('ack', function(alarmType, silenceTime) { - //TODO: Announcement hack a1/a2 - var level = alarmType2Level[alarmType] || alarmType; - ctx.notifications.ack(level, silenceTime, true); - }); - socket.on('disconnect', function () { - io.emit('clients', --watchers); - }); - }); - } - - websocket.update = function update ( ) { - console.log('running websocket.update'); - var sgvMgdl = ctx.data.sgvs.length > 0 ? ctx.data.sgvs[ctx.data.sgvs.length - 1].mgdl : null; - if (sgvMgdl) { - if (lastData.sgvs) { - var delta = ctx.data.calculateDelta(lastData); - if (delta.delta) { - console.log('lastData full size', JSON.stringify(lastData).length,'bytes'); - if (delta.sgvs) { console.log('patientData update size', JSON.stringify(delta).length,'bytes'); } - emitData(delta); - } else { console.log('delta calculation indicates no new data is present'); } - } - lastData = ctx.data.clone(); - } - }; - - websocket.emitNotification = function emitNotification (notify) { - if (notify.clear) { - io.emit('clear_alarm', true); - console.info('emitted clear_alarm to all clients'); - } else if (notify.level === levels.WARN) { - io.emit('alarm', notify); - console.info('emitted alarm to all clients'); - } else if (notify.level === levels.URGENT) { - io.emit('urgent_alarm', notify); - console.info('emitted urgent_alarm to all clients'); - } else if (notify.isAnnouncement) { - io.emit('announcement', notify); - console.info('emitted announcement to all clients'); - } - }; - - start( ); - listeners( ); - - return websocket(); -} - -module.exports = init; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..5163a7cd578 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10002 @@ +{ + "name": "nightscout", + "version": "15.0.3", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.20.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", + "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==" + }, + "@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + } + }, + "@babel/generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", + "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "requires": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "requires": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", + "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz", + "integrity": "sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.2.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "requires": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", + "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", + "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.10", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==" + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "requires": { + "@babel/types": "^7.20.2" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "requires": { + "@babel/types": "^7.20.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==" + }, + "@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "requires": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + } + }, + "@babel/helpers": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", + "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", + "requires": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.13", + "@babel/types": "^7.20.7" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", + "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==" + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz", + "integrity": "sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz", + "integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz", + "integrity": "sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", + "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.11.tgz", + "integrity": "sha512-tA4N427a7fjf1P0/2I4ScsHGc5jcHPbb30xMbaTke2gxDuWpUfXDuX1FEymJwKk4tuGUvGcejAR6HdZVqmmPyw==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz", + "integrity": "sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", + "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz", + "integrity": "sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "requires": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "requires": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz", + "integrity": "sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==", + "requires": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-simple-access": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "requires": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", + "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "requires": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/traverse": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", + "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.13", + "@babel/types": "^7.20.7", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==" + }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.19.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", + "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@parse/node-apn": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.1.3.tgz", + "integrity": "sha512-Bwhmbm895lEIF2772PJ8dSvBjrtOG9/q/TDMxmX40IgZxQFoXS73+JUIKTq3CA7SUB/Szu5roJINQ0L2U/1MJw==", + "requires": { + "debug": "4.3.3", + "jsonwebtoken": "8.5.1", + "node-forge": "1.3.0", + "verror": "1.10.1" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/eslint": { + "version": "8.4.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", + "integrity": "sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==", + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" + }, + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + }, + "@types/tough-cookie": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", + "dev": true + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "requires": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==" + }, + "@webpack-cli/info": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "requires": { + "envinfo": "^7.7.3" + } + }, + "@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==" + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha512-I+Wi+qiE2kUXyrRhNsWv6XsjUTBJjSoVSctKNBfLG5zG/Xe7Rjbxf13+vqYHNTwHaFU+FtSlVxOCTiMEVtPv0A==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" + } + } + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==" + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" + }, + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "requires": { + "debug": "^4.3.4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha512-H3LU5RLiSsGXPhN+Nipar0iR0IofH+8r89G2y1tBKxQ/agagKyAjhkAFDRBfodP2caPrNKHpAWNIM/c9yeL7uA==" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==" + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" + }, + "aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.0" + } + }, + "axios-cookiejar-support": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-4.0.7.tgz", + "integrity": "sha512-9vpE3y/a2l2Vs2XEJE4L2z0GWnlpJ4Xj+kDaoCtrpPfS1J3oikXBrxRJX6H62/ZcelOGe+519yW7mqXCIoPXuw==", + "requires": { + "http-cookie-agent": "^5.0.4" + } + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, + "babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "benv": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/benv/-/benv-3.3.0.tgz", + "integrity": "sha512-5eq2WZ+tZsLLP+Zt7Z1jLaQxGfSsU5tTPKl5knv7uuDSJBhy//H8TIDLPynFLyUBoDqDXptVF8ODKIpA0TzziA==", + "dev": true, + "requires": { + "jsdom": ">= 10.0", + "rewire": "^2.3.1" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + } + } + }, + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "bootevent": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/bootevent/-/bootevent-0.0.1.tgz", + "integrity": "sha512-kKeAwPdpIAguDxuSjOCsUI/o5WWYYX0JrWBSk7SYRm6yGY4xtsTier0x9lfVpJy2Y1gGDg4xKcMWSY2l03BY/Q==", + "requires": { + "chainsaw": "~0.1.0" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "bson": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", + "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==" + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001448", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001448.tgz", + "integrity": "sha512-tq2YI+MJnooG96XpbTRYkBxLxklZPOdLmNIOdIhvf7SNJan6u5vCKum8iT7ZfCt70m1GPkuC7P3TtX6UuhupuA==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "requires": { + "traverse": ">=0.3.0 <0.4" + }, + "dependencies": { + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" + } + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "requires": { + "source-map": "~0.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "core-js-compat": { + "version": "3.27.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.27.2.tgz", + "integrity": "sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg==", + "requires": { + "browserslist": "^4.21.4" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-loader": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", + "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "requires": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "cssmin": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/cssmin/-/cssmin-0.4.3.tgz", + "integrity": "sha512-A0hOtBUV5dPB/gRKsLZsqoyeFHuKIWCXdumFdsK/0nlhwK1NRZHwtm0svdTA4uEpixZD+o0xs7MirNsG2X1JBg==" + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "cssstyle": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.3.1.tgz", + "integrity": "sha512-tNvaxM5blOnxanyxI6panOsnfiyLRj3HV4qjqqS45WPNS1usdYWRUQjqTEEELK73lpeP/1KoIGYUwrBn/VcECA==", + "requires": { + "cssom": "0.3.x" + } + }, + "csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "csv-stringify": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", + "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" + }, + "d3": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + }, + "d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + }, + "dependencies": { + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha512-B0n2zDIXpzLzKeoEozorDSa1cHc1t0NjmxP0zuAxbizNU2MBqYJJKYXrrFdKuQliojXynrxgd7l4ahfg/+aA5g==", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "denque": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==" + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "easyxml": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/easyxml/-/easyxml-2.0.1.tgz", + "integrity": "sha512-qmQ+zRf6ASThHgW/ZTPa19u9TMJLumuyQP25LEKwqx6NAOtTbmdJDVHFAyBxkqnimgB2sGOV4zIG3hapPL2Nxw==", + "requires": { + "elementtree": "^0.1.6", + "inflect": "^0.3.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + }, + "elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "requires": { + "sax": "1.1.4" + } + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "engine.io": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" + } + } + }, + "engine.io-client": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.3.tgz", + "integrity": "sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0" + }, + "dependencies": { + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" + } + } + }, + "engine.io-parser": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" + }, + "enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "env-cmd": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", + "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", + "dev": true, + "requires": { + "commander": "^4.0.0", + "cross-spawn": "^7.0.0" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + } + } + }, + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "errorhandler": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", + "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", + "requires": { + "accepts": "~1.3.7", + "escape-html": "~1.0.3" + } + }, + "es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "globals": { + "version": "13.19.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", + "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "eslint-plugin-security": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.7.0.tgz", + "integrity": "sha512-+ahcCh7M5w7fdFaNccaChBGq8nd3Wa+XvGJS+hY74kvrMhG4EuLbljRIjilOqh1iDMW/EckB1oOWmiVIYlVACQ==", + "dev": true, + "requires": { + "safe-regex": "^2.1.1" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "eslint-webpack-plugin": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-2.7.0.tgz", + "integrity": "sha512-bNaVVUvU4srexGhVcayn/F4pJAz19CWBkKoMx7aSQ4wtTbZQCnG5O9LHCE42mM+JSKOUp7n6vd5CIwzj7lOVGA==", + "dev": true, + "requires": { + "@types/eslint": "^7.29.0", + "arrify": "^2.0.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1" + }, + "dependencies": { + "@types/eslint": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "expose-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-2.0.0.tgz", + "integrity": "sha512-WBpSGlNkn7YwbU2us7O+h0XsoFrB43Y/VCNSpRV4OZFXXKgw8W800BgNxLV0S97N3+KGnFYSCAJi1AV86NO22w==" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + } + } + }, + "express-minify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/express-minify/-/express-minify-1.0.0.tgz", + "integrity": "sha512-04/iYxB79jGeNZBBkbAW7L7FMG4Wtu78F1SayXIKiJD6MfqYnOI3DD8no7QOntgedYCdYUpj+Skg8QWR/2WnMQ==", + "requires": { + "clean-css": "^4.1.7", + "on-headers": "^1.0.1", + "uglify-js": "^3.0.28" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fast-password-entropy": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fast-password-entropy/-/fast-password-entropy-1.1.1.tgz", + "integrity": "sha512-dxm29/BPFrNgyEDygg/lf9c2xQR0vnQhG7+hZjAI39M/3um9fD4xiqG6F0ZjW6bya5m9CI0u6YryHGRtxCGCiw==" + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==" + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "flot": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/flot/-/flot-0.8.3.tgz", + "integrity": "sha512-xg2otcTJDvS+ERK+my4wxG/ASq90QURXtoM4LhacCq0jQW2jbyjdttbRNqU2cPykrpMvJ6b2uSp6SAgYAzj9tQ==" + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha512-3TOY+4TKV0Ml83PXJQY+JFQaHNV38lzQDIzzXYg1kWdBLenGgoZhAs0CKgzI31vi2pWEpQMq/Yi4bpKwCPkw7g==", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "dev": true + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "forwarded-for": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/forwarded-for/-/forwarded-for-1.1.0.tgz", + "integrity": "sha512-1Yam9ht7GyMXMBvuwJfUYqpdtLVodtT5ee5JMBzGiSwVVeh37ZN8LuOWkNHd6ho2zUxpSZCHuQrt1Vjl2AxDNA==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==" + }, + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "requires": { + "duplexer": "^0.1.2" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha512-w0Kz8lJFBoyaurBiNrIvxPqr/gJ6fOfSkpAPOepN3oECqGJag37xPbOv57izi/KP8auHgNYxn5fXtAb+1LsJ6w==", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "helmet": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz", + "integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==" + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-cookie-agent": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-5.0.4.tgz", + "integrity": "sha512-OtvikW69RvfyP6Lsequ0fN5R49S+8QcS9zwd58k6VSr6r57T8G29BkPdyrBcSwLq6ExLs9V+rBlfxu7gDstJag==", + "requires": { + "agent-base": "^7.1.0" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflect": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/inflect/-/inflect-0.3.0.tgz", + "integrity": "sha512-ziAYvtAaDgJYlpdAKr9vsSzBmPTUYPFRj/8L7n9SfaDIXzf+/F07T3bIXQUaYqls+I/aE2DBKBrxcQ7BKZ6iCA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jquery": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz", + "integrity": "sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg==" + }, + "jquery-ui-bundle": { + "version": "1.12.1-migrate", + "resolved": "https://registry.npmjs.org/jquery-ui-bundle/-/jquery-ui-bundle-1.12.1-migrate.tgz", + "integrity": "sha512-ihyHzL6NYkCg1SNIRFrQAkgEeAJmet2G6rrecCRwq6RcWNApV6w2NSjqfuJUUoiKywkvlU+pqHJ5NAAEuTet4w==" + }, + "jquery.tooltips": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jquery.tooltips/-/jquery.tooltips-1.0.0.tgz", + "integrity": "sha512-QQQcWPuk6jKogEIcsewhJmZJ8KN8BbB1vBFUIl+k+j85BMMr+nYPa86ihTFHfne9H8yI9F7okoQRzwkJsk3chg==" + }, + "js-storage": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/js-storage/-/js-storage-1.1.0.tgz", + "integrity": "sha512-XwkyTB3cjwBSaaKo+edR/n8ZbmX/mj5lJpW/O753NYvMpClQeurucceIvX3HeF4ZTTY2YRPXTVzgPByK4pA7aQ==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "jsdom": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.11.0.tgz", + "integrity": "sha512-ou1VyfjwsSuWkudGxb03FotDajxAto6USAlmMZjE2lc0jCznt7sBWkhfRBRaWwbnmDqdMSTKTLT5d9sBFkkM7A==", + "requires": { + "abab": "^1.0.4", + "acorn": "^5.3.0", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": ">= 0.3.1 < 0.4.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.0", + "escodegen": "^1.9.0", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.2.0", + "nwsapi": "^2.0.0", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.83.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.3", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^4.0.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "dependencies": { + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + } + }, + "memfs": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", + "integrity": "sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==", + "dev": true, + "requires": { + "fs-monkey": "^1.0.3" + } + }, + "memory-cache": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", + "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==" + }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + } + } + }, + "minimed-connect-to-nightscout": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/minimed-connect-to-nightscout/-/minimed-connect-to-nightscout-1.5.5.tgz", + "integrity": "sha512-2o/L8Nlc8Hki9H2yBRM8t/q0hFqxvmroZzrHUqwgIqZoZ2YOVBa2iPMIsYnLc/Tu/V8Et6o+BV4QurOLTzzbqw==", + "requires": { + "axios": "^0.26.0", + "axios-cookiejar-support": "^1.0.0", + "common": "^0.2.5", + "lodash": "^4.17.15", + "request": "^2.88.0", + "tough-cookie": "^4.0.0" + }, + "dependencies": { + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "axios": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz", + "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==", + "requires": { + "follow-redirects": "^1.14.8" + } + }, + "axios-cookiejar-support": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.0.tgz", + "integrity": "sha512-9pBlIU5jfrGZTnUQlt8symShviSTOSlOKGtryHx76lJPnKIXDqUT3JDAjJ1ywOQLyfiWrthIt4iJiVP2L2S4jA==", + "requires": { + "is-redirect": "^1.0.0", + "pify": "^5.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "common": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/common/-/common-0.2.5.tgz", + "integrity": "sha1-PHGC9ni9HjaBzVzDSMdZ/o3SI5Q=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, + "follow-redirects": { + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mocha": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz", + "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==", + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.2.0", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "mocha": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.1", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.0.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.20", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", + "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, + "moment-locales-webpack-plugin": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz", + "integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==", + "requires": { + "lodash.difference": "^4.5.0" + } + }, + "moment-timezone": { + "version": "0.5.40", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz", + "integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==", + "requires": { + "moment": ">= 2.9.0" + } + }, + "moment-timezone-data-webpack-plugin": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/moment-timezone-data-webpack-plugin/-/moment-timezone-data-webpack-plugin-1.5.1.tgz", + "integrity": "sha512-1le6a35GgYdWMVYFzrfpE/F6Pk4bj0M3QKD6Iv6ba9LqWGoVqHQRHyCTLvLis5E1J98Sz40ET6yhZzMVakwpjg==", + "requires": { + "find-cache-dir": "^3.0.0", + "make-dir": "^3.0.0" + } + }, + "mongo-url-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongo-url-parser/-/mongo-url-parser-1.0.2.tgz", + "integrity": "sha512-mv0RXncyzNt+gtnKOjD3YJK4I6+r9jk3aU5psa8/E8zvpN03AsQ0DkLJPBs5vPAzCfMs4T8gA2UQgaFx3QANHw==" + }, + "mongodb": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.3.tgz", + "integrity": "sha512-Psm+g3/wHXhjBEktkxXsFMZvd3nemI0r3IPsE0bU+4//PnvNWKkzhZcEsbPcYiWqe8XqXJJEg4Tgtr7Raw67Yw==", + "requires": { + "bl": "^2.2.1", + "bson": "^1.1.4", + "denque": "^1.4.1", + "optional-require": "^1.1.8", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "mongomock": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/mongomock/-/mongomock-0.1.2.tgz", + "integrity": "sha512-QI2HtcbiE43qfMGzFAwXEhM8Toddp0QLxhDofPpj1kq0GbebgNuLqWEhSPAprlr9L5Ci9jrwVDJ4qRe5fkg8nQ==", + "requires": { + "bson": "*" + } + }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "nested-error-stacks": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz", + "integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==", + "dev": true + }, + "nightscout-connect": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/nightscout-connect/-/nightscout-connect-0.0.12.tgz", + "integrity": "sha512-y4Dc+shzkmGQqJG2w2zNp1Jnu4Yzc4VtSCoBiinrssjy+7ksjmOenxVDyh0Xw2UCWMkqr4A4o+iJ28KvrHWcvw==", + "requires": { + "axios": "^1.3.4", + "axios-cookiejar-support": "^4.0.6", + "qs": "^6.11.1", + "tough-cookie": "^4.1.3", + "xstate": "^4.37.1", + "yargs": "^17.7.1" + }, + "dependencies": { + "axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } + } + }, + "node-cache": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-4.2.1.tgz", + "integrity": "sha512-BOb67bWg2dTyax5kdef5WfU3X8xu4wPg+zHzkvls0Q/QpYycIFRLEEIdAx9Wma43DxG6Qzn4illdZoYseKWa4A==", + "requires": { + "clone": "2.x", + "lodash": "^4.17.15" + } + }, + "node-forge": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", + "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==" + }, + "node-releases": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", + "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==" + }, + "nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "nwsapi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, + "optional-require": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz", + "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==", + "requires": { + "require-at": "^1.0.6" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-duration": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.3.tgz", + "integrity": "sha512-hMOZHfUmjxO5hMKn7Eft+ckP2M4nV4yzauLXiw3PndpkASnx5r8pDAMcOAiqxoemqWjMWmz4fOHQM6n6WwETXw==" + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "requires": { + "through": "~2.3" + } + }, + "pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" + }, + "postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + }, + "pushover-notifications": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-1.2.2.tgz", + "integrity": "sha512-+3Xcj+kiMiouZK1Ws8yGBTyl8WMPZZdELgl/iVxYqNwDdlaObBHMhEGPRC6Zb9t0BE27ikOoOqSIO1cKZOtsDA==" + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "random-token": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/random-token/-/random-token-0.0.8.tgz", + "integrity": "sha512-L8osVTR9ZWUNs24m+/S0ibg5aveCme6+nEDmbzqdEog0skWDsTFXs8Y06Zt9y1a9jzZ70gomX9BaMqntYlN+IQ==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "requires": { + "resolve": "^1.9.0" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regexp-tree": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", + "integrity": "sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "regexpu-core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", + "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==" + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" + } + } + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-at": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", + "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, + "rewire": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/rewire/-/rewire-2.5.2.tgz", + "integrity": "sha512-9wOlgRHTOzUv5dQO2XD2qWob+7yi/QXh7SSwLJW5wMAkAdkYuaCtcPuLAXUTllK0MjSvtpxUqAWMuSrrdt9VNw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "requires": { + "regexp-tree": "~0.1.1" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==" + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + } + } + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "share2nightscout-bridge": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/share2nightscout-bridge/-/share2nightscout-bridge-0.2.9.tgz", + "integrity": "sha512-EJf9zGggYHxK4yfhUIg6ES4FTA1RnQ2DG3DUQZR1rPNkNeGjEe2l+NHnCYVzzR9/823A2wbRQuBbN3JO0OaYng==", + "requires": { + "request": "^2.88.0" + }, + "dependencies": { + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "~1.37.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mocha": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz", + "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==", + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.2.0", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=" + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==" + }, + "sshpk": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz", + "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "shiro-trie": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/shiro-trie/-/shiro-trie-0.4.10.tgz", + "integrity": "sha512-W9uKR/lYlyBko88K5lcm8/zpmDLjS7HZsWB+cyhLLsqg+Wdr3sQIwVvDGtmZ/F63rkRjaQkHGTBDYGWQR50UQw==" + }, + "should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "simple-statistics": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-0.7.0.tgz", + "integrity": "sha512-z3a2qaG5uau4gmphKiFgkbkuyLcQP3SFI7RuJv+kxMN9CpCg4QOzteR1fzRTflZwM5RJxeLfYevP3kvAS4dYJA==" + }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + } + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "socket.io": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz", + "integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.2.1", + "socket.io-adapter": "~2.4.0", + "socket.io-parser": "~4.2.1" + } + }, + "socket.io-adapter": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", + "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" + }, + "socket.io-client": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.4.tgz", + "integrity": "sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.2.3", + "socket.io-parser": "~4.2.1" + } + }, + "socket.io-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", + "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, + "spawn-wrap": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "dev": true + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "requires": { + "through": "2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==" + }, + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "requires": { + "duplexer": "~0.1.1" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "style-loader": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", + "integrity": "sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==", + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "supertest": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", + "integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "swagger-ui-dist": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", + "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + }, + "swagger-ui-express": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz", + "integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==", + "requires": { + "swagger-ui-dist": ">=4.11.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "table": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" + }, + "terser": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.1.tgz", + "integrity": "sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==", + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "requires": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + }, + "dependencies": { + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "requires": { + "punycode": "^2.1.0" + } + }, + "traverse": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", + "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "webpack": { + "version": "5.75.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", + "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-bundle-analyzer": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", + "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==", + "dev": true, + "requires": { + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "dependencies": { + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true + } + } + }, + "webpack-cli": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", + "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.2.0", + "@webpack-cli/info": "^1.5.0", + "@webpack-cli/serve": "^1.7.0", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "cross-spawn": "^7.0.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + } + } + }, + "webpack-dev-middleware": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", + "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "mem": "^8.1.1", + "memfs": "^3.2.2", + "mime-types": "^2.1.30", + "range-parser": "^1.2.1", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-hot-middleware": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.25.3.tgz", + "integrity": "sha512-IK/0WAHs7MTu1tzLTjio73LjS3Ov+VvBKQmE8WPlJutgG5zT6Urgq/BbAdRrHTRpyzK0dvAvFh1Qg98akxgZpA==", + "dev": true, + "requires": { + "ansi-html-community": "0.0.8", + "html-entities": "^2.1.0", + "strip-ansi": "^6.0.0" + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, + "workerpool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", + "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + }, + "xstate": { + "version": "4.38.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.2.tgz", + "integrity": "sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json index e02fd5c545c..fc22befa182 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "Nightscout", - "version": "0.8.2-beta2", + "name": "nightscout", + "version": "15.0.3", "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", @@ -16,18 +16,39 @@ "type": "git", "url": "https://github.com/nightscout/cgm-remote-monitor.git" }, - "contributors": { - "name": "Nightscout Team", - "url": "https://github.com/nightscout/cgm-remote-monitor/graphs/contributors" - }, + "contributors": [ + { + "name": "Nightscout Team", + "url": "https://github.com/nightscout/cgm-remote-monitor/graphs/contributors" + } + ], "bugs": { "url": "https://github.com/nightscout/cgm-remote-monitor/issues" }, "scripts": { - "start": "node server.js", - "test": "make test", + "start": "node lib/server/server.js", + "test": "env-cmd -f ./my.test.env mocha --timeout 5000 --require ./tests/hooks.js -exit ./tests/*.test.js", + "test-single": "env-cmd -f ./my.test.env mocha --timeout 5000 --require ./tests/hooks.js --exit ./tests/$TEST.test.js", + "test-ci": "env-cmd -f ./tests/ci.test.env nyc --reporter=lcov --reporter=text-summary mocha --timeout 5000 --require ./tests/hooks.js --exit ./tests/*.test.js", "env": "env", - "postinstall": "node node_modules/bower/bin/bower --allow-root install" + "postinstall": "webpack --mode production --config webpack/webpack.config.js && npm run-script post-generate-keys", + "bundle": "webpack --mode production --config webpack/webpack.config.js && npm run-script post-generate-keys", + "bundle-dev": "webpack --mode development --config webpack/webpack.config.js && npm run-script post-generate-keys", + "bundle-analyzer": "webpack --mode development --config webpack/webpack.config.js --profile --json > stats.json && webpack-bundle-analyzer stats.json", + "post-generate-keys": "node bin/generateRandomString.js >node_modules/.cache/_ns_cache/randomString", + "coverage": "cat ./coverage/lcov.info | env-cmd -f ./tests/ci.test.env codacy-coverage || echo NO COVERAGE", + "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": "lib/server/server.js", + "nodemonConfig": { + "ignore": [ + "tests/*", + "node_modules/*", + "bin/*" + ] }, "config": { "blanket": { @@ -44,46 +65,97 @@ } }, "engines": { - "node": "0.10.x" + "node": "^16.x || ^14.x", + "npm": "^6.x" }, "dependencies": { - "async": "^0.9.0", - "body-parser": "^1.4.3", + "@babel/core": "^7.18.10", + "@babel/preset-env": "^7.18.10", + "@parse/node-apn": "^5.1.3", + "acorn": "^8.0.5", + "acorn-jsx": "^5.3.1", + "async": "^0.9.2", + "babel-loader": "^8.2.5", + "body-parser": "^1.19.0", "bootevent": "0.0.1", - "bower": "^1.3.8", - "browserify-express": "^0.1.4", - "compression": "^1.4.2", - "d3": "^3.5.6", - "errorhandler": "^1.1.1", - "event-stream": "~3.1.5", - "expand-braces": "^0.1.1", - "express": "^4.6.1", - "express-extension-to-accept": "0.0.2", - "forever": "~0.13.0", - "git-rev": "git://github.com/bewest/git-rev.git", - "jquery": "^2.1.4", - "lodash": "^3.9.1", - "long": "~2.2.3", - "minimed-connect-to-nightscout": "git://github.com/mddub/minimed-connect-to-nightscout#v0.2.4", - "moment": "2.10.6", - "moment-timezone": "^0.4.0", - "parse-duration": "^0.1.1", - "mongodb": "^2.0.42", - "mqtt": "~0.3.11", - "node-cache": "^3.0.0", - "pushover-notifications": "0.2.0", - "request": "^2.58.0", - "sgvdata": "git://github.com/ktind/sgvdata.git#wip/protobuf", - "share2nightscout-bridge": "git://github.com/bewest/share2nightscout-bridge.git#wip/generalize", - "socket.io": "^1.3.5", + "braces": "^3.0.2", + "buffer": "^6.0.3", + "compression": "^1.7.4", + "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": "^3.1.8", + "errorhandler": "^1.5.1", + "event-stream": "3.3.4", + "expose-loader": "^2.0.0", + "express": "4.17.1", + "express-minify": "^1.0.0", + "fast-password-entropy": "^1.1.1", + "file-loader": "^6.2.0", + "flot": "^0.8.3", + "forwarded-for": "^1.1.0", + "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", + "jsonwebtoken": "^9.0.0", + "lodash": "^4.17.20", + "memory-cache": "^0.2.0", + "mime": "^2.4.6", + "minimed-connect-to-nightscout": "^1.5.5", + "moment": "^2.27.0", + "moment-locales-webpack-plugin": "^1.2.0", + "moment-timezone": "^0.5.31", + "moment-timezone-data-webpack-plugin": "^1.5.0", + "mongo-url-parser": "^1.0.2", + "mongodb": "^3.6.0", + "mongomock": "^0.1.2", + "nightscout-connect": "^0.0.12", + "node-cache": "^4.2.1", + "parse-duration": "^0.1.3", + "process": "^0.11.10", + "pushover-notifications": "^1.2.2", + "random-token": "0.0.8", + "request": "^2.88.2", + "semver": "^6.3.0", + "share2nightscout-bridge": "^0.2.9", + "shiro-trie": "^0.4.9", + "simple-statistics": "^0.7.0", + "socket.io": "~4.5.4", + "socket.io-client": "^4.5.4", + "stream-browserify": "^3.0.0", + "style-loader": "^0.23.1", + "swagger-ui-dist": "^4.13.2", + "swagger-ui-express": "^4.5.0", "traverse": "^0.6.6", - "simple-statistics": "~0.7.0" + "uuid": "^9.0.0", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0" }, "devDependencies": { - "istanbul": "~0.3.5", - "mocha": "~1.20.1", - "should": "~4.0.4", - "supertest": "~0.13.0", - "benv": "^1.1.0" + "@types/tough-cookie": "^4.0.0", + "axios": "^0.21.1", + "babel-eslint": "^10.1.0", + "benv": "^3.3.0", + "csv-parse": "^4.12.0", + "env-cmd": "^10.1.0", + "eslint": "^7.19.0", + "eslint-plugin-security": "^1.4.0", + "eslint-webpack-plugin": "^2.7.0", + "mocha": "^8.4.0", + "nodemon": "^2.0.19", + "nyc": "^14.1.1", + "should": "^13.2.3", + "supertest": "^3.4.2", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-dev-middleware": "^4.3.0", + "webpack-hot-middleware": "^2.25.2", + "xml2js": "^0.4.23" } } diff --git a/server.js b/server.js index 94db523ab47..d1c3f30897b 100644 --- a/server.js +++ b/server.js @@ -22,64 +22,5 @@ // the Dexcom SGV data. 'use strict'; -/////////////////////////////////////////////////// -// DB Connection setup and utils -/////////////////////////////////////////////////// +require('./lib/server/server'); -var env = require('./env')( ); -var language = require('./lib/language')(); -var translate = language.set(env.settings.language).translate; - -/////////////////////////////////////////////////// -// setup http server -/////////////////////////////////////////////////// -var PORT = env.PORT; - -function create (app) { - var transport = (env.ssl - ? require('https') : require('http')); - if (env.ssl) { - return transport.createServer(env.ssl, app); - } - return transport.createServer(app); -} - -require('./lib/bootevent')(env).boot(function booted (ctx) { - var app = require('./app')(env, ctx); - var server = create(app).listen(PORT); - console.log(translate('Listening on port'), PORT); - - if (env.MQTT_MONITOR) { - ctx.mqtt = require('./lib/mqtt')(env, ctx); - var es = require('event-stream'); - es.pipeline(ctx.mqtt.entries, ctx.entries.map( ), ctx.mqtt.every(ctx.entries)); - } - - /////////////////////////////////////////////////// - // setup socket io for data and message transmission - /////////////////////////////////////////////////// - var websocket = require('./lib/websocket')(env, ctx, server); - - ctx.bus.on('data-processed', function() { - websocket.update(); - }); - - ctx.bus.on('notification', function(notify) { - websocket.emitNotification(notify); - if (ctx.mqtt) { - ctx.mqtt.emitNotification(notify); - } - }); - - //after startup if there are no alarms send all clear - setTimeout(function sendStartupAllClear () { - var alarm = ctx.notifications.findHighestAlarm(); - if (!alarm) { - ctx.bus.emit('notification', { - clear: true - , title: 'All Clear' - , message: 'Server started without alarms' - }); - } - }, 20000); -}); \ No newline at end of file diff --git a/setup.sh b/setup.sh deleted file mode 100755 index e520a97a025..00000000000 --- a/setup.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -sudo apt-get update -sudo apt-get install -y python-software-properties python g++ make git -sudo add-apt-repository ppa:chris-lea/node.js -sudo apt-get update -sudo apt-get install nodejs - -npm install \ No newline at end of file diff --git a/static/admin/js/admin.js b/static/admin/js/admin.js index 4c6cb027f1d..2421519a40b 100644 --- a/static/admin/js/admin.js +++ b/static/admin/js/admin.js @@ -5,13 +5,10 @@ var client = Nightscout.client; var admin_plugins = Nightscout.admin_plugins; - if (serverSettings === undefined) { - console.error('server settings were not loaded, will not call init'); - } else { - client.init(serverSettings, Nightscout.plugins); - } - - // init HTML code - admin_plugins.createHTML( client ); + client.requiredPermission = '*'; + client.init(function loaded () { + // init HTML code + admin_plugins.createHTML( client ); + }); })(); diff --git a/static/api-docs.html b/static/api-docs.html index 51042494373..dfec386b976 100644 --- a/static/api-docs.html +++ b/static/api-docs.html @@ -1,9 +1,11 @@ + - + - Nightscout API - + Swagger UI: Nightscout API + + @@ -22,88 +24,88 @@ - + + - - - - - - - - - - - - - - - - + - - - - + + + + + - - + + + + + + + - - + + -
     
    -
    +
    + + + + + diff --git a/static/clock.html b/static/clock.html deleted file mode 100644 index 149265cfb13..00000000000 --- a/static/clock.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - Nightscout BG NOW - - - - - -
    -

    -
    - - - - - - - diff --git a/static/colorbrewer/LICENSE.txt b/static/colorbrewer/LICENSE.txt new file mode 100644 index 00000000000..2ac775d6e36 --- /dev/null +++ b/static/colorbrewer/LICENSE.txt @@ -0,0 +1,38 @@ +Apache-Style Software License for ColorBrewer software and ColorBrewer Color +Schemes + +Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State +University. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not +use this file except in compliance with the License. You may obtain a copy of +the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations under +the License. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions as source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. The end-user documentation included with the redistribution, if any, must +include the following acknowledgment: "This product includes color +specifications and designs developed by Cynthia Brewer +(http://colorbrewer.org/)." Alternately, this acknowledgment may appear in the +software itself, if and wherever such third-party acknowledgments normally +appear. + +4. The name "ColorBrewer" must not be used to endorse or promote products +derived from this software without prior written permission. For written +permission, please contact Cynthia Brewer at cbrewer@psu.edu. + +5. Products derived from this software may not be called "ColorBrewer", nor +may "ColorBrewer" appear in their name, without prior written permission of +Cynthia Brewer. diff --git a/static/colorbrewer/README.md b/static/colorbrewer/README.md new file mode 100644 index 00000000000..fed63690d50 --- /dev/null +++ b/static/colorbrewer/README.md @@ -0,0 +1,16 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [colorbrewer](#colorbrewer) + + + +colorbrewer +=========== + +Color specifications and designs developed by Cynthia Brewer (http://colorbrewer2.org/). + +This is a shim module of colorbrewer2 by Cythina Brewer for browserify. + +It is also a shim for the [files provided in d3.js lib](https://github.com/mbostock/d3/tree/master/lib/colorbrewer). diff --git a/static/colorbrewer/colorbrewer.css b/static/colorbrewer/colorbrewer.css new file mode 100644 index 00000000000..408b42160f9 --- /dev/null +++ b/static/colorbrewer/colorbrewer.css @@ -0,0 +1,1691 @@ +/* This product includes color specifications and designs developed by Cynthia Brewer (http://colorbrewer.org/). */ +/* CSS specs as packaged in the D3 library (d3js.org). Please see license at http://colorbrewer.org/export/LICENSE.txt */ +.YlGn .q0-3{fill:rgb(247,252,185)} +.YlGn .q1-3{fill:rgb(173,221,142)} +.YlGn .q2-3{fill:rgb(49,163,84)} +.YlGn .q0-4{fill:rgb(255,255,204)} +.YlGn .q1-4{fill:rgb(194,230,153)} +.YlGn .q2-4{fill:rgb(120,198,121)} +.YlGn .q3-4{fill:rgb(35,132,67)} +.YlGn .q0-5{fill:rgb(255,255,204)} +.YlGn .q1-5{fill:rgb(194,230,153)} +.YlGn .q2-5{fill:rgb(120,198,121)} +.YlGn .q3-5{fill:rgb(49,163,84)} +.YlGn .q4-5{fill:rgb(0,104,55)} +.YlGn .q0-6{fill:rgb(255,255,204)} +.YlGn .q1-6{fill:rgb(217,240,163)} +.YlGn .q2-6{fill:rgb(173,221,142)} +.YlGn .q3-6{fill:rgb(120,198,121)} +.YlGn .q4-6{fill:rgb(49,163,84)} +.YlGn .q5-6{fill:rgb(0,104,55)} +.YlGn .q0-7{fill:rgb(255,255,204)} +.YlGn .q1-7{fill:rgb(217,240,163)} +.YlGn .q2-7{fill:rgb(173,221,142)} +.YlGn .q3-7{fill:rgb(120,198,121)} +.YlGn .q4-7{fill:rgb(65,171,93)} +.YlGn .q5-7{fill:rgb(35,132,67)} +.YlGn .q6-7{fill:rgb(0,90,50)} +.YlGn .q0-8{fill:rgb(255,255,229)} +.YlGn .q1-8{fill:rgb(247,252,185)} +.YlGn .q2-8{fill:rgb(217,240,163)} +.YlGn .q3-8{fill:rgb(173,221,142)} +.YlGn .q4-8{fill:rgb(120,198,121)} +.YlGn .q5-8{fill:rgb(65,171,93)} +.YlGn .q6-8{fill:rgb(35,132,67)} +.YlGn .q7-8{fill:rgb(0,90,50)} +.YlGn .q0-9{fill:rgb(255,255,229)} +.YlGn .q1-9{fill:rgb(247,252,185)} +.YlGn .q2-9{fill:rgb(217,240,163)} +.YlGn .q3-9{fill:rgb(173,221,142)} +.YlGn .q4-9{fill:rgb(120,198,121)} +.YlGn .q5-9{fill:rgb(65,171,93)} +.YlGn .q6-9{fill:rgb(35,132,67)} +.YlGn .q7-9{fill:rgb(0,104,55)} +.YlGn .q8-9{fill:rgb(0,69,41)} +.YlGnBu .q0-3{fill:rgb(237,248,177)} +.YlGnBu .q1-3{fill:rgb(127,205,187)} +.YlGnBu .q2-3{fill:rgb(44,127,184)} +.YlGnBu .q0-4{fill:rgb(255,255,204)} +.YlGnBu .q1-4{fill:rgb(161,218,180)} +.YlGnBu .q2-4{fill:rgb(65,182,196)} +.YlGnBu .q3-4{fill:rgb(34,94,168)} +.YlGnBu .q0-5{fill:rgb(255,255,204)} +.YlGnBu .q1-5{fill:rgb(161,218,180)} +.YlGnBu .q2-5{fill:rgb(65,182,196)} +.YlGnBu .q3-5{fill:rgb(44,127,184)} +.YlGnBu .q4-5{fill:rgb(37,52,148)} +.YlGnBu .q0-6{fill:rgb(255,255,204)} +.YlGnBu .q1-6{fill:rgb(199,233,180)} +.YlGnBu .q2-6{fill:rgb(127,205,187)} +.YlGnBu .q3-6{fill:rgb(65,182,196)} +.YlGnBu .q4-6{fill:rgb(44,127,184)} +.YlGnBu .q5-6{fill:rgb(37,52,148)} +.YlGnBu .q0-7{fill:rgb(255,255,204)} +.YlGnBu .q1-7{fill:rgb(199,233,180)} +.YlGnBu .q2-7{fill:rgb(127,205,187)} +.YlGnBu .q3-7{fill:rgb(65,182,196)} +.YlGnBu .q4-7{fill:rgb(29,145,192)} +.YlGnBu .q5-7{fill:rgb(34,94,168)} +.YlGnBu .q6-7{fill:rgb(12,44,132)} +.YlGnBu .q0-8{fill:rgb(255,255,217)} +.YlGnBu .q1-8{fill:rgb(237,248,177)} +.YlGnBu .q2-8{fill:rgb(199,233,180)} +.YlGnBu .q3-8{fill:rgb(127,205,187)} +.YlGnBu .q4-8{fill:rgb(65,182,196)} +.YlGnBu .q5-8{fill:rgb(29,145,192)} +.YlGnBu .q6-8{fill:rgb(34,94,168)} +.YlGnBu .q7-8{fill:rgb(12,44,132)} +.YlGnBu .q0-9{fill:rgb(255,255,217)} +.YlGnBu .q1-9{fill:rgb(237,248,177)} +.YlGnBu .q2-9{fill:rgb(199,233,180)} +.YlGnBu .q3-9{fill:rgb(127,205,187)} +.YlGnBu .q4-9{fill:rgb(65,182,196)} +.YlGnBu .q5-9{fill:rgb(29,145,192)} +.YlGnBu .q6-9{fill:rgb(34,94,168)} +.YlGnBu .q7-9{fill:rgb(37,52,148)} +.YlGnBu .q8-9{fill:rgb(8,29,88)} +.GnBu .q0-3{fill:rgb(224,243,219)} +.GnBu .q1-3{fill:rgb(168,221,181)} +.GnBu .q2-3{fill:rgb(67,162,202)} +.GnBu .q0-4{fill:rgb(240,249,232)} +.GnBu .q1-4{fill:rgb(186,228,188)} +.GnBu .q2-4{fill:rgb(123,204,196)} +.GnBu .q3-4{fill:rgb(43,140,190)} +.GnBu .q0-5{fill:rgb(240,249,232)} +.GnBu .q1-5{fill:rgb(186,228,188)} +.GnBu .q2-5{fill:rgb(123,204,196)} +.GnBu .q3-5{fill:rgb(67,162,202)} +.GnBu .q4-5{fill:rgb(8,104,172)} +.GnBu .q0-6{fill:rgb(240,249,232)} +.GnBu .q1-6{fill:rgb(204,235,197)} +.GnBu .q2-6{fill:rgb(168,221,181)} +.GnBu .q3-6{fill:rgb(123,204,196)} +.GnBu .q4-6{fill:rgb(67,162,202)} +.GnBu .q5-6{fill:rgb(8,104,172)} +.GnBu .q0-7{fill:rgb(240,249,232)} +.GnBu .q1-7{fill:rgb(204,235,197)} +.GnBu .q2-7{fill:rgb(168,221,181)} +.GnBu .q3-7{fill:rgb(123,204,196)} +.GnBu .q4-7{fill:rgb(78,179,211)} +.GnBu .q5-7{fill:rgb(43,140,190)} +.GnBu .q6-7{fill:rgb(8,88,158)} +.GnBu .q0-8{fill:rgb(247,252,240)} +.GnBu .q1-8{fill:rgb(224,243,219)} +.GnBu .q2-8{fill:rgb(204,235,197)} +.GnBu .q3-8{fill:rgb(168,221,181)} +.GnBu .q4-8{fill:rgb(123,204,196)} +.GnBu .q5-8{fill:rgb(78,179,211)} +.GnBu .q6-8{fill:rgb(43,140,190)} +.GnBu .q7-8{fill:rgb(8,88,158)} +.GnBu .q0-9{fill:rgb(247,252,240)} +.GnBu .q1-9{fill:rgb(224,243,219)} +.GnBu .q2-9{fill:rgb(204,235,197)} +.GnBu .q3-9{fill:rgb(168,221,181)} +.GnBu .q4-9{fill:rgb(123,204,196)} +.GnBu .q5-9{fill:rgb(78,179,211)} +.GnBu .q6-9{fill:rgb(43,140,190)} +.GnBu .q7-9{fill:rgb(8,104,172)} +.GnBu .q8-9{fill:rgb(8,64,129)} +.BuGn .q0-3{fill:rgb(229,245,249)} +.BuGn .q1-3{fill:rgb(153,216,201)} +.BuGn .q2-3{fill:rgb(44,162,95)} +.BuGn .q0-4{fill:rgb(237,248,251)} +.BuGn .q1-4{fill:rgb(178,226,226)} +.BuGn .q2-4{fill:rgb(102,194,164)} +.BuGn .q3-4{fill:rgb(35,139,69)} +.BuGn .q0-5{fill:rgb(237,248,251)} +.BuGn .q1-5{fill:rgb(178,226,226)} +.BuGn .q2-5{fill:rgb(102,194,164)} +.BuGn .q3-5{fill:rgb(44,162,95)} +.BuGn .q4-5{fill:rgb(0,109,44)} +.BuGn .q0-6{fill:rgb(237,248,251)} +.BuGn .q1-6{fill:rgb(204,236,230)} +.BuGn .q2-6{fill:rgb(153,216,201)} +.BuGn .q3-6{fill:rgb(102,194,164)} +.BuGn .q4-6{fill:rgb(44,162,95)} +.BuGn .q5-6{fill:rgb(0,109,44)} +.BuGn .q0-7{fill:rgb(237,248,251)} +.BuGn .q1-7{fill:rgb(204,236,230)} +.BuGn .q2-7{fill:rgb(153,216,201)} +.BuGn .q3-7{fill:rgb(102,194,164)} +.BuGn .q4-7{fill:rgb(65,174,118)} +.BuGn .q5-7{fill:rgb(35,139,69)} +.BuGn .q6-7{fill:rgb(0,88,36)} +.BuGn .q0-8{fill:rgb(247,252,253)} +.BuGn .q1-8{fill:rgb(229,245,249)} +.BuGn .q2-8{fill:rgb(204,236,230)} +.BuGn .q3-8{fill:rgb(153,216,201)} +.BuGn .q4-8{fill:rgb(102,194,164)} +.BuGn .q5-8{fill:rgb(65,174,118)} +.BuGn .q6-8{fill:rgb(35,139,69)} +.BuGn .q7-8{fill:rgb(0,88,36)} +.BuGn .q0-9{fill:rgb(247,252,253)} +.BuGn .q1-9{fill:rgb(229,245,249)} +.BuGn .q2-9{fill:rgb(204,236,230)} +.BuGn .q3-9{fill:rgb(153,216,201)} +.BuGn .q4-9{fill:rgb(102,194,164)} +.BuGn .q5-9{fill:rgb(65,174,118)} +.BuGn .q6-9{fill:rgb(35,139,69)} +.BuGn .q7-9{fill:rgb(0,109,44)} +.BuGn .q8-9{fill:rgb(0,68,27)} +.PuBuGn .q0-3{fill:rgb(236,226,240)} +.PuBuGn .q1-3{fill:rgb(166,189,219)} +.PuBuGn .q2-3{fill:rgb(28,144,153)} +.PuBuGn .q0-4{fill:rgb(246,239,247)} +.PuBuGn .q1-4{fill:rgb(189,201,225)} +.PuBuGn .q2-4{fill:rgb(103,169,207)} +.PuBuGn .q3-4{fill:rgb(2,129,138)} +.PuBuGn .q0-5{fill:rgb(246,239,247)} +.PuBuGn .q1-5{fill:rgb(189,201,225)} +.PuBuGn .q2-5{fill:rgb(103,169,207)} +.PuBuGn .q3-5{fill:rgb(28,144,153)} +.PuBuGn .q4-5{fill:rgb(1,108,89)} +.PuBuGn .q0-6{fill:rgb(246,239,247)} +.PuBuGn .q1-6{fill:rgb(208,209,230)} +.PuBuGn .q2-6{fill:rgb(166,189,219)} +.PuBuGn .q3-6{fill:rgb(103,169,207)} +.PuBuGn .q4-6{fill:rgb(28,144,153)} +.PuBuGn .q5-6{fill:rgb(1,108,89)} +.PuBuGn .q0-7{fill:rgb(246,239,247)} +.PuBuGn .q1-7{fill:rgb(208,209,230)} +.PuBuGn .q2-7{fill:rgb(166,189,219)} +.PuBuGn .q3-7{fill:rgb(103,169,207)} +.PuBuGn .q4-7{fill:rgb(54,144,192)} +.PuBuGn .q5-7{fill:rgb(2,129,138)} +.PuBuGn .q6-7{fill:rgb(1,100,80)} +.PuBuGn .q0-8{fill:rgb(255,247,251)} +.PuBuGn .q1-8{fill:rgb(236,226,240)} +.PuBuGn .q2-8{fill:rgb(208,209,230)} +.PuBuGn .q3-8{fill:rgb(166,189,219)} +.PuBuGn .q4-8{fill:rgb(103,169,207)} +.PuBuGn .q5-8{fill:rgb(54,144,192)} +.PuBuGn .q6-8{fill:rgb(2,129,138)} +.PuBuGn .q7-8{fill:rgb(1,100,80)} +.PuBuGn .q0-9{fill:rgb(255,247,251)} +.PuBuGn .q1-9{fill:rgb(236,226,240)} +.PuBuGn .q2-9{fill:rgb(208,209,230)} +.PuBuGn .q3-9{fill:rgb(166,189,219)} +.PuBuGn .q4-9{fill:rgb(103,169,207)} +.PuBuGn .q5-9{fill:rgb(54,144,192)} +.PuBuGn .q6-9{fill:rgb(2,129,138)} +.PuBuGn .q7-9{fill:rgb(1,108,89)} +.PuBuGn .q8-9{fill:rgb(1,70,54)} +.PuBu .q0-3{fill:rgb(236,231,242)} +.PuBu .q1-3{fill:rgb(166,189,219)} +.PuBu .q2-3{fill:rgb(43,140,190)} +.PuBu .q0-4{fill:rgb(241,238,246)} +.PuBu .q1-4{fill:rgb(189,201,225)} +.PuBu .q2-4{fill:rgb(116,169,207)} +.PuBu .q3-4{fill:rgb(5,112,176)} +.PuBu .q0-5{fill:rgb(241,238,246)} +.PuBu .q1-5{fill:rgb(189,201,225)} +.PuBu .q2-5{fill:rgb(116,169,207)} +.PuBu .q3-5{fill:rgb(43,140,190)} +.PuBu .q4-5{fill:rgb(4,90,141)} +.PuBu .q0-6{fill:rgb(241,238,246)} +.PuBu .q1-6{fill:rgb(208,209,230)} +.PuBu .q2-6{fill:rgb(166,189,219)} +.PuBu .q3-6{fill:rgb(116,169,207)} +.PuBu .q4-6{fill:rgb(43,140,190)} +.PuBu .q5-6{fill:rgb(4,90,141)} +.PuBu .q0-7{fill:rgb(241,238,246)} +.PuBu .q1-7{fill:rgb(208,209,230)} +.PuBu .q2-7{fill:rgb(166,189,219)} +.PuBu .q3-7{fill:rgb(116,169,207)} +.PuBu .q4-7{fill:rgb(54,144,192)} +.PuBu .q5-7{fill:rgb(5,112,176)} +.PuBu .q6-7{fill:rgb(3,78,123)} +.PuBu .q0-8{fill:rgb(255,247,251)} +.PuBu .q1-8{fill:rgb(236,231,242)} +.PuBu .q2-8{fill:rgb(208,209,230)} +.PuBu .q3-8{fill:rgb(166,189,219)} +.PuBu .q4-8{fill:rgb(116,169,207)} +.PuBu .q5-8{fill:rgb(54,144,192)} +.PuBu .q6-8{fill:rgb(5,112,176)} +.PuBu .q7-8{fill:rgb(3,78,123)} +.PuBu .q0-9{fill:rgb(255,247,251)} +.PuBu .q1-9{fill:rgb(236,231,242)} +.PuBu .q2-9{fill:rgb(208,209,230)} +.PuBu .q3-9{fill:rgb(166,189,219)} +.PuBu .q4-9{fill:rgb(116,169,207)} +.PuBu .q5-9{fill:rgb(54,144,192)} +.PuBu .q6-9{fill:rgb(5,112,176)} +.PuBu .q7-9{fill:rgb(4,90,141)} +.PuBu .q8-9{fill:rgb(2,56,88)} +.BuPu .q0-3{fill:rgb(224,236,244)} +.BuPu .q1-3{fill:rgb(158,188,218)} +.BuPu .q2-3{fill:rgb(136,86,167)} +.BuPu .q0-4{fill:rgb(237,248,251)} +.BuPu .q1-4{fill:rgb(179,205,227)} +.BuPu .q2-4{fill:rgb(140,150,198)} +.BuPu .q3-4{fill:rgb(136,65,157)} +.BuPu .q0-5{fill:rgb(237,248,251)} +.BuPu .q1-5{fill:rgb(179,205,227)} +.BuPu .q2-5{fill:rgb(140,150,198)} +.BuPu .q3-5{fill:rgb(136,86,167)} +.BuPu .q4-5{fill:rgb(129,15,124)} +.BuPu .q0-6{fill:rgb(237,248,251)} +.BuPu .q1-6{fill:rgb(191,211,230)} +.BuPu .q2-6{fill:rgb(158,188,218)} +.BuPu .q3-6{fill:rgb(140,150,198)} +.BuPu .q4-6{fill:rgb(136,86,167)} +.BuPu .q5-6{fill:rgb(129,15,124)} +.BuPu .q0-7{fill:rgb(237,248,251)} +.BuPu .q1-7{fill:rgb(191,211,230)} +.BuPu .q2-7{fill:rgb(158,188,218)} +.BuPu .q3-7{fill:rgb(140,150,198)} +.BuPu .q4-7{fill:rgb(140,107,177)} +.BuPu .q5-7{fill:rgb(136,65,157)} +.BuPu .q6-7{fill:rgb(110,1,107)} +.BuPu .q0-8{fill:rgb(247,252,253)} +.BuPu .q1-8{fill:rgb(224,236,244)} +.BuPu .q2-8{fill:rgb(191,211,230)} +.BuPu .q3-8{fill:rgb(158,188,218)} +.BuPu .q4-8{fill:rgb(140,150,198)} +.BuPu .q5-8{fill:rgb(140,107,177)} +.BuPu .q6-8{fill:rgb(136,65,157)} +.BuPu .q7-8{fill:rgb(110,1,107)} +.BuPu .q0-9{fill:rgb(247,252,253)} +.BuPu .q1-9{fill:rgb(224,236,244)} +.BuPu .q2-9{fill:rgb(191,211,230)} +.BuPu .q3-9{fill:rgb(158,188,218)} +.BuPu .q4-9{fill:rgb(140,150,198)} +.BuPu .q5-9{fill:rgb(140,107,177)} +.BuPu .q6-9{fill:rgb(136,65,157)} +.BuPu .q7-9{fill:rgb(129,15,124)} +.BuPu .q8-9{fill:rgb(77,0,75)} +.RdPu .q0-3{fill:rgb(253,224,221)} +.RdPu .q1-3{fill:rgb(250,159,181)} +.RdPu .q2-3{fill:rgb(197,27,138)} +.RdPu .q0-4{fill:rgb(254,235,226)} +.RdPu .q1-4{fill:rgb(251,180,185)} +.RdPu .q2-4{fill:rgb(247,104,161)} +.RdPu .q3-4{fill:rgb(174,1,126)} +.RdPu .q0-5{fill:rgb(254,235,226)} +.RdPu .q1-5{fill:rgb(251,180,185)} +.RdPu .q2-5{fill:rgb(247,104,161)} +.RdPu .q3-5{fill:rgb(197,27,138)} +.RdPu .q4-5{fill:rgb(122,1,119)} +.RdPu .q0-6{fill:rgb(254,235,226)} +.RdPu .q1-6{fill:rgb(252,197,192)} +.RdPu .q2-6{fill:rgb(250,159,181)} +.RdPu .q3-6{fill:rgb(247,104,161)} +.RdPu .q4-6{fill:rgb(197,27,138)} +.RdPu .q5-6{fill:rgb(122,1,119)} +.RdPu .q0-7{fill:rgb(254,235,226)} +.RdPu .q1-7{fill:rgb(252,197,192)} +.RdPu .q2-7{fill:rgb(250,159,181)} +.RdPu .q3-7{fill:rgb(247,104,161)} +.RdPu .q4-7{fill:rgb(221,52,151)} +.RdPu .q5-7{fill:rgb(174,1,126)} +.RdPu .q6-7{fill:rgb(122,1,119)} +.RdPu .q0-8{fill:rgb(255,247,243)} +.RdPu .q1-8{fill:rgb(253,224,221)} +.RdPu .q2-8{fill:rgb(252,197,192)} +.RdPu .q3-8{fill:rgb(250,159,181)} +.RdPu .q4-8{fill:rgb(247,104,161)} +.RdPu .q5-8{fill:rgb(221,52,151)} +.RdPu .q6-8{fill:rgb(174,1,126)} +.RdPu .q7-8{fill:rgb(122,1,119)} +.RdPu .q0-9{fill:rgb(255,247,243)} +.RdPu .q1-9{fill:rgb(253,224,221)} +.RdPu .q2-9{fill:rgb(252,197,192)} +.RdPu .q3-9{fill:rgb(250,159,181)} +.RdPu .q4-9{fill:rgb(247,104,161)} +.RdPu .q5-9{fill:rgb(221,52,151)} +.RdPu .q6-9{fill:rgb(174,1,126)} +.RdPu .q7-9{fill:rgb(122,1,119)} +.RdPu .q8-9{fill:rgb(73,0,106)} +.PuRd .q0-3{fill:rgb(231,225,239)} +.PuRd .q1-3{fill:rgb(201,148,199)} +.PuRd .q2-3{fill:rgb(221,28,119)} +.PuRd .q0-4{fill:rgb(241,238,246)} +.PuRd .q1-4{fill:rgb(215,181,216)} +.PuRd .q2-4{fill:rgb(223,101,176)} +.PuRd .q3-4{fill:rgb(206,18,86)} +.PuRd .q0-5{fill:rgb(241,238,246)} +.PuRd .q1-5{fill:rgb(215,181,216)} +.PuRd .q2-5{fill:rgb(223,101,176)} +.PuRd .q3-5{fill:rgb(221,28,119)} +.PuRd .q4-5{fill:rgb(152,0,67)} +.PuRd .q0-6{fill:rgb(241,238,246)} +.PuRd .q1-6{fill:rgb(212,185,218)} +.PuRd .q2-6{fill:rgb(201,148,199)} +.PuRd .q3-6{fill:rgb(223,101,176)} +.PuRd .q4-6{fill:rgb(221,28,119)} +.PuRd .q5-6{fill:rgb(152,0,67)} +.PuRd .q0-7{fill:rgb(241,238,246)} +.PuRd .q1-7{fill:rgb(212,185,218)} +.PuRd .q2-7{fill:rgb(201,148,199)} +.PuRd .q3-7{fill:rgb(223,101,176)} +.PuRd .q4-7{fill:rgb(231,41,138)} +.PuRd .q5-7{fill:rgb(206,18,86)} +.PuRd .q6-7{fill:rgb(145,0,63)} +.PuRd .q0-8{fill:rgb(247,244,249)} +.PuRd .q1-8{fill:rgb(231,225,239)} +.PuRd .q2-8{fill:rgb(212,185,218)} +.PuRd .q3-8{fill:rgb(201,148,199)} +.PuRd .q4-8{fill:rgb(223,101,176)} +.PuRd .q5-8{fill:rgb(231,41,138)} +.PuRd .q6-8{fill:rgb(206,18,86)} +.PuRd .q7-8{fill:rgb(145,0,63)} +.PuRd .q0-9{fill:rgb(247,244,249)} +.PuRd .q1-9{fill:rgb(231,225,239)} +.PuRd .q2-9{fill:rgb(212,185,218)} +.PuRd .q3-9{fill:rgb(201,148,199)} +.PuRd .q4-9{fill:rgb(223,101,176)} +.PuRd .q5-9{fill:rgb(231,41,138)} +.PuRd .q6-9{fill:rgb(206,18,86)} +.PuRd .q7-9{fill:rgb(152,0,67)} +.PuRd .q8-9{fill:rgb(103,0,31)} +.OrRd .q0-3{fill:rgb(254,232,200)} +.OrRd .q1-3{fill:rgb(253,187,132)} +.OrRd .q2-3{fill:rgb(227,74,51)} +.OrRd .q0-4{fill:rgb(254,240,217)} +.OrRd .q1-4{fill:rgb(253,204,138)} +.OrRd .q2-4{fill:rgb(252,141,89)} +.OrRd .q3-4{fill:rgb(215,48,31)} +.OrRd .q0-5{fill:rgb(254,240,217)} +.OrRd .q1-5{fill:rgb(253,204,138)} +.OrRd .q2-5{fill:rgb(252,141,89)} +.OrRd .q3-5{fill:rgb(227,74,51)} +.OrRd .q4-5{fill:rgb(179,0,0)} +.OrRd .q0-6{fill:rgb(254,240,217)} +.OrRd .q1-6{fill:rgb(253,212,158)} +.OrRd .q2-6{fill:rgb(253,187,132)} +.OrRd .q3-6{fill:rgb(252,141,89)} +.OrRd .q4-6{fill:rgb(227,74,51)} +.OrRd .q5-6{fill:rgb(179,0,0)} +.OrRd .q0-7{fill:rgb(254,240,217)} +.OrRd .q1-7{fill:rgb(253,212,158)} +.OrRd .q2-7{fill:rgb(253,187,132)} +.OrRd .q3-7{fill:rgb(252,141,89)} +.OrRd .q4-7{fill:rgb(239,101,72)} +.OrRd .q5-7{fill:rgb(215,48,31)} +.OrRd .q6-7{fill:rgb(153,0,0)} +.OrRd .q0-8{fill:rgb(255,247,236)} +.OrRd .q1-8{fill:rgb(254,232,200)} +.OrRd .q2-8{fill:rgb(253,212,158)} +.OrRd .q3-8{fill:rgb(253,187,132)} +.OrRd .q4-8{fill:rgb(252,141,89)} +.OrRd .q5-8{fill:rgb(239,101,72)} +.OrRd .q6-8{fill:rgb(215,48,31)} +.OrRd .q7-8{fill:rgb(153,0,0)} +.OrRd .q0-9{fill:rgb(255,247,236)} +.OrRd .q1-9{fill:rgb(254,232,200)} +.OrRd .q2-9{fill:rgb(253,212,158)} +.OrRd .q3-9{fill:rgb(253,187,132)} +.OrRd .q4-9{fill:rgb(252,141,89)} +.OrRd .q5-9{fill:rgb(239,101,72)} +.OrRd .q6-9{fill:rgb(215,48,31)} +.OrRd .q7-9{fill:rgb(179,0,0)} +.OrRd .q8-9{fill:rgb(127,0,0)} +.YlOrRd .q0-3{fill:rgb(255,237,160)} +.YlOrRd .q1-3{fill:rgb(254,178,76)} +.YlOrRd .q2-3{fill:rgb(240,59,32)} +.YlOrRd .q0-4{fill:rgb(255,255,178)} +.YlOrRd .q1-4{fill:rgb(254,204,92)} +.YlOrRd .q2-4{fill:rgb(253,141,60)} +.YlOrRd .q3-4{fill:rgb(227,26,28)} +.YlOrRd .q0-5{fill:rgb(255,255,178)} +.YlOrRd .q1-5{fill:rgb(254,204,92)} +.YlOrRd .q2-5{fill:rgb(253,141,60)} +.YlOrRd .q3-5{fill:rgb(240,59,32)} +.YlOrRd .q4-5{fill:rgb(189,0,38)} +.YlOrRd .q0-6{fill:rgb(255,255,178)} +.YlOrRd .q1-6{fill:rgb(254,217,118)} +.YlOrRd .q2-6{fill:rgb(254,178,76)} +.YlOrRd .q3-6{fill:rgb(253,141,60)} +.YlOrRd .q4-6{fill:rgb(240,59,32)} +.YlOrRd .q5-6{fill:rgb(189,0,38)} +.YlOrRd .q0-7{fill:rgb(255,255,178)} +.YlOrRd .q1-7{fill:rgb(254,217,118)} +.YlOrRd .q2-7{fill:rgb(254,178,76)} +.YlOrRd .q3-7{fill:rgb(253,141,60)} +.YlOrRd .q4-7{fill:rgb(252,78,42)} +.YlOrRd .q5-7{fill:rgb(227,26,28)} +.YlOrRd .q6-7{fill:rgb(177,0,38)} +.YlOrRd .q0-8{fill:rgb(255,255,204)} +.YlOrRd .q1-8{fill:rgb(255,237,160)} +.YlOrRd .q2-8{fill:rgb(254,217,118)} +.YlOrRd .q3-8{fill:rgb(254,178,76)} +.YlOrRd .q4-8{fill:rgb(253,141,60)} +.YlOrRd .q5-8{fill:rgb(252,78,42)} +.YlOrRd .q6-8{fill:rgb(227,26,28)} +.YlOrRd .q7-8{fill:rgb(177,0,38)} +.YlOrRd .q0-9{fill:rgb(255,255,204)} +.YlOrRd .q1-9{fill:rgb(255,237,160)} +.YlOrRd .q2-9{fill:rgb(254,217,118)} +.YlOrRd .q3-9{fill:rgb(254,178,76)} +.YlOrRd .q4-9{fill:rgb(253,141,60)} +.YlOrRd .q5-9{fill:rgb(252,78,42)} +.YlOrRd .q6-9{fill:rgb(227,26,28)} +.YlOrRd .q7-9{fill:rgb(189,0,38)} +.YlOrRd .q8-9{fill:rgb(128,0,38)} +.YlOrBr .q0-3{fill:rgb(255,247,188)} +.YlOrBr .q1-3{fill:rgb(254,196,79)} +.YlOrBr .q2-3{fill:rgb(217,95,14)} +.YlOrBr .q0-4{fill:rgb(255,255,212)} +.YlOrBr .q1-4{fill:rgb(254,217,142)} +.YlOrBr .q2-4{fill:rgb(254,153,41)} +.YlOrBr .q3-4{fill:rgb(204,76,2)} +.YlOrBr .q0-5{fill:rgb(255,255,212)} +.YlOrBr .q1-5{fill:rgb(254,217,142)} +.YlOrBr .q2-5{fill:rgb(254,153,41)} +.YlOrBr .q3-5{fill:rgb(217,95,14)} +.YlOrBr .q4-5{fill:rgb(153,52,4)} +.YlOrBr .q0-6{fill:rgb(255,255,212)} +.YlOrBr .q1-6{fill:rgb(254,227,145)} +.YlOrBr .q2-6{fill:rgb(254,196,79)} +.YlOrBr .q3-6{fill:rgb(254,153,41)} +.YlOrBr .q4-6{fill:rgb(217,95,14)} +.YlOrBr .q5-6{fill:rgb(153,52,4)} +.YlOrBr .q0-7{fill:rgb(255,255,212)} +.YlOrBr .q1-7{fill:rgb(254,227,145)} +.YlOrBr .q2-7{fill:rgb(254,196,79)} +.YlOrBr .q3-7{fill:rgb(254,153,41)} +.YlOrBr .q4-7{fill:rgb(236,112,20)} +.YlOrBr .q5-7{fill:rgb(204,76,2)} +.YlOrBr .q6-7{fill:rgb(140,45,4)} +.YlOrBr .q0-8{fill:rgb(255,255,229)} +.YlOrBr .q1-8{fill:rgb(255,247,188)} +.YlOrBr .q2-8{fill:rgb(254,227,145)} +.YlOrBr .q3-8{fill:rgb(254,196,79)} +.YlOrBr .q4-8{fill:rgb(254,153,41)} +.YlOrBr .q5-8{fill:rgb(236,112,20)} +.YlOrBr .q6-8{fill:rgb(204,76,2)} +.YlOrBr .q7-8{fill:rgb(140,45,4)} +.YlOrBr .q0-9{fill:rgb(255,255,229)} +.YlOrBr .q1-9{fill:rgb(255,247,188)} +.YlOrBr .q2-9{fill:rgb(254,227,145)} +.YlOrBr .q3-9{fill:rgb(254,196,79)} +.YlOrBr .q4-9{fill:rgb(254,153,41)} +.YlOrBr .q5-9{fill:rgb(236,112,20)} +.YlOrBr .q6-9{fill:rgb(204,76,2)} +.YlOrBr .q7-9{fill:rgb(153,52,4)} +.YlOrBr .q8-9{fill:rgb(102,37,6)} +.Purples .q0-3{fill:rgb(239,237,245)} +.Purples .q1-3{fill:rgb(188,189,220)} +.Purples .q2-3{fill:rgb(117,107,177)} +.Purples .q0-4{fill:rgb(242,240,247)} +.Purples .q1-4{fill:rgb(203,201,226)} +.Purples .q2-4{fill:rgb(158,154,200)} +.Purples .q3-4{fill:rgb(106,81,163)} +.Purples .q0-5{fill:rgb(242,240,247)} +.Purples .q1-5{fill:rgb(203,201,226)} +.Purples .q2-5{fill:rgb(158,154,200)} +.Purples .q3-5{fill:rgb(117,107,177)} +.Purples .q4-5{fill:rgb(84,39,143)} +.Purples .q0-6{fill:rgb(242,240,247)} +.Purples .q1-6{fill:rgb(218,218,235)} +.Purples .q2-6{fill:rgb(188,189,220)} +.Purples .q3-6{fill:rgb(158,154,200)} +.Purples .q4-6{fill:rgb(117,107,177)} +.Purples .q5-6{fill:rgb(84,39,143)} +.Purples .q0-7{fill:rgb(242,240,247)} +.Purples .q1-7{fill:rgb(218,218,235)} +.Purples .q2-7{fill:rgb(188,189,220)} +.Purples .q3-7{fill:rgb(158,154,200)} +.Purples .q4-7{fill:rgb(128,125,186)} +.Purples .q5-7{fill:rgb(106,81,163)} +.Purples .q6-7{fill:rgb(74,20,134)} +.Purples .q0-8{fill:rgb(252,251,253)} +.Purples .q1-8{fill:rgb(239,237,245)} +.Purples .q2-8{fill:rgb(218,218,235)} +.Purples .q3-8{fill:rgb(188,189,220)} +.Purples .q4-8{fill:rgb(158,154,200)} +.Purples .q5-8{fill:rgb(128,125,186)} +.Purples .q6-8{fill:rgb(106,81,163)} +.Purples .q7-8{fill:rgb(74,20,134)} +.Purples .q0-9{fill:rgb(252,251,253)} +.Purples .q1-9{fill:rgb(239,237,245)} +.Purples .q2-9{fill:rgb(218,218,235)} +.Purples .q3-9{fill:rgb(188,189,220)} +.Purples .q4-9{fill:rgb(158,154,200)} +.Purples .q5-9{fill:rgb(128,125,186)} +.Purples .q6-9{fill:rgb(106,81,163)} +.Purples .q7-9{fill:rgb(84,39,143)} +.Purples .q8-9{fill:rgb(63,0,125)} +.Blues .q0-3{fill:rgb(222,235,247)} +.Blues .q1-3{fill:rgb(158,202,225)} +.Blues .q2-3{fill:rgb(49,130,189)} +.Blues .q0-4{fill:rgb(239,243,255)} +.Blues .q1-4{fill:rgb(189,215,231)} +.Blues .q2-4{fill:rgb(107,174,214)} +.Blues .q3-4{fill:rgb(33,113,181)} +.Blues .q0-5{fill:rgb(239,243,255)} +.Blues .q1-5{fill:rgb(189,215,231)} +.Blues .q2-5{fill:rgb(107,174,214)} +.Blues .q3-5{fill:rgb(49,130,189)} +.Blues .q4-5{fill:rgb(8,81,156)} +.Blues .q0-6{fill:rgb(239,243,255)} +.Blues .q1-6{fill:rgb(198,219,239)} +.Blues .q2-6{fill:rgb(158,202,225)} +.Blues .q3-6{fill:rgb(107,174,214)} +.Blues .q4-6{fill:rgb(49,130,189)} +.Blues .q5-6{fill:rgb(8,81,156)} +.Blues .q0-7{fill:rgb(239,243,255)} +.Blues .q1-7{fill:rgb(198,219,239)} +.Blues .q2-7{fill:rgb(158,202,225)} +.Blues .q3-7{fill:rgb(107,174,214)} +.Blues .q4-7{fill:rgb(66,146,198)} +.Blues .q5-7{fill:rgb(33,113,181)} +.Blues .q6-7{fill:rgb(8,69,148)} +.Blues .q0-8{fill:rgb(247,251,255)} +.Blues .q1-8{fill:rgb(222,235,247)} +.Blues .q2-8{fill:rgb(198,219,239)} +.Blues .q3-8{fill:rgb(158,202,225)} +.Blues .q4-8{fill:rgb(107,174,214)} +.Blues .q5-8{fill:rgb(66,146,198)} +.Blues .q6-8{fill:rgb(33,113,181)} +.Blues .q7-8{fill:rgb(8,69,148)} +.Blues .q0-9{fill:rgb(247,251,255)} +.Blues .q1-9{fill:rgb(222,235,247)} +.Blues .q2-9{fill:rgb(198,219,239)} +.Blues .q3-9{fill:rgb(158,202,225)} +.Blues .q4-9{fill:rgb(107,174,214)} +.Blues .q5-9{fill:rgb(66,146,198)} +.Blues .q6-9{fill:rgb(33,113,181)} +.Blues .q7-9{fill:rgb(8,81,156)} +.Blues .q8-9{fill:rgb(8,48,107)} +.Greens .q0-3{fill:rgb(229,245,224)} +.Greens .q1-3{fill:rgb(161,217,155)} +.Greens .q2-3{fill:rgb(49,163,84)} +.Greens .q0-4{fill:rgb(237,248,233)} +.Greens .q1-4{fill:rgb(186,228,179)} +.Greens .q2-4{fill:rgb(116,196,118)} +.Greens .q3-4{fill:rgb(35,139,69)} +.Greens .q0-5{fill:rgb(237,248,233)} +.Greens .q1-5{fill:rgb(186,228,179)} +.Greens .q2-5{fill:rgb(116,196,118)} +.Greens .q3-5{fill:rgb(49,163,84)} +.Greens .q4-5{fill:rgb(0,109,44)} +.Greens .q0-6{fill:rgb(237,248,233)} +.Greens .q1-6{fill:rgb(199,233,192)} +.Greens .q2-6{fill:rgb(161,217,155)} +.Greens .q3-6{fill:rgb(116,196,118)} +.Greens .q4-6{fill:rgb(49,163,84)} +.Greens .q5-6{fill:rgb(0,109,44)} +.Greens .q0-7{fill:rgb(237,248,233)} +.Greens .q1-7{fill:rgb(199,233,192)} +.Greens .q2-7{fill:rgb(161,217,155)} +.Greens .q3-7{fill:rgb(116,196,118)} +.Greens .q4-7{fill:rgb(65,171,93)} +.Greens .q5-7{fill:rgb(35,139,69)} +.Greens .q6-7{fill:rgb(0,90,50)} +.Greens .q0-8{fill:rgb(247,252,245)} +.Greens .q1-8{fill:rgb(229,245,224)} +.Greens .q2-8{fill:rgb(199,233,192)} +.Greens .q3-8{fill:rgb(161,217,155)} +.Greens .q4-8{fill:rgb(116,196,118)} +.Greens .q5-8{fill:rgb(65,171,93)} +.Greens .q6-8{fill:rgb(35,139,69)} +.Greens .q7-8{fill:rgb(0,90,50)} +.Greens .q0-9{fill:rgb(247,252,245)} +.Greens .q1-9{fill:rgb(229,245,224)} +.Greens .q2-9{fill:rgb(199,233,192)} +.Greens .q3-9{fill:rgb(161,217,155)} +.Greens .q4-9{fill:rgb(116,196,118)} +.Greens .q5-9{fill:rgb(65,171,93)} +.Greens .q6-9{fill:rgb(35,139,69)} +.Greens .q7-9{fill:rgb(0,109,44)} +.Greens .q8-9{fill:rgb(0,68,27)} +.Oranges .q0-3{fill:rgb(254,230,206)} +.Oranges .q1-3{fill:rgb(253,174,107)} +.Oranges .q2-3{fill:rgb(230,85,13)} +.Oranges .q0-4{fill:rgb(254,237,222)} +.Oranges .q1-4{fill:rgb(253,190,133)} +.Oranges .q2-4{fill:rgb(253,141,60)} +.Oranges .q3-4{fill:rgb(217,71,1)} +.Oranges .q0-5{fill:rgb(254,237,222)} +.Oranges .q1-5{fill:rgb(253,190,133)} +.Oranges .q2-5{fill:rgb(253,141,60)} +.Oranges .q3-5{fill:rgb(230,85,13)} +.Oranges .q4-5{fill:rgb(166,54,3)} +.Oranges .q0-6{fill:rgb(254,237,222)} +.Oranges .q1-6{fill:rgb(253,208,162)} +.Oranges .q2-6{fill:rgb(253,174,107)} +.Oranges .q3-6{fill:rgb(253,141,60)} +.Oranges .q4-6{fill:rgb(230,85,13)} +.Oranges .q5-6{fill:rgb(166,54,3)} +.Oranges .q0-7{fill:rgb(254,237,222)} +.Oranges .q1-7{fill:rgb(253,208,162)} +.Oranges .q2-7{fill:rgb(253,174,107)} +.Oranges .q3-7{fill:rgb(253,141,60)} +.Oranges .q4-7{fill:rgb(241,105,19)} +.Oranges .q5-7{fill:rgb(217,72,1)} +.Oranges .q6-7{fill:rgb(140,45,4)} +.Oranges .q0-8{fill:rgb(255,245,235)} +.Oranges .q1-8{fill:rgb(254,230,206)} +.Oranges .q2-8{fill:rgb(253,208,162)} +.Oranges .q3-8{fill:rgb(253,174,107)} +.Oranges .q4-8{fill:rgb(253,141,60)} +.Oranges .q5-8{fill:rgb(241,105,19)} +.Oranges .q6-8{fill:rgb(217,72,1)} +.Oranges .q7-8{fill:rgb(140,45,4)} +.Oranges .q0-9{fill:rgb(255,245,235)} +.Oranges .q1-9{fill:rgb(254,230,206)} +.Oranges .q2-9{fill:rgb(253,208,162)} +.Oranges .q3-9{fill:rgb(253,174,107)} +.Oranges .q4-9{fill:rgb(253,141,60)} +.Oranges .q5-9{fill:rgb(241,105,19)} +.Oranges .q6-9{fill:rgb(217,72,1)} +.Oranges .q7-9{fill:rgb(166,54,3)} +.Oranges .q8-9{fill:rgb(127,39,4)} +.Reds .q0-3{fill:rgb(254,224,210)} +.Reds .q1-3{fill:rgb(252,146,114)} +.Reds .q2-3{fill:rgb(222,45,38)} +.Reds .q0-4{fill:rgb(254,229,217)} +.Reds .q1-4{fill:rgb(252,174,145)} +.Reds .q2-4{fill:rgb(251,106,74)} +.Reds .q3-4{fill:rgb(203,24,29)} +.Reds .q0-5{fill:rgb(254,229,217)} +.Reds .q1-5{fill:rgb(252,174,145)} +.Reds .q2-5{fill:rgb(251,106,74)} +.Reds .q3-5{fill:rgb(222,45,38)} +.Reds .q4-5{fill:rgb(165,15,21)} +.Reds .q0-6{fill:rgb(254,229,217)} +.Reds .q1-6{fill:rgb(252,187,161)} +.Reds .q2-6{fill:rgb(252,146,114)} +.Reds .q3-6{fill:rgb(251,106,74)} +.Reds .q4-6{fill:rgb(222,45,38)} +.Reds .q5-6{fill:rgb(165,15,21)} +.Reds .q0-7{fill:rgb(254,229,217)} +.Reds .q1-7{fill:rgb(252,187,161)} +.Reds .q2-7{fill:rgb(252,146,114)} +.Reds .q3-7{fill:rgb(251,106,74)} +.Reds .q4-7{fill:rgb(239,59,44)} +.Reds .q5-7{fill:rgb(203,24,29)} +.Reds .q6-7{fill:rgb(153,0,13)} +.Reds .q0-8{fill:rgb(255,245,240)} +.Reds .q1-8{fill:rgb(254,224,210)} +.Reds .q2-8{fill:rgb(252,187,161)} +.Reds .q3-8{fill:rgb(252,146,114)} +.Reds .q4-8{fill:rgb(251,106,74)} +.Reds .q5-8{fill:rgb(239,59,44)} +.Reds .q6-8{fill:rgb(203,24,29)} +.Reds .q7-8{fill:rgb(153,0,13)} +.Reds .q0-9{fill:rgb(255,245,240)} +.Reds .q1-9{fill:rgb(254,224,210)} +.Reds .q2-9{fill:rgb(252,187,161)} +.Reds .q3-9{fill:rgb(252,146,114)} +.Reds .q4-9{fill:rgb(251,106,74)} +.Reds .q5-9{fill:rgb(239,59,44)} +.Reds .q6-9{fill:rgb(203,24,29)} +.Reds .q7-9{fill:rgb(165,15,21)} +.Reds .q8-9{fill:rgb(103,0,13)} +.Greys .q0-3{fill:rgb(240,240,240)} +.Greys .q1-3{fill:rgb(189,189,189)} +.Greys .q2-3{fill:rgb(99,99,99)} +.Greys .q0-4{fill:rgb(247,247,247)} +.Greys .q1-4{fill:rgb(204,204,204)} +.Greys .q2-4{fill:rgb(150,150,150)} +.Greys .q3-4{fill:rgb(82,82,82)} +.Greys .q0-5{fill:rgb(247,247,247)} +.Greys .q1-5{fill:rgb(204,204,204)} +.Greys .q2-5{fill:rgb(150,150,150)} +.Greys .q3-5{fill:rgb(99,99,99)} +.Greys .q4-5{fill:rgb(37,37,37)} +.Greys .q0-6{fill:rgb(247,247,247)} +.Greys .q1-6{fill:rgb(217,217,217)} +.Greys .q2-6{fill:rgb(189,189,189)} +.Greys .q3-6{fill:rgb(150,150,150)} +.Greys .q4-6{fill:rgb(99,99,99)} +.Greys .q5-6{fill:rgb(37,37,37)} +.Greys .q0-7{fill:rgb(247,247,247)} +.Greys .q1-7{fill:rgb(217,217,217)} +.Greys .q2-7{fill:rgb(189,189,189)} +.Greys .q3-7{fill:rgb(150,150,150)} +.Greys .q4-7{fill:rgb(115,115,115)} +.Greys .q5-7{fill:rgb(82,82,82)} +.Greys .q6-7{fill:rgb(37,37,37)} +.Greys .q0-8{fill:rgb(255,255,255)} +.Greys .q1-8{fill:rgb(240,240,240)} +.Greys .q2-8{fill:rgb(217,217,217)} +.Greys .q3-8{fill:rgb(189,189,189)} +.Greys .q4-8{fill:rgb(150,150,150)} +.Greys .q5-8{fill:rgb(115,115,115)} +.Greys .q6-8{fill:rgb(82,82,82)} +.Greys .q7-8{fill:rgb(37,37,37)} +.Greys .q0-9{fill:rgb(255,255,255)} +.Greys .q1-9{fill:rgb(240,240,240)} +.Greys .q2-9{fill:rgb(217,217,217)} +.Greys .q3-9{fill:rgb(189,189,189)} +.Greys .q4-9{fill:rgb(150,150,150)} +.Greys .q5-9{fill:rgb(115,115,115)} +.Greys .q6-9{fill:rgb(82,82,82)} +.Greys .q7-9{fill:rgb(37,37,37)} +.Greys .q8-9{fill:rgb(0,0,0)} +.PuOr .q0-3{fill:rgb(241,163,64)} +.PuOr .q1-3{fill:rgb(247,247,247)} +.PuOr .q2-3{fill:rgb(153,142,195)} +.PuOr .q0-4{fill:rgb(230,97,1)} +.PuOr .q1-4{fill:rgb(253,184,99)} +.PuOr .q2-4{fill:rgb(178,171,210)} +.PuOr .q3-4{fill:rgb(94,60,153)} +.PuOr .q0-5{fill:rgb(230,97,1)} +.PuOr .q1-5{fill:rgb(253,184,99)} +.PuOr .q2-5{fill:rgb(247,247,247)} +.PuOr .q3-5{fill:rgb(178,171,210)} +.PuOr .q4-5{fill:rgb(94,60,153)} +.PuOr .q0-6{fill:rgb(179,88,6)} +.PuOr .q1-6{fill:rgb(241,163,64)} +.PuOr .q2-6{fill:rgb(254,224,182)} +.PuOr .q3-6{fill:rgb(216,218,235)} +.PuOr .q4-6{fill:rgb(153,142,195)} +.PuOr .q5-6{fill:rgb(84,39,136)} +.PuOr .q0-7{fill:rgb(179,88,6)} +.PuOr .q1-7{fill:rgb(241,163,64)} +.PuOr .q2-7{fill:rgb(254,224,182)} +.PuOr .q3-7{fill:rgb(247,247,247)} +.PuOr .q4-7{fill:rgb(216,218,235)} +.PuOr .q5-7{fill:rgb(153,142,195)} +.PuOr .q6-7{fill:rgb(84,39,136)} +.PuOr .q0-8{fill:rgb(179,88,6)} +.PuOr .q1-8{fill:rgb(224,130,20)} +.PuOr .q2-8{fill:rgb(253,184,99)} +.PuOr .q3-8{fill:rgb(254,224,182)} +.PuOr .q4-8{fill:rgb(216,218,235)} +.PuOr .q5-8{fill:rgb(178,171,210)} +.PuOr .q6-8{fill:rgb(128,115,172)} +.PuOr .q7-8{fill:rgb(84,39,136)} +.PuOr .q0-9{fill:rgb(179,88,6)} +.PuOr .q1-9{fill:rgb(224,130,20)} +.PuOr .q2-9{fill:rgb(253,184,99)} +.PuOr .q3-9{fill:rgb(254,224,182)} +.PuOr .q4-9{fill:rgb(247,247,247)} +.PuOr .q5-9{fill:rgb(216,218,235)} +.PuOr .q6-9{fill:rgb(178,171,210)} +.PuOr .q7-9{fill:rgb(128,115,172)} +.PuOr .q8-9{fill:rgb(84,39,136)} +.PuOr .q0-10{fill:rgb(127,59,8)} +.PuOr .q1-10{fill:rgb(179,88,6)} +.PuOr .q2-10{fill:rgb(224,130,20)} +.PuOr .q3-10{fill:rgb(253,184,99)} +.PuOr .q4-10{fill:rgb(254,224,182)} +.PuOr .q5-10{fill:rgb(216,218,235)} +.PuOr .q6-10{fill:rgb(178,171,210)} +.PuOr .q7-10{fill:rgb(128,115,172)} +.PuOr .q8-10{fill:rgb(84,39,136)} +.PuOr .q9-10{fill:rgb(45,0,75)} +.PuOr .q0-11{fill:rgb(127,59,8)} +.PuOr .q1-11{fill:rgb(179,88,6)} +.PuOr .q2-11{fill:rgb(224,130,20)} +.PuOr .q3-11{fill:rgb(253,184,99)} +.PuOr .q4-11{fill:rgb(254,224,182)} +.PuOr .q5-11{fill:rgb(247,247,247)} +.PuOr .q6-11{fill:rgb(216,218,235)} +.PuOr .q7-11{fill:rgb(178,171,210)} +.PuOr .q8-11{fill:rgb(128,115,172)} +.PuOr .q9-11{fill:rgb(84,39,136)} +.PuOr .q10-11{fill:rgb(45,0,75)} +.BrBG .q0-3{fill:rgb(216,179,101)} +.BrBG .q1-3{fill:rgb(245,245,245)} +.BrBG .q2-3{fill:rgb(90,180,172)} +.BrBG .q0-4{fill:rgb(166,97,26)} +.BrBG .q1-4{fill:rgb(223,194,125)} +.BrBG .q2-4{fill:rgb(128,205,193)} +.BrBG .q3-4{fill:rgb(1,133,113)} +.BrBG .q0-5{fill:rgb(166,97,26)} +.BrBG .q1-5{fill:rgb(223,194,125)} +.BrBG .q2-5{fill:rgb(245,245,245)} +.BrBG .q3-5{fill:rgb(128,205,193)} +.BrBG .q4-5{fill:rgb(1,133,113)} +.BrBG .q0-6{fill:rgb(140,81,10)} +.BrBG .q1-6{fill:rgb(216,179,101)} +.BrBG .q2-6{fill:rgb(246,232,195)} +.BrBG .q3-6{fill:rgb(199,234,229)} +.BrBG .q4-6{fill:rgb(90,180,172)} +.BrBG .q5-6{fill:rgb(1,102,94)} +.BrBG .q0-7{fill:rgb(140,81,10)} +.BrBG .q1-7{fill:rgb(216,179,101)} +.BrBG .q2-7{fill:rgb(246,232,195)} +.BrBG .q3-7{fill:rgb(245,245,245)} +.BrBG .q4-7{fill:rgb(199,234,229)} +.BrBG .q5-7{fill:rgb(90,180,172)} +.BrBG .q6-7{fill:rgb(1,102,94)} +.BrBG .q0-8{fill:rgb(140,81,10)} +.BrBG .q1-8{fill:rgb(191,129,45)} +.BrBG .q2-8{fill:rgb(223,194,125)} +.BrBG .q3-8{fill:rgb(246,232,195)} +.BrBG .q4-8{fill:rgb(199,234,229)} +.BrBG .q5-8{fill:rgb(128,205,193)} +.BrBG .q6-8{fill:rgb(53,151,143)} +.BrBG .q7-8{fill:rgb(1,102,94)} +.BrBG .q0-9{fill:rgb(140,81,10)} +.BrBG .q1-9{fill:rgb(191,129,45)} +.BrBG .q2-9{fill:rgb(223,194,125)} +.BrBG .q3-9{fill:rgb(246,232,195)} +.BrBG .q4-9{fill:rgb(245,245,245)} +.BrBG .q5-9{fill:rgb(199,234,229)} +.BrBG .q6-9{fill:rgb(128,205,193)} +.BrBG .q7-9{fill:rgb(53,151,143)} +.BrBG .q8-9{fill:rgb(1,102,94)} +.BrBG .q0-10{fill:rgb(84,48,5)} +.BrBG .q1-10{fill:rgb(140,81,10)} +.BrBG .q2-10{fill:rgb(191,129,45)} +.BrBG .q3-10{fill:rgb(223,194,125)} +.BrBG .q4-10{fill:rgb(246,232,195)} +.BrBG .q5-10{fill:rgb(199,234,229)} +.BrBG .q6-10{fill:rgb(128,205,193)} +.BrBG .q7-10{fill:rgb(53,151,143)} +.BrBG .q8-10{fill:rgb(1,102,94)} +.BrBG .q9-10{fill:rgb(0,60,48)} +.BrBG .q0-11{fill:rgb(84,48,5)} +.BrBG .q1-11{fill:rgb(140,81,10)} +.BrBG .q2-11{fill:rgb(191,129,45)} +.BrBG .q3-11{fill:rgb(223,194,125)} +.BrBG .q4-11{fill:rgb(246,232,195)} +.BrBG .q5-11{fill:rgb(245,245,245)} +.BrBG .q6-11{fill:rgb(199,234,229)} +.BrBG .q7-11{fill:rgb(128,205,193)} +.BrBG .q8-11{fill:rgb(53,151,143)} +.BrBG .q9-11{fill:rgb(1,102,94)} +.BrBG .q10-11{fill:rgb(0,60,48)} +.PRGn .q0-3{fill:rgb(175,141,195)} +.PRGn .q1-3{fill:rgb(247,247,247)} +.PRGn .q2-3{fill:rgb(127,191,123)} +.PRGn .q0-4{fill:rgb(123,50,148)} +.PRGn .q1-4{fill:rgb(194,165,207)} +.PRGn .q2-4{fill:rgb(166,219,160)} +.PRGn .q3-4{fill:rgb(0,136,55)} +.PRGn .q0-5{fill:rgb(123,50,148)} +.PRGn .q1-5{fill:rgb(194,165,207)} +.PRGn .q2-5{fill:rgb(247,247,247)} +.PRGn .q3-5{fill:rgb(166,219,160)} +.PRGn .q4-5{fill:rgb(0,136,55)} +.PRGn .q0-6{fill:rgb(118,42,131)} +.PRGn .q1-6{fill:rgb(175,141,195)} +.PRGn .q2-6{fill:rgb(231,212,232)} +.PRGn .q3-6{fill:rgb(217,240,211)} +.PRGn .q4-6{fill:rgb(127,191,123)} +.PRGn .q5-6{fill:rgb(27,120,55)} +.PRGn .q0-7{fill:rgb(118,42,131)} +.PRGn .q1-7{fill:rgb(175,141,195)} +.PRGn .q2-7{fill:rgb(231,212,232)} +.PRGn .q3-7{fill:rgb(247,247,247)} +.PRGn .q4-7{fill:rgb(217,240,211)} +.PRGn .q5-7{fill:rgb(127,191,123)} +.PRGn .q6-7{fill:rgb(27,120,55)} +.PRGn .q0-8{fill:rgb(118,42,131)} +.PRGn .q1-8{fill:rgb(153,112,171)} +.PRGn .q2-8{fill:rgb(194,165,207)} +.PRGn .q3-8{fill:rgb(231,212,232)} +.PRGn .q4-8{fill:rgb(217,240,211)} +.PRGn .q5-8{fill:rgb(166,219,160)} +.PRGn .q6-8{fill:rgb(90,174,97)} +.PRGn .q7-8{fill:rgb(27,120,55)} +.PRGn .q0-9{fill:rgb(118,42,131)} +.PRGn .q1-9{fill:rgb(153,112,171)} +.PRGn .q2-9{fill:rgb(194,165,207)} +.PRGn .q3-9{fill:rgb(231,212,232)} +.PRGn .q4-9{fill:rgb(247,247,247)} +.PRGn .q5-9{fill:rgb(217,240,211)} +.PRGn .q6-9{fill:rgb(166,219,160)} +.PRGn .q7-9{fill:rgb(90,174,97)} +.PRGn .q8-9{fill:rgb(27,120,55)} +.PRGn .q0-10{fill:rgb(64,0,75)} +.PRGn .q1-10{fill:rgb(118,42,131)} +.PRGn .q2-10{fill:rgb(153,112,171)} +.PRGn .q3-10{fill:rgb(194,165,207)} +.PRGn .q4-10{fill:rgb(231,212,232)} +.PRGn .q5-10{fill:rgb(217,240,211)} +.PRGn .q6-10{fill:rgb(166,219,160)} +.PRGn .q7-10{fill:rgb(90,174,97)} +.PRGn .q8-10{fill:rgb(27,120,55)} +.PRGn .q9-10{fill:rgb(0,68,27)} +.PRGn .q0-11{fill:rgb(64,0,75)} +.PRGn .q1-11{fill:rgb(118,42,131)} +.PRGn .q2-11{fill:rgb(153,112,171)} +.PRGn .q3-11{fill:rgb(194,165,207)} +.PRGn .q4-11{fill:rgb(231,212,232)} +.PRGn .q5-11{fill:rgb(247,247,247)} +.PRGn .q6-11{fill:rgb(217,240,211)} +.PRGn .q7-11{fill:rgb(166,219,160)} +.PRGn .q8-11{fill:rgb(90,174,97)} +.PRGn .q9-11{fill:rgb(27,120,55)} +.PRGn .q10-11{fill:rgb(0,68,27)} +.PiYG .q0-3{fill:rgb(233,163,201)} +.PiYG .q1-3{fill:rgb(247,247,247)} +.PiYG .q2-3{fill:rgb(161,215,106)} +.PiYG .q0-4{fill:rgb(208,28,139)} +.PiYG .q1-4{fill:rgb(241,182,218)} +.PiYG .q2-4{fill:rgb(184,225,134)} +.PiYG .q3-4{fill:rgb(77,172,38)} +.PiYG .q0-5{fill:rgb(208,28,139)} +.PiYG .q1-5{fill:rgb(241,182,218)} +.PiYG .q2-5{fill:rgb(247,247,247)} +.PiYG .q3-5{fill:rgb(184,225,134)} +.PiYG .q4-5{fill:rgb(77,172,38)} +.PiYG .q0-6{fill:rgb(197,27,125)} +.PiYG .q1-6{fill:rgb(233,163,201)} +.PiYG .q2-6{fill:rgb(253,224,239)} +.PiYG .q3-6{fill:rgb(230,245,208)} +.PiYG .q4-6{fill:rgb(161,215,106)} +.PiYG .q5-6{fill:rgb(77,146,33)} +.PiYG .q0-7{fill:rgb(197,27,125)} +.PiYG .q1-7{fill:rgb(233,163,201)} +.PiYG .q2-7{fill:rgb(253,224,239)} +.PiYG .q3-7{fill:rgb(247,247,247)} +.PiYG .q4-7{fill:rgb(230,245,208)} +.PiYG .q5-7{fill:rgb(161,215,106)} +.PiYG .q6-7{fill:rgb(77,146,33)} +.PiYG .q0-8{fill:rgb(197,27,125)} +.PiYG .q1-8{fill:rgb(222,119,174)} +.PiYG .q2-8{fill:rgb(241,182,218)} +.PiYG .q3-8{fill:rgb(253,224,239)} +.PiYG .q4-8{fill:rgb(230,245,208)} +.PiYG .q5-8{fill:rgb(184,225,134)} +.PiYG .q6-8{fill:rgb(127,188,65)} +.PiYG .q7-8{fill:rgb(77,146,33)} +.PiYG .q0-9{fill:rgb(197,27,125)} +.PiYG .q1-9{fill:rgb(222,119,174)} +.PiYG .q2-9{fill:rgb(241,182,218)} +.PiYG .q3-9{fill:rgb(253,224,239)} +.PiYG .q4-9{fill:rgb(247,247,247)} +.PiYG .q5-9{fill:rgb(230,245,208)} +.PiYG .q6-9{fill:rgb(184,225,134)} +.PiYG .q7-9{fill:rgb(127,188,65)} +.PiYG .q8-9{fill:rgb(77,146,33)} +.PiYG .q0-10{fill:rgb(142,1,82)} +.PiYG .q1-10{fill:rgb(197,27,125)} +.PiYG .q2-10{fill:rgb(222,119,174)} +.PiYG .q3-10{fill:rgb(241,182,218)} +.PiYG .q4-10{fill:rgb(253,224,239)} +.PiYG .q5-10{fill:rgb(230,245,208)} +.PiYG .q6-10{fill:rgb(184,225,134)} +.PiYG .q7-10{fill:rgb(127,188,65)} +.PiYG .q8-10{fill:rgb(77,146,33)} +.PiYG .q9-10{fill:rgb(39,100,25)} +.PiYG .q0-11{fill:rgb(142,1,82)} +.PiYG .q1-11{fill:rgb(197,27,125)} +.PiYG .q2-11{fill:rgb(222,119,174)} +.PiYG .q3-11{fill:rgb(241,182,218)} +.PiYG .q4-11{fill:rgb(253,224,239)} +.PiYG .q5-11{fill:rgb(247,247,247)} +.PiYG .q6-11{fill:rgb(230,245,208)} +.PiYG .q7-11{fill:rgb(184,225,134)} +.PiYG .q8-11{fill:rgb(127,188,65)} +.PiYG .q9-11{fill:rgb(77,146,33)} +.PiYG .q10-11{fill:rgb(39,100,25)} +.RdBu .q0-3{fill:rgb(239,138,98)} +.RdBu .q1-3{fill:rgb(247,247,247)} +.RdBu .q2-3{fill:rgb(103,169,207)} +.RdBu .q0-4{fill:rgb(202,0,32)} +.RdBu .q1-4{fill:rgb(244,165,130)} +.RdBu .q2-4{fill:rgb(146,197,222)} +.RdBu .q3-4{fill:rgb(5,113,176)} +.RdBu .q0-5{fill:rgb(202,0,32)} +.RdBu .q1-5{fill:rgb(244,165,130)} +.RdBu .q2-5{fill:rgb(247,247,247)} +.RdBu .q3-5{fill:rgb(146,197,222)} +.RdBu .q4-5{fill:rgb(5,113,176)} +.RdBu .q0-6{fill:rgb(178,24,43)} +.RdBu .q1-6{fill:rgb(239,138,98)} +.RdBu .q2-6{fill:rgb(253,219,199)} +.RdBu .q3-6{fill:rgb(209,229,240)} +.RdBu .q4-6{fill:rgb(103,169,207)} +.RdBu .q5-6{fill:rgb(33,102,172)} +.RdBu .q0-7{fill:rgb(178,24,43)} +.RdBu .q1-7{fill:rgb(239,138,98)} +.RdBu .q2-7{fill:rgb(253,219,199)} +.RdBu .q3-7{fill:rgb(247,247,247)} +.RdBu .q4-7{fill:rgb(209,229,240)} +.RdBu .q5-7{fill:rgb(103,169,207)} +.RdBu .q6-7{fill:rgb(33,102,172)} +.RdBu .q0-8{fill:rgb(178,24,43)} +.RdBu .q1-8{fill:rgb(214,96,77)} +.RdBu .q2-8{fill:rgb(244,165,130)} +.RdBu .q3-8{fill:rgb(253,219,199)} +.RdBu .q4-8{fill:rgb(209,229,240)} +.RdBu .q5-8{fill:rgb(146,197,222)} +.RdBu .q6-8{fill:rgb(67,147,195)} +.RdBu .q7-8{fill:rgb(33,102,172)} +.RdBu .q0-9{fill:rgb(178,24,43)} +.RdBu .q1-9{fill:rgb(214,96,77)} +.RdBu .q2-9{fill:rgb(244,165,130)} +.RdBu .q3-9{fill:rgb(253,219,199)} +.RdBu .q4-9{fill:rgb(247,247,247)} +.RdBu .q5-9{fill:rgb(209,229,240)} +.RdBu .q6-9{fill:rgb(146,197,222)} +.RdBu .q7-9{fill:rgb(67,147,195)} +.RdBu .q8-9{fill:rgb(33,102,172)} +.RdBu .q0-10{fill:rgb(103,0,31)} +.RdBu .q1-10{fill:rgb(178,24,43)} +.RdBu .q2-10{fill:rgb(214,96,77)} +.RdBu .q3-10{fill:rgb(244,165,130)} +.RdBu .q4-10{fill:rgb(253,219,199)} +.RdBu .q5-10{fill:rgb(209,229,240)} +.RdBu .q6-10{fill:rgb(146,197,222)} +.RdBu .q7-10{fill:rgb(67,147,195)} +.RdBu .q8-10{fill:rgb(33,102,172)} +.RdBu .q9-10{fill:rgb(5,48,97)} +.RdBu .q0-11{fill:rgb(103,0,31)} +.RdBu .q1-11{fill:rgb(178,24,43)} +.RdBu .q2-11{fill:rgb(214,96,77)} +.RdBu .q3-11{fill:rgb(244,165,130)} +.RdBu .q4-11{fill:rgb(253,219,199)} +.RdBu .q5-11{fill:rgb(247,247,247)} +.RdBu .q6-11{fill:rgb(209,229,240)} +.RdBu .q7-11{fill:rgb(146,197,222)} +.RdBu .q8-11{fill:rgb(67,147,195)} +.RdBu .q9-11{fill:rgb(33,102,172)} +.RdBu .q10-11{fill:rgb(5,48,97)} +.RdGy .q0-3{fill:rgb(239,138,98)} +.RdGy .q1-3{fill:rgb(255,255,255)} +.RdGy .q2-3{fill:rgb(153,153,153)} +.RdGy .q0-4{fill:rgb(202,0,32)} +.RdGy .q1-4{fill:rgb(244,165,130)} +.RdGy .q2-4{fill:rgb(186,186,186)} +.RdGy .q3-4{fill:rgb(64,64,64)} +.RdGy .q0-5{fill:rgb(202,0,32)} +.RdGy .q1-5{fill:rgb(244,165,130)} +.RdGy .q2-5{fill:rgb(255,255,255)} +.RdGy .q3-5{fill:rgb(186,186,186)} +.RdGy .q4-5{fill:rgb(64,64,64)} +.RdGy .q0-6{fill:rgb(178,24,43)} +.RdGy .q1-6{fill:rgb(239,138,98)} +.RdGy .q2-6{fill:rgb(253,219,199)} +.RdGy .q3-6{fill:rgb(224,224,224)} +.RdGy .q4-6{fill:rgb(153,153,153)} +.RdGy .q5-6{fill:rgb(77,77,77)} +.RdGy .q0-7{fill:rgb(178,24,43)} +.RdGy .q1-7{fill:rgb(239,138,98)} +.RdGy .q2-7{fill:rgb(253,219,199)} +.RdGy .q3-7{fill:rgb(255,255,255)} +.RdGy .q4-7{fill:rgb(224,224,224)} +.RdGy .q5-7{fill:rgb(153,153,153)} +.RdGy .q6-7{fill:rgb(77,77,77)} +.RdGy .q0-8{fill:rgb(178,24,43)} +.RdGy .q1-8{fill:rgb(214,96,77)} +.RdGy .q2-8{fill:rgb(244,165,130)} +.RdGy .q3-8{fill:rgb(253,219,199)} +.RdGy .q4-8{fill:rgb(224,224,224)} +.RdGy .q5-8{fill:rgb(186,186,186)} +.RdGy .q6-8{fill:rgb(135,135,135)} +.RdGy .q7-8{fill:rgb(77,77,77)} +.RdGy .q0-9{fill:rgb(178,24,43)} +.RdGy .q1-9{fill:rgb(214,96,77)} +.RdGy .q2-9{fill:rgb(244,165,130)} +.RdGy .q3-9{fill:rgb(253,219,199)} +.RdGy .q4-9{fill:rgb(255,255,255)} +.RdGy .q5-9{fill:rgb(224,224,224)} +.RdGy .q6-9{fill:rgb(186,186,186)} +.RdGy .q7-9{fill:rgb(135,135,135)} +.RdGy .q8-9{fill:rgb(77,77,77)} +.RdGy .q0-10{fill:rgb(103,0,31)} +.RdGy .q1-10{fill:rgb(178,24,43)} +.RdGy .q2-10{fill:rgb(214,96,77)} +.RdGy .q3-10{fill:rgb(244,165,130)} +.RdGy .q4-10{fill:rgb(253,219,199)} +.RdGy .q5-10{fill:rgb(224,224,224)} +.RdGy .q6-10{fill:rgb(186,186,186)} +.RdGy .q7-10{fill:rgb(135,135,135)} +.RdGy .q8-10{fill:rgb(77,77,77)} +.RdGy .q9-10{fill:rgb(26,26,26)} +.RdGy .q0-11{fill:rgb(103,0,31)} +.RdGy .q1-11{fill:rgb(178,24,43)} +.RdGy .q2-11{fill:rgb(214,96,77)} +.RdGy .q3-11{fill:rgb(244,165,130)} +.RdGy .q4-11{fill:rgb(253,219,199)} +.RdGy .q5-11{fill:rgb(255,255,255)} +.RdGy .q6-11{fill:rgb(224,224,224)} +.RdGy .q7-11{fill:rgb(186,186,186)} +.RdGy .q8-11{fill:rgb(135,135,135)} +.RdGy .q9-11{fill:rgb(77,77,77)} +.RdGy .q10-11{fill:rgb(26,26,26)} +.RdYlBu .q0-3{fill:rgb(252,141,89)} +.RdYlBu .q1-3{fill:rgb(255,255,191)} +.RdYlBu .q2-3{fill:rgb(145,191,219)} +.RdYlBu .q0-4{fill:rgb(215,25,28)} +.RdYlBu .q1-4{fill:rgb(253,174,97)} +.RdYlBu .q2-4{fill:rgb(171,217,233)} +.RdYlBu .q3-4{fill:rgb(44,123,182)} +.RdYlBu .q0-5{fill:rgb(215,25,28)} +.RdYlBu .q1-5{fill:rgb(253,174,97)} +.RdYlBu .q2-5{fill:rgb(255,255,191)} +.RdYlBu .q3-5{fill:rgb(171,217,233)} +.RdYlBu .q4-5{fill:rgb(44,123,182)} +.RdYlBu .q0-6{fill:rgb(215,48,39)} +.RdYlBu .q1-6{fill:rgb(252,141,89)} +.RdYlBu .q2-6{fill:rgb(254,224,144)} +.RdYlBu .q3-6{fill:rgb(224,243,248)} +.RdYlBu .q4-6{fill:rgb(145,191,219)} +.RdYlBu .q5-6{fill:rgb(69,117,180)} +.RdYlBu .q0-7{fill:rgb(215,48,39)} +.RdYlBu .q1-7{fill:rgb(252,141,89)} +.RdYlBu .q2-7{fill:rgb(254,224,144)} +.RdYlBu .q3-7{fill:rgb(255,255,191)} +.RdYlBu .q4-7{fill:rgb(224,243,248)} +.RdYlBu .q5-7{fill:rgb(145,191,219)} +.RdYlBu .q6-7{fill:rgb(69,117,180)} +.RdYlBu .q0-8{fill:rgb(215,48,39)} +.RdYlBu .q1-8{fill:rgb(244,109,67)} +.RdYlBu .q2-8{fill:rgb(253,174,97)} +.RdYlBu .q3-8{fill:rgb(254,224,144)} +.RdYlBu .q4-8{fill:rgb(224,243,248)} +.RdYlBu .q5-8{fill:rgb(171,217,233)} +.RdYlBu .q6-8{fill:rgb(116,173,209)} +.RdYlBu .q7-8{fill:rgb(69,117,180)} +.RdYlBu .q0-9{fill:rgb(215,48,39)} +.RdYlBu .q1-9{fill:rgb(244,109,67)} +.RdYlBu .q2-9{fill:rgb(253,174,97)} +.RdYlBu .q3-9{fill:rgb(254,224,144)} +.RdYlBu .q4-9{fill:rgb(255,255,191)} +.RdYlBu .q5-9{fill:rgb(224,243,248)} +.RdYlBu .q6-9{fill:rgb(171,217,233)} +.RdYlBu .q7-9{fill:rgb(116,173,209)} +.RdYlBu .q8-9{fill:rgb(69,117,180)} +.RdYlBu .q0-10{fill:rgb(165,0,38)} +.RdYlBu .q1-10{fill:rgb(215,48,39)} +.RdYlBu .q2-10{fill:rgb(244,109,67)} +.RdYlBu .q3-10{fill:rgb(253,174,97)} +.RdYlBu .q4-10{fill:rgb(254,224,144)} +.RdYlBu .q5-10{fill:rgb(224,243,248)} +.RdYlBu .q6-10{fill:rgb(171,217,233)} +.RdYlBu .q7-10{fill:rgb(116,173,209)} +.RdYlBu .q8-10{fill:rgb(69,117,180)} +.RdYlBu .q9-10{fill:rgb(49,54,149)} +.RdYlBu .q0-11{fill:rgb(165,0,38)} +.RdYlBu .q1-11{fill:rgb(215,48,39)} +.RdYlBu .q2-11{fill:rgb(244,109,67)} +.RdYlBu .q3-11{fill:rgb(253,174,97)} +.RdYlBu .q4-11{fill:rgb(254,224,144)} +.RdYlBu .q5-11{fill:rgb(255,255,191)} +.RdYlBu .q6-11{fill:rgb(224,243,248)} +.RdYlBu .q7-11{fill:rgb(171,217,233)} +.RdYlBu .q8-11{fill:rgb(116,173,209)} +.RdYlBu .q9-11{fill:rgb(69,117,180)} +.RdYlBu .q10-11{fill:rgb(49,54,149)} +.Spectral .q0-3{fill:rgb(252,141,89)} +.Spectral .q1-3{fill:rgb(255,255,191)} +.Spectral .q2-3{fill:rgb(153,213,148)} +.Spectral .q0-4{fill:rgb(215,25,28)} +.Spectral .q1-4{fill:rgb(253,174,97)} +.Spectral .q2-4{fill:rgb(171,221,164)} +.Spectral .q3-4{fill:rgb(43,131,186)} +.Spectral .q0-5{fill:rgb(215,25,28)} +.Spectral .q1-5{fill:rgb(253,174,97)} +.Spectral .q2-5{fill:rgb(255,255,191)} +.Spectral .q3-5{fill:rgb(171,221,164)} +.Spectral .q4-5{fill:rgb(43,131,186)} +.Spectral .q0-6{fill:rgb(213,62,79)} +.Spectral .q1-6{fill:rgb(252,141,89)} +.Spectral .q2-6{fill:rgb(254,224,139)} +.Spectral .q3-6{fill:rgb(230,245,152)} +.Spectral .q4-6{fill:rgb(153,213,148)} +.Spectral .q5-6{fill:rgb(50,136,189)} +.Spectral .q0-7{fill:rgb(213,62,79)} +.Spectral .q1-7{fill:rgb(252,141,89)} +.Spectral .q2-7{fill:rgb(254,224,139)} +.Spectral .q3-7{fill:rgb(255,255,191)} +.Spectral .q4-7{fill:rgb(230,245,152)} +.Spectral .q5-7{fill:rgb(153,213,148)} +.Spectral .q6-7{fill:rgb(50,136,189)} +.Spectral .q0-8{fill:rgb(213,62,79)} +.Spectral .q1-8{fill:rgb(244,109,67)} +.Spectral .q2-8{fill:rgb(253,174,97)} +.Spectral .q3-8{fill:rgb(254,224,139)} +.Spectral .q4-8{fill:rgb(230,245,152)} +.Spectral .q5-8{fill:rgb(171,221,164)} +.Spectral .q6-8{fill:rgb(102,194,165)} +.Spectral .q7-8{fill:rgb(50,136,189)} +.Spectral .q0-9{fill:rgb(213,62,79)} +.Spectral .q1-9{fill:rgb(244,109,67)} +.Spectral .q2-9{fill:rgb(253,174,97)} +.Spectral .q3-9{fill:rgb(254,224,139)} +.Spectral .q4-9{fill:rgb(255,255,191)} +.Spectral .q5-9{fill:rgb(230,245,152)} +.Spectral .q6-9{fill:rgb(171,221,164)} +.Spectral .q7-9{fill:rgb(102,194,165)} +.Spectral .q8-9{fill:rgb(50,136,189)} +.Spectral .q0-10{fill:rgb(158,1,66)} +.Spectral .q1-10{fill:rgb(213,62,79)} +.Spectral .q2-10{fill:rgb(244,109,67)} +.Spectral .q3-10{fill:rgb(253,174,97)} +.Spectral .q4-10{fill:rgb(254,224,139)} +.Spectral .q5-10{fill:rgb(230,245,152)} +.Spectral .q6-10{fill:rgb(171,221,164)} +.Spectral .q7-10{fill:rgb(102,194,165)} +.Spectral .q8-10{fill:rgb(50,136,189)} +.Spectral .q9-10{fill:rgb(94,79,162)} +.Spectral .q0-11{fill:rgb(158,1,66)} +.Spectral .q1-11{fill:rgb(213,62,79)} +.Spectral .q2-11{fill:rgb(244,109,67)} +.Spectral .q3-11{fill:rgb(253,174,97)} +.Spectral .q4-11{fill:rgb(254,224,139)} +.Spectral .q5-11{fill:rgb(255,255,191)} +.Spectral .q6-11{fill:rgb(230,245,152)} +.Spectral .q7-11{fill:rgb(171,221,164)} +.Spectral .q8-11{fill:rgb(102,194,165)} +.Spectral .q9-11{fill:rgb(50,136,189)} +.Spectral .q10-11{fill:rgb(94,79,162)} +.RdYlGn .q0-3{fill:rgb(252,141,89)} +.RdYlGn .q1-3{fill:rgb(255,255,191)} +.RdYlGn .q2-3{fill:rgb(145,207,96)} +.RdYlGn .q0-4{fill:rgb(215,25,28)} +.RdYlGn .q1-4{fill:rgb(253,174,97)} +.RdYlGn .q2-4{fill:rgb(166,217,106)} +.RdYlGn .q3-4{fill:rgb(26,150,65)} +.RdYlGn .q0-5{fill:rgb(215,25,28)} +.RdYlGn .q1-5{fill:rgb(253,174,97)} +.RdYlGn .q2-5{fill:rgb(255,255,191)} +.RdYlGn .q3-5{fill:rgb(166,217,106)} +.RdYlGn .q4-5{fill:rgb(26,150,65)} +.RdYlGn .q0-6{fill:rgb(215,48,39)} +.RdYlGn .q1-6{fill:rgb(252,141,89)} +.RdYlGn .q2-6{fill:rgb(254,224,139)} +.RdYlGn .q3-6{fill:rgb(217,239,139)} +.RdYlGn .q4-6{fill:rgb(145,207,96)} +.RdYlGn .q5-6{fill:rgb(26,152,80)} +.RdYlGn .q0-7{fill:rgb(215,48,39)} +.RdYlGn .q1-7{fill:rgb(252,141,89)} +.RdYlGn .q2-7{fill:rgb(254,224,139)} +.RdYlGn .q3-7{fill:rgb(255,255,191)} +.RdYlGn .q4-7{fill:rgb(217,239,139)} +.RdYlGn .q5-7{fill:rgb(145,207,96)} +.RdYlGn .q6-7{fill:rgb(26,152,80)} +.RdYlGn .q0-8{fill:rgb(215,48,39)} +.RdYlGn .q1-8{fill:rgb(244,109,67)} +.RdYlGn .q2-8{fill:rgb(253,174,97)} +.RdYlGn .q3-8{fill:rgb(254,224,139)} +.RdYlGn .q4-8{fill:rgb(217,239,139)} +.RdYlGn .q5-8{fill:rgb(166,217,106)} +.RdYlGn .q6-8{fill:rgb(102,189,99)} +.RdYlGn .q7-8{fill:rgb(26,152,80)} +.RdYlGn .q0-9{fill:rgb(215,48,39)} +.RdYlGn .q1-9{fill:rgb(244,109,67)} +.RdYlGn .q2-9{fill:rgb(253,174,97)} +.RdYlGn .q3-9{fill:rgb(254,224,139)} +.RdYlGn .q4-9{fill:rgb(255,255,191)} +.RdYlGn .q5-9{fill:rgb(217,239,139)} +.RdYlGn .q6-9{fill:rgb(166,217,106)} +.RdYlGn .q7-9{fill:rgb(102,189,99)} +.RdYlGn .q8-9{fill:rgb(26,152,80)} +.RdYlGn .q0-10{fill:rgb(165,0,38)} +.RdYlGn .q1-10{fill:rgb(215,48,39)} +.RdYlGn .q2-10{fill:rgb(244,109,67)} +.RdYlGn .q3-10{fill:rgb(253,174,97)} +.RdYlGn .q4-10{fill:rgb(254,224,139)} +.RdYlGn .q5-10{fill:rgb(217,239,139)} +.RdYlGn .q6-10{fill:rgb(166,217,106)} +.RdYlGn .q7-10{fill:rgb(102,189,99)} +.RdYlGn .q8-10{fill:rgb(26,152,80)} +.RdYlGn .q9-10{fill:rgb(0,104,55)} +.RdYlGn .q0-11{fill:rgb(165,0,38)} +.RdYlGn .q1-11{fill:rgb(215,48,39)} +.RdYlGn .q2-11{fill:rgb(244,109,67)} +.RdYlGn .q3-11{fill:rgb(253,174,97)} +.RdYlGn .q4-11{fill:rgb(254,224,139)} +.RdYlGn .q5-11{fill:rgb(255,255,191)} +.RdYlGn .q6-11{fill:rgb(217,239,139)} +.RdYlGn .q7-11{fill:rgb(166,217,106)} +.RdYlGn .q8-11{fill:rgb(102,189,99)} +.RdYlGn .q9-11{fill:rgb(26,152,80)} +.RdYlGn .q10-11{fill:rgb(0,104,55)} +.Accent .q0-3{fill:rgb(127,201,127)} +.Accent .q1-3{fill:rgb(190,174,212)} +.Accent .q2-3{fill:rgb(253,192,134)} +.Accent .q0-4{fill:rgb(127,201,127)} +.Accent .q1-4{fill:rgb(190,174,212)} +.Accent .q2-4{fill:rgb(253,192,134)} +.Accent .q3-4{fill:rgb(255,255,153)} +.Accent .q0-5{fill:rgb(127,201,127)} +.Accent .q1-5{fill:rgb(190,174,212)} +.Accent .q2-5{fill:rgb(253,192,134)} +.Accent .q3-5{fill:rgb(255,255,153)} +.Accent .q4-5{fill:rgb(56,108,176)} +.Accent .q0-6{fill:rgb(127,201,127)} +.Accent .q1-6{fill:rgb(190,174,212)} +.Accent .q2-6{fill:rgb(253,192,134)} +.Accent .q3-6{fill:rgb(255,255,153)} +.Accent .q4-6{fill:rgb(56,108,176)} +.Accent .q5-6{fill:rgb(240,2,127)} +.Accent .q0-7{fill:rgb(127,201,127)} +.Accent .q1-7{fill:rgb(190,174,212)} +.Accent .q2-7{fill:rgb(253,192,134)} +.Accent .q3-7{fill:rgb(255,255,153)} +.Accent .q4-7{fill:rgb(56,108,176)} +.Accent .q5-7{fill:rgb(240,2,127)} +.Accent .q6-7{fill:rgb(191,91,23)} +.Accent .q0-8{fill:rgb(127,201,127)} +.Accent .q1-8{fill:rgb(190,174,212)} +.Accent .q2-8{fill:rgb(253,192,134)} +.Accent .q3-8{fill:rgb(255,255,153)} +.Accent .q4-8{fill:rgb(56,108,176)} +.Accent .q5-8{fill:rgb(240,2,127)} +.Accent .q6-8{fill:rgb(191,91,23)} +.Accent .q7-8{fill:rgb(102,102,102)} +.Dark2 .q0-3{fill:rgb(27,158,119)} +.Dark2 .q1-3{fill:rgb(217,95,2)} +.Dark2 .q2-3{fill:rgb(117,112,179)} +.Dark2 .q0-4{fill:rgb(27,158,119)} +.Dark2 .q1-4{fill:rgb(217,95,2)} +.Dark2 .q2-4{fill:rgb(117,112,179)} +.Dark2 .q3-4{fill:rgb(231,41,138)} +.Dark2 .q0-5{fill:rgb(27,158,119)} +.Dark2 .q1-5{fill:rgb(217,95,2)} +.Dark2 .q2-5{fill:rgb(117,112,179)} +.Dark2 .q3-5{fill:rgb(231,41,138)} +.Dark2 .q4-5{fill:rgb(102,166,30)} +.Dark2 .q0-6{fill:rgb(27,158,119)} +.Dark2 .q1-6{fill:rgb(217,95,2)} +.Dark2 .q2-6{fill:rgb(117,112,179)} +.Dark2 .q3-6{fill:rgb(231,41,138)} +.Dark2 .q4-6{fill:rgb(102,166,30)} +.Dark2 .q5-6{fill:rgb(230,171,2)} +.Dark2 .q0-7{fill:rgb(27,158,119)} +.Dark2 .q1-7{fill:rgb(217,95,2)} +.Dark2 .q2-7{fill:rgb(117,112,179)} +.Dark2 .q3-7{fill:rgb(231,41,138)} +.Dark2 .q4-7{fill:rgb(102,166,30)} +.Dark2 .q5-7{fill:rgb(230,171,2)} +.Dark2 .q6-7{fill:rgb(166,118,29)} +.Dark2 .q0-8{fill:rgb(27,158,119)} +.Dark2 .q1-8{fill:rgb(217,95,2)} +.Dark2 .q2-8{fill:rgb(117,112,179)} +.Dark2 .q3-8{fill:rgb(231,41,138)} +.Dark2 .q4-8{fill:rgb(102,166,30)} +.Dark2 .q5-8{fill:rgb(230,171,2)} +.Dark2 .q6-8{fill:rgb(166,118,29)} +.Dark2 .q7-8{fill:rgb(102,102,102)} +.Paired .q0-3{fill:rgb(166,206,227)} +.Paired .q1-3{fill:rgb(31,120,180)} +.Paired .q2-3{fill:rgb(178,223,138)} +.Paired .q0-4{fill:rgb(166,206,227)} +.Paired .q1-4{fill:rgb(31,120,180)} +.Paired .q2-4{fill:rgb(178,223,138)} +.Paired .q3-4{fill:rgb(51,160,44)} +.Paired .q0-5{fill:rgb(166,206,227)} +.Paired .q1-5{fill:rgb(31,120,180)} +.Paired .q2-5{fill:rgb(178,223,138)} +.Paired .q3-5{fill:rgb(51,160,44)} +.Paired .q4-5{fill:rgb(251,154,153)} +.Paired .q0-6{fill:rgb(166,206,227)} +.Paired .q1-6{fill:rgb(31,120,180)} +.Paired .q2-6{fill:rgb(178,223,138)} +.Paired .q3-6{fill:rgb(51,160,44)} +.Paired .q4-6{fill:rgb(251,154,153)} +.Paired .q5-6{fill:rgb(227,26,28)} +.Paired .q0-7{fill:rgb(166,206,227)} +.Paired .q1-7{fill:rgb(31,120,180)} +.Paired .q2-7{fill:rgb(178,223,138)} +.Paired .q3-7{fill:rgb(51,160,44)} +.Paired .q4-7{fill:rgb(251,154,153)} +.Paired .q5-7{fill:rgb(227,26,28)} +.Paired .q6-7{fill:rgb(253,191,111)} +.Paired .q0-8{fill:rgb(166,206,227)} +.Paired .q1-8{fill:rgb(31,120,180)} +.Paired .q2-8{fill:rgb(178,223,138)} +.Paired .q3-8{fill:rgb(51,160,44)} +.Paired .q4-8{fill:rgb(251,154,153)} +.Paired .q5-8{fill:rgb(227,26,28)} +.Paired .q6-8{fill:rgb(253,191,111)} +.Paired .q7-8{fill:rgb(255,127,0)} +.Paired .q0-9{fill:rgb(166,206,227)} +.Paired .q1-9{fill:rgb(31,120,180)} +.Paired .q2-9{fill:rgb(178,223,138)} +.Paired .q3-9{fill:rgb(51,160,44)} +.Paired .q4-9{fill:rgb(251,154,153)} +.Paired .q5-9{fill:rgb(227,26,28)} +.Paired .q6-9{fill:rgb(253,191,111)} +.Paired .q7-9{fill:rgb(255,127,0)} +.Paired .q8-9{fill:rgb(202,178,214)} +.Paired .q0-10{fill:rgb(166,206,227)} +.Paired .q1-10{fill:rgb(31,120,180)} +.Paired .q2-10{fill:rgb(178,223,138)} +.Paired .q3-10{fill:rgb(51,160,44)} +.Paired .q4-10{fill:rgb(251,154,153)} +.Paired .q5-10{fill:rgb(227,26,28)} +.Paired .q6-10{fill:rgb(253,191,111)} +.Paired .q7-10{fill:rgb(255,127,0)} +.Paired .q8-10{fill:rgb(202,178,214)} +.Paired .q9-10{fill:rgb(106,61,154)} +.Paired .q0-11{fill:rgb(166,206,227)} +.Paired .q1-11{fill:rgb(31,120,180)} +.Paired .q2-11{fill:rgb(178,223,138)} +.Paired .q3-11{fill:rgb(51,160,44)} +.Paired .q4-11{fill:rgb(251,154,153)} +.Paired .q5-11{fill:rgb(227,26,28)} +.Paired .q6-11{fill:rgb(253,191,111)} +.Paired .q7-11{fill:rgb(255,127,0)} +.Paired .q8-11{fill:rgb(202,178,214)} +.Paired .q9-11{fill:rgb(106,61,154)} +.Paired .q10-11{fill:rgb(255,255,153)} +.Paired .q0-12{fill:rgb(166,206,227)} +.Paired .q1-12{fill:rgb(31,120,180)} +.Paired .q2-12{fill:rgb(178,223,138)} +.Paired .q3-12{fill:rgb(51,160,44)} +.Paired .q4-12{fill:rgb(251,154,153)} +.Paired .q5-12{fill:rgb(227,26,28)} +.Paired .q6-12{fill:rgb(253,191,111)} +.Paired .q7-12{fill:rgb(255,127,0)} +.Paired .q8-12{fill:rgb(202,178,214)} +.Paired .q9-12{fill:rgb(106,61,154)} +.Paired .q10-12{fill:rgb(255,255,153)} +.Paired .q11-12{fill:rgb(177,89,40)} +.Pastel1 .q0-3{fill:rgb(251,180,174)} +.Pastel1 .q1-3{fill:rgb(179,205,227)} +.Pastel1 .q2-3{fill:rgb(204,235,197)} +.Pastel1 .q0-4{fill:rgb(251,180,174)} +.Pastel1 .q1-4{fill:rgb(179,205,227)} +.Pastel1 .q2-4{fill:rgb(204,235,197)} +.Pastel1 .q3-4{fill:rgb(222,203,228)} +.Pastel1 .q0-5{fill:rgb(251,180,174)} +.Pastel1 .q1-5{fill:rgb(179,205,227)} +.Pastel1 .q2-5{fill:rgb(204,235,197)} +.Pastel1 .q3-5{fill:rgb(222,203,228)} +.Pastel1 .q4-5{fill:rgb(254,217,166)} +.Pastel1 .q0-6{fill:rgb(251,180,174)} +.Pastel1 .q1-6{fill:rgb(179,205,227)} +.Pastel1 .q2-6{fill:rgb(204,235,197)} +.Pastel1 .q3-6{fill:rgb(222,203,228)} +.Pastel1 .q4-6{fill:rgb(254,217,166)} +.Pastel1 .q5-6{fill:rgb(255,255,204)} +.Pastel1 .q0-7{fill:rgb(251,180,174)} +.Pastel1 .q1-7{fill:rgb(179,205,227)} +.Pastel1 .q2-7{fill:rgb(204,235,197)} +.Pastel1 .q3-7{fill:rgb(222,203,228)} +.Pastel1 .q4-7{fill:rgb(254,217,166)} +.Pastel1 .q5-7{fill:rgb(255,255,204)} +.Pastel1 .q6-7{fill:rgb(229,216,189)} +.Pastel1 .q0-8{fill:rgb(251,180,174)} +.Pastel1 .q1-8{fill:rgb(179,205,227)} +.Pastel1 .q2-8{fill:rgb(204,235,197)} +.Pastel1 .q3-8{fill:rgb(222,203,228)} +.Pastel1 .q4-8{fill:rgb(254,217,166)} +.Pastel1 .q5-8{fill:rgb(255,255,204)} +.Pastel1 .q6-8{fill:rgb(229,216,189)} +.Pastel1 .q7-8{fill:rgb(253,218,236)} +.Pastel1 .q0-9{fill:rgb(251,180,174)} +.Pastel1 .q1-9{fill:rgb(179,205,227)} +.Pastel1 .q2-9{fill:rgb(204,235,197)} +.Pastel1 .q3-9{fill:rgb(222,203,228)} +.Pastel1 .q4-9{fill:rgb(254,217,166)} +.Pastel1 .q5-9{fill:rgb(255,255,204)} +.Pastel1 .q6-9{fill:rgb(229,216,189)} +.Pastel1 .q7-9{fill:rgb(253,218,236)} +.Pastel1 .q8-9{fill:rgb(242,242,242)} +.Pastel2 .q0-3{fill:rgb(179,226,205)} +.Pastel2 .q1-3{fill:rgb(253,205,172)} +.Pastel2 .q2-3{fill:rgb(203,213,232)} +.Pastel2 .q0-4{fill:rgb(179,226,205)} +.Pastel2 .q1-4{fill:rgb(253,205,172)} +.Pastel2 .q2-4{fill:rgb(203,213,232)} +.Pastel2 .q3-4{fill:rgb(244,202,228)} +.Pastel2 .q0-5{fill:rgb(179,226,205)} +.Pastel2 .q1-5{fill:rgb(253,205,172)} +.Pastel2 .q2-5{fill:rgb(203,213,232)} +.Pastel2 .q3-5{fill:rgb(244,202,228)} +.Pastel2 .q4-5{fill:rgb(230,245,201)} +.Pastel2 .q0-6{fill:rgb(179,226,205)} +.Pastel2 .q1-6{fill:rgb(253,205,172)} +.Pastel2 .q2-6{fill:rgb(203,213,232)} +.Pastel2 .q3-6{fill:rgb(244,202,228)} +.Pastel2 .q4-6{fill:rgb(230,245,201)} +.Pastel2 .q5-6{fill:rgb(255,242,174)} +.Pastel2 .q0-7{fill:rgb(179,226,205)} +.Pastel2 .q1-7{fill:rgb(253,205,172)} +.Pastel2 .q2-7{fill:rgb(203,213,232)} +.Pastel2 .q3-7{fill:rgb(244,202,228)} +.Pastel2 .q4-7{fill:rgb(230,245,201)} +.Pastel2 .q5-7{fill:rgb(255,242,174)} +.Pastel2 .q6-7{fill:rgb(241,226,204)} +.Pastel2 .q0-8{fill:rgb(179,226,205)} +.Pastel2 .q1-8{fill:rgb(253,205,172)} +.Pastel2 .q2-8{fill:rgb(203,213,232)} +.Pastel2 .q3-8{fill:rgb(244,202,228)} +.Pastel2 .q4-8{fill:rgb(230,245,201)} +.Pastel2 .q5-8{fill:rgb(255,242,174)} +.Pastel2 .q6-8{fill:rgb(241,226,204)} +.Pastel2 .q7-8{fill:rgb(204,204,204)} +.Set1 .q0-3{fill:rgb(228,26,28)} +.Set1 .q1-3{fill:rgb(55,126,184)} +.Set1 .q2-3{fill:rgb(77,175,74)} +.Set1 .q0-4{fill:rgb(228,26,28)} +.Set1 .q1-4{fill:rgb(55,126,184)} +.Set1 .q2-4{fill:rgb(77,175,74)} +.Set1 .q3-4{fill:rgb(152,78,163)} +.Set1 .q0-5{fill:rgb(228,26,28)} +.Set1 .q1-5{fill:rgb(55,126,184)} +.Set1 .q2-5{fill:rgb(77,175,74)} +.Set1 .q3-5{fill:rgb(152,78,163)} +.Set1 .q4-5{fill:rgb(255,127,0)} +.Set1 .q0-6{fill:rgb(228,26,28)} +.Set1 .q1-6{fill:rgb(55,126,184)} +.Set1 .q2-6{fill:rgb(77,175,74)} +.Set1 .q3-6{fill:rgb(152,78,163)} +.Set1 .q4-6{fill:rgb(255,127,0)} +.Set1 .q5-6{fill:rgb(255,255,51)} +.Set1 .q0-7{fill:rgb(228,26,28)} +.Set1 .q1-7{fill:rgb(55,126,184)} +.Set1 .q2-7{fill:rgb(77,175,74)} +.Set1 .q3-7{fill:rgb(152,78,163)} +.Set1 .q4-7{fill:rgb(255,127,0)} +.Set1 .q5-7{fill:rgb(255,255,51)} +.Set1 .q6-7{fill:rgb(166,86,40)} +.Set1 .q0-8{fill:rgb(228,26,28)} +.Set1 .q1-8{fill:rgb(55,126,184)} +.Set1 .q2-8{fill:rgb(77,175,74)} +.Set1 .q3-8{fill:rgb(152,78,163)} +.Set1 .q4-8{fill:rgb(255,127,0)} +.Set1 .q5-8{fill:rgb(255,255,51)} +.Set1 .q6-8{fill:rgb(166,86,40)} +.Set1 .q7-8{fill:rgb(247,129,191)} +.Set1 .q0-9{fill:rgb(228,26,28)} +.Set1 .q1-9{fill:rgb(55,126,184)} +.Set1 .q2-9{fill:rgb(77,175,74)} +.Set1 .q3-9{fill:rgb(152,78,163)} +.Set1 .q4-9{fill:rgb(255,127,0)} +.Set1 .q5-9{fill:rgb(255,255,51)} +.Set1 .q6-9{fill:rgb(166,86,40)} +.Set1 .q7-9{fill:rgb(247,129,191)} +.Set1 .q8-9{fill:rgb(153,153,153)} +.Set2 .q0-3{fill:rgb(102,194,165)} +.Set2 .q1-3{fill:rgb(252,141,98)} +.Set2 .q2-3{fill:rgb(141,160,203)} +.Set2 .q0-4{fill:rgb(102,194,165)} +.Set2 .q1-4{fill:rgb(252,141,98)} +.Set2 .q2-4{fill:rgb(141,160,203)} +.Set2 .q3-4{fill:rgb(231,138,195)} +.Set2 .q0-5{fill:rgb(102,194,165)} +.Set2 .q1-5{fill:rgb(252,141,98)} +.Set2 .q2-5{fill:rgb(141,160,203)} +.Set2 .q3-5{fill:rgb(231,138,195)} +.Set2 .q4-5{fill:rgb(166,216,84)} +.Set2 .q0-6{fill:rgb(102,194,165)} +.Set2 .q1-6{fill:rgb(252,141,98)} +.Set2 .q2-6{fill:rgb(141,160,203)} +.Set2 .q3-6{fill:rgb(231,138,195)} +.Set2 .q4-6{fill:rgb(166,216,84)} +.Set2 .q5-6{fill:rgb(255,217,47)} +.Set2 .q0-7{fill:rgb(102,194,165)} +.Set2 .q1-7{fill:rgb(252,141,98)} +.Set2 .q2-7{fill:rgb(141,160,203)} +.Set2 .q3-7{fill:rgb(231,138,195)} +.Set2 .q4-7{fill:rgb(166,216,84)} +.Set2 .q5-7{fill:rgb(255,217,47)} +.Set2 .q6-7{fill:rgb(229,196,148)} +.Set2 .q0-8{fill:rgb(102,194,165)} +.Set2 .q1-8{fill:rgb(252,141,98)} +.Set2 .q2-8{fill:rgb(141,160,203)} +.Set2 .q3-8{fill:rgb(231,138,195)} +.Set2 .q4-8{fill:rgb(166,216,84)} +.Set2 .q5-8{fill:rgb(255,217,47)} +.Set2 .q6-8{fill:rgb(229,196,148)} +.Set2 .q7-8{fill:rgb(179,179,179)} +.Set3 .q0-3{fill:rgb(141,211,199)} +.Set3 .q1-3{fill:rgb(255,255,179)} +.Set3 .q2-3{fill:rgb(190,186,218)} +.Set3 .q0-4{fill:rgb(141,211,199)} +.Set3 .q1-4{fill:rgb(255,255,179)} +.Set3 .q2-4{fill:rgb(190,186,218)} +.Set3 .q3-4{fill:rgb(251,128,114)} +.Set3 .q0-5{fill:rgb(141,211,199)} +.Set3 .q1-5{fill:rgb(255,255,179)} +.Set3 .q2-5{fill:rgb(190,186,218)} +.Set3 .q3-5{fill:rgb(251,128,114)} +.Set3 .q4-5{fill:rgb(128,177,211)} +.Set3 .q0-6{fill:rgb(141,211,199)} +.Set3 .q1-6{fill:rgb(255,255,179)} +.Set3 .q2-6{fill:rgb(190,186,218)} +.Set3 .q3-6{fill:rgb(251,128,114)} +.Set3 .q4-6{fill:rgb(128,177,211)} +.Set3 .q5-6{fill:rgb(253,180,98)} +.Set3 .q0-7{fill:rgb(141,211,199)} +.Set3 .q1-7{fill:rgb(255,255,179)} +.Set3 .q2-7{fill:rgb(190,186,218)} +.Set3 .q3-7{fill:rgb(251,128,114)} +.Set3 .q4-7{fill:rgb(128,177,211)} +.Set3 .q5-7{fill:rgb(253,180,98)} +.Set3 .q6-7{fill:rgb(179,222,105)} +.Set3 .q0-8{fill:rgb(141,211,199)} +.Set3 .q1-8{fill:rgb(255,255,179)} +.Set3 .q2-8{fill:rgb(190,186,218)} +.Set3 .q3-8{fill:rgb(251,128,114)} +.Set3 .q4-8{fill:rgb(128,177,211)} +.Set3 .q5-8{fill:rgb(253,180,98)} +.Set3 .q6-8{fill:rgb(179,222,105)} +.Set3 .q7-8{fill:rgb(252,205,229)} +.Set3 .q0-9{fill:rgb(141,211,199)} +.Set3 .q1-9{fill:rgb(255,255,179)} +.Set3 .q2-9{fill:rgb(190,186,218)} +.Set3 .q3-9{fill:rgb(251,128,114)} +.Set3 .q4-9{fill:rgb(128,177,211)} +.Set3 .q5-9{fill:rgb(253,180,98)} +.Set3 .q6-9{fill:rgb(179,222,105)} +.Set3 .q7-9{fill:rgb(252,205,229)} +.Set3 .q8-9{fill:rgb(217,217,217)} +.Set3 .q0-10{fill:rgb(141,211,199)} +.Set3 .q1-10{fill:rgb(255,255,179)} +.Set3 .q2-10{fill:rgb(190,186,218)} +.Set3 .q3-10{fill:rgb(251,128,114)} +.Set3 .q4-10{fill:rgb(128,177,211)} +.Set3 .q5-10{fill:rgb(253,180,98)} +.Set3 .q6-10{fill:rgb(179,222,105)} +.Set3 .q7-10{fill:rgb(252,205,229)} +.Set3 .q8-10{fill:rgb(217,217,217)} +.Set3 .q9-10{fill:rgb(188,128,189)} +.Set3 .q0-11{fill:rgb(141,211,199)} +.Set3 .q1-11{fill:rgb(255,255,179)} +.Set3 .q2-11{fill:rgb(190,186,218)} +.Set3 .q3-11{fill:rgb(251,128,114)} +.Set3 .q4-11{fill:rgb(128,177,211)} +.Set3 .q5-11{fill:rgb(253,180,98)} +.Set3 .q6-11{fill:rgb(179,222,105)} +.Set3 .q7-11{fill:rgb(252,205,229)} +.Set3 .q8-11{fill:rgb(217,217,217)} +.Set3 .q9-11{fill:rgb(188,128,189)} +.Set3 .q10-11{fill:rgb(204,235,197)} +.Set3 .q0-12{fill:rgb(141,211,199)} +.Set3 .q1-12{fill:rgb(255,255,179)} +.Set3 .q2-12{fill:rgb(190,186,218)} +.Set3 .q3-12{fill:rgb(251,128,114)} +.Set3 .q4-12{fill:rgb(128,177,211)} +.Set3 .q5-12{fill:rgb(253,180,98)} +.Set3 .q6-12{fill:rgb(179,222,105)} +.Set3 .q7-12{fill:rgb(252,205,229)} +.Set3 .q8-12{fill:rgb(217,217,217)} +.Set3 .q9-12{fill:rgb(188,128,189)} +.Set3 .q10-12{fill:rgb(204,235,197)} +.Set3 .q11-12{fill:rgb(255,237,111)} \ No newline at end of file diff --git a/static/colorbrewer/colorbrewer.js b/static/colorbrewer/colorbrewer.js new file mode 100644 index 00000000000..2382df2e795 --- /dev/null +++ b/static/colorbrewer/colorbrewer.js @@ -0,0 +1,315 @@ +// This product includes color specifications and designs developed by Cynthia Brewer (http://colorbrewer.org/). +// JavaScript specs as packaged in the D3 library (d3js.org). Please see license at http://colorbrewer.org/export/LICENSE.txt +!function() { + +var colorbrewer = {YlGn: { +3: ["#f7fcb9","#addd8e","#31a354"], +4: ["#ffffcc","#c2e699","#78c679","#238443"], +5: ["#ffffcc","#c2e699","#78c679","#31a354","#006837"], +6: ["#ffffcc","#d9f0a3","#addd8e","#78c679","#31a354","#006837"], +7: ["#ffffcc","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#005a32"], +8: ["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#005a32"], +9: ["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#006837","#004529"] +},YlGnBu: { +3: ["#edf8b1","#7fcdbb","#2c7fb8"], +4: ["#ffffcc","#a1dab4","#41b6c4","#225ea8"], +5: ["#ffffcc","#a1dab4","#41b6c4","#2c7fb8","#253494"], +6: ["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#2c7fb8","#253494"], +7: ["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#0c2c84"], +8: ["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#0c2c84"], +9: ["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"] +},GnBu: { +3: ["#e0f3db","#a8ddb5","#43a2ca"], +4: ["#f0f9e8","#bae4bc","#7bccc4","#2b8cbe"], +5: ["#f0f9e8","#bae4bc","#7bccc4","#43a2ca","#0868ac"], +6: ["#f0f9e8","#ccebc5","#a8ddb5","#7bccc4","#43a2ca","#0868ac"], +7: ["#f0f9e8","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#08589e"], +8: ["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#08589e"], +9: ["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#0868ac","#084081"] +},BuGn: { +3: ["#e5f5f9","#99d8c9","#2ca25f"], +4: ["#edf8fb","#b2e2e2","#66c2a4","#238b45"], +5: ["#edf8fb","#b2e2e2","#66c2a4","#2ca25f","#006d2c"], +6: ["#edf8fb","#ccece6","#99d8c9","#66c2a4","#2ca25f","#006d2c"], +7: ["#edf8fb","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#005824"], +8: ["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#005824"], +9: ["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#006d2c","#00441b"] +},PuBuGn: { +3: ["#ece2f0","#a6bddb","#1c9099"], +4: ["#f6eff7","#bdc9e1","#67a9cf","#02818a"], +5: ["#f6eff7","#bdc9e1","#67a9cf","#1c9099","#016c59"], +6: ["#f6eff7","#d0d1e6","#a6bddb","#67a9cf","#1c9099","#016c59"], +7: ["#f6eff7","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016450"], +8: ["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016450"], +9: ["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016c59","#014636"] +},PuBu: { +3: ["#ece7f2","#a6bddb","#2b8cbe"], +4: ["#f1eef6","#bdc9e1","#74a9cf","#0570b0"], +5: ["#f1eef6","#bdc9e1","#74a9cf","#2b8cbe","#045a8d"], +6: ["#f1eef6","#d0d1e6","#a6bddb","#74a9cf","#2b8cbe","#045a8d"], +7: ["#f1eef6","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#034e7b"], +8: ["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#034e7b"], +9: ["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#045a8d","#023858"] +},BuPu: { +3: ["#e0ecf4","#9ebcda","#8856a7"], +4: ["#edf8fb","#b3cde3","#8c96c6","#88419d"], +5: ["#edf8fb","#b3cde3","#8c96c6","#8856a7","#810f7c"], +6: ["#edf8fb","#bfd3e6","#9ebcda","#8c96c6","#8856a7","#810f7c"], +7: ["#edf8fb","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#6e016b"], +8: ["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#6e016b"], +9: ["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#810f7c","#4d004b"] +},RdPu: { +3: ["#fde0dd","#fa9fb5","#c51b8a"], +4: ["#feebe2","#fbb4b9","#f768a1","#ae017e"], +5: ["#feebe2","#fbb4b9","#f768a1","#c51b8a","#7a0177"], +6: ["#feebe2","#fcc5c0","#fa9fb5","#f768a1","#c51b8a","#7a0177"], +7: ["#feebe2","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177"], +8: ["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177"], +9: ["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177","#49006a"] +},PuRd: { +3: ["#e7e1ef","#c994c7","#dd1c77"], +4: ["#f1eef6","#d7b5d8","#df65b0","#ce1256"], +5: ["#f1eef6","#d7b5d8","#df65b0","#dd1c77","#980043"], +6: ["#f1eef6","#d4b9da","#c994c7","#df65b0","#dd1c77","#980043"], +7: ["#f1eef6","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#91003f"], +8: ["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#91003f"], +9: ["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#980043","#67001f"] +},OrRd: { +3: ["#fee8c8","#fdbb84","#e34a33"], +4: ["#fef0d9","#fdcc8a","#fc8d59","#d7301f"], +5: ["#fef0d9","#fdcc8a","#fc8d59","#e34a33","#b30000"], +6: ["#fef0d9","#fdd49e","#fdbb84","#fc8d59","#e34a33","#b30000"], +7: ["#fef0d9","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#990000"], +8: ["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#990000"], +9: ["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#b30000","#7f0000"] +},YlOrRd: { +3: ["#ffeda0","#feb24c","#f03b20"], +4: ["#ffffb2","#fecc5c","#fd8d3c","#e31a1c"], +5: ["#ffffb2","#fecc5c","#fd8d3c","#f03b20","#bd0026"], +6: ["#ffffb2","#fed976","#feb24c","#fd8d3c","#f03b20","#bd0026"], +7: ["#ffffb2","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"], +8: ["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"], +9: ["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026","#800026"] +},YlOrBr: { +3: ["#fff7bc","#fec44f","#d95f0e"], +4: ["#ffffd4","#fed98e","#fe9929","#cc4c02"], +5: ["#ffffd4","#fed98e","#fe9929","#d95f0e","#993404"], +6: ["#ffffd4","#fee391","#fec44f","#fe9929","#d95f0e","#993404"], +7: ["#ffffd4","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#8c2d04"], +8: ["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#8c2d04"], +9: ["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#993404","#662506"] +},Purples: { +3: ["#efedf5","#bcbddc","#756bb1"], +4: ["#f2f0f7","#cbc9e2","#9e9ac8","#6a51a3"], +5: ["#f2f0f7","#cbc9e2","#9e9ac8","#756bb1","#54278f"], +6: ["#f2f0f7","#dadaeb","#bcbddc","#9e9ac8","#756bb1","#54278f"], +7: ["#f2f0f7","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#4a1486"], +8: ["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#4a1486"], +9: ["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"] +},Blues: { +3: ["#deebf7","#9ecae1","#3182bd"], +4: ["#eff3ff","#bdd7e7","#6baed6","#2171b5"], +5: ["#eff3ff","#bdd7e7","#6baed6","#3182bd","#08519c"], +6: ["#eff3ff","#c6dbef","#9ecae1","#6baed6","#3182bd","#08519c"], +7: ["#eff3ff","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#084594"], +8: ["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#084594"], +9: ["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"] +},Greens: { +3: ["#e5f5e0","#a1d99b","#31a354"], +4: ["#edf8e9","#bae4b3","#74c476","#238b45"], +5: ["#edf8e9","#bae4b3","#74c476","#31a354","#006d2c"], +6: ["#edf8e9","#c7e9c0","#a1d99b","#74c476","#31a354","#006d2c"], +7: ["#edf8e9","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#005a32"], +8: ["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#005a32"], +9: ["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#006d2c","#00441b"] +},Oranges: { +3: ["#fee6ce","#fdae6b","#e6550d"], +4: ["#feedde","#fdbe85","#fd8d3c","#d94701"], +5: ["#feedde","#fdbe85","#fd8d3c","#e6550d","#a63603"], +6: ["#feedde","#fdd0a2","#fdae6b","#fd8d3c","#e6550d","#a63603"], +7: ["#feedde","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#8c2d04"], +8: ["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#8c2d04"], +9: ["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#a63603","#7f2704"] +},Reds: { +3: ["#fee0d2","#fc9272","#de2d26"], +4: ["#fee5d9","#fcae91","#fb6a4a","#cb181d"], +5: ["#fee5d9","#fcae91","#fb6a4a","#de2d26","#a50f15"], +6: ["#fee5d9","#fcbba1","#fc9272","#fb6a4a","#de2d26","#a50f15"], +7: ["#fee5d9","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#99000d"], +8: ["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#99000d"], +9: ["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"] +},Greys: { +3: ["#f0f0f0","#bdbdbd","#636363"], +4: ["#f7f7f7","#cccccc","#969696","#525252"], +5: ["#f7f7f7","#cccccc","#969696","#636363","#252525"], +6: ["#f7f7f7","#d9d9d9","#bdbdbd","#969696","#636363","#252525"], +7: ["#f7f7f7","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525"], +8: ["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525"], +9: ["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"] +},PuOr: { +3: ["#f1a340","#f7f7f7","#998ec3"], +4: ["#e66101","#fdb863","#b2abd2","#5e3c99"], +5: ["#e66101","#fdb863","#f7f7f7","#b2abd2","#5e3c99"], +6: ["#b35806","#f1a340","#fee0b6","#d8daeb","#998ec3","#542788"], +7: ["#b35806","#f1a340","#fee0b6","#f7f7f7","#d8daeb","#998ec3","#542788"], +8: ["#b35806","#e08214","#fdb863","#fee0b6","#d8daeb","#b2abd2","#8073ac","#542788"], +9: ["#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788"], +10: ["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"], +11: ["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"] +},BrBG: { +3: ["#d8b365","#f5f5f5","#5ab4ac"], +4: ["#a6611a","#dfc27d","#80cdc1","#018571"], +5: ["#a6611a","#dfc27d","#f5f5f5","#80cdc1","#018571"], +6: ["#8c510a","#d8b365","#f6e8c3","#c7eae5","#5ab4ac","#01665e"], +7: ["#8c510a","#d8b365","#f6e8c3","#f5f5f5","#c7eae5","#5ab4ac","#01665e"], +8: ["#8c510a","#bf812d","#dfc27d","#f6e8c3","#c7eae5","#80cdc1","#35978f","#01665e"], +9: ["#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e"], +10: ["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"], +11: ["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"] +},PRGn: { +3: ["#af8dc3","#f7f7f7","#7fbf7b"], +4: ["#7b3294","#c2a5cf","#a6dba0","#008837"], +5: ["#7b3294","#c2a5cf","#f7f7f7","#a6dba0","#008837"], +6: ["#762a83","#af8dc3","#e7d4e8","#d9f0d3","#7fbf7b","#1b7837"], +7: ["#762a83","#af8dc3","#e7d4e8","#f7f7f7","#d9f0d3","#7fbf7b","#1b7837"], +8: ["#762a83","#9970ab","#c2a5cf","#e7d4e8","#d9f0d3","#a6dba0","#5aae61","#1b7837"], +9: ["#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837"], +10: ["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"], +11: ["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"] +},PiYG: { +3: ["#e9a3c9","#f7f7f7","#a1d76a"], +4: ["#d01c8b","#f1b6da","#b8e186","#4dac26"], +5: ["#d01c8b","#f1b6da","#f7f7f7","#b8e186","#4dac26"], +6: ["#c51b7d","#e9a3c9","#fde0ef","#e6f5d0","#a1d76a","#4d9221"], +7: ["#c51b7d","#e9a3c9","#fde0ef","#f7f7f7","#e6f5d0","#a1d76a","#4d9221"], +8: ["#c51b7d","#de77ae","#f1b6da","#fde0ef","#e6f5d0","#b8e186","#7fbc41","#4d9221"], +9: ["#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221"], +10: ["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"], +11: ["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"] +},RdBu: { +3: ["#ef8a62","#f7f7f7","#67a9cf"], +4: ["#ca0020","#f4a582","#92c5de","#0571b0"], +5: ["#ca0020","#f4a582","#f7f7f7","#92c5de","#0571b0"], +6: ["#b2182b","#ef8a62","#fddbc7","#d1e5f0","#67a9cf","#2166ac"], +7: ["#b2182b","#ef8a62","#fddbc7","#f7f7f7","#d1e5f0","#67a9cf","#2166ac"], +8: ["#b2182b","#d6604d","#f4a582","#fddbc7","#d1e5f0","#92c5de","#4393c3","#2166ac"], +9: ["#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac"], +10: ["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"], +11: ["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"] +},RdGy: { +3: ["#ef8a62","#ffffff","#999999"], +4: ["#ca0020","#f4a582","#bababa","#404040"], +5: ["#ca0020","#f4a582","#ffffff","#bababa","#404040"], +6: ["#b2182b","#ef8a62","#fddbc7","#e0e0e0","#999999","#4d4d4d"], +7: ["#b2182b","#ef8a62","#fddbc7","#ffffff","#e0e0e0","#999999","#4d4d4d"], +8: ["#b2182b","#d6604d","#f4a582","#fddbc7","#e0e0e0","#bababa","#878787","#4d4d4d"], +9: ["#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d"], +10: ["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"], +11: ["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"] +},RdYlBu: { +3: ["#fc8d59","#ffffbf","#91bfdb"], +4: ["#d7191c","#fdae61","#abd9e9","#2c7bb6"], +5: ["#d7191c","#fdae61","#ffffbf","#abd9e9","#2c7bb6"], +6: ["#d73027","#fc8d59","#fee090","#e0f3f8","#91bfdb","#4575b4"], +7: ["#d73027","#fc8d59","#fee090","#ffffbf","#e0f3f8","#91bfdb","#4575b4"], +8: ["#d73027","#f46d43","#fdae61","#fee090","#e0f3f8","#abd9e9","#74add1","#4575b4"], +9: ["#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4"], +10: ["#a50026","#d73027","#f46d43","#fdae61","#fee090","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"], +11: ["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"] +},Spectral: { +3: ["#fc8d59","#ffffbf","#99d594"], +4: ["#d7191c","#fdae61","#abdda4","#2b83ba"], +5: ["#d7191c","#fdae61","#ffffbf","#abdda4","#2b83ba"], +6: ["#d53e4f","#fc8d59","#fee08b","#e6f598","#99d594","#3288bd"], +7: ["#d53e4f","#fc8d59","#fee08b","#ffffbf","#e6f598","#99d594","#3288bd"], +8: ["#d53e4f","#f46d43","#fdae61","#fee08b","#e6f598","#abdda4","#66c2a5","#3288bd"], +9: ["#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd"], +10: ["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"], +11: ["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"] +},RdYlGn: { +3: ["#fc8d59","#ffffbf","#91cf60"], +4: ["#d7191c","#fdae61","#a6d96a","#1a9641"], +5: ["#d7191c","#fdae61","#ffffbf","#a6d96a","#1a9641"], +6: ["#d73027","#fc8d59","#fee08b","#d9ef8b","#91cf60","#1a9850"], +7: ["#d73027","#fc8d59","#fee08b","#ffffbf","#d9ef8b","#91cf60","#1a9850"], +8: ["#d73027","#f46d43","#fdae61","#fee08b","#d9ef8b","#a6d96a","#66bd63","#1a9850"], +9: ["#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850"], +10: ["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"], +11: ["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"] +},Accent: { +3: ["#7fc97f","#beaed4","#fdc086"], +4: ["#7fc97f","#beaed4","#fdc086","#ffff99"], +5: ["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0"], +6: ["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f"], +7: ["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17"], +8: ["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17","#666666"] +},Dark2: { +3: ["#1b9e77","#d95f02","#7570b3"], +4: ["#1b9e77","#d95f02","#7570b3","#e7298a"], +5: ["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e"], +6: ["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02"], +7: ["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d"], +8: ["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"] +},Paired: { +3: ["#a6cee3","#1f78b4","#b2df8a"], +4: ["#a6cee3","#1f78b4","#b2df8a","#33a02c"], +5: ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99"], +6: ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c"], +7: ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f"], +8: ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00"], +9: ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6"], +10: ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a"], +11: ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99"], +12: ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99","#b15928"] +},Pastel1: { +3: ["#fbb4ae","#b3cde3","#ccebc5"], +4: ["#fbb4ae","#b3cde3","#ccebc5","#decbe4"], +5: ["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6"], +6: ["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc"], +7: ["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd"], +8: ["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec"], +9: ["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec","#f2f2f2"] +},Pastel2: { +3: ["#b3e2cd","#fdcdac","#cbd5e8"], +4: ["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4"], +5: ["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9"], +6: ["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae"], +7: ["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc"], +8: ["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc","#cccccc"] +},Set1: { +3: ["#e41a1c","#377eb8","#4daf4a"], +4: ["#e41a1c","#377eb8","#4daf4a","#984ea3"], +5: ["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00"], +6: ["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33"], +7: ["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628"], +8: ["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf"], +9: ["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf","#999999"] +},Set2: { +3: ["#66c2a5","#fc8d62","#8da0cb"], +4: ["#66c2a5","#fc8d62","#8da0cb","#e78ac3"], +5: ["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854"], +6: ["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f"], +7: ["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494"], +8: ["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494","#b3b3b3"] +},Set3: { +3: ["#8dd3c7","#ffffb3","#bebada"], +4: ["#8dd3c7","#ffffb3","#bebada","#fb8072"], +5: ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3"], +6: ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462"], +7: ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69"], +8: ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5"], +9: ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9"], +10: ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd"], +11: ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5"], +12: ["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f"] +}}; + +if (typeof define === "function" && define.amd) { + define(colorbrewer); +} else if (typeof module === "object" && module.exports) { + module.exports = colorbrewer; +} else { + this.colorbrewer = colorbrewer; +} + +}(); diff --git a/static/colorbrewer/index.js b/static/colorbrewer/index.js new file mode 100644 index 00000000000..cfb923ecc27 --- /dev/null +++ b/static/colorbrewer/index.js @@ -0,0 +1 @@ +module.exports = require('./colorbrewer.js'); diff --git a/static/colorbrewer/package.json b/static/colorbrewer/package.json new file mode 100644 index 00000000000..134be320366 --- /dev/null +++ b/static/colorbrewer/package.json @@ -0,0 +1,65 @@ +{ + "_from": "colorbrewer", + "_id": "colorbrewer@1.0.0", + "_inBundle": false, + "_integrity": "sha1-T5czO5abp2Ejgr5LwzlLNB+0yKI=", + "_location": "/colorbrewer", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "colorbrewer", + "name": "colorbrewer", + "escapedName": "colorbrewer", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/colorbrewer/-/colorbrewer-1.0.0.tgz", + "_shasum": "4f97333b969ba7612382be4bc3394b341fb4c8a2", + "_spec": "colorbrewer", + "_where": "/Users/sulka/Documents/nightscout/cgm-remote-monitor", + "author": { + "name": "Cynthia Brewer" + }, + "bugs": { + "url": "https://github.com/saikocat/colorbrewer/issues", + "email": "saikocatz@gmail.com" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "A shim module of colorbrewer2 by Cythina Brewer for browserify", + "files": [ + "colorbrewer.css", + "colorbrewer.js", + "index.js" + ], + "homepage": "http://colorbrewer2.org/", + "keywords": [ + "colors", + "design", + "visualization", + "cartography", + "svg", + "d3", + "browserify" + ], + "license": [ + { + "type": "Apache-Style", + "url": "https://github.com/saikocat/colorbrewer/blob/master/LICENSE.txt" + } + ], + "main": "index.js", + "name": "colorbrewer", + "repository": { + "type": "git", + "url": "git+https://github.com/saikocat/colorbrewer.git" + }, + "style": "colorbrewer.css", + "version": "1.0.0" +} diff --git a/static/css/drawer.css b/static/css/drawer.css index 245809ef0f3..37867b8fa87 100644 --- a/static/css/drawer.css +++ b/static/css/drawer.css @@ -16,12 +16,12 @@ color: #eee; display: none; font-size: 16px; - height: 100%; - overflow-y: auto; + overflow-y: scroll; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; position: absolute; margin-top: 45px; right: -200px; - width: 300px; top: 0; z-index: 1; } @@ -46,25 +46,39 @@ input[type=number]:invalid { display: block; text-align: right; padding: 10px 0; + 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); color: #eee; display: none; font-size: 16px; - height: 100%; - overflow-y: auto; + overflow-y: scroll; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; position: absolute; margin-top: 45px; right: -200px; - width: 300px; top: 0; z-index: 1; } +#adminNotifyContent { + margin: 10px; +} + +.adminNotifyMessage { + margin-left: 10px; +} + +.adminNotifyMessageAdditionalInfo { + margin-left: 10px; + font-size: 11px; +} + #treatmentDrawer input { box-sizing: border-box; } @@ -84,6 +98,10 @@ input[type=number]:invalid { vertical-align: middle; } +#treatmentDrawer fieldset fieldset .left-column span { + width: 96px; +} + #treatmentDrawer .left-column input, #treatmentDrawer .left-column select { width: 140px; } @@ -148,6 +166,7 @@ input[type=number]:invalid { } #about a { color: #fff; + text-decoration: underline; } form { @@ -201,33 +220,49 @@ h1, legend, } #toolbar { - background: url(/images/logo2.png) no-repeat 3px 3px #333; - border-bottom: 1px solid #999; - top: 0; - margin: 0; - height: 44px; text-shadow: 0 0 5px black; + display: flex; + height: 44px; + margin: 0 0 10px; + padding: 0 15px 0 40px; + position: relative; + align-items: center; + background: url("../images/logo2.png") no-repeat 3px center #333; + border-bottom: 1px solid #999; + justify-content: space-between; } #toolbar .customTitle { color: #ccc; font-size: 16px; - margin-top: 0; - margin-left: 42px; - padding-top: 10px; - padding-right: 150px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } + +#toolbar .button-close { + color: #404040; + text-align: center; + text-shadow: none; + height: 20px; + width: 20px; + padding: 5px; + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + background: grey; + border: 2px solid #404040; + border-radius: 5px; +} + +#toolbar .button-close + #buttonbar { + margin-right: 40px; +} + #buttonbar { - padding-right: 10px; - height: 44px; opacity: 0.75; vertical-align: middle; - position: absolute; - right: 0; - z-index: 500; } #buttonbar a, @@ -237,11 +272,17 @@ h1, legend, height: 44px; line-height: 44px; } + +#buttonbar .selected { + color: red; +} + #buttonbar a { - float: left; + float: right; text-decoration: none; width: 34px; } + #buttonbar i { padding-left: 12px; } @@ -310,6 +351,10 @@ ul.navigation { display: block; } +.navigation li.multilink { + display: flex; +} + .navigation a { text-decoration: none; background-color: #808080; @@ -318,6 +363,12 @@ ul.navigation { margin: 10px 0; } +.navigation a.multilink { + display: flex; + flex-grow: 1; + justify-content: center; +} + .navigation a:hover { background-color: #989898; } @@ -329,8 +380,9 @@ ul.navigation { color: #eee; display: none; font-size: 16px; - height: 100%; - overflow-y: auto; + overflow-y: scroll; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; position: absolute; margin-top: 45px; right: -200px; @@ -359,4 +411,4 @@ ul.navigation { #boluscalcDrawer .foodinput { background: #505050; margin: 10px; -} \ No newline at end of file +} diff --git a/static/css/dropdown.css b/static/css/dropdown.css index a2211b84983..49d5f82af6a 100644 --- a/static/css/dropdown.css +++ b/static/css/dropdown.css @@ -13,7 +13,7 @@ display: inline; } -.dropdown-menu li a { +.dropdown-menu li a, .dropdown-menu li label { display: block; text-decoration: none; white-space: nowrap; diff --git a/static/css/jquery.tooltips.css b/static/css/jquery.tooltips.css new file mode 100644 index 00000000000..fe800b9b6af --- /dev/null +++ b/static/css/jquery.tooltips.css @@ -0,0 +1,109 @@ +.tooltip strong, .tooltip b { + font-weight: 700 +} +.tooltip { + position: absolute; + font-size: 10px; + padding: 3px 5px; + opacity: 0.9; + z-index: 9999; + word-wrap: break-word; +} +.tooltip .tooltip-inner { + background-color: #000; + color: #fff; + max-width: 200px; + padding: 3px 8px; + text-align: center; + box-shadow: 0 0 5px 0 black; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; +} +.tooltip .tooltip-arrow { + position: absolute; + width: 0; + height: 0; + line-height: 0; + border: 5px dashed #000; +} +.tooltip .tooltip-arrow-n { + border-bottom-color: #000; +} +.tooltip .tooltip-arrow-s { + border-top-color: #000; +} +.tooltip .tooltip-arrow-e { + border-left-color: #000; +} +.tooltip .tooltip-arrow-w { + border-right-color: #000; +} +.tooltip.tooltip-n .tooltip-arrow { + top: 0px; + left: 50%; + margin-left: -5px; + border-bottom-style: solid; + border-top: none; + border-left-color: transparent; + border-right-color: transparent; +} +.tooltip.tooltip-nw .tooltip-arrow { + top: 0; + left: 10px; + border-bottom-style: solid; + border-top: none; + border-left-color: transparent; + border-right-color: transparent; +} +.tooltip.tooltip-ne .tooltip-arrow { + top: 0; + right: 10px; + border-bottom-style: solid; + border-top: none; + border-left-color: transparent; + border-right-color: transparent; +} +.tooltip.tooltip-s .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-style: solid; + border-bottom: none; + border-left-color: transparent; + border-right-color: transparent; +} +.tooltip.tooltip-sw .tooltip-arrow { + bottom: 0; + left: 10px; + border-top-style: solid; + border-bottom: none; + border-left-color: transparent; + border-right-color: transparent; +} +.tooltip.tooltip-se .tooltip-arrow { + bottom: 0; + right: 10px; + border-top-style: solid; + border-bottom: none; + border-left-color: transparent; + border-right-color: transparent; +} +.tooltip.tooltip-e .tooltip-arrow { + right: 0; + top: 50%; + margin-top: -5px; + border-left-style: solid; + border-right: none; + border-top-color: transparent; + border-bottom-color: transparent; +} +.tooltip.tooltip-w .tooltip-arrow { + left: 0; + top: 50%; + margin-top: -5px; + border-right-style: solid; + border-left: none; + border-top-color: transparent; + border-bottom-color: transparent; +} diff --git a/static/css/main.css b/static/css/main.css index 7852c2ec954..dd5abb9c9b4 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,15 +1,99 @@ -@import url("//fonts.googleapis.com/css?family=Ubuntu:300,400,500,700,300italic,400italic,500italic,700italic"); -@import url("//fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,300,400,600,700,800"); -@import url("/glyphs/css/fontello.css"); +@import url('https://fonts.googleapis.com/css?family=Ubuntu:400,700'); + +@font-face { + font-family: 'nsicons'; + src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABiAAA8AAAAAKlQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+IFOpY21hcAAAAdgAAADRAAACqp1fmY1jdnQgAAACrAAAABMAAAAgBtX/BGZwZ20AAALAAAAFkAAAC3CKkZBZZ2FzcAAACFAAAAAIAAAACAAAABBnbHlmAAAIWAAADLAAABUafX7y9GhlYWQAABUIAAAAMgAAADYQM64raGhlYQAAFTwAAAAgAAAAJAhYBH9obXR4AAAVXAAAADIAAABUTzz/8mxvY2EAABWQAAAALAAAACw1sDpbbWF4cAAAFbwAAAAgAAAAIAFcDDxuYW1lAAAV3AAAAX0AAALBgOz0q3Bvc3QAABdcAAAApgAAAQSyxpkWcHJlcAAAGAQAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZD7GOIGBlYGBqYppDwMDQw+EZnzAYMjIBBRlYGVmwAoC0lxTGBxeMHwKYQ76n8UQxRzEMA0ozAiSAwAGawxRAHic7ZLJDcIwEEWfIew7pAKOiCLYSqACqqAgTnSSTnzgQKYB+M4YJHpgrBfJX+PEmhegA7TFVhQQ7gRS3ZSGJm8zbPKCq/ZzLfXHwbOqL/XDgp3s/Hop4ZvsPPmpoHPr79o0SUtvKnSDLj36DPSdEWMmTJmpe8GSFaUau/xrnB7hsyvTvJ0UxowmSsw0ljLJbMwk4zGjyRMzckDMyAYxIy+y7aQ/4Vk5ckV9cdLt6ocjf1hwZBLbOXKK7R3ZxQ6OPGNHR8axkyP32NmhfAMTZEkIAAAAeJxjYEADEhDIHPQ/C4QBEmwD3QB4nK1WaXfTRhQdeUmchCwlCy1qYcTEabBGJmzBgAlBsmMgXZytlaCLFDvpvvGJ3+Bf82Tac+g3flrvGy8kkLTncJqTo3fnzdXM22USWpLYC+uRlJsvxdTWJo3sPAnphk3LUXwoO3shZYrJ3wVREK2W2rcdh0REIlC1rrBEEPseWZpkfOhRRsu2pFdNyi096S5b40G9Vd9+GjrKsTuhpGYzdGg9siVVGFWiSKY9UtKmZaj6K0krvL/CzFfNUMKITiJpvBnG0EjeG2e0ymg1tuMoimyy3ChSJJrhQRR5lNUS5+SKCQzKB82Q8sqnEeXD/Iis2KOcVrBLttP8vi95p3c5P7Ffb1G25EAfyI7s4Ox0JV+EW1th3LST7ShUEXbXd0Js2exU/2aP8ppGA7crMr3QjGCpfIUQKz+hzP4hWS2cT/mSR6NaspETQetlTuxLPoHW44gpcc0YWdDd0QkR1P2SMwz2mD4e/PHeKZYLEwJ4HMt6RyWcCBMpYXM0SdowcmAlZYsqqfWumDjldVrEW8J+7drRl85o41B3YjxbDx1bOVHJ8WhSp5lMndpJzaMpDaKUdCZ4zK8DKD+iSV5tYzWJlUfTOGbGhEQiAi3cS1NBLDuxpCkEzaMZvbkbprl2LVqkyQP13KP39OZWuLnTU9oO9LNGf1anYjrYC9PpaeQv8Wna5SJF6frpGX5M4kHWAjKRLTbDlIMHb/0O0svXlhyF1wbY7u3zK6h91kTwpAH7G9AeT9UpCUyFmFWIVkBirWtZlsnVrBapyNR3Q5pWvqzTBIpyHBfHvoxx/V8zM5aYEr7fidOzIy49c+1LCNMcfJt1PZrXqcVyAXFmeU6nWZbv6zTH8gOd5lme1+kIS1unoyw/1GmB5Uc6HWN5QQuadN/BkIsw5AIOkDCEpQNDWF6CISwVDGG5CENYFmEIyyUYwvJjGMJyGYawvKxl1dRTSePamVgGbEJgYo4eucxF5WoquVRCu2hUakOeEm6VVBTPqn9loF488oY5sBZIl8iaXzHOlY9G5fjWFS1vGjtXwLHqbx+O9jnxUtaLhT8F/9XWVCW9Ys3Dk6vwG4aebCeqNql4dE2Xz1U9uv5fVFRYC/QbSIVYKMqybHBnIoSPOp2GaqCVQ8xszDy063XLmp/D/TcxQhZQ/fg3FBoL3INOWUlZ7eCs1dfbstw7g3I4EyxJMTfz+lb4IiOz0n6RWcqej3wecAWMSmXYagOtFbzZJzEPmd4kzwRxW1E2SNrYzgSJDRzzgHnznQQmYeqqDeRO4YYN+AVhbsF5J1yieqMsh+5F7PMopPxbp+JE9qhojMCz2Rthr+9Cym9xDCQ0+aV+DFQVoakYNRXQNFJuqAZfxtm6bULGDvQjKnbDsqziw8cW95WSbRmEfKSI1aOjn9Zeok6q3H5mFJfvnb4FwSA1MX9733RxkMq7WskyR20DU7calVPXmkPjVYfq5lH1vePsEzlrmm66Jx56X9Oq28HFXCyw9m0O0lImF9T1YYUNosvFpVDqZTRJ77gHGBYY0O9Qio3/q/rYfJ4rVYXRcSTfTtS30edgDPwP2H9H9QPQ92Pocg0uz/eaE59u9OFsma6iF+un6Dcwa625WboG3NB0A+IhR62OuMoNfKcGcXqkuRzpIeBj3RXiAcAmgMXgE921jOZTAKP5jDk+wOfMYdBkDoMt5jDYZs4awA5zGOwyh8Eecxh8wZx1gC+ZwyBkDoOIOQyeMCcAeMocBl8xh8HXzGHwDXPuA3zLHAYxcxgkzGGwr+nWMMwtXtBdoLZBVaADU09Y3MPiUFNlyP6OF4b9vUHM/sEgpv6o6faQ+hMvDPVng5j6i0FM/VXTnSH1N14Y6u8GMfUPg5j6TL8Yy2UGv4x8lwoHlF1sPufvifcP28VAuQABAAH//wAPeJzlWG1sW9d5Pu/5uOfeS/Lykry8pCTqiiJFUqZkiiYp0rUsmZI/KNtUIjO0J7muI6y164V2lLVJ3GVBt6QfiIPNLrJg6L9FReIf2Ya1zY8NxpZhWLAV3YBmQOCk//qjP5wNSIbOA9rGlveeKzqWE6Duhv0ZBkrnnvOeD97n/Xje95BwQu78C9ug/0ZipESOkvPkSLNVMCnjDRCkC1Kw1jmgBwnV6DrRQFsnwGGdcMbXCSNsnRBB1omQYp1Iebaz3Do0PpWIunldDE5AxNHyU5DN5AvTtXqj2qhX3JijSQ0ljdocK/FGrZ6oJDxIfCxt1BtzUMsX8IN7qtsnC7V9oGZLgLOqUYeqBfDnbipw7aWwA6710jXTc94tF4RhuRB0bZvrScvhTtgQhfKVtSUpwy7E04EdpdKOQDruhHX50OlvXX4a5bh9YCB+4CF65FB8gEeZ61pSPn2ZXr79iuVS45prXcc110xn8++KR8LMsYN2MOil03pUt/FQFj5SPP5SmrmWG7AN7+S5k55hB3DERr914vffq+CEEwpaVvXbr3+7Gg4xjYUcPI9VrxNCAO3wCxZlAZIlO5r5Lf0CgScEMEJYFx+MrFBAnbez1Vy2mtWUguOOVshiI5Xupj9WNDYzUHET8WrFZdG0eyPt9tw03EBd4cBL9LCjBn+ppO+7KI2/35e6abUcjare56f4PhoJk8+QObIIshlvzs3YjIIkvAZAaGseneHQ0e+ayyvNMpGMM8kvEMJx7gy+KSeMr+k40ARojxJBqegSIegKoYK2h45+N4D7dvTXS/g1NiT+R1/U3LV9Cydw4YF7VldXmy4hB/bP7t1V2pH3htwYakJzDBGfyDUKMl6NQz4MmosuGUMDZEcz+ekI6n5UOSROFPIo0mTEcROjlfosKN91WQKy0yALGAgj4Drw783jzWmIG8ZbRhT/x07v3yzvP316P7yd9Qwmh3QzFNws52pQH4O3czUxpicbG5uXNuh6daNqT9rH7b+ZPz4/UoeX7h6x+eZjWwcsnAaLx7SUzlkt1z/jkMQTdLjyyualV6BU26iFw8ftSUKVrenX0NaSjJBisyAYJYC+Ri8QSh9XWuZdwjmsqNhvZ2O5esxW7hcbna7lLUgIjHOJCiigAhLK+SquVK73Bgx3n+4CvJ12b7/v+17k5X/+YxrF7mvnZ7p0eXZj803f+2ABHfH82ZdfPnveI+zO7TvfY4/6sVAms6TZnE0nLMEJhRYDygmn5AIaDiWih+/MqeRrRGiab0BtBS2qteNufNhJOVFdpCZyUstMgd/k0Q4F1SgL9M0QhoTrNGolsJhH56DhWJApYafiAf3CMc3S2kvC5h1Na7c1rSMsbamNwmP++EudF6/+wTLtvvD6N0/87pf+4cYPHtee+eub1776844QS0tCdLa2W9rdMZ6ktq898iLiv/ydy7j3xUeeeuvJJ9/6V9UgFRD6Mf4xsouUmzu9eJAhZmgRoaJOkDWlBk67+KB8BRXA2/EEoh1QVsnVphAZNvlMGLFiozkjiB4bt7IPsVYUjW4H+Zx55oxpVk0PnwHPrAQC+AxUTA+fKKya721D9hNLzQ4H7q7C7v3jP9yORvHah/Qd+k3E4jWHMgO2vGtHQtcRDTmL7+1wkZzIoSEeaCf6TriNjv/qq9i0bfW0743D4VdfDT/hqs5rr4U/vTBcUgt8f/8eO4n61clj5GBz4cxqex69asZEn6+ND9mcAUNdc7GuIVsgESMMZGOGeY9RTHmUnj312UeOHV6cKGbSsagU7gSmrIyF+q3nkAwQh3TRp6QFBcSC2UtqE4AEgXkL0ancVsM81sDciGkM40UZpC+sItQ6JkGfwBF1AsEn+odJFKC5Zjpf6dATT56AlC7PmYHYuCbCyyEplwYGDcntZ/WgPZR4WLO1Qy4X+rgZ1s9KHUxxTrcSua21+lJy0NBZ5FkZhHAq8bAIy0WHc2NrsQmnZ7oYud2vqHnbiw9V0I3jyyD2hvR2yjblF43gXqE1PXTsYCWcGgpDUPprBwbTO2VQOsvblgZmhNif6i8dtCGIPn4vxsOkQQ6QxebBHSCY4dNOCz2aUc6QpgVlfoxTLumaykpYlawR0DToEgAV64Cx7k3E3Hw+68c6UhLqWhsGx0UV1mMY19mM1FCV1Qrqt+KqdImkFd+aV8qfY4lt4QA3Tb2nm35zZe7UrhdaRugI1wzh5XYX3aHsXvCnktEUVhrBd7eFRufuNhN+b1e3dDEUaHCZH/Ki8cFgeCHn4EQ0E7C1wdR4Z1uQKH/8OZ1hU8QlU2SsOdovrHyno9RHqtIS0HbKGRzwY6WmHEvVQBlpUfQ3RIO+hyiwSmooSIBYsXTyWMIFLwC6pdmcOsDtoG4EinsmIXX1g6uwk4/sHpgLi5SX4vZcucl2/siJBeNGOmgYIc8JBGPOcLF45uJV+vpTxxZFqZt/pmY71I7WnmmfEi20o18nnKRvoR1H8O0bzdo4oNcRlUEwhrCQEUjODN+eoQEx3n04ZEXBa+en8VP1SSveTyHTfg51maps+uNEf5zdPl517Fsf+hUfi2Dx9StGX2j5Xb8FexHLQifst2C1LFdNYF/xrqqFH2NzrENMkiFN0iG/2Xz04YcOLx5KaAE+DToqXOo7AdNNywrRADGNgHmaGAjFIGuYNIHoEvS1IHAeXMRymK5oaL8QPTw/P9+Z7xxbPnhg/8K+2Vxh3MmOudlMWAyjJfOFjOagc9ZFnwDzGPuRrQouHsE0imZlWcUaDeQBXNfIWqqmVlyBehmdxoyLtZ9E2ik0qhH6YTkznBnPp8Y3/1T7PkbdG7oe6vluiX/wHSs9uZDPDlima+jx+NRuAwKl5EwblmcPnops/vJzgBFnZnf2Djw1tgeKc5NeWZPfF+INI86Icu/bxBzJLsxNpgzO92HqXSiH7OUvdx6D3c3mLUkNKc10+Yubf0RIyNfnBupTJxEyiNl8kkxjJdkij5BTeNN4inyDPNf86uewir3Q3beTB8z6WBTdBbk37kYtZgbi5loiFmYBdFrGRYCvhQwqNEq3WMBxBlbIwEBwMSIZA0iukGQylDz89eee/Z0nv/zEeu/c59c+e/I3TnSOtY8eOrgwP7v3M7tr1fLURNGJbX2ig2iABDK35qmA2aLwOSj4EloX2wUWRlP9/pm+wO/ANPKMJuMO9tldYQO5SFWBiouqWAXK/vZY5F7BiHL3OsYjHZ/L/7i4d5xO7i79uD+Gv+gL3nvQBLxe3FOEyd2T7/YF7zj27brybvpDdO/+tiOTqVsfYDwPs+hwUWJHiUf6z//0iv7ea58Y/+yu/BPjzSeuDxdpachv4R/70h9dUV+qmtf6Enj+1lX/O09iu5V7+UXkfYF8cbS5mAxQLhJRmzEeQddD24PAPKuSb/+SiRkXwwjLoo6KJnICe+QIIcNDcccK4TEiElEZWMRHI9WIuhFNwVYdUfCLB+RIP5T4xY8utdgNxzyw+SeYzZ6/0YMWRsfzwPYbeug/pCkX2clbH9C/+lnQNNQa7fn3z8MhTa04oLumyll37vwC3e/vkc4SyHXnm78V1ilSKnojbZlYJRscjAvEYNxgPaIyGO3hxZlTrYeXB66RHukXrToW1boqWqX0i1a5SqSQS4SUdhYLmZHUkBMNmlLDxZgKIgG8dEBkNALV6WzfpdTFQrnUtH/56JNjo+9Pwwru1K01dqPVMvWPbmJCD2o8iOms6NVyH90cq0Mtx4O5mlektc0u/Nnm317f8GP7hmrh4nWvuJqr1XI0qdrV4v3YhxD7bzcfz49QgjrWFc8TSaV/XdCkD9aQmtELAN5hfE2AZNBDBSl67KHJJAJfIzrnepfoOl8lXOdLqRQhqanU1MSO7Kg3POBGrIDZV0AQFdBQClDYPo03HvmUXmbhCv0hnEEfvB/x/fp4AaF34VLRux/w5qXt6tgg5P8kfrhJZ/5f4/dY5H8NP/0Yf4JMkG7zWGaIEpEFzUePbEX4BcKl4LKngwQhoYdVqgBUBkPATKwRzFtal2gaXVU/pi0lk8mJ5EQ+60xX47Yhhib+O+D4mw9ARiMPBsX8/LyK+TlM9iClHm92WvuQjcuDWH0H1YUT2RhJDIvxCxJvP3gt6hHOxYoOQgQXNdiq4zDPH56ZsW1CZo7MHN4/b++x9+RzeKaVV7+WiD4shDQLCKkxpzcK6uYjCqOWLhOeTMQ+sYS11Cve+ifVNsC2x5LlwZo7MBYcKFilwUzUovQH1HLSqcmUL654Rc9zg6x+b9uEqfuHYDM2sqdczue9tGMl9ZgXKKVrk3PD3t5SJTOZHIuGk/rYYDGjZGz13q7/AkuMTot4nGNgZGBgAGJz3s7P8fw2Xxm4mV8ARRiucWx1hdH///7PYmVjDgJyORiYQKIASF0L1wAAeJxjYGRgYA76n8XAwMr6/+//X6xsDEARFCAKAI+DBeB4nGN+wcDAvACIBYEYyGZqAtKR//+CMUgsEsJnsoaxIepYyhgYWFn//0fHIHkAW04XaAAAAAAAAADEAQwCAgJKAsoDMAN6BDwEwgUgBYYGLgc4B5YIFAiUCRQJlAn+Co0AAQAAABUAhgANAAAAAAACADQARABzAAAAkgtwAAAAAHicdZDNSsNAFIVP+ie24EJBcDcrbRHSHyiCuCgUWnCni4LgJk3TJCWdKTNToS/hO/ggvorP4ml6ES2YYZjvnnvmzs0FcI4vBDh8Q+4DBzhhdOAK+UG4Sh4L18iPwnW08CTcoP4q3MQtYuEWLvDOCkHtlNEKH8IBzoKWcIV8JVwl3wjXyEPhOi6DsXCD+otwE7MgFW7hOvgcm83O5mnmVXvcUYNe/07Nd8pQynVUqGjrM2OdGqml0T4pChPGZq1dHhvtnpN0W0RWIjlmiXW50aof9kSZJjqxkU8W+8ruLR14v1RLa9ZqIjXVxppVEvsw835z3+3+foujNNhgB4scKTJ4KLSpdngO0EMfd6Q5HYrOgyuHRoSCSoQtb2RlxjEecS8ZaaoJHQU55PgN1tQcb8Zl1uGZ+ZS3C9awR7m/0YxOWyr7WLGjkH399Uzp0aUvKl9e/PTs8MZ3BlQ9O9t3Z8tuFCZHfSrOYZ9bUYmph+U0PNV7dLn++a9vwN+AuwAAAHicbYxbCsIwFAVzND5atb7dRcHXhuLtxRZvk5ImgrtXEQuC8zVnPo7qqQ+p+s8WPfShMcAQI4yRIMUEU8yQYY4FllhhjQ222Knk7iTWnMdGNxJbzUUVtDi6zclYYsmp8iRcHLPfPfzMPrnrtGRpvl2HygZds41Z63zIbawv7N/3ZIRSKs0rSmU5uZgQ2D/yc2enzo6dHTrbJ6WL/iqmbZV6AhuvQPgAAHicY/DewXAiKGIjI2Nf5AbGnRwMHAzJBRsZWJ02MTAyaIEYm7mYGDkgLD4GMIvNaRfTAaA0J5DN7rSLwQHCZmZw2ajC2BEYscGhI2Ijc4rLRjUQbxdHAwMji0NHckgESEkkEGzmYWLk0drB+L91A0vvRiYGFwAMdiP0AAA=) format('woff'), + url(data:application/font-svg;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxtZXRhZGF0YT5Db3B5cmlnaHQgKEMpIDIwMTcgYnkgb3JpZ2luYWwgYXV0aG9ycyBAIGZvbnRlbGxvLmNvbTwvbWV0YWRhdGE+CjxkZWZzPgo8Zm9udCBpZD0ibnNpY29ucyIgaG9yaXotYWR2LXg9IjEwMDAiID4KPGZvbnQtZmFjZSBmb250LWZhbWlseT0ibnNpY29ucyIgZm9udC13ZWlnaHQ9IjQwMCIgZm9udC1zdHJldGNoPSJub3JtYWwiIHVuaXRzLXBlci1lbT0iMTAwMCIgYXNjZW50PSI4NTAiIGRlc2NlbnQ9Ii0xNTAiIC8+CjxtaXNzaW5nLWdseXBoIGhvcml6LWFkdi14PSIxMDAwIiAvPgo8Z2x5cGggZ2x5cGgtbmFtZT0idm9sdW1lLXVwIiB1bmljb2RlPSImI3hlODAwOyIgZD0iTTQyOSA2NTR2LTYwOHEwLTE0LTExLTI1dC0yNS0xMC0yNSAxMGwtMTg2IDE4NmgtMTQ2cS0xNSAwLTI1IDExdC0xMSAyNXYyMTRxMCAxNSAxMSAyNXQyNSAxMWgxNDZsMTg2IDE4NnExMCAxMCAyNSAxMHQyNS0xMCAxMS0yNXogbTIxNC0zMDRxMC00Mi0yNC03OXQtNjMtNTJxLTUtMy0xNC0zLTE0IDAtMjUgMTB0LTEwIDI2cTAgMTIgNiAyMHQxNyAxNCAxOSAxMiAxNiAyMSA2IDMxLTYgMzItMTYgMjAtMTkgMTMtMTcgMTMtNiAyMHEwIDE1IDEwIDI2dDI1IDEwcTkgMCAxNC0zIDM5LTE1IDYzLTUydDI0LTc5eiBtMTQzIDBxMC04NS00OC0xNTh0LTEyNS0xMDVxLTctMy0xNC0zLTE1IDAtMjYgMTF0LTEwIDI1cTAgMjIgMjEgMzMgMzIgMTYgNDMgMjUgNDEgMzAgNjQgNzV0MjMgOTctMjMgOTctNjQgNzVxLTExIDktNDMgMjUtMjEgMTEtMjEgMzMgMCAxNCAxMCAyNXQyNSAxMXE4IDAgMTUtMyA3OC0zMyAxMjUtMTA1dDQ4LTE1OHogbTE0MyAwcTAtMTI4LTcxLTIzNnQtMTg5LTE1OHEtNy0zLTE0LTMtMTUgMC0yNSAxMXQtMTEgMjVxMCAyMCAyMiAzMyA0IDIgMTIgNnQxMyA2cTI1IDE0IDQ2IDI4IDY4IDUxIDEwNyAxMjd0MzggMTYxLTM4IDE2MS0xMDcgMTI3cS0yMSAxNS00NiAyOC00IDMtMTMgNnQtMTIgNnEtMjIgMTMtMjIgMzMgMCAxNSAxMSAyNXQyNSAxMXE3IDAgMTQtMyAxMTgtNTEgMTg5LTE1OHQ3MS0yMzZ6IiBob3Jpei1hZHYteD0iOTI4LjYiIC8+Cgo8Z2x5cGggZ2x5cGgtbmFtZT0icGx1cyIgdW5pY29kZT0iJiN4ZTgwMTsiIGQ9Ik03ODYgNDM5di0xMDdxMC0yMi0xNi0zOHQtMzgtMTVoLTIzMnYtMjMzcTAtMjItMTYtMzd0LTM4LTE2aC0xMDdxLTIyIDAtMzggMTZ0LTE1IDM3djIzM2gtMjMycS0yMyAwLTM4IDE1dC0xNiAzOHYxMDdxMCAyMyAxNiAzOHQzOCAxNmgyMzJ2MjMycTAgMjIgMTUgMzh0MzggMTZoMTA3cTIzIDAgMzgtMTZ0MTYtMzh2LTIzMmgyMzJxMjMgMCAzOC0xNnQxNi0zOHoiIGhvcml6LWFkdi14PSI3ODUuNyIgLz4KCjxnbHlwaCBnbHlwaC1uYW1lPSJlZGl0IiB1bmljb2RlPSImI3hlODAyOyIgZD0iTTQ5NiAxODlsNjQgNjUtODUgODUtNjQtNjV2LTMxaDUzdi01NGgzMnogbTI0NSA0MDJxLTkgOS0xOCAwbC0xOTYtMTk2cS05LTkgMC0xOHQxOCAwbDE5NiAxOTZxOSA5IDAgMTh6IG00NS0zMzF2LTEwNnEwLTY3LTQ3LTExNHQtMTE0LTQ3aC00NjRxLTY3IDAtMTE0IDQ3dC00NyAxMTR2NDY0cTAgNjYgNDcgMTEzdDExNCA0OGg0NjRxMzUgMCA2NS0xNCA5LTQgMTAtMTMgMi0xMC01LTE2bC0yNy0yOHEtOC04LTE4LTQtMTMgMy0yNSAzaC00NjRxLTM3IDAtNjMtMjZ0LTI3LTYzdi00NjRxMC0zNyAyNy02M3Q2My0yN2g0NjRxMzcgMCA2MyAyN3QyNiA2M3Y3MHEwIDcgNSAxMmwzNiAzNnE4IDggMjAgNHQxMS0xNnogbS01NCA0MTFsMTYxLTE2MC0zNzUtMzc1aC0xNjF2MTYweiBtMjQ4LTczbC01MS01Mi0xNjEgMTYxIDUxIDUycTE2IDE1IDM4IDE1dDM4LTE1bDg1LTg1cTE2LTE2IDE2LTM4dC0xNi0zOHoiIGhvcml6LWFkdi14PSIxMDAwIiAvPgoKPGdseXBoIGdseXBoLW5hbWU9ImxvY2siIHVuaWNvZGU9IiYjeGU4MDM7IiBkPSJNMTc5IDQyMWgyODV2MTA4cTAgNTktNDIgMTAxdC0xMDEgNDEtMTAxLTQxLTQxLTEwMXYtMTA4eiBtNDY0LTUzdi0zMjJxMC0yMi0xNi0zN3QtMzgtMTZoLTUzNXEtMjMgMC0zOCAxNnQtMTYgMzd2MzIycTAgMjIgMTYgMzh0MzggMTVoMTd2MTA4cTAgMTAyIDc0IDE3NnQxNzYgNzQgMTc3LTc0IDczLTE3NnYtMTA4aDE4cTIzIDAgMzgtMTV0MTYtMzh6IiBob3Jpei1hZHYteD0iNjQyLjkiIC8+Cgo8Z2x5cGggZ2x5cGgtbmFtZT0iY2FuY2VsLWNpcmNsZWQyIiB1bmljb2RlPSImI3hlODA0OyIgZD0iTTYxMiAyNDhsLTgxLTgycS02LTUtMTMtNXQtMTMgNWwtNzYgNzctNzctNzdxLTUtNS0xMy01dC0xMiA1bC04MiA4MnEtNiA2LTYgMTN0NiAxM2w3NiA3Ni03NiA3N3EtNiA1LTYgMTJ0NiAxM2w4MiA4MnE1IDUgMTIgNXQxMy01bDc3LTc3IDc2IDc3cTYgNSAxMyA1dDEzLTVsODEtODJxNi01IDYtMTN0LTYtMTJsLTc2LTc3IDc2LTc2cTYtNiA2LTEzdC02LTEzeiBtMTIwIDEwMnEwIDgzLTQxIDE1MnQtMTEwIDExMS0xNTIgNDEtMTUzLTQxLTExMC0xMTEtNDEtMTUyIDQxLTE1MiAxMTAtMTExIDE1My00MSAxNTIgNDEgMTEwIDExMSA0MSAxNTJ6IG0xMjUgMHEwLTExNy01Ny0yMTV0LTE1Ni0xNTYtMjE1LTU4LTIxNiA1OC0xNTUgMTU2LTU4IDIxNSA1OCAyMTUgMTU1IDE1NiAyMTYgNTggMjE1LTU4IDE1Ni0xNTYgNTctMjE1eiIgaG9yaXotYWR2LXg9Ijg1Ny4xIiAvPgoKPGdseXBoIGdseXBoLW5hbWU9ImNhbmNlbC1jaXJjbGVkIiB1bmljb2RlPSImI3hlODA1OyIgZD0iTTY0MSAyMjRxMCAxNC0xMCAyNWwtMTAxIDEwMSAxMDEgMTAxcTEwIDExIDEwIDI1IDAgMTUtMTAgMjZsLTUxIDUwcS0xMCAxMS0yNSAxMS0xNSAwLTI1LTExbC0xMDEtMTAxLTEwMSAxMDFxLTExIDExLTI1IDExLTE2IDAtMjYtMTFsLTUwLTUwcS0xMS0xMS0xMS0yNiAwLTE0IDExLTI1bDEwMS0xMDEtMTAxLTEwMXEtMTEtMTEtMTEtMjUgMC0xNSAxMS0yNmw1MC01MHExMC0xMSAyNi0xMSAxNCAwIDI1IDExbDEwMSAxMDEgMTAxLTEwMXExMC0xMSAyNS0xMSAxNSAwIDI1IDExbDUxIDUwcTEwIDExIDEwIDI2eiBtMjE2IDEyNnEwLTExNy01Ny0yMTV0LTE1Ni0xNTYtMjE1LTU4LTIxNiA1OC0xNTUgMTU2LTU4IDIxNSA1OCAyMTUgMTU1IDE1NiAyMTYgNTggMjE1LTU4IDE1Ni0xNTYgNTctMjE1eiIgaG9yaXotYWR2LXg9Ijg1Ny4xIiAvPgoKPGdseXBoIGdseXBoLW5hbWU9ImNhbmNlbCIgdW5pY29kZT0iJiN4ZTgwNjsiIGQ9Ik03MjQgMTEycTAtMjItMTUtMzhsLTc2LTc2cS0xNi0xNS0zOC0xNXQtMzggMTVsLTE2NCAxNjUtMTY0LTE2NXEtMTYtMTUtMzgtMTV0LTM4IDE1bC03NiA3NnEtMTYgMTYtMTYgMzh0MTYgMzhsMTY0IDE2NC0xNjQgMTY0cS0xNiAxNi0xNiAzOHQxNiAzOGw3NiA3NnExNiAxNiAzOCAxNnQzOC0xNmwxNjQtMTY0IDE2NCAxNjRxMTYgMTYgMzggMTZ0MzgtMTZsNzYtNzZxMTUtMTUgMTUtMzh0LTE1LTM4bC0xNjQtMTY0IDE2NC0xNjRxMTUtMTUgMTUtMzh6IiBob3Jpei1hZHYteD0iNzg1LjciIC8+Cgo8Z2x5cGggZ2x5cGgtbmFtZT0iY29nIiB1bmljb2RlPSImI3hlODA3OyIgZD0iTTU3MSAzNTBxMCA1OS00MSAxMDF0LTEwMSA0Mi0xMDEtNDItNDItMTAxIDQyLTEwMSAxMDEtNDIgMTAxIDQyIDQxIDEwMXogbTI4NiA2MXYtMTI0cTAtNy00LTEzdC0xMS03bC0xMDQtMTZxLTEwLTMwLTIxLTUxIDE5LTI3IDU5LTc3IDYtNiA2LTEzdC01LTEzcS0xNS0yMS01NS02MXQtNTMtMzlxLTcgMC0xNCA1bC03NyA2MHEtMjUtMTMtNTEtMjEtOS03Ni0xNi0xMDQtNC0xNi0yMC0xNmgtMTI0cS04IDAtMTQgNXQtNiAxMmwtMTYgMTAzcS0yNyA5LTUwIDIxbC03OS02MHEtNi01LTE0LTUtOCAwLTE0IDYtNzAgNjQtOTIgOTQtNCA1LTQgMTMgMCA2IDUgMTIgOCAxMiAyOCAzN3QzMCA0MHEtMTUgMjgtMjMgNTVsLTEwMiAxNXEtNyAxLTExIDd0LTUgMTN2MTI0cTAgNyA1IDEzdDEwIDdsMTA0IDE2cTggMjUgMjIgNTEtMjMgMzItNjAgNzctNiA3LTYgMTQgMCA1IDUgMTIgMTUgMjAgNTUgNjB0NTMgNDBxNyAwIDE1LTVsNzctNjBxMjQgMTMgNTAgMjEgOSA3NiAxNyAxMDQgMyAxNiAyMCAxNmgxMjRxNyAwIDEzLTV0Ny0xMmwxNS0xMDNxMjgtOSA1MS0yMGw3OSA1OXE1IDUgMTMgNSA3IDAgMTQtNSA3Mi02NyA5Mi05NSA0LTUgNC0xMiAwLTctNC0xMy05LTEyLTI5LTM3dC0zMC00MHExNS0yOCAyMy01NGwxMDItMTZxNy0xIDEyLTd0NC0xM3oiIGhvcml6LWFkdi14PSI4NTcuMSIgLz4KCjxnbHlwaCBnbHlwaC1uYW1lPSJoZWxwLWNpcmNsZWQiIHVuaWNvZGU9IiYjeGU4MDg7IiBkPSJNNTAwIDgydjEwN3EwIDgtNSAxM3QtMTMgNWgtMTA3cS04IDAtMTMtNXQtNS0xM3YtMTA3cTAtOCA1LTEzdDEzLTVoMTA3cTggMCAxMyA1dDUgMTN6IG0xNDMgMzc1cTAgNDktMzEgOTF0LTc3IDY1LTk1IDIzcS0xMzYgMC0yMDctMTE5LTktMTMgNC0yNGw3NC01NXE0LTQgMTAtNCA5IDAgMTQgNyAzMCAzOCA0OCA1MSAxOSAxNCA0OCAxNCAyNyAwIDQ4LTE1dDIxLTMzcTAtMjEtMTEtMzR0LTM4LTI1cS0zNS0xNS02NS00OHQtMjktNzB2LTIwcTAtOCA1LTEzdDEzLTVoMTA3cTggMCAxMyA1dDUgMTNxMCAxMCAxMiAyN3QzMCAyOHExOCAxMCAyOCAxNnQyNSAxOSAyNSAyNyAxNiAzNCA3IDQ1eiBtMjE0LTEwN3EwLTExNy01Ny0yMTV0LTE1Ni0xNTYtMjE1LTU4LTIxNiA1OC0xNTUgMTU2LTU4IDIxNSA1OCAyMTUgMTU1IDE1NiAyMTYgNTggMjE1LTU4IDE1Ni0xNTYgNTctMjE1eiIgaG9yaXotYWR2LXg9Ijg1Ny4xIiAvPgoKPGdseXBoIGdseXBoLW5hbWU9InRpbnQiIHVuaWNvZGU9IiYjeGU4MDk7IiBkPSJNMjg2IDIwN3EwIDIwLTExIDM5LTEgMC05IDEydC0xNCAyMS0xNCAyNS0xMiAyOHEtMiA5LTEyIDl0LTExLTlxLTQtMTMtMTItMjh0LTE0LTI1LTE0LTIxLTktMTJxLTExLTE5LTExLTM5IDAtMjkgMjEtNTB0NTAtMjEgNTEgMjEgMjEgNTB6IG0yODUgNzJxMC0xMTktODMtMjAydC0yMDItODQtMjAyIDg0LTg0IDIwMnEwIDgxIDQ1IDE1MyA0IDUgMzUgNTF0NTYgODQgNTYgOTkgNDYgMTEzcTUgMTYgMTkgMjZ0MjkgOSAyOS05IDE4LTI2cTE2LTUyIDQ3LTExM3Q1NS05OSA1Ni04NCAzNS01MXE0NS03MSA0NS0xNTN6IiBob3Jpei1hZHYteD0iNTcxLjQiIC8+Cgo8Z2x5cGggZ2x5cGgtbmFtZT0ibWVudSIgdW5pY29kZT0iJiN4ZjBjOTsiIGQ9Ik04NTcgMTAwdi03MXEwLTE1LTEwLTI1dC0yNi0xMWgtNzg1cS0xNSAwLTI1IDExdC0xMSAyNXY3MXEwIDE1IDExIDI1dDI1IDExaDc4NXExNSAwIDI2LTExdDEwLTI1eiBtMCAyODZ2LTcycTAtMTQtMTAtMjV0LTI2LTEwaC03ODVxLTE1IDAtMjUgMTB0LTExIDI1djcycTAgMTQgMTEgMjV0MjUgMTBoNzg1cTE1IDAgMjYtMTB0MTAtMjV6IG0wIDI4NXYtNzFxMC0xNC0xMC0yNXQtMjYtMTFoLTc4NXEtMTUgMC0yNSAxMXQtMTEgMjV2NzFxMCAxNSAxMSAyNnQyNSAxMGg3ODVxMTUgMCAyNi0xMHQxMC0yNnoiIGhvcml6LWFkdi14PSI4NTcuMSIgLz4KCjxnbHlwaCBnbHlwaC1uYW1lPSJzb3J0LW51bWJlci11cCIgdW5pY29kZT0iJiN4ZjE2MjsiIGQ9Ik03NTEgMTE3cTAgMzYtMjQgNjV0LTU4IDMwcS0yOSAwLTQ2LTIxdC0xNy01MiAyMC01MyA1OC0yMnEyOCAwIDQ4IDE1dDE5IDM4eiBtLTM0MC03MXEwLTYtNi0xM2wtMTc4LTE3OHEtNS01LTEzLTUtNiAwLTEyIDVsLTE3OSAxNzlxLTggOS00IDE5IDQgMTEgMTcgMTFoMTA3djc2OHEwIDggNSAxM3QxMyA1aDEwN3E4IDAgMTMtNXQ1LTEzdi03NjhoMTA3cTggMCAxMy01dDUtMTN6IG00MTggMzlxMC0zNS03LTY4dC0yMy02NC0zOC01My01NS0zNi03MS0xNHEtMzUgMC02MCA5LTE0IDQtMjQgOGwyMiA2M3E5LTQgMTctNiAyMS03IDQyLTcgNDcgMCA3NSAzM3QzNyA4MWgtMXEtMTEtMTMtMzQtMjF0LTQ3LThxLTU5IDAtOTcgNDB0LTM3IDk3cTAgNTggNDAgOTl0MTAxIDQxcTY5IDAgMTE1LTUzdDQ1LTE0MXogbS0xNiA0MDB2LTY0aC0yNjJ2NjRoOTN2MjQxcTAgNCAwIDExdDEgOXY5aC0ybC0zLTdxLTUtNy0xNS0xN2wtMzUtMzItNDUgNDggMTA3IDEwM2g2OHYtMzY1aDkzeiIgaG9yaXotYWR2LXg9Ijg1Ny4xIiAvPgoKPGdseXBoIGdseXBoLW5hbWU9ImNhbGMiIHVuaWNvZGU9IiYjeGYxZWM7IiBkPSJNMjE0LTdxMCAyOS0yMSA1MHQtNTAgMjEtNTEtMjEtMjEtNTAgMjEtNTEgNTEtMjEgNTAgMjEgMjEgNTF6IG0yMTUgMHEwIDI5LTIxIDUwdC01MSAyMS01MC0yMS0yMS01MCAyMS01MSA1MC0yMSA1MSAyMSAyMSA1MXogbS0yMTUgMjE0cTAgMzAtMjEgNTF0LTUwIDIxLTUxLTIxLTIxLTUxIDIxLTUwIDUxLTIxIDUwIDIxIDIxIDUweiBtNDI5LTIxNHEwIDI5LTIxIDUwdC01MSAyMS01MC0yMS0yMS01MCAyMS01MSA1MC0yMSA1MSAyMSAyMSA1MXogbS0yMTQgMjE0cTAgMzAtMjEgNTF0LTUxIDIxLTUwLTIxLTIxLTUxIDIxLTUwIDUwLTIxIDUxIDIxIDIxIDUweiBtLTIxNSAyMTRxMCAzMC0yMSA1MXQtNTAgMjEtNTEtMjEtMjEtNTEgMjEtNTAgNTEtMjEgNTAgMjEgMjEgNTB6IG00MjktMjE0cTAgMzAtMjEgNTF0LTUxIDIxLTUwLTIxLTIxLTUxIDIxLTUwIDUwLTIxIDUxIDIxIDIxIDUweiBtLTIxNCAyMTRxMCAzMC0yMSA1MXQtNTEgMjEtNTAtMjEtMjEtNTEgMjEtNTAgNTAtMjEgNTEgMjEgMjEgNTB6IG00MjgtNDI4djIxNHEwIDI5LTIxIDUwdC01MCAyMi01MC0yMi0yMi01MHYtMjE0cTAtMjkgMjItNTB0NTAtMjIgNTAgMjIgMjEgNTB6IG0tMjE0IDQyOHEwIDMwLTIxIDUxdC01MSAyMS01MC0yMS0yMS01MSAyMS01MCA1MC0yMSA1MSAyMSAyMSA1MHogbTIxNCAxNzl2MTQzcTAgMTQtMTAgMjV0LTI2IDExaC03MTRxLTE0IDAtMjUtMTF0LTExLTI1di0xNDNxMC0xNCAxMS0yNXQyNS0xMWg3MTRxMTUgMCAyNiAxMXQxMCAyNXogbTAtMTc5cTAgMzAtMjEgNTF0LTUwIDIxLTUxLTIxLTIxLTUxIDIxLTUwIDUxLTIxIDUwIDIxIDIxIDUweiBtNzIgMzU4di04NThxMC0yOS0yMi01MHQtNTAtMjFoLTc4NnEtMjkgMC01MCAyMXQtMjEgNTB2ODU4cTAgMjkgMjEgNTB0NTAgMjFoNzg2cTI5IDAgNTAtMjF0MjItNTB6IiBob3Jpei1hZHYteD0iMTAwMCIgLz4KCjxnbHlwaCBnbHlwaC1uYW1lPSJjaGFydC1saW5lIiB1bmljb2RlPSImI3hmMjAxOyIgZD0iTTExNDMtN3YtNzJoLTExNDN2ODU4aDcxdi03ODZoMTA3MnogbS03MiA2OTZ2LTI0MnEwLTEyLTEwLTE3dC0yMCA0bC02OCA2OC0zNTMtMzUzcS02LTYtMTMtNnQtMTMgNmwtMTMwIDEzMC0yMzItMjMzLTEwNyAxMDggMzI3IDMyNnE1IDYgMTIgNnQxMy02bDEzMC0xMzAgMjU5IDI1OS02NyA2OHEtOSA4LTUgMTl0MTcgMTFoMjQzcTcgMCAxMi01dDUtMTN6IiBob3Jpei1hZHYteD0iMTE0Mi45IiAvPgoKPGdseXBoIGdseXBoLW5hbWU9ImJhdHRlcnktNCIgdW5pY29kZT0iJiN4ZjI0MDsiIGQ9Ik0xMDcxIDU2NHYtNDI4aC05Mjh2NDI4aDkyOHogbTcyLTMyMWg3MXYyMTRoLTcxdjE2MXEwIDgtNSAxM3QtMTMgNWgtMTAzNnEtNyAwLTEyLTV0LTYtMTN2LTUzNnEwLTggNi0xM3QxMi01aDEwMzZxOCAwIDEzIDV0NSAxM3YxNjF6IG0xNDMgMjE0di0yMTRxMC0zMC0yMS01MXQtNTEtMjF2LTg5cTAtMzctMjYtNjN0LTYzLTI2aC0xMDM2cS0zNiAwLTYzIDI2dC0yNiA2M3Y1MzZxMCAzNyAyNiA2M3Q2MyAyNmgxMDM2cTM3IDAgNjMtMjZ0MjYtNjN2LTg5cTMwIDAgNTEtMjF0MjEtNTF6IiBob3Jpei1hZHYteD0iMTI4NS43IiAvPgoKPGdseXBoIGdseXBoLW5hbWU9ImJhdHRlcnktMyIgdW5pY29kZT0iJiN4ZjI0MTsiIGQ9Ik0xNDMgMTM2djQyOGg3MTR2LTQyOGgtNzE0eiBtMTA3MSAzOTNxMzAgMCA1MS0yMXQyMS01MXYtMjE0cTAtMzAtMjEtNTF0LTUxLTIxdi04OXEwLTM3LTI2LTYzdC02My0yNmgtMTAzNnEtMzYgMC02MyAyNnQtMjYgNjN2NTM2cTAgMzcgMjYgNjN0NjMgMjZoMTAzNnEzNyAwIDYzLTI2dDI2LTYzdi04OXogbTAtMjg2djIxNGgtNzF2MTYxcTAgOC01IDEzdC0xMyA1aC0xMDM2cS03IDAtMTItNXQtNi0xM3YtNTM2cTAtOCA2LTEzdDEyLTVoMTAzNnE4IDAgMTMgNXQ1IDEzdjE2MWg3MXoiIGhvcml6LWFkdi14PSIxMjg1LjciIC8+Cgo8Z2x5cGggZ2x5cGgtbmFtZT0iYmF0dGVyeS0yIiB1bmljb2RlPSImI3hmMjQyOyIgZD0iTTE0MyAxMzZ2NDI4aDUwMHYtNDI4aC01MDB6IG0xMDcxIDM5M3EzMCAwIDUxLTIxdDIxLTUxdi0yMTRxMC0zMC0yMS01MXQtNTEtMjF2LTg5cTAtMzctMjYtNjN0LTYzLTI2aC0xMDM2cS0zNiAwLTYzIDI2dC0yNiA2M3Y1MzZxMCAzNyAyNiA2M3Q2MyAyNmgxMDM2cTM3IDAgNjMtMjZ0MjYtNjN2LTg5eiBtMC0yODZ2MjE0aC03MXYxNjFxMCA4LTUgMTN0LTEzIDVoLTEwMzZxLTcgMC0xMi01dC02LTEzdi01MzZxMC04IDYtMTN0MTItNWgxMDM2cTggMCAxMyA1dDUgMTN2MTYxaDcxeiIgaG9yaXotYWR2LXg9IjEyODUuNyIgLz4KCjxnbHlwaCBnbHlwaC1uYW1lPSJiYXR0ZXJ5LTEiIHVuaWNvZGU9IiYjeGYyNDM7IiBkPSJNMTQzIDEzNnY0MjhoMjg2di00MjhoLTI4NnogbTEwNzEgMzkzcTMwIDAgNTEtMjF0MjEtNTF2LTIxNHEwLTMwLTIxLTUxdC01MS0yMXYtODlxMC0zNy0yNi02M3QtNjMtMjZoLTEwMzZxLTM2IDAtNjMgMjZ0LTI2IDYzdjUzNnEwIDM3IDI2IDYzdDYzIDI2aDEwMzZxMzcgMCA2My0yNnQyNi02M3YtODl6IG0wLTI4NnYyMTRoLTcxdjE2MXEwIDgtNSAxM3QtMTMgNWgtMTAzNnEtNyAwLTEyLTV0LTYtMTN2LTUzNnEwLTggNi0xM3QxMi01aDEwMzZxOCAwIDEzIDV0NSAxM3YxNjFoNzF6IiBob3Jpei1hZHYteD0iMTI4NS43IiAvPgoKPGdseXBoIGdseXBoLW5hbWU9ImJhdHRlcnktMCIgdW5pY29kZT0iJiN4ZjI0NDsiIGQ9Ik0xMjE0IDUyOXEzMCAwIDUxLTIxdDIxLTUxdi0yMTRxMC0zMC0yMS01MXQtNTEtMjF2LTg5cTAtMzctMjYtNjN0LTYzLTI2aC0xMDM2cS0zNiAwLTYzIDI2dC0yNiA2M3Y1MzZxMCAzNyAyNiA2M3Q2MyAyNmgxMDM2cTM3IDAgNjMtMjZ0MjYtNjN2LTg5eiBtMC0yODZ2MjE0aC03MXYxNjFxMCA4LTUgMTN0LTEzIDVoLTEwMzZxLTcgMC0xMi01dC02LTEzdi01MzZxMC04IDYtMTN0MTItNWgxMDM2cTggMCAxMyA1dDUgMTN2MTYxaDcxeiIgaG9yaXotYWR2LXg9IjEyODUuNyIgLz4KCjxnbHlwaCBnbHlwaC1uYW1lPSJob3VyZ2xhc3MiIHVuaWNvZGU9IiYjeGYyNTQ7IiBkPSJNODM5LTQzcTggMCAxMy01dDUtMTN2LTcxcTAtOC01LTEzdC0xMy01aC04MjFxLTggMC0xMyA1dC01IDEzdjcxcTAgOCA1IDEzdDEzIDVoODIxeiBtLTc2NiAzNnExIDMxIDkgNjB0MTYgNTMgMjYgNDggMzAgNDMgMzYgMzggMzcgMzQgMzkgMzEgMzcgMjYgMzYgMjRxLTI0IDE2LTM2IDI0dC0zNyAyNy0zOSAzMC0zNyAzNC0zNiAzOS0zMCA0Mi0yNiA0OC0xNiA1My05IDYwaDcxMnEtMi0zMC05LTYwdC0xNy01My0yNi00OC0zMC00Mi0zNi0zOS0zNi0zNC00MC0zMC0zNy0yNy0zNi0yNHEyNC0xNiAzNi0yNHQzNy0yNiA0MC0zMSAzNi0zNCAzNi0zOCAzMC00MyAyNi00OCAxNy01MyA5LTYwaC03MTJ6IG03NjYgODU3cTggMCAxMy01dDUtMTN2LTcxcTAtOC01LTEzdC0xMy01aC04MjFxLTggMC0xMyA1dC01IDEzdjcxcTAgOCA1IDEzdDEzIDVoODIxeiIgaG9yaXotYWR2LXg9Ijg1Ny4xIiAvPgo8L2ZvbnQ+CjwvZGVmcz4KPC9zdmc+) format('svg'); + font-weight: normal; + 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,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; +} + + [class^="icon-"]:before, [class*=" icon-"]:before, + [class^="plugicon-"]:before, [class*=" plugicon-"]:before { + font-family: "nsicons"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +[class^="plugicon-"]:before, [class*=" plugicon-"]:before { + font-family: "pluginicons"; +} + +.icon-volume:before { content: '\e800'; } +.icon-plus:before { content: '\e801'; } +.icon-edit:before { content: '\e802'; } +.icon-lock:before { content: '\e803'; } +.icon-menu:before { content: '\f0c9'; } +.icon-calc:before { content: '\f1ec'; } +.icon-battery-100:before { content: '\f240'; } +.icon-battery-75:before { content: '\f241'; } +.icon-battery-50:before { content: '\f242'; } +.icon-battery-25:before { content: '\f243'; } +.icon-battery-0:before { content: '\f244'; } +.icon-cancel-circled2:before { content: '\e804'; } +.icon-cancel-circled:before { content: '\e805'; } +.icon-cancel:before { content: '\e806'; } +.icon-cog:before { content: '\e807'; } +.icon-help-circled:before { content: '\e808'; } +.icon-tint:before { content: '\e809'; } +.icon-sort-numeric:before { content: '\f162'; } +.icon-chart-line:before { content: '\f201'; } +.icon-hourglass:before { content: '\f254'; } + +/* Plugin Icons id-s (copy from generated icon style.css) */ +.plugicon-database:before { content: "\e901"; } +.plugicon-notifies:before { content: "\e91a"; } html, body { - height: 100%; margin: 0; padding: 0; } + body { - font-family: 'Open Sans', Helvetica, Arial, sans-serif; + font-family: 'Ubuntu', sans-serif; background: #000; color: #bdbdbd; } @@ -17,15 +101,30 @@ body { .container { bottom: 0; display: block; - height: 100%; left: 0; margin: 0; padding: 0; top: 45px; - width: 100%; z-index: 2; } +button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.ui-button { + background-color: grey; + border: 1px solid white; + padding: 2px; +} + +a, a:visited, a:link { + color: #2196f3; + text-decoration: none; +} + #container.announcing .customTitle { color: orange; } @@ -39,15 +138,16 @@ body { } .primary { - font-family: 'Ubuntu', Helvetica, Arial, sans-serif; + font-family: 'Ubuntu', Verdana, Helvetica, Arial, sans-serif; vertical-align: middle; clear: both; + white-space: nowrap; } .bgStatus { float: right; text-align: center; - white-space: nowrap; + white-space: normal; padding-right: 20px; } @@ -63,6 +163,10 @@ body { color: #4cff00; } +.bgStatus .bgSpan { + white-space: nowrap; +} + .bgStatus .currentBG { font-size: 120px; line-height: 100px; @@ -115,7 +219,7 @@ body { } .pill em, .pill label { - padding: 2px; + padding: 2px 3px; white-space: nowrap; } @@ -138,17 +242,17 @@ body { color: red; } -.alarming-timeago #lastEntry.pill.warn { +.alarming-timeago .timeago.pill.warn { border-color: yellow; } -.alarming-timeago #lastEntry.pill.warn label { +.alarming-timeago .timeago.pill.warn label { background: yellow; } -.alarming-timeago #lastEntry.pill.urgent { +.alarming-timeago .timeago.pill.urgent { border-color: red; } -.alarming-timeago #lastEntry.pill.urgent label { +.alarming-timeago .timeago.pill.urgent label { background: red; } @@ -163,14 +267,16 @@ body { .status { color: #808080; font-size: 100px; + white-space: normal; } .statusBox { text-align: center; - width: 250px; + width: 40%; } .statusPills { font-size: 20px; line-height: 23px; + margin-left: 5px; } .loading .statusPills { @@ -180,6 +286,7 @@ body { .statusPills .pill { background: #808080; border-color: #808080; + margin-right: 5px; } .statusPills .pill em { @@ -206,12 +313,23 @@ body { padding: 0; } -#silenceBtn { +#silenceBtn, #viewMenu { z-index: 99; border-radius: 5px; border: 2px solid #bdbdbd; } +#viewMenu { + left: 150px; + right: inherit; + font-size: 18px; +} + +#viewMenu li label input[type="checkbox"] { + height: 14px; + margin-right: 5px; +} + #silenceBtn a { font-size: 40px } @@ -220,6 +338,7 @@ body { margin: 10px; border: 2px solid #000; border-radius: 5px; + display: inline-block; width: 360px; } @@ -291,6 +410,7 @@ body { border-radius: 8px; float: right; min-width: 150px; + max-width: 400px; } .alarms { @@ -306,7 +426,7 @@ body { margin: 4px; padding: 0; color: #808080; - width: 250px; + width: 40%; text-align: center; } @@ -329,9 +449,36 @@ body { display: none; } -@media (max-width: 800px) { +.toolbar-title { + text-align: left; + padding: 0 10px; +} + +.toolbar-title h2 { + margin: 0; +} + +.page-content { + padding: 10px; +} + +.authentication-status { + padding: 10px; + border-top: 1px solid #bdbdbd; +} + +.authentication-status a { + color: #2196f3; + text-decoration: underline; +} + +.authentication-status .small { + font-size: 12px; +} + +@media (max-width: 1000px) { .bgStatus { - width: 300px; + width: 450px; } .bgButton { @@ -374,6 +521,7 @@ body { .focus-range { margin: 0; + width: 50%; } .focus-range li { @@ -390,9 +538,14 @@ body { } @media (max-width: 750px) { + .x.axis { + font-size: 2.5vmin !important; + } + + .bgStatus { - width: 240px; - padding: 0; + width: 50%; + padding: 0 0 20px 0; } .pill.rawbg em { @@ -403,6 +556,10 @@ body { font-size: 12px; } + .statusBox { + width: auto; + } + .bgStatus .currentBG { font-size: 70px; line-height: 60px; @@ -481,6 +638,13 @@ body { font-size: 20px; } + #viewMenu { + left: inherit; + top: 170px; + right: 10px; + text-align: left; + } + .status { padding-top: 0; font-size: 20px; @@ -520,6 +684,10 @@ body { #chartContainer { font-size: 14px; } + .y.axis { + font-size: 2.5vmin !important; + } + } @media (max-height: 600px) { @@ -529,9 +697,25 @@ body { } @media (max-height: 480px) { - #toolbar { + #container #toolbar { float: right; height: auto; + background: none; + border-bottom: none; + margin: 0px; + } + + #buttonbar { + margin-right: 0px; + padding-right: 15px; + margin-top: 15px; + height: 44px; + opacity: 0.75; + vertical-align: middle; + position: absolute; + right: 0; + z-index: 500; + width: 400px; } #toolbar .customTitle { @@ -634,14 +818,10 @@ body { display: block; } - .statusBox { - width: 220px; - } - .statusPills { display: inline; margin-left: auto; font-size: 12px; line-height: 13px; } -} +} \ No newline at end of file diff --git a/static/css/report.css b/static/css/report.css index 6fa3350cb90..6994eb96b6c 100644 --- a/static/css/report.css +++ b/static/css/report.css @@ -18,31 +18,95 @@ body { text-align:center; } +#explanation { + font: 11px verdana, arial, sans-serif; +} + #tabnav { text-align: left; - margin: 1em 0 1em 0; - font: bold 11px verdana, arial, sans-serif; + margin: 1em 0 0; + font: bold 11pt verdana, arial, sans-serif; border-bottom: 1px solid #6c6; list-style-type: none; - padding: 3px 10px 3px 10px; + padding: 0 0 0 15px; } -#tabnav li{ - display: inline; - padding: 3px 4px; - border: 1px solid #6c6; +#tabnav li { + display: inline-block; + padding: 10px 15px; + border-top: 1px solid #6c6; + border-right: 1px solid #6c6; + border-bottom: none; background-color: #cfc; color: #666; - margin-right: 0; + margin: 0 0 -1px 0; text-decoration: none; - border-bottom: none; +} + +#tabnav li:first-child { + border-left: 1px solid #6c6; } #tabnav .selected { background: #fff; + border-bottom: 1px solid #fff; } #tabnav li:hover { background: #9f9; cursor:pointer; } + +.tooltip { + top: 0; + left: 0; + color: #444; + position: absolute; + text-align: left; + padding: 4px; + font-size: 14px; + line-height: 15pt; + background: white; + border: 2px solid black; + border-radius: 8px; + float: right; + min-width: 150px; + max-width: 400px; +} + +main { + padding: 15px; +} + +input[type=date], +input[type=text], +input[type=number], +select { + font: 13px verdana, arial, sans-serif; +} + +label { + display: inline-flex; + justify-content: flex-start; + align-items: center; + margin-right: 7px; +} + +#rp_to { + margin-right: 10px; +} + +.presetdates { + display: inline-block; + margin-right: 8px; +} + +#rp_show { + background-color: #cfc; + border: 1px solid #6c6; + color: #666; + padding: 10px; + font-weight: bold; + font-size: 12pt; + text-transform: uppercase; +} \ No newline at end of file diff --git a/static/css/translations.css b/static/css/translations.css new file mode 100644 index 00000000000..b1fae30e253 --- /dev/null +++ b/static/css/translations.css @@ -0,0 +1,16 @@ +body { + color: black; + font-family: 'Open Sans', Helvetica, Arial, sans-serif; + background-color: white; +} + +th, +td { + vertical-align: top; + text-align: left; + border: 1px solid +} + +#translations { + padding: 15px; +} diff --git a/static/css/ui-darkness/images/animated-overlay.gif b/static/css/ui-darkness/images/animated-overlay.gif new file mode 100755 index 00000000000..d441f75ebfb Binary files /dev/null and b/static/css/ui-darkness/images/animated-overlay.gif differ diff --git a/static/css/ui-darkness/images/ui-bg_flat_30_cccccc_40x100.png b/static/css/ui-darkness/images/ui-bg_flat_30_cccccc_40x100.png new file mode 100644 index 00000000000..41ddde6c6fc Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_flat_30_cccccc_40x100.png differ diff --git a/static/css/ui-darkness/images/ui-bg_flat_50_5c5c5c_40x100.png b/static/css/ui-darkness/images/ui-bg_flat_50_5c5c5c_40x100.png new file mode 100644 index 00000000000..13782df7a49 Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_flat_50_5c5c5c_40x100.png differ diff --git a/static/css/ui-darkness/images/ui-bg_glass_20_555555_1x400.png b/static/css/ui-darkness/images/ui-bg_glass_20_555555_1x400.png new file mode 100644 index 00000000000..d24328e4f81 Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_glass_20_555555_1x400.png differ diff --git a/static/css/ui-darkness/images/ui-bg_glass_40_0078a3_1x400.png b/static/css/ui-darkness/images/ui-bg_glass_40_0078a3_1x400.png new file mode 100644 index 00000000000..507fc4ccb1b Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_glass_40_0078a3_1x400.png differ diff --git a/static/css/ui-darkness/images/ui-bg_glass_40_ffc73d_1x400.png b/static/css/ui-darkness/images/ui-bg_glass_40_ffc73d_1x400.png new file mode 100644 index 00000000000..45c415aed53 Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_glass_40_ffc73d_1x400.png differ diff --git a/static/css/ui-darkness/images/ui-bg_gloss-wave_25_333333_500x100.png b/static/css/ui-darkness/images/ui-bg_gloss-wave_25_333333_500x100.png new file mode 100644 index 00000000000..f711ba7a189 Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_gloss-wave_25_333333_500x100.png differ diff --git a/static/css/ui-darkness/images/ui-bg_highlight-soft_80_eeeeee_1x100.png b/static/css/ui-darkness/images/ui-bg_highlight-soft_80_eeeeee_1x100.png new file mode 100644 index 00000000000..5734412d8cb Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_highlight-soft_80_eeeeee_1x100.png differ diff --git a/static/css/ui-darkness/images/ui-bg_inset-soft_25_000000_1x100.png b/static/css/ui-darkness/images/ui-bg_inset-soft_25_000000_1x100.png new file mode 100644 index 00000000000..b9bc434b77d Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_inset-soft_25_000000_1x100.png differ diff --git a/static/css/ui-darkness/images/ui-bg_inset-soft_30_f58400_1x100.png b/static/css/ui-darkness/images/ui-bg_inset-soft_30_f58400_1x100.png new file mode 100644 index 00000000000..e394446ef9e Binary files /dev/null and b/static/css/ui-darkness/images/ui-bg_inset-soft_30_f58400_1x100.png differ diff --git a/static/css/ui-darkness/images/ui-icons_222222_256x240.png b/static/css/ui-darkness/images/ui-icons_222222_256x240.png new file mode 100644 index 00000000000..82fad68f5f7 Binary files /dev/null and b/static/css/ui-darkness/images/ui-icons_222222_256x240.png differ diff --git a/static/css/ui-darkness/images/ui-icons_4b8e0b_256x240.png b/static/css/ui-darkness/images/ui-icons_4b8e0b_256x240.png new file mode 100644 index 00000000000..2e41f6e335a Binary files /dev/null and b/static/css/ui-darkness/images/ui-icons_4b8e0b_256x240.png differ diff --git a/static/css/ui-darkness/images/ui-icons_a83300_256x240.png b/static/css/ui-darkness/images/ui-icons_a83300_256x240.png new file mode 100644 index 00000000000..c4d307eacf0 Binary files /dev/null and b/static/css/ui-darkness/images/ui-icons_a83300_256x240.png differ diff --git a/static/css/ui-darkness/images/ui-icons_cccccc_256x240.png b/static/css/ui-darkness/images/ui-icons_cccccc_256x240.png new file mode 100644 index 00000000000..50456e39583 Binary files /dev/null and b/static/css/ui-darkness/images/ui-icons_cccccc_256x240.png differ diff --git a/static/css/ui-darkness/images/ui-icons_ffffff_256x240.png b/static/css/ui-darkness/images/ui-icons_ffffff_256x240.png new file mode 100644 index 00000000000..cd6c26561dc Binary files /dev/null and b/static/css/ui-darkness/images/ui-icons_ffffff_256x240.png differ diff --git a/static/css/ui-darkness/jquery-ui.css b/static/css/ui-darkness/jquery-ui.css new file mode 100644 index 00000000000..84034949921 --- /dev/null +++ b/static/css/ui-darkness/jquery-ui.css @@ -0,0 +1,1225 @@ +/*! jQuery UI - v1.11.4 - 2015-03-11 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Segoe%20UI%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=333333&bgTextureHeader=gloss_wave&bgImgOpacityHeader=25&borderColorHeader=333333&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=000000&bgTextureContent=inset_soft&bgImgOpacityContent=25&borderColorContent=666666&fcContent=ffffff&iconColorContent=cccccc&bgColorDefault=555555&bgTextureDefault=glass&bgImgOpacityDefault=20&borderColorDefault=666666&fcDefault=eeeeee&iconColorDefault=cccccc&bgColorHover=0078a3&bgTextureHover=glass&bgImgOpacityHover=40&borderColorHover=59b4d4&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=f58400&bgTextureActive=inset_soft&bgImgOpacityActive=30&borderColorActive=ffaf0f&fcActive=ffffff&iconColorActive=222222&bgColorHighlight=eeeeee&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=80&borderColorHighlight=cccccc&fcHighlight=2e7db2&iconColorHighlight=4b8e0b&bgColorError=ffc73d&bgTextureError=glass&bgImgOpacityError=40&borderColorError=ffb73d&fcError=111111&iconColorError=a83300&bgColorOverlay=5c5c5c&bgTextureOverlay=flat&bgImgOpacityOverlay=50&opacityOverlay=80&bgColorShadow=cccccc&bgTextureShadow=flat&bgImgOpacityShadow=30&opacityShadow=60&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-clearfix { + min-height: 0; /* support: IE7 */ +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); /* support: IE8 */ +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; +} + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-accordion .ui-accordion-header { + display: block; + cursor: pointer; + position: relative; + margin: 2px 0 0 0; + padding: .5em .5em .5em .7em; + min-height: 0; /* support: IE7 */ + font-size: 100%; +} +.ui-accordion .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-icons .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-header .ui-accordion-header-icon { + position: absolute; + left: .5em; + top: 50%; + margin-top: -8px; +} +.ui-accordion .ui-accordion-content { + padding: 1em 2.2em; + border-top: 0; + overflow: auto; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-button { + display: inline-block; + position: relative; + padding: 0; + line-height: normal; + margin-right: .1em; + cursor: pointer; + vertical-align: middle; + text-align: center; + overflow: visible; /* removes extra width in IE */ +} +.ui-button, +.ui-button:link, +.ui-button:visited, +.ui-button:hover, +.ui-button:active { + text-decoration: none; +} +/* to make room for the icon, a width needs to be set here */ +.ui-button-icon-only { + width: 2.2em; +} +/* button elements seem to need a little more width */ +button.ui-button-icon-only { + width: 2.4em; +} +.ui-button-icons-only { + width: 3.4em; +} +button.ui-button-icons-only { + width: 3.7em; +} + +/* button text element */ +.ui-button .ui-button-text { + display: block; + line-height: normal; +} +.ui-button-text-only .ui-button-text { + padding: .4em 1em; +} +.ui-button-icon-only .ui-button-text, +.ui-button-icons-only .ui-button-text { + padding: .4em; + text-indent: -9999999px; +} +.ui-button-text-icon-primary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 1em .4em 2.1em; +} +.ui-button-text-icon-secondary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 2.1em .4em 1em; +} +.ui-button-text-icons .ui-button-text { + padding-left: 2.1em; + padding-right: 2.1em; +} +/* no icon support for input elements, provide padding by default */ +input.ui-button { + padding: .4em 1em; +} + +/* button icon element(s) */ +.ui-button-icon-only .ui-icon, +.ui-button-text-icon-primary .ui-icon, +.ui-button-text-icon-secondary .ui-icon, +.ui-button-text-icons .ui-icon, +.ui-button-icons-only .ui-icon { + position: absolute; + top: 50%; + margin-top: -8px; +} +.ui-button-icon-only .ui-icon { + left: 50%; + margin-left: -8px; +} +.ui-button-text-icon-primary .ui-button-icon-primary, +.ui-button-text-icons .ui-button-icon-primary, +.ui-button-icons-only .ui-button-icon-primary { + left: .5em; +} +.ui-button-text-icon-secondary .ui-button-icon-secondary, +.ui-button-text-icons .ui-button-icon-secondary, +.ui-button-icons-only .ui-button-icon-secondary { + right: .5em; +} + +/* button sets */ +.ui-buttonset { + margin-right: 7px; +} +.ui-buttonset .ui-button { + margin-left: 0; + margin-right: -.3em; +} + +/* workarounds */ +/* reset extra padding in Firefox, see h5bp.com/l */ +input.ui-button::-moz-focus-inner, +button.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 45%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} +.ui-dialog { + overflow: hidden; + position: absolute; + top: 0; + left: 0; + padding: .2em; + outline: 0; +} +.ui-dialog .ui-dialog-titlebar { + padding: .4em 1em; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 0; + white-space: nowrap; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 20px; + margin: -10px 0 0 0; + padding: 1px; + height: 20px; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background-image: none; + margin-top: .5em; + padding: .3em 1em .5em .4em; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-se { + width: 12px; + height: 12px; + right: -5px; + bottom: -5px; + background-position: 16px 16px; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +.ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-menu { + list-style: none; + padding: 0; + margin: 0; + display: block; + outline: none; +} +.ui-menu .ui-menu { + position: absolute; +} +.ui-menu .ui-menu-item { + position: relative; + margin: 0; + padding: 3px 1em 3px .4em; + cursor: pointer; + min-height: 0; /* support: IE7 */ + /* support: IE10, see #8844 */ + list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} +.ui-menu .ui-menu-divider { + margin: 5px 0; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-state-focus, +.ui-menu .ui-state-active { + margin: -1px; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item { + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: 0; + bottom: 0; + left: .2em; + margin: auto 0; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + left: auto; + right: 0; +} +.ui-progressbar { + height: 2em; + text-align: left; + overflow: hidden; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height: 100%; +} +.ui-progressbar .ui-progressbar-overlay { + background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw=="); + height: 100%; + filter: alpha(opacity=25); /* support: IE8 */ + opacity: 0.25; +} +.ui-progressbar-indeterminate .ui-progressbar-value { + background-image: none; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; + -ms-touch-action: none; + touch-action: none; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-selectable { + -ms-touch-action: none; + touch-action: none; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-selectmenu-menu { + padding: 0; + margin: 0; + position: absolute; + top: 0; + left: 0; + display: none; +} +.ui-selectmenu-menu .ui-menu { + overflow: auto; + /* Support: IE7 */ + overflow-x: hidden; + padding-bottom: 1px; +} +.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { + font-size: 1em; + font-weight: bold; + line-height: 1.5; + padding: 2px 0.4em; + margin: 0.5em 0 0 0; + height: auto; + border: 0; +} +.ui-selectmenu-open { + display: block; +} +.ui-selectmenu-button { + display: inline-block; + overflow: hidden; + position: relative; + text-decoration: none; + cursor: pointer; +} +.ui-selectmenu-button span.ui-icon { + right: 0.5em; + left: auto; + margin-top: -8px; + position: absolute; + top: 50%; +} +.ui-selectmenu-button span.ui-selectmenu-text { + text-align: left; + padding: 0.4em 2.1em 0.4em 1em; + display: block; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; + -ms-touch-action: none; + touch-action: none; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} + +/* support: IE8 - See #6727 */ +.ui-slider.ui-state-disabled .ui-slider-handle, +.ui-slider.ui-state-disabled .ui-slider-range { + filter: inherit; +} + +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} + +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +} +.ui-sortable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-spinner { + position: relative; + display: inline-block; + overflow: hidden; + padding: 0; + vertical-align: middle; +} +.ui-spinner-input { + border: none; + background: none; + color: inherit; + padding: 0; + margin: .2em 0; + vertical-align: middle; + margin-left: .4em; + margin-right: 22px; +} +.ui-spinner-button { + width: 16px; + height: 50%; + font-size: .5em; + padding: 0; + margin: 0; + text-align: center; + position: absolute; + cursor: default; + display: block; + overflow: hidden; + right: 0; +} +/* more specificity required here to override default borders */ +.ui-spinner a.ui-spinner-button { + border-top: none; + border-bottom: none; + border-right: none; +} +/* vertically center icon */ +.ui-spinner .ui-icon { + position: absolute; + margin-top: -8px; + top: 50%; + left: 0; +} +.ui-spinner-up { + top: 0; +} +.ui-spinner-down { + bottom: 0; +} + +/* TR overrides */ +.ui-spinner .ui-icon-triangle-1-s { + /* need to fix icons sprite */ + background-position: -65px -16px; +} +.ui-tabs { + position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ + padding: .2em; +} +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 0; + margin: 1px .2em 0 0; + border-bottom-width: 0; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav .ui-tabs-anchor { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active { + margin-bottom: -1px; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor { + cursor: text; +} +.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { + cursor: pointer; +} +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} +.ui-tooltip { + padding: 8px; + position: absolute; + z-index: 9999; + max-width: 300px; + -webkit-box-shadow: 0 0 5px #aaa; + box-shadow: 0 0 5px #aaa; +} +body .ui-tooltip { + border-width: 2px; +} + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Segoe UI,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Segoe UI,Arial,sans-serif; + font-size: 1em; +} +.ui-widget-content { + border: 1px solid #666666; + background: #000000 url("images/ui-bg_inset-soft_25_000000_1x100.png") 50% bottom repeat-x; + color: #ffffff; +} +.ui-widget-content a { + color: #ffffff; +} +.ui-widget-header { + border: 1px solid #333333; + background: #333333 url("images/ui-bg_gloss-wave_25_333333_500x100.png") 50% 50% repeat-x; + color: #ffffff; + font-weight: bold; +} +.ui-widget-header a { + color: #ffffff; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { + border: 1px solid #666666; + background: #555555 url("images/ui-bg_glass_20_555555_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #eeeeee; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #eeeeee; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + border: 1px solid #59b4d4; + background: #0078a3 url("images/ui-bg_glass_40_0078a3_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #ffffff; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited { + color: #ffffff; + text-decoration: none; +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #ffaf0f; + background: #f58400 url("images/ui-bg_inset-soft_30_f58400_1x100.png") 50% 50% repeat-x; + font-weight: bold; + color: #ffffff; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #ffffff; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #cccccc; + background: #eeeeee url("images/ui-bg_highlight-soft_80_eeeeee_1x100.png") 50% top repeat-x; + color: #2e7db2; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #2e7db2; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #ffb73d; + background: #ffc73d url("images/ui-bg_glass_40_ffc73d_1x400.png") 50% 50% repeat-x; + color: #111111; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #111111; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #111111; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("images/ui-icons_cccccc_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); +} +.ui-state-default .ui-icon { + background-image: url("images/ui-icons_cccccc_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); +} +.ui-state-active .ui-icon { + background-image: url("images/ui-icons_222222_256x240.png"); +} +.ui-state-highlight .ui-icon { + background-image: url("images/ui-icons_4b8e0b_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("images/ui-icons_a83300_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 6px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 6px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 6px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 6px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #5c5c5c url("images/ui-bg_flat_50_5c5c5c_40x100.png") 50% 50% repeat-x; + opacity: .8; + filter: Alpha(Opacity=80); /* support: IE8 */ +} +.ui-widget-shadow { + margin: -7px 0 0 -7px; + padding: 7px; + background: #cccccc url("images/ui-bg_flat_30_cccccc_40x100.png") 50% 50% repeat-x; + opacity: .6; + filter: Alpha(Opacity=60); /* support: IE8 */ + border-radius: 8px; +} diff --git a/static/css/ui-darkness/jquery-ui.min.css b/static/css/ui-darkness/jquery-ui.min.css new file mode 100644 index 00000000000..cf359df6859 --- /dev/null +++ b/static/css/ui-darkness/jquery-ui.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.11.4 - 2015-03-11 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Segoe%20UI%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=333333&bgTextureHeader=gloss_wave&bgImgOpacityHeader=25&borderColorHeader=333333&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=000000&bgTextureContent=inset_soft&bgImgOpacityContent=25&borderColorContent=666666&fcContent=ffffff&iconColorContent=cccccc&bgColorDefault=555555&bgTextureDefault=glass&bgImgOpacityDefault=20&borderColorDefault=666666&fcDefault=eeeeee&iconColorDefault=cccccc&bgColorHover=0078a3&bgTextureHover=glass&bgImgOpacityHover=40&borderColorHover=59b4d4&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=f58400&bgTextureActive=inset_soft&bgImgOpacityActive=30&borderColorActive=ffaf0f&fcActive=ffffff&iconColorActive=222222&bgColorHighlight=eeeeee&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=80&borderColorHighlight=cccccc&fcHighlight=2e7db2&iconColorHighlight=4b8e0b&bgColorError=ffc73d&bgTextureError=glass&bgImgOpacityError=40&borderColorError=ffb73d&fcError=111111&iconColorError=a83300&bgColorOverlay=5c5c5c&bgTextureOverlay=flat&bgImgOpacityOverlay=50&opacityOverlay=80&bgColorShadow=cccccc&bgTextureShadow=flat&bgImgOpacityShadow=30&opacityShadow=60&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:none}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{right:0.5em;left:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:left;padding:0.4em 2.1em 0.4em 1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Segoe UI,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Segoe UI,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #666;background:#000 url("images/ui-bg_inset-soft_25_000000_1x100.png") 50% bottom repeat-x;color:#fff}.ui-widget-content a{color:#fff}.ui-widget-header{border:1px solid #333;background:#333 url("images/ui-bg_gloss-wave_25_333333_500x100.png") 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #666;background:#555 url("images/ui-bg_glass_20_555555_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#eee}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#eee;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #59b4d4;background:#0078a3 url("images/ui-bg_glass_40_0078a3_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#fff}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#fff;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #ffaf0f;background:#f58400 url("images/ui-bg_inset-soft_30_f58400_1x100.png") 50% 50% repeat-x;font-weight:bold;color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #ccc;background:#eee url("images/ui-bg_highlight-soft_80_eeeeee_1x100.png") 50% top repeat-x;color:#2e7db2}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#2e7db2}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #ffb73d;background:#ffc73d url("images/ui-bg_glass_40_ffc73d_1x400.png") 50% 50% repeat-x;color:#111}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#111}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#111}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_cccccc_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_cccccc_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_4b8e0b_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_a83300_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:6px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:6px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:6px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:6px}.ui-widget-overlay{background:#5c5c5c url("images/ui-bg_flat_50_5c5c5c_40x100.png") 50% 50% repeat-x;opacity:.8;filter:Alpha(Opacity=80)}.ui-widget-shadow{margin:-7px 0 0 -7px;padding:7px;background:#ccc url("images/ui-bg_flat_30_cccccc_40x100.png") 50% 50% repeat-x;opacity:.6;filter:Alpha(Opacity=60);border-radius:8px} \ No newline at end of file diff --git a/static/css/ui-darkness/theme.css b/static/css/ui-darkness/theme.css new file mode 100644 index 00000000000..5d1da4c882e --- /dev/null +++ b/static/css/ui-darkness/theme.css @@ -0,0 +1,410 @@ +/*! + * jQuery UI CSS Framework 1.11.4 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/theming/ + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Segoe%20UI%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=333333&bgTextureHeader=gloss_wave&bgImgOpacityHeader=25&borderColorHeader=333333&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=000000&bgTextureContent=inset_soft&bgImgOpacityContent=25&borderColorContent=666666&fcContent=ffffff&iconColorContent=cccccc&bgColorDefault=555555&bgTextureDefault=glass&bgImgOpacityDefault=20&borderColorDefault=666666&fcDefault=eeeeee&iconColorDefault=cccccc&bgColorHover=0078a3&bgTextureHover=glass&bgImgOpacityHover=40&borderColorHover=59b4d4&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=f58400&bgTextureActive=inset_soft&bgImgOpacityActive=30&borderColorActive=ffaf0f&fcActive=ffffff&iconColorActive=222222&bgColorHighlight=eeeeee&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=80&borderColorHighlight=cccccc&fcHighlight=2e7db2&iconColorHighlight=4b8e0b&bgColorError=ffc73d&bgTextureError=glass&bgImgOpacityError=40&borderColorError=ffb73d&fcError=111111&iconColorError=a83300&bgColorOverlay=5c5c5c&bgTextureOverlay=flat&bgImgOpacityOverlay=50&opacityOverlay=80&bgColorShadow=cccccc&bgTextureShadow=flat&bgImgOpacityShadow=30&opacityShadow=60&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Segoe UI,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Segoe UI,Arial,sans-serif; + font-size: 1em; +} +.ui-widget-content { + border: 1px solid #666666; + background: #000000 url("images/ui-bg_inset-soft_25_000000_1x100.png") 50% bottom repeat-x; + color: #ffffff; +} +.ui-widget-content a { + color: #ffffff; +} +.ui-widget-header { + border: 1px solid #333333; + background: #333333 url("images/ui-bg_gloss-wave_25_333333_500x100.png") 50% 50% repeat-x; + color: #ffffff; + font-weight: bold; +} +.ui-widget-header a { + color: #ffffff; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { + border: 1px solid #666666; + background: #555555 url("images/ui-bg_glass_20_555555_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #eeeeee; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #eeeeee; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + border: 1px solid #59b4d4; + background: #0078a3 url("images/ui-bg_glass_40_0078a3_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #ffffff; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited { + color: #ffffff; + text-decoration: none; +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #ffaf0f; + background: #f58400 url("images/ui-bg_inset-soft_30_f58400_1x100.png") 50% 50% repeat-x; + font-weight: bold; + color: #ffffff; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #ffffff; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #cccccc; + background: #eeeeee url("images/ui-bg_highlight-soft_80_eeeeee_1x100.png") 50% top repeat-x; + color: #2e7db2; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #2e7db2; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #ffb73d; + background: #ffc73d url("images/ui-bg_glass_40_ffc73d_1x400.png") 50% 50% repeat-x; + color: #111111; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #111111; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #111111; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("images/ui-icons_cccccc_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); +} +.ui-state-default .ui-icon { + background-image: url("images/ui-icons_cccccc_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); +} +.ui-state-active .ui-icon { + background-image: url("images/ui-icons_222222_256x240.png"); +} +.ui-state-highlight .ui-icon { + background-image: url("images/ui-icons_4b8e0b_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("images/ui-icons_a83300_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 6px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 6px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 6px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 6px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #5c5c5c url("images/ui-bg_flat_50_5c5c5c_40x100.png") 50% 50% repeat-x; + opacity: .8; + filter: Alpha(Opacity=80); /* support: IE8 */ +} +.ui-widget-shadow { + margin: -7px 0 0 -7px; + padding: 7px; + background: #cccccc url("images/ui-bg_flat_30_cccccc_40x100.png") 50% 50% repeat-x; + opacity: .6; + filter: Alpha(Opacity=60); /* support: IE8 */ + border-radius: 8px; +} diff --git a/static/css/ui-lightness/images/animated-overlay.gif b/static/css/ui-lightness/images/animated-overlay.gif new file mode 100755 index 00000000000..d441f75ebfb Binary files /dev/null and b/static/css/ui-lightness/images/animated-overlay.gif differ diff --git a/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png b/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png new file mode 100644 index 00000000000..e08c25afcda Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png differ diff --git a/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png b/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png new file mode 100644 index 00000000000..68baaa6f430 Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png differ diff --git a/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png b/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png new file mode 100644 index 00000000000..5244eed424d Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png differ diff --git a/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png b/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png new file mode 100644 index 00000000000..001aaaa7789 Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png differ diff --git a/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png b/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png new file mode 100644 index 00000000000..12ffd858fd6 Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png differ diff --git a/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png b/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100644 index 00000000000..02d997a3449 Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png b/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png new file mode 100644 index 00000000000..f07b2ab4bdc Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png differ diff --git a/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png b/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png new file mode 100644 index 00000000000..8c16f5e1706 Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png differ diff --git a/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png b/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png new file mode 100644 index 00000000000..1f75d12650f Binary files /dev/null and b/static/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png differ diff --git a/static/css/ui-lightness/images/ui-icons_222222_256x240.png b/static/css/ui-lightness/images/ui-icons_222222_256x240.png new file mode 100644 index 00000000000..82fad68f5f7 Binary files /dev/null and b/static/css/ui-lightness/images/ui-icons_222222_256x240.png differ diff --git a/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png b/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png new file mode 100644 index 00000000000..0c554acbc62 Binary files /dev/null and b/static/css/ui-lightness/images/ui-icons_228ef1_256x240.png differ diff --git a/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png b/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png new file mode 100644 index 00000000000..d76d10a8392 Binary files /dev/null and b/static/css/ui-lightness/images/ui-icons_ef8c08_256x240.png differ diff --git a/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png b/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png new file mode 100644 index 00000000000..5e9d27a7247 Binary files /dev/null and b/static/css/ui-lightness/images/ui-icons_ffd27a_256x240.png differ diff --git a/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png b/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png new file mode 100644 index 00000000000..cd6c26561dc Binary files /dev/null and b/static/css/ui-lightness/images/ui-icons_ffffff_256x240.png differ diff --git a/static/css/ui-lightness/jquery-ui.css b/static/css/ui-lightness/jquery-ui.css new file mode 100644 index 00000000000..4b955f04bdc --- /dev/null +++ b/static/css/ui-lightness/jquery-ui.css @@ -0,0 +1,1225 @@ +/*! jQuery UI - v1.11.4 - 2015-03-11 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-clearfix { + min-height: 0; /* support: IE7 */ +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); /* support: IE8 */ +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; +} + + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-accordion .ui-accordion-header { + display: block; + cursor: pointer; + position: relative; + margin: 2px 0 0 0; + padding: .5em .5em .5em .7em; + min-height: 0; /* support: IE7 */ + font-size: 100%; +} +.ui-accordion .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-icons .ui-accordion-icons { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-header .ui-accordion-header-icon { + position: absolute; + left: .5em; + top: 50%; + margin-top: -8px; +} +.ui-accordion .ui-accordion-content { + padding: 1em 2.2em; + border-top: 0; + overflow: auto; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-button { + display: inline-block; + position: relative; + padding: 0; + line-height: normal; + margin-right: .1em; + cursor: pointer; + vertical-align: middle; + text-align: center; + overflow: visible; /* removes extra width in IE */ +} +.ui-button, +.ui-button:link, +.ui-button:visited, +.ui-button:hover, +.ui-button:active { + text-decoration: none; +} +/* to make room for the icon, a width needs to be set here */ +.ui-button-icon-only { + width: 2.2em; +} +/* button elements seem to need a little more width */ +button.ui-button-icon-only { + width: 2.4em; +} +.ui-button-icons-only { + width: 3.4em; +} +button.ui-button-icons-only { + width: 3.7em; +} + +/* button text element */ +.ui-button .ui-button-text { + display: block; + line-height: normal; +} +.ui-button-text-only .ui-button-text { + padding: .4em 1em; +} +.ui-button-icon-only .ui-button-text, +.ui-button-icons-only .ui-button-text { + padding: .4em; + text-indent: -9999999px; +} +.ui-button-text-icon-primary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 1em .4em 2.1em; +} +.ui-button-text-icon-secondary .ui-button-text, +.ui-button-text-icons .ui-button-text { + padding: .4em 2.1em .4em 1em; +} +.ui-button-text-icons .ui-button-text { + padding-left: 2.1em; + padding-right: 2.1em; +} +/* no icon support for input elements, provide padding by default */ +input.ui-button { + padding: .4em 1em; +} + +/* button icon element(s) */ +.ui-button-icon-only .ui-icon, +.ui-button-text-icon-primary .ui-icon, +.ui-button-text-icon-secondary .ui-icon, +.ui-button-text-icons .ui-icon, +.ui-button-icons-only .ui-icon { + position: absolute; + top: 50%; + margin-top: -8px; +} +.ui-button-icon-only .ui-icon { + left: 50%; + margin-left: -8px; +} +.ui-button-text-icon-primary .ui-button-icon-primary, +.ui-button-text-icons .ui-button-icon-primary, +.ui-button-icons-only .ui-button-icon-primary { + left: .5em; +} +.ui-button-text-icon-secondary .ui-button-icon-secondary, +.ui-button-text-icons .ui-button-icon-secondary, +.ui-button-icons-only .ui-button-icon-secondary { + right: .5em; +} + +/* button sets */ +.ui-buttonset { + margin-right: 7px; +} +.ui-buttonset .ui-button { + margin-left: 0; + margin-right: -.3em; +} + +/* workarounds */ +/* reset extra padding in Firefox, see h5bp.com/l */ +input.ui-button::-moz-focus-inner, +button.ui-button::-moz-focus-inner { + border: 0; + padding: 0; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 45%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} +.ui-dialog { + overflow: hidden; + position: absolute; + top: 0; + left: 0; + padding: .2em; + outline: 0; +} +.ui-dialog .ui-dialog-titlebar { + padding: .4em 1em; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 0; + white-space: nowrap; + width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 20px; + margin: -10px 0 0 0; + padding: 1px; + height: 20px; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background-image: none; + margin-top: .5em; + padding: .3em 1em .5em .4em; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-se { + width: 12px; + height: 12px; + right: -5px; + bottom: -5px; + background-position: 16px 16px; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +.ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-menu { + list-style: none; + padding: 0; + margin: 0; + display: block; + outline: none; +} +.ui-menu .ui-menu { + position: absolute; +} +.ui-menu .ui-menu-item { + position: relative; + margin: 0; + padding: 3px 1em 3px .4em; + cursor: pointer; + min-height: 0; /* support: IE7 */ + /* support: IE10, see #8844 */ + list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} +.ui-menu .ui-menu-divider { + margin: 5px 0; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-state-focus, +.ui-menu .ui-state-active { + margin: -1px; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item { + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: 0; + bottom: 0; + left: .2em; + margin: auto 0; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + left: auto; + right: 0; +} +.ui-progressbar { + height: 2em; + text-align: left; + overflow: hidden; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height: 100%; +} +.ui-progressbar .ui-progressbar-overlay { + background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw=="); + height: 100%; + filter: alpha(opacity=25); /* support: IE8 */ + opacity: 0.25; +} +.ui-progressbar-indeterminate .ui-progressbar-value { + background-image: none; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; + -ms-touch-action: none; + touch-action: none; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-selectable { + -ms-touch-action: none; + touch-action: none; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-selectmenu-menu { + padding: 0; + margin: 0; + position: absolute; + top: 0; + left: 0; + display: none; +} +.ui-selectmenu-menu .ui-menu { + overflow: auto; + /* Support: IE7 */ + overflow-x: hidden; + padding-bottom: 1px; +} +.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { + font-size: 1em; + font-weight: bold; + line-height: 1.5; + padding: 2px 0.4em; + margin: 0.5em 0 0 0; + height: auto; + border: 0; +} +.ui-selectmenu-open { + display: block; +} +.ui-selectmenu-button { + display: inline-block; + overflow: hidden; + position: relative; + text-decoration: none; + cursor: pointer; +} +.ui-selectmenu-button span.ui-icon { + right: 0.5em; + left: auto; + margin-top: -8px; + position: absolute; + top: 50%; +} +.ui-selectmenu-button span.ui-selectmenu-text { + text-align: left; + padding: 0.4em 2.1em 0.4em 1em; + display: block; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; + -ms-touch-action: none; + touch-action: none; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} + +/* support: IE8 - See #6727 */ +.ui-slider.ui-state-disabled .ui-slider-handle, +.ui-slider.ui-state-disabled .ui-slider-range { + filter: inherit; +} + +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} + +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +} +.ui-sortable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-spinner { + position: relative; + display: inline-block; + overflow: hidden; + padding: 0; + vertical-align: middle; +} +.ui-spinner-input { + border: none; + background: none; + color: inherit; + padding: 0; + margin: .2em 0; + vertical-align: middle; + margin-left: .4em; + margin-right: 22px; +} +.ui-spinner-button { + width: 16px; + height: 50%; + font-size: .5em; + padding: 0; + margin: 0; + text-align: center; + position: absolute; + cursor: default; + display: block; + overflow: hidden; + right: 0; +} +/* more specificity required here to override default borders */ +.ui-spinner a.ui-spinner-button { + border-top: none; + border-bottom: none; + border-right: none; +} +/* vertically center icon */ +.ui-spinner .ui-icon { + position: absolute; + margin-top: -8px; + top: 50%; + left: 0; +} +.ui-spinner-up { + top: 0; +} +.ui-spinner-down { + bottom: 0; +} + +/* TR overrides */ +.ui-spinner .ui-icon-triangle-1-s { + /* need to fix icons sprite */ + background-position: -65px -16px; +} +.ui-tabs { + position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ + padding: .2em; +} +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 0; + margin: 1px .2em 0 0; + border-bottom-width: 0; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav .ui-tabs-anchor { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active { + margin-bottom: -1px; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor, +.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor { + cursor: text; +} +.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { + cursor: pointer; +} +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + padding: 1em 1.4em; + background: none; +} +.ui-tooltip { + padding: 8px; + position: absolute; + z-index: 9999; + max-width: 300px; + -webkit-box-shadow: 0 0 5px #aaa; + box-shadow: 0 0 5px #aaa; +} +body .ui-tooltip { + border-width: 2px; +} + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif; + font-size: 1em; +} +.ui-widget-content { + border: 1px solid #dddddd; + background: #eeeeee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x; + color: #333333; +} +.ui-widget-content a { + color: #333333; +} +.ui-widget-header { + border: 1px solid #e78f08; + background: #f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x; + color: #ffffff; + font-weight: bold; +} +.ui-widget-header a { + color: #ffffff; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { + border: 1px solid #cccccc; + background: #f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #1c94c4; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #1c94c4; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + border: 1px solid #fbcb09; + background: #fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #c77405; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited { + color: #c77405; + text-decoration: none; +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #fbd850; + background: #ffffff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #eb8f00; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #eb8f00; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #fed22f; + background: #ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x; + color: #363636; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #cd0a0a; + background: #b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat; + color: #ffffff; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #ffffff; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #ffffff; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("images/ui-icons_222222_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); +} +.ui-state-default .ui-icon { + background-image: url("images/ui-icons_ef8c08_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: url("images/ui-icons_ef8c08_256x240.png"); +} +.ui-state-active .ui-icon { + background-image: url("images/ui-icons_ef8c08_256x240.png"); +} +.ui-state-highlight .ui-icon { + background-image: url("images/ui-icons_228ef1_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("images/ui-icons_ffd27a_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 4px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #666666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat; + opacity: .5; + filter: Alpha(Opacity=50); /* support: IE8 */ +} +.ui-widget-shadow { + margin: -5px 0 0 -5px; + padding: 5px; + background: #000000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x; + opacity: .2; + filter: Alpha(Opacity=20); /* support: IE8 */ + border-radius: 5px; +} diff --git a/static/css/ui-lightness/jquery-ui.min.css b/static/css/ui-lightness/jquery-ui.min.css new file mode 100644 index 00000000000..efccc47678c --- /dev/null +++ b/static/css/ui-lightness/jquery-ui.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.11.4 - 2015-03-11 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:none}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{right:0.5em;left:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:left;padding:0.4em 2.1em 0.4em 1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_ef8c08_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_228ef1_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_ffd27a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);border-radius:5px} \ No newline at end of file diff --git a/static/css/ui-lightness/theme.css b/static/css/ui-lightness/theme.css new file mode 100644 index 00000000000..5db92db3eba --- /dev/null +++ b/static/css/ui-lightness/theme.css @@ -0,0 +1,410 @@ +/*! + * jQuery UI CSS Framework 1.11.4 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/theming/ + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif; + font-size: 1em; +} +.ui-widget-content { + border: 1px solid #dddddd; + background: #eeeeee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x; + color: #333333; +} +.ui-widget-content a { + color: #333333; +} +.ui-widget-header { + border: 1px solid #e78f08; + background: #f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x; + color: #ffffff; + font-weight: bold; +} +.ui-widget-header a { + color: #ffffff; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default { + border: 1px solid #cccccc; + background: #f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #1c94c4; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited { + color: #1c94c4; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus { + border: 1px solid #fbcb09; + background: #fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #c77405; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited { + color: #c77405; + text-decoration: none; +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active { + border: 1px solid #fbd850; + background: #ffffff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x; + font-weight: bold; + color: #eb8f00; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #eb8f00; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #fed22f; + background: #ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x; + color: #363636; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #cd0a0a; + background: #b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat; + color: #ffffff; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #ffffff; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #ffffff; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("images/ui-icons_222222_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); +} +.ui-state-default .ui-icon { + background-image: url("images/ui-icons_ef8c08_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon { + background-image: url("images/ui-icons_ef8c08_256x240.png"); +} +.ui-state-active .ui-icon { + background-image: url("images/ui-icons_ef8c08_256x240.png"); +} +.ui-state-highlight .ui-icon { + background-image: url("images/ui-icons_228ef1_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("images/ui-icons_ffd27a_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-carat-1-n { background-position: 0 0; } +.ui-icon-carat-1-ne { background-position: -16px 0; } +.ui-icon-carat-1-e { background-position: -32px 0; } +.ui-icon-carat-1-se { background-position: -48px 0; } +.ui-icon-carat-1-s { background-position: -64px 0; } +.ui-icon-carat-1-sw { background-position: -80px 0; } +.ui-icon-carat-1-w { background-position: -96px 0; } +.ui-icon-carat-1-nw { background-position: -112px 0; } +.ui-icon-carat-2-n-s { background-position: -128px 0; } +.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 4px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #666666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat; + opacity: .5; + filter: Alpha(Opacity=50); /* support: IE8 */ +} +.ui-widget-shadow { + margin: -5px 0 0 -5px; + padding: 5px; + background: #000000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x; + opacity: .2; + filter: Alpha(Opacity=20); /* support: IE8 */ + border-radius: 5px; +} diff --git a/static/font/nsicons.eot b/static/font/nsicons.eot new file mode 100755 index 00000000000..7efbe9a3f14 Binary files /dev/null and b/static/font/nsicons.eot differ diff --git a/static/font/nsicons.svg b/static/font/nsicons.svg new file mode 100755 index 00000000000..b564d0f8dc3 --- /dev/null +++ b/static/font/nsicons.svg @@ -0,0 +1,32 @@ + + + +Copyright (C) 2017 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/font/nsicons.ttf b/static/font/nsicons.ttf new file mode 100755 index 00000000000..d93db9d8feb Binary files /dev/null and b/static/font/nsicons.ttf differ diff --git a/static/font/nsicons.woff b/static/font/nsicons.woff new file mode 100755 index 00000000000..c2b07fb4e71 Binary files /dev/null and b/static/font/nsicons.woff differ diff --git a/static/font/nsicons.woff2 b/static/font/nsicons.woff2 new file mode 100755 index 00000000000..a090ad239d8 Binary files /dev/null and b/static/font/nsicons.woff2 differ diff --git a/static/glyphs/LICENSE.txt b/static/glyphs/LICENSE.txt old mode 100644 new mode 100755 index 1970f449a11..b8a9414fbb6 --- a/static/glyphs/LICENSE.txt +++ b/static/glyphs/LICENSE.txt @@ -12,7 +12,7 @@ Font license info ## Font Awesome - Copyright (C) 2012 by Dave Gandy + Copyright (C) 2016 by Dave Gandy Author: Dave Gandy License: SIL () diff --git a/static/glyphs/README.txt b/static/glyphs/README.txt old mode 100644 new mode 100755 index 43e23f2833b..beaab3366f0 --- a/static/glyphs/README.txt +++ b/static/glyphs/README.txt @@ -2,16 +2,16 @@ This webfont is generated by http://fontello.com open source project. ================================================================================ -Please, note, that you should obey original font licences, used to make this +Please, note, that you should obey original font licenses, used to make this webfont pack. Details available in LICENSE.txt file. - Usually, it's enough to publish content of LICENSE.txt file somewhere on your site in "About" section. - If your project is open-source, usually, it will be ok to make LICENSE.txt - file publically available in your repository. + file publicly available in your repository. -- Fonts, used in Fontello, don't require to make clickable links on your site. +- Fonts, used in Fontello, don't require a clickable link on your site. But any kind of additional authors crediting is welcome. ================================================================================ @@ -29,8 +29,8 @@ Comments on archive content - LICENSE.txt - license info about source fonts, used to build your one. -- config.json - keeps your settings. You can import it back to fontello anytime, - to continue your work +- config.json - keeps your settings. You can import it back into fontello + anytime, to continue your work Why so many CSS files ? @@ -38,17 +38,17 @@ Why so many CSS files ? Because we like to fit all your needs :) -- basic file, .css - is usually enougth, in contains @font-face - and character codes definition +- basic file, .css - is usually enough, it contains @font-face + and character code definitions - *-ie7.css - if you need IE7 support, but still don't wish to put char codes directly into html - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face - rules, but still wish to benefit of css generation. That can be very - convenient for automated assets build systems. When you need to update font - - no needs to manually edit files, just override old version with archive - content. See fontello source codes for example. + rules, but still wish to benefit from css generation. That can be very + convenient for automated asset build systems. When you need to update font - + no need to manually edit files, just override old version with archive + content. See fontello source code for examples. - *-embedded.css - basic css file, but with embedded WOFF font, to avoid CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. @@ -63,11 +63,11 @@ Because we like to fit all your needs :) Attention for server setup -------------------------- -You MUST setup server to reply with proper `mime-types` for font files. In other -case, some browsers will fail to show fonts. +You MUST setup server to reply with proper `mime-types` for font files - +otherwise some browsers will fail to show fonts. Usually, `apache` already has necessary settings, but `nginx` and other -webservers should be tuned. Here is list of mime types for our file extentions: +webservers should be tuned. Here is list of mime types for our file extensions: - `application/vnd.ms-fontobject` - eot - `application/x-font-woff` - woff diff --git a/static/glyphs/config.json b/static/glyphs/config.json old mode 100644 new mode 100755 index d8524e5c26b..a76965262fc --- a/static/glyphs/config.json +++ b/static/glyphs/config.json @@ -6,6 +6,12 @@ "units_per_em": 1000, "ascent": 850, "glyphs": [ + { + "uid": "41087bc74d4b20b55059c60a33bf4008", + "css": "edit", + "code": 59409, + "src": "fontawesome" + }, { "uid": "63b3012c8cbe3654ba5bea598235aa3a", "css": "angle-double-up", @@ -107,6 +113,24 @@ "css": "cog", "code": 59398, "src": "websymbols" + }, + { + "uid": "c1f1975c885aa9f3dad7810c53b82074", + "css": "lock", + "code": 59410, + "src": "fontawesome" + }, + { + "uid": "657ab647f6248a6b57a5b893beaf35a9", + "css": "lock-open", + "code": 59411, + "src": "fontawesome" + }, + { + "uid": "f48ae54adfb27d8ada53d0fd9e34ee10", + "css": "trash-empty", + "code": 59412, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/static/glyphs/css/animation.css b/static/glyphs/css/animation.css old mode 100644 new mode 100755 diff --git a/static/glyphs/css/fontello-codes.css b/static/glyphs/css/fontello-codes.css old mode 100644 new mode 100755 index 014c5ddd4fd..994b55ca64d --- a/static/glyphs/css/fontello-codes.css +++ b/static/glyphs/css/fontello-codes.css @@ -15,4 +15,8 @@ .icon-calc:before { content: '\e80d'; } /* '' */ .icon-tint:before { content: '\e80e'; } /* '' */ .icon-chart-line:before { content: '\e80f'; } /* '' */ -.icon-sort-numeric:before { content: '\e810'; } /* '' */ \ No newline at end of file +.icon-sort-numeric:before { content: '\e810'; } /* '' */ +.icon-edit:before { content: '\e811'; } /* '' */ +.icon-lock:before { content: '\e812'; } /* '' */ +.icon-lock-open:before { content: '\e813'; } /* '' */ +.icon-trash-empty:before { content: '\e814'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/css/fontello-embedded.css b/static/glyphs/css/fontello-embedded.css old mode 100644 new mode 100755 index a41d4de3298..92ad1b9d889 --- a/static/glyphs/css/fontello-embedded.css +++ b/static/glyphs/css/fontello-embedded.css @@ -1,15 +1,15 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?87658047'); - src: url('../font/fontello.eot?87658047#iefix') format('embedded-opentype'), - url('../font/fontello.svg?87658047#fontello') format('svg'); + src: url('../font/fontello.eot?73205958'); + src: url('../font/fontello.eot?73205958#iefix') format('embedded-opentype'), + url('../font/fontello.svg?73205958#fontello') format('svg'); font-weight: normal; font-style: normal; } @font-face { font-family: 'fontello'; - src: url('data:application/octet-stream;base64,d09GRgABAAAAABKsAA4AAAAAIAQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABRAAAAEQAAABWPilJKmNtYXAAAAGIAAAAOgAAAUrQIRm3Y3Z0IAAAAcQAAAAKAAAACgAAAABmcGdtAAAB0AAABZQAAAtwiJCQWWdhc3AAAAdkAAAACAAAAAgAAAAQZ2x5ZgAAB2wAAAfoAAAMxIkINSdoZWFkAAAPVAAAADYAAAA2BjgrqGhoZWEAAA+MAAAAIAAAACQIJgPvaG10eAAAD6wAAAAuAAAASD/HAABsb2NhAAAP3AAAACYAAAAmHSwZmG1heHAAABAEAAAAIAAAACAAwwwIbmFtZQAAECQAAAF3AAACzcydGx1wb3N0AAARnAAAAKUAAAD0bFz0+nByZXAAABJEAAAAZQAAAHvdawOFeJxjYGTuZpzAwMrAwVTFtIeBgaEHQjM+YDBkZGJgYGJgZWbACgLSXFMYHF4wvBBgDvqfxRDFHMQwHSjMCJIDAO/YC9F4nGNgYGBmgGAZBkYGEHAB8hjBfBYGDSDNBqQZGZgYGF4I/P8PUvCCAURLMELVAwEjG8OIBwB1lwa+AAAAAAAAAAAAAAAAAAB4nK1WaXMTRxCd1WHLNj6CDxI2gVnGcox2VpjLCBDG7EoW4BzylexCjl1Ldu6LT/wG/ZpekVSRb/y0vB4d2GAnVVQoSv2m9+1M9+ueXpPQksReWI+k3HwpprY2aWTnSUg3bFqO4kPZ2QspU0z+LoiCaLXUvu04JCISgap1hSWC2PfI0iTjQ48yWrYlvWpSbulJd9kaD+qt+vbT0FGO3QklNZuhQ+uRLanCqBJFMu2RkjYtw9VfSVrh5yvMfNUMJYLoJJLGm2EMj+Rn44xWGa3GdhxFkU2WG0WKRDM8iCKPslpin1wxQUD5oBlSXvk0onyEH5EVe5TTCnHJdprf9yU/6R3OvyTieouyJQf+QHZkB3unK/ki0toK46adbEehivB0fSfEI5uT6p/sUV7TaOB2RaYnzQiWyleQWPkJZfYPyWrhfMqXPBrVkoOcCFovc2Jf8g60HkdMiWsmyILujk6IoO6XnKHYY/q4+OO9XSwXIQTIOJb1jkq4EEYpYbOaJG0EOYiSskWV1HpHTJzyOi3iLWG/Tu3oS2e0Sag7MZ6th46tnKjkeDSp00ymTu2k5tGUBlFKOhM85tcBlB/RJK+2sZrEyqNpbDNjJJFQoIVzaSqIZSeWNAXRPJrRm7thmmvXokWaPFDPPXpPb26Fmzs9p+3AP2v8Z3UqpoO9MJ2eDshKfJp2uUnRun56hn8m8UPWAiqRLTbDlMVDtn4H5eVjS47CawNs957zK+h99kTIpIH4G/AeL9UpBUyFmFVQC9201rUsy9RqVotUZOq7IU0rX9ZpAk05Dn1jX8Y4/q+ZGUtMCd/vxOnZEZeeufYlyDSH3GZdj+Z1arFdgM5sz+k0y/Z9nebYfqDTPNvzOh1ha+t0lO2HOi2w/UinY2wvaEGT7jsEchGBXMAGEoGwdRAI20sIhK1CIGwXEQjbIgJhu4RA2H6MQNguIxC2l7Wsmn4qaRw7E8sARYgDoznuyGVuKldTyaUSrotGpzbkKXKrpKJ4Vv0rA/3ikTesgbVAukTW/IpJrnxUleOPrmh508S5Ao5Vf3tzXJ8TD2W/WPhT8L/amqqkV6x5ZHIVeSPQk+NE1yYVj67p8rmqR9f/i4oOa4F+A6UQC0VZlg2+mZDwUafTUA1c5RAzGzMP1/W6Zc3P4fybGCEL6H78NxQaC9yDTllJWe1gr9XXj2W5twflsCdYkmK+zOtb4YuMzEr7RWYpez7yecAVMCqVYasNXK3gzXsS85DpTfJMELcVZYOkjceZILGBYx4wb76TICRMXbWB2imcsIG8YMwp2O+EQ1RvlOVwe6F9Ho2Uf2tX7MgZFU0Q+G32Rtjrs1DyW6yBhCe/1NdAVSFNxbipgEsj5YZq8GFcrdtGMk6gr6jYDcuyig8fR9x3So5lIPlIEatHRz+tvUKd1Ln9yihu3zv9CIJBaWL+9r6Z4qCUd7WSZVZtA1O3GpVT15rDxasO3c2j7nvH2Sdy1jTddE/c9L6mVbeDg7lZEO3bHJSlTC6o68MOG6jLzaXQ6mVckt52DzAsMKDfoRUb/1f3cfg8V6oKo+NIvZ2oH6PPYgzyDzh/R/UF6OcxTLmGlOd7lxOfbtzD2TJdxV2sn+LfwKy15mbpGnBD0w2Yh6xaHbrKDXynBjo90tyO9BDwse4K8QBgE8Bi8InuWsbzKYDxfMYcH+Bz5jBoMofBFnMYbDNnDWCHOQx2mcNgjzkMvmDOOsCXzGEQModBxBwGT5gTADxlDoOvmMPga+Yw+IY59wG+ZQ6DmDkMEuYw2Nd0ayhzixd0F6htUBXowPQTFvewONRUGbK/44Vhf28Qs38wiKk/aro9pP7EC0P92SCm/mIQU3/VdGdI/Y0Xhvq7QUz9wyCmPtMvxnKZwV9GvkuFA8ouNp/z98T7B8IaQLYAAQAB//8AD3icrVZtbFPXGT7vuV+OfXNtx/eDOPaNc0PuTW3HSe3rezMwlwQYgSzLRAlR6lkhpUn4KGz9AUkKFepQJ9RJI2yaNBqthSLKD4QyddO0rT82VDQxTZMQUte1A+3nxKSJH92GJi04e69tGCC0iq5xdO57nvP1nPe8X4RdXV19g7nEFEiUdJDNZIQ0DwS/snXwWV1tZtmeTNT2qJbXqcQYOerKOvUY186BIYGrOXlF5sOA/xIkoVC0HdeDjeC3rm12Goys5p0c5YePX35h9/KrI4DCFfzO9U+eOP3alAOT5391ofLH9p4XWOA52q+KR3gGZCEm8jEZxgZ2RtOb09Tc2g2T95f/evn48PDx5d1Dr1fWUWfqte27z09Ont8xk9TYAHAsJMX4mq44w1MIsCGBj3Z3Prul+t2UYQwYsJbgH601E/RFopAMCfzMikdZ2pPpkvEeRi8IfmPaLjh5DdTiE1E6wSc4ib91k4twbTwPLHaA5Z8IzgkIczdvclyC9wGOq67w12vgrVv3QZ5H8CFuhxvcjLjoc0ONIAsLCaBu8+3g+g1yexKK3KorXITFDfk23PzmLewk+CeC8LLgH+wTkP5L6LoPctyjICEMIQ07EckaspYEf55sjTaHOAZ1l1fRDjo7DNN2NMirMm/VZLcmI55Dk2A2pb1S9p6ZLXlp+qdsqS6Xsr7spe+ZmRKUsuVST9rzxj2Ag96YB6XxEsAhbxdCJSA1Dtj8mf4QORgkjxxylq6KPHJwDXMD2E5eTYLM47kaMrLqRKyo7RQ68ioTtU3s81pUVrHv3FBSqbSu08VsCbxMZeDe1cEyVAbohsHyXl29d1VJga7SDUrqA129oei6ggvgS6XMvqwHgxUob4L+wUqlehL6VR1SSvW3OIcQ9iGOraSXbPT9ab2d6WxFq0Z/6jJMt+gTrTF8etZwyOeVTYD+3ufh/zRX8e1xdT9zm9lBLNKH9phL60HfHhWBFzTBQr/vtExLsFyzF4qu41oYEjZCQVM1VxNUTbBNDBMYApjbvwyGFmbPyu36YpuivjPzSihw7ZogLsycC6cSiwk1/vbsfLNwfnxhjE4cLsO7p+Jt2lvTc8HQB78RQvPT55TWxKmEsubs9HxAvHpVEI9M/6hNebE0NrYwNoY8uUd0bpISRrHwgLhtsL/PTPl65/6X3h+M/B+PAJ01AIXxz/kaT/Mw/GP3LZDt5HkSHZB2jX55Q6Gnfmf+M+5sPbiz9UUo4IpPEIWffIGKeBqd+Lb6PrONoSRAZLTVaIglvq3m1QifMiMOuGioaLVomi5cWL57t3rh7l1gl8aXzkwsLU2cWRpnaB2r3K1WlsbPnMERv63HndVD6AdfQ0uTiIpxR41JQZbBuFOMFiAKXY5tuaomC5oTQSEi/A32VN+Gc9u3D+2nL/UeOzaytHQKzkHHig4dncO/Hxk+8IPvzfTOw9Sx4aXqyBKegfFt9QCe8RwJEf6nTQz4e3c8+MFeiMPe6pvVv9y4L1TfhL11bgdoH65rI91kD3L7at5MrQkyft6QQNbBf9Xa23uAibtuAwon8JpayLuOhXk6hjkek0jRxXyiye2MaqqdskTDlDf5Ip8EjxZtTPU6aEW7l6Kz54D2Te3ZM3X548v+Z/nD5arewgYqr74+IelTBxZOBxJia7JCp/8xzZZ7YtGoqIs0yAfEcJga5cDhLfamo3wl09LMiQJLv5HdN1mqzObmjNFRYy43WylN7svOpUZHU7wU4O18UHaqwy36WmlNoqLnHSefKmdiikL7gi0tUjAciVADyqlNrNln6LrRZ7Kb9MozMSkgiyohzTW9voP6CWCFEyedJEuKxCNDZCepkIPkCPk2aR9IfOuVw9/cP/X18edGtg5u6Ld7M2ZHslUOh3ja3JPRMJDxOtSqGj+ieWDVEOpwDwMS6OA8OtIAagIUUf285uvRYe6D6HgmZkhUsqxq6FhCY3ksWs+oxYYDfpRel6XdnvlJutRNs/25Txp9+HED+PizBuASjgB2/tAAPpQj9xxJVSX6u7AKjWXD2cTKnWQ6nWRakmkBBR9ub3z/qadra99/rP/pffyxfvXlj5JpmmurtXCtgV5f9A9dDMugvtuA4MTKxdqhZWzrvvwvup7pRV/z844Zl5v8Gs22DN/2LNMQ0DqBV/z6sgut3LRsFzMPPpCHStV0RlMV0EMQkPgIS2VgI2KgKeTfP3HxzkXoYdv7W70wl9DjfKRkl7ie63JMVJpSYlNTsy6HxJiMPGbmL9JLR3Zs43Jj5tFCVKaRWOHoyCQztLNRR66+x84zIYwJ7cixLcoAcuSUjmghChhBe6FeOlq16gxJO+sxGLHz/35jiLktB7dUz2KBdeL2S7DV/wKzuSnQ/HchKGxjyit36C8+FYNN/hz+xF8P4pT6DDVImNUVLDKG6AG05l5SRn/fVexqEUm9BnKjAo/VFjqziqGafwYME2XfphQ/CBRRTViThaGQVyUALNtQd64fEARflbYp1Oq2vIcaRR2jrl2n4A++hSE4KadbJHlvog9ScKW60qG33pC716X1dMrOf3+msHBSSdEEnHSnvzMLqW4DTMc02zA7KBGVxqXWYDC9NhJKj68fg+dTCoyEg1J3OlCQjHh1SkkN4Gbru612VdfVbYX8zOnCAm4MXYXtW/fQ2S3+Nm2m6XT7SYeqhkzVFkntjo2Or8uR/wDYZ3UuAAEAAAABAAC/H7FUXw889QALA+gAAAAA0VMJHAAAAADRUt7s//z/aQR3A1IAAAAIAAIAAAAAAAB4nGNgZGBgDvqfxRDFUsbA8P8PSzkDUAQFCAEAfcMFHXicY37BwMC8koGBqQmCmVcB8T0ofoHE9oDygZjJAEpbMzCwlAGxOwMDAODAC/wAAAAAAAAAeADGARQBVgGoAg4CYgLaA2QDkgPEA+AEcAVKBZwF2AZiAAAAAQAAABIAhgANAAAAAAACAAAAEABzAAAAMAtwAAAAAHicdZHNSsNAFEa/aWvVFlQU3HpXUhHTH+hGEAqVutFNkW4ljWmSkmbKZFroa/gOPowv4bP4NZ2KtJiQzLln7ty5mQA4xzcUNleXz4YVjhhtuIRDPDgu0z86rpCfHR+gjlfHVfo3xzXcInJcxwU+WEFVjhlN8elY4UydOi7hRF05LtPfOa6QHxwf4FK9OK7SB45rGKnccR3X6quv5yuTRLGVRv9GOq12V8Yr0VRJ5qfiL2ysTS49mejMhmmqvUDPtjwMo0Xqm224HUehyROdSdtrbdVTmIXGt+H7unq+jDrWTmRi9EwGLkPmRk/DwHqxtfP7ZvPvfuhDY44VDBIeVQwLQYP2hmMHLbT5IwRjZggzN1kJMvhIaXwsuCIuZnLGPT4TRhltyIyU7CHge7bnh6SI61NWMXuzu/GItN4jKbywL4/d7WY9kbIi0y/s+2/vOZbcrUNruWrdpSm6Egx2agjPYz03pQnoveJULO09mrz/+b4f4GSETQB4nG2NWwrCMBREMxpfabXqPgJVKK4nvQ1t4TYpaaK4ewuiIDg/cw4MjFiId5T4n7MQWGAJiRXW2GCLHRQy5NjjgAJHnHDOO8ujpj4Q26YwrmWrG5/qudJ4+vHGP5wcrEuqNjHa8NTX6otVuSTffvVWZR+8lOWBjCPLn5v13XMarBw5TbvOp9CymSZJhknG3kVFnQlRc+9sPvmZ3LwOPQnxAnKBPYsAAAB4nGPw3sFwIihiIyNjX+QGxp0cDBwMyQUbGVidNjIwaEFoDhR6JwMDAycyi5nBZaMKY0dgxAaHjoiNzCkuG9VAvF0cDQyMLA4dySERICWRQLCRgUdrB+P/1g0svRuZGFwAB9MiuAAAAA==') format('woff'), - url('data:application/octet-stream;base64,AAEAAAAOAIAAAwBgT1MvMj4pSSoAAADsAAAAVmNtYXDQIRm3AAABRAAAAUpjdnQgAAAAAAAAFAwAAAAKZnBnbYiQkFkAABQYAAALcGdhc3AAAAAQAAAUBAAAAAhnbHlmiQg1JwAAApAAAAzEaGVhZAY4K6gAAA9UAAAANmhoZWEIJgPvAAAPjAAAACRobXR4P8cAAAAAD7AAAABIbG9jYR0sGZgAAA/4AAAAJm1heHAAwwwIAAAQIAAAACBuYW1lzJ0bHQAAEEAAAALNcG9zdGxc9PoAABMQAAAA9HByZXDdawOFAAAfiAAAAHsAAQOLAZAABQAIAnoCvAAAAIwCegK8AAAB4AAxAQIAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZABA6ADoEANS/2oAWgNSAJcAAAABAAAAAAAAAAAAAwAAAAMAAAAcAAEAAAAAAEQAAwABAAAAHAAEACgAAAAGAAQAAQACAADoEP//AAAAAOgA//8AABgBAAEAAAAAAAAAAAEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABP///4kDqgMzABEAIQBDAEwADUAKS0ZBMR4WDQQELSsRND4CFzIeAg4DIi4CNxQeAj4DNzQuASIOATcXNjIVFAYPAQYPAQ4BHQEzNTQ2Nz4BPwE2Nz4BNzQmIyIDFBYyNi4CBkp+rGFfrnxMAUp+rMCufEx2OF6CkIBgNgFeor6kXNcfLWEEAQYFAjgWDHUGAwEUBxMMBhMUAVRAUxEqQyoCJkYoAV5frnxMAUp+rL+ufkpKfq5fR4RcOgI2YIBJX6JeXqJRZR0XBAgBBQQBHQwaGCUaAwYCAQgECwcGESgjMUT+jSAiIkAiASQAAAAAAgAAAAACWAJjABUAKwAItScaEQQCLSslFA8BBiIvAQcGIi8BJjQ3ATYyFwEWNRQPAQYiLwEHBiIvASY0NwE2MhcBFgJYBhwFDgbc2wUQBRsGBgEEBQ4GAQQGBhwFDgbc2wUQBRsGBgEEBQ4GAQQGdgcGHAUF29sFBRwGDgYBBAUF/vwGzwcGHAUF3NwFBRwGDgYBBAYG/vwGAAAAAgAAAAACWAJ0ABUAKwAItSIaDAQCLSsBFAcBBiInASY0PwE2Mh8BNzYyHwEWNRQHAQYiJwEmND8BNjIfATc2Mh8BFgJYBv78BRAE/vwGBhsGDgbb3AUQBBwGBv78BRAE/vwGBhsGDgbb3AUQBBwGAXAHBv78BgYBBAYOBhwFBdzcBQUcBs8HBv78BQUBBAYOBhwFBdzcBQUcBgAAAwAA/4kDqgMzAAwAGAAkAAq3HRkRDQsFAy0rJTIWFRQGIyEiJjQ2FwEyFhQGJyEiJjQ2NwEyFhQGIyEiLgE2NwNCKj48LP0mLDw+KgLaLDw8LP0mLDw8LALaLDw+Kv0mKzwBPCxaPC0qPj5WPgEBbD5UPgE8VjwBAW0+VT4+VjwBAAAAAwAAAAAD3gKXAAwAIgAyAAq3LiceFgwGAy0rNyImPQE0NjIWHQEUBgEyFhcVFAYnFAYnISImJxE0NjMhMhYDETQmJyEiBhcRFBYzITI20RUgICoeHgKPLDwBPitcQP3DQVoBXEACPUFaZx4W/cMVIAEeFgI9FSDCHhbRFR4eFdEVIAE5PCtoLD4BQVwBWkIBOEFcXP6HATgWHgEgFf7IFR4eAAAEAAAAAAPeApcADAAZAC8APwANQAo7NCsjGRMMBgQtKyUiJjc1NDYyFhcVFAYnIiY9ATQ2MhYdARQGATIWFxUUBicUBichIiYnETQ2MyEyFgMRNCYnISIGFxEUFjMhMjYBbRUgAR4sHAEesRUgICoeHgKPLDwBPitcQP3DQVoBXEACPUFaZx4W/cMVIAEeFgI9FSDCHhbRFR4eFdEVIAEeFtEVHh4V0RUgATk8K2gsPgFBXAFaQgE4QVxc/ocBOBYeASAV/sgVHh4AAAACAAD/aQPoA1EAJwAwAAi1LioeCgItKwEVBwYHFwcnBg8BIycmJwcnNyYvATU3NjcnNxc2PwEzFxYXNxcHFhcHNCYiDgEWMjYD6LkKC3hmnxQfHo8bFRahZXkLCMfHBwx4ZaAPIByPHBYanmZ3DQeiVnhUAlh0WgGljhobF51kdgoLwsUHC3dkoBUZHI4cFRifZHcIDMPDBwx1ZJwbFWM8VFR4VFQAAAAFAAAAAAPeApcADAAZACYAPABMAA9ADEhBODAmIBkTDAYFLSslIiY3NTQ2MhYXFRQGJyImPQE0NjIWHQEUBiUiJjc1NDYyFh0BFAYBMhYXFRQGJxQGJyEiJicRNDYzITIWAxE0JichIgYXERQWMyEyNgFtFSABHiwcAR6xFSAgKh4eASMVIAEeLB4eAVYsPAE+K1xA/cNBWgFcQAI9QVpnHhb9wxUgAR4WAj0VIMIeFtEVHh4V0RUgAR4W0RUeHhXRFSABHhbRFR4eFdEVIAE5PCtoLD4BQVwBWkIBOEFcXP6HATgWHgEgFf7IFR4eAAAGAAAAAAPeApcADAAZACYAMwBJAFkAEUAOVU5FPTMtJiAZEwwGBi0rJSImNzU0NjIWFxUUBiciJj0BNDYyFh0BFAYlIiYnNTQ2MhYdARQGJyImNzU0NjIWHQEUBgEyFhcVFAYnFAYnISImJxE0NjMhMhYDETQmJyEiBhcRFBYzITI2AW0VIAEeLBwBHrEVICAqHh4BwBYeASAqHh6yFSABHiweHgFWLDwBPitcQP3DQVoBXEACPUFaZx4W/cMVIAEeFgI9FSDCHhbRFR4eFdEVIAEeFtEVHh4V0RUgAR4W0RUeHhXRFSABHhbRFR4eFdEVIAE5PCtoLD4BQVwBWkIBOEFcXP6HATgWHgEgFf7IFR4eAAACAAD/ugNIAwIACAAUAAi1EQsEAAItKwEyFhAGICYQNgE3JwcnBxcHFzcXNwGkrvb2/qT29gEEmlaamFiamliYmlYDAvb+pPb2AVz2/lyaVpiYVpqYVpiYVgAAAAMAAP9tA+gDTwAFAA4AFgAKtxYTDgoEAwMtKzURMwERASU2NCc3FhcUBxc2ECc3FhAH7AFi/p4BoElJR2kCay97e0yamo4BoAEh/B4BISNKzExKapSRZS93AWB7Spr+TJoAAAABAAD/agPoA1IACwAGswkDAS0rNREhESERIREhESERAWcBGgFn/pn+5tEBGgFn/pn+5v6ZAWcAAAMAAP9qAjADUgAbACgAYgAKt00yJiAYCgMtKwEUDgEUHgEdARQGIiY9ATQ+ATQuAT0BNDYyFhUFBwYXFjMyNzYnJiMiEzQ+Aj8BNjU3BiInFxQfAxYmFiMUDgIPAgYmBjUGHQE+AjU0MhUUHgEXNTQvAiYvAS4BAjBgYmJgrNisYGJiYK7Urv4eEgQIXHyEWA4eYGp4kAgcDBkdXAJk9GQEWi0TEREMHgwCCgYIDA8PAiJaCHRENEJ6BlwrEg0FDAcEAm4saF48XGYudiJOTiJ2LmZcPF5oLHYgTk4gBg4IBjQyChQ2/koSHiQOGBxcHjI2NjIgWisTFRUCMAoSEg4KDxAQAiIBWiBCBCYwIh4eIjAmBEIeXCkTDggUDBYAAA0AAP9qA6EDUgAIABEAGgAjACwANQA+AEcAUwBcAGwAdQCFAB9AHIF5dG9pYFtWUkxGQT04NC8rJiEdGRQPCwYCDS0rFzQmIgYeAT4BNzQmIg4BFj4BJzQmIgYeAjYFNCYiDgEWPgEnNCYiDgEeATYnNCYiBh4CNgU0JiIOAR4BNic0JiIOAR4BNgE1NC4BBhcVFB4BNgM0JiIOAR4BNjc1NCYjISIGHQEUFhchMjYHNCYiBh4CNhMRFAYjISImNRE0NjMhMhbWKjosAig+JtkqPCgCLDgu2So6LAIoPiYBryo8KAIsOC7YKjwoAiw4LtkqOiwCKD4mAa8qPCgCLDgu2Co8KAIsOC4Bqio6LAEqPCjVKjwoAiw4LtQUEP02DhYWDgLKDxYBKjosAig+JkosHPzuHSoqHQMSHSoHHSoqOiwCKB8dKio6LAIo9R4qKjwoAiy6HSoqOiwCKPUeKio8KAIs8h4qKjwoAiy6HioqPCgCLPIeKio8KAIs/nDWHSoCLhvWHSoCLgHHHioqPCgCLM+PDhYWDo8PFAEWpR4qKjwoAiwBgvymHSoqHQNaHSoqAAACAAD/+AI7Ay8AFgAwAAi1JhoUCQItKyU0JyIvAS4BJyYiBw4CDwEGFRQWMjYlFA4BJic0NzY/AT4BNz4BHgEXHgMXFhUBHgsBCA4GEAQCFAEEEAwICQsqOiwBHKbupgEtBB84GT4PBRweGgYQPDQ8BS3PFBMMFQkgDAkJDR4UCwwTFB0qKmV3pgKqdVFIBS5UJnozERQCEBMzekxeA0dTAAAAAAIAAP+xBHcDCwAFAB8ACLUbEQMBAi0rBRUhETMRARUUBi8BAQYiLwEHJwE2Mh8BAScmNjsBMhYEd/uJRwPoFApE/p8GDgaC6GsBRgYOBoIBA0MJCA3zBwoHSANa/O4CuPIMCglE/p8GBoLpbAFGBgaCAQNDCRYKAAP//AAABEcCagARAC8AWgAKt1U1JRIMAAMtKzciJjcRBwYuATY/ATYWFxEUBikBIiY/ATY0JiIGFRQGIiY1NDc2MhYUDwEzMhYOAQEWFRQOASY3NDYyFgcUFjI2NCYHIiY0NjcyPgEmJyIHDgEuATc2MzIWBxSdFSABHRQqEg4UZxwwASABwP78IR4Z0RQoOioeKiA0MpJlM3iHFSACHAGHN2SKZgEgKCIBJjYmJhsVICAVEBYCGg4ZCgoqJBALKlY7VAFZIBUBTA8KDigqCDMOIhr+YBUgQBnRFDsoJx8WHh4WSDMyZZAzeB4qIAElM0lGYgJmRBUgIBUbJiY2KAEeLBwCFiIUAhYSDhYoE05WOi4AAAEAAAABAAC/H7FUXw889QALA+gAAAAA0VMJHAAAAADRUt7s//z/aQR3A1IAAAAIAAIAAAAAAAAAAQAAA1L/agBaBHYAAP/8BHcAAQAAAAAAAAAAAAAAAAAAABID6AAAA6kAAAKCAAACggAAA6oAAAPeAAAD3gAAA+gAAAPeAAAD3gAAA0gAAAPoAAAD6AAAAjAAAAPoAAACOwAABHYAAARHAAAAAAAAAHgAxgEUAVYBqAIOAmIC2gNkA5IDxAPgBHAFSgWcBdgGYgAAAAEAAAASAIYADQAAAAAAAgAAABAAcwAAADALcAAAAAAAAAASAN4AAQAAAAAAAAA1AAAAAQAAAAAAAQAIADUAAQAAAAAAAgAHAD0AAQAAAAAAAwAIAEQAAQAAAAAABAAIAEwAAQAAAAAABQALAFQAAQAAAAAABgAIAF8AAQAAAAAACgArAGcAAQAAAAAACwATAJIAAwABBAkAAABqAKUAAwABBAkAAQAQAQ8AAwABBAkAAgAOAR8AAwABBAkAAwAQAS0AAwABBAkABAAQAT0AAwABBAkABQAWAU0AAwABBAkABgAQAWMAAwABBAkACgBWAXMAAwABBAkACwAmAclDb3B5cmlnaHQgKEMpIDIwMTUgYnkgb3JpZ2luYWwgYXV0aG9ycyBAIGZvbnRlbGxvLmNvbWZvbnRlbGxvUmVndWxhcmZvbnRlbGxvZm9udGVsbG9WZXJzaW9uIDEuMGZvbnRlbGxvR2VuZXJhdGVkIGJ5IHN2ZzJ0dGYgZnJvbSBGb250ZWxsbyBwcm9qZWN0Lmh0dHA6Ly9mb250ZWxsby5jb20AQwBvAHAAeQByAGkAZwBoAHQAIAAoAEMAKQAgADIAMAAxADUAIABiAHkAIABvAHIAaQBnAGkAbgBhAGwAIABhAHUAdABoAG8AcgBzACAAQAAgAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAGYAbwBuAHQAZQBsAGwAbwBSAGUAZwB1AGwAYQByAGYAbwBuAHQAZQBsAGwAbwBmAG8AbgB0AGUAbABsAG8AVgBlAHIAcwBpAG8AbgAgADEALgAwAGYAbwBuAHQAZQBsAGwAbwBHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAGgAdAB0AHAAOgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAABAgEDAQQBBQEGAQcBCAEJAQoBCwEMAQ0BDgEPARABEQESDGhlbHAtY2lyY2xlZA9hbmdsZS1kb3VibGUtdXARYW5nbGUtZG91YmxlLWRvd24EbWVudQpiYXR0ZXJ5LTI1CmJhdHRlcnktNTADY29nCmJhdHRlcnktNzULYmF0dGVyeS0xMDAOY2FuY2VsLWNpcmNsZWQGdm9sdW1lBHBsdXMJaG91cmdsYXNzBGNhbGMEdGludApjaGFydC1saW5lDHNvcnQtbnVtZXJpYwAAAAEAAf//AA8AAAAAAAAAAAAAAACwACwgsABVWEVZICBLuAAOUUuwBlNaWLA0G7AoWWBmIIpVWLACJWG5CAAIAGNjI2IbISGwAFmwAEMjRLIAAQBDYEItsAEssCBgZi2wAiwgZCCwwFCwBCZasigBCkNFY0VSW1ghIyEbilggsFBQWCGwQFkbILA4UFghsDhZWSCxAQpDRWNFYWSwKFBYIbEBCkNFY0UgsDBQWCGwMFkbILDAUFggZiCKimEgsApQWGAbILAgUFghsApgGyCwNlBYIbA2YBtgWVlZG7ABK1lZI7AAUFhlWVktsAMsIEUgsAQlYWQgsAVDUFiwBSNCsAYjQhshIVmwAWAtsAQsIyEjISBksQViQiCwBiNCsQEKQ0VjsQEKQ7AAYEVjsAMqISCwBkMgiiCKsAErsTAFJbAEJlFYYFAbYVJZWCNZISCwQFNYsAErGyGwQFkjsABQWGVZLbAFLLAHQyuyAAIAQ2BCLbAGLLAHI0IjILAAI0JhsAJiZrABY7ABYLAFKi2wBywgIEUgsAtDY7gEAGIgsABQWLBAYFlmsAFjYESwAWAtsAgssgcLAENFQiohsgABAENgQi2wCSywAEMjRLIAAQBDYEItsAosICBFILABKyOwAEOwBCVgIEWKI2EgZCCwIFBYIbAAG7AwUFiwIBuwQFlZI7AAUFhlWbADJSNhRESwAWAtsAssICBFILABKyOwAEOwBCVgIEWKI2EgZLAkUFiwABuwQFkjsABQWGVZsAMlI2FERLABYC2wDCwgsAAjQrILCgNFWCEbIyFZKiEtsA0ssQICRbBkYUQtsA4ssAFgICCwDENKsABQWCCwDCNCWbANQ0qwAFJYILANI0JZLbAPLCCwEGJmsAFjILgEAGOKI2GwDkNgIIpgILAOI0IjLbAQLEtUWLEEZERZJLANZSN4LbARLEtRWEtTWLEEZERZGyFZJLATZSN4LbASLLEAD0NVWLEPD0OwAWFCsA8rWbAAQ7ACJUKxDAIlQrENAiVCsAEWIyCwAyVQWLEBAENgsAQlQoqKIIojYbAOKiEjsAFhIIojYbAOKiEbsQEAQ2CwAiVCsAIlYbAOKiFZsAxDR7ANQ0dgsAJiILAAUFiwQGBZZrABYyCwC0NjuAQAYiCwAFBYsEBgWWawAWNgsQAAEyNEsAFDsAA+sgEBAUNgQi2wEywAsQACRVRYsA8jQiBFsAsjQrAKI7AAYEIgYLABYbUQEAEADgBCQopgsRIGK7ByKxsiWS2wFCyxABMrLbAVLLEBEystsBYssQITKy2wFyyxAxMrLbAYLLEEEystsBkssQUTKy2wGiyxBhMrLbAbLLEHEystsBwssQgTKy2wHSyxCRMrLbAeLACwDSuxAAJFVFiwDyNCIEWwCyNCsAojsABgQiBgsAFhtRAQAQAOAEJCimCxEgYrsHIrGyJZLbAfLLEAHistsCAssQEeKy2wISyxAh4rLbAiLLEDHistsCMssQQeKy2wJCyxBR4rLbAlLLEGHistsCYssQceKy2wJyyxCB4rLbAoLLEJHistsCksIDywAWAtsCosIGCwEGAgQyOwAWBDsAIlYbABYLApKiEtsCsssCorsCoqLbAsLCAgRyAgsAtDY7gEAGIgsABQWLBAYFlmsAFjYCNhOCMgilVYIEcgILALQ2O4BABiILAAUFiwQGBZZrABY2AjYTgbIVktsC0sALEAAkVUWLABFrAsKrABFTAbIlktsC4sALANK7EAAkVUWLABFrAsKrABFTAbIlktsC8sIDWwAWAtsDAsALABRWO4BABiILAAUFiwQGBZZrABY7ABK7ALQ2O4BABiILAAUFiwQGBZZrABY7ABK7AAFrQAAAAAAEQ+IzixLwEVKi2wMSwgPCBHILALQ2O4BABiILAAUFiwQGBZZrABY2CwAENhOC2wMiwuFzwtsDMsIDwgRyCwC0NjuAQAYiCwAFBYsEBgWWawAWNgsABDYbABQ2M4LbA0LLECABYlIC4gR7AAI0KwAiVJiopHI0cjYSBYYhshWbABI0KyMwEBFRQqLbA1LLAAFrAEJbAEJUcjRyNhsAlDK2WKLiMgIDyKOC2wNiywABawBCWwBCUgLkcjRyNhILAEI0KwCUMrILBgUFggsEBRWLMCIAMgG7MCJgMaWUJCIyCwCEMgiiNHI0cjYSNGYLAEQ7ACYiCwAFBYsEBgWWawAWNgILABKyCKimEgsAJDYGQjsANDYWRQWLACQ2EbsANDYFmwAyWwAmIgsABQWLBAYFlmsAFjYSMgILAEJiNGYTgbI7AIQ0awAiWwCENHI0cjYWAgsARDsAJiILAAUFiwQGBZZrABY2AjILABKyOwBENgsAErsAUlYbAFJbACYiCwAFBYsEBgWWawAWOwBCZhILAEJWBkI7ADJWBkUFghGyMhWSMgILAEJiNGYThZLbA3LLAAFiAgILAFJiAuRyNHI2EjPDgtsDgssAAWILAII0IgICBGI0ewASsjYTgtsDkssAAWsAMlsAIlRyNHI2GwAFRYLiA8IyEbsAIlsAIlRyNHI2EgsAUlsAQlRyNHI2GwBiWwBSVJsAIlYbkIAAgAY2MjIFhiGyFZY7gEAGIgsABQWLBAYFlmsAFjYCMuIyAgPIo4IyFZLbA6LLAAFiCwCEMgLkcjRyNhIGCwIGBmsAJiILAAUFiwQGBZZrABYyMgIDyKOC2wOywjIC5GsAIlRlJYIDxZLrErARQrLbA8LCMgLkawAiVGUFggPFkusSsBFCstsD0sIyAuRrACJUZSWCA8WSMgLkawAiVGUFggPFkusSsBFCstsD4ssDUrIyAuRrACJUZSWCA8WS6xKwEUKy2wPyywNiuKICA8sAQjQoo4IyAuRrACJUZSWCA8WS6xKwEUK7AEQy6wKystsEAssAAWsAQlsAQmIC5HI0cjYbAJQysjIDwgLiM4sSsBFCstsEEssQgEJUKwABawBCWwBCUgLkcjRyNhILAEI0KwCUMrILBgUFggsEBRWLMCIAMgG7MCJgMaWUJCIyBHsARDsAJiILAAUFiwQGBZZrABY2AgsAErIIqKYSCwAkNgZCOwA0NhZFBYsAJDYRuwA0NgWbADJbACYiCwAFBYsEBgWWawAWNhsAIlRmE4IyA8IzgbISAgRiNHsAErI2E4IVmxKwEUKy2wQiywNSsusSsBFCstsEMssDYrISMgIDywBCNCIzixKwEUK7AEQy6wKystsEQssAAVIEewACNCsgABARUUEy6wMSotsEUssAAVIEewACNCsgABARUUEy6wMSotsEYssQABFBOwMiotsEcssDQqLbBILLAAFkUjIC4gRoojYTixKwEUKy2wSSywCCNCsEgrLbBKLLIAAEErLbBLLLIAAUErLbBMLLIBAEErLbBNLLIBAUErLbBOLLIAAEIrLbBPLLIAAUIrLbBQLLIBAEIrLbBRLLIBAUIrLbBSLLIAAD4rLbBTLLIAAT4rLbBULLIBAD4rLbBVLLIBAT4rLbBWLLIAAEArLbBXLLIAAUArLbBYLLIBAEArLbBZLLIBAUArLbBaLLIAAEMrLbBbLLIAAUMrLbBcLLIBAEMrLbBdLLIBAUMrLbBeLLIAAD8rLbBfLLIAAT8rLbBgLLIBAD8rLbBhLLIBAT8rLbBiLLA3Ky6xKwEUKy2wYyywNyuwOystsGQssDcrsDwrLbBlLLAAFrA3K7A9Ky2wZiywOCsusSsBFCstsGcssDgrsDsrLbBoLLA4K7A8Ky2waSywOCuwPSstsGossDkrLrErARQrLbBrLLA5K7A7Ky2wbCywOSuwPCstsG0ssDkrsD0rLbBuLLA6Ky6xKwEUKy2wbyywOiuwOystsHAssDorsDwrLbBxLLA6K7A9Ky2wciyzCQQCA0VYIRsjIVlCK7AIZbADJFB4sAEVMC0AS7gAyFJYsQEBjlmwAbkIAAgAY3CxAAVCsQAAKrEABUKxAAgqsQAFQrEACCqxAAVCuQAAAAkqsQAFQrkAAAAJKrEDAESxJAGIUViwQIhYsQNkRLEmAYhRWLoIgAABBECIY1RYsQMARFlZWVmxAAwquAH/hbAEjbECAEQA') format('truetype'); + src: url('data:application/octet-stream;base64,d09GRgABAAAAABmIAA8AAAAAKsgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+IUkiY21hcAAAAdgAAACmAAACiOnFAFpjdnQgAAACgAAAABMAAAAgBtX/AmZwZ20AAAKUAAAFkAAAC3CKkZBZZ2FzcAAACCQAAAAIAAAACAAAABBnbHlmAAAILAAADcEAABWEVKl1aWhlYWQAABXwAAAAMgAAADYTyTiUaGhlYQAAFiQAAAAgAAAAJAfIA/ZobXR4AAAWRAAAADoAAABYTOP/+2xvY2EAABaAAAAALgAAAC47fjW8bWF4cAAAFrAAAAAgAAAAIAFdDDxuYW1lAAAW0AAAAXcAAALNzJ0eIHBvc3QAABhIAAAAxAAAAR260xEFcHJlcAAAGQwAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZK5nnMDAysDAVMW0h4GBoQdCMz5gMGRkAooysDIzYAUBaa4pDA4vGF6IMAf9z2KIYg5imA4UZgTJAQDp2AvBAHic7ZLbDQIxDATnuPA+3lwXVEJBfFHwdgHrsGUQaSzFcqJoJ8ASGM3DNBjeDNR6uTv0/siu9xvPPtOqr/vn40pV71uvC88237hizYatz+2ZOHDkxJkLV27cmT264r+mKsOU3Vxp/uj5BmeKQnlSqMQVyqGCs0fBFlCwDxRsBoVyq2BbKNTrFGwQBbtEwVZRsF8UbBoFO0fB9lHwP/Af+sH8BeUZMdoAAHicY2BAAxIQyBz0PxOEARJmA9sAeJytVml300YUHXlJnIQsJQstamHExGmwRiZswYAJQbJjIF2crZWgixQ76b7xid/gX/Nk2nPoN35a7xsvJJC053Cak6N3583VzNtlElqS2AvrkZSbL8XU1iaN7DwJ6YZNy1F8KDt7IWWKyd8FURCtltq3HYdERCJQta6wRBD7HlmaZHzoUUbLtqRXTcotPekuW+NBvVXffho6yrE7oaRmM3RoPbIlVRhVokimPVLSpmWo+itJK7y/wsxXzVDCiE4iabwZxtBI3htntMpoNbbjKIpsstwoUiSa4UEUeZTVEufkigkMygfNkPLKpxHlw/yIrNijnFawS7bT/L4vead3OT+xX29RtuRAH8iO7ODsdCVfhFtbYdy0k+0oVBF213dCbNnsVP9mj/KaRgO3KzK90IxgqXyFECs/ocz+IVktnE/5kkejWrKRE0HrZU7sSz6B1uOIKXHNGFnQ3dEJEdT9kjMM9pg+Hvzx3imWCxMCeBzLekclnAgTKWFzNEnaMHJgJWWLKqn1rpg45XVaxFvCfu3a0ZfOaONQd2I8Ww8dWzlRyfFoUqeZTJ3aSc2jKQ2ilHQmeMyvAyg/oklebWM1iZVH0zhmxoREIgIt3EtTQSw7saQpBM2jGb25G6a5di1apMkD9dyj9/TmVri501PaDvSzRn9Wp2I62AvT6WnkL/Fp2uUiRen66Rl+TOJB1gIykS02w5SDB2/9DtLL15YchdcG2O7t8yuofdZE8KQB+xvQHk/VKQlMhZhViFZAYq1rWZbJ1awWqcjUd0OaVr6s0wSKchwXx76Mcf1fMzOWmBK+34nTsyMuPXPtSwjTHHybdT2a16nFcgFxZnlOp1mW7+s0x/IDneZZntfpCEtbp6MsP9RpgeVHOh1jeUELmnTfwZCLMOQCDpAwhKUDQ1hegiEsFQxhuQhDWBZhCMslGMLyYxjCchmGsLysZdXUU0nj2plYBmxCYGKOHrnMReVqKrlUQrtoVGpDnhJulVQUz6p/ZaBePPKGObAWSJfIml8xzpWPRuX41hUtbxo7V8Cx6m8fjvY58VLWi4U/Bf/V1lQlvWLNw5Or8BuGnmwnqjapeHRNl89VPbr+X1RUWAv0G0iFWCjKsmxwZyKEjzqdhmqglUPMbMw8tOt1y5qfw/03MUIWUP34NxQaC9yDTllJWe3grNXX27LcO4NyOBMsSTE38/pW+CIjs9J+kVnKno98HnAFjEpl2GoDrRW82ScxD5neJM8EcVtRNkja2M4EiQ0c84B5850EJmHqqg3kTuGGDfgFYW7BeSdconqjLIfuRezzKKT8W6fiRPaoaIzAs9kbYa/vQspvcQwkNPmlfgxUFaGpGDUV0DRSbqgGX8bZum1Cxg70Iyp2w7Ks4sPHFveVkm0ZhHykiNWjo5/WXqJOqtx+ZhSX752+BcEgNTF/e990cZDKu1rJMkdtA1O3GpVT15pD41WH6uZR9b3j7BM5a5puuiceel/TqtvBxVwssPZtDtJSJhfU9WGFDaLLxaVQ6mU0Se+4BxgWGNDvUIqN/6v62HyeK1WF0XEk307Ut9HnYAz8D9h/R/UD0Pdj6HINLs/3mhOfbvThbJmuohfrp+g3MGutuVm6BtzQdAPiIUetjrjKDXynBnF6pLkc6SHgY90V4gHAJoDF4BPdtYzmUwCj+Yw5PsDnzGHQZA6DLeYw2GbOGsAOcxjsMofBHnMYfMGcdYAvmcMgZA6DiDkMnjAnAHjKHAZfMYfB18xh8A1z7gN8yxwGMXMYJMxhsK/p1jDMLV7QXaC2QVWgA1NPWNzD4lBTZcj+jheG/b1BzP7BIKb+qOn2kPoTLwz1Z4OY+otBTP1V050h9TdeGOrvBjH1D4OY+ky/GMtlBr+MfJcKB5RdbD7n74n3D9vFQLkAAQAB//8AD3icrVh7bFvXeT/fueeee3lJXr7uQyKpK4qUSJmiKIVPx6JpSpathxknsWmFVhWFdiQ5taw4wOJHkyBovQyBByz2gqBLhbWxgyR/BJnXdH90K9A0aFqkGArEAYo+5mB/DUOKDenQbcay2fS+e0knbubNSFY9zj33u+ec+72/33cJu3HjxlnhdSFPAmSA7CR1cv67+WRCEBnM7Pm2cl+zViJcYAJnG4SJAhPXiSRTKq0SQgROhBYRiSyJ8iFCAWiDUAoHCVC4J1Ir32ajLNGNO+08WPOEQmNR3ewxB11i30igUKVmzqKqEM/SsmbRqlAuZCGuQtks5XSN+wD/VOiDfLFQKldhB9hjuZBMxAXNyJWylM8//cahhy49VQecvI3Xk1uXz5z/aqsEyy+/9crSL/pHDzHgIt1qeE5wATQp5OEhDRq1/YH0zjRN7h6G5Zvbf3Dp6fn5py89NPPM0jZaan117qGXl5dfvn+1z2QyoNr6POGeobDAKcjMLfHAcOKu6fafxOLxWhwGCUXpCW3Sh4lORshIbbgPqCiBQOkMEal4nFCBHmcgEOE4ACFrptZjahoXwyNDGsoZHwPJHpKFMpRyJhjF21Jpk0dFlX9wRfSzKOfA8AYYvy3xpIRk8coVUYxymyCK7Wv8PYf4wQc3iZwj0WG9y/+Jm/wPgQA+IAKdEYEBO47GFOA4sQXAlWua2aOZDv+oVeQ0hUyifXL9ULYH5P92VOS/fQ3ZxJc6PF/5oMPz7YjwmGQzZzOpfsr0ezZRFDtEzq9cwT22AAIhXX/3kB4ySKZqO2RgqHomtiSg+JwKLQ4Eld/AC4EmwZt6uFf1Aon19Q6GE1rA26Oaskg84HaJ2shQzkAnTAzEk4WSCTlD4ylnXnbmSM+iPwpT6Wolcz2ZqVTT9O8ylc68krHn1fT15EgFKpnFymi6Wl2oAhyrNqpQWagAbFQPIKkCHd5x+Hv6Z8h7nOTIdG0Stc0J8rkhyJRInEjrGFUcRN4iEmNSg0gSaxImsTqQu7JbkgNWr6GFVC8u9YDH5r4cT26HQiln9IHGkWMTZUl1REgFCqX8QM4QAoUk3nMzoBl4X3pfj8XSlkXPZSpQHVmqXX9nchGWanT75OIRy7j+jh4Dy6Db9dgPLeN93bJ03AB3V0YeyVRhcgkWp2Dr5NJS+1nYalgQ09s/wTW2aOwW+XrJGNlB7q3VmYCBRGQUUXRThRGXTFyYSpAiyS3i4tzVIC4XbxLu4nXbSNWJUj47kkwMxCJhLeTtVXs7wnpsU8WT5aItrCPl55ccNmzZMlGw3vwiOvgc6sA4u/Fl4UPhfpIi42S+NjM0mIhHwr09MgVdA4Jq8ftUiYtMIoIwE8Cg242JVAAqbKCjYAJZRx/2zKJGvWRuLDuS7rfCTNRHQJe4ZEopTJmJVDIlpcrJMSiWS+UUZtMdkDcNs2xKhikVkphhMXsKH/6N4j699pLWb52L6MbF1a+45XfflTynVy/4YtFzUSP8rbVTXunlhdMN2nx8EV59Lhwxv7lyUnH/8MeS+9TKBb03+lxU73lp5ZTseecdyXNi5c8j+sOVRuN0o4HeeqvNk6SCFWix9gAXBQEUoqDZJZV5qYcTt0Lc68SFNJfSIm5ZdjeI2y03ieyW65Gwbfo9sztrE1vz49nR4dRALJyMJG91AN//6QCfPPl/eAMkHAJOFr6gW3yugOGf0V2ezJGDZLV2GMuqwMCDQpMNV4D7mY+qMvF6iHeduO205WkRr6J4G8TrVZpE8Sr14VRHhc0D990zu6u2fWJbsZAdTeWH8/9TkcE7KTL1iSJTvw+tvm1LjZPv/B61+3kUbcfi94RZgRKZaGS4NqSFggGMPq/HrbixOsxQHMhxjLc1ICLD1CQLqCIU189jSX8JyhhmGHMYWGV45dLVq+1Xrl4Ftrmw+WJzc7P54uaCQDu0pavtpc2FF1/EJ/bYrVcbmAfuxUhRiUHStZQL7HeGgorAyC5DZQJME/v9WAtgDbeIRAwIGOnFQB4CMFQqpMqGqUlmyY8Tv/TPcLj9LbgwNzfzZbo+9uST9c3N5+ACDFyzYCAx/9P6/NEX/nR17BS0npzfbNc3iX02uXEUediHzpOtjdjSCljxNxAwoZsJrJtpGPOyOVzuJu6A/SOJvcjDwCe/cATCcKT9jfY/vn9z0v4GHOnIeJSO4/kRMkwOk121qWKOUqEEjDbvoQgjERsxzkS+gT7POKY2ipiSshamPrtAf5Ln9u8LRwZTfWHJwRsqaBbYjue4ZxUQNHbcVBclbhr5XLmUQowYQnyJ4KNYRhxiav2CkTQSmkp9lCd5kfdBlRYLCDMtMIuFMYrZMgt0vHX4cOuNX75hXy797FLbCjJ56alnmqrVOnr6vBz19PYt0ZV/W2GLo6FAwGN5qMJlj89H44vy49OFqSf40kjQK3okRh/NPLJcWVrLnozv3Rs/mV1bqiw/kjkZ27s3xlWZF3KKVmrPB61BtSe6ZOVKpVxscSSk63RcCQZVxef30zgsxqZYcjxuWfHxJJuylraEVFnzGI5SbNtdRN3KiPDDJEEypEiqZIbsJ0vkGDlB/oh8rfb0g1hUNho7RplbKQ0GEf2jynUjqAqKW1daZsgnuP0eGVG8m7W8LipySgmIpEU0rbdJens9swEJbQE9TdLT4+2Ze+ZrT33l8T947Pj6Iw+3vrT4wMK+++t7du+amtxeuXtrIT8+NpLWQp3fYBghvonFhlvggHa76lQh5VBoSbyVoIIFpd990iU4E8CUBVyyTVUSbhIx/SAWiKMdO+lF6m4PBTqYrdhNQz9Pb8vQ4WryV+nKMM1szf6qew9/2SX88k4P4PX0tjRktmZ+0SX8TPNfL6mGodK/9RnQ3TafiV77qC+d7hOCfWkJJza5v3v9dyvt7P3eZ+5/e5P+mfv2Yz/vS9NsxBnh3S71vXP2S+3h1S4Fzlx7zXnnIo5OPvsPOiGMYT4ZI4O1gS5ad3IYtmENjCjaRDRB61Et3MvEnpGhQipuO34qGZcwNIDrdmM1hCGWTBXKiBvQdFVUt2kJpgGWG2QVyw7VgKHbuNwoDERf++g1GGX9W3urPjFqRZm/Ol4TRt/TQh7dFfO4XF5Lc3tCGnK4euo1+vqJ+2fFbCP5RMGvUX+w8ER9SZzp5OI32SnBjWmun+ypzfa4EbqbQb8gsACCH/RbELH/EBn2Ukxg2IlQ4ThHZE/oPrxQsmAfMk9IX0TXVK+TLTFVGSOiPhDIBzAO+Bh0WqqU05GgzKUJzOXs1H+dnRE+1JTp9kvYVJz5cB1msAs5A8JOl+z9V0mRZoXFax/Rv/6tR3HZa/iZXx+D3dxeMS0bCua5G9cQ5M7QoxiJY2SRrNUeZqBQmCGC4lIE1waRXKJLEjc8wNyAYJe2iEJckuJqYWeJib2BF5E0uQpebFlIXQsCeeDA3nvmZqendlS33V0sZNLJoVh/JBwc08ZIAAK+DsIvBySOXQhmOQPLLN8C8STO7UjQ7exYRBNir+KDfM5QAbCdQbuW7Uwp2WYuJCWnn8lV0dpof/SDcilvP/wmls8+LR1UtSPRcYjB2+1r2GG8rw1vS1vpWCH3/Gr+9LN6jEbh2fLKH69BbDgOyVIyGcHKrvsNGlZ7FSU96HenFyYacDCmQ92nqMNpOa/Gw+2WHqvhYRPDqX7DsozZfG71fP40HgxD+bndh+natH1MJJksDduAgRpxjWLOMoZDexe2Ze1CSG58LPwD+gonPnI3Zr1ZkGp6rTrhx6ZCIqyAlZTOTGLfurvzrWOcSAITJLaBdsJnqxgYjAgM1Q+Ei8AfwkaRiohaRTs+RFqP7Pm2G/dt6a7HRv7OG8wv9KLaXbduYQQ27rjn4MGDNawB0zu3V+zez4oYIdQE11wIDoYQk+h5HZIYywb6Qkj/NB2WB3KmhU09TzmZU0JgZmLmxNppO5BgQqKIwdHp1DX4l9qBWhF0l+tHriD+Dy7vbI/vXF7eCZcTlkuQIrLi9bTHhwpQGoTLQwVxUO4pX2yfvUiP5y/m/Rn/Af/3Jw9M9pfg+ZtHtN862jlgahlUFuJRmQmFoe4ZuyU8QYZzF9pnL0C2cLHg8x3wZ5y88DH9Q7S1hHkBUZKIcA0QqtANQumjtpZZAyGKDRcY1BOhoVLIb3+WCA1ggVDBFDEupC4sNfMOZkUF5Yy/gr7GyQbA5Zhx/deolpgReOGnX6dBnL56bKJB79t+sf2WgxZhyojBsbUXXlg7ZjmY6WOsux4yRMZro8xmBZMQGs+GTusIXB4lIoBjLpsnEeqhoWJCH3Q+laAxpISDWpA1hw3zVvaQ5SoYwkVDHVSNfaf3QdFhrMsfnHr+8nkaOOfTAKuAw+Ixy/wdJo88T7/u9A833hSCqDMf6myMTJCd5AhZqR16YBfl8paB3oALOHI9g7rsfmZg9OZnBhU7cdXV8nmp7FEoB5kvE8ntdj45uJvELbnrqyuHlr90cP++e+tzM5M7tEEtaf8k/FjwAcUZgY672cnVvMN9CPEjlnUs1FWAPPatCS6Jur2mW8ZTgY6+cLX97Q811m9nM7AUeVBWnOHcp9PnFKkzlZTZtkfGnvoyleX22f+MMPFNzuCfFLnU9biive6NlCtjfMdMu1J/ISvw3fYPbCJM2uP/Mm+v0MD133g0RdHo6qRt6f34xuu/yU5PZWnIYeJBHXsc7UHlvwFt9oFbAAAAeJxjYGRgYABi6+qaU/H8Nl8ZuJlfAEUYbkhXVcHo/3/+Z7KUMwcBuRwMTCBRAFtvDJ8AAHicY2BkYGAO+p/FwMBS9v/P//8s5QxAERQgBgCjFAbFeJxjfsHAwLzy/3+mJgYGEGZeBcT3oPgFEtsDygdiJgMobc3AwFIGxO7//4D5IP0LgFiQgQEAfdISGwAAAAAAAAC4ARgBeAHWAkYCzANAA94EkgTQBRIFQgXwBvoHWAe2CG4JZAmsCf4KwgAAAAEAAAAWAIYADQAAAAAAAgA0AEQAcwAAAJILcAAAAAB4nHWQ3WrCMBiG38yfbQrb2GCny9FQxuoPDEQQBIeebCcyPB211rZSG0mj4G3sHnYxu4ldy17bOIayljTP9+TLl68BcI1vCOTPE0fOAmeMcj7BKXqWC/TPlovkF8slVPFmuUz/brmCBwSWq7jBByuI4jmjBT4tC1yJS8snuBB3lgv0j5aL5J7lEm7Fq+UyvWe5golILVdxL74GarXVURAaWRvUZbvZ6sjpViqqKHFj6a5NqHQq+3KuEuPHsXI8tdzz2A/Wsav34X6e+DqNVCJbTnOvRn7ia9f4s131dBO0jZnLuVZLObQZcqXVwveMExqz6jYaf8/DAAorbKER8apCGEjUaOuc22iihQ5pygzJzDwrQgIXMY2LNXeE2UrKuM8xZ5TQ+syIyQ48fpdHfkwKuD9mFX20ehhPSLszosxL9uWwu8OsESnJMt3Mzn57T7HhaW1aw127LnXWlcTwoIbkfezWFjQevZPdiqHtosH3n//7AelzhFMAeJxtjetKAzEUhDNtrO7FXrzUpwiswuLzpGcPu8FsEnJR+vauSAuC82PmGxgYsRK/qsX/OmKFNSRusMEt7lChRoMW99hihz0OeMAjnvCMI15EO7ENikwky8NOu9GyGnw5LVHC4U8f/JeTM7tSn3TOHM/qrb9i363Jj9f63jcXfO26LWlHbC83m09vy8wy2JKqyZc4Wp2SJG1JZuNyTZOOWVnjuE1+IbesoyHJg8nSevqofkz5wK7JUadJ8RzyWYhvVYpJbnicY/DewXAiKGIjI2Nf5AbGnRwMHAzJBRsZWJ02MTAyaIEYm7mYGDkgLD4GMIvNaRfTAaA0J5DN7rSLwQHCZmZw2ajC2BEYscGhI2Ijc4rLRjUQbxdHAwMji0NHckgESEkkEGzmYWLk0drB+L91A0vvRiYGFwAMdiP0AAA=') format('woff'), + url('data:application/octet-stream;base64,AAEAAAAPAIAAAwBwR1NVQiCLJXoAAAD8AAAAVE9TLzI+IUkiAAABUAAAAFZjbWFw6cUAWgAAAagAAAKIY3Z0IAbV/wIAAB6wAAAAIGZwZ22KkZBZAAAe0AAAC3BnYXNwAAAAEAAAHqgAAAAIZ2x5ZlSpdWkAAAQwAAAVhGhlYWQTyTiUAAAZtAAAADZoaGVhB8gD9gAAGewAAAAkaG10eEzj//sAABoQAAAAWGxvY2E7fjW8AAAaaAAAAC5tYXhwAV0MPAAAGpgAAAAgbmFtZcydHiAAABq4AAACzXBvc3S60xEFAAAdiAAAAR1wcmVw5UErvAAAKkAAAACGAAEAAAAKADAAPgACREZMVAAObGF0bgAaAAQAAAAAAAAAAQAAAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAEDfwGQAAUAAAJ6ArwAAACMAnoCvAAAAeAAMQECAAACAAUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBmRWQAQOgA6BQDUv9qAFoDUgCXAAAAAQAAAAAAAAAAAAUAAAADAAAALAAAAAQAAAF8AAEAAAAAAHYAAwABAAAALAADAAoAAAF8AAQASgAAAAQABAABAADoFP//AADoAP//AAAAAQAEAAAAAQACAAMABAAFAAYABwAIAAkACgALAAwADQAOAA8AEAARABIAEwAUABUAAAEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAQwAAAAAAAAAFQAA6AAAAOgAAAAAAQAA6AEAAOgBAAAAAgAA6AIAAOgCAAAAAwAA6AMAAOgDAAAABAAA6AQAAOgEAAAABQAA6AUAAOgFAAAABgAA6AYAAOgGAAAABwAA6AcAAOgHAAAACAAA6AgAAOgIAAAACQAA6AkAAOgJAAAACgAA6AoAAOgKAAAACwAA6AsAAOgLAAAADAAA6AwAAOgMAAAADQAA6A0AAOgNAAAADgAA6A4AAOgOAAAADwAA6A8AAOgPAAAAEAAA6BAAAOgQAAAAEQAA6BEAAOgRAAAAEgAA6BIAAOgSAAAAEwAA6BMAAOgTAAAAFAAA6BQAAOgUAAAAFQAE////iQOqAzMAEQAhAEMATACQtzMmIwMFBAFHS7AKUFhANgAGAwQDBgRtAAQFAwQFawAHCAICB2UAAAADBgADYAAFAAgHBQhhAAIBAQJUAAICAVkAAQIBTRtANwAGAwQDBgRtAAQFAwQFawAHCAIIBwJtAAAAAwYAA2AABQAIBwUIYQACAQECVAACAgFZAAECAU1ZQAwTEy8cFRcYFyQJBR0rETQ+AhcyHgIOAyIuAjcUHgI+Azc0LgEiDgE3FzYyFRQGDwEGDwEOAR0BMzU0Njc+AT8BNjc+ATc0JiMiAxQWMjYuAgZKfqxhX658TAFKfqzArnxMdjhegpCAYDYBXqK+pFzXHy1hBAEGBQI4Fgx1BgMBFAcTDAYTFAFUQFMRKkMqAiZGKAFeX658TAFKfqy/rn5KSn6uX0eEXDoCNmCASV+iXl6iUWUdFwQIAQUEAR0MGhglGgMGAgEIBAsHBhEoIzFE/o0gIiJAIgEkAAIAAAAAAlgCYwAVACsAK0AoHQECBQcBAwICRwAFAgVvAAIDAm8EAQMAA28BAQAAZhcUGBcUFAYFGislFA8BBiIvAQcGIi8BJjQ3ATYyFwEWNRQPAQYiLwEHBiIvASY0NwE2MhcBFgJYBhwFDgbc2wUQBBwGBgEEBQ4GAQQGBhwFDgbc2wUQBBwGBgEEBQ4GAQQGdgcGHAUF29sFBRwGDgYBBAUF/vwGzwcGHAUF3NwFBRwGDgYBBAYG/vwGAAAAAAIAAAAAAlgCdQAVACsAK0AoJQEDAQ8BAAMCRwUBBAEEbwIBAQMBbwADAANvAAAAZhQXGBQXFAYFGisBFAcBBiInASY0PwE2Mh8BNzYyHwEWNRQHAQYiJwEmND8BNjIfATc2Mh8BFgJYBv78BRAE/vwGBhwFDgbb3AUQBBwGBv78BRAE/vwGBhwFDgbb3AUQBBwGAXAHBv78BgYBBAYOBhwFBdzcBQUcBs8HBv78BQUBBAYOBhwGBtvbBgYcBgAAAAMAAP+JA6oDMwAMABgAJABCQD8IAQQABQIEBWAHAQIAAwACA2AGAQABAQBUBgEAAAFYAAEAAUwaGQ4NAQAgHRkkGiMUEQ0YDhcIBQAMAQsJBRQrJTIWFRQGIyEiJjQ2FwEyFhQGJyEiJjQ2NwEyFhQGIyEiLgE2NwNCKj48LP0mLDw+KgLaLDw8LP0mLDw8LALaLDw+Kv0mKzwBPCxaPC0qPj5WPgEBbD5UPgE8VjwBAW0+VT4+VjwBAAAAAwAAAAAD3gKXAAwAIgAyAERAQQIBAQYABgEAbQMIAgAHBgAHawAFAAYBBQZgAAcEBAdUAAcHBFgABAcETAEAMS4pJiEeGRYUEw4NBwYADAEMCQUUKzciJj0BNDYyFh0BFAYBMhYXFRQGJxQGJyEiJicRNDYzITIWAxE0JichIgYXERQWMyEyNtEVICAqHh4Cjyw8AT4rXED9w0FaAVxAAj1BWmceFv3DFSABHhYCPRUgwh4W0RUeHhXRFSABOTwraCw+AUFcAVpCAThBXFz+hwE4Fh4BIBX+yBUeHgAAAAAEAAAAAAPeApcADAAZAC8APwBPQEwEAwIBCAAIAQBtBQsCCgQACQgACWsABwAIAQcIYAAJBgYJVAAJCQZYAAYJBkwODQEAPjs2My4rJiMhIBsaFBMNGQ4ZBwYADAEMDAUUKyUiJjc1NDYyFhcVFAYnIiY9ATQ2MhYdARQGATIWFxUUBicUBichIiYnETQ2MyEyFgMRNCYnISIGFxEUFjMhMjYBbRUgAR4sHAEesRUgICoeHgKPLDwBPitcQP3DQVoBXEACPUFaZx4W/cMVIAEeFgI9FSDCHhbRFR4eFdEVIAEeFtEVHh4V0RUgATk8K2gsPgFBXAFaQgE4QVxc/ocBOBYeASAV/sgVHh4AAgAA/2kD6ANRACcAMABKQEclJCMiGxoZGAgCARUUAQAEAwIQDw4HBgUEBwADA0cRAQMBRgACAQMBAgNtAAMAAQMAawABAQxIAAAADQBJLy4rKh8eGgQFFSsBFQcGBxcHJwYPASMnJicHJzcmLwE1NzY3JzcXNj8BMxcWFzcXBxYXBzQmIg4BFjI2A+i5Cgt4Zp8UHx6PGxUWoWV5CwjHxwcMeGWgDyAcjxwWGp5mdw0HolZ4VAJYdFoBpY4aGxedZHYKC8LFBwt3ZKAVGRyOHBUYn2R3CAzDwwcMdWScGxVjPFRUeFRUAAUAAAAAA94ClwAMABkAJgA8AEwAWkBXBgUDAwEKAAoBAG0HDgQNAgwGAAsKAAtrAAkACgEJCmAACwgIC1QACwsIWAAICwhMGxoODQEAS0hDQDs4MzAuLSgnISAaJhsmFBMNGQ4ZBwYADAEMDwUUKyUiJjc1NDYyFhcVFAYnIiY9ATQ2MhYdARQGJSImNzU0NjIWHQEUBgEyFhcVFAYnFAYnISImJxE0NjMhMhYDETQmJyEiBhcRFBYzITI2AW0VIAEeLBwBHrEVICAqHh4BIxUgAR4sHh4BViw8AT4rXED9w0FaAVxAAj1BWmceFv3DFSABHhYCPRUgwh4W0RUeHhXRFSABHhbRFR4eFdEVIAEeFtEVHh4V0RUgATk8K2gsPgFBXAFaQgE4QVxc/ocBOBYeASAV/sgVHh4AAAAGAAAAAAPeApcADAAZACYAMwBJAFkAZUBiCAcFAwQBDAAMAQBtCREGEAQPAg4IAA0MAA1rAAsADAELDGAADQoKDVQADQ0KWAAKDQpMKCcbGg4NAQBYVVBNSEVAPTs6NTQuLSczKDMhIBomGyYUEw0ZDhkHBgAMAQwSBRQrJSImNzU0NjIWFxUUBiciJj0BNDYyFh0BFAYlIiYnNTQ2MhYdARQGJyImNzU0NjIWHQEUBgEyFhcVFAYnFAYnISImJxE0NjMhMhYDETQmJyEiBhcRFBYzITI2AW0VIAEeLBwBHrEVICAqHh4BwBYeASAqHh6yFSABHiweHgFWLDwBPitcQP3DQVoBXEACPUFaZx4W/cMVIAEeFgI9FSDCHhbRFR4eFdEVIAEeFtEVHh4V0RUgAR4W0RUeHhXRFSABHhbRFR4eFdEVIAE5PCtoLD4BQVwBWkIBOEFcXP6HATgWHgEgFf7IFR4eAAACAAD/ugNIAwIACAAUAChAJRQTEhEQDw4NDAsKCwEAAUcCAQABAG8AAQFmAQAFBAAIAQgDBRQrATIWEAYgJhA2ATcnBycHFwcXNxc3AaSu9vb+pPb2AQSaVpqYWJqaWJiaVgMC9v6k9vYBXPb+XJpWmJhWmphWmJhWAAAAAwAA/20D6ANPAAUADgAWACpAJwkBAQABRxMSCgMEAEUWDgQDAUQAAAEAbwIBAQFmAAAABQAFEQMFFSs1ETMBEQElNjQnNxYXFAcXNhAnNxYQB+wBYv6eAaBJSUdpAmsve3tMmpqOAaABIfweASEjSsxMSmqUkWUvdwFge0qa/kyaAAAAAQAA/2oD6ANSAAsALkArAgEAAQMBAANtBgUCAwQBAwRrAAEBDEgABAQNBEkAAAALAAsREREREQcFGSs1ESERIREhESERIREBZwEaAWf+mf7m0QEaAWf+mf7m/pkBZwAAAwAA/2oCMANSABsAKABiAEVAQjUyAgIDNgEEAlhNAgAGA0cABQQGBAUGbQAGAAQGAGsAAgAEBQIEYAADAwFYAAEBDEgAAAANAElTUhobJCcdGgcFGisBFA4BFB4BHQEUBiImPQE0PgE0LgE9ATQ2MhYVBQcGFxYzMjc2JyYjIhM0PgI/ATY1NwYiJxcUHwMWJhYjFA4CDwIGJgY1Bh0BPgI1NDIVFB4BFzU0LwImLwEuAQIwYGJiYKzYrGBiYmCu1K7+HhIECFx8hFgOHmBqeJAIHAwZHVwCZPRkBFotExERDB4MAgoGCAwPDwIiWgh0RDRCegZcKxINBQwHBAJuLGhePFxmLnYiTk4idi5mXDxeaCx2IE5OIAYOCAY0MgoUNv5KEh4kDhgcXB4yNjYyIForExUVAjAKEhIOCg8QEAIiAVogQgQmMCIeHiIwJgRCHlwpEw4IFAwWAAAADQAA/2oDoQNSAAgAEQAaACMALAA1AD4ARwBTAFwAbAB1AIUAgUB+XQEVFG1UPy0ECwo2JBIDBQQDRwAVFhIOAwoLFQpgFxMPAwsQDAgDBAULBGANCQIFBgICAAEFAGAAFBQZWAAZGQxIEQcDAwEBGFgAGBgNGEmEgXx5dHNwb2toY2BbWldWUlFMS0ZFQkE9PDk4NDMwLysqFBMUExQTFBMSGgUdKxc0JiIGHgE+ATc0JiIOARY+ASc0JiIGHgI2BTQmIg4BFj4BJzQmIg4BHgE2JzQmIgYeAjYFNCYiDgEeATYnNCYiDgEeATYBNTQuAQYHFRQeATYDNCYiDgEeATY3NTQmIyEiBh0BFBYzITI2BzQmIgYeAjYTERQGIyEiJjURNDYzITIW1io6LAIoPibZKjwoAiw4LtkqOiwCKD4mAa8qPCgCLDgu2Co8KAIsOC7ZKjosAig+JgGvKjwoAiw4LtgqPCgCLDguAaoqOioBLDgs1yo8KAIsOC7UFBD9Ng4WFg4Cyg8WASo6LAIoPiZKLBz87h0qKh0DEh0qBx0qKjosAigfHSoqOiwCKPUeKio8KAIsuh0qKjosAij1HioqPCgCLPIeKio8KAIsuh4qKjwoAizyHioqPCgCLP5w1h0qAi4b1h0qAi4Bxx4qKjwoAizPjw4WFg6PDhYWpR4qKjwoAiwBgvymHSoqHQNaHSoqAAIAAP/4AjsDLwAWAC8AJEAhAAMAA28AAAEAbwABAgIBVAABAQJYAAIBAkwcFBoZBAUYKyU0JyIvAS4BJyYiBw4CDwEGFRQWMjYlFA4BJic0NzY/AT4BNz4BHgEXHgMXFgEeCwEIDgYQBAIUAQQQDAgJCyo6LAEcpu6mAS0EHzgZPg8FHB4cBBA+MEADLc8UEwwVCSAMCQkNHhQLDBMUHSoqZXemAqp1UUgFLlQmejQQFAIQEjR6TFwFRwAAAgAA/7EEdwMLAAUAHwBLQEgYCwIEBRcSEAMDBBEBAgMDRwABBQFvAAUEBW8ABAMEbwADAgNvBgECAAACUgYBAgIAVgAAAgBKAAAdGxUUDg0ABQAFEREHBRYrBRUhETMRARUUBi8BAQYiLwEHJwE2Mh8BAScmNjsBMhYEd/uJRwPoFApE/p8GDgaC6GsBRwUOBoIBA0MJCA3zBwoHSANa/O4CuPIMCglE/p8GBoLpbAFGBgaCAQNECBYKAAAD//wAAARHAmoAEQAvAFoAZkBjBAEKAgFHAAMKCQoDCW0ABwkFCQcFbQwBBAsBAgoEAmAACgAJBwoJYAgBBQAABVQIAQUFAFgGDgENBAAFAEwUEgEAV1VOTUlIREI/Pjo5NTQsKiYlIB8bGhIvFC8AEQERDwUUKzciJjcRBwYuATY/ATYWFxEUBikBIiY/ATY0JiIGFRQGIiY1NDc2MhYUDwEzMhYOAQEWFRQOASY3NDYyFgcUFjI2NCYHIiY0NjcyPgEmJyIHDgEuATc2MzIWBxSdFSABHRQqEg4UZxwwASABwP78IR4Z0RQoOioeKiA0MpJlM3iHFSACHAGHN2SKZgEgKCIBJjYmJhsVICAVEBYCGg4ZCgoqJBALKlY7VAFZIBUBTA8KDigqCDMOIhr+YBUgQBnRFDsoJx8WHh4WSDMyZZAzeB4qIAElM0lGYgJmRBUgIBUbJiY2KAEeLBwCFiIUAhYSDhYoE05WOi4AAAUAAP/5A+QDCwAGAA8AOQA+AEgBB0AVQD47EAMCAQcABDQBAQACR0EBBAFGS7AKUFhAMAAHAwQDBwRtAAAEAQEAZQADAAQAAwRgCAEBAAYFAQZfAAUCAgVUAAUFAlgAAgUCTBtLsAtQWEApAAAEAQEAZQcBAwAEAAMEYAgBAQAGBQEGXwAFAgIFVAAFBQJYAAIFAkwbS7AXUFhAMAAHAwQDBwRtAAAEAQEAZQADAAQAAwRgCAEBAAYFAQZfAAUCAgVUAAUFAlgAAgUCTBtAMQAHAwQDBwRtAAAEAQQAAW0AAwAEAAMEYAgBAQAGBQEGXwAFAgIFVAAFBQJYAAIFAkxZWVlAFgAAREM9PDEuKSYeGxYTAAYABhQJBRUrJTcnBxUzFQEmDwEGFj8BNhMVFAYjISImNRE0NjchMhceAQ8BBicmIyEiBgcRFBYXITI2PQE0PwE2FgMXASM1AQcnNzYyHwEWFAHwQFVANQEVCQnECRIJxAkkXkP+MENeXkMB0CMeCQMHGwgKDQz+MCU0ATYkAdAlNAUkCBg3of6JoQJvM6EzECwQVRC9QVVBHzYBkgkJxAkSCcQJ/r5qQ15eQwHQQl4BDgQTBhwIBAM0Jf4wJTQBNiRGBwUkCAgBj6D+iaABLjShNA8PVRAsAAIAAP/5AoMDCwAHAB8AKkAnBQMCAAECAQACbQACAm4ABAEBBFQABAQBWAABBAFMIxMlNhMQBgUaKxMhNTQmDgEXBREUBgchIiYnETQ2FzM1NDYyFgcVMzIWswEdVHZUAQHQIBb96RceASAWEZTMlgISFx4BpWw7VAJQPaH+vhYeASAVAUIWIAFsZpSUZmweAAEAAP/5A6EDDAAlADBALQQBAgEAAQIAbQAAAwEAA2sAAwNuAAUBAQVUAAUFAVgAAQUBTBMlNSMVJAYFGisBFRQGByMiJj0BNCYOAQcVMzIWFxEUBgchIiYnETQ2FyE1ND4BFgOhFg4kDhZSeFIBNRceASAW/ekXHgEgFgF3ktCQAhGPDxQBFg6PO1QCUD1sHhf+vhYeASAVAUIWIAFsZ5IClgAABgAA/7EDEgMLAA8AHwAvADsAQwBnAGRAYVdFAgYIKSEZEQkBBgABAkcFAwIBBgAGAQBtBAICAAcGAAdrAA4ACQgOCWAPDQIIDAoCBgEIBl4ABwsLB1QABwcLWAALBwtMZWRhXltZU1JPTElHQT8UJBQmJiYmJiMQBR0rAREUBisBIiY1ETQ2OwEyFhcRFAYrASImNRE0NjsBMhYXERQGKwEiJjURNDY7ATIWExEhERQeATMhMj4BATMnJicjBgcFFRQGKwERFAYjISImJxEjIiY9ATQ2OwE3PgE3MzIWHwEzMhYBHgoIJAgKCggkCAqPCggkCAoKCCQICo4KByQICgoIJAcKSP4MCAgCAdACCAj+ifobBAWxBgQB6woINjQl/jAlNAE1CAoKCKwnCSwWshcqCSetCAoBt/6/CAoKCAFBCAoKCP6/CAoKCAFBCAoKCP6/CAoKCAFBCAoK/mQCEf3vDBQKChQCZUEFAQEFUyQICv3vLkRCLgITCggkCApdFRwBHhRdCgABAAAAAQAAO3t8yl8PPPUACwPoAAAAANgbenoAAAAA2Bt6ev/8/2kEdwNSAAAACAACAAAAAAAAAAEAAANS/2oAAAR2//z//wR3AAEAAAAAAAAAAAAAAAAAAAAWA+gAAAOp//8CggAAAoIAAAOqAAAD3gAAA94AAAPoAAAD3gAAA94AAANIAAAD6AAAA+gAAAIwAAAD6AAAAjsAAAR2AAAER//8A+gAAAKCAAADoAAAAxEAAAAAAAAAuAEYAXgB1gJGAswDQAPeBJIE0AUSBUIF8Ab6B1gHtghuCWQJrAn+CsIAAAABAAAAFgCGAA0AAAAAAAIANABEAHMAAACSC3AAAAAAAAAAEgDeAAEAAAAAAAAANQAAAAEAAAAAAAEACAA1AAEAAAAAAAIABwA9AAEAAAAAAAMACABEAAEAAAAAAAQACABMAAEAAAAAAAUACwBUAAEAAAAAAAYACABfAAEAAAAAAAoAKwBnAAEAAAAAAAsAEwCSAAMAAQQJAAAAagClAAMAAQQJAAEAEAEPAAMAAQQJAAIADgEfAAMAAQQJAAMAEAEtAAMAAQQJAAQAEAE9AAMAAQQJAAUAFgFNAAMAAQQJAAYAEAFjAAMAAQQJAAoAVgFzAAMAAQQJAAsAJgHJQ29weXJpZ2h0IChDKSAyMDE4IGJ5IG9yaWdpbmFsIGF1dGhvcnMgQCBmb250ZWxsby5jb21mb250ZWxsb1JlZ3VsYXJmb250ZWxsb2ZvbnRlbGxvVmVyc2lvbiAxLjBmb250ZWxsb0dlbmVyYXRlZCBieSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABDACkAIAAyADAAMQA4ACAAYgB5ACAAbwByAGkAZwBpAG4AYQBsACAAYQB1AHQAaABvAHIAcwAgAEAAIABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBmAG8AbgB0AGUAbABsAG8AUgBlAGcAdQBsAGEAcgBmAG8AbgB0AGUAbABsAG8AZgBvAG4AdABlAGwAbABvAFYAZQByAHMAaQBvAG4AIAAxAC4AMABmAG8AbgB0AGUAbABsAG8ARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXAAxoZWxwLWNpcmNsZWQPYW5nbGUtZG91YmxlLXVwEWFuZ2xlLWRvdWJsZS1kb3duBG1lbnUKYmF0dGVyeS0yNQpiYXR0ZXJ5LTUwA2NvZwpiYXR0ZXJ5LTc1C2JhdHRlcnktMTAwDmNhbmNlbC1jaXJjbGVkBnZvbHVtZQRwbHVzCWhvdXJnbGFzcwRjYWxjBHRpbnQKY2hhcnQtbGluZQxzb3J0LW51bWVyaWMEZWRpdARsb2NrCWxvY2stb3Blbgt0cmFzaC1lbXB0eQAAAAAAAAEAAf//AA8AAAAAAAAAAAAAAAAAAAAAABgAGAAYABgDUv9pA1L/abAALCCwAFVYRVkgIEu4AA5RS7AGU1pYsDQbsChZYGYgilVYsAIlYbkIAAgAY2MjYhshIbAAWbAAQyNEsgABAENgQi2wASywIGBmLbACLCBkILDAULAEJlqyKAEKQ0VjRVJbWCEjIRuKWCCwUFBYIbBAWRsgsDhQWCGwOFlZILEBCkNFY0VhZLAoUFghsQEKQ0VjRSCwMFBYIbAwWRsgsMBQWCBmIIqKYSCwClBYYBsgsCBQWCGwCmAbILA2UFghsDZgG2BZWVkbsAErWVkjsABQWGVZWS2wAywgRSCwBCVhZCCwBUNQWLAFI0KwBiNCGyEhWbABYC2wBCwjISMhIGSxBWJCILAGI0KxAQpDRWOxAQpDsAFgRWOwAyohILAGQyCKIIqwASuxMAUlsAQmUVhgUBthUllYI1khILBAU1iwASsbIbBAWSOwAFBYZVktsAUssAdDK7IAAgBDYEItsAYssAcjQiMgsAAjQmGwAmJmsAFjsAFgsAUqLbAHLCAgRSCwC0NjuAQAYiCwAFBYsEBgWWawAWNgRLABYC2wCCyyBwsAQ0VCKiGyAAEAQ2BCLbAJLLAAQyNEsgABAENgQi2wCiwgIEUgsAErI7AAQ7AEJWAgRYojYSBkILAgUFghsAAbsDBQWLAgG7BAWVkjsABQWGVZsAMlI2FERLABYC2wCywgIEUgsAErI7AAQ7AEJWAgRYojYSBksCRQWLAAG7BAWSOwAFBYZVmwAyUjYUREsAFgLbAMLCCwACNCsgsKA0VYIRsjIVkqIS2wDSyxAgJFsGRhRC2wDiywAWAgILAMQ0qwAFBYILAMI0JZsA1DSrAAUlggsA0jQlktsA8sILAQYmawAWMguAQAY4ojYbAOQ2AgimAgsA4jQiMtsBAsS1RYsQRkRFkksA1lI3gtsBEsS1FYS1NYsQRkRFkbIVkksBNlI3gtsBIssQAPQ1VYsQ8PQ7ABYUKwDytZsABDsAIlQrEMAiVCsQ0CJUKwARYjILADJVBYsQEAQ2CwBCVCioogiiNhsA4qISOwAWEgiiNhsA4qIRuxAQBDYLACJUKwAiVhsA4qIVmwDENHsA1DR2CwAmIgsABQWLBAYFlmsAFjILALQ2O4BABiILAAUFiwQGBZZrABY2CxAAATI0SwAUOwAD6yAQEBQ2BCLbATLACxAAJFVFiwDyNCIEWwCyNCsAojsAFgQiBgsAFhtRAQAQAOAEJCimCxEgYrsHIrGyJZLbAULLEAEystsBUssQETKy2wFiyxAhMrLbAXLLEDEystsBgssQQTKy2wGSyxBRMrLbAaLLEGEystsBsssQcTKy2wHCyxCBMrLbAdLLEJEystsB4sALANK7EAAkVUWLAPI0IgRbALI0KwCiOwAWBCIGCwAWG1EBABAA4AQkKKYLESBiuwcisbIlktsB8ssQAeKy2wICyxAR4rLbAhLLECHistsCIssQMeKy2wIyyxBB4rLbAkLLEFHistsCUssQYeKy2wJiyxBx4rLbAnLLEIHistsCgssQkeKy2wKSwgPLABYC2wKiwgYLAQYCBDI7ABYEOwAiVhsAFgsCkqIS2wKyywKiuwKiotsCwsICBHICCwC0NjuAQAYiCwAFBYsEBgWWawAWNgI2E4IyCKVVggRyAgsAtDY7gEAGIgsABQWLBAYFlmsAFjYCNhOBshWS2wLSwAsQACRVRYsAEWsCwqsAEVMBsiWS2wLiwAsA0rsQACRVRYsAEWsCwqsAEVMBsiWS2wLywgNbABYC2wMCwAsAFFY7gEAGIgsABQWLBAYFlmsAFjsAErsAtDY7gEAGIgsABQWLBAYFlmsAFjsAErsAAWtAAAAAAARD4jOLEvARUqLbAxLCA8IEcgsAtDY7gEAGIgsABQWLBAYFlmsAFjYLAAQ2E4LbAyLC4XPC2wMywgPCBHILALQ2O4BABiILAAUFiwQGBZZrABY2CwAENhsAFDYzgtsDQssQIAFiUgLiBHsAAjQrACJUmKikcjRyNhIFhiGyFZsAEjQrIzAQEVFCotsDUssAAWsAQlsAQlRyNHI2GwCUMrZYouIyAgPIo4LbA2LLAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjILAIQyCKI0cjRyNhI0ZgsARDsAJiILAAUFiwQGBZZrABY2AgsAErIIqKYSCwAkNgZCOwA0NhZFBYsAJDYRuwA0NgWbADJbACYiCwAFBYsEBgWWawAWNhIyAgsAQmI0ZhOBsjsAhDRrACJbAIQ0cjRyNhYCCwBEOwAmIgsABQWLBAYFlmsAFjYCMgsAErI7AEQ2CwASuwBSVhsAUlsAJiILAAUFiwQGBZZrABY7AEJmEgsAQlYGQjsAMlYGRQWCEbIyFZIyAgsAQmI0ZhOFktsDcssAAWICAgsAUmIC5HI0cjYSM8OC2wOCywABYgsAgjQiAgIEYjR7ABKyNhOC2wOSywABawAyWwAiVHI0cjYbAAVFguIDwjIRuwAiWwAiVHI0cjYSCwBSWwBCVHI0cjYbAGJbAFJUmwAiVhuQgACABjYyMgWGIbIVljuAQAYiCwAFBYsEBgWWawAWNgIy4jICA8ijgjIVktsDossAAWILAIQyAuRyNHI2EgYLAgYGawAmIgsABQWLBAYFlmsAFjIyAgPIo4LbA7LCMgLkawAiVGUlggPFkusSsBFCstsDwsIyAuRrACJUZQWCA8WS6xKwEUKy2wPSwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xKwEUKy2wPiywNSsjIC5GsAIlRlJYIDxZLrErARQrLbA/LLA2K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrErARQrsARDLrArKy2wQCywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixKwEUKy2wQSyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbErARQrLbBCLLA1Ky6xKwEUKy2wQyywNishIyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=') format('truetype'); } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ @@ -17,7 +17,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?87658047#fontello') format('svg'); + src: url('../font/fontello.svg?73205958#fontello') format('svg'); } } */ @@ -68,4 +68,8 @@ .icon-calc:before { content: '\e80d'; } /* '' */ .icon-tint:before { content: '\e80e'; } /* '' */ .icon-chart-line:before { content: '\e80f'; } /* '' */ -.icon-sort-numeric:before { content: '\e810'; } /* '' */ \ No newline at end of file +.icon-sort-numeric:before { content: '\e810'; } /* '' */ +.icon-edit:before { content: '\e811'; } /* '' */ +.icon-lock:before { content: '\e812'; } /* '' */ +.icon-lock-open:before { content: '\e813'; } /* '' */ +.icon-trash-empty:before { content: '\e814'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/css/fontello-ie7-codes.css b/static/glyphs/css/fontello-ie7-codes.css old mode 100644 new mode 100755 index 6cc26c1385a..78423d3d1d6 --- a/static/glyphs/css/fontello-ie7-codes.css +++ b/static/glyphs/css/fontello-ie7-codes.css @@ -15,4 +15,8 @@ .icon-calc { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-tint { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -.icon-sort-numeric { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file +.icon-sort-numeric { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-trash-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file diff --git a/static/glyphs/css/fontello-ie7.css b/static/glyphs/css/fontello-ie7.css old mode 100644 new mode 100755 index 4770e782110..00e81dcf09c --- a/static/glyphs/css/fontello-ie7.css +++ b/static/glyphs/css/fontello-ie7.css @@ -26,4 +26,8 @@ .icon-calc { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-tint { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -.icon-sort-numeric { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file +.icon-sort-numeric { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-trash-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } \ No newline at end of file diff --git a/static/glyphs/css/fontello.css b/static/glyphs/css/fontello.css old mode 100644 new mode 100755 index 1bdd8aedcc4..6548310e8e0 --- a/static/glyphs/css/fontello.css +++ b/static/glyphs/css/fontello.css @@ -1,10 +1,11 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?87142642'); - src: url('../font/fontello.eot?87142642#iefix') format('embedded-opentype'), - url('../font/fontello.woff?87142642') format('woff'), - url('../font/fontello.ttf?87142642') format('truetype'), - url('../font/fontello.svg?87142642#fontello') format('svg'); + src: url('../font/fontello.eot?50735338'); + src: url('../font/fontello.eot?50735338#iefix') format('embedded-opentype'), + url('../font/fontello.woff2?50735338') format('woff2'), + url('../font/fontello.woff?50735338') format('woff'), + url('../font/fontello.ttf?50735338') format('truetype'), + url('../font/fontello.svg?50735338#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -14,7 +15,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?87142642#fontello') format('svg'); + src: url('../font/fontello.svg?50735338#fontello') format('svg'); } } */ @@ -70,4 +71,8 @@ .icon-calc:before { content: '\e80d'; } /* '' */ .icon-tint:before { content: '\e80e'; } /* '' */ .icon-chart-line:before { content: '\e80f'; } /* '' */ -.icon-sort-numeric:before { content: '\e810'; } /* '' */ \ No newline at end of file +.icon-sort-numeric:before { content: '\e810'; } /* '' */ +.icon-edit:before { content: '\e811'; } /* '' */ +.icon-lock:before { content: '\e812'; } /* '' */ +.icon-lock-open:before { content: '\e813'; } /* '' */ +.icon-trash-empty:before { content: '\e814'; } /* '' */ \ No newline at end of file diff --git a/static/glyphs/demo.html b/static/glyphs/demo.html old mode 100644 new mode 100755 index 3ba8dbc7164..0ec1b165923 --- a/static/glyphs/demo.html +++ b/static/glyphs/demo.html @@ -229,11 +229,11 @@ } @font-face { font-family: 'fontello'; - src: url('./font/fontello.eot?65044999'); - src: url('./font/fontello.eot?65044999#iefix') format('embedded-opentype'), - url('./font/fontello.woff?65044999') format('woff'), - url('./font/fontello.ttf?65044999') format('truetype'), - url('./font/fontello.svg?65044999#fontello') format('svg'); + src: url('./font/fontello.eot?92660326'); + src: url('./font/fontello.eot?92660326#iefix') format('embedded-opentype'), + url('./font/fontello.woff?92660326') format('woff'), + url('./font/fontello.ttf?92660326') format('truetype'), + url('./font/fontello.svg?92660326#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -275,11 +275,11 @@ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } - + - - - - - - - - diff --git a/static/js/client.js b/static/js/client.js index c29ebf8b204..179edcf68df 100644 --- a/static/js/client.js +++ b/static/js/client.js @@ -1,7 +1,11 @@ 'use strict'; -if (serverSettings === undefined) { - console.error('server settings were not loaded, will not call init'); -} else { - window.Nightscout.client.init(serverSettings, Nightscout.plugins); -} +$(document).on('online', function() { + console.log('Application got online event, reloading'); + window.location.reload(); +}); + +$(document).ready(function() { + console.log('Application got ready event'); + window.Nightscout.client.init(); +}); \ No newline at end of file diff --git a/static/js/foodinit.js b/static/js/foodinit.js new file mode 100644 index 00000000000..d2e90ddcc23 --- /dev/null +++ b/static/js/foodinit.js @@ -0,0 +1,6 @@ +'use strict'; + +$(document).ready(function() { + console.log('Application got ready event'); + window.Nightscout.foodclient(); +}); diff --git a/static/js/profileinit.js b/static/js/profileinit.js new file mode 100644 index 00000000000..470f8bf623a --- /dev/null +++ b/static/js/profileinit.js @@ -0,0 +1,6 @@ +'use strict'; + +$(document).ready(function() { + console.log('Application got ready event'); + window.Nightscout.profileclient(); +}); diff --git a/static/js/reportinit.js b/static/js/reportinit.js new file mode 100644 index 00000000000..f88c05647d5 --- /dev/null +++ b/static/js/reportinit.js @@ -0,0 +1,6 @@ +'use strict'; + +$(document).ready(function() { + console.log('Application got ready event'); + window.Nightscout.reportclient(); +}); diff --git a/static/report/css/compare.css b/static/report/css/compare.css new file mode 100644 index 00000000000..1be8cac1e39 --- /dev/null +++ b/static/report/css/compare.css @@ -0,0 +1,97 @@ + +@import url("../../mfb/mfb.min.css"); +#chartContainer, #toolbar, #view, #templates { + margin: 0 5%; + width: 80%; + position: relative; + min-height: 1em; +} + +#chartContainer svg { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + + +.reticle .controls>span, +.reticle .controls .begin, +.reticle .controls .end { + display: block; + float: left; + margin: 0; + +} +.reticle .controls .interstice { + width: 100%; + text-align: center; + line-height: 120%; + font-size: 200%; +} +.reticle .controls .begin { + position: absolute; + left: 0; +} +.reticle .controls .end { + position: absolute; + right: 0; +} + +.reticle .controls INPUT { + line-height: 120%; + font-size: 200%; +} +.reticle .controls INPUT.end-input { + text-align: right; +} + +#menu_holder { +} + +#menu { +} + +.reticle .timeline { + min-height: 8em; + border: 1px solid pink; +} + +.axis path, +.axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.pool { + min-height: 500px; + border: 1px solid blue; +} + +#templates { + display: none; +} + +.debug #templates { + display: block; +} + +#view.collapse .pool { + position: absolute; + left: 0; + top: 0; + right: 0; + border: 1px solid silver; +} + +#view .observations { + position: relative; + padding-bottom: 500px; +} + +#view .gridlines line { + stroke: silver; + stroke-opacity: 60%; +} + diff --git a/static/report/index.html b/static/report/index.html deleted file mode 100644 index 4b6c5e27560..00000000000 --- a/static/report/index.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - Nightscout reporting - - - - - - - - - - - - - - - - - - - - - - - - - -

    Nightscout reporting

    -
      -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - From: - To: - Today - Last 2 days - Last 3 days - Last week - Last 2 weeks - Last month - Last 3 months -
    - Notes contain: -
    - Event Type: -
    - Mo - Tu - We - Th - Fr - Sa - Su -
    - Target bg range bottom: - - top: - -
    - Order: - - -   - - -
    - -
    -
    -
    - -
    - Authentication status: - - - - - - - - - - - - - - - diff --git a/static/report/js/flotcandle.js b/static/report/js/flotcandle.js index 3ac641d1cf5..cdf6a5faa0a 100644 --- a/static/report/js/flotcandle.js +++ b/static/report/js/flotcandle.js @@ -84,4 +84,4 @@ name: 'candle', version: '1.0' }); -})(jQuery); \ No newline at end of file +})($); diff --git a/static/report/js/loopalyzer.js b/static/report/js/loopalyzer.js new file mode 100644 index 00000000000..ebe434f2e64 --- /dev/null +++ b/static/report/js/loopalyzer.js @@ -0,0 +1,52 @@ +// Moves backward one day +function loopalyzerBackward() { + var moment = window.moment; + var from = moment($("#rp_from").val()).subtract(1,'day'); + var to = moment($("#rp_to").val()).subtract(1,'day'); + $("#rp_from").val(from.format('YYYY-MM-DD')); + $("#rp_to").val(to.format('YYYY-MM-DD')); + $("#rp_show").click(); +} + +// Moves backward same amount as shown (e.g. whole week) +function loopalyzerMoreBackward() { + var moment = window.moment; + var from = moment($("#rp_from").val()) + var to = moment($("#rp_to").val()) + var diff = to.diff(from, 'days') + 1; + from.subtract(diff, 'days'); + to.subtract(diff, 'days'); + $("#rp_from").val(from.format('YYYY-MM-DD')); + $("#rp_to").val(to.format('YYYY-MM-DD')); + $("#rp_show").click(); +} + +// Moves forward one day +function loopalyzerForward() { + var moment = window.moment; + var from = moment($("#rp_from").val()).add(1,'day'); + var to = moment($("#rp_to").val()).add(1,'day'); + if (to <= moment()) { + $("#rp_from").val(from.format('YYYY-MM-DD')); + $("#rp_to").val(to.format('YYYY-MM-DD')); + $("#rp_show").click(); + } +} + +// Moves forward same amount as shown (e.g. whole week) +function loopalyzerMoreForward() { + var moment = window.moment; + var from = moment($("#rp_from").val()) + var to = moment($("#rp_to").val()) + var diff = to.diff(from, 'days') + 1; + from.add(diff, 'days'); + to.add(diff, 'days'); + if (to > moment()) { + to = moment(); + from = moment(); + from.subtract(diff-1, 'days'); + } + $("#rp_from").val(from.format('YYYY-MM-DD')); + $("#rp_to").val(to.format('YYYY-MM-DD')); + $("#rp_show").click(); +} diff --git a/static/report/js/report.js b/static/report/js/report.js deleted file mode 100644 index 3cae18ca326..00000000000 --- a/static/report/js/report.js +++ /dev/null @@ -1,668 +0,0 @@ -// TODO: -// - bypass nightmode in reports -// - on save/delete treatment ctx.bus.emit('data-received'); is not enough. we must add something like 'data-updated' - -(function () { - 'use strict'; - //for the tests window isn't the global object - var $ = window.$; - var _ = window._; - var moment = window.moment; - var Nightscout = window.Nightscout; - var client = Nightscout.client; - var report_plugins = Nightscout.report_plugins; - - if (serverSettings === undefined) { - console.error('server settings were not loaded, will not call init'); - } else { - client.init(serverSettings, Nightscout.plugins); - } - - // init HTML code - report_plugins.addHtmlFromPlugins( client ); - // make show() accessible outside for treatments.js - report_plugins.show = show; - - var translate = client.translate; - - var maxInsulinValue = 0 - ,maxCarbsValue = 0; - var maxdays = 3 * 31; - var datastorage = {}; - var daystoshow = {}; - var sorteddaystoshow = []; - - var targetBGdefault = { - 'mg/dl': { - low: client.settings.thresholds.bgTargetBottom - , high: client.settings.thresholds.bgTargetTop - } - , 'mmol': { - low: client.utils.scaleMgdl(client.settings.thresholds.bgTargetBottom) - , high: client.utils.scaleMgdl(client.settings.thresholds.bgTargetTop) - } - }; - - var ONE_MIN_IN_MS = 60000; - - prepareGUI(); - - // ****** FOOD CODE START ****** - var food_categories = []; - var food_list = []; - - var filter = { - category: '' - , subcategory: '' - , name: '' - }; - - function fillFoodForm(event) { - $('#rp_category').empty().append(''); - Object.keys(food_categories).forEach(function eachCategory(s) { - $('#rp_category').append(''); - }); - filter.category = ''; - fillFoodSubcategories(); - - $('#rp_category').change(fillFoodSubcategories); - $('#rp_subcategory').change(doFoodFilter); - $('#rp_name').on('input',doFoodFilter); - - return maybePrevent(event); - } - - function fillFoodSubcategories(event) { - filter.category = $('#rp_category').val(); - filter.subcategory = ''; - $('#rp_subcategory').empty().append(''); - if (filter.category !== '') { - Object.keys(food_categories[filter.category]).forEach(function eachSubCategory(s) { - $('#rp_subcategory').append(''); - }); - } - doFoodFilter(); - return maybePrevent(event); - } - - function doFoodFilter(event) { - if (event) { - filter.category = $('#rp_category').val(); - filter.subcategory = $('#rp_subcategory').val(); - filter.name = $('#rp_name').val(); - } - $('#rp_food').empty(); - for (var i=0; i' + o + ''); - } - - return maybePrevent(event); - } - - $('#info').html(''+translate('Loading food database')+' ...'); - $.ajax('/api/v1/food/regular.json', { - success: function foodLoadSuccess(records) { - records.forEach(function (r) { - food_list.push(r); - if (r.category && !food_categories[r.category]) { food_categories[r.category] = {}; } - if (r.category && r.subcategory) { food_categories[r.category][r.subcategory] = true; } - }); - fillFoodForm(); - } - }).done(function() { - if (food_list.length) { - enableFoodGUI(); - } else { - disableFoodGUI(); - } - }).fail(function() { - disableFoodGUI(); - }); - - function enableFoodGUI( ) { - $('#info').html(''); - - $('.rp_foodgui').css('display',''); - $('#rp_food').change(function (event) { - $('#rp_enablefood').prop('checked',true); - return maybePrevent(event); - }); - } - - function disableFoodGUI(){ - $('#info').html(''); - $('.rp_foodgui').css('display','none'); - } - - // ****** FOOD CODE END ****** - - function prepareGUI() { - $('.presetdates').click(function(event) { - var days = $(this).attr('days'); - $('#rp_enabledate').prop('checked',true); - return setDataRange(event,days); - }); - $('#rp_show').click(show); - $('#rp_notes').bind('input', function (event) { - $('#rp_enablenotes').prop('checked',true); - return maybePrevent(event); - }); - $('#rp_eventtype').bind('input', function (event) { - $('#rp_enableeventtype').prop('checked',true); - return maybePrevent(event); - }); - - // fill careportal events - $('#rp_eventtype').empty(); - _.each(client.careportal.events, function eachEvent(event) { - $('#rp_eventtype').append(''); - }); - $('#rp_eventtype').append(''); - - $('#rp_targetlow').val(targetBGdefault[client.settings.units.toLowerCase()].low); - $('#rp_targethigh').val(targetBGdefault[client.settings.units.toLowerCase()].high); - - if (client.settings.scaleY === 'linear') { - $('#rp_linear').prop('checked', true); - } else { - $('#rp_log').prop('checked', true); - } - - $('.menutab').click(switchreport_handler); - - setDataRange(null,7); - } - - function sgvToColor(sgv,options) { - var color = 'darkgreen'; - - if (sgv > options.targetHigh) { - color = 'red'; - } else if (sgv < options.targetLow) { - color = 'red'; - } - - return color; - } - - function show(event) { - var options = { - width: 1000 - , height: 300 - , targetLow: 3.5 - , targetHigh: 10 - , raw: true - , notes: true - , food: true - , insulin: true - , carbs: true - , iob : true - , cob : true - , basal : true - , scale: report_plugins.consts.scaleYFromSettings(client) - , units: client.settings.units - }; - - // default time range if no time range specified in GUI - var zone = client.sbx.data.profile.getTimezone(); - var timerange = '&find[created_at][$gte]='+moment.tz('2000-01-01',zone).toISOString(); - //console.log(timerange,zone); - options.targetLow = parseFloat($('#rp_targetlow').val().replace(',','.')); - options.targetHigh = parseFloat($('#rp_targethigh').val().replace(',','.')); - options.raw = $('#rp_optionsraw').is(':checked'); - options.iob = $('#rp_optionsiob').is(':checked'); - options.cob = $('#rp_optionscob').is(':checked'); - options.basal = $('#rp_optionsbasal').is(':checked'); - options.notes = $('#rp_optionsnotes').is(':checked'); - options.food = $('#rp_optionsfood').is(':checked'); - options.insulin = $('#rp_optionsinsulin').is(':checked'); - options.carbs = $('#rp_optionscarbs').is(':checked'); - options.scale = ( $('#rp_linear').is(':checked') ? report_plugins.consts.SCALE_LINEAR : report_plugins.consts.SCALE_LOG ); - options.order = ( $('#rp_oldestontop').is(':checked') ? report_plugins.consts.ORDER_OLDESTONTOP : report_plugins.consts.ORDER_NEWESTONTOP ); - options.width = parseInt($('#rp_size :selected').attr('x')); - options.height = parseInt($('#rp_size :selected').attr('y')); - - var matchesneeded = 0; - - // date range - function datefilter() { - if ($('#rp_enabledate').is(':checked')) { - matchesneeded++; - var from = moment.tz($('#rp_from').val().replace(/\//g,'-') + 'T00:00:00',zone); - var to = moment.tz($('#rp_to').val().replace(/\//g,'-') + 'T23:59:59',zone); - timerange = '&find[created_at][$gte]='+from.toISOString()+'&find[created_at][$lt]='+to.toISOString(); - //console.log($('#rp_from').val(),$('#rp_to').val(),zone,timerange); - while (from <= to) { - if (daystoshow[from.format('YYYY-MM-DD')]) { - daystoshow[from.format('YYYY-MM-DD')]++; - } else { - daystoshow[from.format('YYYY-MM-DD')] = 1; - } - from.add(1, 'days'); - } - } - console.log('Dayfilter: ',daystoshow); - foodfilter(); - } - - //food filter - function foodfilter() { - if ($('#rp_enablefood').is(':checked')) { - matchesneeded++; - var _id = $('#rp_food').val(); - if (_id) { - var treatmentData; - var tquery = '?find[boluscalc.foods._id]=' + _id + timerange; - $.ajax('/api/v1/treatments.json'+tquery, { - success: function (xhr) { - treatmentData = xhr.map(function (treatment) { - return moment.tz(treatment.created_at,zone).format('YYYY-MM-DD'); - }); - // unique it - treatmentData = $.grep(treatmentData, function(v, k){ - return $.inArray(v ,treatmentData) === k; - }); - treatmentData.sort(function(a, b) { return a > b; }); - } - }).done(function () { - console.log('Foodfilter: ',treatmentData); - for (var d=0; d b; }); - } - }).done(function () { - console.log('Notesfilter: ',treatmentData); - for (var d=0; d b; }); - } - }).done(function () { - console.log('Eventtypefilter: ',treatmentData); - for (var d=0; d'+translate('Loading')+' ...'); - for (var d in daystoshow) { - if (daystoshow[d]===matchesneeded) { - if (count < maxdays) { - $('#info').append($('
    ')); - count++; - loadData(d, options, dataLoadedCallback); - } else { - $('#info').append($('
    '+d+' '+translate('not displayed')+'.
    ')); - delete daystoshow[d]; - } - } else { - delete daystoshow[d]; - } - } - if (count===0) { - $('#info').html(''+translate('Result is empty')+''); - $('#rp_show').css('display',''); - } - } - - var dayscount = 0; - var loadeddays = 0; - - function countDays() { - for (var d in daystoshow) { - if (daystoshow.hasOwnProperty(d)) { - if (daystoshow[d]===matchesneeded) { - if (dayscount < maxdays) { - dayscount++; - } - } - } - } - console.log('Total: ', daystoshow, 'Matches needed: ', matchesneeded, 'Will be loaded: ', dayscount); - } - - function dataLoadedCallback (day) { - loadeddays++; - sorteddaystoshow.push(day); - if (loadeddays === dayscount) { - sorteddaystoshow.sort(); - var from = sorteddaystoshow[0]; - if (options.order === report_plugins.consts.ORDER_NEWESTONTOP) { - sorteddaystoshow.reverse(); - } - loadTempBasals(from, function showreportscallback() { showreports(options); }); - } - } - - $('#rp_show').css('display','none'); - daystoshow = {}; - - datefilter(); - return maybePrevent(event); - } - - function showreports(options) { - // prepare some data used in more reports - datastorage.allstatsrecords = []; - datastorage.alldays = 0; - sorteddaystoshow.forEach(function eachDay(day) { - datastorage.allstatsrecords = datastorage.allstatsrecords.concat(datastorage[day].statsrecords); - datastorage.alldays++; - }); - options.maxInsulinValue = maxInsulinValue; - options.maxCarbsValue = maxCarbsValue; - - - report_plugins.eachPlugin(function (plugin) { - // jquery plot doesn't draw to hidden div - $('#'+plugin.name+'-placeholder').css('display',''); - //console.log('Drawing ',plugin.name); - plugin.report(datastorage,sorteddaystoshow,options); - if (!$('#'+plugin.name).hasClass('selected')) { - $('#'+plugin.name+'-placeholder').css('display','none'); - } - }); - - $('#info').html(''); - $('#rp_show').css('display',''); - } - - function setDataRange(event,days) { - $('#rp_to').val(moment().format('YYYY-MM-DD')); - $('#rp_from').val(moment().add(-days+1, 'days').format('YYYY-MM-DD')); - return maybePrevent(event); - } - - function switchreport_handler(event) { - var id = $(this).attr('id'); - - $('.menutab').removeClass('selected'); - $('#'+id).addClass('selected'); - - $('.tabplaceholder').css('display','none'); - $('#'+id+'-placeholder').css('display',''); - return maybePrevent(event); - } - - function loadData(day, options, callback) { - // check for loaded data - if (datastorage[day] && day !== moment().format('YYYY-MM-DD')) { - callback(day); - return; - } - // patientData = [actual, predicted, mbg, treatment, cal, devicestatusData]; - var data = {}; - var cgmData = [] - , mbgData = [] - , treatmentData = [] - , calData = [] - ; - var from; - if (client.sbx.data.profile.getTimezone()) { - from = moment(day).tz(client.sbx.data.profile.getTimezone()).startOf('day').format('x'); - } else { - from = moment(day).startOf('day').format('x'); - } - from = parseInt(from); - var to = from + 1000 * 60 * 60 * 24; - var query = '?find[date][$gte]='+from+'&find[date][$lt]='+to+'&count=10000'; - - $('#info-' + day).html(''+translate('Loading CGM data of')+' '+day+' ...'); - $.ajax('/api/v1/entries.json'+query, { - success: function (xhr) { - xhr.forEach(function (element) { - if (element) { - if (element.mbg) { - mbgData.push({ - y: element.mbg - , mills: element.date - , d: element.dateString - , device: element.device - }); - } else if (element.sgv) { - cgmData.push({ - y: element.sgv - , mills: element.date - , d: element.dateString - , device: element.device - , filtered: element.filtered - , unfiltered: element.unfiltered - , noise: element.noise - , rssi: element.rssi - , sgv: element.sgv - }); - } else if (element.type === 'cal') { - calData.push({ - mills: element.date - , d: element.dateString - , scale: element.scale - , intercept: element.intercept - , slope: element.slope - }); - } - } - }); - // sometimes cgm contains duplicates. uniq it. - data.sgv = cgmData.slice(); - data.sgv.sort(function(a, b) { return a.mills - b.mills; }); - var lastDate = 0; - data.sgv = data.sgv.filter(function(d) { - var ok = (lastDate + ONE_MIN_IN_MS) < d.mills; - lastDate = d.mills; - return ok; - }); - data.mbg = mbgData.slice(); - data.mbg.sort(function(a, b) { return a.mills - b.mills; }); - data.cal = calData.slice(); - data.cal.sort(function(a, b) { return a.mills - b.mills; }); - } - }).done(function () { - $('#info-' + day).html(''+translate('Loading treatments data of')+' '+day+' ...'); - var tquery = '?find[created_at][$gte]='+new Date(from).toISOString()+'&find[created_at][$lt]='+new Date(to).toISOString(); - $.ajax('/api/v1/treatments.json'+tquery, { - success: function (xhr) { - treatmentData = xhr.map(function (treatment) { - var timestamp = new Date(treatment.timestamp || treatment.created_at); - treatment.mills = timestamp.getTime(); - return treatment; - }); - data.treatments = treatmentData.slice(); - data.treatments.sort(function(a, b) { return a.mills - b.mills; }); - } - }).done(function () { - $('#info-' + day).html(''+translate('Processing data of')+' '+day+' ...'); - processData(data, day, options, callback); - }); - - }); - } - - function loadTempBasals(from, callback) { - $('#info-' + from).html(''+translate('Loading temp basal data') + ' ...'); - var tquery = '?find[created_at][$gte]='+moment(from).subtract(32, 'days').toISOString()+'&find[eventType][$eq]=Temp Basal'; - $.ajax('/api/v1/treatments.json'+tquery, { - success: function (xhr) { - var treatmentData = xhr.map(function (treatment) { - var timestamp = new Date(treatment.timestamp || treatment.created_at); - treatment.mills = timestamp.getTime(); - return treatment; - }); - datastorage.tempbasaltreatments = treatmentData.slice(); - datastorage.tempbasaltreatments.sort(function(a, b) { return a.mills - b.mills; }); - } - }).done(function () { - callback(); - }); - } - - function processData(data, day, options, callback) { - // treatments - data.treatments.forEach(function (d) { - if (parseFloat(d.insulin) > maxInsulinValue) { - maxInsulinValue = parseFloat(d.insulin); - } - if (parseFloat(d.carbs) > maxCarbsValue) { - maxCarbsValue = parseFloat(d.carbs); - } - }); - - var cal = data.cal[data.cal.length-1]; - var temp1 = [ ]; - var rawbg = Nightscout.plugins('rawbg'); - if (cal) { - temp1 = data.sgv.map(function (entry) { - entry.mgdl = entry.y; // value names changed from enchilada - var rawBg = rawbg.calc(entry, cal); - return { mills: entry.mills, date: new Date(entry.mills - 2 * 1000), y: rawBg, sgv: client.utils.scaleMgdl(rawBg), color: 'gray', type: 'rawbg', filtered: entry.filtered, unfiltered: entry.unfiltered }; - }).filter(function(entry) { return entry.y > 0}); - } - var temp2 = data.sgv.map(function (obj) { - return { mills: obj.mills, date: new Date(obj.mills), y: obj.y, sgv: client.utils.scaleMgdl(obj.y), color: sgvToColor(client.utils.scaleMgdl(obj.y),options), type: 'sgv', noise: obj.noise, filtered: obj.filtered, unfiltered: obj.unfiltered}; - }); - data.sgv = [].concat(temp1, temp2); - - //Add MBG's also, pretend they are SGV's - data.sgv = data.sgv.concat(data.mbg.map(function (obj) { return { date: new Date(obj.mills), y: obj.y, sgv: client.utils.scaleMgdl(obj.y), color: 'red', type: 'mbg', device: obj.device } })); - - // make sure data range will be exactly 24h - var from; - if (client.sbx.data.profile.getTimezone()) { - from = moment(day).tz(client.sbx.data.profile.getTimezone()).startOf('day').toDate(); - } else { - from = moment(day).startOf('day').toDate(); - } - var to = new Date(from.getTime() + 1000 * 60 * 60 * 24); - data.sgv.push({ date: from, y: 40, sgv: 40, color: 'transparent', type: 'rawbg'}); - data.sgv.push({ date: to, y: 40, sgv: 40, color: 'transparent', type: 'rawbg'}); - - // clear error data. we don't need it to display them - data.sgv = data.sgv.filter(function (d) { - if (d.y < 39) { - return false; - } - return true; - }); - - // for other reports - data.statsrecords = data.sgv.filter(function(r) { - if (r.type) { - return r.type === 'sgv'; - } else { - return true; - } - }).map(function (r) { - var ret = {}; - ret.sgv = parseFloat(r.sgv); - ret.bgValue = parseInt(r.y); - ret.displayTime = r.date; - return ret; - }); - - - datastorage[day] = data; - callback(day); - } - - function maybePrevent(event) { - if (event) { - event.preventDefault(); - } - return false; - } -})(); \ No newline at end of file diff --git a/static/robots.txt b/static/robots.txt index 1f53798bb4f..68ad19bb9f2 100644 --- a/static/robots.txt +++ b/static/robots.txt @@ -1,2 +1,2 @@ -User-agent: * -Disallow: / +User-agent: Browsershots +Disallow: diff --git a/swagger.yaml b/swagger.yaml deleted file mode 100644 index 3dce8801407..00000000000 --- a/swagger.yaml +++ /dev/null @@ -1,637 +0,0 @@ -swagger: '2.0' -info: - title: Nightscout API - description: Own your DData with the Nightscout API - version: "0.8.0" - license: - name: AGPL 3 - url: https://www.gnu.org/licenses/agpl.txt -basePath: /api/v1 -produces: - - application/json -security: - - api_secret: [] - -paths: - - /entries/{spec}: - get: - summary: All Entries matching query - description: | - The Entries endpoint returns information about the - Nightscout entries. - - parameters: - - name: spec - in: path - type: string - description: | - entry id, such as `55cf81bc436037528ec75fa5` or a type filter such - as `sgv`, `mbg`, etc. - - default: sgv - required: true - - name: find - in: query - description: | - The query used to find entries, support nested query syntax, for - example `find[dateString][$gte]=2015-08-27`. All find parameters - are interpreted as strings. - required: false - type: string - - name: count - in: query - description: Number of entries to return. - required: false - type: number - tags: - - Entries - responses: - "200": - description: An array of entries - schema: - $ref: '#/definitions/Entries' - default: - description: Entries - schema: - $ref: '#/definitions/Entries' - - /slice/{storage}/{field}/{type}/{prefix}/{regex}: - get: - summary: All Entries matching query - description: The Entries endpoint returns information about the Nightscout entries. - parameters: - - name: storage - in: path - type: string - description: Prefix to use in constructing a prefix-based regex, default is `entries`. - required: true - default: entries - - name: field - in: path - type: string - description: Name of the field to use Regex against in query object, default is `dateString`. - default: dateString - required: true - - name: type - in: path - type: string - description: The type field to search against, default is sgv. - required: true - default: sgv - - name: prefix - in: path - type: string - description: Prefix to use in constructing a prefix-based regex. - required: true - default: '2015' - - name: regex - in: path - type: string - description: | - Tail part of regexp to use in expanding/construccting a query object. - Regexp also has bash-style brace and glob expansion applied to it, - creating ways to search for modal times of day, perhaps using - something like this syntax: `T{15..17}:.*`, this would search for - all records from 3pm to 5pm. - required: true - default: .* - - name: find - in: query - description: | - The query used to find entries, support nested query syntax, for - example `find[dateString][$gte]=2015-08-27`. All find parameters - are interpreted as strings. - required: false - type: string - - name: count - in: query - description: Number of entries to return. - required: false - type: number - tags: - - Entries - responses: - "200": - description: An array of entries - schema: - $ref: '#/definitions/Entries' - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' - - /echo/{storage}/{spec}: - get: - summary: View generated Mongo Query object - description: | - Information about the mongo query object created by the query. - - parameters: - - name: storage - in: path - type: string - description: | - `entries`, or `treatments` to select the storage layer. - - default: sgv - required: true - - name: spec - in: path - type: string - description: | - entry id, such as `55cf81bc436037528ec75fa5` or a type filter such - as `sgv`, `mbg`, etc. - This parameter is optional. - - default: sgv - required: true - - name: find - in: query - description: | - The query used to find entries, support nested query syntax, for - example `find[dateString][$gte]=2015-08-27`. All find parameters - are interpreted as strings. - required: false - type: string - - name: count - in: query - description: Number of entries to return. - required: false - type: number - tags: - - Entries - - Debug - responses: - "200": - description: An array of entries - schema: - $ref: '#/definitions/MongoQuery' - - /times/echo/{prefix}/{regex}: - get: - summary: Echo the query object to be used. - description: Echo debug information about the query object constructed. - parameters: - - name: prefix - in: path - type: string - description: Prefix to use in constructing a prefix-based regex. - required: true - - name: regex - in: path - type: string - description: | - Tail part of regexp to use in expanding/construccting a query object. - Regexp also has bash-style brace and glob expansion applied to it, - creating ways to search for modal times of day, perhaps using - something like this syntax: `T{15..17}:.*`, this would search for - all records from 3pm to 5pm. - required: true - - name: find - in: query - description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. - required: false - type: string - - name: count - in: query - description: Number of entries to return. - required: false - type: number - tags: - - Entries - - Debug - responses: - "200": - description: An array of entries - schema: - $ref: '#/definitions/MongoQuery' - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' - - - /times/{prefix}/{regex}: - get: - summary: All Entries matching query - description: The Entries endpoint returns information about the Nightscout entries. - parameters: - - name: prefix - in: path - type: string - description: Prefix to use in constructing a prefix-based regex. - required: true - - name: regex - in: path - type: string - description: | - Tail part of regexp to use in expanding/construccting a query object. - Regexp also has bash-style brace and glob expansion applied to it, - creating ways to search for modal times of day, perhaps using - something like this syntax: `T{15..17}:.*`, this would search for - all records from 3pm to 5pm. - required: true - - name: find - in: query - description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. - required: false - type: string - - name: count - in: query - description: Number of entries to return. - required: false - type: number - tags: - - Entries - responses: - "200": - description: An array of entries - schema: - $ref: '#/definitions/Entries' - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' - - - /entries: - get: - summary: All Entries matching query - description: The Entries endpoint returns information about the Nightscout entries. - parameters: - - name: find - in: query - description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. - required: false - type: string - - name: count - in: query - description: Number of entries to return. - required: false - type: number - tags: - - Entries - responses: - "200": - description: An array of entries - schema: - $ref: '#/definitions/Entries' - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' - - post: - tags: - - Entries - summary: Add new entries. - description: "" - operationId: addEntries - consumes: - - application/json - - text/plain - produces: - - application/json - - text/plain - parameters: - - in: body - name: body - description: Entries to be uploaded. - required: true - schema: - $ref: "#/definitions/Entries" - responses: - "405": - description: Invalid input - "200": - description: Rejected list of entries. Empty list is success. - delete: - tags: - - Entries - summary: Delete entries matching query. - description: "Remove entries, same search syntax as GET." - operationId: remove - parameters: - - name: find - in: query - description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. - required: false - type: string - - name: count - in: query - description: Number of entries to return. - required: false - type: number - responses: - "200": - description: Empty list is success. - - /treatments: - get: - summary: Treatments - description: The Treatments endpoint returns information about the Nightscout treatments. - tags: - - Treatments - parameters: - - name: find - in: query - description: - The query used to find entries, supports nested query syntax. Examples - `find[insulin][$gte]=3` - `find[carb][$gte]=100` - `find[eventType]=Correction+Bolus` - All find parameters are interpreted as strings. - required: false - type: string - - name: count - in: query - description: Number of entries to return. - required: false - type: number - responses: - "200": - description: An array of treatments - schema: - $ref: '#/definitions/Treatments' - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' - post: - tags: - - Treatments - summary: Add new treatments. - description: "" - operationId: addTreatments - consumes: - - application/json - produces: - - application/json - parameters: - - in: body - name: body - description: Treatments to be uploaded. - required: true - schema: - $ref: "#/definitions/Treatments" - responses: - "405": - description: Invalid input - "200": - description: Rejected list of treatments. Empty list is success. - - /profile: - get: - summary: Profile - description: The Profile endpoint returns information about the Nightscout Treatment Profiles. - tags: - - Profile - responses: - "200": - description: An array of treatments - schema: - $ref: '#/definitions/Profile' - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' - - /status: - get: - summary: Status - description: Server side status, default settings and capabilities. - tags: - - Status - responses: - "200": - description: Server capabilities and status. - schema: - $ref: '#/definitions/Status' - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' - -securityDefinitions: - api_secret: - type: apiKey - name: api_secret - in: header - description: The hash of the API_SECRET env var - -definitions: - Entry: - properties: - type: - type: string - description: "sgv, mbg, cal, etc" - dateString: - type: string - description: dateString, prefer ISO `8601` - date: - type: number - description: Epoch - sgv: - type: number - description: The glucose reading. (only available for sgv types) - direction: - type: string - description: Direction of glucose trend reported by CGM. (only available for sgv types) - noise: - type: number - description: Noise level at time of reading. (only available for sgv types) - filtered: - type: number - description: The raw filtered value directly from CGM transmitter. (only available for sgv types) - unfiltered: - type: number - description: The raw unfiltered value directly from CGM transmitter. (only available for sgv types) - rssi: - type: number - description: The signal strength from CGM transmitter. (only available for sgv types) - - Entries: - type: array - items: - $ref: '#/definitions/Entry' - - Treatment: - properties: - _id: - type: string - description: Internally assigned id. - eventType: - type: string - description: The type of treatment event. - created_at: - type: string - description: The date of the event, might be set retroactively . - glucose: - type: string - description: Current glucose. - glucoseType: - type: string - description: Method used to obtain glucose, Finger or Sensor. - carbs: - type: number - description: Number of carbs. - insulin: - type: number - description: Amount of insulin, if any. - units: - type: string - description: The units for the glucose value, mg/dl or mmol. - notes: - type: string - description: Description/notes of treatment. - enteredBy: - type: string - description: Who entered the treatment. - - Treatments: - type: array - items: - $ref: '#/definitions/Treatment' - - Profile: - properties: - sens: - type: integer - description: 'Internally assigned id' - dia: - type: integer - description: 'Internally assigned id' - carbratio: - type: integer - description: 'Internally assigned id' - carbs_hr: - type: integer - description: 'Internally assigned id' - _id: - type: string - description: 'Internally assigned id' - - Status: - properties: - apiEnabled: - type: boolean - description: 'Whether or not the REST API is enabled.' - careportalEnabled: - type: boolean - description: 'Whether or not the careportal is enabled in the API.' - head: - type: string - description: 'The git identifier for the running instance of the app.' - name: - type: string - description: Nightscout (static) - version: - type: string - description: 'The version label of the app.' - settings: - $ref: '#/definitions/Settings' - extendedSettings: - $ref: '#/definitions/ExtendedSettings' - - Settings: - properties: - units: - type: string - description: Default units for glucose measurements across the server. - timeFormat: - type: string - description: Default time format - enum: - - 12 - - 24 - customTitle: - type: string - description: Default custom title to be displayed system wide. - nightMode: - type: boolean - description: Should Night mode be enabled by default? - theme: - type: string - description: Default theme to be displayed system wide, `default` or `colors`. - language: - type: string - description: Default language code to be used system wide - showPlugins: - type: string - description: Plugins that should be shown by default - showRawbg: - type: string - description: If Raw BG is enabled when should it be shown? `never`, `always`, `noise` - alarmTypes: - type: array - items: - type: string - enum: - - simple - - predict - description: Enabled alarm types, can be multiple - alarmUrgentHigh: - type: boolean - description: Enable/Disable client-side Urgent High alarms by default, for use with `simple` alarms. - alarmHigh: - type: boolean - description: Enable/Disable client-side High alarms by default, for use with `simple` alarms. - alarmLow: - type: boolean - description: Enable/Disable client-side Low alarms by default, for use with `simple` alarms. - alarmUrgentLow: - type: boolean - description: Enable/Disable client-side Urgent Low alarms by default, for use with `simple` alarms. - alarmTimeagoWarn: - type: boolean - description: Enable/Disable client-side stale data alarms by default. - alarmTimeagoWarnMins: - type: number - description: Number of minutes before a stale data warning is generated. - alarmTimeagoUrgent: - type: boolean - description: Enable/Disable client-side urgent stale data alarms by default. - alarmTimeagoUrgentMins: - type: number - description: Number of minutes before a stale data warning is generated. - enable: - type: array - items: - type: string - description: Enabled features - thresholds: - $ref: '#/definitions/Threshold' - - Threshold: - properties: - bg_high: - type: integer - description: 'High BG range.' - bg_target_top: - type: integer - description: 'Top of target range.' - bg_target_bottom: - type: integer - description: 'Bottom of target range.' - bg_low: - type: integer - description: 'Low BG range.' - - ExtendedSettings: - description: Extended settings of client side plugins - - MongoQuery: - description: Mongo Query object. - - - Error: - properties: - code: - type: integer - format: int32 - message: - type: string - fields: - type: object - diff --git a/testing/populate.js b/testing/populate.js index 61bbadf0342..c4d9e1422a1 100644 --- a/testing/populate.js +++ b/testing/populate.js @@ -3,25 +3,28 @@ var mongodb = require('mongodb'); var env = require('./../env')(); -var util = require('./helpers/util'); +var util = require('./util'); main(); function main() { var MongoClient = mongodb.MongoClient; - MongoClient.connect(env.mongo, function connected(err, db) { + MongoClient.connect(env.storageURI, { "useUnifiedTopology" : true, "useNewUrlParser" : true }, function connected(err, client) { console.log('Connecting to mongo...'); if (err) { console.log('Error occurred: ', err); throw err; } + + var db = client.db(); + populate_collection(db); }); } function populate_collection(db) { - var cgm_collection = db.collection(env.mongo_collection); + var cgm_collection = db.collection(env.entries_collection); var new_cgm_record = util.get_cgm_record(); cgm_collection.insert(new_cgm_record, function (err) { diff --git a/tests/XX_clean.test.js b/tests/XX_clean.test.js new file mode 100644 index 00000000000..9b921a01721 --- /dev/null +++ b/tests/XX_clean.test.js @@ -0,0 +1,39 @@ +'use strict'; + +var _ = require('lodash'); +var language = require('../lib/language')(); + +describe('Clean MONGO after tests', function ( ) { + this.timeout(10000); + var self = this; + + var api = require('../lib/api/'); + beforeEach(function (done) { + process.env.API_SECRET = 'this is my long pass phrase'; + self.env = require('../lib/server/env')(); + self.env.settings.authDefaultRoles = 'readable'; + self.env.settings.enable = ['careportal', 'api']; + this.wares = require('../lib/middleware/')(self.env); + self.app = require('express')(); + self.app.enable('api'); + require('../lib/server/bootevent')(self.env, language).boot(function booted(ctx) { + self.ctx = ctx; + self.ctx.ddata = require('../lib/data/ddata')(); + self.app.use('/api', api(self.env, ctx)); + done(); + }); + }); + + it('wipe treatment data', function (done) { + self.ctx.treatments().remove({ }, function ( ) { + done(); + }); + }); + + it('wipe entries data', function (done) { + self.ctx.entries().remove({ }, function ( ) { + done(); + }); + }); + +}); diff --git a/tests/adminnotifies.test.js b/tests/adminnotifies.test.js new file mode 100644 index 00000000000..86655708d26 --- /dev/null +++ b/tests/adminnotifies.test.js @@ -0,0 +1,109 @@ + +'use strict'; + +require('should'); +var language = require('../lib/language')(); + +const ctx = {}; + +ctx.bus = {}; +ctx.bus.on = function mockOn(channel, f) { }; +ctx.settings = {}; +ctx.settings.adminNotifiesEnabled = true; + +const mockJqueryResults = {}; +const mockButton = {}; + +mockButton.click = function() {}; +mockButton.css = function() {}; +mockButton.show = function() {}; + +const mockDrawer = {}; + +const mockJQuery = function mockJquery(p) { + if (p == '#adminnotifies') return mockButton; + if (p == '#adminNotifiesDrawer') return mockDrawer; + return mockJqueryResults; +}; + +const mockClient = {}; + +mockClient.translate = language.translate; +mockClient.headers = function () {return {};} + +const adminnotifies = require('../lib/adminnotifies')(ctx); + +var window = {}; +//global.window = window; + +window.setTimeout = function () { return; } + +describe('adminnotifies', function ( ) { + + after( function tearDown(done) { + delete global.window; + done(); + }); + + it('should aggregate a message', function () { + + const notify = { + title: 'Foo' + , message: 'Bar' + }; + + adminnotifies.addNotify(notify); + adminnotifies.addNotify(notify); + + const notifies = adminnotifies.getNotifies(); + + notifies.length.should.equal(1); + }); + + /* + it('should display a message', function (done) { + + const notify2 = { + title: 'FooFoo' + , message: 'BarBar' + }; + + adminnotifies.addNotify(notify2); + adminnotifies.addNotify(notify2); + + const notifies = adminnotifies.getNotifies(); + + mockJQuery.ajax = function mockAjax() { + + const rVal = notifies; + + rVal.done = function(callback) { + callback({ + message: { + notifies, + notifyCount: notifies.length + } + }); + return rVal; + } + + rVal.fail = function() {}; + + return rVal; + } + + const adminnotifiesClient = require('../lib/client/adminnotifiesclient')(mockClient,mockJQuery); + + mockDrawer.html = function (html) { + console.log(html); + html.indexOf('You have administration messages').should.be.greaterThan(0); + html.indexOf('Event repeated 2 times').should.be.greaterThan(0); + done(); + } + + adminnotifiesClient.prepare(); + + }); +*/ + +}); \ No newline at end of file diff --git a/tests/admintools.test.js b/tests/admintools.test.js index c990d8639bc..4b7801419cd 100644 --- a/tests/admintools.test.js +++ b/tests/admintools.test.js @@ -29,6 +29,9 @@ var someData = { 'created_at': '2025-09-28T16:41:07.144Z' } ], + '/api/v1/devicestatus/?find[created_at][$lte]=': { + n: 1 + }, '/api/v1/treatments.json?&find[created_at][$gte]=': [ { '_id': '5609a9203c8104a8195b1c1e', @@ -38,6 +41,9 @@ var someData = { 'created_at': '2025-09-28T20:54:00.000Z' } ], + '/api/v1/treatments/?find[created_at][$lte]=': { + n: 1 + }, '/api/v1/entries.json?&find[date][$gte]=': [ { '_id': '560983f326c5a592d9b9ae0c', @@ -51,19 +57,26 @@ var someData = { 'rssi': 178, 'noise': 1 } - ] - }; + ], + '/api/v1/entries/?find[date][$lte]=': { + n: 1 + }, +}; describe('admintools', function ( ) { var self = this; - + this.timeout(45000); // TODO: see why this test takes longer on CI to complete before(function (done) { benv.setup(function() { - self.$ = require('jquery'); - self.$.localStorage = require('./fixtures/localstorage'); - self.$.fn.tipsy = function mockTipsy ( ) { }; + benv.require(__dirname + '/../node_modules/.cache/_ns_cache/public/js/bundle.app.js'); + + self.$ = $; + + self.localCookieStorage = self.localStorage = self.$.localStorage = require('./fixtures/localstorage'); + + self.$.fn.tooltip = function mockTooltip ( ) { }; self.$.fn.dialog = function mockDialog (opts) { function maybeCall (name, obj) { @@ -79,21 +92,29 @@ describe('admintools', function ( ) { }); }; - var indexHtml = read(__dirname + '/../static/admin/index.html', 'utf8'); + var indexHtml = read(__dirname + '/../views/adminindex.html', 'utf8'); self.$('body').html(indexHtml); //var filesys = require('fs'); //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) self.$.ajax = function mockAjax (url, opts) { + if (url && url.url) { + url = url.url; + } //logfile.write(url+'\n'); - //console.log(url,opts); + //console.log('Mock ajax:',url,opts); if (opts && opts.success && opts.success.call) { if (url.indexOf('/api/v1/treatments.json?&find[created_at][$gte]=')===0) { url = '/api/v1/treatments.json?&find[created_at][$gte]='; - } - if (url.indexOf('/api/v1/entries.json?&find[date][$gte]=')===0) { + } else if (url.indexOf('/api/v1/entries.json?&find[date][$gte]=')===0) { url = '/api/v1/entries.json?&find[date][$gte]='; + } else if (url.indexOf('/api/v1/devicestatus/?find[created_at][$lte]=')===0) { + url = '/api/v1/devicestatus/?find[created_at][$lte]='; + } else if (url.indexOf('/api/v1/treatments/?find[created_at][$lte]=')===0) { + url = '/api/v1/treatments/?find[created_at][$lte]='; + } else if (url.indexOf('/api/v1/entries/?find[date][$lte]=')===0) { + url = '/api/v1/entries/?find[date][$lte]='; } return { done: function mockDone (fn) { @@ -114,7 +135,11 @@ describe('admintools', function ( ) { } return { done: function mockDone (fn) { - fn({message: 'OK'}); + if (url.indexOf('status.json') > -1) { + fn(serverSettings); + } else { + fn({message: {message: 'OK'}}); + } return self.$.ajax(); }, fail: function mockFail () { @@ -128,23 +153,41 @@ describe('admintools', function ( ) { var d3 = require('d3'); //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; + //d3.timer = function mockTimer() { }; + let timer = d3.timer(function mockTimer() { }); + timer.stop(); + + var cookieStorageType = self.localStorage._type benv.expose({ $: self.$ , jQuery: self.$ , d3: d3 , serverSettings: serverSettings + , localCookieStorage: self.localStorage + , cookieStorageType: self.localStorage + , localStorage: self.localStorage , io: { connect: function mockConnect ( ) { return { - on: function mockOn ( ) { } + on: function mockOn (event, callback) { + if ('connect' === event && callback) { + callback(); + } + } + , emit: function mockEmit (event, data, callback) { + if ('authorize' === event && callback) { + callback({ + read: true + }); + } + } }; } } }); - benv.require(__dirname + '/../bundle/bundle.source.js'); + //benv.require(__dirname + '/../bundle/bundle.source.js'); benv.require(__dirname + '/../static/admin/js/admin.js'); done(); @@ -157,10 +200,9 @@ describe('admintools', function ( ) { }); it ('should produce some html', function (done) { - var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; @@ -176,7 +218,8 @@ describe('admintools', function ( ) { return true; }; - client.init(serverSettings, plugins); + client.init(); + client.dataUpdate(nowData); //var result = $('body').html(); @@ -192,6 +235,12 @@ describe('admintools', function ( ) { $('#admin_cleanstatusdb_0_html + button').click(); $('#admin_cleanstatusdb_0_status').text().should.equal('All records removed ...'); // devicestatus code result + $('#admin_cleanstatusdb_1_html + button').text().should.equal('Delete old documents'); // devicestatus button + $('#admin_cleanstatusdb_1_status').text().should.equal(''); // devicestatus init result + + $('#admin_cleanstatusdb_1_html + button').click(); + $('#admin_cleanstatusdb_1_status').text().should.equal('1 records deleted'); // devicestatus code result + $('#admin_futureitems_0_html + button').text().should.equal('Remove treatments in the future'); // futureitems button 0 $('#admin_futureitems_0_status').text().should.equal('Database contains 1 future records'); // futureitems init result 0 @@ -204,6 +253,18 @@ describe('admintools', function ( ) { $('#admin_futureitems_1_html + button').click(); $('#admin_futureitems_1_status').text().should.equal('Record 560983f326c5a592d9b9ae0c removed ...'); // futureitems code result 1 + $('#admin_cleantreatmentsdb_0_html + button').text().should.equal('Delete old documents'); // treatments button + $('#admin_cleantreatmentsdb_0_status').text().should.equal(''); // treatments init result + + $('#admin_cleantreatmentsdb_0_html + button').click(); + $('#admin_cleantreatmentsdb_0_status').text().should.equal('1 records deleted'); // treatments code result + + $('#admin_cleanentriesdb_0_html + button').text().should.equal('Delete old documents'); // entries button + $('#admin_cleanentriesdb_0_status').text().should.equal(''); // entries init result + + $('#admin_cleanentriesdb_0_html + button').click(); + $('#admin_cleanentriesdb_0_status').text().should.equal('1 records deleted'); // entries code result + done(); }); diff --git a/tests/api.alexa.test.js b/tests/api.alexa.test.js new file mode 100644 index 00000000000..7075b8e6d5b --- /dev/null +++ b/tests/api.alexa.test.js @@ -0,0 +1,101 @@ +'use strict'; + +const fs = require('fs'); +const request = require('supertest'); +const language = require('../lib/language')(fs); + +const bodyParser = require('body-parser'); + +require('should'); + +describe('Alexa REST api', function ( ) { + this.timeout(10000); + const apiRoot = require('../lib/api/root'); + const api = require('../lib/api/'); + before(function (done) { + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../lib/server/env')( ); + env.settings.enable = ['alexa']; + env.settings.authDefaultRoles = 'readable'; + env.api_secret = 'this is my long pass phrase'; + this.wares = require('../lib/middleware/')(env); + this.app = require('express')( ); + this.app.enable('api'); + var self = this; + require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { + self.app.use('/api', bodyParser({ + limit: 1048576 * 50 + }), apiRoot(env, ctx)); + + self.app.use('/api/v1', bodyParser({ + limit: 1048576 * 50 + }), api(env, ctx)); + done( ); + }); + }); + + it('Launch Request', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "LaunchRequest", + "locale": "en-US" + } + }) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + + const launchText = 'What would you like to check on Nightscout?'; + + res.body.response.outputSpeech.text.should.equal(launchText); + res.body.response.reprompt.outputSpeech.text.should.equal(launchText); + res.body.response.shouldEndSession.should.equal(false); + done( ); + }); + }); + + it('Launch Request With Intent', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "LaunchRequest", + "locale": "en-US", + "intent": { + "name": "UNKNOWN" + } + } + }) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + + const unknownIntentText = 'I\'m sorry, I don\'t know what you\'re asking for.'; + + res.body.response.outputSpeech.text.should.equal(unknownIntentText); + res.body.response.shouldEndSession.should.equal(true); + done( ); + }); + }); + + it('Session Ended', function (done) { + request(this.app) + .post('/api/v1/alexa') + .send({ + "request": { + "type": "SessionEndedRequest", + "locale": "en-US" + } + }) + .expect(200) + .end(function (err) { + if (err) return done(err); + + done( ); + }); + }); +}); + diff --git a/tests/api.devicestatus.test.js b/tests/api.devicestatus.test.js new file mode 100644 index 00000000000..50b6c202d08 --- /dev/null +++ b/tests/api.devicestatus.test.js @@ -0,0 +1,97 @@ +'use strict'; + +var _ = require('lodash'); +var request = require('supertest'); +var should = require('should'); +var language = require('../lib/language')(); + +describe('Devicestatus API', function ( ) { + this.timeout(10000); + var self = this; + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + + var api = require('../lib/api/'); + beforeEach(function (done) { + process.env.API_SECRET = 'this is my long pass phrase'; + self.env = require('../lib/server/env')(); + self.env.settings.authDefaultRoles = 'readable'; + self.env.settings.enable = ['careportal', 'api']; + this.wares = require('../lib/middleware/')(self.env); + self.app = require('express')(); + self.app.enable('api'); + require('../lib/server/bootevent')(self.env, language).boot(function booted(ctx) { + self.ctx = ctx; + self.ctx.ddata = require('../lib/data/ddata')(); + self.app.use('/api', api(self.env, ctx)); + done(); + }); + }); + + it('post a devicestatus, query, delete, verify gone', function (done) { + // insert a devicestatus - needs to be unique from example data + console.log('Inserting devicestatus entry'); + request(self.app) + .post('/api/devicestatus/') + .set('api-secret', known || '') + .send({ + device: 'xdripjs://rigName' + , xdripjs: { + state: 6 + , stateString: 'OK' + , txStatus: 0 + , txStatusString: 'OK' + } + , created_at: '2018-12-16T01:00:52Z' + }) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + // make sure devicestatus was inserted successfully + console.log('Ensuring devicestatus entry was inserted successfully'); + request(self.app) + .get('/api/devicestatus/') + .query('find[created_at][$gte]=2018-12-16') + .query('find[created_at][$lte]=2018-12-17') + .set('api-secret', known || '') + .expect(200) + .expect(function (response) { + console.log(JSON.stringify(response.body[0])); + response.body[0].xdripjs.state.should.equal(6); + response.body[0].utcOffset.should.equal(0); + }) + .end(function (err) { + if (err) { + done(err); + } else { + // delete the treatment + console.log('Deleting test treatment entry'); + request(self.app) + .delete('/api/devicestatus/') + .query('find[created_at][$gte]=2018-12-16') + .set('api-secret', known || '') + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + // make sure it was deleted + console.log('Testing if devicestatus was deleted'); + request(self.app) + .get('/api/devicestatus/') + .query('find[created_at][$lte]=2018-12-16') + .set('api-secret', known || '') + .expect(200) + .expect(function (response) { + response.body.length.should.equal(0); + }) + .end(done); + } + }); + } + }); + } + }); + }); +}); diff --git a/tests/api.entries.test.js b/tests/api.entries.test.js index 9696269d686..07fe0cc4aeb 100644 --- a/tests/api.entries.test.js +++ b/tests/api.entries.test.js @@ -2,42 +2,72 @@ var request = require('supertest'); var load = require('./fixtures/load'); -var bootevent = require('../lib/bootevent'); +var bootevent = require('../lib/server/bootevent'); +var language = require('../lib/language')(); +const _ = require('lodash'); + require('should'); +const FIVE_MINUTES=1000*60*5; + describe('Entries REST api', function ( ) { var entries = require('../lib/api/entries/'); + var self = this; + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; this.timeout(10000); before(function (done) { - var env = require('../env')( ); - this.wares = require('../lib/middleware/')(env); - this.archive = null; - this.app = require('express')( ); - this.app.enable('api'); - var self = this; - bootevent(env).boot(function booted (ctx) { - self.app.use('/', entries(self.app, self.wares, ctx)); - self.archive = require('../lib/entries')(env, ctx); - - var creating = load('json'); - creating.push({type: 'sgv', sgv: 100, date: Date.now()}); - self.archive.create(creating, done); + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + self.env = require('../lib/server/env')( ); + self.env.settings.authDefaultRoles = 'readable'; + self.wares = require('../lib/middleware/')(self.env); + self.archive = null; + self.app = require('express')( ); + self.app.enable('api'); + bootevent(self.env, language).boot(function booted (ctx) { + self.app.use('/', entries(self.app, self.wares, ctx, self.env)); + self.archive = require('../lib/server/entries')(self.env, ctx); + self.ctx = ctx; + done(); }); }); beforeEach(function (done) { var creating = load('json'); - creating.push({type: 'sgv', sgv: 100, date: Date.now()}); - this.archive.create(creating, done); + + for (let i = 0; i < 20; i++) { + const e = {type: 'sgv', sgv: 100, date: Date.now()}; + e.date = e.date - FIVE_MINUTES * i; + creating.push(e); + } + + creating = _.sortBy(creating, function(item) { + return item.date; + }); + + function setupDone() { + console.log('Setup complete'); + done(); + } + + function waitForASecond() { + // wait for event processing of cache entries to actually finish + setTimeout(function() { + setupDone(); + }, 100); + } + + self.archive.create(creating, waitForASecond); + }); afterEach(function (done) { - this.archive( ).remove({ }, done); + self.archive( ).remove({ }, done); }); after(function (done) { - this.archive( ).remove({ }, done); + self.archive( ).remove({ }, done); }); // keep this test pinned at or near the top in order to validate all @@ -46,7 +76,7 @@ describe('Entries REST api', function ( ) { // function callback logic in entries.js. it('gets requested number of entries', function (done) { var count = 30; - request(this.app) + request(self.app) .get('/entries.json?find[dateString][$gte]=2014-07-19&count=' + count) .expect(200) .end(function (err, res) { @@ -57,17 +87,53 @@ describe('Entries REST api', function ( ) { it('gets default number of entries', function (done) { var defaultCount = 10; - request(this.app) + request(self.app) + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(defaultCount); + done( ); + }); + }); + + it('gets entries in right order', function (done) { + var defaultCount = 10; + request(self.app) .get('/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') .expect(200) .end(function (err, res) { res.body.should.be.instanceof(Array).and.have.lengthOf(defaultCount); + + var array = res.body; + var firstEntry = array[0]; + var secondEntry = array[1]; + + firstEntry.date.should.be.above(secondEntry.date); + + done( ); + }); + }); + + it('gets entries in right order without type specifier', function (done) { + var defaultCount = 10; + request(self.app) + .get('/entries.json') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(defaultCount); + + var array = res.body; + var firstEntry = array[0]; + var secondEntry = array[1]; + + firstEntry.date.should.be.above(secondEntry.date); + done( ); }); }); it('/echo/ api shows query', function (done) { - request(this.app) + request(self.app) .get('/echo/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') .expect(200) .end(function (err, res) { @@ -81,7 +147,7 @@ describe('Entries REST api', function ( ) { }); it('/slice/ can slice time', function (done) { - var app = this.app; + var app = self.app; request(app) .get('/slice/entries/dateString/sgv/2014-07.json?count=20') .expect(200) @@ -93,7 +159,7 @@ describe('Entries REST api', function ( ) { it('/times/echo can describe query', function (done) { - var app = this.app; + var app = self.app; request(app) .get('/times/echo/2014-07/.*T{00..05}:.json?count=20&find[sgv][$gte]=160') .expect(200) @@ -106,7 +172,7 @@ describe('Entries REST api', function ( ) { }); it('/slice/ can slice with multiple prefix', function (done) { - var app = this.app; + var app = self.app; request(app) .get('/slice/entries/dateString/sgv/2014-07-{17..20}.json?count=20') .expect(200) @@ -117,7 +183,7 @@ describe('Entries REST api', function ( ) { }); it('/slice/ can slice time with prefix and no results', function (done) { - var app = this.app; + var app = self.app; request(app) .get('/slice/entries/dateString/sgv/1999-07.json?count=20&find[sgv][$lte]=401') .expect(200) @@ -128,7 +194,7 @@ describe('Entries REST api', function ( ) { }); it('/times/ can get modal times', function (done) { - var app = this.app; + var app = self.app; request(app) .get('/times/2014-07-/{0..30}T.json?') .expect(200) @@ -139,7 +205,7 @@ describe('Entries REST api', function ( ) { }); it('/times/ can get modal minutes and times', function (done) { - var app = this.app; + var app = self.app; request(app) .get('/times/20{14..15}-07/T{09..10}.json?') .expect(200) @@ -149,7 +215,7 @@ describe('Entries REST api', function ( ) { }); }); it('/times/ can get multiple prefixen and modal minutes and times', function (done) { - var app = this.app; + var app = self.app; request(app) .get('/times/20{14..15}/T.*:{00..60}.json?') .expect(200) @@ -160,7 +226,7 @@ describe('Entries REST api', function ( ) { }); it('/entries/current.json', function (done) { - request(this.app) + request(self.app) .get('/entries/current.json') .expect(200) .end(function (err, res) { @@ -171,8 +237,8 @@ describe('Entries REST api', function ( ) { }); it('/entries/:id', function (done) { - var app = this.app; - this.archive.list({count: 1}, function(err, records) { + var app = self.app; + self.archive.list({count: 1}, function(err, records) { var currentId = records.pop()._id.toString(); request(app) .get('/entries/'+currentId+'.json') @@ -186,7 +252,7 @@ describe('Entries REST api', function ( ) { }); it('/entries/:model', function (done) { - var app = this.app; + var app = self.app; request(app) .get('/entries/sgv/.json?count=10&find[dateString][$gte]=2014') .expect(200) @@ -196,19 +262,19 @@ describe('Entries REST api', function ( ) { }); }); - it('/entries/preview', function (done) { - request(this.app) + it('disallow POST by readable /entries/preview', function (done) { + request(self.app) .post('/entries/preview.json') .send(load('json')) - .expect(201) + .expect(401) .end(function (err, res) { - res.body.should.be.instanceof(Array).and.have.lengthOf(30); + // res.body.should.be.instanceof(Array).and.have.lengthOf(30); done(); }); }); it('disallow deletes unauthorized', function (done) { - var app = this.app; + var app = self.app; request(app) .delete('/entries/sgv?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') @@ -228,4 +294,124 @@ describe('Entries REST api', function ( ) { }); }); + it('post an entry, query, delete, verify gone', function (done) { + // insert a glucose entry - needs to be unique from example data + console.log('Inserting glucose entry') + request(self.app) + .post('/entries/') + .set('api-secret', known || '') + .send({ + "type": "sgv", "sgv": "199", "dateString": "2014-07-20T00:44:15.000-07:00" + , "date": 1405791855000, "device": "dexcom", "direction": "NOT COMPUTABLE" + }) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + // make sure treatment was inserted successfully + console.log('Ensuring glucose entry was inserted successfully'); + request(self.app) + .get('/entries.json?find[dateString][$gte]=2014-07-20&count=100') + .set('api-secret', known || '') + .expect(200) + .expect(function (response) { + var entry = response.body[0]; + entry.sgv.should.equal('199'); + entry.utcOffset.should.equal(-420); + }) + .end(function (err) { + if (err) { + done(err); + } else { + // delete the glucose entry + console.log('Deleting test glucose entry'); + request(self.app) + .delete('/entries.json?find[dateString][$gte]=2014-07-20&count=100') + .set('api-secret', known || '') + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + // make sure it was deleted + console.log('Testing if glucose entry was deleted'); + request(self.app) + .get('/entries.json?find[dateString][$gte]=2014-07-20&count=100') + .set('api-secret', known || '') + .expect(200) + .expect(function (response) { + response.body.length.should.equal(0); + }) + .end(done); + } + }); + } + }); + } + }); + }); + + it('post multiple entries, query, delete, verify gone', function (done) { + // insert a glucose entry - needs to be unique from example data + console.log('Inserting glucose entry') + request(self.app) + .post('/entries/') + .set('api-secret', known || '') + .send([{ + "type": "sgv", "sgv": "199", "dateString": "2014-07-20T00:44:15.000-07:00" + , "date": 1405791855000, "device": "dexcom", "direction": "NOT COMPUTABLE" + }, { + "type": "sgv", "sgv": "200", "dateString": "2014-07-20T00:44:15.001-07:00" + , "date": 1405791855001, "device": "dexcom", "direction": "NOT COMPUTABLE" + }]) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + // make sure treatment was inserted successfully + console.log('Ensuring glucose entry was inserted successfully'); + request(self.app) + .get('/entries.json?find[dateString][$gte]=2014-07-20&count=100') + .set('api-secret', known || '') + .expect(200) + .expect(function (response) { + var entry = response.body[0]; + response.body.length.should.equal(2); + entry.sgv.should.equal('200'); + entry.utcOffset.should.equal(-420); + }) + .end(function (err) { + if (err) { + done(err); + } else { + // delete the glucose entry + console.log('Deleting test glucose entry'); + request(self.app) + .delete('/entries.json?find[dateString][$gte]=2014-07-20&count=100') + .set('api-secret', known || '') + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + // make sure it was deleted + console.log('Testing if glucose entries were deleted'); + request(self.app) + .get('/entries.json?find[dateString][$gte]=2014-07-20&count=100') + .set('api-secret', known || '') + .expect(200) + .expect(function (response) { + response.body.length.should.equal(0); + }) + .end(done); + } + }); + } + }); + } + }); + }); + }); diff --git a/tests/api.root.test.js b/tests/api.root.test.js new file mode 100644 index 00000000000..d2c3609a117 --- /dev/null +++ b/tests/api.root.test.js @@ -0,0 +1,42 @@ +'use strict'; + +const request = require('supertest'); +require('should'); + +describe('Root REST API', function() { + const self = this + , instance = require('./fixtures/api/instance') + , semver = require('semver') + ; + + this.timeout(15000); + + before(async () => { + self.instance = await instance.create({}); + self.app = self.instance.app; + self.env = self.instance.env; + }); + + + after(function after () { + self.instance.server.close(); + }); + + + it('GET /api/versions', async () => { + let res = await request(self.app) + .get('/api/versions') + .expect(200); + + res.body.length.should.be.aboveOrEqual(3); + res.body.forEach(obj => { + const fields = Object.getOwnPropertyNames(obj); + fields.sort().should.be.eql(['url', 'version']); + + semver.valid(obj.version).should.be.ok(); + obj.url.should.startWith('/api'); + }); + }); + +}); + diff --git a/tests/api.security.test.js b/tests/api.security.test.js new file mode 100644 index 00000000000..fd76696f7bd --- /dev/null +++ b/tests/api.security.test.js @@ -0,0 +1,161 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +const request = require('supertest'); +var language = require('../lib/language')(); +require('should'); +const jwt = require('jsonwebtoken'); + +describe('Security of REST API V1', function() { + const self = this + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject'); + + this.timeout(30000); + + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + + before(function(done) { + var api = require('../lib/api/'); + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + self.env = require('../lib/server/env')(); + self.env.settings.authDefaultRoles = 'denied'; + this.wares = require('../lib/middleware/')(self.env); + self.app = require('express')(); + self.app.enable('api'); + require('../lib/server/bootevent')(self.env, language).boot(async function booted (ctx) { + self.app.use('/api/v1', api(self.env, ctx)); + self.app.use('/api/v2/authorization', ctx.authorization.endpoints); + let authResult = await authSubject(ctx.authorization.storage); + self.subject = authResult.subject; + self.token = authResult.accessToken; + + done(); + }); + }); + + it('Should fail on false token', function(done) { + request(self.app) + .get('/api/v2/authorization/request/12345') + .expect(401) + .end(function(err, res) { + console.log(res.error); + res.error.status.should.equal(401); + done(); + }); + }); + + it('Data load should fail unauthenticated', function(done) { + request(self.app) + .get('/api/v1/entries.json') + .expect(401) + .end(function(err, res) { + console.log(res.error); + res.error.status.should.equal(401); + done(); + }); + }); + + it('Should return a JWT on token', function(done) { + const now = Math.round(Date.now() / 1000) - 1; + request(self.app) + .get('/api/v2/authorization/request/' + self.token.read) + .expect(200) + .end(function(err, res) { + const decodedToken = jwt.decode(res.body.token); + decodedToken.accessToken.should.equal(self.token.read); + decodedToken.iat.should.be.aboveOrEqual(now); + decodedToken.exp.should.be.above(decodedToken.iat); + done(); + }); + }); + + it('Should return a JWT with default roles on broken role token', function(done) { + const now = Math.round(Date.now() / 1000) - 1; + request(self.app) + .get('/api/v2/authorization/request/' + self.token.noneSubject) + .expect(200) + .end(function(err, res) { + const decodedToken = jwt.decode(res.body.token); + decodedToken.accessToken.should.equal(self.token.noneSubject); + decodedToken.iat.should.be.aboveOrEqual(now); + decodedToken.exp.should.be.above(decodedToken.iat); + done(); + }); + }); + + it('Data load should succeed with API SECRET', function(done) { + request(self.app) + .get('/api/v1/entries.json') + .set('api-secret', known) + .expect(200) + .end(function(err, res) { + done(); + }); + }); + + it('Data load should succeed with GET token', function(done) { + request(self.app) + .get('/api/v1/entries.json?token=' + self.token.read) + .expect(200) + .end(function(err, res) { + done(); + }); + }); + + it('Data load should succeed with token in place of a secret', function(done) { + request(self.app) + .get('/api/v1/entries.json') + .set('api-secret', self.token.read) + .expect(200) + .end(function(err, res) { + done(); + }); + }); + + it('Data load should succeed with a bearer token', function(done) { + request(self.app) + .get('/api/v2/authorization/request/' + self.token.read) + .expect(200) + .end(function(err, res) { + const token = res.body.token; + request(self.app) + .get('/api/v1/entries.json') + .set('Authorization', 'Bearer ' + token) + .expect(200) + .end(function(err, res) { + done(); + }); + }); + }); + + it('Data load fail succeed with a false bearer token', function(done) { + request(self.app) + .get('/api/v1/entries.json') + .set('Authorization', 'Bearer 1234567890') + .expect(401) + .end(function(err, res) { + done(); + }); + }); + + it('/verifyauth should return OK for Bearer tokens', function (done) { + request(self.app) + .get('/api/v2/authorization/request/' + self.token.adminAll) + .expect(200) + .end(function(err, res) { + const token = res.body.token; + request(self.app) + .get('/api/v1/verifyauth') + .set('Authorization', 'Bearer ' + token) + .expect(200) + .end(function(err, res) { + res.body.message.message.should.equal('OK'); + res.body.message.isAdmin.should.equal(true); + done(); + }); + }); + }); + +}); diff --git a/tests/api.status.test.js b/tests/api.status.test.js index 5044c2afdd2..5426689aceb 100644 --- a/tests/api.status.test.js +++ b/tests/api.status.test.js @@ -1,19 +1,24 @@ 'use strict'; var request = require('supertest'); +var language = require('../lib/language')(); + require('should'); describe('Status REST api', function ( ) { var api = require('../lib/api/'); before(function (done) { - var env = require('../env')( ); + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../lib/server/env')( ); env.settings.enable = ['careportal', 'rawbg']; + env.settings.authDefaultRoles = 'readable'; env.api_secret = 'this is my long pass phrase'; this.wares = require('../lib/middleware/')(env); this.app = require('express')( ); this.app.enable('api'); var self = this; - require('../lib/bootevent')(env).boot(function booted (ctx) { + require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { self.app.use('/api', api(env, ctx)); done(); }); @@ -43,6 +48,27 @@ describe('Status REST api', function ( ) { }); }); + it('/status.svg', function (done) { + request(this.app) + .get('/api/status.svg') + .end(function(err, res) { + res.statusCode.should.equal(302); + done(); + }); + }); + + it('/status.txt', function (done) { + request(this.app) + .get('/api/status.txt') + .expect(200, 'STATUS OK') + .end(function(err, res) { + res.type.should.equal('text/plain'); + res.statusCode.should.equal(200); + done(); + }); + }); + + it('/status.js', function (done) { request(this.app) .get('/api/status.js') diff --git a/tests/api.treatments.test.js b/tests/api.treatments.test.js index 3f588172684..40e2d052de7 100644 --- a/tests/api.treatments.test.js +++ b/tests/api.treatments.test.js @@ -1,71 +1,247 @@ 'use strict'; +var _ = require('lodash'); var request = require('supertest'); var should = require('should'); +var language = require('../lib/language')(); +var _moment = require('moment'); describe('Treatment API', function ( ) { + this.timeout(10000); var self = this; + var api_secret_hash = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + var api = require('../lib/api/'); - before(function (done) { + beforeEach(function (done) { process.env.API_SECRET = 'this is my long pass phrase'; - self.env = require('../env')(); - self.env.settings.enable = ['careportal']; + self.env = require('../lib/server/env')(); + self.env.settings.authDefaultRoles = 'readable'; + self.env.settings.enable = ['careportal', 'api']; this.wares = require('../lib/middleware/')(self.env); self.app = require('express')(); self.app.enable('api'); - require('../lib/bootevent')(self.env).boot(function booted(ctx) { + require('../lib/server/bootevent')(self.env, language).boot(function booted(ctx) { self.ctx = ctx; + self.ctx.ddata = require('../lib/data/ddata')(); self.app.use('/api', api(self.env, ctx)); done(); }); }); after(function () { - delete process.env.API_SECRET; + // delete process.env.API_SECRET; }); - it('post a some treatments', function (done) { - self.ctx.bus.on('data-loaded', function dataWasLoaded ( ) { - self.ctx.data.treatments.length.should.equal(3); - self.ctx.data.treatments[0].mgdl.should.equal(100); - should.not.exist(self.ctx.data.treatments[0].eventTime); - should.not.exist(self.ctx.data.treatments[0].notes); + it('post single treatments', function (done) { - should.not.exist(self.ctx.data.treatments[1].eventTime); - should.not.exist(self.ctx.data.treatments[1].glucose); - should.not.exist(self.ctx.data.treatments[1].glucoseType); - should.not.exist(self.ctx.data.treatments[1].units); - self.ctx.data.treatments[1].insulin.should.equal(2); - self.ctx.data.treatments[2].carbs.should.equal(30); + self.ctx.treatments().remove({ }, function ( ) { + var now = (new Date()).toISOString(); + request(self.app) + .post('/api/treatments/') + .set('api-secret', api_secret_hash || '') + .send({eventType: 'Meal Bolus', created_at: now, carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl', notes: ''}) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + self.ctx.treatments.list({}, function (err, list) { + var sorted = _.sortBy(list, function (treatment) { + return treatment.created_at; + }); + sorted.length.should.equal(2); + sorted[0].glucose.should.equal(100); + sorted[0].notes.should.equal(''); + should.not.exist(sorted[0].eventTime); + sorted[0].insulin.should.equal(2); + sorted[1].carbs.should.equal(30); + done(); + }); + } + }); - done(); }); + }); + + /* + it('saving entry without created_at should fail', function (done) { self.ctx.treatments().remove({ }, function ( ) { request(self.app) .post('/api/treatments/') .set('api-secret', self.env.api_secret || '') - .send({eventType: 'BG Check', glucose: 100, preBolus: '0', glucoseType: 'Finger', units: 'mg/dl', notes: ''}) + .send({eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) + .expect(422) + .end(function (err) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + }); +*/ + + it('post single treatments in zoned time format', function (done) { + + var current_time = Date.now(); + console.log('Testing date with local format: ', _moment(current_time).format("YYYY-MM-DDTHH:mm:ss.SSSZZ")); + + self.ctx.treatments().remove({ }, function ( ) { + request(self.app) + .post('/api/treatments/') + .set('api-secret', api_secret_hash || '') + .send({eventType: 'Meal Bolus', created_at: _moment(current_time).format("YYYY-MM-DDTHH:mm:ss.SSSZZ"), carbs: '30', insulin: '2.00', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) .expect(200) .end(function (err) { if (err) { done(err); + } else { + self.ctx.treatments.list({}, function (err, list) { + var sorted = _.sortBy(list, function (treatment) { + return treatment.created_at; + }); + console.log(sorted); + sorted.length.should.equal(1); + sorted[0].glucose.should.equal(100); + should.not.exist(sorted[0].eventTime); + sorted[0].insulin.should.equal(2); + sorted[0].carbs.should.equal(30); + var zonedTime = _moment(current_time).utc().format("YYYY-MM-DDTHH:mm:ss.SSS") + "Z"; + sorted[0].created_at.should.equal(zonedTime); + sorted[0].utcOffset.should.equal(-1* new Date().getTimezoneOffset()); + done(); + }); } }); + }); + }); + + + it('post a treatment array', function (done) { + self.ctx.treatments().remove({ }, function ( ) { + var now = (new Date()).toISOString(); request(self.app) .post('/api/treatments/') - .set('api-secret', self.env.api_secret || '') - .send({eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucoseType: 'Finger', units: 'mg/dl'}) + .set('api-secret', api_secret_hash || '') + .send([ + {eventType: 'BG Check', created_at: now, glucose: 100, preBolus: '0', glucoseType: 'Finger', units: 'mg/dl', notes: ''} + , {eventType: 'Meal Bolus', created_at: now, carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'} + ]) .expect(200) .end(function (err) { if (err) { done(err); + } else { + self.ctx.treatments.list({}, function (err, list) { + list.length.should.equal(3); + should.not.exist(list[0].eventTime); + should.not.exist(list[1].eventTime); + + done(); + }); } }); + }); + }); + + it('post a treatment array and dedupe', function (done) { + self.ctx.treatments().remove({ }, function ( ) { + var now = (new Date()).toISOString(); + request(self.app) + .post('/api/treatments/') + .set('api-secret', api_secret_hash || '') + .send([ + {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'BG Check', glucose: 100, units: 'mg/dl', created_at: now} + , {eventType: 'Meal Bolus', created_at: now, carbs: '30', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'} + ]) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + self.ctx.treatments.list({}, function (err, list) { + var sorted = _.sortBy(list, function (treatment) { + return treatment.created_at; + }); + + if (sorted.length !== 3) { + console.info('unexpected result length, sorted treatments:', sorted); + } + sorted.length.should.equal(3); + sorted[0].glucose.should.equal(100); + done(); + }); + } + }); }); }); -}); \ No newline at end of file + it('post a treatment, query, delete, verify gone', function (done) { + // insert a treatment - needs to be unique from example data + console.log('Inserting treatment entry'); + var now = (new Date()).toISOString(); + request(self.app) + .post('/api/treatments/') + .set('api-secret', api_secret_hash || '') + .send({eventType: 'Meal Bolus', created_at: now, carbs: '99', insulin: '2.00', preBolus: '15', glucose: 100, glucoseType: 'Finger', units: 'mg/dl'}) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + // make sure treatment was inserted successfully + console.log('Ensuring treatment entry was inserted successfully'); + request(self.app) + .get('/api/treatments/') + .query('find[carbs]=99') + .set('api-secret', api_secret_hash || '') + .expect(200) + .expect(function (response) { + response.body[0].carbs.should.equal(99); + }) + .end(function (err) { + if (err) { + done(err); + } else { + // delete the treatment + console.log('Deleting test treatment entry'); + request(self.app) + .delete('/api/treatments/') + .query('find[carbs]=99') + .set('api-secret', api_secret_hash || '') + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + // make sure it was deleted + console.log('Testing if entry was deleted'); + request(self.app) + .get('/api/treatments/') + .query('find[carbs]=99') + .set('api-secret', api_secret_hash || '') + .expect(200) + .expect(function (response) { + response.body.length.should.equal(0); + }) + .end(done); + } + }); + } + }); + } + }); + }); +}); diff --git a/tests/api.unauthorized.test.js b/tests/api.unauthorized.test.js index 64e7d254bd0..e1cfae62ac8 100644 --- a/tests/api.unauthorized.test.js +++ b/tests/api.unauthorized.test.js @@ -3,24 +3,28 @@ var request = require('supertest'); var load = require('./fixtures/load'); var should = require('should'); +var language = require('../lib/language')(); describe('authed REST api', function ( ) { var entries = require('../lib/api/entries/'); + this.timeout(20000); + before(function (done) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; process.env.API_SECRET = 'this is my long pass phrase'; - var env = require('../env')( ); + var env = require('../lib/server/env')( ); + env.settings.authDefaultRoles = 'readable'; this.wares = require('../lib/middleware/')(env); this.archive = null; this.app = require('express')( ); this.app.enable('api'); var self = this; self.known_key = known; - require('../lib/bootevent')(env).boot(function booted (ctx) { - self.app.use('/', entries(self.app, self.wares, ctx)); - self.archive = require('../lib/entries')(env, ctx); + require('../lib/server/bootevent')(env, language).boot(function booted (ctx) { + self.app.use('/', entries(self.app, self.wares, ctx, env)); + self.archive = require('../lib/server/entries')(env, ctx); var creating = load('json'); // creating.push({type: 'sgv', sgv: 100, date: Date.now()}); @@ -59,6 +63,19 @@ describe('authed REST api', function ( ) { }); }); + it('/entries/preview', function (done) { + var known_key = this.known_key; + request(this.app) + .post('/entries/preview.json') + .set('api-secret', known_key) + .send(load('json')) + .expect(201) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(30); + done(); + }); + }); + it('allow authorized POST', function (done) { var app = this.app; var known_key = this.known_key; diff --git a/tests/api.verifyauth.test.js b/tests/api.verifyauth.test.js index 1cb9b295cae..fe4f22a7d78 100644 --- a/tests/api.verifyauth.test.js +++ b/tests/api.verifyauth.test.js @@ -1,19 +1,26 @@ 'use strict'; var request = require('supertest'); +var language = require('../lib/language')(); + require('should'); describe('Verifyauth REST api', function ( ) { var self = this; + this.timeout(10000); + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + var api = require('../lib/api/'); before(function (done) { - self.env = require('../env')( ); - self.env.api_secret = 'this is my long pass phrase'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + self.env = require('../lib/server/env')( ); + self.env.settings.authDefaultRoles = 'denied'; this.wares = require('../lib/middleware/')(self.env); self.app = require('express')( ); self.app.enable('api'); - require('../lib/bootevent')(self.env).boot(function booted (ctx) { + require('../lib/server/bootevent')(self.env, language).boot(function booted (ctx) { self.app.use('/api', api(self.env, ctx)); done(); }); @@ -24,7 +31,7 @@ describe('Verifyauth REST api', function ( ) { .get('/api/verifyauth') .expect(200) .end(function(err, res) { - res.body.message.should.equal('UNAUTHORIZED'); + res.body.message.message.should.equal('UNAUTHORIZED'); done(); }); }); @@ -32,10 +39,10 @@ describe('Verifyauth REST api', function ( ) { it('/verifyauth should return OK', function (done) { request(self.app) .get('/api/verifyauth') - .set('api-secret', self.env.api_secret || '') + .set('api-secret', known || '') .expect(200) .end(function(err, res) { - res.body.message.should.equal('OK'); + res.body.message.message.should.equal('OK'); done(); }); }); diff --git a/tests/api3.basic.test.js b/tests/api3.basic.test.js new file mode 100644 index 00000000000..28f4c667ee1 --- /dev/null +++ b/tests/api3.basic.test.js @@ -0,0 +1,42 @@ +'use strict'; + +const request = require('supertest'); +require('should'); + +describe('Basic REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + ; + + this.timeout(15000); + + before(async () => { + self.instance = await instance.create({}); + self.app = self.instance.app; + self.env = self.instance.env; + }); + + + after(function after () { + self.instance.ctx.bus.teardown(); + }); + + + it('GET /version', async () => { + let res = await request(self.app) + .get('/api/v3/version') + .expect(200); + + const apiConst = require('../lib/api3/const.json') + , software = require('../package.json') + , result = res.body.result; + + res.body.status.should.equal(200); + result.version.should.equal(software.version); + result.apiVersion.should.equal(apiConst.API3_VERSION); + result.srvDate.should.be.within(testConst.YEAR_2019, testConst.YEAR_2050); + }); + +}); + diff --git a/tests/api3.create.test.js b/tests/api3.create.test.js new file mode 100644 index 00000000000..054d3e3254b --- /dev/null +++ b/tests/api3.create.test.js @@ -0,0 +1,578 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 CREATE', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + , utils = require('./fixtures/api3/utils') + ; + + self.validDoc = { + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 CREATE', + eventType: 'Correction Bolus', + insulin: 0.3 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(20000); + + + /** + * Cleanup after successful creation + */ + self.delete = async function deletePermanent (identifier) { + let res = await self.instance.delete(`${self.url}/${identifier}?permanent=true`, self.jwt.delete) + .expect(200); + + res.body.status.should.equal(200); + }; + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + return res.body.result; + }; + + + /** + * Get document detail for futher processing + */ + self.search = async function search (date) { + let res = await self.instance.get(`${self.url}?date$eq=${date}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + return res.body.result; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.col = 'treatments' + self.url = `/api/v3/${self.col}`; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'create', + 'update', + 'read', + 'delete', + 'all' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.cache = self.instance.cacheMonitor; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + beforeEach(() => { + self.cache.clear(); + }); + + + afterEach(() => { + self.cache.shouldBeEmpty(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.post(`${self.url}`) + .send(self.validDoc) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.post(`/api/v3/NOT_EXIST`, self.jwt.create) + .send(self.validDoc) + .expect(404); + + res.body.status.should.equal(404); + should.not.exist(res.body.result); + }); + + + it('should require create permission', async () => { + let res = await self.instance.post(`${self.url}`, self.jwt.read) + .send(self.validDoc) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:create'); + }); + + + it('should reject empty body', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send({ }) + .expect(400); + + res.body.status.should.equal(400); + }); + + + it('should accept valid document', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(self.validDoc) + .expect(201); + + res.body.status.should.equal(201); + res.body.identifier.should.equal(self.validDoc.identifier); + res.headers.location.should.equal(`${self.url}/${self.validDoc.identifier}`); + const lastModifiedBody = res.body.lastModified; + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier); + body.should.containEql(self.validDoc); + body.srvModified.should.equal(lastModifiedBody); + self.cache.nextShouldEql(self.col, self.validDoc) + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + (body.srvCreated - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiCreate.name); + + await self.delete(self.validDoc.identifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should reject missing date', async () => { + let doc = Object.assign({}, self.validDoc); + delete doc.date; + + let res = await self.instance.post(self.url, self.jwt.create) + .send(doc) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date null', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { date: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date ABC', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { date: 'ABC' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date -1', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { date: -1 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + + it('should reject invalid date 1 (too old)', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { date: 1 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid date - illegal format', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { date: '2019-20-60T50:90:90' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing date field'); + }); + + + it('should reject invalid utcOffset -5000', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { utcOffset: -5000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should reject invalid utcOffset ABC', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { utcOffset: 'ABC' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should accept valid utcOffset', async () => { + const doc = Object.assign({}, self.validDoc, { utcOffset: 120 }); + + await self.instance.post(self.url, self.jwt.create) + .send(doc) + .expect(201); + + let body = await self.get(self.validDoc.identifier); + body.utcOffset.should.equal(120); + self.cache.nextShouldEql(self.col, doc) + + await self.delete(self.validDoc.identifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should reject invalid utcOffset null', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { utcOffset: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing utcOffset field'); + }); + + + it('should reject missing app', async () => { + let doc = Object.assign({}, self.validDoc); + delete doc.app; + + let res = await self.instance.post(self.url, self.jwt.create) + .send(doc) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should reject invalid app null', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { app: null })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should reject empty app', async () => { + let res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { app: '' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Bad or missing app field'); + }); + + + it('should normalize date and store utcOffset', async () => { + await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { date: '2019-06-10T08:07:08,576+02:00' })) + .expect(201); + + let body = await self.get(self.validDoc.identifier); + body.date.should.equal(1560146828576); + body.utcOffset.should.equal(120); + self.cache.nextShouldEql(self.col, body) + + await self.delete(self.validDoc.identifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should require update permission for deduplication', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc); + + await self.instance.post(self.url, self.jwt.create) + .send(doc) + .expect(201); + + let createdBody = await self.get(doc.identifier); + createdBody.should.containEql(doc); + self.cache.nextShouldEql(self.col, doc) + + const doc2 = Object.assign({}, doc); + let res = await self.instance.post(self.url, self.jwt.create) + .send(doc2) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:update'); + await self.delete(doc.identifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should upsert document (matched by identifier)', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc); + + await self.instance.post(self.url, self.jwt.create) + .send(doc) + .expect(201); + + let createdBody = await self.get(doc.identifier); + createdBody.should.containEql(doc); + self.cache.nextShouldEql(self.col, doc) + + const doc2 = Object.assign({}, doc, { + insulin: 0.5 + }); + + let resPost2 = await self.instance.post(`${self.url}`, self.jwt.all) + .send(doc2) + .expect(200); + + resPost2.body.status.should.equal(200); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + self.cache.nextShouldEql(self.col, doc2) + + await self.delete(doc2.identifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should deduplicate document by created_at+eventType', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + await new Promise((resolve, reject) => { + self.instance.ctx.treatments.create([doc], async (err) => { // let's insert the document in APIv1's way + should.not.exist(err); + doc._id = doc._id.toString(); + self.cache.nextShouldEql(self.col, doc) + + err ? reject(err) : resolve(doc); + }); + }); + + const doc2 = Object.assign({}, doc, { + insulin: 0.4, + identifier: utils.randomString('32', 'aA#') + }); + delete doc2._id; // APIv1 updates input document, we must get rid of _id for the next round + + const resPost2 = await self.instance.post(`${self.url}`, self.jwt.all) + .send(doc2) + .expect(200); + + resPost2.body.status.should.equal(200); + resPost2.body.identifier.should.equal(doc2.identifier); + resPost2.body.isDeduplication.should.equal(true); + // doc (first document) was created "the old way" without identifier, so _id becomes the identifier for doc + resPost2.body.deduplicatedIdentifier.should.equal(doc._id); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + self.cache.nextShouldEql(self.col, doc2) + + await self.delete(doc2.identifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should not deduplicate treatment only by created_at', async () => { + self.validDoc.date = (new Date()).getTime(); + self.validDoc.identifier = utils.randomString('32', 'aA#'); + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + await new Promise((resolve, reject) => { + self.instance.ctx.treatments.create([doc], async (err) => { // let's insert the document in APIv1's way + should.not.exist(err); + doc._id = doc._id.toString(); + + self.cache.nextShouldEql(self.col, doc) + err ? reject(err) : resolve(doc); + }); + }); + + const oldBody = await self.get(doc._id); + delete doc._id; // APIv1 updates input document, we must get rid of _id for the next round + oldBody.should.containEql(doc); + + const doc2 = Object.assign({}, doc, { + eventType: 'Meal Bolus', + insulin: 0.4, + identifier: utils.randomString('32', 'aA#') + }); + + await self.instance.post(`${self.url}`, self.jwt.all) + .send(doc2) + .expect(201); + + let updatedBody = await self.get(doc2.identifier); + updatedBody.should.containEql(doc2); + updatedBody.identifier.should.not.equal(oldBody.identifier); + should.not.exist(updatedBody.isDeduplication); + should.not.exist(updatedBody.deduplicatedIdentifier); + self.cache.nextShouldEql(self.col, doc2) + + await self.delete(doc2.identifier); + self.cache.nextShouldDeleteLast(self.col) + + await self.delete(oldBody.identifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should overwrite deleted document', async () => { + const date1 = new Date() + , identifier = utils.randomString('32', 'aA#') + , doc = Object.assign({}, self.validDoc, { identifier, date: date1.toISOString() }); + + await self.instance.post(self.url, self.jwt.create) + .send(doc) + .expect(201); + self.cache.nextShouldEql(self.col, Object.assign({}, doc, { date: date1.getTime() })); + + let res = await self.instance.delete(`${self.url}/${identifier}`, self.jwt.delete) + .expect(200); + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col) + + const date2 = new Date(); + res = await self.instance.post(self.url, self.jwt.create) + .send(Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() })) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:update'); + self.cache.shouldBeEmpty() + + const doc2 = Object.assign({}, self.validDoc, { identifier, date: date2.toISOString() }); + res = await self.instance.post(`${self.url}`, self.jwt.all) + .send(doc2) + .expect(200); + + res.body.status.should.equal(200); + res.body.identifier.should.equal(identifier); + self.cache.nextShouldEql(self.col, Object.assign({}, doc2, { date: date2.getTime() })); + + let body = await self.get(identifier); + body.date.should.equal(date2.getTime()); + body.identifier.should.equal(identifier); + + await self.delete(identifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should calculate the identifier', async () => { + self.validDoc.date = (new Date()).getTime(); + delete self.validDoc.identifier; + const validIdentifier = opTools.calculateIdentifier(self.validDoc); + + let res = await self.instance.post(self.url, self.jwt.create) + .send(self.validDoc) + .expect(201); + + res.body.status.should.equal(201); + res.body.identifier.should.equal(validIdentifier); + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + + let body = await self.get(validIdentifier); + body.should.containEql(self.validDoc); + self.cache.nextShouldEql(self.col, self.validDoc); + + await self.delete(validIdentifier); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('should deduplicate by identifier calculation', async () => { + self.validDoc.date = (new Date()).getTime(); + delete self.validDoc.identifier; + const validIdentifier = opTools.calculateIdentifier(self.validDoc); + + let res = await self.instance.post(self.url, self.jwt.create) + .send(self.validDoc) + .expect(201); + + res.body.status.should.equal(201); + res.body.identifier.should.equal(validIdentifier); + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + + let body = await self.get(validIdentifier); + body.should.containEql(self.validDoc); + self.cache.nextShouldEql(self.col, self.validDoc); + + delete self.validDoc.identifier; + res = await self.instance.post(`${self.url}`, self.jwt.update) + .send(self.validDoc) + .expect(200); + + res.body.status.should.equal(200); + res.body.identifier.should.equal(validIdentifier); + res.body.isDeduplication.should.equal(true); + should.not.exist(res.body.deduplicatedIdentifier); // no identifier change occured + res.headers.location.should.equal(`${self.url}/${validIdentifier}`); + self.validDoc.identifier = validIdentifier; + self.cache.nextShouldEql(self.col, self.validDoc); + + body = await self.search(self.validDoc.date); + body.length.should.equal(1); + + await self.delete(validIdentifier); + self.cache.nextShouldDeleteLast(self.col) + }); + +}); + diff --git a/tests/api3.delete.test.js b/tests/api3.delete.test.js new file mode 100644 index 00000000000..7d709fe5e6b --- /dev/null +++ b/tests/api3.delete.test.js @@ -0,0 +1,65 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 UPDATE', function() { + const self = this + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + ; + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/treatments'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'delete' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.cache = self.instance.cacheMonitor; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + beforeEach(() => { + self.cache.clear(); + }); + + + afterEach(() => { + self.cache.shouldBeEmpty(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.delete(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.delete(`/api/v3/NOT_EXIST`, self.jwt.delete) + .send(self.validDoc) + .expect(404); + + res.body.status.should.equal(404); + }); + +}); + diff --git a/tests/api3.generic.workflow.test.js b/tests/api3.generic.workflow.test.js new file mode 100644 index 00000000000..01b5ede3394 --- /dev/null +++ b/tests/api3.generic.workflow.test.js @@ -0,0 +1,337 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('Generic REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.urlLastModified = '/api/v3/lastModified'; + self.historyTimestamp = 0; + + self.docOriginal = { + eventType: 'Correction Bolus', + insulin: 1, + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' Generic REST API3' + }; + self.identifier = opTools.calculateIdentifier(self.docOriginal); + self.docOriginal.identifier = self.identifier; + + this.timeout(30000); + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.col = 'treatments'; + self.urlCol = `/api/v3/${self.col}`; + self.urlResource = self.urlCol + '/' + self.identifier; + self.urlHistory = self.urlCol + '/history'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'create', + 'update', + 'read', + 'delete' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.cache = self.instance.cacheMonitor; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + beforeEach(() => { + self.cache.clear(); + }); + + + afterEach(() => { + self.cache.shouldBeEmpty(); + }); + + + self.checkHistoryExistence = async function checkHistoryExistence (assertions) { + + let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.length.should.be.above(0); + res.body.result.should.matchAny(value => { + value.identifier.should.be.eql(self.identifier); + value.srvModified.should.be.above(self.historyTimestamp); + + if (typeof(assertions) === 'function') { + assertions(value); + } + + self.historyTimestamp = value.srvModified; + }); + }; + + + it('LAST MODIFIED to get actual server timestamp', async () => { + let res = await self.instance.get(`${self.urlLastModified}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + self.historyTimestamp = res.body.result.collections.treatments; + if (!self.historyTimestamp) { + self.historyTimestamp = res.body.result.srvDate - (10 * 60 * 1000); + } + self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019); + }); + + + it('STATUS to get actual server timestamp', async () => { + let res = await self.instance.get(`/api/v3/status`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + self.historyTimestamp = res.body.result.srvDate; + self.historyTimestamp.should.be.aboveOrEqual(testConst.YEAR_2019); + }); + + + it('READ of not existing document is not found', async () => { + await self.instance.get(`${self.urlResource}`, self.jwt.read) + .expect(404); + }); + + + it('SEARCH of not existing document (not found)', async () => { + let res = await self.instance.get(`${self.urlCol}`, self.jwt.read) + .query({ 'identifier_eq': self.identifier }) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.should.have.length(0); + }); + + + it('DELETE of not existing document is not found', async () => { + await self.instance.delete(`${self.urlResource}`, self.jwt.delete) + .expect(404); + }); + + + it('CREATE new document', async () => { + await self.instance.post(`${self.urlCol}`, self.jwt.create) + .send(self.docOriginal) + .expect(201); + + self.cache.nextShouldEql(self.col, self.docOriginal) + }); + + + it('READ existing document', async () => { + let res = await self.instance.get(`${self.urlResource}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.should.containEql(self.docOriginal); + self.docActual = res.body.result; + + if (self.historyTimestamp >= self.docActual.srvModified) { + self.historyTimestamp = self.docActual.srvModified - 1; + } + }); + + + it('SEARCH existing document (found)', async () => { + let res = await self.instance.get(`${self.urlCol}`, self.jwt.read) + .query({ 'identifier$eq': self.identifier }) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.length.should.be.above(0); + res.body.result.should.matchAny(value => { + value.identifier.should.be.eql(self.identifier); + }); + }); + + + it('new document in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('UPDATE document', async () => { + self.docActual.insulin = 0.5; + + let res = await self.instance.put(`${self.urlResource}`, self.jwt.update) + .send(self.docActual) + .expect(200); + + res.body.status.should.equal(200); + self.docActual.subject = self.subject.apiUpdate.name; + delete self.docActual.srvModified; + + self.cache.nextShouldEql(self.col, self.docActual) + }); + + + it('document changed in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('document changed in READ', async () => { + let res = await self.instance.get(`${self.urlResource}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + delete self.docActual.srvModified; + res.body.result.should.containEql(self.docActual); + self.docActual = res.body.result; + }); + + + it('PATCH document', async () => { + self.docActual.carbs = 5; + self.docActual.insulin = 0.4; + + let res = await self.instance.patch(`${self.urlResource}`, self.jwt.update) + .send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin }) + .expect(200); + + res.body.status.should.equal(200); + delete self.docActual.srvModified; + + self.cache.nextShouldEql(self.col, self.docActual) + }); + + + it('document changed in HISTORY', async () => { + await self.checkHistoryExistence(); + }); + + + it('document changed in READ', async () => { + let res = await self.instance.get(`${self.urlResource}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + delete self.docActual.srvModified; + res.body.result.should.containEql(self.docActual); + self.docActual = res.body.result; + }); + + + it('soft DELETE', async () => { + let res = await self.instance.delete(`${self.urlResource}`, self.jwt.delete) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('READ of deleted is gone', async () => { + await self.instance.get(`${self.urlResource}`, self.jwt.read) + .expect(410); + }); + + + + it('SEARCH of deleted document missing it', async () => { + let res = await self.instance.get(`${self.urlCol}`, self.jwt.read) + .query({ 'identifier_eq': self.identifier }) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.should.have.length(0); + }); + + + it('document deleted in HISTORY', async () => { + await self.checkHistoryExistence(value => { + value.isValid.should.be.eql(false); + }); + }); + + + it('permanent DELETE', async () => { + let res = await self.instance.delete(`${self.urlResource}`, self.jwt.delete) + .query({ 'permanent': 'true' }) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col) + }); + + + it('READ of permanently deleted is not found', async () => { + await self.instance.get(`${self.urlResource}`, self.jwt.read) + .expect(404); + }); + + + it('document permanently deleted not in HISTORY', async () => { + let res = await self.instance.get(`${self.urlHistory}/${self.historyTimestamp}`, self.jwt.read); + + res.body.status.should.equal(200); + res.body.result.should.matchEach(value => { + value.identifier.should.not.be.eql(self.identifier); + }); + }); + + + it('should not modify read-only document', async () => { + await self.instance.post(`${self.urlCol}`, self.jwt.create) + .send(Object.assign({}, self.docOriginal, { isReadOnly: true })) + .expect(201); + + let res = await self.instance.get(`${self.urlResource}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + self.docActual = res.body.result; + delete self.docActual.srvModified; + const readOnlyMessage = 'Trying to modify read-only document'; + + self.cache.nextShouldEql(self.col, self.docActual) + self.cache.shouldBeEmpty() + + res = await self.instance.post(`${self.urlCol}`, self.jwt.update) + .send(Object.assign({}, self.docActual, { insulin: 0.41 })) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.put(`${self.urlResource}`, self.jwt.update) + .send(Object.assign({}, self.docActual, { insulin: 0.42 })) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.patch(`${self.urlResource}`, self.jwt.update) + .send({ insulin: 0.43 }) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.delete(`${self.urlResource}`, self.jwt.delete) + .query({ 'permanent': 'true' }) + .expect(422); + res.body.message.should.equal(readOnlyMessage); + + res = await self.instance.get(`${self.urlResource}`, self.jwt.read) + .expect(200); + res.body.status.should.equal(200); + res.body.result.should.containEql(self.docOriginal); + }); + +}); + diff --git a/tests/api3.patch.test.js b/tests/api3.patch.test.js new file mode 100644 index 00000000000..3e729d2dd56 --- /dev/null +++ b/tests/api3.patch.test.js @@ -0,0 +1,239 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 PATCH', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.validDoc = { + date: (new Date()).getTime(), + utcOffset: -180, + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 PATCH', + eventType: 'Correction Bolus', + insulin: 0.3 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + return res.body.result; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.col = 'treatments'; + self.url = `/api/v3/${self.col}`; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'create', + 'update', + 'read' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.urlIdent = `${self.url}/${self.validDoc.identifier}`; + self.cache = self.instance.cacheMonitor; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + beforeEach(() => { + self.cache.clear(); + }); + + + afterEach(() => { + self.cache.shouldBeEmpty(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.patch(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.patch(`/api/v3/NOT_EXIST`, self.jwt.update) + .send(self.validDoc) + .expect(404); + + res.body.status.should.equal(404); + }); + + + it('should not found not existing document', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(self.validDoc) + .expect(404); + + res.body.status.should.equal(404); + + // now let's insert the document for further patching + res = await self.instance.post(`${self.url}`, self.jwt.create) + .send(self.validDoc) + .expect(201); + + res.body.status.should.equal(201); + self.cache.nextShouldEql(self.col, self.validDoc) + }); + + + it('should reject identifier alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { identifier: 'MODIFIED'})) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field identifier cannot be modified by the client'); + }); + + + it('should reject date alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { date: self.validDoc.date + 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field date cannot be modified by the client'); + }); + + + it('should reject utcOffset alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { utcOffset: self.utcOffset - 120 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field utcOffset cannot be modified by the client'); + }); + + + it('should reject eventType alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { eventType: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field eventType cannot be modified by the client'); + }); + + + it('should reject device alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { device: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field device cannot be modified by the client'); + }); + + + it('should reject app alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { app: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field app cannot be modified by the client'); + }); + + + it('should reject srvCreated alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { srvCreated: self.validDoc.date - 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvCreated cannot be modified by the client'); + }); + + + it('should reject subject alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { subject: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field subject cannot be modified by the client'); + }); + + + it('should reject srvModified alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { srvModified: self.validDoc.date - 100000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvModified cannot be modified by the client'); + }); + + + it('should reject modifiedBy alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { modifiedBy: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field modifiedBy cannot be modified by the client'); + }); + + + it('should reject isValid alteration', async () => { + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { isValid: false })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field isValid cannot be modified by the client'); + }); + + + it('should patch document', async () => { + self.validDoc.carbs = 10; + + let res = await self.instance.patch(self.urlIdent, self.jwt.update) + .send(self.validDoc) + .expect(200); + + res.body.status.should.equal(200); + + let body = await self.get(self.validDoc.identifier); + body.carbs.should.equal(10); + body.insulin.should.equal(0.3); + body.subject.should.equal(self.subject.apiCreate.name); + body.modifiedBy.should.equal(self.subject.apiUpdate.name); + + self.cache.nextShouldEql(self.col, body) + }); + +}); + diff --git a/tests/api3.read.test.js b/tests/api3.read.test.js new file mode 100644 index 00000000000..c0dbbf1fe32 --- /dev/null +++ b/tests/api3.read.test.js @@ -0,0 +1,224 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 READ', function () { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.validDoc = { + date: (new Date()).getTime(), + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 READ', + uploaderBattery: 58 + }; + self.validDoc.identifier = opTools.calculateIdentifier(self.validDoc); + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.col = 'devicestatus'; + self.url = `/api/v3/${self.col}`; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'create', + 'read', + 'delete' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.cache = self.instance.cacheMonitor; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + beforeEach(() => { + self.cache.clear(); + }); + + + afterEach(() => { + self.cache.shouldBeEmpty(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.get(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.get(`/api/v3/NOT_EXIST/NOT_EXIST`, self.jwt.read) + .send(self.validDoc) + .expect(404); + + res.body.status.should.equal(404); + should.not.exist(res.body.result); + self.cache.shouldBeEmpty() + }); + + + it('should not found not existing document', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}`, self.jwt.read) + .expect(404); + + res.body.status.should.equal(404); + should.not.exist(res.body.result); + self.cache.shouldBeEmpty() + }); + + + it('should read just created document', async () => { + let res = await self.instance.post(`${self.url}`, self.jwt.create) + .send(self.validDoc) + .expect(201); + + res.body.status.should.equal(201); + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + const result = res.body.result; + result.should.containEql(self.validDoc); + result.should.have.property('srvCreated').which.is.a.Number(); + result.should.have.property('srvModified').which.is.a.Number(); + result.should.have.property('subject'); + self.validDoc.subject = result.subject; // let's store subject for later tests + + self.cache.nextShouldEql(self.col, self.validDoc) + }); + + + it('should contain only selected fields', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=date,device,subject`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + const correct = { + date: self.validDoc.date, + device: self.validDoc.device, + subject: self.validDoc.subject + }; + res.body.result.should.eql(correct); + }); + + + it('should contain all fields', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}?fields=_all`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + for (let fieldName of ['app', 'date', 'device', 'identifier', 'srvModified', 'uploaderBattery', 'subject']) { + res.body.result.should.have.property(fieldName); + } + }); + + + it('should not send unmodified document since', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}`, self.jwt.read) + .set('If-Modified-Since', new Date(new Date().getTime() + 1000).toUTCString()) + .expect(304); + + res.body.should.be.empty(); + }); + + + it('should send modified document since', async () => { + let res = await self.instance.get(`${self.url}/${self.validDoc.identifier}`, self.jwt.read) + .set('If-Modified-Since', new Date(new Date(self.validDoc.date).getTime() - 1000).toUTCString()) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.should.containEql(self.validDoc); + }); + + + it('should recognize softly deleted document', async () => { + let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}`, self.jwt.delete) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col) + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}`, self.jwt.read) + .expect(410); + + res.body.status.should.equal(410); + should.not.exist(res.body.result); + }); + + + it('should not find permanently deleted document', async () => { + let res = await self.instance.delete(`${self.url}/${self.validDoc.identifier}?permanent=true`, self.jwt.delete) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col) + + res = await self.instance.get(`${self.url}/${self.validDoc.identifier}`, self.jwt.read) + .expect(404); + + res.body.status.should.equal(404); + should.not.exist(res.body.result); + }); + + + it('should find document created by APIv1', async () => { + + const doc = Object.assign({}, self.validDoc, { + created_at: new Date(self.validDoc.date).toISOString() + }); + delete doc.identifier; + + await new Promise((resolve, reject) => { + self.instance.ctx.devicestatus.create([doc], async (err) => { // let's insert the document in APIv1's way + + should.not.exist(err); + doc._id = doc._id.toString(); + self.cache.nextShouldEql(self.col, doc) + + err ? reject(err) : resolve(doc); + }); + }); + + const identifier = doc._id.toString(); + delete doc._id; + + let res = await self.instance.get(`${self.url}/${identifier}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.should.containEql(doc); + + res = await self.instance.delete(`${self.url}/${identifier}?permanent=true`, self.jwt.delete) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col) + }); + + +}) +; + diff --git a/tests/api3.renderer.test.js b/tests/api3.renderer.test.js new file mode 100644 index 00000000000..5946a431923 --- /dev/null +++ b/tests/api3.renderer.test.js @@ -0,0 +1,292 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +require('should'); + +describe('API3 output renderers', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + , _ = require('lodash') + , xml2js = require('xml2js') + , csvParse = require('csv-parse/lib/sync') + ; + + self.historyFrom = (new Date()).getTime() - 1000; // starting timestamp for HISTORY operations + + self.doc1 = testConst.SAMPLE_ENTRIES[0]; + self.doc1.date = (new Date()).getTime() - (5 * 60 * 1000); + self.doc1.identifier = opTools.calculateIdentifier(self.doc1); + + self.doc2 = testConst.SAMPLE_ENTRIES[1]; + self.doc2.date = (new Date()).getTime(); + self.doc2.identifier = opTools.calculateIdentifier(self.doc2); + + self.xmlParser = new xml2js.Parser({ + explicitArray: false + }); + + self.csvParserOptions = { + columns: true, + skip_empty_lines: true + }; + + self.timeout(15000); + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.col = 'entries'; + self.url = `/api/v3/${self.col}`; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'create', + 'read', + 'delete' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.cache = self.instance.cacheMonitor; + }); + + + after(() => { + self.instance.server.close(); + }); + + + beforeEach(() => { + self.cache.clear(); + }); + + + afterEach(() => { + self.cache.shouldBeEmpty(); + }); + + + /** + * Checks if all properties from obj1 are string identical in obj2 + * (comparison of properties is made using toString()) + * @param {Object} obj1 + * @param {Object} obj2 + */ + self.checkProps = function checkProps (obj1, obj2) { + for (let propName in obj1) { + obj1[propName].toString().should.eql(obj2[propName].toString()); + } + }; + + + /** + * Checks if all objects from arrModel exist in arr + * (with string identical properties) + * @param arrModel + * @param arr + */ + self.checkItems = function checkItems (arrModel, arr) { + for (let itemModel of arrModel) { + const item = _.find(arr, (doc) => doc.identifier === itemModel.identifier); + item.should.not.be.empty(); + self.checkProps(itemModel, item); + } + }; + + + /** + * Checks if given text is valid XML. + * Next checks if all objects from arrModel exist in parsed array + * (with string identical properties) + * @param arrModel + * @param xmlText + * @returns {Promise} + */ + self.checkXmlItems = async function checkXmlItems (arrModel, xmlText) { + xmlText.should.startWith(''); + + const xml = await self.xmlParser.parseStringPromise(xmlText); + xml.items.should.not.be.empty(); + let items = xml.items.item; + items.should.be.Array(); + items.length.should.be.aboveOrEqual(arrModel.length); + + self.checkItems(arrModel, items); + }; + + + /** + * Checks if given text is valid CSV. + * Next checks if all objects from arrModel exist in parsed array + * (with string identical properties) + * @param arrModel + * @param csvText + * @returns {Promise} + */ + self.checkCsvItems = async function checkXmlItems (arrModel, csvText) { + csvText.should.not.be.empty(); + + const items = csvParse(csvText, self.csvParserOptions); + items.should.be.Array(); + items.length.should.be.aboveOrEqual(arrModel.length); + + self.checkItems(arrModel, items); + }; + + + it('should create 2 mock documents', async () => { + + async function createDoc (doc) { + + let res = await self.instance.post(`${self.url}`, self.jwt.create) + .send(doc) + .expect(201); + + res.body.status.should.equal(201); + + res = await self.instance.get(`${self.url}/${doc.identifier}`, self.jwt.read) + .expect(200); + return res.body; + } + + self.doc1json = await createDoc(self.doc1); + self.cache.nextShouldEql(self.col, self.doc1) + + self.doc2json = await createDoc(self.doc2); + self.cache.nextShouldEql(self.col, self.doc2) + }); + + + it('READ/SEARCH/HISTORY should not accept unsupported content type', async () => { + + async function check406 (request) { + const res = await request + .expect(406); + res.status.should.equal(406); + res.body.message.should.eql('Unsupported output format requested'); + should.not.exist(res.body.result); + } + + await check406(self.instance.get(`${self.url}/${self.doc1.identifier}.ttf?fields=_all`, self.jwt.read)); + await check406(self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all`, self.jwt.read) + .set('Accept', 'font/ttf')); + + await check406(self.instance.get(`${self.url}.ttf?fields=_all`, self.jwt.read)); + await check406(self.instance.get(`${self.url}?fields=_all`, self.jwt.read) + .set('Accept', 'font/ttf')); + + await check406(self.instance.get(`${self.url}/history/${self.doc1.date}.ttf`, self.jwt.read)); + await check406(self.instance.get(`${self.url}/history/${self.doc1.date}`, self.jwt.read) + .set('Accept', 'font/ttf')); + }); + + + it('READ should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}/${self.doc1.identifier}.xml?fields=_all`, self.jwt.read) + .expect(200); + + res.text.should.startWith(''); + + const xml = await self.xmlParser.parseStringPromise(res.text); + xml.item.should.not.be.empty(); + self.checkProps(self.doc1, xml.item); + + let res2 = await self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all`, self.jwt.read) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.eql(res2.text); + }); + + + it('READ should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}/${self.doc1.identifier}.csv?fields=_all`, self.jwt.read) + .expect(200); + + await self.checkCsvItems([self.doc1], res.text); + + let res2 = await self.instance.get(`${self.url}/${self.doc1.identifier}?fields=_all`, self.jwt.read) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.eql(res2.text); + }); + + + it('SEARCH should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}.xml?date$gte=${self.doc1.date}`, self.jwt.read) + .expect(200); + + await self.checkXmlItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}?date$gte=${self.doc1.date}`, self.jwt.read) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('SEARCH should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}.csv?date$gte=${self.doc1.date}`, self.jwt.read) + .expect(200); + + await self.checkCsvItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}?date$gte=${self.doc1.date}`, self.jwt.read) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('HISTORY should accept xml content type', async () => { + let res = await self.instance.get(`${self.url}/history/${self.historyFrom}.xml`, self.jwt.read) + .expect(200); + + await self.checkXmlItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}/history/${self.historyFrom}`, self.jwt.read) + .set('Accept', 'application/xml') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('HISTORY should accept csv content type', async () => { + let res = await self.instance.get(`${self.url}/history/${self.historyFrom}.csv`, self.jwt.read) + .expect(200); + + await self.checkCsvItems([self.doc1, self.doc2], res.text); + + let res2 = await self.instance.get(`${self.url}/history/${self.historyFrom}`, self.jwt.read) + .set('Accept', 'text/csv') + .expect(200); + + res.text.should.be.eql(res2.text); + }); + + + it('should remove mock documents', async () => { + + async function deleteDoc (identifier) { + let res = await self.instance.delete(`${self.url}/${identifier}`, self.jwt.delete) + .query({ 'permanent': 'true' }) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col); + } + + await deleteDoc(self.doc1.identifier); + await deleteDoc(self.doc2.identifier); + }); +}); + diff --git a/tests/api3.search.test.js b/tests/api3.search.test.js new file mode 100644 index 00000000000..aeafc84630c --- /dev/null +++ b/tests/api3.search.test.js @@ -0,0 +1,281 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 SEARCH', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , opTools = require('../lib/api3/shared/operationTools') + ; + + self.docs = testConst.SAMPLE_ENTRIES; + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = function get (identifier, done) { + self.instance.get(`${self.url}/${identifier}`, self.jwt.read) + .expect(200) + .end((err, res) => { + should.not.exist(err); + done(res.body); + }); + }; + + + /** + * Create given document in a promise + */ + self.create = (doc) => new Promise((resolve) => { + doc.identifier = opTools.calculateIdentifier(doc); + self.instance.post(`${self.url}`, self.jwt.all) + .send(doc) + .end((err) => { + should.not.exist(err); + self.get(doc.identifier, resolve); + }); + }); + + + before(async () => { + self.testStarted = new Date(); + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.url = '/api/v3/entries'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'read', + 'all' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.urlTest = `${self.url}?srvModified$gte=${self.testStarted.getTime()}`; + + const promises = testConst.SAMPLE_ENTRIES.map(doc => self.create(doc)); + self.docs = await Promise.all(promises); + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.get(self.url) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + should.not.exist(res.body.result); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.get(`/api/v3/NOT_EXIST`, self.jwt.read) + .send(self.validDoc) + .expect(404); + + res.body.status.should.equal(404); + should.not.exist(res.body.result); + }); + + + it('should found at least 10 documents', async () => { + let res = await self.instance.get(self.url, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.length.should.be.aboveOrEqual(self.docs.length); + }); + + + it('should found at least 10 documents from test start', async () => { + let res = await self.instance.get(self.urlTest, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.length.should.be.aboveOrEqual(self.docs.length); + }); + + + it('should reject invalid limit - not a number', async () => { + let res = await self.instance.get(`${self.url}?limit=INVALID`, self.jwt.read) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter limit out of tolerance'); + should.not.exist(res.body.result); + }); + + + it('should reject invalid limit - negative number', async () => { + let res = await self.instance.get(`${self.url}?limit=-1`, self.jwt.read) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter limit out of tolerance'); + should.not.exist(res.body.result); + }); + + + it('should reject invalid limit - zero', async () => { + let res = await self.instance.get(`${self.url}?limit=0`, self.jwt.read) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter limit out of tolerance'); + should.not.exist(res.body.result); + }); + + + it('should accept valid limit', async () => { + let res = await self.instance.get(`${self.url}?limit=3`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.length.should.equal(3); + }); + + + it('should reject invalid skip - not a number', async () => { + let res = await self.instance.get(`${self.url}?skip=INVALID`, self.jwt.read) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter skip out of tolerance'); + should.not.exist(res.body.result); + }); + + + it('should reject invalid skip - negative number', async () => { + let res = await self.instance.get(`${self.url}?skip=-5`, self.jwt.read) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter skip out of tolerance'); + should.not.exist(res.body.result); + }); + + + it('should reject both sort and sort$desc', async () => { + let res = await self.instance.get(`${self.url}?sort=date&sort$desc=created_at`, self.jwt.read) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Parameters sort and sort_desc cannot be combined'); + should.not.exist(res.body.result); + }); + + + it('should sort well by date field', async () => { + let res = await self.instance.get(`${self.urlTest}&sort=date`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + const ascending = res.body.result; + const length = ascending.length; + length.should.be.aboveOrEqual(self.docs.length); + + res = await self.instance.get(`${self.urlTest}&sort$desc=date`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + const descending = res.body.result; + descending.length.should.equal(length); + + for (let i in ascending) { + ascending[i].should.eql(descending[length - i - 1]); + + if (i > 0) { + ascending[i - 1].date.should.be.lessThanOrEqual(ascending[i].date); + } + } + }); + + + it('should skip documents', async () => { + let res = await self.instance.get(`${self.url}?sort=date&limit=8`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + const fullDocs = res.body.result; + fullDocs.length.should.equal(8); + + res = await self.instance.get(`${self.url}?sort=date&skip=3&limit=5`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + const skipDocs = res.body.result; + skipDocs.length.should.equal(5); + + for (let i = 0; i < 3; i++) { + skipDocs[i].should.be.eql(fullDocs[i + 3]); + } + }); + + + it('should project selected fields', async () => { + let res = await self.instance.get(`${self.url}?fields=date,app,subject`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.forEach(doc => { + const docFields = Object.getOwnPropertyNames(doc); + docFields.sort().should.be.eql(['app', 'date', 'subject']); + }); + }); + + + it('should project all fields', async () => { + let res = await self.instance.get(`${self.url}?fields=_all`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.forEach(doc => { + Object.getOwnPropertyNames(doc).length.should.be.aboveOrEqual(10); + Object.prototype.hasOwnProperty.call(doc, '_id').should.not.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'identifier').should.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'srvModified').should.be.true(); + Object.prototype.hasOwnProperty.call(doc, 'srvCreated').should.be.true(); + }); + }); + + + it('should not exceed the limit of docs count', async () => { + const apiApp = self.instance.ctx.apiApp + , limitBackup = apiApp.get('API3_MAX_LIMIT'); + apiApp.set('API3_MAX_LIMIT', 5); + let res = await self.instance.get(`${self.url}?limit=10`, self.jwt.read) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Parameter limit out of tolerance'); + apiApp.set('API3_MAX_LIMIT', limitBackup); + }); + + + it('should respect the ceiling (hard) limit of docs', async () => { + const apiApp = self.instance.ctx.apiApp + , limitBackup = apiApp.get('API3_MAX_LIMIT'); + apiApp.set('API3_MAX_LIMIT', 5); + let res = await self.instance.get(`${self.url}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + res.body.result.length.should.equal(5); + apiApp.set('API3_MAX_LIMIT', limitBackup); + }); + +}); + diff --git a/tests/api3.security.test.js b/tests/api3.security.test.js new file mode 100644 index 00000000000..4899081fa2f --- /dev/null +++ b/tests/api3.security.test.js @@ -0,0 +1,78 @@ +/* eslint require-atomic-updates: 0 */ +'use strict'; + +const request = require('supertest') + , apiConst = require('../lib/api3/const.json') + ; +require('should'); + +describe('Security of REST API3', function() { + const self = this + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + ; + + this.timeout(30000); + + + before(async () => { + self.http = await instance.create({ useHttps: false }); + self.https = await instance.create({ }); + + let authResult = await authSubject(self.https.ctx.authorization.storage, [ + 'denied', + 'read', + 'delete' + ], self.https.app); + self.subject = authResult.subject; + self.jwt = authResult.jwt; + }); + + + after(() => { + self.http.ctx.bus.teardown(); + self.https.ctx.bus.teardown(); + }); + + + it('should require token', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_MISSING_OR_BAD_TOKEN); + }); + + + it('should require valid token', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Authorization', 'Bearer invalid_token') + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal(apiConst.MSG.HTTP_401_BAD_TOKEN); + }); + + + it('should deny subject denied', async () => { + let res = await request(self.https.baseUrl) + .get('/api/v3/test') + .set('Authorization', `Bearer ${self.jwt.denied}`) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal(apiConst.MSG.HTTP_403_MISSING_PERMISSION.replace('{0}', 'api:entries:read')); + }); + + + it('should allow subject with read permission', async () => { + await request(self.https.baseUrl) + .get('/api/v3/test', self.jwt.read) + .set('Authorization', `Bearer ${self.jwt.read}`) + .expect(200); + }); + + +}); diff --git a/tests/api3.socket.test.js b/tests/api3.socket.test.js new file mode 100644 index 00000000000..fe71b04e019 --- /dev/null +++ b/tests/api3.socket.test.js @@ -0,0 +1,183 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('Socket.IO in REST API3', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , apiConst = require('../lib/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , utils = require('./fixtures/api3/utils') + ; + + self.identifier = utils.randomString('32', 'aA#'); // let's have a brand new identifier for your testing document + + self.docOriginal = { + identifier: self.identifier, + eventType: 'Correction Bolus', + insulin: 1, + date: (new Date()).getTime(), + app: testConst.TEST_APP + }; + + this.timeout(30000); + + before(async () => { + self.instance = await instance.create({ + storageSocket: true + }); + + self.app = self.instance.app; + self.env = self.instance.env; + self.colName = 'treatments'; + self.urlCol = `/api/v3/${self.colName}`; + self.urlResource = self.urlCol + '/' + self.identifier; + self.urlHistory = self.urlCol + '/history'; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'create', + 'update', + 'delete' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.accessToken = authResult.accessToken; + self.socket = self.instance.clientSocket; + }); + + + after(() => { + if(self.instance && self.instance.clientSocket && self.instance.clientSocket.connected) { + self.instance.clientSocket.disconnect(); + } + self.instance.ctx.bus.teardown(); + }); + + + it('should not subscribe without accessToken', done => { + self.socket.emit('subscribe', { }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN); + done(); + }); + }); + + + it('should not subscribe by invalid accessToken', done => { + self.socket.emit('subscribe', { accessToken: 'INVALID' }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_MISSING_OR_BAD_ACCESS_TOKEN); + done(); + }); + }); + + + it('should not subscribe by subject with no rights', done => { + self.socket.emit('subscribe', { accessToken: self.accessToken.denied }, function (data) { + data.success.should.not.equal(true); + data.message.should.equal(apiConst.MSG.SOCKET_UNAUTHORIZED_TO_ANY); + done(); + }); + }); + + + it('should subscribe by valid accessToken', done => { + const cols = ['entries', 'treatments']; + + self.socket.emit('subscribe', { + accessToken: self.accessToken.all, + collections: cols + }, function (data) { + data.success.should.equal(true); + should(data.collections.sort()).be.eql(cols); + done(); + }); + }); + + + it('should emit create event on CREATE', done => { + + self.socket.once('create', (event) => { + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docOriginal); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.post(`${self.urlCol}`, self.jwt.create) + .send(self.docOriginal) + .expect(201) + .end((err) => { + should.not.exist(err); + }); + }); + + + it('should emit update event on UPDATE', done => { + + self.docActual.insulin = 0.5; + + self.socket.once('update', (event) => { + delete self.docActual.srvModified; + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docActual); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.put(`${self.urlResource}`, self.jwt.update) + .send(self.docActual) + .expect(200) + .end((err) => { + should.not.exist(err); + self.docActual.subject = self.subject.apiUpdate.name; + }); + }); + + + it('should emit update event on PATCH', done => { + + self.docActual.carbs = 5; + self.docActual.insulin = 0.4; + + self.socket.once('update', (event) => { + delete self.docActual.srvModified; + event.colName.should.equal(self.colName); + event.doc.should.containEql(self.docActual); + delete event.doc.subject; + self.docActual = event.doc; + done(); + }); + + self.instance.patch(`${self.urlResource}`, self.jwt.update) + .send({ 'carbs': self.docActual.carbs, 'insulin': self.docActual.insulin }) + .expect(200) + .end((err) => { + should.not.exist(err); + }); + }); + + + it('should emit delete event on DELETE', done => { + + self.socket.once('delete', (event) => { + event.colName.should.equal(self.colName); + event.identifier.should.equal(self.identifier); + done(); + }); + + self.instance.delete(`${self.urlResource}`, self.jwt.delete) + .expect(200) + .end((err) => { + should.not.exist(err); + }); + }); + +}); + diff --git a/tests/api3.update.test.js b/tests/api3.update.test.js new file mode 100644 index 00000000000..14f4fb877fb --- /dev/null +++ b/tests/api3.update.test.js @@ -0,0 +1,315 @@ +/* eslint require-atomic-updates: 0 */ +/* global should */ +'use strict'; + +require('should'); + +describe('API3 UPDATE', function() { + const self = this + , testConst = require('./fixtures/api3/const.json') + , instance = require('./fixtures/api3/instance') + , authSubject = require('./fixtures/api3/authSubject') + , utils = require('./fixtures/api3/utils') + ; + + self.validDoc = { + identifier: utils.randomString('32', 'aA#'), + date: (new Date()).getTime(), + utcOffset: -180, + app: testConst.TEST_APP, + device: testConst.TEST_DEVICE + ' API3 UPDATE', + eventType: 'Correction Bolus', + insulin: 0.3 + }; + + self.timeout(15000); + + + /** + * Get document detail for futher processing + */ + self.get = async function get (identifier) { + let res = await self.instance.get(`${self.url}/${identifier}`, self.jwt.read) + .expect(200); + + res.body.status.should.equal(200); + return res.body.result; + }; + + + before(async () => { + self.instance = await instance.create({}); + + self.app = self.instance.app; + self.env = self.instance.env; + self.col = 'treatments' + self.url = `/api/v3/${self.col}`; + + let authResult = await authSubject(self.instance.ctx.authorization.storage, [ + 'read', + 'update', + 'delete', + 'all' + ], self.instance.app); + + self.subject = authResult.subject; + self.jwt = authResult.jwt; + self.urlIdent = `${self.url}/${self.validDoc.identifier}` + self.cache = self.instance.cacheMonitor; + }); + + + after(() => { + self.instance.ctx.bus.teardown(); + }); + + + beforeEach(() => { + self.cache.clear(); + }); + + + afterEach(() => { + self.cache.shouldBeEmpty(); + }); + + + it('should require authentication', async () => { + let res = await self.instance.put(`${self.url}/FAKE_IDENTIFIER`) + .expect(401); + + res.body.status.should.equal(401); + res.body.message.should.equal('Missing or bad access token or JWT'); + }); + + + it('should not found not existing collection', async () => { + let res = await self.instance.put(`/api/v3/NOT_EXIST`, self.jwt.update) + .send(self.validDoc) + .expect(404); + + res.body.status.should.equal(404); + }); + + + it('should require update permission for upsert', async () => { + let res = await self.instance.put(`${self.url}/${self.validDoc.identifier}`, self.jwt.update) + .send(self.validDoc) + .expect(403); + + res.body.status.should.equal(403); + res.body.message.should.equal('Missing permission api:treatments:create'); + }); + + + it('should upsert not existing document', async () => { + let res = await self.instance.put(`${self.url}/${self.validDoc.identifier}`, self.jwt.all) + .send(self.validDoc) + .expect(201); + + res.body.status.should.equal(201); + res.body.identifier.should.equal(self.validDoc.identifier); + self.cache.nextShouldEql(self.col, self.validDoc) + + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier, self.jwt.read); + body.should.containEql(self.validDoc); + should.not.exist(body.modifiedBy); + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + (body.srvCreated - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiAll.name); + }); + + + it('should update the document', async () => { + self.validDoc.carbs = 10; + delete self.validDoc.insulin; + + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(self.validDoc) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldEql(self.col, self.validDoc) + + const lastModified = new Date(res.headers['last-modified']).getTime(); // Last-Modified has trimmed milliseconds + + let body = await self.get(self.validDoc.identifier, self.jwt.read); + body.should.containEql(self.validDoc); + should.not.exist(body.insulin); + should.not.exist(body.modifiedBy); + + const ms = body.srvModified % 1000; + (body.srvModified - ms).should.equal(lastModified); + body.subject.should.equal(self.subject.apiUpdate.name); + }); + + + it('should update unmodified document since', async () => { + const doc = Object.assign({}, self.validDoc, { + carbs: 11 + }); + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .set('If-Unmodified-Since', new Date(new Date().getTime() + 1000).toUTCString()) + .send(doc) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldEql(self.col, doc) + + let body = await self.get(self.validDoc.identifier, self.jwt.read); + body.should.containEql(doc); + }); + + + it('should not update document modified since', async () => { + const doc = Object.assign({}, self.validDoc, { + carbs: 12 + }); + let body = await self.get(doc.identifier); + self.validDoc = body; + + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .set('If-Unmodified-Since', new Date(new Date(body.srvModified).getTime() - 1000).toUTCString()) + .send(doc) + .expect(412); + + res.body.status.should.equal(412); + + body = await self.get(doc.identifier, self.jwt.read); + body.should.eql(self.validDoc); + }); + + + it('should reject date alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { date: self.validDoc.date + 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field date cannot be modified by the client'); + }); + + + it('should reject utcOffset alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { utcOffset: self.utcOffset - 120 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field utcOffset cannot be modified by the client'); + }); + + + it('should reject eventType alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { eventType: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field eventType cannot be modified by the client'); + }); + + + it('should reject device alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { device: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field device cannot be modified by the client'); + }); + + + it('should reject app alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { app: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field app cannot be modified by the client'); + }); + + + it('should reject srvCreated alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { srvCreated: self.validDoc.date - 10000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvCreated cannot be modified by the client'); + }); + + + it('should reject subject alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { subject: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field subject cannot be modified by the client'); + }); + + + it('should reject srvModified alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { srvModified: self.validDoc.date - 100000 })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field srvModified cannot be modified by the client'); + }); + + + it('should reject modifiedBy alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { modifiedBy: 'MODIFIED' })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field modifiedBy cannot be modified by the client'); + }); + + + it('should reject isValid alteration', async () => { + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { isValid: false })) + .expect(400); + + res.body.status.should.equal(400); + res.body.message.should.equal('Field isValid cannot be modified by the client'); + }); + + + it('should ignore identifier alteration in body', async () => { + self.validDoc = await self.get(self.validDoc.identifier); + + let res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(Object.assign({}, self.validDoc, { identifier: 'MODIFIED' })) + .expect(200); + + res.body.status.should.equal(200); + delete self.validDoc.srvModified; + self.cache.nextShouldEql(self.col, self.validDoc) + }); + + + it('should not update deleted document', async () => { + let res = await self.instance.delete(self.urlIdent, self.jwt.delete) + .expect(200); + + res.body.status.should.equal(200); + self.cache.nextShouldDeleteLast(self.col) + + res = await self.instance.put(self.urlIdent, self.jwt.update) + .send(self.validDoc) + .expect(410); + + res.body.status.should.equal(410); + }); + +}); + diff --git a/tests/ar2.test.js b/tests/ar2.test.js index 9d3fed4a397..3b0d344a66f 100644 --- a/tests/ar2.test.js +++ b/tests/ar2.test.js @@ -1,58 +1,46 @@ 'use strict'; -var should = require('should'); -var levels = require('../lib/levels'); +const should = require('should'); +const helper = require('./inithelper')(); -var FIVE_MINS = 300000; -var SIX_MINS = 360000; +const FIVE_MINS = 300000; +const SIX_MINS = 360000; describe('ar2', function ( ) { + var ctx = helper.getctx(); - var ar2 = require('../lib/plugins/ar2')(); - var delta = require('../lib/plugins/delta')(); - - var env = require('../env')(); - var ctx = {}; - ctx.data = require('../lib/data')(env, ctx); + ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); + var ar2 = require('../lib/plugins/ar2')(ctx); + var bgnow = require('../lib/plugins/bgnow')(ctx); + + var env = require('../lib/server/env')(); + var now = Date.now(); var before = now - FIVE_MINS; function prepareSandbox(base) { var sbx = base || require('../lib/sandbox')().serverInit(env, ctx); + bgnow.setProperties(sbx); ar2.setProperties(sbx); - delta.setProperties(sbx); return sbx; } - function rawSandbox() { - var envRaw = require('../env')(); - envRaw.extendedSettings = {'ar2': {useRaw: true}}; - - var sbx = require('../lib/sandbox')().serverInit(envRaw, ctx).withExtendedSettings(ar2); - - sbx.offerProperty('rawbg', function setFakeRawBG() { - return {}; - }); - - return prepareSandbox(sbx); - } - it('should plot a cone', function () { - ctx.data.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; var sbx = prepareSandbox(); var cone = ar2.forecastCone(sbx); cone.length.should.equal(26); }); it('should plot a line if coneFactor is 0', function () { - ctx.data.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; - var env0 = require('../env')(); + var env0 = require('../lib/server/env')(); env0.extendedSettings = { ar2: { coneFactor: 0 } }; var sbx = require('../lib/sandbox')().serverInit(env0, ctx).withExtendedSettings(ar2); - + bgnow.setProperties(sbx); var cone = ar2.forecastCone(sbx); cone.length.should.equal(13); }); @@ -60,7 +48,7 @@ describe('ar2', function ( ) { it('Not trigger an alarm when in range', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; var sbx = prepareSandbox(); ar2.checkNotifications(sbx); @@ -71,7 +59,7 @@ describe('ar2', function ( ) { it('should trigger a warning when going above target', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 150, mills: before}, {mgdl: 170, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 150, mills: before}, {mgdl: 170, mills: now}]; var sbx = prepareSandbox(); sbx.offerProperty('iob', function setFakeIOB() { @@ -82,7 +70,7 @@ describe('ar2', function ( ) { }); ar2.checkNotifications(sbx); var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.WARN); + highest.level.should.equal(helper.ctx.levels.WARN); highest.title.should.equal('Warning, HIGH predicted'); highest.message.should.equal('BG Now: 170 +20 ↗ mg/dl\nBG 15m: 206 mg/dl\nIOB: 1.25U'); @@ -91,12 +79,12 @@ describe('ar2', function ( ) { it('should trigger a urgent alarm when going high fast', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 140, mills: before}, {mgdl: 200, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 140, mills: before}, {mgdl: 200, mills: now}]; var sbx = prepareSandbox(); ar2.checkNotifications(sbx); var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.URGENT); + highest.level.should.equal(helper.ctx.levels.URGENT); highest.title.should.equal('Urgent, HIGH'); done(); @@ -104,12 +92,12 @@ describe('ar2', function ( ) { it('should trigger a warning when below target', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 90, mills: before}, {mgdl: 80, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 90, mills: before}, {mgdl: 80, mills: now}]; var sbx = prepareSandbox(); ar2.checkNotifications(sbx); var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.WARN); + highest.level.should.equal(helper.ctx.levels.WARN); highest.title.should.equal('Warning, LOW'); done(); @@ -117,12 +105,12 @@ describe('ar2', function ( ) { it('should trigger a warning when almost below target', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 90, mills: before}, {mgdl: 83, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 90, mills: before}, {mgdl: 83, mills: now}]; var sbx = prepareSandbox(); ar2.checkNotifications(sbx); var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.WARN); + highest.level.should.equal(helper.ctx.levels.WARN); highest.title.should.equal('Warning, LOW predicted'); done(); @@ -130,12 +118,12 @@ describe('ar2', function ( ) { it('should trigger a urgent alarm when falling fast', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 120, mills: before}, {mgdl: 85, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 120, mills: before}, {mgdl: 85, mills: now}]; var sbx = prepareSandbox(); ar2.checkNotifications(sbx); var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.URGENT); + highest.level.should.equal(helper.ctx.levels.URGENT); highest.title.should.equal('Urgent, LOW predicted'); done(); @@ -145,96 +133,31 @@ describe('ar2', function ( ) { ctx.notifications.initRequests(); //same as previous test but prev is 10 mins ago, so delta isn't enough to trigger an urgent alarm - ctx.data.sgvs = [{mgdl: 120, mills: before - SIX_MINS}, {mgdl: 85, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 120, mills: before - SIX_MINS}, {mgdl: 85, mills: now}]; var sbx = prepareSandbox(); ar2.checkNotifications(sbx); var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.WARN); + highest.level.should.equal(helper.ctx.levels.WARN); highest.title.should.equal('Warning, LOW predicted'); done(); }); - it('should include current raw bg and raw bg forecast when predicting w/raw', function (done) { - ctx.notifications.initRequests(); - ctx.data.sgvs = [{unfiltered: 113680, filtered: 111232, mgdl: 100, mills: before, noise: 1}, {unfiltered: 183680, filtered: 111232, mgdl: 100, mills: now, noise: 1}]; - ctx.data.cals = [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}]; - - var envRaw = require('../env')(); - envRaw.extendedSettings = {'ar2': {useRaw: true}}; - - var sbx = require('../lib/sandbox')().serverInit(envRaw, ctx).withExtendedSettings(ar2); + it('should handle virtAsst requests', function (done) { + var now = Date.now(); + var before = now - FIVE_MINS; - sbx.offerProperty('rawbg', function setFakeIOB() { - return {displayLine: 'Raw BG: 200 mg/dl Clean'}; - }); - sbx.offerProperty('iob', function setFakeIOB() { - return {displayLine: 'IOB: 1.25U'}; - }); - sbx.offerProperty('direction', function setFakeDirection() { - return {value: 'FortyFiveUp', label: '↗', entity: '↗'}; - }); - - sbx = prepareSandbox(sbx); - - ar2.checkNotifications(sbx.withExtendedSettings(ar2)); - - var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.WARN); - highest.title.should.equal('Warning, HIGH predicted w/raw'); - highest.message.should.equal('BG Now: 100 +0 ↗ mg/dl\nRaw BG: 200 mg/dl Clean\nRaw BG 15m: 400 mg/dl\nIOB: 1.25U'); - - done(); - }); - - it('should not trigger an alarm when raw is missing or 0', function (done) { - ctx.notifications.initRequests(); - ctx.data.sgvs = [{unfiltered: 0, filtered: 0, mgdl: 100, mills: before, noise: 1}, {unfiltered: 0, filtered: 0, mgdl: 100, mills: now, noise: 1}]; - ctx.data.cals = [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}]; - - var sbx = rawSandbox(); - ar2.checkNotifications(sbx.withExtendedSettings(ar2)); - should.not.exist(ctx.notifications.findHighestAlarm()); - - done(); - }); - - - it('should trigger a warning (no urgent for raw) when raw is falling really fast, but sgv is steady', function (done) { - ctx.notifications.initRequests(); - ctx.data.sgvs = [{unfiltered: 113680, filtered: 111232, mgdl: 100, mills: before, noise: 1}, {unfiltered: 43680, filtered: 111232, mgdl: 100, mills: now, noise: 1}]; - ctx.data.cals = [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}]; - - var sbx = rawSandbox(); - sbx.offerProperty('rawbg', function setFakeIOB() { - return {}; - }); - - ar2.checkNotifications(sbx.withExtendedSettings(ar2)); - var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.WARN); - highest.title.should.equal('Warning, LOW predicted w/raw'); - - done(); - }); - - it('should trigger a warning (no urgent for raw) when raw is rising really fast, but sgv is steady', function (done) { - ctx.notifications.initRequests(); - ctx.data.sgvs = [{unfiltered: 113680, filtered: 111232, mgdl: 100, mills: before, noise: 1}, {unfiltered: 183680, filtered: 111232, mgdl: 100, mills: now, noise: 1}]; - ctx.data.cals = [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}]; - - var sbx = rawSandbox(); - sbx.offerProperty('rawbg', function setFakeIOB() { - return {}; - }); + ctx.ddata.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + var sbx = prepareSandbox(); - ar2.checkNotifications(sbx.withExtendedSettings(ar2)); - var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.WARN); - highest.title.should.equal('Warning, HIGH predicted w/raw'); + ar2.virtAsst.intentHandlers.length.should.equal(1); - done(); + ar2.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('AR2 Forecast'); + response.should.equal('According to the AR2 forecast you are expected to be between 109 and 120 over the next in 30 minutes'); + done(); + }, [], sbx); }); }); \ No newline at end of file diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js index 2d1b354350b..c1ed3903089 100644 --- a/tests/basalprofileplugin.test.js +++ b/tests/basalprofileplugin.test.js @@ -1,15 +1,22 @@ -require('should'); +const should = require('should'); +const fs = require('fs'); +const language = require('../lib/language')(fs); -describe('basalprofile', function ( ) { +const helper = require('./inithelper')(); - var basal = require('../lib/plugins/basalprofile')(); +describe('basalprofile', function ( ) { var sandbox = require('../lib/sandbox')(); - var env = require('../env')(); - var ctx = {}; - ctx.data = require('../lib/data')(env, ctx); + var env = require('../lib/server/env')(); + var ctx = { + settings: {} + , language: language + }; + ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); + var basal = require('../lib/plugins/basalprofile')(ctx); + var profileData = { 'timezone': 'UTC', @@ -46,28 +53,62 @@ describe('basalprofile', function ( ) { ] }; - - var profile = require('../lib/profilefunctions')([profileData]); + var profile = require('../lib/profilefunctions')([profileData], helper.ctx); it('update basal profile pill', function (done) { - - var clientSettings = {}; var data = {}; - var pluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.value.should.equal('0.175U'); - done(); + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('0.175U'); + done(); + } } + , language: language }; - var time = new Date('2015-06-21T00:00:00').getTime(); + var time = new Date('2015-06-21T00:00:00+00:00'); + + console.log('TIME1', time); - var sbx = sandbox.clientInit(clientSettings, time, pluginBase, data); + var sbx = sandbox.clientInit(ctx, time, data); sbx.data.profile = profile; + basal.setProperties(sbx); basal.updateVisualisation(sbx); }); - + it('should handle virtAsst requests', function (done) { + var data = {}; + + var ctx = { + settings: {} + , pluginBase: { } + , language: language + }; + + var time = new Date('2015-06-21T00:00:00+00:00'); + + var sbx = sandbox.clientInit(ctx, time, data); + sbx.data.profile = profile; + + basal.virtAsst.intentHandlers.length.should.equal(1); + basal.virtAsst.rollupHandlers.length.should.equal(1); + + basal.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Current Basal'); + response.should.equal('Your current basal is 0.175 units per hour'); + + basal.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + should.not.exist(err); + response.results.should.equal('Your current basal is 0.175 units per hour'); + response.priority.should.equal(1); + done(); + }); + + }, [], sbx); + }); + }); \ No newline at end of file diff --git a/tests/bgnow.test.js b/tests/bgnow.test.js new file mode 100644 index 00000000000..98be53a48eb --- /dev/null +++ b/tests/bgnow.test.js @@ -0,0 +1,233 @@ +'use strict'; + +var should = require('should'); +var _ = require('lodash'); +const helper = require('./inithelper')(); + +var FIVE_MINS = 300000; +var SIX_MINS = 360000; + +describe('BG Now', function ( ) { + + const ctx = helper.ctx; + + var bgnow = require('../lib/plugins/bgnow')(ctx); + var sandbox = require('../lib/sandbox')(ctx); + + var now = Date.now(); + var before = now - FIVE_MINS; + + it('should calculate BG Delta', function (done) { + var ctx = { + settings: { units: 'mg/dl' } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal(ctx.settings.units); + options.value.should.equal('+5'); + should.not.exist(options.info); + done(); + } + , language: { translate: function(text) { return text; } } + } + }; + + ctx.language = ctx.pluginBase.language; + ctx.levels = require('../lib/levels'); + + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 105}]}; + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + bgnow.setProperties(sbx); + + var delta = sbx.properties.delta; + delta.mgdl.should.equal(5); + delta.interpolated.should.equal(false); + delta.scaled.should.equal(5); + delta.display.should.equal('+5'); + + bgnow.updateVisualisation(sbx); + }); + + it('should calculate BG Delta by interpolating when more than 5mins apart', function (done) { + var data = {sgvs: [{mills: before - SIX_MINS, mgdl: 100}, {mills: now, mgdl: 105}]}; + + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.label.should.equal(ctx.settings.units); + options.value.should.equal('+2 *'); + findInfoValue('Elapsed Time', options.info).should.equal('11 mins'); + findInfoValue('Absolute Delta', options.info).should.equal('5 mg/dl'); + findInfoValue('Interpolated', options.info).should.equal('103 mg/dl'); + done(); + } + } + , language: require('../lib/language')() + , moment: helper.ctx.moment + }; + + var sbx = sandbox.clientInit(ctx, now, data); + + bgnow.setProperties(sbx); + + var delta = sbx.properties.delta; + delta.mgdl.should.equal(2); + delta.interpolated.should.equal(true); + delta.scaled.should.equal(2); + delta.display.should.equal('+2'); + bgnow.updateVisualisation(sbx); + + }); + + it('should calculate BG Delta in mmol', function (done) { + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + , language: require('../lib/language')() + , moment: helper.ctx.moment + }; + + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 105}]}; + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + var gotbgnow = false; + var gotdelta = false; + var gotbuckets = false; + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + if (name === 'bgnow') { + var bgnowProp = setter(); + bgnowProp.mean.should.equal(105); + bgnowProp.last.should.equal(105); + bgnowProp.mills.should.equal(now); + gotbgnow = true; + } else if (name === 'delta') { + var result = setter(); + result.mgdl.should.equal(5); + result.interpolated.should.equal(false); + result.scaled.should.equal(0.2); + result.display.should.equal('+0.2'); + gotdelta = true; + } else if (name === 'buckets') { + var buckets = setter(); + buckets[0].mean.should.equal(105); + buckets[1].mean.should.equal(100); + gotbuckets = true; + } + + if (gotbgnow && gotdelta && gotbuckets) { + done(); + } + }; + + bgnow.setProperties(sbx); + }); + + it('should calculate BG Delta in mmol and not show a change because of rounding', function (done) { + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + , language: require('../lib/language')() + , moment: helper.ctx.moment + }; + + var data = {sgvs: [{mills: before, mgdl: 85}, {mills: now, mgdl: 85}]}; + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + var gotbgnow = false; + var gotdelta = false; + var gotbuckets = false; + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + if (name === 'bgnow') { + var bgnowProp = setter(); + bgnowProp.mean.should.equal(85); + bgnowProp.last.should.equal(85); + bgnowProp.mills.should.equal(now); + gotbgnow = true; + } else if (name === 'delta') { + var result = setter(); + result.mgdl.should.equal(0); + result.interpolated.should.equal(false); + result.scaled.should.equal(0); + result.display.should.equal('+0'); + gotdelta = true; + } else if (name === 'buckets') { + var buckets = setter(); + buckets[0].mean.should.equal(85); + buckets[1].mean.should.equal(85); + gotbuckets = true; + } + + if (gotbgnow && gotdelta && gotbuckets) { + done(); + } + + }; + + bgnow.setProperties(sbx); + }); + + it('should calculate BG Delta in mmol by interpolating when more than 5mins apart', function (done) { + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + , language: require('../lib/language')() + , moment: helper.ctx.moment + }; + + var data = {sgvs: [{mills: before - SIX_MINS, mgdl: 100}, {mills: now, mgdl: 105}]}; + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + var gotbgnow = false; + var gotdelta = false; + var gotbuckets = false; + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + if (name === 'bgnow') { + var bgnowProp = setter(); + bgnowProp.mean.should.equal(105); + bgnowProp.last.should.equal(105); + bgnowProp.mills.should.equal(now); + gotbgnow = true; + } else if (name === 'delta') { + var result = setter(); + result.mgdl.should.equal(2); + result.interpolated.should.equal(true); + result.scaled.should.equal(0.1); + result.display.should.equal('+0.1'); + gotdelta = true; + } else if (name === 'buckets') { + var buckets = setter(); + buckets[0].mean.should.equal(105); + buckets[1].isEmpty.should.equal(true); + buckets[2].mean.should.equal(100); + gotbuckets = true; + } + + if (gotbgnow && gotdelta && gotbuckets) { + done(); + } + }; + + bgnow.setProperties(sbx); + }); + +}); + +function findInfoValue (label, info) { + var found = _.find(info, function checkLine (line) { + return line.label === label; + }); + return found && found.value; +} diff --git a/tests/boluswizardpreview.test.js b/tests/boluswizardpreview.test.js index 3954342e1c1..7590e8700a9 100644 --- a/tests/boluswizardpreview.test.js +++ b/tests/boluswizardpreview.test.js @@ -1,25 +1,26 @@ var should = require('should'); var Stream = require('stream'); -var levels = require('../lib/levels'); +const helper = require('./inithelper')(); describe('boluswizardpreview', function ( ) { + var env = require('../lib/server/env')(); + env.testMode = true; - var boluswizardpreview = require('../lib/plugins/boluswizardpreview')(); - var ar2 = require('../lib/plugins/ar2')(); - var iob = require('../lib/plugins/iob')(); - var delta = require('../lib/plugins/delta')(); + var ctx = helper.getctx(); - var env = require('../env')(); - env.testMode = true; - var ctx = {}; - ctx.data = require('../lib/data')(env, ctx); + ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); + var boluswizardpreview = require('../lib/plugins/boluswizardpreview')(ctx); + var ar2 = require('../lib/plugins/ar2')(ctx); + var iob = require('../lib/plugins/iob')(ctx); + var bgnow = require('../lib/plugins/bgnow')(ctx); + function prepareSandbox ( ) { var sbx = require('../lib/sandbox')().serverInit(env, ctx); - iob.setProperties(sbx); + bgnow.setProperties(sbx); ar2.setProperties(sbx); - delta.setProperties(sbx); + iob.setProperties(sbx); boluswizardpreview.setProperties(sbx); sbx.offerProperty('direction', function setFakeDirection() { return {value: 'FortyFiveUp', label: '↗', entity: '↗'}; @@ -40,9 +41,9 @@ describe('boluswizardpreview', function ( ) { it('should calculate IOB results correctly with 0 IOB', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; - ctx.data.treatments = []; - ctx.data.profiles = [profile]; + ctx.ddata.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.ddata.treatments = []; + ctx.ddata.profiles = [profile]; var sbx = prepareSandbox(); var results = boluswizardpreview.calc(sbx); @@ -59,8 +60,8 @@ describe('boluswizardpreview', function ( ) { it('should calculate IOB results correctly with 1.0 U IOB', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; - ctx.data.treatments = [{mills: now, insulin: '1.0'}]; + ctx.ddata.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.ddata.treatments = [{mills: now, insulin: '1.0'}]; var profile = { dia: 3 @@ -69,7 +70,7 @@ describe('boluswizardpreview', function ( ) { , target_low: 50 }; - ctx.data.profiles = [profile]; + ctx.ddata.profiles = [profile]; var sbx = prepareSandbox(); var results = boluswizardpreview.calc(sbx); @@ -86,8 +87,8 @@ describe('boluswizardpreview', function ( ) { it('should calculate IOB results correctly with 1.0 U IOB resulting in going low', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; - ctx.data.treatments = [{mills: now, insulin: '1.0'}]; + ctx.ddata.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.ddata.treatments = [{mills: now, insulin: '1.0'}]; var profile = { dia: 3 @@ -98,7 +99,7 @@ describe('boluswizardpreview', function ( ) { }; - ctx.data.profiles = [profile]; + ctx.ddata.profiles = [profile]; var sbx = prepareSandbox(); var results = boluswizardpreview.calc(sbx); @@ -129,14 +130,22 @@ describe('boluswizardpreview', function ( ) { }; var sandbox = require('../lib/sandbox')(); - var pluginBase = {}; - var clientSettings = { units: 'mmol' }; + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + , moment: helper.ctx.moment + }; + + ctx.language = require('../lib/language')(); + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]}; data.treatments = [{mills: now, insulin: '1.0'}]; - data.profile = require('../lib/profilefunctions')([profileData]); - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); - var iob = require('../lib/plugins/iob')(); - sbx.properties.iob = iob.calcTotal(data.treatments, data.profile, now); + data.devicestatus = []; + data.profile = require('../lib/profilefunctions')([profileData], ctx); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sbx.properties.iob = iob.calcTotal(data.treatments, data.devicestatus, data.profile, now); var results = boluswizardpreview.calc(sbx); @@ -165,14 +174,22 @@ describe('boluswizardpreview', function ( ) { }; var sandbox = require('../lib/sandbox')(); - var pluginBase = {}; - var clientSettings = { units: 'mmol' }; + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: {} + , moment: helper.ctx.moment + }; + + ctx.language = require('../lib/language')(); + var data = {sgvs: [{mills: before, mgdl: 175}, {mills: now, mgdl: 153}]}; data.treatments = [{mills: now, insulin: '0.45'}]; - data.profile = require('../lib/profilefunctions')([profileData]); - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); - var iob = require('../lib/plugins/iob')(); - sbx.properties.iob = iob.calcTotal(data.treatments, data.profile, now); + data.devicestatus = []; + data.profile = require('../lib/profilefunctions')([profileData], ctx); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + sbx.properties.iob = iob.calcTotal(data.treatments, data.devicestatus, data.profile, now); var results = boluswizardpreview.calc(sbx); @@ -189,9 +206,9 @@ describe('boluswizardpreview', function ( ) { it('Not trigger an alarm when in range', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: before, mgdl: 95}, {mills: now, mgdl: 100}]; - ctx.data.treatments = []; - ctx.data.profiles = [profile]; + ctx.ddata.sgvs = [{mills: before, mgdl: 95}, {mills: now, mgdl: 100}]; + ctx.ddata.treatments = []; + ctx.ddata.profiles = [profile]; var sbx = prepareSandbox(); boluswizardpreview.checkNotifications(sbx); @@ -203,29 +220,29 @@ describe('boluswizardpreview', function ( ) { it('trigger a warning when going out of range', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: before, mgdl: 175}, {mills: now, mgdl: 180}]; - ctx.data.treatments = []; - ctx.data.profiles = [profile]; + ctx.ddata.sgvs = [{mills: before, mgdl: 175}, {mills: now, mgdl: 180}]; + ctx.ddata.treatments = []; + ctx.ddata.profiles = [profile]; var sbx = prepareSandbox(); boluswizardpreview.checkNotifications(sbx); var highest = ctx.notifications.findHighestAlarm(); - highest.level.should.equal(levels.WARN); + highest.level.should.equal(ctx.levels.WARN); highest.title.should.equal('Warning, Check BG, time to bolus?'); - highest.message.should.equal('BG Now: 180 +5 ↗ mg/dl\nBG 15m: 187 mg/dl\nBWP: 0.66U\nIOB: 0U'); + highest.message.should.equal('BG Now: 180 +5 ↗ mg/dl\nBG 15m: 187 mg/dl\nBWP: 0.66U'); done(); }); it('trigger an urgent alarms when going too high', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}]; - ctx.data.treatments = []; - ctx.data.profiles = [profile]; + ctx.ddata.sgvs = [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}]; + ctx.ddata.treatments = []; + ctx.ddata.profiles = [profile]; var sbx = prepareSandbox(); boluswizardpreview.checkNotifications(sbx); - ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + ctx.notifications.findHighestAlarm().level.should.equal(ctx.levels.URGENT); done(); }); @@ -234,9 +251,9 @@ describe('boluswizardpreview', function ( ) { ctx.notifications.resetStateForTests(); ctx.notifications.initRequests(); - ctx.data.sgvs = [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}]; - ctx.data.treatments = [{mills: before, insulin: '5.0'}]; - ctx.data.profiles = [profile]; + ctx.ddata.sgvs = [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}]; + ctx.ddata.treatments = [{mills: before, insulin: '5.0'}]; + ctx.ddata.profiles = [profile]; var sbx = prepareSandbox(); @@ -257,35 +274,34 @@ describe('boluswizardpreview', function ( ) { }); it('set a pill to the BWP with infos', function (done) { - var pluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.label.should.equal('BWP'); - options.value.should.equal('0.50U'); - done(); + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.label.should.equal('BWP'); + options.value.should.equal('0.50U'); + done(); + } } + , moment: helper.ctx.moment }; - - var clientSettings = {}; - - var loadedProfile = require('../lib/profilefunctions')(); + + ctx.language = require('../lib/language')(); + var loadedProfile = require('../lib/profilefunctions')(null, ctx); loadedProfile.loadData([profile]); var data = { sgvs: [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}] , treatments: [{mills: before, insulin: '1.5'}] + , devicestatus: [] , profile: loadedProfile }; - var sbx = require('../lib/sandbox')().clientInit(clientSettings, Date.now(), pluginBase, data); + var sbx = require('../lib/sandbox')().clientInit(ctx, Date.now(), data); iob.setProperties(sbx); boluswizardpreview.setProperties(sbx); boluswizardpreview.updateVisualisation(sbx); - - ctx.notifications.resetStateForTests(); - ctx.notifications.initRequests(); - ctx.data.profiles = [profile]; - }); -}); \ No newline at end of file +}); diff --git a/tests/bridge.test.js b/tests/bridge.test.js index 99c1587fab4..60afacd1b79 100644 --- a/tests/bridge.test.js +++ b/tests/bridge.test.js @@ -10,6 +10,7 @@ describe('bridge', function ( ) { bridge: { userName: 'nightscout' , password: 'wearenotwaiting' + , interval: 60000 } } }; @@ -27,6 +28,7 @@ describe('bridge', function ( ) { opts.login.accountName.should.equal('nightscout'); opts.login.password.should.equal('wearenotwaiting'); + opts.interval.should.equal(60000); }); it('store entries from share', function (done) { @@ -39,4 +41,43 @@ describe('bridge', function ( ) { bridge.bridged(mockEntries)(null); }); + it('set too low bridge interval option from env', function () { + var tooLowInterval = { + extendedSettings: { + bridge: { interval: 900 } + } + }; + + var opts = bridge.options(tooLowInterval); + should.exist(opts); + + opts.interval.should.equal(156000); + }); + + it('set too high bridge interval option from env', function () { + var tooHighInterval = { + extendedSettings: { + bridge: { interval: 500000 } + } + }; + + var opts = bridge.options(tooHighInterval); + should.exist(opts); + + opts.interval.should.equal(156000); + }); + + it('set no bridge interval option from env', function () { + var noInterval = { + extendedSettings: { + bridge: { } + } + }; + + var opts = bridge.options(noInterval); + should.exist(opts); + + opts.interval.should.equal(156000); + }); + }); diff --git a/tests/cannulaage.test.js b/tests/cannulaage.test.js index 60ecdfd4281..5b001248020 100644 --- a/tests/cannulaage.test.js +++ b/tests/cannulaage.test.js @@ -1,16 +1,18 @@ 'use strict'; require('should'); -var levels = require('../lib/levels'); +const helper = require('./inithelper')(); +const levels = helper.ctx.levels; describe('cage', function ( ) { - var cage = require('../lib/plugins/cannulaage')(); - var sandbox = require('../lib/sandbox')(); - var env = require('../env')(); - var ctx = {}; - ctx.data = require('../lib/data')(env, ctx); - ctx.notifications = require('../lib/notifications')(env, ctx); + var env = require('../lib/server/env')(); + var ctx = helper.getctx(); + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var cage = require('../lib/plugins/cannulaage')(ctx); + var sandbox = require('../lib/sandbox')(ctx); function prepareSandbox ( ) { var sbx = require('../lib/sandbox')().serverInit(env, ctx); sbx.offerProperty('iob', function () { @@ -21,48 +23,53 @@ describe('cage', function ( ) { it('set a pill to the current cannula age', function (done) { - var clientSettings = {}; - var data = { - treatments: [ + sitechangeTreatments: [ {eventType: 'Site Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} , {eventType: 'Site Change', notes: 'Bar', mills: Date.now() - 24 * 60 * 60000} ] }; - var pluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.value.should.equal('24h'); - options.info[1].value.should.equal('Bar'); - done(); + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('24h'); + options.info[1].value.should.equal('Bar'); + done(); + } } }; - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + ctx.language = require('../lib/language')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + cage.setProperties(sbx); cage.updateVisualisation(sbx); }); it('set a pill to the current cannula age', function (done) { - var clientSettings = {}; - var data = { - treatments: [ + sitechangeTreatments: [ {eventType: 'Site Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} , {eventType: 'Site Change', notes: '', mills: Date.now() - 59 * 60000} ] }; - var pluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.value.should.equal('0h'); - options.info.length.should.equal(1); - done(); + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('0h'); + options.info.length.should.equal(1); + done(); + } } }; - - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + ctx.language = require('../lib/language')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + cage.setProperties(sbx); cage.updateVisualisation(sbx); }); @@ -73,13 +80,14 @@ describe('cage', function ( ) { var before = Date.now() - (48 * 60 * 60 * 1000); - ctx.data.treatments = [{eventType: 'Site Change', mills: before}]; + ctx.ddata.sitechangeTreatments = [{eventType: 'Site Change', mills: before}]; var sbx = prepareSandbox(); sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + cage.setProperties(sbx); cage.checkNotifications(sbx); - var highest = ctx.notifications.findHighestAlarm(); + var highest = ctx.notifications.findHighestAlarm('CAGE'); highest.level.should.equal(levels.WARN); highest.title.should.equal('Cannula age 48 hours'); done(); diff --git a/tests/careportal.test.js b/tests/careportal.test.js index e7c371ee195..1da34673535 100644 --- a/tests/careportal.test.js +++ b/tests/careportal.test.js @@ -2,8 +2,6 @@ require('should'); var benv = require('benv'); -var read = require('fs').readFileSync; -var serverSettings = require('./fixtures/default-server-settings'); var nowData = { sgvs: [ @@ -12,73 +10,56 @@ var nowData = { , treatments: [] }; -describe('client', function ( ) { - var self = this; +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +describe('careportal', function ( ) { + this.timeout(60000); // TODO: see why this test takes longer on Travis to complete + + var headless = require('./fixtures/headless')(benv, this); before(function (done) { - benv.setup(function() { - self.$ = require('jquery'); - self.$.localStorage = require('./fixtures/localstorage'); - - self.$.fn.tipsy = function mockTipsy ( ) { }; - - var indexHtml = read(__dirname + '/../static/index.html', 'utf8'); - self.$('body').html(indexHtml); - - var d3 = require('d3'); - //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; - - benv.expose({ - $: self.$ - , jQuery: self.$ - , d3: d3 - , io: { - connect: function mockConnect ( ) { - return { - on: function mockOn ( ) { } - }; - } - } - }); + + const t = Date.now(); + console.log('Starting headless setup for Careportal test'); + + function d () { + console.log('Done called by headless', Date.now() - t ); done(); - }); + } + + headless.setup({mockAjax: true}, d); + console.log('Headless setup for Careportal test done'); }); after(function (done) { - benv.teardown(); - done(); + headless.teardown( ); + done( ); }); - it ('open careportal, and enter a treatment', function (done) { - var plugins = require('../lib/plugins/')().registerClientDefaults(); - var client = require('../lib/client'); - - self.$.ajax = function mockAjax ( ) { - return { - done: function mockDone (fn) { - fn(); - done(); - return self.$.ajax(); - } - , fail: function mockFail ( ) { - return self.$.ajax(); - } - }; - }; - var hashauth = require('../lib/hashauth'); + it ('open careportal, and enter a treatment', async () =>{ + + console.log('Careportal test client start'); + + var client = window.Nightscout.client; + + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; next(true); }; + console.log('Careportal test client init'); + client.init(); + sleep(50); - client.init(serverSettings, plugins); - client.dataUpdate(nowData); + console.log('Careportal test client data update'); + client.dataUpdate(nowData, true); + sleep(50); client.careportal.prepareEvents(); - client.careportal.toggleDrawer(); $('#eventType').val('Snack Bolus'); $('#glucoseValue').val('100'); @@ -109,7 +90,12 @@ describe('client', function ( ) { return true; }; + window.alert = function mockAlert(messages) { messages.should.equal(''); }; + + console.log('Careportal test saving'); + client.careportal.save(); + }); }); diff --git a/tests/ci.test.env b/tests/ci.test.env new file mode 100644 index 00000000000..f5e240f8381 --- /dev/null +++ b/tests/ci.test.env @@ -0,0 +1,8 @@ +CUSTOMCONNSTR_mongo=mongodb://127.0.0.1:27017/testdb +API_SECRET=abcdefghij123 +HOSTNAME=localhost +INSECURE_USE_HTTP=true +PORT=1337 +NODE_ENV=production +CI=true +CODACY_PROJECT_TOKEN=cff7ab3377d6434a9355fd051dbb4595 \ No newline at end of file diff --git a/tests/client.renderer.test.js b/tests/client.renderer.test.js new file mode 100644 index 00000000000..5dc707ab2b1 --- /dev/null +++ b/tests/client.renderer.test.js @@ -0,0 +1,75 @@ +'use strict'; + +require('should'); +let _ = require('lodash'); + +let renderer = require('../lib/client/renderer'); + +describe('renderer', () => { + describe('bubbleScale', () => { + const MAX_DELTA = 0.0001; + const PREV_CHART_WIDTHS = [ + { width: 400, expectedScale: 3.5 } + , { width: 500, expectedScale: 2.625 } + , { width: 900, expectedScale: 1.75 } + ]; + + _.forEach(PREV_CHART_WIDTHS, (prev) => { + describe(`prevChartWidth < ${prev.width}`, () => { + let mockClient = { + utils: true + , chart: { prevChartWidth: prev.width } + , focusRangeMS: true + }; + it('scales correctly', () => { + renderer(mockClient, {}).bubbleScale().should.be.approximately(prev.expectedScale, MAX_DELTA); + }); + }); + }); + }); + + describe('highlightBrushPoints', () => { + const BRUSH_EXTENTS = [ + { mills: 100, times: [200, 300], expectedOpacity: 0.5, expectation: 'Uses default opacity' } + , { mills: 300, times: [100, 200], expectedOpacity: 0.5, expectation: 'Uses default opacity' } + , { mills: 200, times: [100, 300], expectedOpacity: 1, expectation: 'Calculates opacity' } + ]; + + _.forEach(BRUSH_EXTENTS, (extent) => { + let mockData = { + mills: extent.mills + }; + + let mockClient = { + chart: { + brush: { + extent: () => { + let extents = []; + for (let time of extent.times) { + extents.push({ getTime: () => { + return time; + }}); + } + return extents; + } + } + , futureOpacity: (millsDifference) => { return 1; } + , createAdjustedRange: () => { return [ + { getTime: () => { return extent.times[0]}}, + { getTime: () => { return extent.times[1]}} + ] } + } + , latestSGV: { mills: 120 } + }; + + describe(`data.mills ${extent.mills} and chart().brush.extent() times ${extent.times}`, () => { + it(extent.expectation, () => { + var selectedRange = mockClient.chart.createAdjustedRange(); + var from = selectedRange[0].getTime(); + var to = selectedRange[1].getTime(); + renderer(mockClient, {}).highlightBrushPoints(mockData, from, to).should.equal(extent.expectedOpacity); + }); + }); + }); + }); +}); diff --git a/tests/client.test.js b/tests/client.test.js.temporary_removed similarity index 100% rename from tests/client.test.js rename to tests/client.test.js.temporary_removed diff --git a/tests/cob.test.js b/tests/cob.test.js index 336dffe41b3..4b54cddb699 100644 --- a/tests/cob.test.js +++ b/tests/cob.test.js @@ -1,17 +1,23 @@ 'use strict'; +const _ = require('lodash'); +const helper = require('./inithelper')(); + require('should'); describe('COB', function ( ) { - var cob = require('../lib/plugins/cob')(); + var ctx = helper.ctx; + + var cob = require('../lib/plugins/cob')(ctx); var profileData = { - sens: 95 + startDate: '2015-06-21' + , sens: 95 , carbratio: 18 , carbs_hr: 30 }; - var profile = require('../lib/profilefunctions')([profileData]); + var profile = require('../lib/profilefunctions')([profileData], ctx); it('should calculate IOB, multiple treatments', function() { @@ -26,9 +32,11 @@ describe('COB', function ( ) { } ]; - var after100 = cob.cobTotal(treatments, profile, new Date('2015-05-29T02:03:49.827Z').getTime()); - var before10 = cob.cobTotal(treatments, profile, new Date('2015-05-29T03:45:10.670Z').getTime()); - var after10 = cob.cobTotal(treatments, profile, new Date('2015-05-29T03:45:11.670Z').getTime()); + var devicestatus = []; + + var after100 = cob.cobTotal(treatments, devicestatus, profile, new Date('2015-05-29T02:03:49.827Z').getTime()); + var before10 = cob.cobTotal(treatments, devicestatus, profile, new Date('2015-05-29T03:45:10.670Z').getTime()); + var after10 = cob.cobTotal(treatments, devicestatus, profile, new Date('2015-05-29T03:45:11.670Z').getTime()); after100.cob.should.equal(100); Math.round(before10.cob).should.equal(59); @@ -44,17 +52,19 @@ describe('COB', function ( ) { } ]; + var devicestatus = []; + var rightAfterCorrection = new Date('2015-05-29T04:41:40.174Z').getTime(); var later1 = new Date('2015-05-29T05:04:40.174Z').getTime(); var later2 = new Date('2015-05-29T05:20:00.174Z').getTime(); var later3 = new Date('2015-05-29T05:50:00.174Z').getTime(); var later4 = new Date('2015-05-29T06:50:00.174Z').getTime(); - var result1 = cob.cobTotal(treatments, profile, rightAfterCorrection); - var result2 = cob.cobTotal(treatments, profile, later1); - var result3 = cob.cobTotal(treatments, profile, later2); - var result4 = cob.cobTotal(treatments, profile, later3); - var result5 = cob.cobTotal(treatments, profile, later4); + var result1 = cob.cobTotal(treatments, devicestatus, profile, rightAfterCorrection); + var result2 = cob.cobTotal(treatments, devicestatus, profile, later1); + var result3 = cob.cobTotal(treatments, devicestatus, profile, later2); + var result4 = cob.cobTotal(treatments, devicestatus, profile, later3); + var result5 = cob.cobTotal(treatments, devicestatus, profile, later4); result1.cob.should.equal(8); result2.cob.should.equal(6); @@ -64,9 +74,6 @@ describe('COB', function ( ) { }); it('set a pill to the current COB', function (done) { - - var clientSettings = {}; - var data = { treatments: [{ carbs: '8' @@ -75,19 +82,114 @@ describe('COB', function ( ) { , profile: profile }; - var pluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.value.should.equal('8g'); - done(); - } + ctx.pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('8g'); + done(); + } }; var sandbox = require('../lib/sandbox')(); - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + var sbx = sandbox.clientInit(ctx, Date.now(), data); cob.setProperties(sbx); cob.updateVisualisation(sbx); }); + it('should handle virtAsst requests', function (done) { + var data = { + treatments: [{ + carbs: '8' + , 'mills': Date.now() - 60000 //1m ago + }] + , profile: profile + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + cob.setProperties(sbx); + + cob.virtAsst.intentHandlers.length.should.equal(1); + + cob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Current COB'); + response.should.equal('You have 8 carbohydrates on board'); + done(); + }, [], sbx); + + }); + + describe('from devicestatus', function () { + var time = Date.now(); + var treatments = [{ + mills: time - 1, + carbs: '20' + }]; + + var OPENAPS_DEVICESTATUS = { + device: 'openaps://pi1', + openaps: { + enacted: { + COB: 30 + } + } + }; + + var treatmentCOB = cob.fromTreatments(treatments, OPENAPS_DEVICESTATUS, profile, time).cob; + + it('should fall back to treatment data if no devicestatus data', function() { + cob.cobTotal(treatments, [], profile, time).should.containEql({ + source: 'Care Portal', + cob: treatmentCOB + }); + }); + + it('should fall back to treatments if openaps devicestatus is present but empty', function() { + var devicestatus = [{ + device: 'openaps://pi1', + mills: time - 1, + openaps: {} + }]; + cob.cobTotal(treatments, devicestatus, profile, time).cob.should.equal(treatmentCOB); + }); + + it('should fall back to treatments if openaps devicestatus is present but too stale', function() { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - cob.RECENCY_THRESHOLD - 1, openaps: {enacted: {COB: 5, timestamp: time - cob.RECENCY_THRESHOLD - 1} } })]; + cob.cobTotal(treatments, devicestatus, profile, time).should.containEql({ + source: 'Care Portal', + cob: treatmentCOB + }); + }); + + it('should return COB data from OpenAPS', function () { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {enacted: {COB: 5, timestamp: time - 1} } })]; + cob.cobTotal(treatments, devicestatus, profile, time).should.containEql({ + cob: 5, + source: 'OpenAPS', + device: 'openaps://pi1' + }); + }); + + it('should return COB data from Loop', function () { + + var LOOP_DEVICESTATUS = { + device: 'loop://iPhone', + loop: { + cob: { + cob: 5 + } + } + }; + + var devicestatus = [_.merge(LOOP_DEVICESTATUS, { mills: time - 1, loop: {cob: {timestamp: time - 1} } })]; + cob.cobTotal(treatments, devicestatus, profile, time).should.containEql({ + cob: 5, + source: 'Loop', + device: 'loop://iPhone' + }); + }); + + }); + -}); \ No newline at end of file +}); diff --git a/tests/data.calcdelta.test.js b/tests/data.calcdelta.test.js new file mode 100644 index 00000000000..164229cc509 --- /dev/null +++ b/tests/data.calcdelta.test.js @@ -0,0 +1,78 @@ +'use strict'; + +require('should'); + +var calcDelta = require('../lib/data/calcdelta'); + +describe('Data', function ( ) { + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + + it('should return original data if there are no changes', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = calcDelta(ddata,ddata); + delta.should.equal(ddata); + }); + + it('adding one sgv record should return delta with one sgv', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var newData = ddata.clone(); + newData.sgvs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = calcDelta(ddata,newData); + delta.delta.should.equal(true); + delta.sgvs.length.should.equal(1); + }); + + it('should update sgv if changed', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var newData = ddata.clone(); + newData.sgvs = [{mgdl: 110, mills: before},{mgdl: 100, mills: now}]; + var delta = calcDelta(ddata,newData); + delta.delta.should.equal(true); + delta.sgvs.length.should.equal(1); + }); + + it('adding one treatment record should return delta with one treatment', function() { + var ddata = require('../lib/data/ddata')(); + ddata.treatments = [{_id: 'someid_1', mgdl: 100, mills: before},{_id: 'someid_2', mgdl: 100, mills: now}]; + var newData = ddata.clone(); + newData.treatments = [{_id: 'someid_1', mgdl: 100, mills: before},{_id: 'someid_2', mgdl: 100, mills: now},{_id: 'someid_3', mgdl: 100, mills:98}]; + var delta = calcDelta(ddata,newData); + delta.delta.should.equal(true); + delta.treatments.length.should.equal(1); + }); + + it('changes to treatments, mbgs and cals should be calculated even if sgvs is not changed', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + ddata.treatments = [{_id: 'someid_1', mgdl: 100, mills: before},{_id: 'someid_2', mgdl: 100, mills: now}]; + ddata.mbgs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + ddata.cals = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var newData = ddata.clone(); + newData.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.treatments = [{_id: 'someid_3', mgdl: 100, mills:101},{_id: 'someid_1', mgdl: 100, mills: before},{_id: 'someid_2', mgdl: 100, mills: now}]; + newData.mbgs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.cals = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = calcDelta(ddata,newData); + delta.delta.should.equal(true); + delta.treatments.length.should.equal(1); + delta.mbgs.length.should.equal(1); + delta.cals.length.should.equal(1); + }); + + it('delta should include profile', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + ddata.profiles = {foo:true}; + var newData = ddata.clone(); + newData.sgvs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.profiles = {bar:true}; + var delta = calcDelta(ddata,newData); + delta.profiles.bar.should.equal(true); + }); + +}); \ No newline at end of file diff --git a/tests/data.test.js b/tests/data.test.js deleted file mode 100644 index 60c74abe035..00000000000 --- a/tests/data.test.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -require('should'); - -describe('Data', function ( ) { - - var env = require('../env')(); - var ctx = {}; - var data = require('../lib/data')(env, ctx); - - var now = Date.now(); - var before = now - (5 * 60 * 1000); - - - it('should return original data if there are no changes', function() { - data.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - var delta = data.calculateDeltaBetweenDatasets(data,data); - delta.should.equal(data); - }); - - it('adding one sgv record should return delta with one sgv', function() { - data.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - var newData = data.clone(); - newData.sgvs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - var delta = data.calculateDeltaBetweenDatasets(data,newData); - delta.delta.should.equal(true); - delta.sgvs.length.should.equal(1); - }); - - it('adding one treatment record should return delta with one treatment', function() { - data.treatments = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - var newData = data.clone(); - newData.treatments = [{mgdl: 100, mills: before},{mgdl: 100, mills: now},{mgdl: 100, mills:98}]; - var delta = data.calculateDeltaBetweenDatasets(data,newData); - delta.delta.should.equal(true); - delta.treatments.length.should.equal(1); - }); - - it('changes to treatments, mbgs and cals should be calculated even if sgvs is not changed', function() { - data.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - data.treatments = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - data.mbgs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - data.cals = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - var newData = data.clone(); - newData.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - newData.treatments = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - newData.mbgs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - newData.cals = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - var delta = data.calculateDeltaBetweenDatasets(data,newData); - delta.delta.should.equal(true); - delta.treatments.length.should.equal(1); - delta.mbgs.length.should.equal(1); - delta.cals.length.should.equal(1); - }); - - it('delta should include profile and devicestatus object if changed', function() { - data.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - data.profiles = {foo:true}; - data.devicestatus = {foo:true}; - var newData = data.clone(); - newData.sgvs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; - newData.profiles = {bar:true}; - newData.devicestatus = {bar:true}; - var delta = data.calculateDeltaBetweenDatasets(data,newData); - delta.profiles.bar.should.equal(true); - delta.devicestatus.bar.should.equal(true); - }); - - it('update treatment display BGs', function() { - data.sgvs = [{mgdl: 90, mills: before},{mgdl: 100, mills: now}]; - data.treatments = [ - {mills: before, glucose: 100, units: 'mgdl'} //with glucose and units - , {mills: before, glucose: 5.5, units: 'mmol'} //with glucose and units - , {mills: now - 120000, insulin: '1.00'} //without glucose, between sgvs - , {mills: now + 60000, insulin: '1.00'} //without glucose, after sgvs - , {mills: before - 120000, insulin: '1.00'} //without glucose, before sgvs - ]; - data.updateTreatmentDisplayBGs(); - data.treatments[0].mgdl.should.equal(100); - data.treatments[1].mmol.should.equal(5.5); - data.treatments[2].mgdl.should.equal(95); - data.treatments[3].mgdl.should.equal(100); - data.treatments[4].mgdl.should.equal(90); - }); - -}); \ No newline at end of file diff --git a/tests/data.treatmenttocurve.test.js b/tests/data.treatmenttocurve.test.js new file mode 100644 index 00000000000..2cdc5b0042d --- /dev/null +++ b/tests/data.treatmenttocurve.test.js @@ -0,0 +1,37 @@ +'use strict'; + +require('should'); + +var fitTreatmentsToBGCurve = require('../lib/data/treatmenttocurve'); + +describe('Data', function ( ) { + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + var settings = require('../lib/settings')(); + + it('update treatment display BGs', function() { + var ddata = require('../lib/data/ddata')(); + ddata.sgvs = [{mgdl: 90, mills: before},{mgdl: 100, mills: now}]; + ddata.treatments = [ + {_id: 'someid_1', mills: before, glucose: 100, units: 'mgdl'} //with glucose and units + , {_id: 'someid_2', mills: before, glucose: 5.5, units: 'mmol'} //with glucose and units + , {_id: 'someid_3', mills: now - 120000, insulin: '1.00'} //without glucose, between sgvs + , {_id: 'someid_4', mills: now + 60000, insulin: '1.00'} //without glucose, after sgvs + , {_id: 'someid_5', mills: before - 120000, insulin: '1.00'} //without glucose, before sgvs + ]; + fitTreatmentsToBGCurve(ddata, { + settings: settings + } + , { + language: require('../lib/language')() + } + ); + ddata.treatments[0].mgdl.should.equal(100); + ddata.treatments[1].mmol.should.equal(5.5); + ddata.treatments[2].mgdl.should.equal(95); + ddata.treatments[3].mgdl.should.equal(100); + ddata.treatments[4].mgdl.should.equal(90); + }); + +}); \ No newline at end of file diff --git a/tests/dbsize.test.js b/tests/dbsize.test.js new file mode 100644 index 00000000000..ce3f373c2c7 --- /dev/null +++ b/tests/dbsize.test.js @@ -0,0 +1,317 @@ +'use strict'; + +const fs = require('fs'); +const language = require('../lib/language')(fs); +const levels = require('../lib/levels'); +require('should'); + +var topctx = { + levels: levels +} + +describe('Database Size', function() { + + var dataInRange = { dbstats: { dataSize: 1024 * 1024 * 137, indexSize: 1024 * 1024 * 48, fileSize: 1024 * 1024 * 256 } }; + var dataWarn = { dbstats: { dataSize: 1024 * 1024 * 250, indexSize: 1024 * 1024 * 100, fileSize: 1024 * 1024 * 360 } }; + var dataUrgent = { dbstats: { dataSize: 1024 * 1024 * 300, indexSize: 1024 * 1024 * 150, fileSize: 1024 * 1024 * 496 } }; + + var env = require('../lib/server/env')(); + + it('display database size in range', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: language + , levels: levels + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), dataInRange); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('dbsize'); + var result = setter(); + result.display.should.equal('37%'); + result.status.should.equal('current'); + done(); + }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('display database size warning', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: language + , levels: levels + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), dataWarn); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('dbsize'); + var result = setter(); + result.display.should.equal('70%'); + result.status.should.equal('warn'); + done(); + }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + + dbsize.setProperties(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('display database size urgent', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: language + , levels: levels + }; + + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('dbsize'); + var result = setter(); + result.display.should.equal('90%'); + result.status.should.equal('urgent'); + done(); + }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('display database size warning notiffication', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: language + , notifications: require('../lib/notifications')(env, topctx) + , levels: levels + }; + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, Date.now(), dataWarn); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + + dbsize.setProperties(sbx); + dbsize.checkNotifications(sbx); + + var notif = ctx.notifications.findHighestAlarm('Database Size'); + notif.level.should.equal(ctx.levels.WARN); + notif.title.should.equal('Warning Database Size near its limits!'); + notif.message.should.equal('Database size is 350 MiB out of 496 MiB. Please backup and clean up database!'); + done(); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('display database size urgent notiffication', function(done) { + var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: {} + , language: language + , notifications: require('../lib/notifications')(env, topctx) + , levels: levels + }; + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + + var dbsize = require('../lib/plugins/dbsize')(ctx); + + dbsize.setProperties(sbx); + dbsize.checkNotifications(sbx); + + var notif = ctx.notifications.findHighestAlarm('Database Size'); + notif.level.should.equal(ctx.levels.URGENT); + notif.title.should.equal('Urgent Database Size near its limits!'); + notif.message.should.equal('Database size is 450 MiB out of 496 MiB. Please backup and clean up database!'); + done(); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('set a pill to the database size in percent', function(done) { + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('90%'); + options.labelClass.should.equal('plugicon-database'); + options.pillClass.should.equal('urgent'); + done(); + } + } + , language: language + , levels: levels + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + dbsize.updateVisualisation(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('set a pill to the database size in MiB', function(done) { + var ctx = { + settings: { + extendedSettings: { + empty: false + , dbsize: { + inMib: true + } + } + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('450MiB'); + options.labelClass.should.equal('plugicon-database'); + options.pillClass.should.equal('urgent'); + done(); + } + } + , language: language + , levels: levels + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx.withExtendedSettings(dbsize)); + dbsize.updateVisualisation(sbx); + + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('configure warn level percentage', function(done) { + + var ctx = { + settings: { + extendedSettings: { + empty: false + , dbsize: { + warnPercentage: 30 + } + } + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('37%'); + options.pillClass.should.equal('warn'); + done(); + } + } + , language: language + , levels: levels + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataInRange); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx.withExtendedSettings(dbsize)); + dbsize.updateVisualisation(sbx); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('configure urgent level percentage', function(done) { + + var ctx = { + settings: { + extendedSettings: { + empty: false + , dbsize: { + warnPercentage: 30 + , urgentPercentage: 36 + } + } + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('37%'); + options.pillClass.should.equal('urgent'); + done(); + } + } + , language: language + , levels: levels + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataInRange); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx.withExtendedSettings(dbsize)); + dbsize.updateVisualisation(sbx); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('hide the pill if there is no info regarding database size', function(done) { + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.hide.should.equal(true); + done(); + } + } + , language: language + , levels: levels + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), {}); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + dbsize.updateVisualisation(sbx); + }); + + // ~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~. + + it('should handle virtAsst requests', function(done) { + + var ctx = { + settings: {} + , language: language + , levels: levels + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), dataUrgent); + var dbsize = require('../lib/plugins/dbsize')(ctx); + dbsize.setProperties(sbx); + + dbsize.virtAsst.intentHandlers.length.should.equal(1); + + dbsize.virtAsst.intentHandlers[0].intentHandler(function next (title, response) { + title.should.equal('Database file size'); + response.should.equal('450 MiB. That is 90% of available database space.'); + + done(); + + }, [], sbx); + + }); + +}); diff --git a/tests/ddata.test.js b/tests/ddata.test.js new file mode 100644 index 00000000000..034847b89a9 --- /dev/null +++ b/tests/ddata.test.js @@ -0,0 +1,60 @@ + +'use strict'; + +var should = require('should'); + + +describe('ddata', function ( ) { + // var sandbox = require('../lib/sandbox')(); + // var env = require('../lib/server/env')(); + var ctx = {}; + ctx.ddata = require('../lib/data/ddata')(); + + it('should be a module', function (done) { + var libddata = require('../lib/data/ddata'); + var ddata = libddata( ); + should.exist(ddata); + should.exist(libddata); + should.exist(libddata.call); + ddata = ctx.ddata.clone( ); + should.exist(ddata); + done( ); + }); + + it('has #clone( )', function (done) { + should.exist(ctx.ddata.treatments); + should.exist(ctx.ddata.sgvs); + should.exist(ctx.ddata.mbgs); + should.exist(ctx.ddata.cals); + should.exist(ctx.ddata.profiles); + should.exist(ctx.ddata.devicestatus); + should.exist(ctx.ddata.lastUpdated); + var ddata = ctx.ddata.clone( ); + should.exist(ddata); + should.exist(ddata.treatments); + should.exist(ddata.sgvs); + should.exist(ddata.mbgs); + should.exist(ddata.cals); + should.exist(ddata.profiles); + should.exist(ddata.devicestatus); + should.exist(ddata.lastUpdated); + done( ); + }); + + // TODO: ensure partition function gets called via: + // Properties + // * ddata.devicestatus + // * ddata.mbgs + // * ddata.sgvs + // * ddata.treatments + // * ddata.profiles + // * ddata.lastUpdated + // Methods + // * ddata.processTreatments + // * ddata.processDurations + // * ddata.clone + // * ddata.split + + +}); + diff --git a/tests/delta.test.js b/tests/delta.test.js deleted file mode 100644 index cd250d499d5..00000000000 --- a/tests/delta.test.js +++ /dev/null @@ -1,132 +0,0 @@ -'use strict'; - -require('should'); -var _ =require('lodash'); - -var FIVE_MINS = 300000; -var SIX_MINS = 360000; - -describe('Delta', function ( ) { - var delta = require('../lib/plugins/delta')(); - var sandbox = require('../lib/sandbox')(); - - var pluginBase = {}; - var now = Date.now(); - var before = now - FIVE_MINS; - - it('should calculate BG Delta', function (done) { - var clientSettings = { units: 'mg/dl' }; - var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 105}]}; - - var callbackPluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.label.should.equal(clientSettings.units); - options.value.should.equal('+5'); - options.info.length.should.equal(0); - done(); - } - }; - - var sbx = sandbox.clientInit(clientSettings, Date.now(), callbackPluginBase, data); - - delta.setProperties(sbx); - - var prop = sbx.properties.delta; - prop.mgdl.should.equal(5); - prop.interpolated.should.equal(false); - prop.scaled.should.equal(5); - prop.display.should.equal('+5'); - - delta.updateVisualisation(sbx); - }); - - it('should calculate BG Delta by interpolating when more than 5mins apart', function (done) { - var clientSettings = { units: 'mg/dl' }; - var data = {sgvs: [{mills: before - SIX_MINS, mgdl: 100}, {mills: now, mgdl: 105}]}; - - var callbackPluginBase = { - updatePillText: function mockedUpdatePillText (plugin, options) { - options.label.should.equal(clientSettings.units); - options.value.should.equal('+2 *'); - findInfoValue('Elapsed Time', options.info).should.equal('11 mins'); - findInfoValue('Absolute Delta', options.info).should.equal('5 mg/dl'); - findInfoValue('Interpolated', options.info).should.equal('103 mg/dl'); - done(); - } - }; - - var sbx = sandbox.clientInit(clientSettings, Date.now(), callbackPluginBase, data); - - delta.setProperties(sbx); - - var prop = sbx.properties.delta; - prop.mgdl.should.equal(2); - prop.interpolated.should.equal(true); - prop.scaled.should.equal(2); - prop.display.should.equal('+2'); - delta.updateVisualisation(sbx); - - }); - - it('should calculate BG Delta in mmol', function (done) { - var clientSettings = { units: 'mmol' }; - var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 105}]}; - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); - - sbx.offerProperty = function mockedOfferProperty (name, setter) { - name.should.equal('delta'); - var result = setter(); - result.mgdl.should.equal(5); - result.interpolated.should.equal(false); - result.scaled.should.equal(0.2); - result.display.should.equal('+0.2'); - done(); - }; - - delta.setProperties(sbx); - }); - - it('should calculate BG Delta in mmol and not show a change because of rounding', function (done) { - var clientSettings = { units: 'mmol' }; - var data = {sgvs: [{mills: before, mgdl: 85}, {mills: now, mgdl: 85}]}; - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); - - sbx.offerProperty = function mockedOfferProperty (name, setter) { - name.should.equal('delta'); - var result = setter(); - result.mgdl.should.equal(0); - result.interpolated.should.equal(false); - result.scaled.should.equal(0); - result.display.should.equal('+0'); - done(); - }; - - delta.setProperties(sbx); - }); - - it('should calculate BG Delta in mmol by interpolating when more than 5mins apart', function (done) { - var clientSettings = { units: 'mmol' }; - var data = {sgvs: [{mills: before - SIX_MINS, mgdl: 100}, {mills: now, mgdl: 105}]}; - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); - - sbx.offerProperty = function mockedOfferProperty (name, setter) { - name.should.equal('delta'); - var result = setter(); - result.mgdl.should.equal(2); - result.interpolated.should.equal(true); - result.scaled.should.equal(0.1); - result.display.should.equal('+0.1'); - done(); - }; - - delta.setProperties(sbx); - }); - -}); - -function findInfoValue (label, info) { - var found = _.find(info, function checkLine (line) { - return line.label === label; - }); - return found && found.value; -} diff --git a/tests/direction.test.js b/tests/direction.test.js index bf421795d01..8654415fdb1 100644 --- a/tests/direction.test.js +++ b/tests/direction.test.js @@ -3,15 +3,23 @@ require('should'); describe('BG direction', function ( ) { + + var now = Date.now(); + function setupSandbox(data, pluginBase) { - var clientSettings = {}; + var ctx = { + settings: {} + , pluginBase: pluginBase || {} + }; + + ctx.language = require('../lib/language')(); var sandbox = require('../lib/sandbox')(); - return sandbox.clientInit(clientSettings, Date.now(), pluginBase || {}, data); + return sandbox.clientInit(ctx, Date.now(), data); } it('set the direction property - Flat', function (done) { - var sbx = setupSandbox({sgvs: [{direction: 'Flat'}]}); + var sbx = setupSandbox({sgvs: [{mills: now, direction: 'Flat'}]}); sbx.offerProperty = function mockedOfferProperty (name, setter) { name.should.equal('direction'); @@ -28,7 +36,7 @@ describe('BG direction', function ( ) { }); it('set the direction property Double Up', function (done) { - var sbx = setupSandbox({sgvs: [{direction: 'DoubleUp'}]}); + var sbx = setupSandbox({sgvs: [{mills: now, direction: 'DoubleUp'}]}); sbx.offerProperty = function mockedOfferProperty (name, setter) { name.should.equal('direction'); @@ -52,7 +60,7 @@ describe('BG direction', function ( ) { } }; - var sbx = setupSandbox({sgvs: [{direction: 'Flat'}]}, pluginBase); + var sbx = setupSandbox({sgvs: [{mills: now, direction: 'Flat'}]}, pluginBase); var direction = require('../lib/plugins/direction')(); direction.setProperties(sbx); direction.updateVisualisation(sbx); @@ -61,35 +69,35 @@ describe('BG direction', function ( ) { it('get the info for a direction', function () { var direction = require('../lib/plugins/direction')(); - direction.info({direction: 'NONE'}).label.should.equal('⇼'); - direction.info({direction: 'NONE'}).entity.should.equal('⇼'); + direction.info({mills: now, direction: 'NONE'}).label.should.equal('⇼'); + direction.info({mills: now, direction: 'NONE'}).entity.should.equal('⇼'); - direction.info({direction: 'DoubleUp'}).label.should.equal('⇈'); - direction.info({direction: 'DoubleUp'}).entity.should.equal('⇈'); + direction.info({mills: now, direction: 'DoubleUp'}).label.should.equal('⇈'); + direction.info({mills: now, direction: 'DoubleUp'}).entity.should.equal('⇈'); - direction.info({direction: 'SingleUp'}).label.should.equal('↑'); - direction.info({direction: 'SingleUp'}).entity.should.equal('↑'); + direction.info({mills: now, direction: 'SingleUp'}).label.should.equal('↑'); + direction.info({mills: now, direction: 'SingleUp'}).entity.should.equal('↑'); - direction.info({direction: 'FortyFiveUp'}).label.should.equal('↗'); - direction.info({direction: 'FortyFiveUp'}).entity.should.equal('↗'); + direction.info({mills: now, direction: 'FortyFiveUp'}).label.should.equal('↗'); + direction.info({mills: now, direction: 'FortyFiveUp'}).entity.should.equal('↗'); - direction.info({direction: 'Flat'}).label.should.equal('→'); - direction.info({direction: 'Flat'}).entity.should.equal('→'); + direction.info({mills: now, direction: 'Flat'}).label.should.equal('→'); + direction.info({mills: now, direction: 'Flat'}).entity.should.equal('→'); - direction.info({direction: 'FortyFiveDown'}).label.should.equal('↘'); - direction.info({direction: 'FortyFiveDown'}).entity.should.equal('↘'); + direction.info({mills: now, direction: 'FortyFiveDown'}).label.should.equal('↘'); + direction.info({mills: now, direction: 'FortyFiveDown'}).entity.should.equal('↘'); - direction.info({direction: 'SingleDown'}).label.should.equal('↓'); - direction.info({direction: 'SingleDown'}).entity.should.equal('↓'); + direction.info({mills: now, direction: 'SingleDown'}).label.should.equal('↓'); + direction.info({mills: now, direction: 'SingleDown'}).entity.should.equal('↓'); - direction.info({direction: 'DoubleDown'}).label.should.equal('⇊'); - direction.info({direction: 'DoubleDown'}).entity.should.equal('⇊'); + direction.info({mills: now, direction: 'DoubleDown'}).label.should.equal('⇊'); + direction.info({mills: now, direction: 'DoubleDown'}).entity.should.equal('⇊'); - direction.info({direction: 'NOT COMPUTABLE'}).label.should.equal('-'); - direction.info({direction: 'NOT COMPUTABLE'}).entity.should.equal('-'); + direction.info({mills: now, direction: 'NOT COMPUTABLE'}).label.should.equal('-'); + direction.info({mills: now, direction: 'NOT COMPUTABLE'}).entity.should.equal('-'); - direction.info({direction: 'RATE OUT OF RANGE'}).label.should.equal('⇕'); - direction.info({direction: 'RATE OUT OF RANGE'}).entity.should.equal('⇕'); + direction.info({mills: now, direction: 'RATE OUT OF RANGE'}).label.should.equal('⇕'); + direction.info({mills: now, direction: 'RATE OUT OF RANGE'}).entity.should.equal('⇕'); }); diff --git a/tests/env.test.js b/tests/env.test.js index 14398a60135..0de2a686054 100644 --- a/tests/env.test.js +++ b/tests/env.test.js @@ -2,55 +2,154 @@ require('should'); -describe('env', function ( ) { - it('show the right plugins', function () { +describe('env', function () { + it( 'show the right plugins', function () { process.env.SHOW_PLUGINS = 'iob'; process.env.ENABLE = 'iob cob'; - var env = require('../env')(); + var env = require( '../lib/server/env' )(); var showPlugins = env.settings.showPlugins; - showPlugins.should.containEql('iob'); - showPlugins.should.containEql('delta'); - showPlugins.should.containEql('direction'); - showPlugins.should.containEql('upbat'); - showPlugins.should.containEql('cob'); + showPlugins.should.containEql( 'iob' ); + showPlugins.should.containEql( 'delta' ); + showPlugins.should.containEql( 'direction' ); + showPlugins.should.containEql( 'upbat' ); delete process.env.SHOW_PLUGINS; delete process.env.ENABLE; - }); + } ); - it('get extended settings', function () { + it( 'get extended settings', function () { process.env.ENABLE = 'scaryplugin'; process.env.SCARYPLUGIN_DO_THING = 'yes'; - var env = require('../env')(); - env.settings.isEnabled('scaryplugin').should.equal(true); + var env = require( '../lib/server/env' )(); + env.settings.isEnabled( 'scaryplugin' ).should.equal( true ); //Note the camelCase - env.extendedSettings.scaryplugin.doThing.should.equal('yes'); + env.extendedSettings.scaryplugin.doThing.should.equal( 'yes' ); delete process.env.ENABLE; delete process.env.SCARYPLUGIN_DO_THING; - }); + } ); - it('add pushover to enable if one of the env vars is set', function () { + it( 'add pushover to enable if one of the env vars is set', function () { process.env.PUSHOVER_API_TOKEN = 'abc12345'; - var env = require('../env')(); - env.settings.enable.should.containEql('pushover'); - env.extendedSettings.pushover.apiToken.should.equal('abc12345'); + var env = require( '../lib/server/env' )(); + env.settings.enable.should.containEql( 'pushover' ); + env.extendedSettings.pushover.apiToken.should.equal( 'abc12345' ); delete process.env.PUSHOVER_API_TOKEN; - }); + } ); - it('add pushover to enable if one of the weird azure env vars is set', function () { + it( 'add pushover to enable if one of the weird azure env vars is set', function () { process.env.CUSTOMCONNSTR_PUSHOVER_API_TOKEN = 'abc12345'; - var env = require('../env')(); - env.settings.enable.should.containEql('pushover'); - env.extendedSettings.pushover.apiToken.should.equal('abc12345'); + var env = require( '../lib/server/env' )(); + env.settings.enable.should.containEql( 'pushover' ); + env.extendedSettings.pushover.apiToken.should.equal( 'abc12345' ); delete process.env.PUSHOVER_API_TOKEN; + } ); + + it( 'readENVTruthy ', function () { + process.env.INSECURE_USE_HTTP = 'true'; + var env = require( '../lib/server/env' )(); + env.insecureUseHttp.should.be.true(); + process.env.INSECURE_USE_HTTP = 'false'; + env = require( '../lib/server/env' )(); + env.insecureUseHttp.should.be.false(); + process.env.INSECURE_USE_HTTP = 'not set ok, so use default value false'; + env = require( '../lib/server/env' )(); + env.insecureUseHttp.should.be.false(); + delete process.env.INSECURE_USE_HTTP; // unset INSECURE_USE_HTTP + process.env.SECURE_HSTS_HEADER = 'true'; + env = require( '../lib/server/env' )(); + env.insecureUseHttp.should.be.false(); // not defined should be false + env.secureHstsHeader.should.be.true(); }); -}); + describe( 'DISPLAY_UNITS', function () { + const MMOL = 'mmol'; + const MGDL = 'mg/dl'; + describe ( 'mmol', function () { + it( 'mmol => mmol', function () { + process.env.DISPLAY_UNITS = MMOL; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MMOL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'mmol/l => mmol', function () { + process.env.DISPLAY_UNITS = 'mmol/l'; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MMOL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'mmol/L => mmol', function () { + process.env.DISPLAY_UNITS = 'mmol/L'; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MMOL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'MMOL => mmol', function () { + process.env.DISPLAY_UNITS = 'MMOL'; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MMOL ); + delete process.env.DISPLAY_UNITS; + } ); + } ); + + describe ( 'mg/dl', function () { + it( 'mg/dl => mg/dl', function () { + process.env.DISPLAY_UNITS = MGDL; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'mg/dL => mg/dl', function () { + process.env.DISPLAY_UNITS = 'mg/dL'; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'MG/DL => mg/dl', function () { + process.env.DISPLAY_UNITS = 'MG/DL'; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( 'mgdl => mg/dl', function () { + process.env.DISPLAY_UNITS = 'mgdl'; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + } ); + + describe ( 'default: mg/dl', function () { + it( ' => mg/dl', function () { + var random; + while (!random || random.toLowerCase() === MGDL) + random = [...Array(~~(Math.random()*20)+1)].map(i=>(~~(Math.random()*36)).toString(36)).join(''); + + process.env.DISPLAY_UNITS = random; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + + it( ' => mg/dl', function () { + delete process.env.DISPLAY_UNITS; + var env = require( '../lib/server/env' )(); + env.settings.units.should.equal( MGDL ); + delete process.env.DISPLAY_UNITS; + } ); + } ); + } ); +}) diff --git a/tests/errorcodes.test.js b/tests/errorcodes.test.js index ed1b158637b..b8d3617710f 100644 --- a/tests/errorcodes.test.js +++ b/tests/errorcodes.test.js @@ -4,44 +4,45 @@ var levels = require('../lib/levels'); describe('errorcodes', function ( ) { - var errorcodes = require('../lib/plugins/errorcodes')(); - var now = Date.now(); - var env = require('../env')(); + var env = require('../lib/server/env')(); var ctx = {}; - ctx.data = require('../lib/data')(env, ctx); + ctx.ddata = require('../lib/data/ddata')(); ctx.notifications = require('../lib/notifications')(env, ctx); + ctx.language = require('../lib/language')(); + ctx.levels = levels; + var errorcodes = require('../lib/plugins/errorcodes')(ctx); it('Not trigger an alarm when in range', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 100, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 100, mills: now}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); errorcodes.checkNotifications(sbx); - should.not.exist(ctx.notifications.findHighestAlarm()); + should.not.exist(ctx.notifications.findHighestAlarm('CGM Error Code')); done(); }); it('should trigger a urgent alarm when ???', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 10, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 10, mills: now}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); errorcodes.checkNotifications(sbx); - ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + ctx.notifications.findHighestAlarm('CGM Error Code').level.should.equal(levels.URGENT); done(); }); it('should trigger a urgent alarm when hourglass', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 9, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 9, mills: now}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); errorcodes.checkNotifications(sbx); - var findHighestAlarm = ctx.notifications.findHighestAlarm(); + var findHighestAlarm = ctx.notifications.findHighestAlarm('CGM Error Code'); findHighestAlarm.level.should.equal(levels.URGENT); findHighestAlarm.pushoverSound.should.equal('alien'); @@ -50,11 +51,11 @@ describe('errorcodes', function ( ) { it('should trigger a low notification when needing calibration', function (done) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: 5, mills: now}]; + ctx.ddata.sgvs = [{mgdl: 5, mills: now}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); errorcodes.checkNotifications(sbx); - should.not.exist(ctx.notifications.findHighestAlarm()); + should.not.exist(ctx.notifications.findHighestAlarm('CGM Error Code')); var info = _.first(ctx.notifications.findUnSnoozeable()); info.level.should.equal(levels.INFO); info.pushoverSound.should.equal('intermission'); @@ -66,11 +67,11 @@ describe('errorcodes', function ( ) { for (var i = 1; i < 9; i++) { ctx.notifications.initRequests(); - ctx.data.sgvs = [{mgdl: i, mills: now}]; + ctx.ddata.sgvs = [{mgdl: i, mills: now}]; var sbx = require('../lib/sandbox')().serverInit(env, ctx); errorcodes.checkNotifications(sbx); - should.not.exist(ctx.notifications.findHighestAlarm()); + should.not.exist(ctx.notifications.findHighestAlarm('CGM Error Code')); _.first(ctx.notifications.findUnSnoozeable()).level.should.be.lessThan(levels.WARN); } done(); diff --git a/tests/expressextensions.test.js b/tests/expressextensions.test.js new file mode 100644 index 00000000000..b65ebd8952b --- /dev/null +++ b/tests/expressextensions.test.js @@ -0,0 +1,33 @@ +'use strict'; + +require('should'); + +var extensionsMiddleware = require('../lib/middleware/express-extension-to-accept.js'); + +var acceptJsonRequests = extensionsMiddleware(['json']); + +describe('Express extension middleware', function ( ) { + + it('Valid json request should be given accept header for application/json', function () { + var entriesRequest = { + path: '/api/v1/entries.json', + url: '/api/v1/entries.json', + headers: {} + }; + + acceptJsonRequests(entriesRequest, {}, () => {}); + entriesRequest.headers.accept.should.equal('application/json'); + }); + + it('Invalid json request should NOT be given accept header', function () { + var invalidEntriesRequest = { + path: '/api/v1/entriesXjson', + url: '/api/v1/entriesXjson', + headers: {} + }; + + acceptJsonRequests(invalidEntriesRequest, {}, () => {}); + should(invalidEntriesRequest.headers.accept).not.be.ok; + }); + +}); diff --git a/tests/fail.test.js b/tests/fail.test.js new file mode 100644 index 00000000000..eefda445b3d --- /dev/null +++ b/tests/fail.test.js @@ -0,0 +1,14 @@ +'use strict'; + +require('should'); + +// This test is included just so we have an easy to template to intentionally cause +// builds to fail + +describe('fail', function ( ) { + + it('should not fail', function () { + true.should.equal(true); + }); + +}); diff --git a/tests/fixtures/api/instance.js b/tests/fixtures/api/instance.js new file mode 100644 index 00000000000..45f49185cdf --- /dev/null +++ b/tests/fixtures/api/instance.js @@ -0,0 +1,98 @@ +'use strict'; + +const fs = require('fs') + , path = require('path') + , language = require('../../../lib/language')() + , apiRoot = require('../../../lib/api/root') + , http = require('http') + , https = require('https') + ; + +function configure () { + const self = { }; + + self.prepareEnv = function prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }) { + + if (useHttps) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + else { + process.env.INSECURE_USE_HTTP = true; + } + process.env.API_SECRET = apiSecret; + + process.env.HOSTNAME = 'localhost'; + const env = require('../../../lib/server/env')(); + + if (useHttps) { + env.ssl = { + key: fs.readFileSync(path.join(__dirname, '../api3/localhost.key')), + cert: fs.readFileSync(path.join(__dirname, '../api3/localhost.crt')) + }; + } + + env.settings.authDefaultRoles = authDefaultRoles; + env.settings.enable = enable; + + return env; + }; + + + /* + * Create new web server instance for testing purposes + */ + self.create = function createHttpServer ({ + apiSecret = 'this is my long pass phrase', + useHttps = true, + authDefaultRoles = '', + enable = ['careportal', 'api'] + }) { + + return new Promise(function (resolve, reject) { + + try { + let instance = { }, + hasBooted = false + ; + + instance.env = self.prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }); + + self.wares = require('../../../lib/middleware/')(instance.env); + instance.app = require('express')(); + instance.app.enable('api'); + + require('../../../lib/server/bootevent')(instance.env, language).boot(function booted (ctx) { + instance.ctx = ctx; + instance.ctx.ddata = require('../../../lib/data/ddata')(); + instance.ctx.apiRootApp = apiRoot(instance.env, ctx); + + instance.app.use('/api', instance.ctx.apiRootApp); + + const transport = useHttps ? https : http; + + instance.server = transport.createServer(instance.env.ssl || { }, instance.app).listen(0); + instance.env.PORT = instance.server.address().port; + + instance.baseUrl = `${useHttps ? 'https' : 'http'}://${instance.env.HOSTNAME}:${instance.env.PORT}`; + + console.log(`Started ${useHttps ? 'SSL' : 'HTTP'} instance on ${instance.baseUrl}`); + hasBooted = true; + resolve(instance); + }); + + setTimeout(function watchDog() { + if (!hasBooted) + reject('timeout'); + }, 30000); + + } catch (err) { + reject(err); + } + }); + }; + + + return self; +} + +module.exports = configure(); \ No newline at end of file diff --git a/tests/fixtures/api3/authSubject.js b/tests/fixtures/api3/authSubject.js new file mode 100644 index 00000000000..5d0385477a1 --- /dev/null +++ b/tests/fixtures/api3/authSubject.js @@ -0,0 +1,138 @@ +'use strict'; + +const _ = require('lodash') + , request = require('supertest') + ; +require('should'); + +function createRole (authStorage, name, permissions) { + + return new Promise((resolve, reject) => { + + let role = _.find(authStorage.roles, { name }); + + if (role) { + resolve(role); + } + else { + authStorage.createRole({ + "name": name, + "permissions": permissions, + "notes": "" + }, function afterCreate (err) { + + if (err) + reject(err); + + role = _.find(authStorage.roles, { name }); + resolve(role); + }); + } + }); +} + + +function createTestSubject (authStorage, subjectName, roles) { + + return new Promise((resolve, reject) => { + + const subjectDbName = 'test-' + subjectName; + let subject = _.find(authStorage.subjects, { name: subjectDbName }); + + if (subject) { + resolve(subject); + } + else { + authStorage.createSubject({ + "name": subjectDbName, + "roles": roles, + "notes": "" + }, function afterCreate (err) { + + if (err) + reject(err); + + subject = _.find(authStorage.subjects, { name: subjectDbName }); + resolve(subject); + }); + } + }); +} + + +async function initJwts (accessToken, tokensNeeded, app) { + const jwt = {} + if (!_.isArray(tokensNeeded) || !app) + return jwt; + + for (const tokenNeeded of tokensNeeded) { + jwt[tokenNeeded] = await new Promise((resolve, reject) => { + try { + const authToken = accessToken[tokenNeeded]; + + request(app) + .get(`/api/v2/authorization/request/${authToken}`) + .expect(200) + .end(function(err, res) { + if (err) + reject(err); + + resolve(res.body.token); + }); + } + catch (e) { + reject(e) + } + }) + } + + return jwt; +} + + +async function authSubject (authStorage, tokensNeeded, app) { + + await createRole(authStorage, 'admin', '*'); + await createRole(authStorage, 'readable', '*:*:read'); + await createRole(authStorage, 'apiAll', 'api:*:*'); + await createRole(authStorage, 'apiAdmin', 'api:*:admin'); + await createRole(authStorage, 'apiCreate', 'api:*:create'); + await createRole(authStorage, 'apiRead', 'api:*:read'); + await createRole(authStorage, 'apiUpdate', 'api:*:update'); + await createRole(authStorage, 'apiDelete', 'api:*:delete'); + await createRole(authStorage, 'noneRole', ''); + + const subject = { + apiAll: await createTestSubject(authStorage, 'apiAll', ['apiAll']), + apiAdmin: await createTestSubject(authStorage, 'apiAdmin', ['apiAdmin']), + apiCreate: await createTestSubject(authStorage, 'apiCreate', ['apiCreate']), + apiRead: await createTestSubject(authStorage, 'apiRead', ['apiRead']), + apiUpdate: await createTestSubject(authStorage, 'apiUpdate', ['apiUpdate']), + apiDelete: await createTestSubject(authStorage, 'apiDelete', ['apiDelete']), + admin: await createTestSubject(authStorage, 'admin', ['admin']), + readable: await createTestSubject(authStorage, 'readable', ['readable']), + denied: await createTestSubject(authStorage, 'denied', ['denied']), + noneSubject: await createTestSubject(authStorage, 'noneSubject', null), + noneRole: await createTestSubject(authStorage, 'noneRole', ['noneRole']) + }; + + const accessToken = { + all: subject.apiAll.accessToken, + admin: subject.apiAdmin.accessToken, + create: subject.apiCreate.accessToken, + read: subject.apiRead.accessToken, + update: subject.apiUpdate.accessToken, + delete: subject.apiDelete.accessToken, + denied: subject.denied.accessToken, + adminAll: subject.admin.accessToken, + readable: subject.readable.accessToken, + noneSubject: subject.noneSubject.accessToken, + noneRole: subject.noneRole.accessToken + }; + + const jwt = await initJwts(accessToken, tokensNeeded, app); + + return {subject, accessToken, jwt}; +} + +module.exports = authSubject; diff --git a/tests/fixtures/api3/cacheMonitor.js b/tests/fixtures/api3/cacheMonitor.js new file mode 100644 index 00000000000..2c4180b0109 --- /dev/null +++ b/tests/fixtures/api3/cacheMonitor.js @@ -0,0 +1,79 @@ +'use strict'; + +/** + * Cache monitoring mechanism for tracking and checking cache updates. + * @param instance + * @constructor + */ +function CacheMonitor (instance) { + + const self = this; + + let operations = [] + , listening = false; + + instance.ctx.bus.on('data-update', operation => { + if (listening) { + operations.push(operation); + } + }); + + self.listen = () => { + listening = true; + return self; + } + + self.stop = () => { + listening = false; + return self; + } + + self.clear = () => { + operations = []; + return self; + } + + self.shouldBeEmpty = () => { + operations.length.should.equal(0) + } + + self.nextShouldEql = (col, doc) => { + operations.length.should.not.equal(0) + + const operation = operations.shift(); + operation.type.should.equal(col); + operation.op.should.equal('update'); + + if (doc) { + operation.changes.should.be.Array(); + operation.changes.length.should.be.eql(1); + const change = operation.changes[0]; + change.should.containEql(doc); + } + } + + self.nextShouldEqlLast = (col, doc) => { + self.nextShouldEql(col, doc); + self.shouldBeEmpty(); + } + + self.nextShouldDelete = (col, _id) => { + operations.length.should.not.equal(0) + + const operation = operations.shift(); + operation.type.should.equal(col); + operation.op.should.equal('remove'); + + if (_id) { + operation.changes.should.equal(_id); + } + } + + self.nextShouldDeleteLast = (col, _id) => { + self.nextShouldDelete(col, _id); + self.shouldBeEmpty(); + } + +} + +module.exports = CacheMonitor; diff --git a/tests/fixtures/api3/const.json b/tests/fixtures/api3/const.json new file mode 100644 index 00000000000..a0acf37cfee --- /dev/null +++ b/tests/fixtures/api3/const.json @@ -0,0 +1,138 @@ +{ + "YEAR_2019": 1546304400000, + "YEAR_2050": 2524611600000, + "TEST_APP": "cgm-remote-monitor.test", + "TEST_DEVICE": "Samsung XCover 4-123456735643809", + + "SAMPLE_ENTRIES": [ + { + "date": 1491717830000.0, + "device": "dexcom", + "direction": "FortyFiveUp", + "filtered": 167584, + "noise": 2, + "rssi": 183, + "sgv": 149, + "type": "sgv", + "unfiltered": 171584, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718130000.0, + "device": "dexcom", + "direction": "FortyFiveUp", + "filtered": 170656, + "noise": 2, + "rssi": 181, + "sgv": 152, + "type": "sgv", + "unfiltered": 175776, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718430000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 173536, + "noise": 2, + "rssi": 185, + "sgv": 155, + "type": "sgv", + "unfiltered": 180864, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491718730000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 177120, + "noise": 2, + "rssi": 186, + "sgv": 159, + "type": "sgv", + "unfiltered": 182080, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719030000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 181088, + "noise": 2, + "rssi": 165, + "sgv": 163, + "type": "sgv", + "unfiltered": 186912, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719330000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 184736, + "noise": 1, + "rssi": 162, + "sgv": 170, + "type": "sgv", + "unfiltered": 188512, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719630000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 187776, + "noise": 1, + "rssi": 175, + "sgv": 175, + "type": "sgv", + "unfiltered": 192608, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491719930000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 190816, + "noise": 1, + "rssi": 181, + "sgv": 179, + "type": "sgv", + "unfiltered": 196640, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491720230000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 194016, + "noise": 1, + "rssi": 203, + "sgv": 181, + "type": "sgv", + "unfiltered": 199008, + "app": "cgm-remote-monitor.test" + }, + + { + "date": 1491720530000.0, + "device": "dexcom", + "direction": "Flat", + "filtered": 197536, + "noise": 1, + "rssi": 184, + "sgv": 186, + "type": "sgv", + "unfiltered": 203296, + "app": "cgm-remote-monitor.test" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/api3/instance.js b/tests/fixtures/api3/instance.js new file mode 100644 index 00000000000..cbeaf6f97d0 --- /dev/null +++ b/tests/fixtures/api3/instance.js @@ -0,0 +1,174 @@ +'use strict'; + +var fs = require('fs') + , language = require('../../../lib/language')() + , api = require('../../../lib/api3/') + , http = require('http') + , https = require('https') + , request = require('supertest') + , websocket = require('../../../lib/server/websocket') + , io = require('socket.io-client') + , CacheMonitor = require('./cacheMonitor') + ; + +function configure () { + const self = { }; + + self.prepareEnv = function prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }) { + + if (useHttps) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + } + else { + process.env.INSECURE_USE_HTTP = true; + } + process.env.API_SECRET = apiSecret; + + process.env.HOSTNAME = 'localhost'; + const env = require('../../../lib/server/env')(); + + if (useHttps) { + env.ssl = { + key: fs.readFileSync(__dirname + '/localhost.key'), + cert: fs.readFileSync(__dirname + '/localhost.crt') + }; + } + + env.settings.authDefaultRoles = authDefaultRoles; + env.settings.enable = enable; + + return env; + }; + + + function addJwt (req, jwt) { + return jwt + ? req.set('Authorization', `Bearer ${jwt}`) + : req; + } + + + self.addSecuredOperations = function addSecuredOperations (instance) { + + instance.get = (url, jwt) => addJwt(request(instance.baseUrl).get(url), jwt); + + instance.post = (url, jwt) => addJwt(request(instance.baseUrl).post(url), jwt); + + instance.put = (url, jwt) => addJwt(request(instance.baseUrl).put(url), jwt); + + instance.patch = (url, jwt) => addJwt(request(instance.baseUrl).patch(url), jwt); + + instance.delete = (url, jwt) => addJwt(request(instance.baseUrl).delete(url), jwt); + }; + + + + self.bindSocket = function bindSocket (storageSocket, instance) { + + return new Promise(function (resolve, reject) { + if (!storageSocket) { + resolve(); + } + else { + let socket = io(`${instance.baseUrl}/storage`, { + origins:"*", + transports: ['websocket', 'flashsocket', 'polling'], + rejectUnauthorized: false + }); + + socket.on('connect', function () { + resolve(socket); + }); + socket.on('connect_error', function (error) { + console.error(error); + reject(error); + }); + } + }); + }; + + + self.unbindSocket = function unbindSocket (instance) { + if (instance.clientSocket.connected) { + instance.clientSocket.disconnect(); + } + }; + + /* + * Create new web server instance for testing purposes + */ + self.create = function createHttpServer ({ + apiSecret = 'this is my long pass phrase', + disableSecurity = false, + useHttps = true, + authDefaultRoles = '', + enable = ['careportal', 'api'], + storageSocket = null + }) { + + return new Promise(function (resolve, reject) { + + try { + let instance = { }, + hasBooted = false + ; + + instance.env = self.prepareEnv({ apiSecret, useHttps, authDefaultRoles, enable }); + + self.wares = require('../../../lib/middleware/')(instance.env); + instance.app = require('express')(); + instance.app.enable('api'); + + require('../../../lib/server/bootevent')(instance.env, language).boot(function booted (ctx) { + instance.ctx = ctx; + instance.ctx.ddata = require('../../../lib/data/ddata')(); + instance.ctx.apiApp = api(instance.env, ctx); + + if (disableSecurity) { + instance.ctx.apiApp.set('API3_SECURITY_ENABLE', false); + } + + instance.app.use('/api/v3', instance.ctx.apiApp); + instance.app.use('/api/v2/authorization', instance.ctx.authorization.endpoints); + + const transport = useHttps ? https : http; + + instance.server = transport.createServer(instance.env.ssl || { }, instance.app).listen(0); + instance.env.PORT = instance.server.address().port; + + instance.baseUrl = `${useHttps ? 'https' : 'http'}://${instance.env.HOSTNAME}:${instance.env.PORT}`; + + self.addSecuredOperations(instance); + instance.cacheMonitor = new CacheMonitor(instance).listen(); + + websocket(instance.env, instance.ctx, instance.server); + + self.bindSocket(storageSocket, instance) + .then((socket) => { + instance.clientSocket = socket; + + console.log(`Started ${useHttps ? 'SSL' : 'HTTP'} instance on ${instance.baseUrl}`); + hasBooted = true; + resolve(instance); + }) + .catch((reason) => { + console.error(reason); + reject(reason); + }); + }); + + setTimeout(function watchDog() { + if (!hasBooted) + reject('timeout'); + }, 30000); + + } catch (err) { + reject(err); + } + }); + }; + + return self; +} + +module.exports = configure(); diff --git a/tests/fixtures/api3/localhost.crt b/tests/fixtures/api3/localhost.crt new file mode 100644 index 00000000000..21a2a39b0a4 --- /dev/null +++ b/tests/fixtures/api3/localhost.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJAIx0y57dTqDpMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xOTAyMDQxOTM1MDhaFw0yOTAyMDExOTM1MDhaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKCeBaqAJU+nrzNUZMsD1jYQpmcw8+6tG69KQY2XmqMsaPupo2ArwUlYD3pm +F1HTf9Lkq8u07rlUyMaSSRYrY56vPrMWGSK5Elm4kF8DNS4b/55KwZC+YQM0ZuJK +wSM6WX4G7JwV936HKJAT+Ec+8Ofq3GQzA9+Z4x2zMwNGC8AghtPjsCk68ORCmr+5 +fdCdC1Rz9hE92Nmofi8e1hUTeZmFROx8hcYRhxYXLIWVxALc/t8yY3MZfsRuZXcP +/3PageAn0ecxhqlWBY23GDQx7OSEZxSEPgqxnAHQfQXIrPRjMkFNHeMM7HTvITAG +VCc99zEG3Jy5hatm+RAajdWBH4sCAwEAAaNQME4wHQYDVR0OBBYEFJJVZn5Y91O7 +JUKeHW4La8eseKKwMB8GA1UdIwQYMBaAFJJVZn5Y91O7JUKeHW4La8eseKKwMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAFOU19t9h6C1Hakkik/93kun +pwG7v8VvDPjKECR5KlNPKNZUOQaiMAVHgNwPWV8q+qvfydzIpDrTd/O5eOaOduLx +gDVDj078Q05j17RUC+ct5yQ6lPgEHlnkI0Zr/hgFyNC+mtK7oIm6BT8wSSRbv7AG +3wQzCA5UvW/BQ8rtNZSC42Jyr0BR0ZS9Fo3Gc4v/nZJlgkiBvU2gKVQ7VRKxybCn +0hDghVwTfBPq7PKmupLX82ktwhYpDJZXCsOVfq9mF6nbQ6b0MieXFD+7cBlEXb1e +3VgtVzKYyqh/Oex4HfMThzAJZSWa0E4FShr5XdTdIc3nB4Vgbsis5l9Yrcp3Xo4= +-----END CERTIFICATE----- diff --git a/tests/fixtures/api3/localhost.key b/tests/fixtures/api3/localhost.key new file mode 100644 index 00000000000..2486c15fefe --- /dev/null +++ b/tests/fixtures/api3/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAoJ4FqoAlT6evM1RkywPWNhCmZzDz7q0br0pBjZeaoyxo+6mj +YCvBSVgPemYXUdN/0uSry7TuuVTIxpJJFitjnq8+sxYZIrkSWbiQXwM1Lhv/nkrB +kL5hAzRm4krBIzpZfgbsnBX3focokBP4Rz7w5+rcZDMD35njHbMzA0YLwCCG0+Ow +KTrw5EKav7l90J0LVHP2ET3Y2ah+Lx7WFRN5mYVE7HyFxhGHFhcshZXEAtz+3zJj +cxl+xG5ldw//c9qB4CfR5zGGqVYFjbcYNDHs5IRnFIQ+CrGcAdB9Bcis9GMyQU0d +4wzsdO8hMAZUJz33MQbcnLmFq2b5EBqN1YEfiwIDAQABAoIBADoh95sGVnrGDjtd +yD1SXi2jSRcAOMmiDesbzS4aOPXmFPlBJMiiDYsmPDPoz3fmPNVvvl40VlLtxN1a +BOnpOl0swFzBGsfehC3FBzvcRVsy9wmrtPNWdHZceQBeXhkJ/WoHx4uWx8Ub1iqP +j8T5mufVsX7yl+xOHk2ZllUQ/R/EEz9x00pkiH8Vsn8DhFI5KNqgi4n4c36T3vrn +MjTp+1o7bJ/cEnvXLi+IG2CO5y5hVbu3iKb+71YOGc6f/AJVzZ3MegC3KMFho9lh +DbDzumMuW8fZNyBfslXXoOr6oDqNq92n/jC/2hR8Xlth/aafisJiIVGydeVdDXhM +gDjdroECgYEAy3hXuo/Q1acncInGhIJvHjS/sVShP2epHz9zp8XuWl4NCuGP5V2c +jLT0hDW+ZKTUFweK9sQJNta81gs4pYc+2HGI8RP65XW4vgesNoKbBcE9xhEq0HMX +KN3/MJiwkNkM95T3nWqulhzNszhgNbZDMAU3Ule+o4n8udwOlFCTeXMCgYEAyhV4 +PoL3wp05BY0ssyKEqld3EqHNlPdQeJe1Dg9LSBy+3Z9sNngRD1/FuTo7RX6UY0FH +MaSI1JwhHSQ+2GNkqdMvVAilTXIDRw8vU9B77bYiHjny8+vMU06I9V3cJ57bNfmR +NUJtPmGO9xQ5UYxhP9rFOcI4MIecSzu1tvqiG4kCgYB01NoS7sdsFrnnvcS2i6rA +PmufqEeaf6w1nBqNyHJPg1eb2t7kRfdBOBp6291CLv71Zkhd3zynN3BguzrAmUL1 +x2Npgh57qTf2LbOt7RqUmFwfIfZikONIfQgt4E7qLSdr9iakRgCPg2R9ty5PSSOV +LDmS131IrE/obLoWYZn8jwKBgQDIaAxMahONA+CFueCHcgcA6yah6qZ3QeCjB0g9 +vjsZM7CxFqX5So8YoRDzxWT8YTCFUjppZ9NujbtlLAnLDJ7KsC2yd7R/Hj9T3CJC +S3JrZoFlWnCvJ7wFLdAzDTcEb8zTNUGlANBX2eYu7/Z8Aex7p9iJlCunLQV5sqhd +4yaaiQKBgQCERrz1XcJpM8S93nXdAv3Nn1bwA1V/ylx42DRxNEBl2JZQ1sQeqN36 +JvXPXhVZ3vTQDhVUqcVgqJIAb2xMviIVBnssOq3+pi/hOs13rakJf4AOulZ/3Si7 +HSLdymfQAMEKczU2261kw4pjPwiurkjAFWbQG2C8RGE/rR2y38PkDg== +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/api3/utils.js b/tests/fixtures/api3/utils.js new file mode 100644 index 00000000000..942f948c10e --- /dev/null +++ b/tests/fixtures/api3/utils.js @@ -0,0 +1,21 @@ +'use strict'; + +function randomString (length, chars) { + let mask = ''; + if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz'; + if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + if (chars.indexOf('#') > -1) mask += '0123456789'; + if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\'; + + let result = ''; + + for (let i = length; i > 0; --i) + result += mask[Math.floor(Math.random() * mask.length)]; + + return result; +} + + +module.exports = { + randomString +}; \ No newline at end of file diff --git a/tests/fixtures/default-server-settings.js b/tests/fixtures/default-server-settings.js index 113a5fa4aeb..ac3d31d25a0 100644 --- a/tests/fixtures/default-server-settings.js +++ b/tests/fixtures/default-server-settings.js @@ -33,4 +33,5 @@ module.exports = { } , extendedSettings: { } } + , runtimeState: 'loaded' }; \ No newline at end of file diff --git a/tests/fixtures/headless.js b/tests/fixtures/headless.js new file mode 100644 index 00000000000..310566e6132 --- /dev/null +++ b/tests/fixtures/headless.js @@ -0,0 +1,186 @@ + +var read = require('fs').readFileSync; +var _ = require('lodash'); + +function headless (benv, binding) { + var self = binding; + function root ( ) { + return benv; + } + + function init (opts, callback) { + + var localStorage = opts.localStorage || './localstorage'; + const t = Date.now(); + + console.log('Headless init'); + + var htmlFile = opts.htmlFile || __dirname + '/../../views/index.html'; + var serverSettings = opts.serverSettings || require('./default-server-settings'); + var someData = opts.mockAjax || { }; + + console.log('Entering setup', Date.now() - t); + + benv.setup(function() { + + console.log('Setting up benv', Date.now() - t); + + benv.require(__dirname + '/../../node_modules/.cache/_ns_cache/public/js/bundle.app.js'); + + console.log('Bundle loaded', Date.now() - t); + + self.$ = $; + + self.localCookieStorage = self.localStorage = self.$.localStorage = require(localStorage); + + self.$.fn.tooltip = function mockTooltip ( ) { }; + + var indexHtml = read(htmlFile, 'utf8'); + self.$('body').html(indexHtml); + + console.log('HTML set', Date.now() - t); + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + if (opts.mockProfileEditor) { + self.$.plot = function mockPlot () { + }; + + self.$.fn.tooltip = function mockTooltip ( ) { }; + + self.$.fn.dialog = function mockDialog (opts) { + function maybeCall (name, obj) { + if (obj[name] && obj[name].call) { + obj[name](); + } + + } + maybeCall('open', opts); + + _.forEach(opts.buttons, function (button) { + maybeCall('click', button); + }); + }; + } + if (opts.mockSimpleAjax) { + someData = opts.mockSimpleAjax; + self.$.ajax = function mockAjax (url, opts) { + if (url && url.url) { + url = url.url; + } + + var returnVal = someData[url] || []; + if (opts && typeof opts.success === 'function') { + opts.success(returnVal); + return self.$.Deferred().resolveWith(returnVal); + } else { + return { + done: function mockDone (fn) { + if (url.indexOf('status.json') > -1) { + fn(serverSettings); + } else { + fn({message: 'OK'}); + } + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + } + }; + } + if (opts.mockAjax) { + self.$.ajax = function mockAjax (url, opts) { + + if (url && url.url) { + url = url.url; + } + + //logfile.write(url+'\n'); + //console.log(url,opts); + if (opts && opts.success && opts.success.call) { + return { + done: function mockDone (fn) { + if (someData[url]) { + console.log('+++++Data for ' + url + ' sent'); + opts.success(someData[url]); + } else { + console.log('-----Data for ' + url + ' missing'); + opts.success([]); + } + fn(); + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + } + return { + done: function mockDone (fn) { + if (url.indexOf('status.json') > -1) { + fn(serverSettings); + } else { + fn({message: 'OK'}); + } + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + }; + } + + console.log('Benv expose', Date.now() - t); + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , serverSettings: serverSettings + , localCookieStorage: self.localStorage + , cookieStorageType: self.localStorage + , localStorage: self.localStorage + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn (event, callback) { + if ('connect' === event && callback) { + callback(); + } + } + , emit: function mockEmit (event, data, callback) { + if ('authorize' === event && callback) { + callback({ + read: true + }); + } + } + }; + } + } + }); + + var extraRequires = opts.benvRequires || [ ]; + extraRequires.forEach(function (req) { + benv.require(req); + }); + callback( ); + }); + + } + + function teardown ( ) { + benv.teardown(); + } + root.setup = init; + root.teardown = teardown; + + return root; +} + +module.exports = headless; diff --git a/tests/fixtures/localstorage.js b/tests/fixtures/localstorage.js index 0942cc2fffc..d1b463e4928 100644 --- a/tests/fixtures/localstorage.js +++ b/tests/fixtures/localstorage.js @@ -6,12 +6,21 @@ var localstorage = { get: function Get(item) { return browserStorage[item] || null; } + , getItem: function Get(item) { + return browserStorage[item] || null; + } , set: function Set(item, value) { browserStorage[item] = value; } + , setItem: function Set(item, value) { + browserStorage[item] = value; + } , remove: function Remove(item) { delete browserStorage[item]; } + , removeItem: function Remove(item) { + delete browserStorage[item]; + } , removeAll: function RemoveAll() { browserStorage = []; } diff --git a/tests/fixtures/openaps-storage/cgm-loop/monitor/cal-zoned.json b/tests/fixtures/openaps-storage/cgm-loop/monitor/cal-zoned.json new file mode 100644 index 00000000000..36ea14225f6 --- /dev/null +++ b/tests/fixtures/openaps-storage/cgm-loop/monitor/cal-zoned.json @@ -0,0 +1,66 @@ +[ + { + "slope": 841.6474113376482, + "system_time": "2016-10-23T17:47:16-07:00", + "scale": 1.0, + "date": 1477248820000.0, + "decay": 0.0, + "display_time": "2016-10-23T11:53:40-07:00", + "subrecords": [ + { + "applied": "2016-10-20T12:27:28", + "entered": "2016-10-20T12:20:38", + "sensor": 132192, + "meter": 148 + }, + { + "applied": "2016-10-21T12:02:23", + "entered": "2016-10-21T11:55:18", + "sensor": 98880, + "meter": 106 + }, + { + "applied": "2016-10-22T00:22:21", + "entered": "2016-10-22T00:15:33", + "sensor": 129344, + "meter": 144 + }, + { + "applied": "2016-10-22T08:52:19", + "entered": "2016-10-22T08:48:23", + "sensor": 73504, + "meter": 80 + }, + { + "applied": "2016-10-23T07:32:15", + "entered": "2016-10-23T07:29:48", + "sensor": 227520, + "meter": 259 + }, + { + "applied": "2016-10-23T13:47:13", + "entered": "2016-10-23T13:40:06", + "sensor": 92976, + "meter": 94 + }, + { + "applied": "2016-10-23T17:47:12", + "entered": "2016-10-23T17:42:48", + "sensor": 116704, + "meter": 136 + }, + { + "applied": "2016-10-23T17:47:12", + "entered": "2016-10-23T17:43:16", + "sensor": 116704, + "meter": 136 + } + ], + "dateString": "2016-10-23T11:53:40-07:00", + "numsub": 8, + "raw": "24f0b00e449db00e12d2fee52d4d8a4090532cf8dc0bbd40000000000000f03f03060300000000000000000816afac0e9400000060040200b0b0ac0e00a6faad0e6a000000408201004ffcad0e0025a8ae0e9000000040f90100bda9ae0e005720af0e50000000201f01004321af0e006c5fb00e03010000c0780300ff5fb00e0036b6b00e5e000000306b0100e1b7b00e0018efb00e88000000e0c7010020f0b00e0034efb00e88000000e0c7010020f0b00e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000333333333333d33f555555555555e53f3cbe", + "intercept": 7435.863161821748, + "device": "openaps://cgm", + "type": "cal" + } +] \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/cgm-loop/monitor/glucose-zoned-merge.json b/tests/fixtures/openaps-storage/cgm-loop/monitor/glucose-zoned-merge.json new file mode 100644 index 00000000000..7734b30dd7a --- /dev/null +++ b/tests/fixtures/openaps-storage/cgm-loop/monitor/glucose-zoned-merge.json @@ -0,0 +1,60 @@ +[ + { + "trend_arrow": "FLAT", + "display_time": "2016-10-23T11:33:36-07:00", + "noise": 1, + "system_time": "2016-10-23T17:27:13-07:00", + "direction": "Flat", + "sgv": 102, + "dateString": "2016-10-23T11:33:36-07:00", + "date": 1477247616000, + "device": "openaps://indy-e1/cgm", + "type": "sgv", + "glucose": 102 + }, + { + "trend_arrow": "FLAT", + "display_time": "2016-10-23T11:28:36-07:00", + "noise": 1, + "system_time": "2016-10-23T17:22:13-07:00", + "direction": "Flat", + "sgv": 98, + "dateString": "2016-10-23T11:28:36-07:00", + "device": "openaps://indy-e1/cgm", + "date": 1477247316000, + "type": "sgv", + "glucose": 98 + }, + { + "trend_arrow": "FLAT", + "display_time": "2016-10-23T11:23:37-07:00", + "noise": 1, + "system_time": "2016-10-23T17:17:13-07:00", + "direction": "Flat", + "sgv": 91, + "dateString": "2016-10-23T11:23:37-07:00", + "date": 1477247017000, + "unfiltered": 90928, + "filtered": 86880, + "device": "openaps://indy-e1/cgm", + "rssi": 196, + "type": "sgv", + "glucose": 91 + }, + { + "trend_arrow": "FLAT", + "display_time": "2016-10-23T11:18:37-07:00", + "noise": 1, + "system_time": "2016-10-23T17:12:13-07:00", + "direction": "Flat", + "sgv": 87, + "dateString": "2016-10-23T11:18:37-07:00", + "device": "openaps://indy-e1/cgm", + "unfiltered": 87760, + "rssi": 193, + "date": 1477246717000, + "filtered": 85264, + "type": "sgv", + "glucose": 87 + } +] \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/config.json b/tests/fixtures/openaps-storage/config.json new file mode 100644 index 00000000000..bec353ed5af --- /dev/null +++ b/tests/fixtures/openaps-storage/config.json @@ -0,0 +1,19 @@ +{ + "collections": { + "entries": { + "input": [ + "../../tests/fixtures/openaps-storage/cgm-loop/monitor/glucose-zoned-merge.json" + , "../../tests/fixtures/openaps-storage/cgm-loop/monitor/cal-zoned.json" + ] + } + , "treatments": { + "input": "../../tests/fixtures/openaps-storage/loop/nightscout/pump-history-formatted.json" + } + , "devicestatus": { + "input": "../../tests/fixtures/openaps-storage/loop/monitor/openaps-status.json" + } + , "profile": { + "input": "../../tests/fixtures/openaps-storage/ns-profile.json" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/loop/monitor/openaps-status.json b/tests/fixtures/openaps-storage/loop/monitor/openaps-status.json new file mode 100644 index 00000000000..54f56134a64 --- /dev/null +++ b/tests/fixtures/openaps-storage/loop/monitor/openaps-status.json @@ -0,0 +1,219 @@ +[ + { + "device": "openaps://indy-e1", + "pump": { + "battery": { + "status": "normal", + "voltage": 1.42 + }, + "status": { + "status": "normal", + "timestamp": "2016-10-23T18:36:13.000Z", + "bolusing": false, + "suspended": false + }, + "reservoir": 124.425, + "clock": "2016-10-23T11:36:28-07:00" + }, + "uploader": { + "battery": 84, + "batteryVoltage": 4017 + }, + "openaps": { + "suggested": { + "bg": 102, + "temp": "absolute", + "snoozeBG": 125, + "timestamp": "2016-10-23T18:38:02.000Z", + "reason": "COB: 16, Dev: 27, BGI: -1.71, ISF: 83, Target: 80; Eventual BG 82 >= 80, 25m@1.050 = 0.437 > req 0.02+20%. Setting temp basal of 0.7U/hr", + "rate": 0.7, + "COB": 16, + "eventualBG": 82, + "duration": 30, + "tick": "+4", + "IOB": 0.569 + }, + "iob": { + "netbasalinsulin": 0.35, + "timestamp": "2016-10-23T18:36:28.000Z", + "activity": 0.0041, + "basaliob": 0.075, + "hightempinsulin": 0.45, + "bolussnooze": 0.341, + "iob": 0.569 + }, + "enacted": { + "bg": 102, + "temp": "absolute", + "snoozeBG": 125, + "recieved": true, + "predBGs": { + "COB": [ + 102, + 105, + 107, + 109, + 111, + 113, + 114, + 115, + 115, + 115, + 114, + 113, + 113, + 111, + 110, + 109, + 107, + 105, + 103, + 102, + 100, + 98, + 96, + 94, + 93, + 91, + 90, + 88, + 87, + 86, + 85, + 84, + 83, + 82, + 82, + 81, + 81, + 81, + 81, + 80 + ], + "aCOB": [ + 102, + 110, + 116, + 122, + 127, + 131, + 134, + 136, + 137, + 137, + 136, + 134, + 132, + 128, + 125, + 121, + 117, + 114, + 110, + 106, + 103, + 100, + 97, + 94, + 92, + 90, + 88, + 86, + 84, + 83, + 82, + 81, + 80, + 80, + 79, + 79, + 78 + ], + "IOB": [ + 102, + 105, + 107, + 108, + 109, + 109, + 109, + 108, + 107, + 104, + 102, + 98, + 95, + 91, + 88, + 84, + 80, + 76, + 73, + 69, + 66, + 63, + 60, + 57, + 55, + 53, + 51, + 49, + 47, + 46, + 45, + 44, + 43, + 42, + 42, + 41 + ] + }, + "rate": 0.675, + "reason": "COB: 16, Dev: 27, BGI: -1.71, ISF: 83, Target: 80; Eventual BG 82 >= 80, 25m@1.050 = 0.437 > req 0.02+20%. Setting temp basal of 0.7U/hr", + "COB": 16, + "eventualBG": 82, + "timestamp": "2016-10-23T18:38:23.000Z", + "duration": 30, + "tick": "+4", + "IOB": 0.569 + } + }, + "mmtune": { + "scanDetails": [ + [ + "916.564", + 5, + -94 + ], + [ + "916.588", + 5, + -92 + ], + [ + "916.612", + 5, + -90 + ], + [ + "916.636", + 5, + -90 + ], + [ + "916.660", + 5, + -90 + ], + [ + "916.684", + 5, + -91 + ] + ], + "setFreq": 916.66, + "timestamp": "2016-10-23T18:35:52.000Z", + "usedDefault": false + } + } +] \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/loop/nightscout/pump-history-formatted.json b/tests/fixtures/openaps-storage/loop/nightscout/pump-history-formatted.json new file mode 100644 index 00000000000..48c7f543595 --- /dev/null +++ b/tests/fixtures/openaps-storage/loop/nightscout/pump-history-formatted.json @@ -0,0 +1,60 @@ +[ + { + "raw_rate": { + "_type": "TempBasal", + "temp": "absolute", + "_description": "TempBasal 2016-10-23T11:45:22 head[2], body[1] op[0x33]", + "timestamp": "2016-10-23T11:45:22-07:00", + "_body": "00", + "_head": "3339", + "rate": 1.425, + "_date": "96ad0b5710" + }, + "raw_duration": { + "_type": "TempBasalDuration", + "_description": "TempBasalDuration 2016-10-23T11:45:22 head[2], body[0] op[0x16]", + "timestamp": "2016-10-23T11:45:22-07:00", + "_body": "", + "_head": "1601", + "duration (min)": 30, + "_date": "96ad0b5710" + }, + "created_at": "2016-10-23T11:45:22-07:00", + "enteredBy": "openaps://medtronic/723", + "rate": 1.425, + "eventType": "Temp Basal", + "timestamp": "2016-10-23T11:45:22-07:00", + "duration": "30", + "medtronic": "mm://openaps/mm-format-ns-treatments/Temp Basal", + "absolute": "1.425" + }, + { + "raw_rate": { + "_type": "TempBasal", + "temp": "absolute", + "_description": "TempBasal 2016-10-23T11:38:50 head[2], body[1] op[0x33]", + "timestamp": "2016-10-23T11:38:50-07:00", + "_body": "00", + "_head": "331b", + "rate": 0.675, + "_date": "b2a60b5710" + }, + "raw_duration": { + "_type": "TempBasalDuration", + "_description": "TempBasalDuration 2016-10-23T11:38:50 head[2], body[0] op[0x16]", + "timestamp": "2016-10-23T11:38:50-07:00", + "_body": "", + "_head": "1601", + "duration (min)": 30, + "_date": "b2a60b5710" + }, + "created_at": "2016-10-23T11:38:50-07:00", + "enteredBy": "openaps://medtronic/723", + "rate": 0.675, + "eventType": "Temp Basal", + "timestamp": "2016-10-23T11:38:50-07:00", + "duration": "30", + "medtronic": "mm://openaps/mm-format-ns-treatments/Temp Basal", + "absolute": "0.675" + } +] \ No newline at end of file diff --git a/tests/fixtures/openaps-storage/ns-profile.json b/tests/fixtures/openaps-storage/ns-profile.json new file mode 100644 index 00000000000..3cfae033f48 --- /dev/null +++ b/tests/fixtures/openaps-storage/ns-profile.json @@ -0,0 +1,119 @@ +[ + { + "_id": "5428ec28b289cbc5f9b7898b", + "defaultProfile": "Default", + "store": { + "Default": { + "dia": "3", + "carbratio": [ + { + "time": "00:00", + "value": "18", + "timeAsSeconds": "0" + }, + { + "time": "06:00", + "value": "12", + "timeAsSeconds": "21600" + }, + { + "time": "10:30", + "value": "20", + "timeAsSeconds": "37800" + }, + { + "time": "12:00", + "value": "20", + "timeAsSeconds": "43200" + }, + { + "time": "17:00", + "value": "18", + "timeAsSeconds": "61200" + } + ], + "carbs_hr": "30", + "delay": "20", + "sens": [ + { + "time": "00:00", + "value": "90", + "timeAsSeconds": "0" + } + ], + "timezone": "America/Los_Angeles", + "basal": [ + { + "time": "00:00", + "value": "0.6", + "timeAsSeconds": "0" + }, + { + "time": "02:30", + "value": "0.6", + "timeAsSeconds": "9000" + }, + { + "time": "03:30", + "value": "0.525", + "timeAsSeconds": "12600" + }, + { + "time": "04:00", + "value": "0.45", + "timeAsSeconds": "14400" + }, + { + "time": "07:00", + "value": "0.55", + "timeAsSeconds": "25200" + }, + { + "time": "11:00", + "value": "0.6", + "timeAsSeconds": "39600" + }, + { + "time": "14:00", + "value": "0.7", + "timeAsSeconds": "50400" + }, + { + "time": "18:30", + "value": "0.6", + "timeAsSeconds": "66600" + }, + { + "time": "19:30", + "value": "0.75", + "timeAsSeconds": "70200" + }, + { + "time": "22:00", + "value": "0.825", + "timeAsSeconds": "79200" + } + ], + "target_low": [ + { + "time": "00:00", + "value": "115", + "timeAsSeconds": "0" + } + ], + "target_high": [ + { + "time": "00:00", + "value": "115", + "timeAsSeconds": "0" + } + ], + "startDate": "1970-01-01T00:00:00.000Z", + "units": "mg/dl" + } + }, + "startDate": "2015-08-17T04:20:00.000Z", + "created_at": "2015-11-08T16:59:03.920Z", + "mills": "1439785200000" + } +] \ No newline at end of file diff --git a/tests/hashauth.test.js b/tests/hashauth.test.js index eea93968da9..287b2c45e70 100644 --- a/tests/hashauth.test.js +++ b/tests/hashauth.test.js @@ -6,13 +6,36 @@ var read = require('fs').readFileSync; var serverSettings = require('./fixtures/default-server-settings'); describe('hashauth', function ( ) { + this.timeout(50000); // TODO: see why this test takes longer on Travis to complete + var self = this; + var headless = require('./fixtures/headless')(benv, this); + + before(function (done) { + done( ); + }); + + after(function (done) { + // cleanup js-storage as it evaluates if the test is running in the window or not when first required + delete require.cache[require.resolve('js-storage')]; + done( ); + }); + + beforeEach(function (done) { + headless.setup({mockAjax: true}, done); + }); + + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + /* before(function (done) { benv.setup(function() { self.$ = require('jquery'); self.$.localStorage = require('./fixtures/localstorage'); - self.$.fn.tipsy = function mockTipsy ( ) { }; + self.$.fn.tooltip = function mockTooltip ( ) { }; var indexHtml = read(__dirname + '/../static/index.html', 'utf8'); self.$('body').html(indexHtml); @@ -41,11 +64,11 @@ describe('hashauth', function ( ) { benv.teardown(); done(); }); + */ it ('should make module unauthorized', function () { - var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { @@ -53,18 +76,17 @@ describe('hashauth', function ( ) { next(true); }; - client.init(serverSettings, plugins); + client.init(); - hashauth.inlineCode().indexOf('Device not authenticated').should.be.greaterThan(0); + hashauth.inlineCode().indexOf('Unauthorized').should.be.greaterThan(0); hashauth.isAuthenticated().should.equal(false); var testnull = (hashauth.hash()===null); testnull.should.equal(true); }); it ('should make module authorized', function () { - var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { @@ -72,16 +94,15 @@ describe('hashauth', function ( ) { next(true); }; - client.init(serverSettings, plugins); + client.init(); - hashauth.inlineCode().indexOf('Device authenticated').should.be.greaterThan(0); + hashauth.inlineCode().indexOf('Admin authorized').should.be.greaterThan(0); hashauth.isAuthenticated().should.equal(true); }); it ('should store hash and the remove authentication', function () { - var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); var localStorage = require('./fixtures/localstorage'); localStorage.remove('apisecrethash'); @@ -91,8 +112,9 @@ describe('hashauth', function ( ) { hashauth.authenticated = true; next(true); }; + hashauth.updateSocketAuth = function mockUpdateSocketAuth() {}; - client.init(serverSettings, plugins); + client.init(); hashauth.processSecret('this is my long pass phrase',true); @@ -105,9 +127,8 @@ describe('hashauth', function ( ) { }); it ('should not store hash', function () { - var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); var localStorage = require('./fixtures/localstorage'); localStorage.remove('apisecrethash'); @@ -118,7 +139,7 @@ describe('hashauth', function ( ) { next(true); }; - client.init(serverSettings, plugins); + client.init(); hashauth.processSecret('this is my long pass phrase',false); @@ -129,16 +150,15 @@ describe('hashauth', function ( ) { }); it ('should report secret too short', function () { - var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); var localStorage = require('./fixtures/localstorage'); localStorage.remove('apisecrethash'); - hashauth.init(client,$); + hashauth.init(client, self.$); - client.init(serverSettings, plugins); + client.init(); window.alert = function mockConfirm (message) { function containsLine (line) { diff --git a/tests/hooks.js b/tests/hooks.js new file mode 100644 index 00000000000..7f847f778e5 --- /dev/null +++ b/tests/hooks.js @@ -0,0 +1,14 @@ +'use strict;' + +function clearRequireCache () { + Object.keys(require.cache).forEach(function(key) { + delete require.cache[key]; + }); +} + +exports.mochaHooks = { + afterEach (done) { + clearRequireCache(); + done(); + } +}; diff --git a/tests/inithelper.js b/tests/inithelper.js new file mode 100644 index 00000000000..c4de997c5fc --- /dev/null +++ b/tests/inithelper.js @@ -0,0 +1,24 @@ + +const fs = require('fs'); +const moment = require('moment-timezone'); +const language = require('../lib/language')(fs); +const settings = require('../lib/settings')(); +const levels = require('../lib/levels'); + +function helper() { + + helper.ctx = { + language: language + , settings: settings + , levels: levels + , moment: moment + }; + + helper.getctx = function getctx () { + return helper.ctx; + } + + return helper; +} + +module.exports = helper; diff --git a/tests/insulinage.test.js b/tests/insulinage.test.js new file mode 100644 index 00000000000..8fcb7e484b6 --- /dev/null +++ b/tests/insulinage.test.js @@ -0,0 +1,96 @@ +'use strict'; + +require('should'); +const helper = require('./inithelper')(); +const levels = helper.ctx.levels; + +describe('insulinage', function ( ) { + var env = require('../lib/server/env')(); + var ctx = helper.getctx(); + ctx.ddata = require('../lib/data/ddata')(); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var iage = require('../lib/plugins/insulinage')(ctx); + var sandbox = require('../lib/sandbox')(ctx); + function prepareSandbox ( ) { + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + sbx.offerProperty('iob', function () { + return {iob: 0}; + }); + return sbx; + } + + it('set a pill to the current insulin age', function (done) { + + var data = { + insulinchangeTreatments: [ + {eventType: 'Insulin Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} + , {eventType: 'Insulin Change', notes: 'Bar', mills: Date.now() - 24 * 60 * 60000} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('1d0h'); + options.info[1].value.should.equal('Bar'); + done(); + } + } + }; + ctx.language = require('../lib/language')(); + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + iage.setProperties(sbx); + iage.updateVisualisation(sbx); + + }); + + it('set a pill to the current insulin age', function (done) { + + var data = { + insulinchangeTreatments: [ + {eventType: 'Insulin Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} + , {eventType: 'Insulin Change', notes: '', mills: Date.now() - 59 * 60000} + ] + }; + + var ctx = { + settings: {} + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.value.should.equal('0h'); + options.info.length.should.equal(1); + done(); + } + } + }; + ctx.language = require('../lib/language')(); + + var sbx = sandbox.clientInit(ctx, Date.now(), data); + iage.setProperties(sbx); + iage.updateVisualisation(sbx); + + }); + + + it('trigger a warning when insulin is 48 hours old', function (done) { + ctx.notifications.initRequests(); + + var before = Date.now() - (48 * 60 * 60 * 1000); + + ctx.ddata.insulinchangeTreatments = [{eventType: 'Insulin Change', mills: before}]; + + var sbx = prepareSandbox(); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + iage.setProperties(sbx); + iage.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('IAGE'); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Insulin reservoir age 48 hours'); + done(); + }); + +}); diff --git a/tests/iob.test.js b/tests/iob.test.js index 0c65e17c6e3..705c9efc992 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.js @@ -1,104 +1,270 @@ 'use strict'; -require('should'); +const _ = require('lodash'); +const should = require('should'); +const helper = require('./inithelper')(); -describe('IOB', function ( ) { - var iob = require('../lib/plugins/iob')(); +describe('IOB', function() { + let ctx = helper.ctx; - it('should calculate IOB', function() { + ctx.settings = require('../lib/settings')(); - var time = Date.now() - , treatments = [ { - mills: time - 1, - insulin: '1.00' - } - ]; - - - var profileData = { - dia: 3, - sens: 0}; - - var profile = require('../lib/profilefunctions')([profileData]); + var iob = require('../lib/plugins/iob')(ctx); - var rightAfterBolus = iob.calcTotal(treatments, profile, time); + it('should handle virtAsst requests', function (done) { - rightAfterBolus.display.should.equal('1.00'); + var sbx = { + properties: { + iob: { + iob: 1.5 + } + } + }; - var afterSomeTime = iob.calcTotal(treatments, profile, time + (60 * 60 * 1000)); + iob.virtAsst.intentHandlers.length.should.equal(1); + iob.virtAsst.rollupHandlers.length.should.equal(1); - afterSomeTime.iob.should.be.lessThan(1); - afterSomeTime.iob.should.be.greaterThan(0); + iob.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Current IOB'); + response.should.equal('You have 1.50 units of insulin on board'); - var afterDIA = iob.calcTotal(treatments, profile, time + (3 * 60 * 60 * 1000)); + iob.virtAsst.rollupHandlers[0].rollupHandler([], sbx, function callback (err, response) { + should.not.exist(err); + response.results.should.equal('and you have 1.50 units of insulin on board.'); + response.priority.should.equal(2); + done(); + }); - afterDIA.iob.should.equal(0); + }, [], sbx); }); - it('should calculate IOB using defaults', function() { + describe('from treatments', function ( ) { - var treatments = [{ - mills: Date.now() - 1, - insulin: '1.00' - }]; + it('should calculate IOB', function() { - var rightAfterBolus = iob.calcTotal(treatments); + var time = Date.now() + , treatments = [ { + mills: time - 1, + insulin: '1.00' + } + ]; + + + var profileData = { + dia: 3, + sens: 0}; - rightAfterBolus.display.should.equal('1.00'); + var profile = require('../lib/profilefunctions')([profileData], ctx); - }); + var rightAfterBolus = iob.calcTotal(treatments, [], profile, time); - it('should not show a negative IOB when approaching 0', function() { + rightAfterBolus.display.should.equal('1.00'); - var time = Date.now() - 1; + var afterSomeTime = iob.calcTotal(treatments, [], profile, time + (60 * 60 * 1000)); - var treatments = [{ - mills: time, - insulin: '5.00' - }]; + afterSomeTime.iob.should.be.lessThan(1); + afterSomeTime.iob.should.be.greaterThan(0); - var whenApproaching0 = iob.calcTotal(treatments, undefined, time + (3 * 60 * 60 * 1000) - (90 * 1000)); + var afterDIA = iob.calcTotal(treatments, [], profile, time + (3 * 60 * 60 * 1000)); - //before fix we got this: AssertionError: expected '-0.00' to be '0.00' - whenApproaching0.display.should.equal('0.00'); + afterDIA.iob.should.equal(0); - }); + }); - it('should calculate IOB using a 4 hour duration', function() { + it('should calculate IOB using defaults', function() { - var time = Date.now() - , treatments = [ { - mills: time - 1, + var treatments = [{ + mills: Date.now() - 1, insulin: '1.00' - } ]; - - var profileData = { - dia: 4, - sens: 0}; + }]; + + var rightAfterBolus = iob.calcTotal(treatments, []); + + rightAfterBolus.display.should.equal('1.00'); + + }); + + it('should not show a negative IOB when approaching 0', function() { + + var time = Date.now() - 1; - var profile = require('../lib/profilefunctions')([profileData]); + var treatments = [{ + mills: time, + insulin: '5.00' + }]; - - var rightAfterBolus = iob.calcTotal(treatments, profile, time); + var whenApproaching0 = iob.calcTotal(treatments, [], undefined, time + (3 * 60 * 60 * 1000) - (90 * 1000)); - rightAfterBolus.display.should.equal('1.00'); + //before fix we got this: AssertionError: expected '-0.00' to be '0.00' + whenApproaching0.display.should.equal('0.00'); - var afterSomeTime = iob.calcTotal(treatments, profile, time + (60 * 60 * 1000)); + }); - afterSomeTime.iob.should.be.lessThan(1); - afterSomeTime.iob.should.be.greaterThan(0); + it('should calculate IOB using a 4 hour duration', function() { - var after3hDIA = iob.calcTotal(treatments, profile, time + (3 * 60 * 60 * 1000)); + var time = Date.now() + , treatments = [ { + mills: time - 1, + insulin: '1.00' + } ]; + + var profileData = { + dia: 4, + sens: 0}; + + var profile = require('../lib/profilefunctions')([profileData], ctx); + + var rightAfterBolus = iob.calcTotal(treatments, [], profile, time); + + rightAfterBolus.display.should.equal('1.00'); + + var afterSomeTime = iob.calcTotal(treatments, [], profile, time + (60 * 60 * 1000)); + + afterSomeTime.iob.should.be.lessThan(1); + afterSomeTime.iob.should.be.greaterThan(0); + + var after3hDIA = iob.calcTotal(treatments, [], profile, time + (3 * 60 * 60 * 1000)); + + after3hDIA.iob.should.greaterThan(0); - after3hDIA.iob.should.greaterThan(0); + var after4hDIA = iob.calcTotal(treatments, [], profile, time + (4 * 60 * 60 * 1000)); - var after4hDIA = iob.calcTotal(treatments, profile, time + (4 * 60 * 60 * 1000)); + after4hDIA.iob.should.equal(0); + + }); - after4hDIA.iob.should.equal(0); }); + describe('from devicestatus', function () { + var time = Date.now(); + var profile = require('../lib/profilefunctions')([{ dia: 3, sens: 0 }], ctx); + var treatments = [{ + mills: time - 1, + insulin: '3.00' + }]; + var treatmentIOB = iob.fromTreatments(treatments, profile, time).iob; + + var OPENAPS_DEVICESTATUS = { + device: 'openaps://pi1', + openaps: { + iob: { + iob: 0.047, + basaliob: -0.298, + activity: 0.0147 + } + } + }; + + it('should fall back to treatment data if no devicestatus data', function() { + iob.calcTotal(treatments, [], profile, time).should.containEql({ + source: 'Care Portal', + iob: treatmentIOB + }); + }); + + it('should fall back to treatments if openaps devicestatus is present but empty', function() { + var devicestatus = [{ + device: 'openaps://pi1', + mills: time - 1, + openaps: {} + }]; + iob.calcTotal(treatments, devicestatus, profile, time).iob.should.equal(treatmentIOB); + }); + + it('should fall back to treatments if openaps devicestatus is present but too stale', function() { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - iob.RECENCY_THRESHOLD - 1, openaps: {iob: {timestamp: time - iob.RECENCY_THRESHOLD - 1} } })]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + source: 'Care Portal', + iob: treatmentIOB + }); + }); + + it('should return IOB data from openaps', function () { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {iob: {timestamp: time - 1} } })]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.047, + basaliob: -0.298, + activity: 0.0147, + source: 'OpenAPS', + device: 'openaps://pi1' + }); + }); + + it('should not blow up with null IOB data from openaps', function () { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {iob: null } })]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + source: 'Care Portal', + display: '3.00' + }); + }); + + it('should return IOB data from openaps post AMA (an array)', function () { + var devicestatus = [_.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {iob: [{ + iob: 0.047, + basaliob: -0.298, + activity: 0.0147, + time: time - 1 + }]}})]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.047, + basaliob: -0.298, + activity: 0.0147, + source: 'OpenAPS', + device: 'openaps://pi1' + }); + }); + + it('should return IOB data from Loop', function () { + + var LOOP_DEVICESTATUS = { + device: 'loop://iPhone', + loop: { + iob: { + iob: 0.75 + } + } + }; + + var devicestatus = [_.merge(LOOP_DEVICESTATUS, { mills: time - 1, loop: {iob: {timestamp: time - 1} } })]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.75, + source: 'Loop', + device: 'loop://iPhone' + }); + }); + + it('should return IOB data from openaps from multiple devices', function () { + var devicestatus = [ + _.merge(OPENAPS_DEVICESTATUS, { mills: time - 1000, openaps: {iob: {timestamp: time - 1000} } }) + , _.merge(OPENAPS_DEVICESTATUS, { mills: time - 1, openaps: {iob: {timestamp: time - 1} } }) + , _.merge(OPENAPS_DEVICESTATUS, { mills: time - 20000, openaps: {iob: {timestamp: time - 20000} } }) + ]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.047, + basaliob: -0.298, + activity: 0.0147, + source: 'OpenAPS', + device: 'openaps://pi1' + }); + }); + + it('should return IOB data from MiniMed Connect', function () { + var devicestatus = [{ + device: 'connect://paradigm', + mills: time - 1, + pump: { iob: { bolusiob: 0.87 } }, + connect: { sensorState: 'copacetic' } + }]; + iob.calcTotal(treatments, devicestatus, profile, time).should.containEql({ + iob: 0.87, + source: 'MM Connect', + device: 'connect://paradigm' + }); + }); + + }); -}); \ No newline at end of file +}); diff --git a/tests/language.test.js b/tests/language.test.js index 81c457f2ba9..0de87999b4a 100644 --- a/tests/language.test.js +++ b/tests/language.test.js @@ -1,5 +1,7 @@ 'use strict'; +const fs = require('fs'); + require('should'); describe('language', function ( ) { @@ -9,16 +11,33 @@ describe('language', function ( ) { language.translate('Carbs').should.equal('Carbs'); }); + it('replace strings in translations', function () { + var language = require('../lib/language')(); + language.translate('%1 records deleted', '1').should.equal('1 records deleted'); + language.translate('%1 records deleted', 1).should.equal('1 records deleted'); + language.translate('%1 records deleted', {params: ['1']}).should.equal('1 records deleted'); + language.translate('Sensor age %1 days %2 hours', '1', '2').should.equal('Sensor age 1 days 2 hours'); + }); + it('translate to French', function () { var language = require('../lib/language')(); language.set('fr'); + language.loadLocalization(fs); language.translate('Carbs').should.equal('Glucides'); }); it('translate to Czech', function () { var language = require('../lib/language')(); language.set('cs'); + language.loadLocalization(fs); language.translate('Carbs').should.equal('Sacharidy'); }); + it('translate to Czech uppercase', function () { + var language = require('../lib/language')(); + language.set('cs'); + language.loadLocalization(fs); + language.translate('carbs', { ci: true }).should.equal('Sacharidy'); + }); + }); diff --git a/tests/loop.test.js b/tests/loop.test.js new file mode 100644 index 00000000000..51d1c39338d --- /dev/null +++ b/tests/loop.test.js @@ -0,0 +1,273 @@ +'use strict'; + +const _ = require('lodash'); +const should = require('should'); +const helper = require('./inithelper')(); + +var ctx_top = helper.getctx(); +ctx_top.language.set('en'); +const language = ctx_top.language; + +var env = require('../lib/server/env')(); +var loop = require('../lib/plugins/loop')(ctx_top); +var sandbox = require('../lib/sandbox')(ctx_top); + +var statuses = [ + { + 'created_at':'2016-08-13T20:09:15Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'enacted':{ + 'timestamp':'2016-08-13T20:09:15Z', + 'rate':0.875, + 'duration':30, + 'received':true + }, + 'version':'0.9.1', + 'recommendedBolus':0, + 'timestamp':'2016-08-13T20:09:15Z', + 'predicted':{ + 'startDate':'2016-08-13T20:03:47Z', + 'values':[ + 149, + 149, + 148, + 148, + 147, + 147 + ] + }, + 'iob':{ + 'timestamp':'2016-08-13T20:05:00Z', + 'iob':0.1733152537837709 + }, + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T20:04:15Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'version':'0.9.1', + 'recommendedBolus':0, + 'timestamp':'2016-08-13T20:04:15Z', + 'failureReason':'SomeError', + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:20Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'timestamp':'2016-08-13T01:18:20Z', + 'version':'0.9.1', + 'iob':{ + 'timestamp':'2016-08-13T01:15:00Z', + 'iob':-0.1205140849137931 + }, + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:20Z', + 'device':'loop://ExamplePhone', + 'loop':{ + 'timestamp':'2016-08-13T01:13:20Z', + 'version':'0.9.1', + 'iob':{ + 'timestamp':'2016-08-13T01:10:00Z', + 'iob':-0.1205140849137931 + }, + 'failureReason':'StaleDataError(\"Glucose Date: 2016-08-12 23:23:49 +0000 or Pump status date: 2016-08-13 01:13:10 +0000 older than 15.0 min\")', + 'name':'Loop' + } + }, + { + 'created_at':'2016-08-13T01:13:15Z', + 'pump':{ + 'reservoir':90.5, + 'clock':'2016-08-13T01:13:10Z', + 'battery':{ + 'status':'normal', + 'voltage':1.5 + }, + 'pumpID':'543204' + }, + 'device':'loop://ExamplePhone', + 'uploader':{ + 'timestamp':'2016-08-13T01:13:15Z', + 'battery':43, + 'name':'ExamplePhone' + } + } +]; + +var now = ctx_top.moment(statuses[0].created_at); + +_.forEach(statuses, function updateMills (status) { + status.mills = ctx_top.moment(status.created_at).valueOf(); +}); + +describe('loop', function ( ) { + + it('should set the property and update the pill and add forecast points', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('Loop ⌁'); + options.value.should.equal('1m ago ↝ 147'); + var first = _.first(options.info); + first.label.should.equal('1m ago'); + first.value.should.equal('Temp Basal Started 0.88U/hour for 30m, IOB: 0.17U, Predicted Min-Max BG: 147-149, Eventual BG: 147'); + } + , addForecastPoints: function mockAddForecastPoints (points) { + points.length.should.equal(6); + done(); + } + } + , language: language + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + + result.display.symbol.should.equal('⌁'); + result.display.code.should.equal('enacted'); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + }; + + loop.setProperties(sbx); + loop.updateVisualisation(sbx); + }); + + it('should show errors', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('Loop x'); + options.value.should.equal('1m ago'); + var first = _.first(options.info); + first.label.should.equal('1m ago'); + first.value.should.equal('Error: SomeError'); + done(); + } + , language: language + }, + language: language + }; + + var errorTime = ctx_top.moment(statuses[1].created_at); + + var sbx = sandbox.clientInit(ctx, errorTime.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + + result.display.symbol.should.equal('x'); + result.display.code.should.equal('error'); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + }; + + loop.setProperties(sbx); + + loop.updateVisualisation(sbx); + + }); + + + it('should check the recieved flag to see if it was received', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx_top) + , language: language + }; + + ctx.notifications.initRequests(); + + var notStatuses = _.cloneDeep(statuses); + notStatuses[0].loop.enacted.received = false; + var sbx = require('../lib/sandbox')().clientInit(ctx, now, {devicestatus: notStatuses}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('loop'); + var result = setter(); + should.exist(result); + result.display.symbol.should.equal('x'); + result.display.code.should.equal('error'); + done(); + }; + + loop.setProperties(sbx); + }); + + it('should generate an alert for a stuck loop', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx_top) + , language: language + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.clone().add(2, 'hours').valueOf(), {devicestatus: statuses}); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + loop.setProperties(sbx); + loop.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Loop'); + highest.level.should.equal(ctx_top.levels.URGENT); + highest.title.should.equal('Loop isn\'t looping'); + done(); + }); + + it('should handle virtAsst requests', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, ctx_top) + , language: language + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + loop.setProperties(sbx); + + loop.virtAsst.intentHandlers.length.should.equal(2); + + loop.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Loop Forecast'); + response.should.equal('According to the loop forecast you are expected to be between 147 and 149 over the next in 25 minutes'); + + loop.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last Loop'); + response.should.equal('The last successful loop was a few seconds ago'); + done(); + }, [], sbx); + + }, [], sbx); + + }); + +}); diff --git a/tests/maker.test.js b/tests/maker.test.js index a1256421d8f..42bcdfddab8 100644 --- a/tests/maker.test.js +++ b/tests/maker.test.js @@ -2,7 +2,11 @@ var should = require('should'); var levels = require('../lib/levels'); describe('maker', function ( ) { - var maker = require('../lib/plugins/maker')({extendedSettings: {maker: {key: '12345'}}}); + var maker = require('../lib/plugins/maker')( + { + extendedSettings: {maker: {key: '12345'}} + , levels: levels + }); //prevent any calls to iftt function noOpMakeRequest (key, event, eventName, callback) { diff --git a/tests/mmconnect.test.js b/tests/mmconnect.test.js index d92d7f4c425..674ae8cd85c 100644 --- a/tests/mmconnect.test.js +++ b/tests/mmconnect.test.js @@ -57,44 +57,6 @@ describe('mmconnect', function () { }); - describe('makeRecentSgvFilter()', function () { - function sgv(date) { - return {type: 'sgv', date: date}; - } - function pumpStatus(date) { - return {type: 'pump_status', date: date}; - } - - it('should return a stateful filter which discards sgvs older than the most recent one seen', function() { - var filter = mmconnect.makeRecentSgvFilter(); - - filter([2, 3, 4].map(sgv)).length.should.equal(3); - - filter([2, 3, 4].map(sgv)).length.should.equal(0); - - var filtered = filter([1, 2, 3, 4, 5, 6].map(sgv)); - filtered.length.should.equal(2); - _.pluck(filtered, 'date').should.containEql(5); - _.pluck(filtered, 'date').should.containEql(6); - }); - - it('should return a stateful filter which allows non-sgv entries to be old', function() { - var filter = mmconnect.makeRecentSgvFilter(); - - filter([2, 3, 4].map(sgv)).length.should.equal(3); - - filter([1, 2, 3, 4, 5].map(pumpStatus)).length.should.equal(5); - - var filtered = filter( - [1, 2, 3, 4, 5].map(pumpStatus).concat( - [3, 4, 5, 6, 7].map(sgv) - ) - ); - _.filter(filtered, {type: 'pump_status'}).length.should.equal(5); - _.filter(filtered, {type: 'sgv'}).length.should.equal(3); - }); - }); - describe('rawDataEntry()', function () { it('should generate a "carelink_raw" entry with sgs truncated and PII redacted', function () { var data = { diff --git a/tests/mongo-storage.test.js b/tests/mongo-storage.test.js new file mode 100644 index 00000000000..506e54e5441 --- /dev/null +++ b/tests/mongo-storage.test.js @@ -0,0 +1,61 @@ +'use strict'; + +var should = require('should'); +var assert = require('assert'); + +describe('mongo storage', function () { + var env = require('../lib/server/env')(); + + before(function (done) { + delete env.api_secret; + done(); + }); + + it('The module should be OK.', function (done) { + should.exist(require('../lib/storage/mongo-storage')); + done(); + }); + + it('After initializing the storage class it should re-use the open connection', function (done) { + var store = require('../lib/storage/mongo-storage'); + store(env, function (err1, db1) { + should.not.exist(err1); + + store(env, function (err2, db2) { + should.not.exist(err2); + assert(db1.db, db2.db, 'Check if the handlers are the same.'); + + done(); + }); + }); + }); + + it('When no connection-string is given the storage-class should throw an error.', function (done) { + delete env.storageURI; + should.not.exist(env.storageURI); + + (function () { + return require('../lib/storage/mongo-storage')(env, false, true); + }).should.throw('MongoDB connection string is missing. Please set MONGODB_URI environment variable'); + + done(); + }); + + it('An invalid connection-string should throw an error.', function (done) { + env.storageURI = 'This is not a MongoDB connection-string'; + + (async function () { + try { + let foo = await require('../lib/storage/mongo-storage')(env, false, true); + false.should.be.true(); + } + catch (err) { + console.log('We have failed, this is good!'); + done(); + } + })(); + + }); + +}); + diff --git a/tests/mqtt.test.js b/tests/mqtt.test.js deleted file mode 100644 index afbe9205ced..00000000000 --- a/tests/mqtt.test.js +++ /dev/null @@ -1,182 +0,0 @@ -'use strict'; - -var should = require('should'); - -var FIVE_MINS = 5 * 60 * 1000; - -describe('mqtt', function ( ) { - - var self = this; - - before(function () { - process.env.MQTT_MONITOR = 'mqtt://user:password@localhost:12345'; - process.env.MONGO='mongodb://localhost/test_db'; - process.env.MONGO_COLLECTION='test_sgvs'; - self.env = require('../env')(); - self.es = require('event-stream'); - self.results = self.es.through(function (ch) { this.push(ch); }); - function outputs (fn) { - return self.es.writeArray(function (err, results) { - fn(err, results); - self.results.write(err || results); - }); - } - function written (data, fn) { - self.results.write(data); - setTimeout(fn, 5); - } - self.mqtt = require('../lib/mqtt')(self.env, {entries: { persist: outputs, create: written }, devicestatus: { create: written } }); - }); - - after(function () { - delete process.env.MQTT_MONITOR; - }); - - var now = Date.now() - , prev1 = now - FIVE_MINS - , prev2 = prev1 - FIVE_MINS - ; - - it('setup env correctly', function (done) { - self.env.mqtt_client_id.should.equal('fSjoHx8buyCtAc474tg8Dt3'); - done(); - }); - - it('handle a download with only sgvs', function (done) { - var packet = { - sgv: [ - {sgv_mgdl: 110, trend: 4, date: prev2} - , {sgv_mgdl: 105, trend: 4, date: prev1} - , {sgv_mgdl: 100, trend: 4, date: now} - ] - }; - - var merged = self.mqtt.sgvSensorMerge(packet); - - merged.length.should.equal(packet.sgv.length); - - done(); - - }); - - it('merge sgvs and sensor records that match up', function (done) { - var packet = { - sgv: [ - {sgv_mgdl: 110, trend: 4, date: prev2} - , {sgv_mgdl: 105, trend: 4, date: prev1} - , {sgv_mgdl: 100, trend: 4, date: now} - ] - , sensor: [ - {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev2} - , {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev1} - , {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} - ] - }; - - var merged = self.mqtt.sgvSensorMerge(packet); - - merged.length.should.equal(packet.sgv.length); - - merged.filter(function (sgv) { - return sgv.filtered && sgv.unfiltered && sgv.rssi; - }).length.should.equal(packet.sgv.length); - - done(); - - }); - - it('downloadProtobuf should dispatch', function (done) { - - var payload = new Buffer('0a1108b70110d6d1fa6318f08df963200428011a1d323031352d30382d32335432323a35333a35352e3634392d30373a303020d7d1fa6328004a1508e0920b10c0850b18b20120d5d1fa6328ef8df963620a534d34313837393135306a053638393250', 'hex'); - - // var payload = self.mqtt.downloads.format(packet); - console.log('yaploda', '/downloads/protobuf', payload); - var l = [ ]; - self.results.on('data', function (chunk) { - l.push(chunk); - console.log('test data', l.length, chunk.length, chunk); - switch (l.length) { - case 0: // devicestatus - break; - case 2: // sgv - break; - case 3: // sgv - chunk.length.should.equal(1); - var first = chunk[0]; - should.exist(first.sgv); - should.exist(first.noise); - should.exist(first.date); - should.exist(first.dateString); - first.type.should.equal('sgv'); - break; - case 4: // cal - break; - case 1: // meter - break; - default: - break; - } - if (l.length >= 5) { - self.results.end( ); - } - }); - self.results.on('end', function ( ) { - done( ); - }); - self.mqtt.client.emit('message', '/downloads/protobuf', payload); - }); - - it('merge sgvs and sensor records that match up, and get the sgvs that don\'t match', function (done) { - var packet = { - sgv: [ - {sgv_mgdl: 110, trend: 4, date: prev2} - , {sgv_mgdl: 105, trend: 4, date: prev1} - , {sgv_mgdl: 100, trend: 4, date: now} - ] - , sensor: [ - {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} - ] - }; - - var merged = self.mqtt.sgvSensorMerge(packet); - - merged.length.should.equal(packet.sgv.length); - - var withBoth = merged.filter(function (sgv) { - return sgv.sgv && sgv.filtered && sgv.unfiltered && sgv.rssi; - }); - - withBoth.length.should.equal(1); - - done(); - - }); - - it('merge sgvs and sensor records that match up, and get the sensors that don\'t match', function (done) { - var packet = { - sgv: [ - {sgv_mgdl: 100, trend: 4, date: now} - ] - , sensor: [ - {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev2} - , {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev1} - , {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} - ] - }; - - var merged = self.mqtt.sgvSensorMerge(packet); - - merged.length.should.equal(packet.sensor.length); - - var withBoth = merged.filter(function (sgv) { - return sgv.sgv && sgv.filtered && sgv.unfiltered && sgv.rssi; - }); - - withBoth.length.should.equal(1); - - done(); - - }); - - -}); diff --git a/tests/notifications-api.test.js b/tests/notifications-api.test.js index 8ef4f31e1ea..a534f1271fa 100644 --- a/tests/notifications-api.test.js +++ b/tests/notifications-api.test.js @@ -16,17 +16,25 @@ describe('Notifications API', function ( ) { var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; delete process.env.API_SECRET; process.env.API_SECRET = 'this is my long pass phrase'; - var env = require('../env')( ); - env.api_secret.should.equal(known); + var env = require('../lib/server/env')( ); + env.enclave.isApiKey(known).should.equal(true); env.testMode = true; var ctx = { bus: new Stream - , data: { + , ddata: { lastUpdated: Date.now() } + , store: { + collection: function ( ) { + return { }; + } + } + , levels: levels }; + ctx.authorization = require('../lib/authorization')(env, ctx); + var notifications = require('../lib/notifications')(env, ctx); ctx.notifications = notifications; @@ -60,7 +68,7 @@ describe('Notifications API', function ( ) { function makeRequest () { request(app) .get('/notifications/ack?level=1') - .set('api-secret', env.api_secret || '') + .set('api-secret', known || '') .expect(200) .end(function (err) { should.not.exist(err); diff --git a/tests/notifications.test.js b/tests/notifications.test.js index 0f0014ef158..054cf4594a7 100644 --- a/tests/notifications.test.js +++ b/tests/notifications.test.js @@ -9,9 +9,10 @@ describe('notifications', function ( ) { var ctx = { bus: new Stream - , data: { + , ddata: { lastUpdated: Date.now() } + , levels: levels }; var notifications = require('../lib/notifications')(env, ctx); diff --git a/tests/openaps-storage.test.js b/tests/openaps-storage.test.js new file mode 100644 index 00000000000..36714181a40 --- /dev/null +++ b/tests/openaps-storage.test.js @@ -0,0 +1,115 @@ +'use strict'; + +var should = require('should'); + +describe('openaps storage', function () { + + var env = require('../lib/server/env')(); + + + before(function (done) { + delete env.api_secret; + env.storageURI = 'openaps://../../tests/fixtures/openaps-storage/config'; + done(); + }); + + it('The module class should be OK.', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + should.exist(storage.ensureIndexes); + done(); + }); + }); + + it('find sgv entries', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + + storage.collection('entries').find({type: 'sgv'}).toArray(function callback (err, results) { + should.not.exist(err); + should.exist(results); + + results.length.should.equal(4); + results[0].sgv.should.equal(102); + + done(); + }); + }); + }); + + it('find cal entries', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + + storage.collection('entries').find({type: 'cal'}).toArray(function callback (err, results) { + should.not.exist(err); + should.exist(results); + + results.length.should.equal(1); + results[0].slope.should.equal(841.6474113376482); + + done(); + }); + }); + }); + + it('find devicestatus entries', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + + storage.collection('devicestatus').find({}).toArray(function callback (err, results) { + should.not.exist(err); + should.exist(results); + + results.length.should.equal(1); + results[0].openaps.enacted.eventualBG.should.equal(82); + + done(); + }); + }); + }); + + it('find treatments', function (done) { + require('../lib/storage/openaps-storage')(env, function callback (err, storage) { + should.not.exist(err); + should.exist(storage.collection); + + storage.collection('treatments').find({}).toArray(function callback (err, results) { + should.not.exist(err); + should.exist(results); + + results.length.should.equal(2); + results[0].eventType.should.equal('Temp Basal'); + + done(); + }); + }); + }); + + it('When no connection-string is given the storage-class should throw an error.', function (done) { + delete env.storageURI; + should.not.exist(env.storageURI); + + (function () { + return require('../lib/storage/openaps-storage')(env); + }).should.throw('openaps config uri is missing or invalid'); + + done(); + }); + + it('An invalid connection-string should throw an error.', function (done) { + env.storageURI = 'This is not an openaps config path'; + + (function () { + return require('../lib/storage/openaps-storage')(env); + }).should.throw(Error); + + done(); + }); + +}); + diff --git a/tests/openaps.test.js b/tests/openaps.test.js new file mode 100644 index 00000000000..ce5d5306fe7 --- /dev/null +++ b/tests/openaps.test.js @@ -0,0 +1,403 @@ +'use strict'; + +const _ = require('lodash'); +const should = require('should'); + +const helper = require('./inithelper')(); + +var top_ctx = helper.getctx(); +top_ctx.language.set('en'); +const language = top_ctx.language; +const levels = top_ctx.levels; + +var env = require('../lib/server/env')(); +var openaps = require('../lib/plugins/openaps')(top_ctx); +var sandbox = require('../lib/sandbox')(top_ctx); + +var statuses = [{ + created_at: '2015-12-05T19:05:00.000Z', + device: 'openaps://abusypi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + timestamp: '2015-12-05T18:59:37.000Z', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + clock: '2015-12-05T10:58:47-08:00' + }, + mmtune: { + scanDetails: [ + ['916.640',4,-64] + , ['916.660',5,-55] + , ['916.680',5,-59] + ] + , setFreq: 916.66 + , timestamp:' 2015-12-05T18:59:37.000Z' + , usedDefault: false + }, + openaps: { + suggested: { + bg: 147, + temp: 'absolute', + snoozeBG: 125, + timestamp: '2015-12-05T19:02:42.000Z', + rate: 0.75, + reason: 'Eventual BG 125>120, no temp, setting 0.75U/hr', + eventualBG: 125, + duration: 30, + tick: '+1' + }, + iob: { + timestamp: '2015-12-05T19:02:42.000Z', + bolusiob: 0, + iob: 0.6068340736133333, + activity: 0.016131569664902996 + }, + enacted: { + bg: 147, + temp: 'absolute', + snoozeBG: 125, + recieved: true, + reason: 'Eventual BG 125>120, no temp, setting 0.75U/hr', + rate: 0.75, + eventualBG: 125, + timestamp: '2015-12-05T19:03:00.000Z', + duration: 30, + tick: '+1', + predBGs: { + IOB: [100, 100, 100, 100] + , aCOB: [100, 100, 100, 100] + , COB: [100, 100, 100, 100] + } + } + } +} +, { + created_at: '2015-12-05T18:05:00.000Z', + device: 'openaps://awaitingpi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + timestamp: '2015-12-05T16:59:37.000Z', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + clock: '2015-12-05T08:58:47-08:00' + }, + openaps: { + suggested: { + bg: 147, + temp: 'absolute', + snoozeBG: 125, + timestamp: '2015-12-05T16:02:42.000Z', + rate: 0.75, + reason: 'Eventual BG 125>120, no temp, setting 0.75U/hr', + eventualBG: 125, + duration: 30, + tick: '+1' + }, + iob: { + timestamp: '2015-12-05T16:02:42.000Z', + bolusiob: 0, + iob: 0.6068340736133333, + activity: 0.016131569664902996 + }, + enacted: { + bg: 147, + temp: 'absolute', + snoozeBG: 125, + recieved: true, + reason: 'Eventual BG 125>120, no temp, setting 0.75U/hr', + rate: 0.75, + eventualBG: 125, + timestamp: '2015-12-05T16:03:00.000Z', + duration: 30, + tick: '+1' + } + } +} +,{ + "_id": { + "$oid": "59aef8cb444d1500109fc8fd" + }, + "device": "openaps://edi1", + "openaps": { + "iob": { + "iob": 1.016, + "activity": 0.0143, + "bolussnooze": 0, + "basaliob": 0.893, + "netbasalinsulin": 0.7, + "hightempinsulin": 2.5, + "microBolusInsulin": 1.7, + "microBolusIOB": 0.933, + "lastBolusTime": 1504638182000, + "timestamp": "2017-09-05T19:18:31.000Z" + }, + "suggested": { + "insulinReq": -0.06, + "bg": 117, + "reservoir": "104.5", + "temp": "absolute", + "snoozeBG": 80, + "rate": 0.75, + "minPredBG": 78, + "IOB": 1.016, + "reason": "COB: 0, Dev: -6, BGI: -2.22, ISF: 31, Target: 80, minPredBG 78, IOBpredBG 78; Eventual BG 80 >= 80, insulinReq -0.06. temp 0.2<0.75U/hr. ", + "COB": 0, + "eventualBG": 80, + "duration": 30, + "tick": -3, + "deliverAt": "2017-09-05T19:18:43.563Z", + "timestamp": "2017-09-05T19:18:43.000Z" + }, + "enacted": { + "insulinReq": -0.06, + "received": true, + "bg": 117, + "reservoir": "104.5", + "temp": "absolute", + "snoozeBG": 80, + "timestamp": "2017-09-05T19:18:49.000Z", + "predBGs": { + "IOB": [ + 117, + 114, + 111, + 108, + 106, + 104, + 102, + 100, + 98, + 97, + 96, + 95, + 94, + 93, + 92, + 91, + 90, + 89, + 88, + 87, + 86, + 86, + 85, + 84, + 83, + 83, + 82, + 81, + 81, + 81, + 80, + 80, + 79, + 79, + 79, + 79, + 78 + ] + }, + "minPredBG": 78, + "deliverAt": "2017-09-05T19:18:43.563Z", + "duration": 30, + "rate": 0.75, + "COB": 0, + "eventualBG": 80, + "reason": "COB: 0, Dev: -6, BGI: -2.22, ISF: 31, Target: 80, minPredBG 78, IOBpredBG 78; Eventual BG 80 >= 80, insulinReq -0.06. temp 0.2<0.75U/hr. ", + "tick": -3, + "IOB": 1.016 + } + }, + "pump": { + "clock": "2017-09-05T21:18:31+02:00", + "battery": { + "status": "normal", + "voltage": 1.55 + }, + "reservoir": 104.5, + "status": { + "status": "normal", + "bolusing": false, + "suspended": false, + "timestamp": "2017-09-05T19:18:29.000Z" + } + }, + "uploader": { + "batteryVoltage": 4131, + "battery": 95 + }, + "created_at": "2017-09-05T19:19:39.899Z" +}]; + +var now = top_ctx.moment(statuses[0].created_at); + +_.forEach(statuses, function updateMills (status) { + status.mills = top_ctx.moment(status.created_at).valueOf(); +}); + +describe('openaps', function ( ) { + + it('set the property and update the pill and add forecast points', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('OpenAPS ⌁'); + options.value.should.equal('2m ago'); + var first = _.first(options.info); + first.label.should.equal('1m ago'); + first.value.should.equal('abusypi ⌁ Enacted @ -55dB'); + var last = _.last(options.info); + last.label.should.equal('1h ago'); + last.value.should.equal('awaitingpi ◉ Waiting'); + } + , addForecastPoints: function mockAddForecastPoints (points) { + points.length.should.equal(12); + done(); + } + } + , language: language + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('openaps'); + var result = setter(); + should.exist(result); + + result.status.symbol.should.equal('⌁'); + result.status.code.should.equal('enacted'); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + + }; + + openaps.setProperties(sbx); + + openaps.updateVisualisation(sbx); + + }); + + it('check the recieved flag to see if it was received', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + , levels: levels + }; + + ctx.notifications.initRequests(); + + var notStatuses = _.cloneDeep(statuses); + notStatuses[0].openaps.enacted.recieved = false; + var sbx = require('../lib/sandbox')().clientInit(ctx, now, {devicestatus: notStatuses}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('openaps'); + var result = setter(); + should.exist(result); + result.status.symbol.should.equal('x'); + result.status.code.should.equal('notenacted'); + done(); + }; + + openaps.setProperties(sbx); + + }); + + it('generate an alert for a stuck loop', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.clone().add(1, 'hours').valueOf(), {devicestatus: statuses}); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + openaps.setProperties(sbx); + openaps.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('OpenAPS'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('OpenAPS isn\'t looping'); + done(); + }); + + it('not generate an alert for a stuck loop, when there is an offline marker', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.clone().add(1, 'hours').valueOf(), { + devicestatus: statuses + , treatments: [{eventType: 'OpenAPS Offline', mills: now.valueOf(), duration: 60}] + }); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + openaps.setProperties(sbx); + openaps.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('OpenAPS'); + should.not.exist(highest); + done(); + }); + + it('should handle virtAsst requests', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + openaps.setProperties(sbx); + + openaps.virtAsst.intentHandlers.length.should.equal(2); + + openaps.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('OpenAPS Forecast'); + response.should.equal('The OpenAPS Eventual BG is 125'); + + openaps.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Last Loop'); + response.should.equal('The last successful loop was 2 minutes ago'); + done(); + }, [], sbx); + + }, [], sbx); + + }); + +}); diff --git a/tests/pebble.test.js b/tests/pebble.test.js index d9b7f7b2912..be6e09e9030 100644 --- a/tests/pebble.test.js +++ b/tests/pebble.test.js @@ -2,10 +2,11 @@ var request = require('supertest'); var should = require('should'); +var language = require('../lib/language')(); //Mocked ctx var ctx = {}; -var env = {}; +// var env = {}; Unused variable var now = Date.now(); function updateMills (entries) { @@ -17,8 +18,8 @@ function updateMills (entries) { return entries; } -ctx.data = require('../lib/data')(env, ctx); -ctx.data.sgvs = updateMills([ +ctx.ddata = require('../lib/data/ddata')(); +ctx.ddata.sgvs = updateMills([ { device: 'dexcom', mgdl: 91, direction: 'Flat', @@ -66,7 +67,7 @@ ctx.data.sgvs = updateMills([ } ]); -ctx.data.cals = updateMills([ +ctx.ddata.cals = updateMills([ { device: 'dexcom', slope: 895.8571693029189, intercept: 34281.06876195567, @@ -75,22 +76,33 @@ ctx.data.cals = updateMills([ } ]); -ctx.data.profiles = [{dia: 4 }]; +ctx.ddata.profiles = [{dia: 4, sens: 70, carbratio: 15, carbs_hr: 30}]; -ctx.data.treatments = updateMills([ +ctx.ddata.treatments = updateMills([ { eventType: 'Snack Bolus', insulin: '1.50', carbs: '22' } ]); -ctx.data.devicestatus.uploaderBattery = 100; +ctx.ddata.devicestatus = [{uploader: {battery: 100}}]; +var bootevent = require('../lib/server/bootevent'); describe('Pebble Endpoint', function ( ) { - var pebble = require('../lib/pebble'); + + this.timeout(10000); + + var pebble = require('../lib/server/pebble'); before(function (done) { - var env = require('../env')( ); + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../lib/server/env')( ); + env.settings.authDefaultRoles = 'readable'; this.app = require('express')( ); this.app.enable('api'); - this.app.use('/pebble', pebble(env, ctx)); - done(); + var self = this; + bootevent(env, language).boot(function booted (context) { + context.ddata = ctx.ddata.clone( ); + self.app.use('/pebble', pebble(env, context)); + done(); + }); }); it('/pebble default(1) count', function (done) { @@ -111,6 +123,7 @@ describe('Pebble Endpoint', function ( ) { should.not.exist(bg.noise); should.not.exist(bg.rssi); should.not.exist(bg.iob); + should.not.exist(bg.cob); bg.battery.should.equal('100'); res.body.cals.length.should.equal(0); @@ -167,7 +180,7 @@ describe('Pebble Endpoint', function ( ) { }); it('/pebble without battery', function (done) { - delete ctx.data.devicestatus.uploaderBattery; + ctx.ddata.devicestatus = []; request(this.app) .get('/pebble') .expect(200) @@ -182,7 +195,7 @@ describe('Pebble Endpoint', function ( ) { }); it('/pebble with a negative battery', function (done) { - ctx.data.devicestatus.uploaderBattery = -1; + ctx.ddata.devicestatus = [{uploader: {battery: -1}}]; request(this.app) .get('/pebble') .expect(200) @@ -197,7 +210,7 @@ describe('Pebble Endpoint', function ( ) { }); it('/pebble with a false battery', function (done) { - ctx.data.devicestatus.uploaderBattery = false; + ctx.ddata.devicestatus = [{uploader: {battery: false}}]; request(this.app) .get('/pebble') .expect(200) @@ -212,19 +225,26 @@ describe('Pebble Endpoint', function ( ) { }); }); -describe('Pebble Endpoint with Raw and IOB', function ( ) { - var pebbleRaw = require('../lib/pebble'); +describe('Pebble Endpoint with Raw and IOB and COB', function ( ) { + var pebbleRaw = require('../lib/server/pebble'); before(function (done) { - ctx.data.devicestatus.uploaderBattery = 100; - var envRaw = require('../env')( ); - envRaw.settings.enable = ['rawbg', 'iob']; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../lib/server/env')( ); + env.settings.enable = ['rawbg', 'iob', 'cob']; + env.settings.authDefaultRoles = 'readable'; this.appRaw = require('express')( ); this.appRaw.enable('api'); - this.appRaw.use('/pebble', pebbleRaw(envRaw, ctx)); - done(); + var self = this; + bootevent(env, language).boot(function booted (context) { + context.ddata = ctx.ddata.clone( ); + self.appRaw.use('/pebble', pebbleRaw(env, context)); + done(); + }); }); it('/pebble', function (done) { + ctx.ddata.devicestatus = [{uploader: {battery: 100}}]; request(this.appRaw) .get('/pebble?count=2') .expect(200) @@ -241,6 +261,8 @@ describe('Pebble Endpoint with Raw and IOB', function ( ) { bg.unfiltered.should.equal(111920); bg.noise.should.equal(1); bg.battery.should.equal('100'); + bg.iob.should.equal('1.50'); + bg.cob.should.equal(22); res.body.cals.length.should.equal(1); var cal = res.body.cals[0]; @@ -251,4 +273,35 @@ describe('Pebble Endpoint with Raw and IOB', function ( ) { }); }); -}); \ No newline at end of file + it('/pebble with no treatments', function (done) { + ctx.ddata.treatments = []; + request(this.appRaw) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + var bg = bgs[0]; + bg.iob.should.equal(0); + bg.cob.should.equal(0); + done(); + }); + }); + + it('/pebble with IOB from devicestatus', function (done) { + ctx.ddata.treatments = []; + ctx.ddata.devicestatus = updateMills([{pump: {iob: {bolusiob: 2.3}}}]); + request(this.appRaw) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + var bg = bgs[0]; + bg.iob.should.equal('2.30'); + bg.cob.should.equal(0); + done(); + }); + }); + +}); diff --git a/tests/pluginbase.test.js b/tests/pluginbase.test.js index 2397720b09e..54946964012 100644 --- a/tests/pluginbase.test.js +++ b/tests/pluginbase.test.js @@ -4,22 +4,28 @@ require('should'); var benv = require('benv'); describe('pluginbase', function ( ) { + this.timeout(50000); // TODO: see why this test takes longer on Travis to complete + + var headless = require('./fixtures/headless')(benv, this); before(function (done) { - benv.setup(function() { - benv.expose({ - $: require('jquery') - , jQuery: require('jquery') - }); - done(); - }); + done( ); }); after(function (done) { - benv.teardown(); - done(); + done( ); + }); + + beforeEach(function (done) { + headless.setup({ }, done); }); + afterEach(function (done) { + headless.teardown( ); + done( ); + }); + + it('does stuff', function() { function div (clazz) { @@ -51,4 +57,4 @@ describe('pluginbase', function ( ) { majorPills.length.should.equal(1); }); -}); \ No newline at end of file +}); diff --git a/tests/plugins.test.js b/tests/plugins.test.js index 0b0fc20ac43..38f6c1898f4 100644 --- a/tests/plugins.test.js +++ b/tests/plugins.test.js @@ -6,9 +6,12 @@ describe('Plugins', function ( ) { it('should find client plugins, but not server only plugins', function (done) { - var plugins = require('../lib/plugins/')().registerClientDefaults(); + var plugins = require('../lib/plugins/')({ + settings: { } + , language: require('../lib/language')() + }).registerClientDefaults(); - plugins('delta').name.should.equal('delta'); + plugins('bgnow').name.should.equal('bgnow'); plugins('rawbg').name.should.equal('rawbg'); //server only plugin @@ -18,7 +21,10 @@ describe('Plugins', function ( ) { }); it('should find sever plugins, but not client only plugins', function (done) { - var plugins = require('../lib/plugins/')().registerServerDefaults(); + var plugins = require('../lib/plugins/')({ + settings: { } + , language: require('../lib/language')() + }).registerServerDefaults(); plugins('rawbg').name.should.equal('rawbg'); plugins('treatmentnotify').name.should.equal('treatmentnotify'); diff --git a/tests/profile.test.js b/tests/profile.test.js index 8171f459e3d..58de12cea2c 100644 --- a/tests/profile.test.js +++ b/tests/profile.test.js @@ -1,9 +1,15 @@ var should = require('should'); -var moment = require('moment-timezone'); +const helper = require('./inithelper')(); +const moment = helper.ctx.moment; describe('Profile', function ( ) { - var profile_empty = require('../lib/profilefunctions')(); + + var profile_empty = require('../lib/profilefunctions')(null, helper.ctx); + + beforeEach(function() { + profile_empty.clear(); + }); it('should say it does not have data before it has data', function() { var hasData = profile_empty.hasData(); @@ -29,9 +35,7 @@ describe('Profile', function ( ) { , 'target_high': 120 }; - var profile = require('../lib/profilefunctions')([profileData]); -// console.log(profile); - + var profile = require('../lib/profilefunctions')([profileData],helper.ctx); var now = Date.now(); it('should know what the DIA is with old style profiles', function() { @@ -71,7 +75,7 @@ describe('Profile', function ( ) { it('should know how to reload data and still know what the low target is with old style profiles', function() { - var profile2 = require('../lib/profilefunctions')([profileData]); + var profile2 = require('../lib/profilefunctions')([profileData], helper.ctx); var profileData2 = { 'dia': 3, 'carbs_hr': 30, @@ -155,7 +159,7 @@ describe('Profile', function ( ) { 'units': 'mmol' }; - var complexProfile = require('../lib/profilefunctions')([complexProfileData]); + var complexProfile = require('../lib/profilefunctions')([complexProfileData], helper.ctx); var noon = new Date('2015-06-22 12:00:00').getTime(); var threepm = new Date('2015-06-22 15:00:00').getTime(); @@ -186,5 +190,199 @@ describe('Profile', function ( ) { dia.should.equal(9); }); + var multiProfileData = + [ + { + "startDate": "2015-06-25T00:00:00.000Z", + "defaultProfile": "20150625-1", + "store": { + "20150625-1": { + "dia": "4", + "timezone": moment.tz().zoneName(), //Assume these are in the localtime zone so tests pass when not on UTC time + "startDate": "1970-01-01T00:00:00.000Z", + 'sens': [ + { + 'time': '00:00', + 'value': 12 + }, + { + 'time': '02:00', + 'value': 13 + }, + { + 'time': '07:00', + 'value': 14 + } + ], + 'carbratio': [ + { + 'time': '00:00', + 'value': 16 + }, + { + 'time': '06:00', + 'value': 15 + }, + { + 'time': '14:00', + 'value': 17 + } + ], + 'carbs_hr': 30, + 'target_low': 4.5, + 'target_high': 8, + "units": "mmol", + "basal": [ + { + "time": "00:00", + "value": "0.5", + "timeAsSeconds": "0" + }, + { + "time": "09:00", + "value": "0.25", + "timeAsSeconds": "32400" + }, + { + "time": "12:30", + "value": "0.9", + "timeAsSeconds": "45000" + }, + { + "time": "17:00", + "value": "0.3", + "timeAsSeconds": "61200" + }, + { + "time": "20:00", + "value": "1", + "timeAsSeconds": "72000" + } + ] + } + }, + "units": "mmol", + "mills": "1435190400000" + }, + { + "startDate": "2015-06-21T00:00:00.000Z", + "defaultProfile": "20190621-1", + "store": { + "20190621-1": { + "dia": "4", + "timezone": moment.tz().zoneName(), //Assume these are in the localtime zone so tests pass when not on UTC time + "startDate": "1970-01-01T00:00:00.000Z", + 'sens': [ + { + 'time': '00:00', + 'value': 11 + }, + { + 'time': '02:00', + 'value': 10 + }, + { + 'time': '07:00', + 'value': 9 + } + ], + 'carbratio': [ + { + 'time': '00:00', + 'value': 12 + }, + { + 'time': '06:00', + 'value': 13 + }, + { + 'time': '14:00', + 'value': 14 + } + ], + 'carbs_hr': 35, + 'target_low': 4.2, + 'target_high': 9, + "units": "mmol", + "basal": [ + { + "time": "00:00", + "value": "0.3", + "timeAsSeconds": "0" + }, + { + "time": "09:00", + "value": "0.4", + "timeAsSeconds": "32400" + }, + { + "time": "12:30", + "value": "0.5", + "timeAsSeconds": "45000" + }, + { + "time": "17:00", + "value": "0.6", + "timeAsSeconds": "61200" + }, + { + "time": "23:00", + "value": "0.7", + "timeAsSeconds": "82800" + } + ] + } + }, + "units": "mmol", + "mills": "1434844800000" + } + ]; + + var multiProfile = require('../lib/profilefunctions')(multiProfileData, helper.ctx); + + var noon = new Date('2015-06-22 12:00:00').getTime(); + var threepm = new Date('2015-06-26 15:00:00').getTime(); + + it('should return profile units when configured', function () { + var value = multiProfile.getUnits(); + value.should.equal('mmol'); + }); + + + it('should know what the basal rate is at 12:00 with multiple profiles', function () { + var value = multiProfile.getBasal(noon); + value.should.equal(0.4); + }); + + it('should know what the basal rate is at 15:00 with multiple profiles', function () { + var value = multiProfile.getBasal(threepm); + value.should.equal(0.9); + }); + + it('should know what the carbratio is at 12:00 with multiple profiles', function () { + var carbRatio = multiProfile.getCarbRatio(noon); + carbRatio.should.equal(13); + }); + + it('should know what the carbratio is at 15:00 with multiple profiles', function () { + var carbRatio = multiProfile.getCarbRatio(threepm); + carbRatio.should.equal(17); + }); + + it('should know what the sensitivity is at 12:00 with multiple profiles', function () { + var dia = multiProfile.getSensitivity(noon); + dia.should.equal(9); + }); + + it('should know what the sensitivity is at 15:00 with multiple profiles', function () { + var dia = multiProfile.getSensitivity(threepm); + dia.should.equal(14); + }); + + + it('should select the correct profile for 15:00 with multiple profiles', function () { + var curProfile = multiProfile.getCurrentProfile(threepm); + curProfile.carbs_hr.should.equal(30); + }); }); \ No newline at end of file diff --git a/tests/profileeditor.test.js b/tests/profileeditor.test.js index f53384e41ad..c55d9add3ac 100644 --- a/tests/profileeditor.test.js +++ b/tests/profileeditor.test.js @@ -4,13 +4,9 @@ require('should'); var _ = require('lodash'); var benv = require('benv'); var read = require('fs').readFileSync; -var serverSettings = require('./fixtures/default-server-settings'); -var nowData = { - sgvs: [ - { mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' } - ] -}; +var nowData = require('../lib/data/ddata')(); +nowData.sgvs.push({ mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' }); var exampleProfile = { defaultProfile : 'Default' @@ -56,12 +52,12 @@ var exampleProfile = { 'target_low':[ { 'time': '00:00', - 'value': 0 + 'value': 100 }], 'target_high':[ { 'time': '00:00', - 'value': 0 + 'value': 120 }] } } @@ -69,107 +65,43 @@ var exampleProfile = { var someData = { - '/api/v1/profile.json': [exampleProfile] + '/api/v1/profile.json?count=20': [exampleProfile] }; describe('Profile editor', function ( ) { - var self = this; + this.timeout(40000); //TODO: see why this test takes longer on Travis to complete + var headless = require('./fixtures/headless')(benv, this); before(function (done) { - benv.setup(function() { - self.$ = require('jquery'); - self.$.localStorage = require('./fixtures/localstorage'); - - self.$.fn.tipsy = function mockTipsy ( ) { }; - - self.$.fn.dialog = function mockDialog (opts) { - function maybeCall (name, obj) { - if (obj[name] && obj[name].call) { - obj[name](); - } - - } - maybeCall('open', opts); - - _.forEach(opts.buttons, function (button) { - maybeCall('click', button); - }); - }; - - var indexHtml = read(__dirname + '/../static/profile/index.html', 'utf8'); - self.$('body').html(indexHtml); - - //var filesys = require('fs'); - //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) - - self.$.ajax = function mockAjax (url, opts) { - //logfile.write(url+'\n'); - //console.log(url,opts); - if (opts && opts.success && opts.success.call) { - return { - done: function mockDone (fn) { - if (someData[url]) { - console.log('+++++Data for ' + url + ' sent'); - opts.success(someData[url]); - } else { - console.log('-----Data for ' + url + ' missing'); - opts.success([]); - } - fn(); - return self.$.ajax(); - }, - fail: function mockFail () { - return self.$.ajax(); - } - }; - } - return { - done: function mockDone (fn) { - fn({message: 'OK'}); - return self.$.ajax(); - }, - fail: function mockFail () { - return self.$.ajax(); - } - }; - }; - - var d3 = require('d3'); - //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; - - benv.expose({ - $: self.$ - , jQuery: self.$ - , d3: d3 - , serverSettings: serverSettings - , io: { - connect: function mockConnect ( ) { - return { - on: function mockOn ( ) { } - }; - } - } - }); + done( ); + }); - benv.require(__dirname + '/../bundle/bundle.source.js'); - benv.require(__dirname + '/../static/profile/js/profileeditor.js'); + after(function (done) { + done( ); + }); - done(); - }); + beforeEach(function (done) { + var opts = { + htmlFile: __dirname + '/../views/profileindex.html' + , mockProfileEditor: true + , mockAjax: someData + , benvRequires: [ + __dirname + '/../static/js/profileinit.js' + ] + }; + headless.setup(opts, done); }); - after(function (done) { - benv.teardown(true); - done(); + afterEach(function (done) { + headless.teardown( ); + done( ); }); it ('should produce some html', function (done) { - var plugins = require('../lib/plugins/')().registerClientDefaults(); var client = require('../lib/client'); - var hashauth = require('../lib/hashauth'); + var hashauth = require('../lib/client/hashauth'); hashauth.init(client,$); hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { hashauth.authenticated = true; @@ -185,15 +117,63 @@ describe('Profile editor', function ( ) { return true; }; - client.init(serverSettings, plugins); + window.Nightscout.profileclient(); + + client.init(); client.dataUpdate(nowData); - //var result = $('body').html(); + // var result = $('body').html(); + // console.log(result); //var filesys = require('fs'); - //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + //var logfile = filesys.createWriteStream('out.html', { flags: 'a'} ) //logfile.write($('body').html()); - //console.log(result); + // database records manipulation + $('#pe_databaserecords option').length.should.be.equal(1); + $('#pe_records_add').click(); + $('#pe_databaserecords option').length.should.be.equal(2); + $('#pe_records_remove').click(); + $('#pe_databaserecords option').length.should.be.equal(1); + $('#pe_records_clone').click(); + $('#pe_databaserecords option').length.should.be.equal(2); + $('#pe_databaserecords option').val(0); + + //console.log($('#pe_databaserecords').html()); + //console.log($('#pe_databaserecords').val()); + + // database records manipulation + $('#pe_profiles option').length.should.be.equal(1); + $('#pe_profile_add').click(); + $('#pe_profiles option').length.should.be.equal(2); + $('#pe_profile_name').val('Test'); + $('#pe_profiles option').val('Default'); + $('#pe_profiles option').val('Test'); + $('#pe_profile_remove').click(); + $('#pe_profiles option').length.should.be.equal(1); + $('#pe_profile_clone').click(); + $('#pe_profiles option').length.should.be.equal(2); + $('#pe_profiles option').val('Default'); + + //console.log($('#pe_profiles').html()); + //console.log($('#pe_profiles').val()); + + + // I:C range + $('#pe_ic_val_0').val().should.be.equal('30'); + $('#pe_ic_placeholder').find('img.addsingle').click(); + $('#pe_ic_val_0').val().should.be.equal('0'); + $('#pe_ic_val_1').val().should.be.equal('30'); + $('#pe_ic_placeholder').find('img.delsingle').click(); + $('#pe_ic_val_0').val().should.be.equal('30'); + + // traget bg range + $('#pe_targetbg_low_0').val().should.be.equal('100'); + $('#pe_targetbg_placeholder').find('img.addtargetbg').click(); + $('#pe_targetbg_low_0').val().should.be.equal('0'); + $('#pe_targetbg_low_1').val().should.be.equal('100'); + $('#pe_targetbg_placeholder').find('img.deltargetbg').click(); + $('#pe_targetbg_low_0').val().should.be.equal('100'); + $('#pe_submit').click(); done(); diff --git a/tests/pump.test.js b/tests/pump.test.js new file mode 100644 index 00000000000..efe6e847dc3 --- /dev/null +++ b/tests/pump.test.js @@ -0,0 +1,427 @@ +'use strict'; + +var _ = require('lodash'); +var should = require('should'); +const helper = require('./inithelper')(); +const moment = helper.ctx.moment; + +var top_ctx = helper.getctx(); +top_ctx.settings = require('../lib/settings')(); +top_ctx.language.set('en'); + +var env = require('../lib/server/env')(); +const levels = top_ctx.levels; +const language = top_ctx.language; + +var profile = require('../lib/profilefunctions')(null, top_ctx); +var pump = require('../lib/plugins/pump')(top_ctx); +var sandbox = require('../lib/sandbox')(top_ctx); + +var statuses = [{ + created_at: '2015-12-05T17:35:00.000Z' + , device: 'openaps://farawaypi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + clock: '2015-12-05T17:32:00.000Z' + } +}, { + created_at: '2015-12-05T19:05:00.000Z' + , device: 'openaps://abusypi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + clock: '2015-12-05T19:02:00.000Z' + } +}]; + +var profileData = +{ + 'timezone': moment.tz.guess() +}; + +var statuses2 = [{ + created_at: '2015-12-05T17:35:00.000Z' + , device: 'openaps://farawaypi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + reservoir_display_override: '50+U', + clock: '2015-12-05T17:32:00.000Z' + } +}, { + created_at: '2015-12-05T19:05:00.000Z' + , device: 'openaps://abusypi' + , pump: { + battery: { + status: 'normal', + voltage: 1.52 + }, + status: { + status: 'normal', + bolusing: false, + suspended: false + }, + reservoir: 86.4, + reservoir_display_override: '50+U', + clock: '2015-12-05T19:02:00.000Z' + } +}]; + +var now = moment(statuses[1].created_at); + +_.forEach(statuses, function updateMills (status) { + status.mills = moment(status.created_at).valueOf(); +}); + +_.forEach(statuses2, function updateMills (status) { + status.mills = moment(status.created_at).valueOf(); +}); + +describe('pump', function ( ) { + + it('set the property and update the pill', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.label.should.equal('Pump'); + options.value.should.equal('86.4U'); + done(); + } + } + , language: language + , levels: levels + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('pump'); + var result = setter(); + should.exist(result); + result.data.level.should.equal(levels.NONE); + result.data.battery.value.should.equal(1.52); + result.data.reservoir.value.should.equal(86.4); + + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + + }; + + pump.setProperties(sbx); + pump.updateVisualisation(sbx); + + }); + + it('use reservoir_display_override when available', function (done) { + var ctx = { + settings: { + units: 'mmol' + } + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.label.should.equal('Pump'); + options.value.should.equal('50+U'); + done(); + } + } + , language: language + , levels: levels + }; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses2}); + + var unmockedOfferProperty = sbx.offerProperty; + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('pump'); + sbx.offerProperty = unmockedOfferProperty; + unmockedOfferProperty(name, setter); + }; + + pump.setProperties(sbx); + pump.updateVisualisation(sbx); + + }); + + it('not generate an alert when pump is ok', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + , levels: levels + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: statuses + }); + sbx.extendedSettings = { 'enableAlerts': true }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + should.not.exist(highest); + + done(); + }); + + it('generate an alert when reservoir is low', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + , levels: levels + }; + + ctx.notifications.initRequests(); + + var lowResStatuses = _.cloneDeep(statuses); + lowResStatuses[1].pump.reservoir = 0.5; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowResStatuses + }); + sbx.extendedSettings = { 'enableAlerts': true }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('URGENT: Pump Reservoir Low'); + + done(); + }); + + it('generate an alert when reservoir is 0', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + , levels: levels + }; + + ctx.notifications.initRequests(); + + var lowResStatuses = _.cloneDeep(statuses); + lowResStatuses[1].pump.reservoir = 0; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowResStatuses + }); + sbx.extendedSettings = { 'enableAlerts': true }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('URGENT: Pump Reservoir Low'); + + done(); + }); + + + it('generate an alert when battery is low', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + , levels: levels + }; + + ctx.notifications.initRequests(); + + var lowBattStatuses = _.cloneDeep(statuses); + lowBattStatuses[1].pump.battery.voltage = 1.33; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowBattStatuses + }); + sbx.extendedSettings = { 'enableAlerts': true }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, Pump Battery Low'); + + done(); + }); + + it('generate an urgent alarm when battery is really low', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + , levels: levels + }; + + ctx.notifications.initRequests(); + + var lowBattStatuses = _.cloneDeep(statuses); + lowBattStatuses[1].pump.battery.voltage = 1.00; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowBattStatuses + }); + sbx.extendedSettings = { 'enableAlerts': true }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('URGENT: Pump Battery Low'); + + done(); + }); + + it('not generate a battery alarm during night when PUMP_WARN_BATT_QUIET_NIGHT is true', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + , dayStart: 24 // Set to 24 so it always evaluates true in test + , dayEnd: 21.0 + } + , pluginBase: { + updatePillText: function mockedUpdatePillText(plugin, options) { + options.label.should.equal('Pump'); + options.value.should.equal('86.4U'); + done(); + } + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: require('../lib/language')() + , levels: levels + }; + + ctx.notifications.initRequests(); + + var lowBattStatuses = _.cloneDeep(statuses); + lowBattStatuses[1].pump.battery.voltage = 1.00; + + var sbx = sandbox.clientInit(ctx, now.valueOf(), { + devicestatus: lowBattStatuses + , profiles: [profileData] + }); + profile.loadData(_.cloneDeep([profileData])); + sbx.data.profile = profile; + + sbx.extendedSettings = { + enableAlerts: true + , warnBattQuietNight: true + }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + should.not.exist(highest); + + done(); + }); + + it('not generate an alert for a stale pump data, when there is an offline marker', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + , levels: levels + }; + + ctx.notifications.initRequests(); + + var sbx = sandbox.clientInit(ctx, now.add(1, 'hours').valueOf(), { + devicestatus: statuses + , treatments: [{eventType: 'OpenAPS Offline', mills: now.valueOf(), duration: 60}] + }); + sbx.extendedSettings = { 'enableAlerts': true }; + pump.setProperties(sbx); + pump.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm('Pump'); + should.not.exist(highest); + done(); + }); + + it('should handle virtAsst requests', function (done) { + var ctx = { + settings: { + units: 'mg/dl' + } + , notifications: require('../lib/notifications')(env, top_ctx) + , language: language + , levels: levels + }; + + ctx.language.set('en'); + var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses}); + pump.setProperties(sbx); + + pump.virtAsst.intentHandlers.length.should.equal(4); + + pump.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Insulin Remaining'); + response.should.equal('You have 86.4 units remaining'); + + pump.virtAsst.intentHandlers[1].intentHandler(function next(title, response) { + title.should.equal('Pump Battery'); + response.should.equal('Your pump battery is at 1.52 volts'); + + pump.virtAsst.intentHandlers[2].intentHandler(function next(title, response) { + title.should.equal('Insulin Remaining'); + response.should.equal('You have 86.4 units remaining'); + + pump.virtAsst.intentHandlers[3].intentHandler(function next(title, response) { + title.should.equal('Pump Battery'); + response.should.equal('Your pump battery is at 1.52 volts'); + done(); + }, [], sbx); + + }, [], sbx); + + }, [], sbx); + + }, [], sbx); + + }); + +}); diff --git a/tests/pushnotify.test.js b/tests/pushnotify.test.js index dc0bc9151eb..bc7a95720e1 100644 --- a/tests/pushnotify.test.js +++ b/tests/pushnotify.test.js @@ -6,9 +6,10 @@ var levels = require('../lib/levels'); describe('pushnotify', function ( ) { it('send a pushover alarm, but only 1 time', function (done) { - var env = require('../env')(); + var env = require('../lib/server/env')(); var ctx = {}; + ctx.levels = levels; ctx.notifications = require('../lib/notifications')(env, ctx); var notify = { @@ -29,7 +30,7 @@ describe('pushnotify', function ( ) { } }; - ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + ctx.pushnotify = require('../lib/server/pushnotify')(env, ctx); ctx.pushnotify.emitNotification(notify); @@ -39,9 +40,9 @@ describe('pushnotify', function ( ) { }); it('send a pushover notification, but only 1 time', function (done) { - var env = require('../env')(); + var env = require('../lib/server/env')(); var ctx = {}; - + ctx.levels = levels; ctx.notifications = require('../lib/notifications')(env, ctx); var notify = { @@ -61,7 +62,7 @@ describe('pushnotify', function ( ) { } }; - ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + ctx.pushnotify = require('../lib/server/pushnotify')(env, ctx); ctx.pushnotify.emitNotification(notify); @@ -71,8 +72,9 @@ describe('pushnotify', function ( ) { }); it('send a pushover alarm, and then cancel', function (done) { - var env = require('../env')(); + var env = require('../lib/server/env')(); var ctx = {}; + ctx.levels = levels; ctx.notifications = require('../lib/notifications')(env, ctx); @@ -97,7 +99,7 @@ describe('pushnotify', function ( ) { } }; - ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + ctx.pushnotify = require('../lib/server/pushnotify')(env, ctx); //first send the warning ctx.pushnotify.emitNotification(notify); diff --git a/tests/pushover.test.js b/tests/pushover.test.js index 598086e5159..c49cc44cf3a 100644 --- a/tests/pushover.test.js +++ b/tests/pushover.test.js @@ -3,6 +3,10 @@ var should = require('should'); var levels = require('../lib/levels'); +var ctx = { + levels:levels +} + describe('pushover', function ( ) { var baseurl = 'https://nightscout.test'; @@ -17,9 +21,10 @@ describe('pushover', function ( ) { , apiToken: '6789' } } + , levels:levels }; - var pushover = require('../lib/plugins/pushover')(env); + var pushover = require('../lib/plugins/pushover')(env, ctx); it('convert a warning to a message and send it', function (done) { @@ -75,9 +80,10 @@ describe('support legacy pushover groupkey', function ( ) { , apiToken: '6789' } } + , levels: levels }; - var pushover = require('../lib/plugins/pushover')(env); + var pushover = require('../lib/plugins/pushover')(env, ctx); it('send', function (done) { @@ -111,9 +117,10 @@ describe('multi announcement pushover', function ( ) { , apiToken: '6789' } } + , levels: levels }; - var pushover = require('../lib/plugins/pushover')(env); + var pushover = require('../lib/plugins/pushover')(env, ctx); it('send multiple pushes if there are multiple keys', function (done) { @@ -155,9 +162,10 @@ describe('announcement only pushover', function ( ) { , apiToken: '6789' } } + , levels: levels }; - var pushover = require('../lib/plugins/pushover')(env); + var pushover = require('../lib/plugins/pushover')(env, ctx); it('send push if announcement', function (done) { diff --git a/tests/query.test.js b/tests/query.test.js new file mode 100644 index 00000000000..92a8a80673a --- /dev/null +++ b/tests/query.test.js @@ -0,0 +1,38 @@ +'use strict'; + +require('should'); + +var moment = require('moment'); + +describe('query', function ( ) { + var query = require('../lib/server/query'); + + it('should provide default options', function ( ) { + var opts = query(); + + var low = moment().utc().subtract(4, 'days').subtract(1, 'minutes').format(); + var high = moment().utc().subtract(4, 'days').add(1, 'minutes').format(); + + opts.date['$gte'].should.be.greaterThan(low); + opts.date['$gte'].should.be.lessThan(high); + }); + + it('should not override non default options', function ( ) { + var opts = query({}, { + deltaAgo: 2 * 24 * 60 * 60000, + dateField: 'created_at' + }); + + var low = moment().utc().subtract(2, 'days').subtract(1, 'minutes').format(); + var high = moment().utc().subtract(2, 'days').add(1, 'minutes').format(); + + opts.created_at['$gte'].should.greaterThan(low); + opts.created_at['$gte'].should.lessThan(high); + }); + + it('should not enforce date filter if query includes id', function ( ) { + var opts = query({ find: { _id: 1234 } }); + + (typeof opts.date).should.equal('undefined') + }); +}); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js index 135525ca149..d581a3d9bf4 100644 --- a/tests/rawbg.test.js +++ b/tests/rawbg.test.js @@ -1,23 +1,28 @@ 'use strict'; require('should'); +const fs = require('fs'); describe('Raw BG', function ( ) { - var rawbg = require('../lib/plugins/rawbg')(); - var sandbox = require('../lib/sandbox')(); + var ctx = { + settings: { units: 'mg/dl'} + , language: require('../lib/language')(fs) + , pluginBase: {} + }; + ctx.language.set('en'); + + var rawbg = require('../lib/plugins/rawbg')(ctx); var now = Date.now(); - var pluginBase = {}; var data = { sgvs: [{unfiltered: 113680, filtered: 111232, mgdl: 110, noise: 1, mills: now}] , cals: [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}] }; - var clientSettings = { - units: 'mg/dl' - }; + it('should calculate Raw BG', function (done) { - var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); sbx.offerProperty = function mockedOfferProperty (name, setter) { name.should.equal('rawbg'); @@ -31,5 +36,22 @@ describe('Raw BG', function ( ) { }); + it('should handle virtAsst requests', function (done) { + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(ctx, Date.now(), data); + + rawbg.setProperties(sbx); + + rawbg.virtAsst.intentHandlers.length.should.equal(1); + + rawbg.virtAsst.intentHandlers[0].intentHandler(function next(title, response) { + title.should.equal('Current Raw BG'); + response.should.equal('Your raw bg is 113'); + + done(); + }, [], sbx); + + }); }); diff --git a/tests/reports.test.js b/tests/reports.test.js index 97f54550702..4e62c7de223 100644 --- a/tests/reports.test.js +++ b/tests/reports.test.js @@ -15,22 +15,125 @@ var nowData = { var someData = { '/api/v1/entries.json?find[date][$gte]=1438992000000&find[date][$lt]=1439078400000&count=10000': [{'_id':'55c697f9459cf1fa5ed71cd8','unfiltered':213888,'filtered':218560,'direction':'Flat','device':'dexcom','rssi':172,'sgv':208,'dateString':'Sat Aug 08 16:58:44 PDT 2015','type':'sgv','date':1439078324000,'noise':1},{'_id':'55c696cc459cf1fa5ed71cd7','unfiltered':217952,'filtered':220864,'direction':'Flat','device':'dexcom','rssi':430,'sgv':212,'dateString':'Sat Aug 08 16:53:45 PDT 2015','type':'sgv','date':1439078025000,'noise':1},{'_id':'55c5d0c6459cf1fa5ed71a04','device':'dexcom','scale':1.1,'dateString':'Sat Aug 08 02:48:05 PDT 2015','date':1439027285000,'type':'cal','intercept':31102.323470336833,'slope':776.9097574914869},{'_id':'55c5d0c5459cf1fa5ed71a03','device':'dexcom','dateString':'Sat Aug 08 02:48:03 PDT 2015','mbg':120,'date':1439027283000,'type':'mbg'}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-08-09T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Correction Bolus','glucose':201,'glucoseType':'Finger','insulin':0.65,'units':'mg/dl','created_at':'2015-08-08T23:22:00.000Z','_id':'55c695628a00a3c97a6611ed'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':163,'glucoseType':'Sensor','insulin':0.7,'units':'mg/dl','created_at':'2015-08-08T22:53:11.021Z','_id':'55c68857cd6dd2036036705f'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-08-09T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Correction Bolus','glucose':201,'glucoseType':'Finger','insulin':0.65,'units':'mg/dl','created_at':'2015-08-08T23:22:00.000Z','_id':'55c695628a00a3c97a6611ed'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':163,'glucoseType':'Sensor','insulin':0.7,'units':'mg/dl','created_at':'2015-08-08T22:53:11.021Z','_id':'55c68857cd6dd2036036705f'}], '/api/v1/entries.json?find[date][$gte]=1439078400000&find[date][$lt]=1439164800000&count=10000': [{'_id':'55c7e85f459cf1fa5ed71dc8','unfiltered':183520,'filtered':193120,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':161,'sgv':149,'dateString':'Sun Aug 09 16:53:40 PDT 2015','type':'sgv','date':1439164420000,'noise':1},{'_id':'55c7e270459cf1fa5ed71dc7','unfiltered':199328,'filtered':192608,'direction':'Flat','device':'dexcom','rssi':161,'sgv':166,'dateString':'Sun Aug 09 16:28:40 PDT 2015','type':'sgv','date':1439162920000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-09T00:00:00.000Z&find[created_at][$lt]=2015-08-10T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Snack Bolus','carbs':18,'insulin':1.1,'created_at':'2015-08-09T22:41:56.253Z','_id':'55c7d734270fbd97191013c2'},{'enteredBy':'Dad','eventType':'Carb Correction','carbs':5,'created_at':'2015-08-09T21:39:13.995Z','_id':'55c7c881270fbd97191013b4'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-09T00:00:00.000Z&find[created_at][$lt]=2015-08-10T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Snack Bolus','carbs':18,'insulin':1.1,'created_at':'2015-08-09T22:41:56.253Z','_id':'55c7d734270fbd97191013c2'},{'enteredBy':'Dad','eventType':'Carb Correction','carbs':5,'created_at':'2015-08-09T21:39:13.995Z','_id':'55c7c881270fbd97191013b4'}], '/api/v1/entries.json?find[date][$gte]=1439164800000&find[date][$lt]=1439251200000&count=10000': [{'_id':'55c93af4459cf1fa5ed71ecc','unfiltered':193248,'filtered':188384,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':194,'sgv':193,'dateString':'Mon Aug 10 16:58:36 PDT 2015','type':'sgv','date':1439251116000,'noise':1},{'_id':'55c939d8459cf1fa5ed71ecb','unfiltered':189888,'filtered':184960,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':931,'sgv':188,'dateString':'Mon Aug 10 16:53:38 PDT 2015','type':'sgv','date':1439250818000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-10T00:00:00.000Z&find[created_at][$lt]=2015-08-11T00:00:00.000Z': [{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':180,'glucoseType':'Sensor','carbs':18,'insulin':1.9,'units':'mg/dl','created_at':'2015-08-10T23:53:31.970Z','_id':'55c9397b865550df020e3560'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':140,'glucoseType':'Finger','carbs':50,'insulin':3.4,'units':'mg/dl','created_at':'2015-08-10T20:41:23.516Z','_id':'55c90c73865550df020e3539'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-10T00:00:00.000Z&find[created_at][$lt]=2015-08-11T00:00:00.000Z&count=1000': [{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':180,'glucoseType':'Sensor','carbs':18,'insulin':1.9,'units':'mg/dl','created_at':'2015-08-10T23:53:31.970Z','_id':'55c9397b865550df020e3560'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':140,'glucoseType':'Finger','carbs':50,'insulin':3.4,'units':'mg/dl','created_at':'2015-08-10T20:41:23.516Z','_id':'55c90c73865550df020e3539'}], '/api/v1/entries.json?find[date][$gte]=1439251200000&find[date][$lt]=1439337600000&count=10000': [{'_id':'55ca8c6e459cf1fa5ed71fe2','unfiltered':174080,'filtered':184576,'direction':'FortyFiveDown','device':'dexcom','rssi':169,'sgv':156,'dateString':'Tue Aug 11 16:58:32 PDT 2015','type':'sgv','date':1439337512000,'noise':1},{'_id':'55ca8b42459cf1fa5ed71fe1','unfiltered':180192,'filtered':192768,'direction':'FortyFiveDown','device':'dexcom','rssi':182,'sgv':163,'dateString':'Tue Aug 11 16:53:32 PDT 2015','type':'sgv','date':1439337212000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-11T00:00:00.000Z&find[created_at][$lt]=2015-08-12T00:00:00.000Z': [{'created_at':'2015-08-11T23:37:00.000Z','eventType':'Snack Bolus','carbs':18,'_id':'55ca8644ca3c57683d19c211'},{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':203,'glucoseType':'Sensor','insulin':1,'preBolus':15,'units':'mg/dl','created_at':'2015-08-11T23:22:00.000Z','_id':'55ca8644ca3c57683d19c210'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-11T00:00:00.000Z&find[created_at][$lt]=2015-08-12T00:00:00.000Z&count=1000': [{'created_at':'2015-08-11T23:37:00.000Z','eventType':'Snack Bolus','carbs':18,'_id':'55ca8644ca3c57683d19c211'},{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':203,'glucoseType':'Sensor','insulin':1,'preBolus':15,'units':'mg/dl','created_at':'2015-08-11T23:22:00.000Z','_id':'55ca8644ca3c57683d19c210'}], '/api/v1/entries.json?find[date][$gte]=1439337600000&find[date][$lt]=1439424000000&count=10000': [{'_id':'55cbddee38a8d88ad1b48647','unfiltered':165760,'filtered':167488,'direction':'Flat','device':'dexcom','rssi':165,'sgv':157,'dateString':'Wed Aug 12 16:58:28 PDT 2015','type':'sgv','date':1439423908000,'noise':1},{'_id':'55cbdccc38a8d88ad1b48644','unfiltered':167456,'filtered':169312,'direction':'Flat','device':'dexcom','rssi':168,'sgv':159,'dateString':'Wed Aug 12 16:53:28 PDT 2015','type':'sgv','date':1439423608000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-12T00:00:00.000Z&find[created_at][$lt]=2015-08-13T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Correction Bolus','insulin':0.8,'created_at':'2015-08-12T23:21:08.907Z','_id':'55cbd4e47e726599048a3f91'},{'enteredBy':'Dad','eventType':'Note','notes':'Milk now','created_at':'2015-08-12T21:23:00.000Z','_id':'55cbba4e7e726599048a3f79'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-12T00:00:00.000Z&find[created_at][$lt]=2015-08-14T23:59:59.999Z&count=1000': [{'enteredBy':'Dad','eventType':'Correction Bolus','insulin':0.8,'created_at':'2015-08-12T23:21:08.907Z','_id':'55cbd4e47e726599048a3f91'},{'enteredBy':'Dad','eventType':'Note','notes':'Milk now','created_at':'2015-08-12T21:23:00.000Z','_id':'55cbba4e7e726599048a3f79'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-12T00:00:00.000Z&find[created_at][$lt]=2015-08-13T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Correction Bolus','insulin':0.8,'created_at':'2015-08-12T23:21:08.907Z','_id':'55cbd4e47e726599048a3f91'},{'enteredBy':'Dad','eventType':'Note','notes':'Milk now','created_at':'2015-08-12T21:23:00.000Z','_id':'55cbba4e7e726599048a3f79'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-12T00:00:00.000Z&find[created_at][$lt]=2015-08-13T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Correction Bolus','insulin':0.8,'created_at':'2015-08-12T23:21:08.907Z','_id':'55cbd4e47e726599048a3f91'},{'enteredBy':'Dad','eventType':'Note','notes':'Milk now','created_at':'2015-08-12T21:23:00.000Z','_id':'55cbba4e7e726599048a3f79'}], '/api/v1/entries.json?find[date][$gte]=1439424000000&find[date][$lt]=1439510400000&count=10000': [{'_id':'55cd2f6738a8d88ad1b48ca1','unfiltered':209792,'filtered':229344,'direction':'SingleDown','device':'dexcom','rssi':436,'sgv':205,'dateString':'Thu Aug 13 16:58:24 PDT 2015','type':'sgv','date':1439510304000,'noise':1},{'_id':'55cd2e3b38a8d88ad1b48c95','unfiltered':220928,'filtered':237472,'direction':'FortyFiveDown','device':'dexcom','rssi':418,'sgv':219,'dateString':'Thu Aug 13 16:53:24 PDT 2015','type':'sgv','date':1439510004000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-13T00:00:00.000Z&find[created_at][$lt]=2015-08-14T00:00:00.000Z': [{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':250,'glucoseType':'Sensor','insulin':0.75,'units':'mg/dl','created_at':'2015-08-13T23:45:56.927Z','_id':'55cd2c3497fa97ac5d8bc53b'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':198,'glucoseType':'Sensor','insulin':1.1,'units':'mg/dl','created_at':'2015-08-13T23:11:00.293Z','_id':'55cd240497fa97ac5d8bc535'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-13T00:00:00.000Z&find[created_at][$lt]=2015-08-14T00:00:00.000Z&count=1000': [{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':250,'glucoseType':'Sensor','insulin':0.75,'units':'mg/dl','created_at':'2015-08-13T23:45:56.927Z','_id':'55cd2c3497fa97ac5d8bc53b'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':198,'glucoseType':'Sensor','insulin':1.1,'units':'mg/dl','created_at':'2015-08-13T23:11:00.293Z','_id':'55cd240497fa97ac5d8bc535'}], '/api/v1/entries.json?find[date][$gte]=1439510400000&find[date][$lt]=1439596800000&count=10000': [{'_id':'55ce80e338a8d88ad1b49397','unfiltered':179936,'filtered':202080,'direction':'SingleDown','device':'dexcom','rssi':179,'sgv':182,'dateString':'Fri Aug 14 16:58:20 PDT 2015','type':'sgv','date':1439596700000,'noise':1},{'_id':'55ce7fb738a8d88ad1b4938d','unfiltered':192288,'filtered':213792,'direction':'SingleDown','device':'dexcom','rssi':180,'sgv':197,'dateString':'Fri Aug 14 16:53:20 PDT 2015','type':'sgv','date':1439596400000,'noise':1}], - '/api/v1/treatments.json?find[created_at][$gte]=2015-08-14T00:00:00.000Z&find[created_at][$lt]=2015-08-15T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Site Change','glucose':268,'glucoseType':'Finger','insulin':1.75,'units':'mg/dl','created_at':'2015-08-14T00:00:00.000Z','_id':'55ce78fe925aa80e7071e5d6'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':89,'glucoseType':'Finger','carbs':54,'insulin':3.15,'units':'mg/dl','created_at':'2015-08-14T21:00:00.000Z','_id':'55ce59bb925aa80e7071e5ba'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-14T00:00:00.000Z&find[created_at][$lt]=2015-08-15T00:00:00.000Z&count=1000': [{'enteredBy':'Dad','eventType':'Site Change','glucose':268,'glucoseType':'Finger','insulin':1.75,'units':'mg/dl','created_at':'2015-08-14T00:00:00.000Z','_id':'55ce78fe925aa80e7071e5d6'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':89,'glucoseType':'Finger','carbs':54,'insulin':3.15,'units':'mg/dl','created_at':'2015-08-14T21:00:00.000Z','_id':'55ce59bb925aa80e7071e5ba'}], '/api/v1/entries.json?find[date][$gte]=1439596800000&find[date][$lt]=1439683200000&count=10000': [{'_id':'55cfd25f38a8d88ad1b49931','unfiltered':283136,'filtered':304768,'direction':'SingleDown','device':'dexcom','rssi':185,'sgv':306,'dateString':'Sat Aug 15 16:58:16 PDT 2015','type':'sgv','date':1439683096000,'noise':1},{'_id':'55cfd13338a8d88ad1b4992e','unfiltered':302528,'filtered':312576,'direction':'FortyFiveDown','device':'dexcom','rssi':179,'sgv':329,'dateString':'Sat Aug 15 16:53:16 PDT 2015','type':'sgv','date':1439682796000,'noise':1}], '/api/v1/food/regular.json': [{'_id':'552ece84a6947ea011db35bb','type':'food','category':'Zakladni','subcategory':'Sladkosti','name':'Bebe male','portion':18,'carbs':12,'gi':1,'unit':'pcs','created_at':'2015-04-15T20:48:04.966Z'}], - '/api/v1/treatments.json?find[eventType]=/BG Check/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.000Z': [ + + '/api/v1/treatments.json?find[eventType]=/BG Check/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-08T23:59:59.999Z': [ + {'created_at':'2015-08-08T00:00:00.000Z'}, + {'created_at':'2015-08-09T00:00:00.000Z'}, + {'created_at':'2015-08-10T00:00:00.000Z'}, + {'created_at':'2015-08-11T00:00:00.000Z'}, + {'created_at':'2015-08-12T00:00:00.000Z'}, + {'created_at':'2015-08-13T00:00:00.000Z'}, + {'created_at':'2015-08-14T00:00:00.000Z'}, + {'created_at':'2015-08-15T00:00:00.000Z'}, + {'created_at':'2015-08-16T00:00:00.000Z'}, + {'created_at':'2015-08-17T00:00:00.000Z'}, + {'created_at':'2015-08-18T00:00:00.000Z'}, + {'created_at':'2015-08-19T00:00:00.000Z'}, + {'created_at':'2015-08-20T00:00:00.000Z'}, + {'created_at':'2015-08-21T00:00:00.000Z'}, + {'created_at':'2015-08-22T00:00:00.000Z'}, + {'created_at':'2015-08-23T00:00:00.000Z'}, + {'created_at':'2015-08-24T00:00:00.000Z'}, + {'created_at':'2015-08-25T00:00:00.000Z'}, + {'created_at':'2015-08-26T00:00:00.000Z'}, + {'created_at':'2015-08-27T00:00:00.000Z'}, + {'created_at':'2015-08-28T00:00:00.000Z'}, + {'created_at':'2015-08-29T00:00:00.000Z'}, + {'created_at':'2015-08-30T00:00:00.000Z'}, + {'created_at':'2015-08-31T00:00:00.000Z'}, + {'created_at':'2015-09-01T00:00:00.000Z'}, + {'created_at':'2015-09-02T00:00:00.000Z'}, + {'created_at':'2015-09-03T00:00:00.000Z'}, + {'created_at':'2015-09-04T00:00:00.000Z'}, + {'created_at':'2015-09-05T00:00:00.000Z'}, + {'created_at':'2015-09-06T00:00:00.000Z'}, + {'created_at':'2015-09-07T00:00:00.000Z'} + ], + '/api/v1/treatments.json?find[eventType]=/BG Check/i&find[created_at][$gte]=2015-08-07T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.999Z': [ + {'created_at':'2015-08-08T00:00:00.000Z'}, + {'created_at':'2015-08-09T00:00:00.000Z'}, + {'created_at':'2015-08-10T00:00:00.000Z'}, + {'created_at':'2015-08-11T00:00:00.000Z'}, + {'created_at':'2015-08-12T00:00:00.000Z'}, + {'created_at':'2015-08-13T00:00:00.000Z'}, + {'created_at':'2015-08-14T00:00:00.000Z'}, + {'created_at':'2015-08-15T00:00:00.000Z'}, + {'created_at':'2015-08-16T00:00:00.000Z'}, + {'created_at':'2015-08-17T00:00:00.000Z'}, + {'created_at':'2015-08-18T00:00:00.000Z'}, + {'created_at':'2015-08-19T00:00:00.000Z'}, + {'created_at':'2015-08-20T00:00:00.000Z'}, + {'created_at':'2015-08-21T00:00:00.000Z'}, + {'created_at':'2015-08-22T00:00:00.000Z'}, + {'created_at':'2015-08-23T00:00:00.000Z'}, + {'created_at':'2015-08-24T00:00:00.000Z'}, + {'created_at':'2015-08-25T00:00:00.000Z'}, + {'created_at':'2015-08-26T00:00:00.000Z'}, + {'created_at':'2015-08-27T00:00:00.000Z'}, + {'created_at':'2015-08-28T00:00:00.000Z'}, + {'created_at':'2015-08-29T00:00:00.000Z'}, + {'created_at':'2015-08-30T00:00:00.000Z'}, + {'created_at':'2015-08-31T00:00:00.000Z'}, + {'created_at':'2015-09-01T00:00:00.000Z'}, + {'created_at':'2015-09-02T00:00:00.000Z'}, + {'created_at':'2015-09-03T00:00:00.000Z'}, + {'created_at':'2015-09-04T00:00:00.000Z'}, + {'created_at':'2015-09-05T00:00:00.000Z'}, + {'created_at':'2015-09-06T00:00:00.000Z'}, + {'created_at':'2015-09-07T00:00:00.000Z'} + ], + '/api/v1/treatments.json?find[eventType]=/BG Check/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.999Z': [ + {'created_at':'2015-08-08T00:00:00.000Z'}, + {'created_at':'2015-08-09T00:00:00.000Z'}, + {'created_at':'2015-08-10T00:00:00.000Z'}, + {'created_at':'2015-08-11T00:00:00.000Z'}, + {'created_at':'2015-08-12T00:00:00.000Z'}, + {'created_at':'2015-08-13T00:00:00.000Z'}, + {'created_at':'2015-08-14T00:00:00.000Z'}, + {'created_at':'2015-08-15T00:00:00.000Z'}, + {'created_at':'2015-08-16T00:00:00.000Z'}, + {'created_at':'2015-08-17T00:00:00.000Z'}, + {'created_at':'2015-08-18T00:00:00.000Z'}, + {'created_at':'2015-08-19T00:00:00.000Z'}, + {'created_at':'2015-08-20T00:00:00.000Z'}, + {'created_at':'2015-08-21T00:00:00.000Z'}, + {'created_at':'2015-08-22T00:00:00.000Z'}, + {'created_at':'2015-08-23T00:00:00.000Z'}, + {'created_at':'2015-08-24T00:00:00.000Z'}, + {'created_at':'2015-08-25T00:00:00.000Z'}, + {'created_at':'2015-08-26T00:00:00.000Z'}, + {'created_at':'2015-08-27T00:00:00.000Z'}, + {'created_at':'2015-08-28T00:00:00.000Z'}, + {'created_at':'2015-08-29T00:00:00.000Z'}, + {'created_at':'2015-08-30T00:00:00.000Z'}, + {'created_at':'2015-08-31T00:00:00.000Z'}, + {'created_at':'2015-09-01T00:00:00.000Z'}, + {'created_at':'2015-09-02T00:00:00.000Z'}, + {'created_at':'2015-09-03T00:00:00.000Z'}, + {'created_at':'2015-09-04T00:00:00.000Z'}, + {'created_at':'2015-09-05T00:00:00.000Z'}, + {'created_at':'2015-09-06T00:00:00.000Z'}, + {'created_at':'2015-09-07T00:00:00.000Z'} + ], + + '/api/v1/treatments.json?find[notes]=/something/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-08T23:59:59.999Z': [ {'created_at':'2015-08-08T00:00:00.000Z'}, {'created_at':'2015-08-09T00:00:00.000Z'}, {'created_at':'2015-08-10T00:00:00.000Z'}, @@ -63,7 +166,7 @@ var someData = { {'created_at':'2015-09-06T00:00:00.000Z'}, {'created_at':'2015-09-07T00:00:00.000Z'} ], - '/api/v1/treatments.json?find[notes]=/something/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.000Z': [ + '/api/v1/treatments.json?find[notes]=/something/i&find[created_at][$gte]=2015-08-07T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.999Z': [ {'created_at':'2015-08-08T00:00:00.000Z'}, {'created_at':'2015-08-09T00:00:00.000Z'}, {'created_at':'2015-08-10T00:00:00.000Z'}, @@ -95,7 +198,74 @@ var someData = { {'created_at':'2015-09-05T00:00:00.000Z'}, {'created_at':'2015-09-06T00:00:00.000Z'}, {'created_at':'2015-09-07T00:00:00.000Z'} + ], + '/api/v1/treatments.json?find[notes]=/something/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.999Z': [ + {'created_at':'2015-08-08T00:00:00.000Z'}, + {'created_at':'2015-08-09T00:00:00.000Z'}, + {'created_at':'2015-08-10T00:00:00.000Z'}, + {'created_at':'2015-08-11T00:00:00.000Z'}, + {'created_at':'2015-08-12T00:00:00.000Z'}, + {'created_at':'2015-08-13T00:00:00.000Z'}, + {'created_at':'2015-08-14T00:00:00.000Z'}, + {'created_at':'2015-08-15T00:00:00.000Z'}, + {'created_at':'2015-08-16T00:00:00.000Z'}, + {'created_at':'2015-08-17T00:00:00.000Z'}, + {'created_at':'2015-08-18T00:00:00.000Z'}, + {'created_at':'2015-08-19T00:00:00.000Z'}, + {'created_at':'2015-08-20T00:00:00.000Z'}, + {'created_at':'2015-08-21T00:00:00.000Z'}, + {'created_at':'2015-08-22T00:00:00.000Z'}, + {'created_at':'2015-08-23T00:00:00.000Z'}, + {'created_at':'2015-08-24T00:00:00.000Z'}, + {'created_at':'2015-08-25T00:00:00.000Z'}, + {'created_at':'2015-08-26T00:00:00.000Z'}, + {'created_at':'2015-08-27T00:00:00.000Z'}, + {'created_at':'2015-08-28T00:00:00.000Z'}, + {'created_at':'2015-08-29T00:00:00.000Z'}, + {'created_at':'2015-08-30T00:00:00.000Z'}, + {'created_at':'2015-08-31T00:00:00.000Z'}, + {'created_at':'2015-09-01T00:00:00.000Z'}, + {'created_at':'2015-09-02T00:00:00.000Z'}, + {'created_at':'2015-09-03T00:00:00.000Z'}, + {'created_at':'2015-09-04T00:00:00.000Z'}, + {'created_at':'2015-09-05T00:00:00.000Z'}, + {'created_at':'2015-09-06T00:00:00.000Z'}, + {'created_at':'2015-09-07T00:00:00.000Z'} + ], + + '/api/v1/devicestatus.json&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-08T23:59:59.999Z?find[openaps][$exists]=true&count=1000': [ + { + 'openaps': { + 'suggested': { + 'temp': 'absolute', + 'bg': 67, + 'tick': '+6', + 'eventualBG': 145, + 'snoozeBG': 145, + 'reason': 'BG 67<74.5, delta 6>0; no high-temp to cancel', + 'timestamp': '2015-08-31T00:00:00.000Z' + } + }, + 'created_at': '2015-08-31T00:00:00.000Z' + } + ], + '/api/v1/devicestatus.json&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.999Z?find[openaps][$exists]=true&count=1000': [ + { + 'openaps': { + 'suggested': { + 'temp': 'absolute', + 'bg': 67, + 'tick': '+6', + 'eventualBG': 145, + 'snoozeBG': 145, + 'reason': 'BG 67<74.5, delta 6>0; no high-temp to cancel', + 'timestamp': '2015-08-31T00:00:00.000Z' + } + }, + 'created_at': '2015-08-31T00:00:00.000Z' + } ] + }; var exampleProfile = [ @@ -156,95 +326,121 @@ exampleProfile[0].startDate.setMilliseconds(0); describe('reports', function ( ) { var self = this; - + var headless = require('./fixtures/headless')(benv, this); + this.timeout(80000); + before(function (done) { - benv.setup(function() { - self.$ = require('jquery'); - self.$.localStorage = require('./fixtures/localstorage'); + done( ); + }); - self.$.fn.tipsy = function mockTipsy ( ) { }; + after(function (done) { + done( ); + }); - self.$.fn.dialog = function mockDialog (opts) { - function maybeCall (name, obj) { - if (obj[name] && obj[name].call) { - obj[name](); - } + beforeEach(function (done) { + var opts = { + htmlFile: __dirname + '/../views/reportindex.html' + , mockProfileEditor: true + , serverSettings: serverSettings + , mockSimpleAjax: someData + , benvRequires: [ + __dirname + '/../static/js/reportinit.js' + ] + }; + headless.setup(opts, done); + }); - } - maybeCall('open', opts); + afterEach(function (done) { + headless.teardown( ); + done( ); + }); - _.forEach(opts.buttons, function (button) { - maybeCall('click', button); - }); - }; - var indexHtml = read(__dirname + '/../static/report/index.html', 'utf8'); - self.$('body').html(indexHtml); + it ('should produce some html', function (done) { + var client = window.Nightscout.client; + + var hashauth = require('../lib/client/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + window.confirm = function mockConfirm () { + return true; + }; + + window.alert = function mockAlert () { + return true; + }; + + + window.setTimeout = function mockSetTimeout (call, timer) { + if (timer == 60000) return; + call(); + }; + + window.Nightscout.reportclient(); + + client.init(function afterInit ( ) { + client.dataUpdate(nowData); + + console.log('Sending profile to client'); + + // Load profile, we need to operate in UTC + client.sbx.data.profile.loadData(exampleProfile); + + $('#treatments').addClass('selected'); + $('a.presetdates :first').click(); + $('#rp_notes').val('something'); + $('#rp_eventtype').val('BG Check'); + $('#rp_from').val('2015-08-08'); + $('#rp_to').val('2015-09-07'); + $('#rp_optionsraw').prop('checked', true); + $('#rp_optionsiob').prop('checked', true); + $('#rp_optionscob').prop('checked', true); + $('#rp_enableeventtype').click(); + $('#rp_enablenotes').click(); + $('#rp_enablefood').click(); + $('#rp_enablefood').click(); + $('#rp_log').prop('checked', true); + $('#rp_optionsopenaps').prop('checked', true); + $('#rp_show').click(); + + $('#rp_linear').prop('checked', true); + $('#rp_show').click(); + $('#dailystats').click(); + + $('img.deleteTreatment:first').click(); + $('img.editTreatment:first').click(); + $('.ui-button:contains("Save")').click(); - //var filesys = require('fs'); - //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) - self.$.ajax = function mockAjax (url, opts) { - //logfile.write(url+'\n'); - return { - done: function mockDone (fn) { - if (opts && opts.success && opts.success.call) { - if (someData[url]) { - //console.log('+++++Data for ' + url + ' sent'); - opts.success(someData[url]); - } else { - //console.log('-----Data for ' + url + ' missing'); - opts.success([]); - } - } - fn(); - return self.$.ajax(); - }, - fail: function mockFail (fn) { - fn({status: 400}); - return self.$.ajax(); - } - }; - }; - - self.$.plot = function mockPlot () { - }; - - var d3 = require('d3'); - //disable all d3 transitions so most of the other code can run with jsdom - d3.timer = function mockTimer() { }; - - benv.expose({ - $: self.$ - , jQuery: self.$ - , d3: d3 - , serverSettings: serverSettings - , io: { - connect: function mockConnect ( ) { - return { - on: function mockOn ( ) { } - }; - } - } - }); - - benv.require(__dirname + '/../bundle/bundle.source.js'); - benv.require(__dirname + '/../static/report/js/report.js'); + var result = $('body').html(); + /* + var filesys = require('fs'); + var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + logfile.write(result); + console.log('RESULT', result); + */ + result.indexOf('Milk now').should.be.greaterThan(-1); // daytoday + result.indexOf('50 g').should.be.greaterThan(-1); // daytoday + result.indexOf('TDD average: 2.9U').should.be.greaterThan(-1); // daytoday + result.indexOf('
    0%100%0%2In Range: 47.6%1016 (100%)Correction Bolus250 (Sensor)0.750%100%0%238%616 (100%)Correction Bolus250 (Sensor)0.75
    + + + + +
    + + + + + +

    Oops - Nightscout is having trouble

    + +

    Don't panic, we can work this out! This happens to the best of us.

    +

    Check the errors below and then refer to the + troubleshooting documentation.

    + +

    Errors occurred during startup:

    +
    <%- errors %>
    +
    + + + + diff --git a/static/food/index.html b/views/foodindex.html similarity index 80% rename from static/food/index.html rename to views/foodindex.html index 6550701a0fa..ddfd0e50d0b 100644 --- a/static/food/index.html +++ b/views/foodindex.html @@ -24,24 +24,17 @@ - - - - - + + + + + + <%- include('preloadCSS')%> + - -
    -
    - Status: Not loaded -
    -

    Nightscout

    -
    -

    Food Editor

    -
    -
    - -
    + + <%- include('partials/toolbar') %> +
    Your database @@ -88,6 +81,15 @@

    Food Editor


    + Fat [g]:
    +
    + Protein [g]:
    +
    + Energy [kJ]:
    +
    @@ -106,16 +108,12 @@

    Food Editor

    - Authentication status: - - - - + + <%- include('partials/authentication-status') %> + + - - - - + diff --git a/views/frame.html b/views/frame.html new file mode 100644 index 00000000000..aff6ea0d6ce --- /dev/null +++ b/views/frame.html @@ -0,0 +1,54 @@ +<% + +let urlArray = []; +let nameArray = []; + +for (let i = 0; i <= 8; i++) { + let u = settings['frameUrl' + i]; + let n = settings['frameName' + i] || " "; + if (u) { + urlArray.push(u); + nameArray.push(n); + } +} + +const sitesPerRow = urlArray.length > 3 ? Math.round(urlArray.length / 2) : urlArray.length; +const rows = urlArray.length > 3 ? 2 : 1; + +%> + + Nightscout multiframe view + + + + + + <% let s = 0; + for (let r = 1; r <= rows; r++) { + %> + <% for (let sp = 0; sp < sitesPerRow; sp++) { + let pointer = sp + s; + %> + <% } %> + + + <% for (let sp = 0; sp < sitesPerRow; sp++) { + let pointer = sp + s; + %> + <% } %> + + <% s += sitesPerRow; } %> +
    <%= nameArray[pointer] %>
    + + + \ No newline at end of file diff --git a/views/index.html b/views/index.html new file mode 100644 index 00000000000..87a4413a502 --- /dev/null +++ b/views/index.html @@ -0,0 +1,785 @@ + + + + + + + + + + Nightscout + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%- include('preloadCSS')%> + + + +
    +
    +

    +

    Loading the client

    +
    +
    +
    +
    +
    +
    +
    +
    + <%- include('partials/toolbar') %> + +
    + +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    ---
    +
    +
    +
    +
    +
    + +
      +
    • Hours:
    • +
    • 2
    • +
    • 3
    • +
    • 4
    • +
    • 6
    • +
    • 12
    • +
    • 24
    • +
    • ...
    • +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    + Settings +
    +
    Units
    +
    +
    +
    +
    +
    Date format
    +
    +
    +
    +
    +
    Language
    +
    + +
    +
    +
    +
    Scale
    +
    + +
    +
    +
    +
    Render Basal
    +
    + +
    +
    +
    +
    Bolus Display Threshold
    +
    + +
    +
    +
    +
    Small Bolus Display
    +
    + +
    +
    +
    +
    Large Bolus Display
    +
    + +
    +
    + +
    +
    Enable Alarms
    +
    +
    +
    +
    +
    + + + mins +
    +
    + + + mins +
    +
    +
    +
    +
    Night Mode
    +
    +
    +
    +
    Edit Mode
    +
    +
    +
    +
    Show Raw BG Data
    +
    +
    +
    +
    +
    +
    Custom Title
    +
    +
    +
    +
    Theme
    +
    +
    +
    +
    +
    +
    Show Plugins
    +
    +
    + + + + <%- include('partials/authentication-status') %> + +
    + About +
    +
    version
    +
    head
    +

    +

    License: AGPL
    +
    Copyright © 2017 Nightscout contributors
    +

    + +
    +
    +
    + +
    +
    + +
    +
    +
    + Log a Treatment + + + +
    + Targets + + +
    + +
    + Glucose Reading + + + + +
    + + + + + +
    + + + +
    + +
    + Sensor + + +
    + + + + + + + + + + + +
    + + + +
    + Event Time + + + + +
    + + +
    +
    +
    + +
    +
    +
    + + Bolus Wizard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + BG: + + + + +
    + + + + + + + + + + 0.00 +
    + Quickpick: +
    + +
    + + or + + Add from database + + +
    +
    +
    + + + Carbs: + + g + + + +
    + + + COB: + + g + + + +
    + + + IOB: + + 0.00 +
    + + Other correction: + + +
    + + Rounding: + + 0.00 +
    + + Calculation is in target range. +
    + + + Insulin needed: + + 0.00 +
    + + Carbs needed: + + + +
    + + Basal rate: + + +
    +
    + +
    + +
    + + + + +
    + + + + +
    + Event Time: + + + + + + +
    + +
    + +
    + + +
    + +
    + + +
    + + + + + + + + + + diff --git a/views/partials/authentication-status.ejs b/views/partials/authentication-status.ejs new file mode 100644 index 00000000000..db969c906b1 --- /dev/null +++ b/views/partials/authentication-status.ejs @@ -0,0 +1,3 @@ +
    + Authentication status: +
    \ No newline at end of file diff --git a/views/partials/toolbar.ejs b/views/partials/toolbar.ejs new file mode 100644 index 00000000000..356f2da56fc --- /dev/null +++ b/views/partials/toolbar.ejs @@ -0,0 +1,35 @@ +
    +

    Nightscout

    + + <%if (type !== 'index') { %> + X + <% } %> + + <%if (type === 'food') { %> +
    + Status: Not loaded +
    + <% } %> + <%if (type === 'profile') { %> +
    + Status: Not loaded +
    + <% } %> + <%if (type === 'index') { %> + + <% } %> +
    + +<%if (title) { %> +
    +

    <%= title %>

    +
    +<% } %> \ No newline at end of file diff --git a/views/preloadCSS.ejs b/views/preloadCSS.ejs new file mode 100644 index 00000000000..c4cd704b109 --- /dev/null +++ b/views/preloadCSS.ejs @@ -0,0 +1,107 @@ + + \ No newline at end of file diff --git a/static/profile/index.html b/views/profileindex.html similarity index 92% rename from static/profile/index.html rename to views/profileindex.html index bc20ca4e60a..501f59d1dbc 100644 --- a/static/profile/index.html +++ b/views/profileindex.html @@ -25,22 +25,15 @@ - - - - - - -
    -
    - Status: Not loaded -
    -

    Nightscout

    -
    + + + + + <%- include('preloadCSS')%> -
    -

    Profile Editor

    -
    + + + <%- include('partials/toolbar') %>
    @@ -60,7 +53,7 @@

    Profile Editor

    - + -
    Record valid from: @@ -80,7 +72,7 @@

    Profile Editor

    - +