├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── frontend ├── Dockerfile ├── containerpilot.json5 ├── lib │ ├── index.js │ ├── public │ │ ├── index.html │ │ ├── js │ │ │ └── bundle.js │ │ └── lib │ │ │ ├── d3.min.js │ │ │ ├── jquery.js │ │ │ ├── lodash.min.js │ │ │ ├── rickshaw.min.css │ │ │ ├── rickshaw.min.js │ │ │ └── style.css │ └── webStream.js ├── memory.sh └── package.json ├── local-compose.yml ├── project_overview.graffle ├── data.plist ├── image1.pdf ├── image15.tiff ├── image16.tiff ├── image17.tiff ├── image18.tiff ├── image19.tiff ├── image2.png ├── image22.tiff ├── image3.tiff └── image6.png ├── project_overview.png ├── sensor ├── Dockerfile ├── containerpilot.json5 ├── lib │ └── index.js └── package.json ├── serializer ├── Dockerfile ├── containerpilot.json5 ├── lib │ └── index.js ├── package.json └── sensors │ └── request_count.sh ├── setup.sh ├── smartthings ├── Dockerfile ├── containerpilot.json5 ├── health.sh ├── lib │ └── index.js ├── package.json └── sensor.groovy └── traefik ├── Dockerfile ├── containerpilot.json5 └── traefik.toml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | _env 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Microservices in Containers with ContainerPilot 2 | 3 | Example microservices implementation using Node.js and Docker. Below is an architectural diagram depicting the composition of services that make up the project. When everything is working a frontend web application is accessible that will display a set of graphs using sensor data. The sensor data is either generated by the smartthings microservice or is sent to the microservice from a SmartThings hub running a Sensor SmartApp. 4 | 5 | ![](./project_overview.png) 6 | 7 | ## Usage 8 | 9 | ### Development 10 | 11 | In local development you can start the microservices by running 12 | 13 | ```console 14 | $ docker-compose -f local-compose.yml up -d 15 | ``` 16 | 17 | Then scale up the frontend by a number of instances (3 in this case): 18 | 19 | ```console 20 | $ docker-compose -f local-compose.yml scale frontend=3 21 | ``` 22 | 23 | Navigate to `http://localhost` in your browser and you will see 3 charts. As data flows into the serializer from the various sensors you will start to see data appear on the charts in real-time. 24 | 25 | To check that all of the local containers are running you can execute the `ps` command by running 26 | 27 | ```console 28 | $ docker-compose -f local-compose.yml ps 29 | ``` 30 | 31 | ### Production 32 | 33 | When deploying to Triton, first setup your environment then run docker-compose. Below is an example of setting your environment variables then pushing the code to production. 34 | 35 | ```console 36 | $ ./setup.sh 37 | $ eval "$(triton env)" 38 | $ docker-compose up -d 39 | $ triton instance get nodejsexample_traefik_1 40 | ``` 41 | 42 | 43 | ## Credits 44 | 45 | This project is inspired by various microservices workshops and trainings. In no particular order, they are: 46 | * https://github.com/lloydbenson/microservices-workshop - Lloyd Benson 47 | * https://github.com/nearform/micro-services-tutorial-iot - nearForm 48 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | consul: 2 | image: autopilotpattern/consul:0.7.2-r0.8 3 | command: > 4 | /usr/local/bin/containerpilot 5 | /bin/consul agent -server 6 | -config-dir=/etc/consul 7 | -log-level=err 8 | -bootstrap-expect 1 9 | -ui-dir /ui 10 | restart: always 11 | labels: 12 | - triton.cns.services=consul 13 | ports: 14 | - "8500:8500" 15 | expose: 16 | - 9090 17 | env_file: 18 | - _env 19 | nats: 20 | image: autopilotpattern/nats:1.0.2-r5 21 | restart: always 22 | expose: 23 | - "9090" 24 | - "4222" 25 | - "8222" 26 | - "6222" 27 | env_file: 28 | - _env 29 | natsboard: 30 | image: d0cker/natsboard:1.0.0 31 | restart: always 32 | ports: 33 | - "3000:3000" 34 | - "3001:3001" 35 | expose: 36 | - "9090" 37 | env_file: 38 | - _env 39 | prometheus: 40 | image: autopilotpattern/prometheus:1.7.1-r24 41 | mem_limit: 128m 42 | restart: always 43 | ports: 44 | - "9090:9090" 45 | env_file: 46 | - _env 47 | influxdb: 48 | image: autopilotpattern/influxdb:1.1.1 49 | restart: always 50 | expose: 51 | - "9090" 52 | - "8086" 53 | - "8083" 54 | env_file: 55 | - _env 56 | environment: 57 | - ADMIN_USER=root 58 | - INFLUXDB_INIT_PWD=root123 59 | - INFLUXDB_ADMIN_ENABLED=true 60 | - INFLUXDB_REPORTING_DISABLED=true 61 | - INFLUXDB_DATA_QUERY_LOG_ENABLED=false 62 | - INFLUXDB_HTTP_LOG_ENABLED=false 63 | - INFLUXDB_CONTINUOUS_QUERIES_LOG_ENABLED=false 64 | traefik: 65 | image: d0cker/traefik:1.3.2 66 | labels: 67 | - triton.cns.services=ui 68 | ports: 69 | - "80:80" 70 | - "8080:8080" 71 | expose: 72 | - "9090" 73 | env_file: 74 | - _env 75 | restart: always 76 | serializer: 77 | image: d0cker/serializer:6.4.0 78 | env_file: 79 | - _env 80 | environment: 81 | - INFLUXDB_USER=root 82 | - INFLUXDB_PWD=root123 83 | expose: 84 | - "80" 85 | - "9090" 86 | restart: always 87 | frontend: 88 | image: d0cker/frontend:6.2.1 89 | env_file: 90 | - _env 91 | expose: 92 | - "80" 93 | - "9090" 94 | restart: always 95 | smartthings: 96 | image: d0cker/smartthings:8.2.1 97 | labels: 98 | - triton.cns.services=smartthings 99 | ports: 100 | - "80" 101 | expose: 102 | - "9090" 103 | env_file: 104 | - _env 105 | environment: 106 | - FAKE_MODE=true 107 | restart: always 108 | humidity: 109 | image: d0cker/sensor:4.3.1 110 | env_file: 111 | - _env 112 | environment: 113 | - SENSOR_TYPE=humidity 114 | expose: 115 | - "9090" 116 | restart: always 117 | motion: 118 | image: d0cker/sensor:4.3.1 119 | env_file: 120 | - _env 121 | environment: 122 | - SENSOR_TYPE=motion 123 | expose: 124 | - "9090" 125 | restart: always 126 | temperature: 127 | image: d0cker/sensor:4.3.1 128 | env_file: 129 | - _env 130 | environment: 131 | - SENSOR_TYPE=temperature 132 | expose: 133 | - "9090" 134 | restart: always 135 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | RUN apk update && \ 4 | apk add curl && \ 5 | rm -rf /var/cache/apk/* 6 | 7 | RUN export CONSUL_VERSION=0.7.0 \ 8 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \ 9 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \ 10 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \ 11 | && unzip /tmp/consul -d /usr/local/bin \ 12 | && rm /tmp/consul.zip \ 13 | && mkdir /config 14 | 15 | # Install ContainerPilot 16 | ENV CONTAINERPILOT_VERSION 3.4.2 17 | RUN export CP_SHA1=5c99ae9ede01e8fcb9b027b5b3cb0cfd8c0b8b88 \ 18 | && curl -Lso /tmp/containerpilot.tar.gz \ 19 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \ 20 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \ 21 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \ 22 | && rm /tmp/containerpilot.tar.gz 23 | 24 | # COPY ContainerPilot configuration 25 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5 26 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH} 27 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH} 28 | 29 | COPY memory.sh /bin/memory.sh 30 | RUN chmod 755 /bin/memory.sh 31 | 32 | # Install our application 33 | RUN mkdir -p /opt/app/lib/public 34 | COPY package.json /opt/app/ 35 | COPY lib/*.js /opt/app/lib/ 36 | COPY lib/public /opt/app/lib/public 37 | RUN cd /opt/app && npm install 38 | 39 | CMD ["/bin/containerpilot"] 40 | -------------------------------------------------------------------------------- /frontend/containerpilot.json5: -------------------------------------------------------------------------------- 1 | { 2 | consul: 'localhost:8500', 3 | jobs: [ 4 | { 5 | name: 'frontend', 6 | port: {{.PORT}}, 7 | exec: 'node /opt/app/', 8 | health: { 9 | exec: '/usr/bin/curl -o /dev/null --fail -s http://localhost:{{.PORT}}/check-it-out', 10 | interval: 2, 11 | ttl: 5 12 | }, 13 | tags: [ 14 | 'traefik.backend=frontend', 15 | 'traefik.frontend.rule=Path: /,/lib/{[a-z]+},/js/{[a-z]+}', 16 | 'traefik.frontend.entryPoints=http,ws,wss' 17 | ] 18 | }, 19 | { 20 | name: 'consul-agent', 21 | exec: ['/usr/local/bin/consul', 'agent', 22 | '-data-dir=/data', 23 | '-config-dir=/config', 24 | '-log-level=err', 25 | '-rejoin', 26 | '-retry-join', '{{ .CONSUL | default "consul" }}', 27 | '-retry-max', '10', 28 | '-retry-interval', '10s'], 29 | restarts: 'unlimited' 30 | }, 31 | { 32 | name: 'onchange-serializer', 33 | exec: 'pkill -SIGHUP node', 34 | when: { 35 | source: 'watch.serializer', 36 | each: 'changed' 37 | } 38 | }, 39 | { 40 | name: 'memory-sensor', 41 | exec: '/bin/memory.sh', 42 | timeout: '5s', 43 | when: { 44 | interval: '5s' 45 | }, 46 | restarts: 'unlimited' 47 | } 48 | ], 49 | watches: [ 50 | { 51 | name: 'serializer', 52 | interval: 3 53 | } 54 | ], 55 | telemetry: { 56 | port: 9090, 57 | tags: ['op'], 58 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet'], 59 | metrics: [ 60 | { 61 | namespace: 'example', 62 | subsystem: 'frontend', 63 | name: 'free_memory', 64 | help: 'Amount of free memory', 65 | type: 'gauge' 66 | } 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Path = require('path'); 6 | const Brule = require('brule'); 7 | const Hapi = require('hapi'); 8 | const Inert = require('inert'); 9 | const Piloted = require('piloted'); 10 | const WebStream = require('./webStream'); 11 | const Wreck = require('wreck'); 12 | 13 | 14 | 15 | function main () { 16 | const serverConfig = { 17 | connections: { 18 | routes: { 19 | files: { 20 | relativeTo: Path.join(__dirname, 'public') 21 | } 22 | } 23 | }, 24 | load: { 25 | sampleInterval: 100 26 | } 27 | }; 28 | 29 | const server = new Hapi.Server(serverConfig); 30 | server.connection({ 31 | port: process.env.PORT, 32 | load: { 33 | maxEventLoopDelay: 30 // 30 milliseconds 34 | } 35 | }); 36 | server.register([Inert, Brule], () => { 37 | server.route({ 38 | method: 'GET', 39 | path: '/{param*}', 40 | handler: { 41 | directory: { 42 | path: '.', 43 | redirectToSlash: true, 44 | index: true 45 | } 46 | } 47 | }); 48 | 49 | server.start(() => { 50 | console.log(`listening at http://localhost:${server.info.port}`); 51 | startReading(WebStream(server.listener)); 52 | }); 53 | }); 54 | } 55 | main(); 56 | 57 | function startReading (webStream) { 58 | let lastEmitted = 0; 59 | setInterval(() => { 60 | const serializer = Piloted.service('serializer'); 61 | if (!serializer) { 62 | console.log('Serializer not found'); 63 | return; 64 | } 65 | 66 | Wreck.get(`http://${serializer.address}:${serializer.port}/read`, { json: 'force' }, (err, res, points) => { 67 | if (err) { 68 | console.error('Error making request to serializer: ' + err); 69 | return; 70 | } 71 | 72 | if (!points || !points.length) { 73 | return; 74 | } 75 | 76 | let toEmit = []; 77 | points.forEach((point) => { 78 | if (!point) { 79 | return; 80 | } 81 | point.time = (new Date(point.time || Date.now())).getTime(); 82 | 83 | if (point.time > lastEmitted) { 84 | lastEmitted = point.time; 85 | toEmit.push(point); 86 | } 87 | }); 88 | 89 | if (toEmit.length) { 90 | webStream.emit([].concat.apply([], toEmit)); 91 | } 92 | }); 93 | }, 1000); 94 | } 95 | -------------------------------------------------------------------------------- /frontend/lib/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

Temperature Sensor

16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |

Humidity Sensor

24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 |

Motion Sensor

32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/lib/public/lib/lodash.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * lodash 3.10.1 (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE 4 | * Build: `lodash modern -o ./lodash.js` 5 | */ 6 | ;(function(){function n(n,t){if(n!==t){var r=null===n,e=n===w,u=n===n,o=null===t,i=t===w,f=t===t;if(n>t&&!o||!u||r&&!i&&f||e&&f)return 1;if(n=n&&9<=n&&13>=n||32==n||160==n||5760==n||6158==n||8192<=n&&(8202>=n||8232==n||8233==n||8239==n||8287==n||12288==n||65279==n); 8 | }function v(n,t){for(var r=-1,e=n.length,u=-1,o=[];++r=F&&gu&&lu?new Dn(t):null,c=t.length;a&&(i=Mn,f=false,t=a);n:for(;++oi(t,a,0)&&u.push(a);return u}function at(n,t){var r=true;return Su(n,function(n,e,u){return r=!!t(n,e,u)}),r}function ct(n,t,r,e){var u=e,o=u;return Su(n,function(n,i,f){i=+t(n,i,f),(r(i,u)||i===e&&i===o)&&(u=i, 14 | o=n)}),o}function lt(n,t){var r=[];return Su(n,function(n,e,u){t(n,e,u)&&r.push(n)}),r}function st(n,t,r,e){var u;return r(n,function(n,r,o){return t(n,r,o)?(u=e?r:n,false):void 0}),u}function pt(n,t,r,e){e||(e=[]);for(var u=-1,o=n.length;++ut&&(t=-t>u?0:u+t),r=r===w||r>u?u:+r||0,0>r&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0,r=Be(u);++e=c)break n;o=e[o],u*="asc"===o||true===o?1:-1;break n}u=t.b-r.b}return u})}function $t(n,t){ 21 | var r=0;return Su(n,function(n,e,u){r+=+t(n,e,u)||0}),r}function St(n,t){var e=-1,u=xr(),o=n.length,i=u===r,f=i&&o>=F,a=f&&gu&&lu?new Dn(void 0):null,c=[];a?(u=Mn,i=false):(f=false,a=t?[]:c);n:for(;++eu(a,s,0)&&((t||f)&&a.push(s),c.push(l))}return c}function Ft(n,t){for(var r=-1,e=t.length,u=Be(e);++r>>1,i=n[o];(r?i<=t:iu?w:o,u=1);++e=F)return t.plant(e).value();for(var u=0,n=r?o[u].apply(this,n):e;++uarguments.length;return typeof e=="function"&&o===w&&Oo(r)?n(r,e,u,i):Ot(r,wr(e,o,4),u,i,t)}}function sr(n,t,r,e,u,o,i,f,a,c){function l(){for(var m=arguments.length,b=m,j=Be(m);b--;)j[b]=arguments[b];if(e&&(j=Mt(j,e,u)),o&&(j=qt(j,o,i)),_||y){var b=l.placeholder,k=v(j,b),m=m-k.length;if(mt?0:t)):[]}function Pr(n,t,r){var e=n?n.length:0;return e?((r?Ur(n,t,r):null==t)&&(t=1),t=e-(+t||0),Et(n,0,0>t?0:t)):[]}function Kr(n){return n?n[0]:w}function Vr(n,t,e){var u=n?n.length:0;if(!u)return-1;if(typeof e=="number")e=0>e?bu(u+e,0):e;else if(e)return e=Lt(n,t), 42 | er?bu(u+r,0):r||0,typeof n=="string"||!Oo(n)&&be(n)?r<=u&&-1t?0:+t||0,e);++r=n&&(t=w),r}}function ae(n,t,r){function e(t,r){r&&iu(r),a=p=h=w,t&&(_=ho(),c=n.apply(s,f),p||a||(f=s=w))}function u(){var n=t-(ho()-l);0>=n||n>t?e(h,a):p=su(u,n)}function o(){e(g,p); 46 | }function i(){if(f=arguments,l=ho(),s=this,h=g&&(p||!y),false===v)var r=y&&!p;else{a||y||(_=l);var e=v-(l-_),i=0>=e||e>v;i?(a&&(a=iu(a)),_=l,c=n.apply(s,f)):a||(a=su(o,e))}return i&&p?p=iu(p):p||t===v||(p=su(u,t)),r&&(i=true,c=n.apply(s,f)),!i||p||a||(f=s=w),c}var f,a,c,l,s,p,h,_=0,v=false,g=true;if(typeof n!="function")throw new Ge(L);if(t=0>t?0:+t||0,true===r)var y=true,g=false;else ge(r)&&(y=!!r.leading,v="maxWait"in r&&bu(+r.maxWait||0,t),g="trailing"in r?!!r.trailing:g);return i.cancel=function(){p&&iu(p),a&&iu(a), 47 | _=0,a=p=h=w},i}function ce(n,t){function r(){var e=arguments,u=t?t.apply(this,e):e[0],o=r.cache;return o.has(u)?o.get(u):(e=n.apply(this,e),r.cache=o.set(u,e),e)}if(typeof n!="function"||t&&typeof t!="function")throw new Ge(L);return r.cache=new ce.Cache,r}function le(n,t){if(typeof n!="function")throw new Ge(L);return t=bu(t===w?n.length-1:+t||0,0),function(){for(var r=arguments,e=-1,u=bu(r.length-t,0),o=Be(u);++et}function pe(n){return h(n)&&Er(n)&&nu.call(n,"callee")&&!cu.call(n,"callee")}function he(n,t,r,e){return e=(r=typeof r=="function"?Bt(r,e,3):w)?r(n,t):w,e===w?dt(n,t,r):!!e}function _e(n){return h(n)&&typeof n.message=="string"&&ru.call(n)==P}function ve(n){return ge(n)&&ru.call(n)==K}function ge(n){var t=typeof n;return!!n&&("object"==t||"function"==t)}function ye(n){ 49 | return null==n?false:ve(n)?uu.test(Qe.call(n)):h(n)&&Rn.test(n)}function de(n){return typeof n=="number"||h(n)&&ru.call(n)==V}function me(n){var t;if(!h(n)||ru.call(n)!=Z||pe(n)||!(nu.call(n,"constructor")||(t=n.constructor,typeof t!="function"||t instanceof t)))return false;var r;return ht(n,function(n,t){r=t}),r===w||nu.call(n,r)}function we(n){return ge(n)&&ru.call(n)==Y}function be(n){return typeof n=="string"||h(n)&&ru.call(n)==G}function xe(n){return h(n)&&Sr(n.length)&&!!Sn[ru.call(n)]}function Ae(n,t){ 50 | return nt||!n||!mu(t))return r;do t%2&&(r+=n),t=yu(t/2),n+=n;while(t);return r}function We(n,t,r){var e=n;return(n=u(n))?(r?Ur(e,t,r):null==t)?n.slice(g(n),y(n)+1):(t+="",n.slice(o(n,t),i(n,t)+1)):n}function $e(n,t,r){return r&&Ur(n,t,r)&&(t=w),n=u(n),n.match(t||Wn)||[]}function Se(n,t,r){return r&&Ur(n,t,r)&&(t=w),h(n)?Ne(n):ut(n,t)}function Fe(n){ 52 | return n}function Ne(n){return bt(ot(n,true))}function Te(n,t,r){if(null==r){var e=ge(t),u=e?zo(t):w;((u=u&&u.length?gt(t,u):w)?u.length:e)||(u=false,r=t,t=n,n=this)}u||(u=gt(t,zo(t)));var o=true,e=-1,i=ve(n),f=u.length;false===r?o=false:ge(r)&&"chain"in r&&(o=r.chain);for(;++e=$)return r}else n=0;return Lu(r,e)}}(),Mu=le(function(n,t){ 55 | return h(n)&&Er(n)?ft(n,pt(t,false,true)):[]}),qu=tr(),Pu=tr(true),Ku=le(function(n){for(var t=n.length,e=t,u=Be(l),o=xr(),i=o===r,f=[];e--;){var a=n[e]=Er(a=n[e])?a:[];u[e]=i&&120<=a.length&&gu&&lu?new Dn(e&&a):null}var i=n[0],c=-1,l=i?i.length:0,s=u[0];n:for(;++c(s?Mn(s,a):o(f,a,0))){for(e=t;--e;){var p=u[e];if(0>(p?Mn(p,a):o(n[e],a,0)))continue n}s&&s.push(a),f.push(a)}return f}),Vu=le(function(t,r){r=pt(r);var e=rt(t,r);return It(t,r.sort(n)),e}),Zu=vr(),Yu=vr(true),Gu=le(function(n){return St(pt(n,false,true)); 56 | }),Ju=le(function(n,t){return Er(n)?ft(n,t):[]}),Xu=le(Jr),Hu=le(function(n){var t=n.length,r=2--n?t.apply(this,arguments):void 0}},Nn.ary=function(n,t,r){return r&&Ur(n,t,r)&&(t=w),t=n&&null==t?n.length:bu(+t||0,0),gr(n,E,w,w,w,w,t)},Nn.assign=Co,Nn.at=no,Nn.before=fe,Nn.bind=_o,Nn.bindAll=vo,Nn.bindKey=go,Nn.callback=Se,Nn.chain=Qr,Nn.chunk=function(n,t,r){t=(r?Ur(n,t,r):null==t)?1:bu(yu(t)||1,1),r=0;for(var e=n?n.length:0,u=-1,o=Be(vu(e/t));rr&&(r=-r>u?0:u+r),e=e===w||e>u?u:+e||0,0>e&&(e+=u),u=r>e?0:e>>>0,r>>>=0;rt?0:t)):[]},Nn.takeRight=function(n,t,r){var e=n?n.length:0;return e?((r?Ur(n,t,r):null==t)&&(t=1),t=e-(+t||0),Et(n,0>t?0:t)):[]},Nn.takeRightWhile=function(n,t,r){ 71 | return n&&n.length?Nt(n,wr(t,r,3),false,true):[]},Nn.takeWhile=function(n,t,r){return n&&n.length?Nt(n,wr(t,r,3)):[]},Nn.tap=function(n,t,r){return t.call(r,n),n},Nn.throttle=function(n,t,r){var e=true,u=true;if(typeof n!="function")throw new Ge(L);return false===r?e=false:ge(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),ae(n,t,{leading:e,maxWait:+t,trailing:u})},Nn.thru=ne,Nn.times=function(n,t,r){if(n=yu(n),1>n||!mu(n))return[];var e=-1,u=Be(xu(n,4294967295));for(t=Bt(t,r,1);++ee?u[e]=t(e):t(e); 72 | return u},Nn.toArray=je,Nn.toPlainObject=ke,Nn.transform=function(n,t,r,e){var u=Oo(n)||xe(n);return t=wr(t,e,4),null==r&&(u||ge(n)?(e=n.constructor,r=u?Oo(n)?new e:[]:$u(ve(e)?e.prototype:w)):r={}),(u?Pn:_t)(n,function(n,e,u){return t(r,n,e,u)}),r},Nn.union=Gu,Nn.uniq=Gr,Nn.unzip=Jr,Nn.unzipWith=Xr,Nn.values=Ee,Nn.valuesIn=function(n){return Ft(n,Re(n))},Nn.where=function(n,t){return re(n,bt(t))},Nn.without=Ju,Nn.wrap=function(n,t){return t=null==t?Fe:t,gr(t,R,w,[n],[])},Nn.xor=function(){for(var n=-1,t=arguments.length;++nr?0:+r||0,e),r-=t.length,0<=r&&n.indexOf(t,r)==r},Nn.escape=function(n){return(n=u(n))&&hn.test(n)?n.replace(sn,c):n},Nn.escapeRegExp=function(n){return(n=u(n))&&bn.test(n)?n.replace(wn,l):n||"(?:)"},Nn.every=te,Nn.find=ro,Nn.findIndex=qu,Nn.findKey=$o,Nn.findLast=eo, 75 | Nn.findLastIndex=Pu,Nn.findLastKey=So,Nn.findWhere=function(n,t){return ro(n,bt(t))},Nn.first=Kr,Nn.floor=ni,Nn.get=function(n,t,r){return n=null==n?w:yt(n,Dr(t),t+""),n===w?r:n},Nn.gt=se,Nn.gte=function(n,t){return n>=t},Nn.has=function(n,t){if(null==n)return false;var r=nu.call(n,t);if(!r&&!Wr(t)){if(t=Dr(t),n=1==t.length?n:yt(n,Et(t,0,-1)),null==n)return false;t=Zr(t),r=nu.call(n,t)}return r||Sr(n.length)&&Cr(t,n.length)&&(Oo(n)||pe(n))},Nn.identity=Fe,Nn.includes=ee,Nn.indexOf=Vr,Nn.inRange=function(n,t,r){ 76 | return t=+t||0,r===w?(r=t,t=0):r=+r||0,n>=xu(t,r)&&nr?bu(e+r,0):xu(r||0,e-1))+1;else if(r)return u=Lt(n,t,true)-1,n=n[u],(t===t?t===n:n!==n)?u:-1; 78 | if(t!==t)return p(n,u,true);for(;u--;)if(n[u]===t)return u;return-1},Nn.lt=Ae,Nn.lte=function(n,t){return n<=t},Nn.max=ti,Nn.min=ri,Nn.noConflict=function(){return Zn._=eu,this},Nn.noop=Le,Nn.now=ho,Nn.pad=function(n,t,r){n=u(n),t=+t;var e=n.length;return er?0:+r||0,n.length),n.lastIndexOf(t,r)==r},Nn.sum=function(n,t,r){if(r&&Ur(n,t,r)&&(t=w),t=wr(t,r,3),1==t.length){n=Oo(n)?n:zr(n),r=n.length;for(var e=0;r--;)e+=+t(n[r])||0;n=e}else n=$t(n,t);return n},Nn.template=function(n,t,r){var e=Nn.templateSettings;r&&Ur(n,t,r)&&(t=r=w),n=u(n),t=nt(tt({},r||t),e,Qn),r=nt(tt({},t.imports),e.imports,Qn); 81 | var o,i,f=zo(r),a=Ft(r,f),c=0;r=t.interpolate||Cn;var l="__p+='";r=Ze((t.escape||Cn).source+"|"+r.source+"|"+(r===gn?jn:Cn).source+"|"+(t.evaluate||Cn).source+"|$","g");var p="sourceURL"in t?"//# sourceURL="+t.sourceURL+"\n":"";if(n.replace(r,function(t,r,e,u,f,a){return e||(e=u),l+=n.slice(c,a).replace(Un,s),r&&(o=true,l+="'+__e("+r+")+'"),f&&(i=true,l+="';"+f+";\n__p+='"),e&&(l+="'+((__t=("+e+"))==null?'':__t)+'"),c=a+t.length,t}),l+="';",(t=t.variable)||(l="with(obj){"+l+"}"),l=(i?l.replace(fn,""):l).replace(an,"$1").replace(cn,"$1;"), 82 | l="function("+(t||"obj")+"){"+(t?"":"obj||(obj={});")+"var __t,__p=''"+(o?",__e=_.escape":"")+(i?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}",t=Jo(function(){return qe(f,p+"return "+l).apply(w,a)}),t.source=l,_e(t))throw t;return t},Nn.trim=We,Nn.trimLeft=function(n,t,r){var e=n;return(n=u(n))?n.slice((r?Ur(e,t,r):null==t)?g(n):o(n,t+"")):n},Nn.trimRight=function(n,t,r){var e=n;return(n=u(n))?(r?Ur(e,t,r):null==t)?n.slice(0,y(n)+1):n.slice(0,i(n,t+"")+1):n; 83 | },Nn.trunc=function(n,t,r){r&&Ur(n,t,r)&&(t=w);var e=U;if(r=W,null!=t)if(ge(t)){var o="separator"in t?t.separator:o,e="length"in t?+t.length||0:e;r="omission"in t?u(t.omission):r}else e=+t||0;if(n=u(n),e>=n.length)return n;if(e-=r.length,1>e)return r;if(t=n.slice(0,e),null==o)return t+r;if(we(o)){if(n.slice(e).search(o)){var i,f=n.slice(0,e);for(o.global||(o=Ze(o.source,(kn.exec(o)||"")+"g")),o.lastIndex=0;n=o.exec(f);)i=n.index;t=t.slice(0,null==i?e:i)}}else n.indexOf(o,e)!=e&&(o=t.lastIndexOf(o), 84 | -1u.__dir__?"Right":"")}),u},zn.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),Pn(["filter","map","takeWhile"],function(n,t){ 86 | var r=t+1,e=r!=T;zn.prototype[n]=function(n,t){var u=this.clone();return u.__iteratees__.push({iteratee:wr(n,t,1),type:r}),u.__filtered__=u.__filtered__||e,u}}),Pn(["first","last"],function(n,t){var r="take"+(t?"Right":"");zn.prototype[n]=function(){return this[r](1).value()[0]}}),Pn(["initial","rest"],function(n,t){var r="drop"+(t?"":"Right");zn.prototype[n]=function(){return this.__filtered__?new zn(this):this[r](1)}}),Pn(["pluck","where"],function(n,t){var r=t?"filter":"map",e=t?bt:ze;zn.prototype[n]=function(n){ 87 | return this[r](e(n))}}),zn.prototype.compact=function(){return this.filter(Fe)},zn.prototype.reject=function(n,t){return n=wr(n,t,1),this.filter(function(t){return!n(t)})},zn.prototype.slice=function(n,t){n=null==n?0:+n||0;var r=this;return r.__filtered__&&(0t)?new zn(r):(0>n?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==w&&(t=+t||0,r=0>t?r.dropRight(-t):r.take(t-n)),r)},zn.prototype.takeRightWhile=function(n,t){return this.reverse().takeWhile(n,t).reverse()},zn.prototype.toArray=function(){return this.take(Ru); 88 | },_t(zn.prototype,function(n,t){var r=/^(?:filter|map|reject)|While$/.test(t),e=/^(?:first|last)$/.test(t),u=Nn[e?"take"+("last"==t?"Right":""):t];u&&(Nn.prototype[t]=function(){function t(n){return e&&i?u(n,1)[0]:u.apply(w,Jn([n],o))}var o=e?[1]:arguments,i=this.__chain__,f=this.__wrapped__,a=!!this.__actions__.length,c=f instanceof zn,l=o[0],s=c||Oo(f);return s&&r&&typeof l=="function"&&1!=l.length&&(c=s=false),l={func:ne,args:[t],thisArg:w},a=c&&!a,e&&!i?a?(f=f.clone(),f.__actions__.push(l),n.call(f)):u.call(w,this.value())[0]:!e&&s?(f=a?f:new zn(this), 89 | f=n.apply(f,o),f.__actions__.push(l),new Ln(f,i)):this.thru(t)})}),Pn("join pop push replace shift sort splice split unshift".split(" "),function(n){var t=(/^(?:replace|split)$/.test(n)?He:Je)[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:join|pop|replace|shift)$/.test(n);Nn.prototype[n]=function(){var n=arguments;return e&&!this.__chain__?t.apply(this.value(),n):this[r](function(r){return t.apply(r,n)})}}),_t(zn.prototype,function(n,t){var r=Nn[t];if(r){var e=r.name+"";(Wu[e]||(Wu[e]=[])).push({ 90 | name:t,func:r})}}),Wu[sr(w,A).name]=[{name:"wrapper",func:w}],zn.prototype.clone=function(){var n=new zn(this.__wrapped__);return n.__actions__=qn(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=qn(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=qn(this.__views__),n},zn.prototype.reverse=function(){if(this.__filtered__){var n=new zn(this);n.__dir__=-1,n.__filtered__=true}else n=this.clone(),n.__dir__*=-1;return n},zn.prototype.value=function(){ 91 | var n,t=this.__wrapped__.value(),r=this.__dir__,e=Oo(t),u=0>r,o=e?t.length:0;n=o;for(var i=this.__views__,f=0,a=-1,c=i.length;++a"'`]/g,pn=RegExp(ln.source),hn=RegExp(sn.source),_n=/<%-([\s\S]+?)%>/g,vn=/<%([\s\S]+?)%>/g,gn=/<%=([\s\S]+?)%>/g,yn=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/,dn=/^\w*$/,mn=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,wn=/^[:!,]|[\\^$.*+?()[\]{}|\/]|(^[0-9a-fA-Fnrtuvx])|([\n\r\u2028\u2029])/g,bn=RegExp(wn.source),xn=/[\u0300-\u036f\ufe20-\ufe23]/g,An=/\\(\\)?/g,jn=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,kn=/\w*$/,In=/^0[xX]/,Rn=/^\[object .+?Constructor\]$/,On=/^\d+$/,En=/[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g,Cn=/($^)/,Un=/['\n\r\u2028\u2029\\]/g,Wn=RegExp("[A-Z\\xc0-\\xd6\\xd8-\\xde]+(?=[A-Z\\xc0-\\xd6\\xd8-\\xde][a-z\\xdf-\\xf6\\xf8-\\xff]+)|[A-Z\\xc0-\\xd6\\xd8-\\xde]?[a-z\\xdf-\\xf6\\xf8-\\xff]+|[A-Z\\xc0-\\xd6\\xd8-\\xde]+|[0-9]+","g"),$n="Array ArrayBuffer Date Error Float32Array Float64Array Function Int8Array Int16Array Int32Array Math Number Object RegExp Set String _ clearTimeout isFinite parseFloat parseInt setTimeout TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array WeakMap".split(" "),Sn={}; 94 | Sn[X]=Sn[H]=Sn[Q]=Sn[nn]=Sn[tn]=Sn[rn]=Sn[en]=Sn[un]=Sn[on]=true,Sn[B]=Sn[D]=Sn[J]=Sn[M]=Sn[q]=Sn[P]=Sn[K]=Sn["[object Map]"]=Sn[V]=Sn[Z]=Sn[Y]=Sn["[object Set]"]=Sn[G]=Sn["[object WeakMap]"]=false;var Fn={};Fn[B]=Fn[D]=Fn[J]=Fn[M]=Fn[q]=Fn[X]=Fn[H]=Fn[Q]=Fn[nn]=Fn[tn]=Fn[V]=Fn[Z]=Fn[Y]=Fn[G]=Fn[rn]=Fn[en]=Fn[un]=Fn[on]=true,Fn[P]=Fn[K]=Fn["[object Map]"]=Fn["[object Set]"]=Fn["[object WeakMap]"]=false;var Nn={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a", 95 | "\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y", 96 | "\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss"},Tn={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},Ln={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},zn={"function":true,object:true},Bn={0:"x30",1:"x31",2:"x32",3:"x33",4:"x34",5:"x35",6:"x36",7:"x37",8:"x38",9:"x39",A:"x41",B:"x42",C:"x43",D:"x44",E:"x45",F:"x46",a:"x61",b:"x62",c:"x63",d:"x64",e:"x65",f:"x66",n:"x6e",r:"x72",t:"x74",u:"x75",v:"x76",x:"x78"},Dn={"\\":"\\", 97 | "'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Mn=zn[typeof exports]&&exports&&!exports.nodeType&&exports,qn=zn[typeof module]&&module&&!module.nodeType&&module,Pn=zn[typeof self]&&self&&self.Object&&self,Kn=zn[typeof window]&&window&&window.Object&&window,Vn=qn&&qn.exports===Mn&&Mn,Zn=Mn&&qn&&typeof global=="object"&&global&&global.Object&&global||Kn!==(this&&this.window)&&Kn||Pn||this,Yn=m();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Zn._=Yn, define(function(){ 98 | return Yn})):Mn&&qn?Vn?(qn.exports=Yn)._=Yn:Mn._=Yn:Zn._=Yn}).call(this); -------------------------------------------------------------------------------- /frontend/lib/public/lib/rickshaw.min.css: -------------------------------------------------------------------------------- 1 | .rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} 2 | -------------------------------------------------------------------------------- /frontend/lib/public/lib/rickshaw.min.js: -------------------------------------------------------------------------------- 1 | (function(root,factory){if(typeof define==="function"&&define.amd){define(["d3"],function(d3){return root.Rickshaw=factory(d3)})}else if(typeof exports==="object"){module.exports=factory(require("d3"))}else{root.Rickshaw=factory(d3)}})(this,function(d3){var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i0){var x=s.data[0].x;var y=s.data[0].y;if(typeof x!="number"||typeof y!="number"&&y!==null){throw"x and y properties of points should be numbers instead of "+typeof x+" and "+typeof y}}if(s.data.length>=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.onConfigure=function(callback){this.configureCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){this.config=this.config||{};if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);Rickshaw.keys(this.config).forEach(function(k){this[k]=this.config[k]},this);if("stack"in args)args.unstack=!args.stack;var renderer=args.renderer||this.renderer&&this.renderer.name||"stack";this.setRenderer(renderer,args);this.configureCallbacks.forEach(function(callback){callback(args)})};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getUTCMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getUTCMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="month"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),date.getUTCMonth())/1e3;if(floor==time)return time;year=date.getUTCFullYear();var month=date.getUTCMonth();if(month==11){month=0;year=year+1}else{month+=1}return Date.UTC(year,month)/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),0)/1e3;if(floor==time)return time;year=date.getUTCFullYear()+1;return Date.UTC(year,0)/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="day"){var nearFuture=new Date((time+unit.seconds-1)*1e3);var rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){date=new Date(time*1e3);floor=new Date(date.getFullYear(),date.getMonth()).getTime()/1e3;if(floor==time)return time;year=date.getFullYear();var month=date.getMonth();if(month==11){month=0;year=year+1}else{month+=1}return new Date(year,month).getTime()/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=new Date(date.getUTCFullYear(),0).getTime()/1e3;if(floor==time)return time;year=date.getFullYear()+1;return new Date(year,0).getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this._renderWidth!==undefined&&this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-x-value",this.textContent)});this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this._renderHeight!==undefined&&this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-y-value",this.textContent) 2 | })}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var domain=this.scale.domain();var renderDomain=this.graph.renderer.domain().y;var extents=[Math.min.apply(Math,domain),Math.max.apply(Math,domain)];var extentMap=d3.scale.linear().domain([0,1]).range(extents);var adjExtents=[extentMap(renderDomain[0]),extentMap(renderDomain[1])];var adjustment=d3.scale.linear().domain(extents).range(adjExtents);var adjustedScale=this.scale.copy().domain(domain.map(adjustment)).range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line){if(l===line){if(self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.indexOf(line.series);line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.jQuery=="undefined"){throw"couldn't find jQuery at window.jQuery"}if(typeof window.jQuery.ui=="undefined"){throw"couldn't find jQuery UI at window.jQuery.ui"}jQuery(function(){jQuery(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];jQuery(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});jQuery(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)0){alignables.forEach(function(el){el.classList.remove("left");el.classList.add("right")});var rightAlignError=this._calcLayoutError(alignables);if(rightAlignError>leftAlignError){alignables.forEach(function(el){el.classList.remove("right");el.classList.add("left")})}}if(typeof this.onRender=="function"){this.onRender(args)}},_calcLayoutError:function(alignables){var parentRect=this.element.parentNode.getBoundingClientRect();var error=0;var alignRight=alignables.forEach(function(el){var rect=el.getBoundingClientRect();if(!rect.width){return}if(rect.right>parentRect.right){error+=rect.right-parentRect.right}if(rect.left=self.previewWidth){frameAfterDrag[0]-=frameAfterDrag[1]-self.previewWidth;frameAfterDrag[1]=self.previewWidth}}self.graphs.forEach(function(graph){var domainScale=d3.scale.linear().interpolate(d3.interpolateNumber).domain([0,self.previewWidth]).range(graph.dataDomain());var windowAfterDrag=[domainScale(frameAfterDrag[0]),domainScale(frameAfterDrag[1])];self.slideCallbacks.forEach(function(callback){callback(graph,windowAfterDrag[0],windowAfterDrag[1])});if(frameAfterDrag[0]===0){windowAfterDrag[0]=undefined}if(frameAfterDrag[1]===self.previewWidth){windowAfterDrag[1]=undefined}graph.window.xMin=windowAfterDrag[0];graph.window.xMax=windowAfterDrag[1];graph.update()})}function onMousedown(){drag.target=d3.event.target;drag.start=self._getClientXFromEvent(d3.event,drag);self.frameBeforeDrag=self.currentFrame.slice();d3.event.preventDefault?d3.event.preventDefault():d3.event.returnValue=false;d3.select(document).on("mousemove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("mouseup.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchmove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("touchend.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",onMouseup)}function onMousedownLeftHandle(datum,index){drag.left=true;onMousedown()}function onMousedownRightHandle(datum,index){drag.right=true;onMousedown()}function onMousedownMiddleHandle(datum,index){drag.left=true;drag.right=true;drag.rigid=true;onMousedown()}function onMouseup(datum,index){d3.select(document).on("mousemove.rickshaw_range_slider_preview",null);d3.select(document).on("mouseup.rickshaw_range_slider_preview",null);d3.select(document).on("touchmove.rickshaw_range_slider_preview",null);d3.select(document).on("touchend.rickshaw_range_slider_preview",null);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",null);delete self.frameBeforeDrag;drag.left=false;drag.right=false;drag.rigid=false}element.select("rect.left_handle").on("mousedown",onMousedownLeftHandle);element.select("rect.right_handle").on("mousedown",onMousedownRightHandle);element.select("rect.middle_handle").on("mousedown",onMousedownMiddleHandle);element.select("rect.left_handle").on("touchstart",onMousedownLeftHandle);element.select("rect.right_handle").on("touchstart",onMousedownRightHandle);element.select("rect.middle_handle").on("touchstart",onMousedownMiddleHandle)},_getClientXFromEvent:function(event,drag){switch(event.type){case"touchstart":case"touchmove":var touchList=event.changedTouches;var touch=null;for(var touchIndex=0;touchIndexyMax)yMax=y});if(!series.length)return;if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var pathNodes=vis.selectAll("path.path").data(data).enter().append("svg:path").classed("path",true).attr("d",this.seriesPathFactory());if(this.stroke){var strokeNodes=vis.selectAll("path.stroke").data(data).enter().append("svg:path").classed("stroke",true).attr("d",this.seriesStrokeFactory())}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=pathNodes[0][i];if(this.stroke)series.stroke=strokeNodes[0][i];this._styleSeries(series);i++},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);if(series.className){d3.select(series.path).classed(series.className,true)}if(series.className&&this.stroke){d3.select(series.stroke).classed(series.className,true)}},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x.magnitude(frequentInterval.magnitude)*(1-this.gapSize);return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}});return Rickshaw}); 4 | -------------------------------------------------------------------------------- /frontend/lib/public/lib/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-left: 20px 3 | } 4 | 5 | h2 { 6 | color: #555555; 7 | font-size: 28px; 8 | font-weight: normal; 9 | float: none; 10 | text-shadow: 0 1px 0 #ffffff; 11 | position: relative; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/lib/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebSocket = require('websocket-stream'); 4 | const Eos = require('end-of-stream'); 5 | 6 | 7 | module.exports = function (server) { 8 | let streamCounter = 0; 9 | const streams = {}; 10 | 11 | const emit = (data) => { 12 | const ids = Object.keys(streams); 13 | ids.forEach((id) => { 14 | streams[id].write(JSON.stringify(data), () => {}); 15 | }); 16 | }; 17 | 18 | const handleStream = (stream) => { 19 | stream.id = streamCounter++; 20 | streams[stream.id] = stream; 21 | 22 | Eos(stream, () => { 23 | delete streams[stream.id]; 24 | }); 25 | }; 26 | 27 | 28 | WebSocket.createServer({ server }, handleStream); 29 | 30 | return { emit }; 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/memory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | # check free memory 3 | total=$(free | awk -F' +' '/Mem/{print $3}') 4 | /bin/containerpilot -putmetric "example_frontend_free_memory=$total" 5 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./lib/index.js", 6 | "keywords": [], 7 | "author": "", 8 | "license": "MPL-2.0", 9 | "dependencies": { 10 | "brule": "2.x.x", 11 | "end-of-stream": "1.x.x", 12 | "hapi": "16.x.x", 13 | "inert": "4.x.x", 14 | "piloted": "3.x.x", 15 | "websocket-stream": "3.x.x", 16 | "wreck": "12.x.x" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /local-compose.yml: -------------------------------------------------------------------------------- 1 | consul: 2 | image: autopilotpattern/consul:0.7.3r39 3 | restart: always 4 | dns: 5 | - 127.0.0.1 6 | labels: 7 | - triton.cns.services=consul 8 | ports: 9 | - "8500:8500" 10 | command: > 11 | /usr/local/bin/containerpilot 12 | environment: 13 | - CONSUL_DEV=1 14 | prometheus: 15 | image: autopilotpattern/prometheus:1.7.1-r20 16 | restart: always 17 | ports: 18 | - "9090:9090" 19 | links: 20 | - consul:consul 21 | environment: 22 | - CONSUL=consul 23 | - CONSUL_AGENT=1 24 | dns: 25 | - 127.0.0.1 26 | nats: 27 | image: autopilotpattern/nats:0.9.6-r1.0.0 28 | restart: always 29 | ports: 30 | - 8222 31 | links: 32 | - consul:consul 33 | environment: 34 | - CONSUL=consul 35 | - CONSUL_AGENT=1 36 | - NATS_USER=ruser 37 | - NATS_PASSWORD=password 38 | natsboard: 39 | image: d0cker/natsboard 40 | restart: always 41 | ports: 42 | - "3000:3000" 43 | - "3001:3001" 44 | links: 45 | - consul:consul 46 | environment: 47 | - CONSUL=consul 48 | - CONSUL_AGENT=1 49 | traefik: 50 | build: ./traefik 51 | ports: 52 | - "80:80" 53 | - "8080:8080" 54 | links: 55 | - consul:consul 56 | environment: 57 | - CONSUL=consul 58 | restart: always 59 | influxdb: 60 | image: autopilotpattern/influxdb:1.1.1 61 | ports: 62 | - "8086:8086" 63 | - "8083:8083" 64 | restart: always 65 | links: 66 | - consul:consul 67 | environment: 68 | - CONSUL=consul 69 | - ADMIN_USER=root 70 | - INFLUXDB_INIT_PWD=root123 71 | - INFLUXDB_ADMIN_ENABLED=true 72 | - INFLUXDB_REPORTING_DISABLED=true 73 | - INFLUXDB_DATA_QUERY_LOG_ENABLED=false 74 | - INFLUXDB_HTTP_LOG_ENABLED=false 75 | - INFLUXDB_CONTINUOUS_QUERIES_LOG_ENABLED=false 76 | serializer: 77 | build: ./serializer 78 | links: 79 | - consul:consul 80 | environment: 81 | - PORT=10000 82 | - CONSUL=consul 83 | - INFLUXDB_USER=root 84 | - INFLUXDB_PWD=root123 85 | expose: 86 | - 10000 87 | - 9090 88 | restart: always 89 | frontend: 90 | build: ./frontend 91 | links: 92 | - consul:consul 93 | environment: 94 | - CONSUL=consul 95 | - PORT=10001 96 | expose: 97 | - 10001 98 | - 9090 99 | restart: always 100 | smartthings: 101 | build: ./smartthings 102 | links: 103 | - consul:consul 104 | expose: 105 | - 8080 106 | environment: 107 | - PORT=8080 108 | - CONSUL=consul 109 | - NATS_USER=ruser 110 | - NATS_PASSWORD=password 111 | - FAKE_MODE=true 112 | restart: always 113 | humidity: 114 | build: ./sensor 115 | links: 116 | - consul:consul 117 | environment: 118 | - SENSOR_TYPE=humidity 119 | - CONSUL=consul 120 | - NATS_USER=ruser 121 | - NATS_PASSWORD=password 122 | restart: always 123 | motion: 124 | build: ./sensor 125 | links: 126 | - consul:consul 127 | environment: 128 | - SENSOR_TYPE=motion 129 | - CONSUL=consul 130 | - NATS_USER=ruser 131 | - NATS_PASSWORD=password 132 | restart: always 133 | temperature: 134 | build: ./sensor 135 | links: 136 | - consul:consul 137 | environment: 138 | - SENSOR_TYPE=temperature 139 | - CONSUL=consul 140 | - NATS_USER=ruser 141 | - NATS_PASSWORD=password 142 | restart: always 143 | -------------------------------------------------------------------------------- /project_overview.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/data.plist -------------------------------------------------------------------------------- /project_overview.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image1.pdf -------------------------------------------------------------------------------- /project_overview.graffle/image15.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image15.tiff -------------------------------------------------------------------------------- /project_overview.graffle/image16.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image16.tiff -------------------------------------------------------------------------------- /project_overview.graffle/image17.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image17.tiff -------------------------------------------------------------------------------- /project_overview.graffle/image18.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image18.tiff -------------------------------------------------------------------------------- /project_overview.graffle/image19.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image19.tiff -------------------------------------------------------------------------------- /project_overview.graffle/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image2.png -------------------------------------------------------------------------------- /project_overview.graffle/image22.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image22.tiff -------------------------------------------------------------------------------- /project_overview.graffle/image3.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image3.tiff -------------------------------------------------------------------------------- /project_overview.graffle/image6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.graffle/image6.png -------------------------------------------------------------------------------- /project_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autopilotpattern/nodejs-example/7ca8cc619e87866d6135488387b1e1ffb24db5e9/project_overview.png -------------------------------------------------------------------------------- /sensor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | RUN apk update && \ 4 | apk add curl && \ 5 | rm -rf /var/cache/apk/* 6 | 7 | RUN export CONSUL_VERSION=0.7.0 \ 8 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \ 9 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \ 10 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \ 11 | && unzip /tmp/consul -d /usr/local/bin \ 12 | && rm /tmp/consul.zip \ 13 | && mkdir /config 14 | 15 | # Install ContainerPilot 16 | ENV CONTAINERPILOT_VERSION 3.4.3 17 | RUN export CP_SHA1=e8258ed166bcb3de3e06638936dcc2cae32c7c58 \ 18 | && curl -Lso /tmp/containerpilot.tar.gz \ 19 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \ 20 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \ 21 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \ 22 | && rm /tmp/containerpilot.tar.gz 23 | 24 | # COPY ContainerPilot configuration 25 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5 26 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH} 27 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH} 28 | 29 | # Install our application 30 | RUN mkdir -p /opt/app/lib 31 | COPY package.json /opt/app/ 32 | COPY lib/* /opt/app/lib/ 33 | RUN cd /opt/app && npm install 34 | 35 | CMD ["/bin/containerpilot"] 36 | -------------------------------------------------------------------------------- /sensor/containerpilot.json5: -------------------------------------------------------------------------------- 1 | { 2 | consul: 'localhost:8500', 3 | jobs: [ 4 | { 5 | name: '{{.SENSOR_TYPE}}', 6 | exec: 'node /opt/app/', 7 | restarts: 'unlimited', 8 | health: { 9 | exec: 'pgrep node', 10 | interval: 10, 11 | ttl: 20 12 | } 13 | }, 14 | { 15 | name: 'consul-agent', 16 | exec: ['/usr/local/bin/consul', 'agent', 17 | '-data-dir=/data', 18 | '-config-dir=/config', 19 | '-log-level=err', 20 | '-rejoin', 21 | '-retry-join', '{{ .CONSUL | default "consul" }}', 22 | '-retry-max', '10', 23 | '-retry-interval', '10s'], 24 | restarts: 'unlimited' 25 | }, 26 | { 27 | name: 'onchange-serializer', 28 | exec: 'pkill -SIGHUP node', 29 | when: { 30 | source: 'watch.serializer', 31 | each: 'changed' 32 | } 33 | }, 34 | { 35 | name: 'onchange-nats', 36 | exec: 'pkill -SIGHUP node', 37 | when: { 38 | source: 'watch.nats', 39 | each: 'changed' 40 | } 41 | } 42 | ], 43 | telemetry: { 44 | port: 9090, 45 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet'] 46 | }, 47 | watches: [ 48 | { 49 | name: 'serializer', 50 | interval: 1 51 | }, 52 | { 53 | name: 'nats', 54 | interval: 3 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /sensor/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Nats = require('nats'); 6 | const Piloted = require('piloted'); 7 | const Wreck = require('wreck'); 8 | 9 | 10 | const internals = { 11 | type: process.env.SENSOR_TYPE 12 | }; 13 | 14 | function setupNats() { 15 | const servers = Piloted.serviceHosts('nats'); 16 | 17 | if (!servers || !servers.length) { 18 | console.error('NATS not found'); 19 | return setTimeout(() => { setupNats(); }, 1000); 20 | } 21 | 22 | const natsServers = servers.map((server) => { 23 | return `nats://${process.env.NATS_USER}:${process.env.NATS_PASSWORD}@${server.address}:4222`; 24 | }); 25 | 26 | const nats = Nats.connect({ servers: natsServers }); 27 | nats.on('error', (err) => { 28 | console.log(err); 29 | }); 30 | 31 | // Subscribe for messages related to the sensor type subject 32 | // and create a queue group so that multiple instances don't 33 | // handle the same message 34 | nats.subscribe(internals.type, { queue: 'sensor' }, writeData); 35 | } 36 | 37 | function writeData (data) { 38 | const serializer = Piloted.service('serializer'); 39 | 40 | if (!serializer) { 41 | console.error('Serializer not found'); 42 | return setTimeout(() => { writeData(data); }, 1000); 43 | } 44 | 45 | Wreck.post(`http://${serializer.address}:${serializer.port}/write/${internals.type}`, { payload: data }, (err) => { 46 | if (err) { 47 | console.error(err); 48 | } 49 | }); 50 | } 51 | 52 | Piloted.on('refresh', () => { 53 | setupNats(); 54 | }); 55 | 56 | setupNats(); 57 | -------------------------------------------------------------------------------- /sensor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "temperature", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "author": "", 7 | "license": "MPL-2.0", 8 | "dependencies": { 9 | "nats": "0.7.x", 10 | "piloted": "3.x.x", 11 | "wreck": "12.x.x" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /serializer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | RUN apk update && \ 4 | apk add curl && \ 5 | rm -rf /var/cache/apk/* 6 | 7 | RUN export CONSUL_VERSION=0.7.0 \ 8 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \ 9 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \ 10 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \ 11 | && unzip /tmp/consul -d /usr/local/bin \ 12 | && rm /tmp/consul.zip \ 13 | && mkdir /config 14 | 15 | # Install ContainerPilot 16 | ENV CONTAINERPILOT_VERSION 3.4.2 17 | RUN export CP_SHA1=5c99ae9ede01e8fcb9b027b5b3cb0cfd8c0b8b88 \ 18 | && curl -Lso /tmp/containerpilot.tar.gz \ 19 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \ 20 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \ 21 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \ 22 | && rm /tmp/containerpilot.tar.gz 23 | 24 | # COPY ContainerPilot configuration 25 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5 26 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH} 27 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH} 28 | 29 | RUN mkdir -p /bin/sensors 30 | COPY sensors/* /bin/sensors/ 31 | RUN chmod 755 /bin/sensors/*.sh 32 | RUN mkdir -p /var/log/app 33 | 34 | # Install our application 35 | RUN mkdir -p /opt/app/lib 36 | COPY package.json /opt/app/ 37 | COPY lib/* /opt/app/lib/ 38 | RUN cd /opt/app && npm install 39 | 40 | CMD ["/bin/containerpilot"] 41 | -------------------------------------------------------------------------------- /serializer/containerpilot.json5: -------------------------------------------------------------------------------- 1 | { 2 | consul: 'localhost:8500', 3 | jobs: [ 4 | { 5 | name: 'serializer', 6 | port: {{.PORT}}, 7 | exec: 'node /opt/app/', 8 | health: { 9 | exec: '/usr/bin/curl -o /dev/null --fail -s http://127.0.0.1:{{.PORT}}/check-it-out', 10 | interval: 1, 11 | ttl: 2 12 | }, 13 | restarts: 'unlimited' 14 | }, 15 | { 16 | name: 'consul-agent', 17 | exec: ['/usr/local/bin/consul', 'agent', 18 | '-data-dir=/data', 19 | '-config-dir=/config', 20 | '-log-level=err', 21 | '-rejoin', 22 | '-retry-join', '{{ .CONSUL | default "consul" }}', 23 | '-retry-max', '10', 24 | '-retry-interval', '10s'], 25 | restarts: 'unlimited' 26 | }, 27 | { 28 | name: 'onchange-influxdb', 29 | exec: 'pkill -SIGHUP node', 30 | when: { 31 | source: 'watch.influxdb', 32 | each: 'changed' 33 | } 34 | }, 35 | { 36 | name: 'requests-sensor', 37 | exec: '/bin/sensors/request_count.sh', 38 | timeout: '5s', 39 | when: { 40 | interval: '5s' 41 | }, 42 | restarts: 'unlimited' 43 | } 44 | ], 45 | watches: [ 46 | { 47 | name: 'influxdb', 48 | interval: 2 49 | } 50 | ], 51 | telemetry: { 52 | port: 9090, 53 | tags: ['op'], 54 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet'], 55 | metrics: [ 56 | { 57 | namespace: 'example', 58 | subsystem: 'serializer', 59 | name: 'requests', 60 | help: 'Number of requests/5s', 61 | type: 'gauge' 62 | }, 63 | { 64 | namespace: 'example', 65 | subsystem: 'process', 66 | name: 'event_delay', 67 | help: 'Node.js event loop delay', 68 | type: 'gauge' 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /serializer/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Brule = require('brule'); 4 | const FlattenDeep = require('lodash.flattendeep'); 5 | const Good = require('good'); 6 | const Hapi = require('hapi'); 7 | const Influx = require('influx'); 8 | const Items = require('items'); 9 | const Piloted = require('piloted'); 10 | const Toppsy = require('toppsy'); 11 | 12 | 13 | const internals = { 14 | dbName: 'sensors', 15 | failCount: 0 16 | }; 17 | 18 | const bufferedDb = { 19 | draining: false, 20 | data: [], 21 | writePoints: function (points) { 22 | console.log('Write in dummy db'); 23 | bufferedDb.data = bufferedDb.data.concat(points); 24 | return new Promise((resolve) => { 25 | resolve(); 26 | }); 27 | }, 28 | query: function(query) { 29 | return new Promise((resolve) => { 30 | resolve(bufferedDb.data); 31 | }); 32 | }, 33 | drain: function () { 34 | if (bufferedDb.draining) { 35 | return; 36 | } 37 | 38 | bufferedDb.draining = true; 39 | Items.serial(bufferedDb.data, (data, next) => { 40 | writePoint(data.measurement, data.fields.value, next); 41 | }, (err) => { 42 | bufferedDb.data = []; 43 | bufferedDb.draining = false; 44 | }); 45 | } 46 | }; 47 | 48 | 49 | function main () { 50 | setupDb(); 51 | setupHapi(); 52 | } 53 | main(); 54 | 55 | 56 | function setupHapi () { 57 | const server = new Hapi.Server({ 58 | load: { 59 | sampleInterval: 100 60 | } 61 | }); 62 | 63 | server.connection({ 64 | port: process.env.PORT, 65 | load: { 66 | maxEventLoopDelay: 100 // 50 milliseconds 67 | } 68 | }); 69 | 70 | const goodOptions = { 71 | reporters: { 72 | fileReporter: [{ 73 | module: 'good-squeeze', 74 | name: 'Squeeze', 75 | args: [{ response: '*' }] 76 | }, { 77 | module: 'good-squeeze', 78 | name: 'SafeJson' 79 | }, { 80 | module: 'good-file', 81 | args: ['/var/log/app/requests.log'] 82 | }] 83 | } 84 | }; 85 | 86 | 87 | server.register([ 88 | Brule, 89 | { register: Good, options: goodOptions }, 90 | { register: Toppsy, options: { namespace: 'example' } } 91 | ], (err) => { 92 | if (err) { 93 | console.error(err); 94 | process.exit(1); 95 | } 96 | 97 | server.route({ 98 | method: 'POST', 99 | path: '/write/{type}', 100 | handler: writeHandler 101 | }); 102 | 103 | server.route({ 104 | method: 'GET', 105 | path: '/read', 106 | handler: readHandler 107 | }); 108 | 109 | server.start((err) => { 110 | if (err) { 111 | console.error(err); 112 | process.exit(1); 113 | } 114 | 115 | console.log(`Hapi server started at http://localhost:${server.info.port}`); 116 | }); 117 | }); 118 | } 119 | 120 | function writeHandler (request, reply) { 121 | writePoint(request.params.type, request.payload.value, (err) => { 122 | reply({ err }); 123 | }); 124 | } 125 | 126 | function readHandler (request, reply) { 127 | let results = []; 128 | Items.parallel(['motion', 'humidity', 'temperature'], (type, next) => { 129 | readPoints(type, 1, (err, result) => { 130 | results = results.concat(result); 131 | next(); 132 | }); 133 | }, () => { 134 | reply(FlattenDeep(results)); 135 | }); 136 | } 137 | 138 | function setupDb () { 139 | const influxServer = Piloted.service('influxdb'); 140 | if (!influxServer && internals.failCount > 10) { 141 | internals.failCount = 0; 142 | Piloted.refresh(); 143 | } 144 | 145 | if (!influxServer) { 146 | internals.failCount++; 147 | internals.db = bufferedDb; 148 | return setTimeout(setupDb, 1000); 149 | } 150 | 151 | internals.db = new Influx.InfluxDB({ 152 | host: influxServer.address, 153 | port: influxServer.port, 154 | username: process.env.INFLUXDB_USER, 155 | password: process.env.INFLUXDB_PWD 156 | }); 157 | 158 | internals.db.createDatabase(internals.dbName) 159 | .then(() => { 160 | bufferedDb.drain(); 161 | }) 162 | .catch((err) => { 163 | console.error(`Error creating Influx database!`); 164 | console.error(err); 165 | 166 | // Influx may not be entirely ready 167 | setTimeout(setupDb, 1000); 168 | }); 169 | } 170 | 171 | Piloted.on('refresh', () => { 172 | setupDb(); 173 | }); 174 | 175 | 176 | function writePoint (type, value, cb) { 177 | internals.db.writePoints([{ measurement: type, fields: { value } }], { database: internals.dbName }) 178 | .then(() => { 179 | return cb(); 180 | }) 181 | .catch((err) => { 182 | return cb(err); 183 | }); 184 | }; 185 | 186 | function readPoints (type, ago, cb) { 187 | ago = ago || 1; 188 | const query = `select * from ${type} where time > now() - ${ago}m`; 189 | internals.db.query(query, { database: internals.dbName }) 190 | .then((rows) => { 191 | if (rows && rows.length) { 192 | for (let i = 0; i < rows.length; ++i) { 193 | rows[i].type = type; 194 | } 195 | } 196 | cb(null, rows); 197 | }) 198 | .catch((err) => { 199 | console.error('Error querying db: ' + err); 200 | return cb(err); 201 | }); 202 | }; 203 | -------------------------------------------------------------------------------- /serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "author": "", 7 | "license": "MPL-2.0", 8 | "dependencies": { 9 | "brule": "2.x.x", 10 | "good": "7.x.x", 11 | "good-file": "6.x.x", 12 | "good-squeeze": "5.x.x", 13 | "hapi": "16.x.x", 14 | "influx": "5.x.x", 15 | "items": "2.x.x", 16 | "lodash.flattendeep": "4.x.x", 17 | "piloted": "3.x.x", 18 | "toppsy": "^1.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /serializer/sensors/request_count.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | 3 | FILE=/var/log/app/requests.log 4 | count=$(wc -l $FILE | awk '{printf $1}') 5 | truncate -s 0 $FILE 6 | ./bin/containerpilot -putmetric "example_serializer_requests=$count" 7 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -o pipefail 3 | 4 | help() { 5 | echo 6 | echo 'Usage ./setup.sh' 7 | echo 8 | echo 'Checks that your Triton and Docker environment is sane and configures' 9 | echo 'an environment file to use.' 10 | echo 11 | } 12 | 13 | 14 | # populated by `check` function whenever we're using Triton 15 | TRITON_USER= 16 | TRITON_DC= 17 | TRITON_ACCOUNT= 18 | 19 | # --------------------------------------------------- 20 | # Top-level commands 21 | 22 | # Check for correct configuration and setup _env file 23 | envcheck() { 24 | 25 | command -v docker >/dev/null 2>&1 || { 26 | echo 27 | tput rev # reverse 28 | tput bold # bold 29 | echo 'Docker is required, but does not appear to be installed.' 30 | tput sgr0 # clear 31 | echo 'See https://docs.joyent.com/public-cloud/api-access/docker' 32 | exit 1 33 | } 34 | command -v json >/dev/null 2>&1 || { 35 | echo 36 | tput rev # reverse 37 | tput bold # bold 38 | echo 'Error! JSON CLI tool is required, but does not appear to be installed.' 39 | tput sgr0 # clear 40 | echo 'See https://apidocs.joyent.com/cloudapi/#getting-started' 41 | exit 1 42 | } 43 | 44 | command -v triton >/dev/null 2>&1 || { 45 | echo 46 | tput rev # reverse 47 | tput bold # bold 48 | echo 'Error! Joyent Triton CLI is required, but does not appear to be installed.' 49 | tput sgr0 # clear 50 | echo 'See https://www.joyent.com/blog/introducing-the-triton-command-line-tool' 51 | exit 1 52 | } 53 | 54 | # make sure Docker client is pointed to the same place as the Triton client 55 | local docker_user=$(docker info 2>&1 | awk -F": " '/SDCAccount:/{print $2}') 56 | local docker_dc=$(echo $DOCKER_HOST | awk -F"/" '{print $3}' | awk -F'.' '{print $1}') 57 | TRITON_USER=$(triton profile get | awk -F": " '/account:/{print $2}') 58 | TRITON_DC=$(triton profile get | awk -F"/" '/url:/{print $3}' | awk -F'.' '{print $1}') 59 | TRITON_ACCOUNT=$(triton account get | awk -F": " '/id:/{print $2}') 60 | 61 | local triton_cns_enabled=$(triton account get | awk -F": " '/cns/{print $2}') 62 | if [ ! "true" == "$triton_cns_enabled" ]; then 63 | echo 64 | tput rev # reverse 65 | tput bold # bold 66 | echo 'Error! Triton CNS is required and not enabled.' 67 | tput sgr0 # clear 68 | echo 69 | exit 1 70 | fi 71 | 72 | # setup environment file 73 | if [ ! -f "_env" ]; then 74 | echo 'CONSUL_AGENT=1' > _env 75 | echo 'LOG_LEVEL=INFO' >> _env 76 | echo 'PORT=80' >> _env 77 | echo >> _env 78 | 79 | set +o pipefail 80 | 81 | echo '# Consul discovery via Triton CNS' >> _env 82 | echo CONSUL=consul.svc.${TRITON_ACCOUNT}.${TRITON_DC}.cns.joyent.com >> _env 83 | echo >> _env 84 | 85 | echo '# NATS auth settings' >> _env 86 | echo 'NATS_USER=ruser' >> _env 87 | echo 'NATS_PASSWORD='$(cat /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 7) >> _env 88 | echo >> _env 89 | 90 | echo 'Edit the _env file with your desired config' 91 | else 92 | echo 'Existing _env file found, exiting' 93 | exit 94 | fi 95 | } 96 | 97 | get_root_password() { 98 | echo $(docker logs ${COMPOSE_PROJECT_NAME:-influxdb}_influxdb_1 2>&1 | \ 99 | awk '/Generated root password/{print $NF}' | \ 100 | awk '{$1=$1};1' 101 | ) | pbcopy 102 | } 103 | 104 | 105 | 106 | # --------------------------------------------------- 107 | # parse arguments 108 | 109 | # Get function list 110 | funcs=($(declare -F -p | cut -d " " -f 3)) 111 | 112 | until 113 | if [ ! -z "$1" ]; then 114 | # check if the first arg is a function in this file, or use a default 115 | if [[ " ${funcs[@]} " =~ " $1 " ]]; then 116 | cmd=$1 117 | shift 1 118 | else 119 | cmd="envcheck" 120 | fi 121 | 122 | $cmd "$@" 123 | if [ $? == 127 ]; then 124 | help 125 | fi 126 | 127 | exit 128 | else 129 | envcheck 130 | fi 131 | do 132 | echo 133 | done 134 | -------------------------------------------------------------------------------- /smartthings/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | RUN apk update && \ 4 | apk add curl && \ 5 | rm -rf /var/cache/apk/* 6 | 7 | RUN export CONSUL_VERSION=0.7.0 \ 8 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \ 9 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \ 10 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \ 11 | && unzip /tmp/consul -d /usr/local/bin \ 12 | && rm /tmp/consul.zip \ 13 | && mkdir /config 14 | 15 | # Install ContainerPilot 16 | ENV CONTAINERPILOT_VERSION 3.4.3 17 | RUN export CP_SHA1=e8258ed166bcb3de3e06638936dcc2cae32c7c58 \ 18 | && curl -Lso /tmp/containerpilot.tar.gz \ 19 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \ 20 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \ 21 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \ 22 | && rm /tmp/containerpilot.tar.gz 23 | 24 | # COPY ContainerPilot configuration 25 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5 26 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH} 27 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH} 28 | 29 | # Install our application 30 | RUN mkdir -p /opt/app/lib 31 | COPY package.json /opt/app/ 32 | COPY health.sh /opt/app/ 33 | RUN chmod 755 /opt/app/health.sh 34 | COPY lib/* /opt/app/lib/ 35 | RUN cd /opt/app && npm install 36 | 37 | CMD ["/bin/containerpilot"] 38 | -------------------------------------------------------------------------------- /smartthings/containerpilot.json5: -------------------------------------------------------------------------------- 1 | { 2 | consul: 'localhost:8500', 3 | jobs: [ 4 | { 5 | name: 'smartthings', 6 | port: {{.PORT}}, 7 | exec: 'node /opt/app/', 8 | health: { 9 | exec: '/opt/app/health.sh', 10 | interval: 5, 11 | ttl: 30 12 | }, 13 | restarts: 'unlimited' 14 | }, 15 | { 16 | name: 'consul-agent', 17 | exec: ['/usr/local/bin/consul', 'agent', 18 | '-data-dir=/data', 19 | '-config-dir=/config', 20 | '-log-level=err', 21 | '-rejoin', 22 | '-retry-join', '{{ .CONSUL | default "consul" }}', 23 | '-retry-max', '10', 24 | '-retry-interval', '10s'], 25 | restarts: 'unlimited' 26 | }, 27 | { 28 | name: 'onchange-nats', 29 | exec: 'pkill -SIGHUP node', 30 | when: { 31 | source: 'watch.nats', 32 | each: 'changed' 33 | } 34 | } 35 | ], 36 | telemetry: { 37 | port: 9090, 38 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet'] 39 | }, 40 | watches: [ 41 | { 42 | name: 'nats', 43 | interval: 3 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /smartthings/health.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | 3 | /usr/bin/curl -o /dev/null --fail -s -H 'Content-Type: application/json' -d '{ "role": "seneca", "cmd": "stats" }' http://localhost:$PORT/act 4 | -------------------------------------------------------------------------------- /smartthings/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Nats = require('nats'); 4 | const Piloted = require('piloted'); 5 | const Seneca = require('seneca'); 6 | 7 | 8 | const internals = {}; 9 | 10 | function setupSeneca() { 11 | const seneca = Seneca(); 12 | const passcode = process.env.PASSCODE || 'secure'; 13 | 14 | seneca.add({ role: 'smartthings', cmd: 'write', type: 'humidity', passcode }, (args, cb) => { 15 | if (!internals.nats) { 16 | return cb(); 17 | } 18 | 19 | internals.nats.publish(type, JSON.stringify({ 20 | type: 'humidity', 21 | time: Date.now(), 22 | value: args.value 23 | })); 24 | cb(); 25 | }); 26 | 27 | seneca.add({ role: 'smartthings', cmd: 'write', type: 'motion', passcode }, (args, cb) => { 28 | if (!internals.nats) { 29 | return cb(); 30 | } 31 | 32 | internals.nats.publish(type, JSON.stringify({ 33 | type: 'motion', 34 | time: Date.now(), 35 | value: args.value 36 | })); 37 | cb(); 38 | }); 39 | 40 | seneca.add({ role: 'smartthings', cmd: 'write', type: 'temperature', passcode }, (args, cb) => { 41 | if (!internals.nats) { 42 | return cb(); 43 | } 44 | internals.nats.publish(type, JSON.stringify({ 45 | type: 'temperature', 46 | time: Date.now(), 47 | value: args.value 48 | })); 49 | cb(); 50 | }); 51 | 52 | seneca.listen({ port: process.env.PORT }); 53 | } 54 | 55 | function setupNats(cb) { 56 | cb = cb || function () {}; 57 | const servers = Piloted.serviceHosts('nats'); 58 | 59 | if (!servers || !servers.length) { 60 | console.error('NATS not found'); 61 | return setTimeout(() => { setupNats(cb); }, 1000); 62 | } 63 | 64 | const natsServers = servers.map((server) => { 65 | return `nats://${process.env.NATS_USER}:${process.env.NATS_PASSWORD}@${server.address}:4222`; 66 | }); 67 | 68 | internals.nats = Nats.connect({ servers: natsServers }); 69 | internals.nats.on('error', (err) => { 70 | console.log(err); 71 | }); 72 | cb(); 73 | } 74 | 75 | function genPoint (type) { 76 | return JSON.stringify({ 77 | type, 78 | time: Date.now(), 79 | value: (type === 'motion') ? Math.floor(Math.random() * 2) : Math.floor(Math.random() * 100) 80 | }); 81 | } 82 | 83 | function genData () { 84 | internals.nats.publish('temperature', genPoint('temperature')); 85 | internals.nats.publish('humidity', genPoint('humidity')); 86 | internals.nats.publish('motion', genPoint('motion')); 87 | } 88 | 89 | Piloted.on('refresh', () => { 90 | setupNats(); 91 | }); 92 | 93 | setupSeneca(); 94 | setupNats(function () { 95 | if (process.env.FAKE_MODE) { 96 | setInterval(genData, 1000); 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /smartthings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smartthings-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "author": "", 7 | "license": "MPL-2.0", 8 | "dependencies": { 9 | "nats": "0.7.x", 10 | "piloted": "3.x.x", 11 | "seneca": "3.x.x" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /smartthings/sensor.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Sensor Reporter", 3 | namespace: "demo.sensor", 4 | author: "Demo", 5 | description: "Send sensor readings some place", 6 | category: "SmartThings Labs", 7 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 8 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 9 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 10 | 11 | 12 | preferences { 13 | section("Select sensor"){ 14 | input "sensor", "capability.temperatureMeasurement", title: "Sensor" 15 | } 16 | section("Set reporting URL"){ 17 | input "reportURL", "string", title: "Report URL" 18 | } 19 | section("Set reporting passcode"){ 20 | input "reportPasscode", "string", title: "Report Passcode" 21 | } 22 | } 23 | 24 | def installed() { 25 | log.debug "Installed with settings: ${settings}" 26 | 27 | initialize() 28 | } 29 | 30 | def updated() { 31 | log.debug "Updated with settings: ${settings}" 32 | 33 | unsubscribe() 34 | initialize() 35 | } 36 | 37 | def initialize() { 38 | subscribe(sensor, "humidity", humidityHandler) 39 | subscribe(sensor, "motion.active", motionActiveHandler) 40 | subscribe(sensor, "motion.inactive", motionInactiveHandler) 41 | subscribe(sensor, "temperature", temperatureHandler) 42 | } 43 | 44 | def humidityHandler(evt) 45 | { 46 | reportHumidity(evt.doubleValue) 47 | } 48 | 49 | def motionActiveHandler(evt) 50 | { 51 | log.debug evt.value 52 | reportActiveMotion() 53 | } 54 | 55 | def motionInactiveHandler(evt) 56 | { 57 | log.debug evt.value 58 | reportInactiveMotion() 59 | } 60 | 61 | def temperatureHandler(evt) 62 | { 63 | reportTemperature(evt.doubleValue) 64 | } 65 | 66 | def reportHumidity(humidity) 67 | { 68 | def params = [ 69 | role: "smartthings", 70 | cmd: "write", 71 | type: "humidity", 72 | value: humidity, 73 | passcode: reportPasscode 74 | ] 75 | 76 | try { 77 | httpPostJson(reportURL, params) { resp -> 78 | resp.headers.each { 79 | log.debug "${it.name} : ${it.value}" 80 | } 81 | log.debug "response contentType: ${resp.contentType}" 82 | log.debug "response data: ${resp.data}" 83 | } 84 | } catch (e) { 85 | log.error "something went wrong sending report: $e" 86 | } 87 | } 88 | 89 | def reportActiveMotion() 90 | { 91 | def params = [ 92 | role: "smartthings", 93 | cmd: "write", 94 | type: "motion", 95 | value: 1, 96 | passcode: reportPasscode 97 | ] 98 | 99 | try { 100 | httpPostJson(reportURL, params) { resp -> 101 | resp.headers.each { 102 | log.debug "${it.name} : ${it.value}" 103 | } 104 | log.debug "response contentType: ${resp.contentType}" 105 | log.debug "response data: ${resp.data}" 106 | } 107 | } catch (e) { 108 | log.error "something went wrong sending report: $e" 109 | } 110 | } 111 | 112 | def reportInactiveMotion() 113 | { 114 | def params = [ 115 | role: "smartthings", 116 | cmd: "write", 117 | type: "motion", 118 | value: 0, 119 | passcode: reportPasscode 120 | ] 121 | 122 | try { 123 | httpPostJson(reportURL, params) { resp -> 124 | resp.headers.each { 125 | log.debug "${it.name} : ${it.value}" 126 | } 127 | log.debug "response contentType: ${resp.contentType}" 128 | log.debug "response data: ${resp.data}" 129 | } 130 | } catch (e) { 131 | log.error "something went wrong sending report: $e" 132 | } 133 | } 134 | 135 | def reportTemperature(temperature) 136 | { 137 | def params = [ 138 | role: "smartthings", 139 | cmd: "write", 140 | type: "temperature", 141 | value: temperature, 142 | passcode: reportPasscode 143 | ] 144 | 145 | try { 146 | httpPostJson(reportURL, params) { resp -> 147 | resp.headers.each { 148 | log.debug "${it.name} : ${it.value}" 149 | } 150 | log.debug "response contentType: ${resp.contentType}" 151 | log.debug "response data: ${resp.data}" 152 | } 153 | } catch (e) { 154 | log.error "something went wrong sending report: $e" 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /traefik/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM traefik:1.4-alpine 2 | 3 | RUN apk update && \ 4 | apk add curl unzip tar && \ 5 | rm -rf /var/cache/apk/* 6 | 7 | # Install Consul Agent 8 | RUN export CONSUL_VERSION=0.7.0 \ 9 | && export CONSUL_CHECKSUM=b350591af10d7d23514ebaa0565638539900cdb3aaa048f077217c4c46653dd8 \ 10 | && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \ 11 | && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \ 12 | && unzip /tmp/consul -d /usr/local/bin \ 13 | && rm /tmp/consul.zip \ 14 | && mkdir /config 15 | 16 | # Install ContainerPilot 17 | ENV CONTAINERPILOT_VERSION 3.4.3 18 | RUN export CP_SHA1=e8258ed166bcb3de3e06638936dcc2cae32c7c58 \ 19 | && curl -Lso /tmp/containerpilot.tar.gz \ 20 | "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \ 21 | && echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \ 22 | && tar zxf /tmp/containerpilot.tar.gz -C /bin \ 23 | && rm /tmp/containerpilot.tar.gz 24 | 25 | # Copy the configurations 26 | COPY traefik.toml /etc/traefik/traefik.toml 27 | ENV CONTAINERPILOT_PATH=/etc/containerpilot.json5 28 | COPY containerpilot.json5 ${CONTAINERPILOT_PATH} 29 | ENV CONTAINERPILOT=${CONTAINERPILOT_PATH} 30 | 31 | ENTRYPOINT [] 32 | CMD ["/bin/containerpilot"] 33 | -------------------------------------------------------------------------------- /traefik/containerpilot.json5: -------------------------------------------------------------------------------- 1 | { 2 | consul: 'localhost:8500', 3 | jobs: [ 4 | { 5 | name: 'traefik', 6 | exec: 'traefik', 7 | port: 8080, 8 | health: { 9 | exec: '/usr/bin/curl -o /dev/null --fail -s http://localhost:8080/', 10 | interval: 2, 11 | ttl: 5 12 | }, 13 | restarts: 'unlimited' 14 | }, 15 | { 16 | name: 'consul-agent', 17 | exec: ['/usr/local/bin/consul', 'agent', 18 | '-data-dir=/data', 19 | '-config-dir=/config', 20 | '-log-level=err', 21 | '-rejoin', 22 | '-retry-join', '{{ .CONSUL | default "consul" }}', 23 | '-retry-max', '10', 24 | '-retry-interval', '10s'], 25 | restarts: 'unlimited' 26 | } 27 | ], 28 | telemetry: { 29 | port: 9090, 30 | interfaces: ['eth1', 'eth0', 'eth0[1]', 'lo', 'lo0', 'inet'] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /traefik/traefik.toml: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Consul Catalog configuration backend 3 | ################################################################ 4 | 5 | # Enable Consul Catalog configuration backend 6 | # 7 | # Optional 8 | # 9 | [consulCatalog] 10 | 11 | # Consul server endpoint 12 | # 13 | # Required 14 | # 15 | endpoint = "localhost:8500" 16 | 17 | # Prefix for Consul catalog tags 18 | # 19 | # Optional 20 | # 21 | prefix = "traefik" 22 | 23 | [web] 24 | address = ":8080" 25 | --------------------------------------------------------------------------------