diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..eb79bfdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +*.log +*/linuxDash.min.js.map +temp diff --git a/README.md b/README.md index e2cecfcb..38a37cfe 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,114 @@ -linux-dash -========== +

+ + + +

-A low-overhead monitoring web dashboard for a linux machine. Simply drop-in the app and go! +

+ v2.0
+ A simple & low-overhead web dashboard for linux systems +

- View Demo +

+ + Demo  |  + + Docs + + +

+

+ + linux-dash Gitter chat + +

- +
-

Installation

-
    -
  1. Download the zip/repo/package
  2. -
  3. Place it in /var/www/ (for Apache)
  4. -
  5. Secure access to the page via .htaccess or method of your choice
  6. -
+## Features +* **Small** ----- Under 400KB on disk _(with .git removed)!_ +* **Simple** ---- A minimalist, beautiful dashboard +* **Easy** ------ Drop-in installation +* **Versatile** -- Choose your stack from Node.js, Go, Python, PHP -Please note: If you would like to limit access to the webpage, please add .htaccess or other security measure. +## Installation -

Support

+### Step 1 +```sh +## 1. clone the repo +git clone --depth 1 https://github.com/afaqurk/linux-dash.git -The information listed here is currently limited and will expand shortly. +## 2. go to the cloned directory +cd linux-dash/app/server - +``` +OR, if you prefer to download manually: + +```sh +## 1. Download the .zip +curl -LOk https://github.com/afaqurk/linux-dash/archive/master.zip && unzip master.zip + +## 2. navigate to downloaded & unzipped dir +cd linux-dash-master/app/server + +``` + +### Step 2 + +See instructions for preferred server linux-dash server _(all included)_: + +* [Node.js](#if-using-nodejs) _(recommended)_ +* [Go](#if-using-go) +* [Python](#if-using-python) +* [PHP](#if-using-php) + +#### If Using Node.js +```sh +## install dependencies +npm install --production + +## start linux-dash (on port 80 by default; may require sudo) +## You may change this with the `LINUX_DASH_SERVER_PORT` environment variable (eg. `LINUX_DASH_SERVER_PORT=8080 node server`) +## or provide a --port flag to the command below +## Additionally, the server will listen on every network interface (`0.0.0.0`). +## You may change this with the `LINUX_DASH_SERVER_HOST` environment variable (eg. `LINUX_DASH_SERVER_HOST=127.0.0.1 node server`) +## or provide a --host flag to the command below +node index.js + +``` + +#### If Using Go +```sh +## start the server (on port 80 by default; may require sudo) +go run index.go +``` + +To build a binary, run `go build && ./server -h`. See [@tehbilly](https://github.com/sergeifilippov)'s notes [here](https://github.com/afaqurk/linux-dash/pull/281) for binary usage options + +#### If Using Python +```sh +# Start the server (on port 80 by default; may require sudo). +python index.py +``` + +#### If Using PHP + +1. Make sure you have the `exec`, `shell_exec`, and `escapeshellarg` functions enabled +2. Point your web server to `app/` directory under `linux-dash` +2. Restart your web server (Apache, nginx, etc.) + - For PHP + Apache setup follow the [Digital Ocean tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-linux-dash-on-ubuntu-14-04). + - For help with nginx setup, see [this gist](https://gist.github.com/sergeifilippov/8909839) by [@sergeifilippov](https://github.com/sergeifilippov). + +## Support + +For general help, please use the [Gitter chat room](https://gitter.im/afaqurk/linux-dash). + +## Security + +**It is strongly recommended** that all linux-dash installations be protected via a security measure of your choice. + +Linux Dash does not provide any security or authentication features. diff --git a/app/index.html b/app/index.html new file mode 100644 index 00000000..e186d792 --- /dev/null +++ b/app/index.html @@ -0,0 +1,27 @@ + + + + + Linux Dash : Simple, beautiful server monitoring web dashboard + + + + + + + + + + + + + + +
+ + + + + diff --git a/app/linuxDash.min.css b/app/linuxDash.min.css new file mode 100644 index 00000000..48132949 --- /dev/null +++ b/app/linuxDash.min.css @@ -0,0 +1,10 @@ +button{outline:0} +html{margin-top:0;padding-top:0;-webkit-transition:all 1s ease;-moz-transition:all 1s ease;-ms-transition:all 1s ease;-o-transition:all 1s ease;transition:all 1s ease}body{letter-spacing:.1rem;font-family:Merriweather,Arial,sans-serif;padding:0;margin:0}.centered{margin:0 auto} +#plugins{padding-top:0;margin-top:0;border:1px;margin-left:10%} +table td,table th{border-bottom:1px solid #f1f1f1}table,table td{font-size:10px}table{width:100%;margin:0;border-collapse:collapse;text-align:left;table-layout:fixed}table td,table th{padding:2px;max-width:250px;word-wrap:break-word}table th{font-weight:600;text-transform:uppercase}table td{font-family:Arial,sans-serif;color:rgba(0,0,0,.65)}table tbody tr:hover td{background-color:#fafafa}table.metrics-table{text-align:center}table.metrics-table tr:last-child td{border:none} +.spinner{margin:100px auto;width:50px;height:30px;text-align:center;font-size:10px}.spinner>div{background-color:#009587;height:100%;width:6px;display:inline-block;-webkit-animation:stretchdelay 1.2s infinite ease-in-out;animation:stretchdelay 1.2s infinite ease-in-out}.spinner .rect2{-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner .rect3{-webkit-animation-delay:-1s;animation-delay:-1s}.spinner .rect4{-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner .rect5{-webkit-animation-delay:-.8s;animation-delay:-.8s}@-webkit-keyframes stretchdelay{0%,100%,40%{-webkit-transform:scaleY(.4)}20%{-webkit-transform:scaleY(1)}}@keyframes stretchdelay{0%,100%,40%{transform:scaleY(.4);-webkit-transform:scaleY(.4)}20%{transform:scaleY(1);-webkit-transform:scaleY(1)}} +nav-bar{display:block;background-color:#f6f8f8;height:40px;padding-left:10%;padding-top:3px;padding-right:5%}nav-bar ul{margin-left:10px;padding:0;list-style-type:none;display:inline}nav-bar ul li a{font-size:11px;text-transform:uppercase;text-decoration:none;line-height:2.3rem;color:grey}nav-bar ul li a:hover{color:#000}nav-bar ul li.active{border-bottom:2px solid #009587}nav-bar ul li.active a{color:#009587}nav-bar ul li{margin-left:20px;display:inline;padding-left:5px;padding-bottom:10px}nav-bar .title{color:#009587;font-weight:300;font-size:20px;letter-spacing:.1rem;float:left;padding-top:5px}nav-bar .right-content{float:right}nav-bar .right-content,nav-bar .right-content a{font-size:11px;color:grey;padding-top:10px}nav-bar .right-content a:hover{color:#000} +.plugin{transition:all .1s linear;vertical-align:text-top;width:400px;display:inline-block;padding:0 0 15px;color:#000;text-align:center;border-radius:2px;box-shadow:0 1px 10px rgba(0,0,0,.13),0 5px 10px rgba(0,0,0,.16);margin:30px 15px 0 0;max-height:400px;overflow:hidden;resize:both;background-color:rgba(0,0,0,.015)}.plugin-hidden .top-bar .heading{color:grey;font-style:italic}.plugin-enlarged{width:50%}@media (max-width:768px){.plugin{max-width:80%;float:none;margin:0 auto 10px}.plugin-body{max-height:323px}.plugin-enlarged{width:80%}}@media (max-width:950px){.plugin{margin:15px 10px 0 0}.plugin-enlarged{width:90%}}.plugin.chart-plugin{resize:none}.plugin-body{background-color:#fff;height:323px;font-size:12px;padding:10px;line-height:30px;overflow:auto;border-top:1px solid #ececec}.plugin.chart-plugin,.plugin.chart-plugin .plugin-body{overflow:hidden;padding:0}.plugin-body-short{height:263px}.plugin last-update{font-size:11px;float:left}.plugin ::-webkit-scrollbar{width:8px;height:8px}.plugin ::-webkit-scrollbar-track{background:#eee;border:thin solid #d3d3d3;box-shadow:0 0 3px #dfdfdf inset}.plugin ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.1);border:thin solid rgba(0,0,0,.1);border-radius:0}.plugin ::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.2)} +.progress-bar{background-color:#eec;border-radius:10px;padding:0;clear:both;display:inline-block;overflow:hidden;white-space:nowrap}.progress-bar>div{background-color:#1EAEDB;width:0%;height:5px;border-radius:5px} +.table-data-plugin .filter-container{padding-bottom:0;margin:0}.table-data-plugin .filter,.table-data-plugin .filter:active,.table-data-plugin .filter:focus{height:20px;padding:5px;border:none;outline-color:transparent;background:0 0;width:100%;margin:0;text-align:center;font-size:15px}.table-data-plugin .filter:focus{border-bottom:1px solid #ff5722}.table-data-plugin thead tr th a,.table-data-plugin thead tr th a:visited{color:#000;text-decoration:none}.table-data-plugin .column-sort-caret{font-size:10px;color:#1EAEDB} +.top-bar{height:15px;padding:15px;font-size:13px;text-transform:capitalize;color:#009587;background-color:#f6f8f8}.top-bar .heading{float:left;cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.ld-top-bar-btn{float:right;font-size:17px;color:#009587;background:#fff;border:1px solid #eee;border-radius:50%;width:30px;height:30px;margin-top:-5px;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-ms-transition:all .5s ease;-o-transition:all .5s ease;cursor:pointer;transition:all .5s ease}.minimize-btn{font-size:19px}.ld-refresh-btn:hover{background-color:#ffeb3b;color:#000}.ld-refresh-btn:active{background-color:#0f9d58}.plugin-hidden .width-toggle-btn{display:none}.plugin-enlarged .width-toggle-btn,.plugin-hidden .minimize-btn{background:#eee;color:#000} \ No newline at end of file diff --git a/app/linuxDash.min.js b/app/linuxDash.min.js new file mode 100644 index 00000000..fbe0206f --- /dev/null +++ b/app/linuxDash.min.js @@ -0,0 +1,2513 @@ +/* + AngularJS v1.3.4 + (c) 2010-2014 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(U,V,u){'use strict';function z(b){return function(){var a=arguments[0],c;c="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.3.4/"+(b?b+"/":"")+a;for(a=1;a").append(b).html();try{return b[0].nodeType===mb?R(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+R(b)})}catch(d){return R(c)}}function pc(b){try{return decodeURIComponent(b)}catch(a){}}function qc(b){var a={},c,d;r((b||"").split("&"),function(b){b&&(c=b.replace(/\+/g, +"%20").split("="),d=pc(c[0]),y(d)&&(b=y(c[1])?pc(c[1]):!0,Jb.call(a,d)?D(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Kb(b){var a=[];r(b,function(b,d){D(b)?r(b,function(b){a.push(Da(d,!0)+(!0===b?"":"="+Da(b,!0)))}):a.push(Da(d,!0)+(!0===b?"":"="+Da(b,!0)))});return a.length?a.join("&"):""}function nb(b){return Da(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Da(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g, +"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,a?"%20":"+")}function Fd(b,a){var c,d,e=ob.length;b=A(b);for(d=0;d/,">"));}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);c.debugInfoEnabled&&a.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);a.unshift("ng");d=Lb(a,c.strictDi);d.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return d},e= +/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;U&&e.test(U.name)&&(c.debugInfoEnabled=!0,U.name=U.name.replace(e,""));if(U&&!f.test(U.name))return d();U.name=U.name.replace(f,"");ha.resumeBootstrap=function(b){r(b,function(b){a.push(b)});d()}}function Hd(){U.name="NG_ENABLE_DEBUG_INFO!"+U.name;U.location.reload()}function Id(b){return ha.element(b).injector().get("$$testability")}function Mb(b,a){a=a||"_";return b.replace(Jd,function(b,d){return(d?a:"")+b.toLowerCase()})}function Kd(){var b;sc|| +((qa=U.jQuery)&&qa.fn.on?(A=qa,C(qa.fn,{scope:Ka.scope,isolateScope:Ka.isolateScope,controller:Ka.controller,injector:Ka.injector,inheritedData:Ka.inheritedData}),b=qa.cleanData,qa.cleanData=function(a){var c;if(Nb)Nb=!1;else for(var d=0,e;null!=(e=a[d]);d++)(c=qa._data(e,"events"))&&c.$destroy&&qa(e).triggerHandler("$destroy");b(a)}):A=S,ha.element=A,sc=!0)}function Ob(b,a,c){if(!b)throw Wa("areq",a||"?",c||"required");return b}function pb(b,a,c){c&&D(b)&&(b=b[b.length-1]);Ob(F(b),a,"not a function, got "+ +(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function La(b,a){if("hasOwnProperty"===b)throw Wa("badname",a);}function tc(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,f=a.length,g=0;g")+d[2];for(d=d[0];d--;)c=c.lastChild;f=Xa(f,c.childNodes);c=e.firstChild;c.textContent=""}else f.push(a.createTextNode(b));e.textContent="";e.innerHTML="";r(f,function(a){e.appendChild(a)});return e}function S(b){if(b instanceof S)return b;var a;I(b)&&(b=P(b),a=!0);if(!(this instanceof +S)){if(a&&"<"!=b.charAt(0))throw Qb("nosel");return new S(b)}if(a){a=V;var c;b=(c=cf.exec(b))?[a.createElement(c[1])]:(c=Dc(b,a))?c.childNodes:[]}Ec(this,b)}function Rb(b){return b.cloneNode(!0)}function tb(b,a){a||ub(b);if(b.querySelectorAll)for(var c=b.querySelectorAll("*"),d=0,e=c.length;d 4096 bytes)!"));else{if(p.cookie!==y)for(y=p.cookie,d=y.split("; "),aa={},f=0;fl&&this.remove(s.key),b},get:function(a){if(l").parent()[0])});var f=ea(a,b,a,c,d,e);B.$$addScopeClass(a);var g=null;return function(b, +c,d){Ob(b,"scope");d=d||{};var e=d.parentBoundTranscludeFn,h=d.transcludeControllers;d=d.futureParentElement;e&&e.$$boundTransclude&&(e=e.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ta(d)&&d.toString().match(/SVG/)?"svg":"html":"html");d="html"!==g?A(U(g,A("
").append(a).html())):c?Ka.clone.call(a):a;if(h)for(var k in h)d.data("$"+k+"Controller",h[k].instance);B.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,e);return d}}function ea(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,q,s,n,w;if(p)for(w= +Array(c.length),q=0;qJ.priority)break;if(z=J.scope)J.templateUrl||(K(z)?(ya("new/isolated scope",L||M,J,T),L=J):ya("new/isolated scope",L,J,T)),M=M||J;ga=J.name;!J.templateUrl&&J.controller&&(z=J.controller,E=E||{},ya("'"+ga+"' controller",E[ga],J,T),E[ga]=J);if(z=J.transclude)x=!0,J.$$tlb||(ya("transclusion",da,J,T),da=J),"element"==z?(C=!0,v=J.priority,z=T,T=e.$$element=A(V.createComment(" "+ga+": "+ +e[ga]+" ")),d=T[0],Ab(g,Ya.call(z,0),d),Ga=B(z,f,v,k&&k.name,{nonTlbTranscludeDirective:da})):(z=A(Rb(d)).contents(),T.empty(),Ga=B(z,f));if(J.template)if(Na=!0,ya("template",aa,J,T),aa=J,z=F(J.template)?J.template(T,e):J.template,z=Pc(z),J.replace){k=J;z=Pb.test(z)?Qc(U(J.templateNamespace,P(z))):[];d=z[0];if(1!=z.length||d.nodeType!==na)throw ka("tplrt",ga,"");Ab(g,T,d);za={$attr:{}};z=W(d,[],za);var mf=a.splice(S+1,a.length-(S+1));L&&y(z);a=a.concat(z).concat(mf);Oc(e,za);za=a.length}else T.html(z); +if(J.templateUrl)Na=!0,ya("template",aa,J,T),aa=J,J.replace&&(k=J),H=G(a.splice(S,a.length-S),T,e,g,x&&Ga,l,q,{controllerDirectives:E,newIsolateScopeDirective:L,templateDirective:aa,nonTlbTranscludeDirective:da}),za=a.length;else if(J.compile)try{R=J.compile(T,e,Ga),F(R)?w(null,R,zb,$):R&&w(R.pre,R.post,zb,$)}catch(ca){c(ca,va(T))}J.terminal&&(H.terminal=!0,v=Math.max(v,J.priority))}H.scope=M&&!0===M.scope;H.transcludeOnThisElement=x;H.elementTranscludeOnThisElement=C;H.templateOnThisElement=Na;H.transclude= +Ga;p.hasElementTranscludeDirective=C;return H}function y(a){for(var b=0,c=a.length;bq.priority)&&-1!=q.restrict.indexOf(f)){if(k){var w={$$start:k,$$end:l};q=C(Object.create(q),w)}b.push(q);h=q}}catch(N){c(N)}}return h}function Oc(a,b){var c=b.$attr,d=a.$attr,e=a.$$element; +r(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});r(b,function(b,f){"class"==f?(M(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function G(a,b,c,d,e,f,g,h){var k=[],l,s,p=b[0],n=a.shift(),w=C({},n,{templateUrl:null,transclude:null,replace:null,$$originalDirective:n}),N=F(n.templateUrl)?n.templateUrl(b, +c):n.templateUrl,t=n.templateNamespace;b.empty();q(O.getTrustedResourceUrl(N)).then(function(q){var v,O;q=Pc(q);if(n.replace){q=Pb.test(q)?Qc(U(t,P(q))):[];v=q[0];if(1!=q.length||v.nodeType!==na)throw ka("tplrt",n.name,N);q={$attr:{}};Ab(d,b,v);var H=W(v,[],q);K(n.scope)&&y(H);a=H.concat(a);Oc(c,q)}else v=p,b.html(q);a.unshift(w);l=aa(a,v,c,e,b,n,f,g,h);r(d,function(a,c){a==v&&(d[c]=b[0])});for(s=ea(b[0].childNodes,e);k.length;){q=k.shift();O=k.shift();var E=k.shift(),B=k.shift(),H=b[0];if(!q.$$destroyed){if(O!== +p){var Q=O.className;h.hasElementTranscludeDirective&&n.replace||(H=Rb(v));Ab(E,A(O),H);M(A(H),Q)}O=l.transcludeOnThisElement?L(q,l.transclude,B):B;l(s,q,H,d,O)}}k=null});return function(a,b,c,d,e){a=e;b.$$destroyed||(k?k.push(b,c,d,a):(l.transcludeOnThisElement&&(a=L(b,l.transclude,e)),l(s,b,c,d,a)))}}function z(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name"+b+"";return c.childNodes[0].childNodes;default:return b}}function Ga(a,b){if("srcdoc"==b)return O.HTML;var c=ta(a);if("xlinkHref"== +b||"form"==c&&"action"==b||"img"!=c&&("src"==b||"ngSrc"==b))return O.RESOURCE_URL}function S(a,c,d,e,f){var h=b(d,!0);if(h){if("multiple"===e&&"select"===ta(a))throw ka("selmulti",va(a));c.push({priority:100,compile:function(){return{pre:function(c,d,l){d=l.$$observers||(l.$$observers={});if(k.test(e))throw ka("nodomevents");l[e]&&(h=b(l[e],!0,Ga(a,e),g[e]||f))&&(l[e]=h(c),(d[e]||(d[e]=[])).$$inter=!0,(l.$$observers&&l.$$observers[e].$$scope||c).$watch(h,function(a,b){"class"===e&&a!=b?l.$updateClass(a, +b):l.$set(e,a)}))}}}})}}function Ab(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=a)return b;for(;a--;)8===b[a].nodeType&&of.call(b,a,1);return b}function Ce(){var b={},a=!1,c=/^(\S+)(\s+as\s+(\w+))?$/;this.register=function(a,c){La(a,"controller");K(a)?C(b,a):b[a]=c};this.allowGlobals=function(){a=!0};this.$get=["$injector","$window",function(d,e){function f(a, +b,c,d){if(!a||!K(a.$scope))throw z("$controller")("noscp",d,b);a.$scope[b]=c}return function(g,h,k,l){var m,p,s;k=!0===k;l&&I(l)&&(s=l);I(g)&&(l=g.match(c),p=l[1],s=s||l[3],g=b.hasOwnProperty(p)?b[p]:tc(h.$scope,p,!0)||(a?tc(e,p,!0):u),pb(g,p,!0));if(k)return k=(D(g)?g[g.length-1]:g).prototype,m=Object.create(k),s&&f(h,s,m,p||g.name),C(function(){d.invoke(g,m,h,p);return m},{instance:m,identifier:s});m=d.instantiate(g,h,p);s&&f(h,s,m,p||g.name);return m}}]}function De(){this.$get=["$window",function(b){return A(b.document)}]} +function Ee(){this.$get=["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function Yb(b,a){if(I(b)){b=b.replace(pf,"");var c=a("Content-Type");if(c&&0===c.indexOf(Sc)&&b.trim()||qf.test(b)&&rf.test(b))b=oc(b)}return b}function Tc(b){var a=ia(),c,d,e;if(!b)return a;r(b.split("\n"),function(b){e=b.indexOf(":");c=R(P(b.substr(0,e)));d=P(b.substr(e+1));c&&(a[c]=a[c]?a[c]+", "+d:d)});return a}function Uc(b){var a=K(b)?b:u;return function(c){a||(a=Tc(b));return c?(c=a[R(c)],void 0=== +c&&(c=null),c):a}}function Vc(b,a,c){if(F(c))return c(b,a);r(c,function(c){b=c(b,a)});return b}function He(){var b=this.defaults={transformResponse:[Yb],transformRequest:[function(a){return K(a)&&"[object File]"!==Ja.call(a)&&"[object Blob]"!==Ja.call(a)?Za(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ua(Zb),put:ua(Zb),patch:ua(Zb)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"},a=!1;this.useApplyAsync=function(b){return y(b)?(a=!!b,this):a};var c=this.interceptors= +[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(d,e,f,g,h,k){function l(a){function c(a){var b=C({},a);b.data=a.data?Vc(a.data,a.headers,d.transformResponse):a.data;a=a.status;return 200<=a&&300>a?b:h.reject(b)}var d={method:"get",transformRequest:b.transformRequest,transformResponse:b.transformResponse},e=function(a){var c=b.headers,d=C({},a.headers),e,f,c=C({},c.common,c[R(a.method)]);a:for(e in c){a=R(e);for(f in d)if(R(f)===a)continue a;d[e]=c[e]}(function(a){var b; +r(a,function(c,d){F(c)&&(b=c(),null!=b?a[d]=b:delete a[d])})})(d);return d}(a);if(!ha.isObject(a))throw z("$http")("badreq",a);C(d,a);d.headers=e;d.method=rb(d.method);var f=[function(a){e=a.headers;var d=Vc(a.data,Uc(e),a.transformRequest);G(d)&&r(e,function(a,b){"content-type"===R(b)&&delete e[b]});G(a.withCredentials)&&!G(b.withCredentials)&&(a.withCredentials=b.withCredentials);return m(a,d,e).then(c,c)},u],g=h.when(d);for(r(t,function(a){(a.request||a.requestError)&&f.unshift(a.request,a.requestError); +(a.response||a.responseError)&&f.push(a.response,a.responseError)});f.length;){a=f.shift();var k=f.shift(),g=g.then(a,k)}g.success=function(a){g.then(function(b){a(b.data,b.status,b.headers,d)});return g};g.error=function(a){g.then(null,function(b){a(b.data,b.status,b.headers,d)});return g};return g}function m(c,f,k){function m(b,c,d,e){function f(){w(c,b,d,e)}M&&(200<=b&&300>b?M.put(r,[b,c,Tc(d),e]):M.remove(r));a?g.$applyAsync(f):(f(),g.$$phase||g.$apply())}function w(a,b,d,e){b=Math.max(b,0);(200<= +b&&300>b?E.resolve:E.reject)({data:a,status:b,headers:Uc(d),config:c,statusText:e})}function t(){var a=l.pendingRequests.indexOf(c);-1!==a&&l.pendingRequests.splice(a,1)}var E=h.defer(),H=E.promise,M,B,r=p(c.url,c.params);l.pendingRequests.push(c);H.then(t,t);!c.cache&&!b.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(M=K(c.cache)?c.cache:K(b.cache)?b.cache:s);if(M)if(B=M.get(r),y(B)){if(B&&F(B.then))return B.then(t,t),B;D(B)?w(B[1],B[0],ua(B[2]),B[3]):w(B,200,{},"OK")}else M.put(r,H); +G(B)&&((B=Wc(c.url)?e.cookies()[c.xsrfCookieName||b.xsrfCookieName]:u)&&(k[c.xsrfHeaderName||b.xsrfHeaderName]=B),d(c.method,r,f,m,k,c.timeout,c.withCredentials,c.responseType));return H}function p(a,b){if(!b)return a;var c=[];Bd(b,function(a,b){null===a||G(a)||(D(a)||(a=[a]),r(a,function(a){K(a)&&(a=fa(a)?a.toISOString():Za(a));c.push(Da(b)+"="+Da(a))}))});0=k&&(q.resolve(s), +p(N.$$intervalId),delete f[N.$$intervalId]);t||b.$apply()},h);f[N.$$intervalId]=q;return N}var f={};e.cancel=function(b){return b&&b.$$intervalId in f?(f[b.$$intervalId].reject("canceled"),a.clearInterval(b.$$intervalId),delete f[b.$$intervalId],!0):!1};return e}]}function Od(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4", +posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January February March April May June July August September October November December".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y", +mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return 1===b?"one":"other"}}}}function ac(b){b=b.split("/");for(var a=b.length;a--;)b[a]=nb(b[a]);return b.join("/")}function Xc(b,a){var c=Aa(b);a.$$protocol=c.protocol;a.$$host=c.hostname;a.$$port=$(c.port)||uf[c.protocol]||null}function Yc(b,a){var c="/"!==b.charAt(0);c&&(b="/"+b);var d=Aa(b);a.$$path=decodeURIComponent(c&&"/"===d.pathname.charAt(0)?d.pathname.substring(1):d.pathname);a.$$search= +qc(d.search);a.$$hash=decodeURIComponent(d.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function xa(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Fa(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function bc(b){return b.substr(0,Fa(b).lastIndexOf("/")+1)}function cc(b,a){this.$$html5=!0;a=a||"";var c=bc(b);Xc(b,this);this.$$parse=function(a){var b=xa(c,a);if(!I(b))throw eb("ipthprfx",a,c);Yc(b,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose= +function(){var a=Kb(this.$$search),b=this.$$hash?"#"+nb(this.$$hash):"";this.$$url=ac(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;(f=xa(b,d))!==u?(g=f,g=(f=xa(a,f))!==u?c+(xa("/",f)||f):b+g):(f=xa(c,d))!==u?g=c+f:c==d+"/"&&(g=c);g&&this.$$parse(g);return!!g}}function dc(b,a){var c=bc(b);Xc(b,this);this.$$parse=function(d){var e=xa(b,d)||xa(c,d),e="#"==e.charAt(0)?xa(a,e):this.$$html5?e: +"";if(!I(e))throw eb("ihshprfx",d,a);Yc(e,this);d=this.$$path;var f=/^\/[A-Z]:(\/.*)/;0===e.indexOf(b)&&(e=e.replace(b,""));f.exec(e)||(d=(e=f.exec(d))?e[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var c=Kb(this.$$search),e=this.$$hash?"#"+nb(this.$$hash):"";this.$$url=ac(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$parseLinkUrl=function(a,c){return Fa(b)==Fa(a)?(this.$$parse(a),!0):!1}}function Zc(b,a){this.$$html5=!0;dc.apply(this,arguments); +var c=bc(b);this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;b==Fa(d)?f=d:(g=xa(c,d))?f=b+a+g:c===d+"/"&&(f=c);f&&this.$$parse(f);return!!f};this.$$compose=function(){var c=Kb(this.$$search),e=this.$$hash?"#"+nb(this.$$hash):"";this.$$url=ac(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function Bb(b){return function(){return this[b]}}function $c(b,a){return function(c){if(G(c))return this[b];this[b]=a(c);this.$$compose();return this}}function Je(){var b= +"",a={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(a){return y(a)?(b=a,this):b};this.html5Mode=function(b){return Ua(b)?(a.enabled=b,this):K(b)?(Ua(b.enabled)&&(a.enabled=b.enabled),Ua(b.requireBase)&&(a.requireBase=b.requireBase),Ua(b.rewriteLinks)&&(a.rewriteLinks=b.rewriteLinks),this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement",function(c,d,e,f){function g(a,b,c){var e=k.url(),f=k.$$state;try{d.url(a,b,c),k.$$state=d.state()}catch(g){throw k.url(e),k.$$state= +f,g;}}function h(a,b){c.$broadcast("$locationChangeSuccess",k.absUrl(),a,k.$$state,b)}var k,l;l=d.baseHref();var m=d.url(),p;if(a.enabled){if(!l&&a.requireBase)throw eb("nobase");p=m.substring(0,m.indexOf("/",m.indexOf("//")+2))+(l||"/");l=e.history?cc:Zc}else p=Fa(m),l=dc;k=new l(p,"#"+b);k.$$parseLinkUrl(m,m);k.$$state=d.state();var s=/^\s*(javascript|mailto):/i;f.on("click",function(b){if(a.rewriteLinks&&!b.ctrlKey&&!b.metaKey&&2!=b.which){for(var e=A(b.target);"a"!==ta(e[0]);)if(e[0]===f[0]|| +!(e=e.parent())[0])return;var g=e.prop("href"),h=e.attr("href")||e.attr("xlink:href");K(g)&&"[object SVGAnimatedString]"===g.toString()&&(g=Aa(g.animVal).href);s.test(g)||!g||e.attr("target")||b.isDefaultPrevented()||!k.$$parseLinkUrl(g,h)||(b.preventDefault(),k.absUrl()!=d.url()&&(c.$apply(),U.angular["ff-684208-preventDefault"]=!0))}});k.absUrl()!=m&&d.url(k.absUrl(),!0);var t=!0;d.onUrlChange(function(a,b){c.$evalAsync(function(){var d=k.absUrl(),e=k.$$state,f;k.$$parse(a);k.$$state=b;f=c.$broadcast("$locationChangeStart", +a,d,b,e).defaultPrevented;k.absUrl()===a&&(f?(k.$$parse(d),k.$$state=e,g(d,!1,e)):(t=!1,h(d,e)))});c.$$phase||c.$digest()});c.$watch(function(){var a=d.url(),b=d.state(),f=k.$$replace,l=a!==k.absUrl()||k.$$html5&&e.history&&b!==k.$$state;if(t||l)t=!1,c.$evalAsync(function(){var d=k.absUrl(),e=c.$broadcast("$locationChangeStart",d,a,k.$$state,b).defaultPrevented;k.absUrl()===d&&(e?(k.$$parse(a),k.$$state=b):(l&&g(d,f,b===k.$$state?null:k.$$state),h(a,b)))});k.$$replace=!1});return k}]}function Ke(){var b= +!0,a=this;this.debugEnabled=function(a){return y(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=c.console||{},e=b[a]||b.log||x;a=!1;try{a=!!e.apply}catch(k){}return a?function(){var a=[];r(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"), +info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function ra(b,a){if("__defineGetter__"===b||"__defineSetter__"===b||"__lookupGetter__"===b||"__lookupSetter__"===b||"__proto__"===b)throw la("isecfld",a);return b}function sa(b,a){if(b){if(b.constructor===b)throw la("isecfn",a);if(b.window===b)throw la("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw la("isecdom",a);if(b===Object)throw la("isecobj", +a);}return b}function ec(b){return b.constant}function Oa(b,a,c,d){sa(b,d);a=a.split(".");for(var e,f=0;1h?ad(g[0],g[1],g[2],g[3],g[4],c,d):function(a,b){var e=0,f;do f=ad(g[e++],g[e++],g[e++],g[e++],g[e++],c,d)(a,b),b=u,a=f;while(e=this.promise.$$state.status&&d&&d.length&&b(function(){for(var b,e,f=0,g=d.length;fa)for(b in l++,f)e.hasOwnProperty(b)||(n--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,h,k=1r&&(y=4-r,u[y]||(u[y]=[]),u[y].push({msg:F(e.exp)?"fn: "+(e.exp.name||e.exp.toString()):e.exp,newVal:g,oldVal:k})); +else if(e===c){v=!1;break a}}catch(A){f(A)}if(!(m=L.$$childHead||L!==this&&L.$$nextSibling))for(;L!==this&&!(m=L.$$nextSibling);)L=L.$parent}while(L=m);if((v||N.length)&&!r--)throw q.$$phase=null,a("infdig",b,u);}while(v||N.length);for(q.$$phase=null;n.length;)try{n.shift()()}catch(da){f(da)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;if(this!==q){for(var b in this.$$listenerCount)m(this,this.$$listenerCount[b],b);a.$$childHead==this&& +(a.$$childHead=this.$$nextSibling);a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=x;this.$on=this.$watch=this.$watchGroup=function(){return x};this.$$listeners={};this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=this.$$watchers= +null}}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a){q.$$phase||N.length||h.defer(function(){N.length&&q.$digest()});N.push({scope:this,expression:a})},$$postDigest:function(a){n.push(a)},$apply:function(a){try{return l("$apply"),this.$eval(a)}catch(b){f(b)}finally{q.$$phase=null;try{q.$digest()}catch(c){throw f(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&v.push(b);t()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b); +var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,m(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,g=!1,h={name:a,targetScope:e,stopPropagation:function(){g=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=Xa([h],arguments,1),l,m;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(m=d.length;lHa)throw Ba("iequirks");var d=ua(ma);d.isEnabled=function(){return b};d.trustAs=c.trustAs;d.getTrusted=c.getTrusted;d.valueOf= +c.valueOf;b||(d.trustAs=d.getTrusted=function(a,b){return b},d.valueOf=oa);d.parseAs=function(b,c){var e=a(c);return e.literal&&e.constant?e:a(c,function(a){return d.getTrusted(b,a)})};var e=d.parseAs,f=d.getTrusted,g=d.trustAs;r(ma,function(a,b){var c=R(b);d[bb("parse_as_"+c)]=function(b){return e(a,b)};d[bb("get_trusted_"+c)]=function(b){return f(a,b)};d[bb("trust_as_"+c)]=function(b){return g(a,b)}});return d}]}function Re(){this.$get=["$window","$document",function(b,a){var c={},d=$((/android (\d+)/.exec(R((b.navigator|| +{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},g,h=/^(Moz|webkit|ms)(?=[A-Z])/,k=f.body&&f.body.style,l=!1,m=!1;if(k){for(var p in k)if(l=h.exec(p)){g=l[0];g=g.substr(0,1).toUpperCase()+g.substr(1);break}g||(g="WebkitOpacity"in k&&"webkit");l=!!("transition"in k||g+"Transition"in k);m=!!("animation"in k||g+"Animation"in k);!d||l&&m||(l=I(f.body.style.webkitTransition),m=I(f.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hasEvent:function(a){if("input"== +a&&9==Ha)return!1;if(G(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:$a(),vendorPrefix:g,transitions:l,animations:m,android:d}}]}function Te(){this.$get=["$templateCache","$http","$q",function(b,a,c){function d(e,f){d.totalPendingRequests++;var g=a.defaults&&a.defaults.transformResponse;if(D(g))for(var h=g,g=[],k=0;kb;b=Math.abs(b);var g=b+"",h="",k=[],l=!1;if(-1!==g.indexOf("e")){var m=g.match(/([\d\.]+)e(-?)(\d+)/);m&&"-"==m[2]&&m[3]>e+1?(g="0",b=0):(h=g,l=!0)}if(l)0b&&(h=b.toFixed(e));else{g=(g.split(ld)[1]||"").length;G(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));b=+(Math.round(+(b.toString()+"e"+e)).toString()+"e"+-e);0===b&&(f=!1);b=(""+b).split(ld);g=b[0];b=b[1]||"";var m=0,p=a.lgSize,s=a.gSize;if(g.length>=p+s)for(m=g.length-p,l=0;lb&&(d="-",b=-b);for(b=""+b;b.length-c)e+=c;0===e&&-12==c&&(e=12);return Cb(e,a,d)}}function Db(b,a){return function(c, +d){var e=c["get"+b](),f=rb(a?"SHORT"+b:b);return d[f][e]}}function md(b){var a=(new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function nd(b){return function(a){var c=md(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return Cb(a,b)}}function hd(b){function a(a){var b;if(b=a.match(c)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=$(b[9]+b[10]),g=$(b[9]+b[11])); +h.call(a,$(b[1]),$(b[2])-1,$(b[3]));f=$(b[4]||0)-f;g=$(b[5]||0)-g;h=$(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));k.call(a,f,g,h,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e,f){var g="",h=[],k,l;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;I(c)&&(c=Ff.test(c)?$(c):a(c));X(c)&&(c=new Date(c));if(!fa(c))return c;for(;e;)(l=Gf.exec(e))?(h=Xa(h,l,1),e=h.pop()):(h.push(e),e=null);f&&"UTC"===f&&(c= +new Date(c.getTime()),c.setMinutes(c.getMinutes()+c.getTimezoneOffset()));r(h,function(a){k=Hf[a];g+=k?k(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Bf(){return function(b){return Za(b,!0)}}function Cf(){return function(b,a){X(b)&&(b=b.toString());if(!D(b)&&!I(b))return b;a=Infinity===Math.abs(Number(a))?Number(a):$(a);if(I(b))return a?0<=a?b.slice(0,a):b.slice(a,b.length):"";var c=[],d,e;a>b.length?a=b.length:a<-b.length&&(a=-b.length);0b||37<=b&&40>=b||s(a)});if(e.hasEvent("paste"))a.on("paste cut",s)}a.on("change",m);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)? +"":d.$viewValue)}}function Gb(b,a){return function(c,d){var e,f;if(fa(c))return c;if(I(c)){'"'==c.charAt(0)&&'"'==c.charAt(c.length-1)&&(c=c.substring(1,c.length-1));if(If.test(c))return new Date(c);b.lastIndex=0;if(e=b.exec(c))return e.shift(),f=d?{yyyy:d.getFullYear(),MM:d.getMonth()+1,dd:d.getDate(),HH:d.getHours(),mm:d.getMinutes(),ss:d.getSeconds(),sss:d.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},r(e,function(b,c){c=r};g.$observe("min",function(a){r=s(a);h.$validate()})}if(y(g.max)||g.ngMax){var n;h.$validators.max=function(a){return!p(a)||G(n)||c(a)<=n};g.$observe("max",function(a){n=s(a);h.$validate()})}}}function qd(b,a,c,d){(d.$$hasNativeValidators=K(a[0].validity))&&d.$parsers.push(function(b){var c=a.prop("validity")|| +{};return c.badInput&&!c.typeMismatch?u:b})}function rd(b,a,c,d,e){if(y(d)){b=b(d);if(!b.constant)throw z("ngModel")("constexpr",c,d);return b(a)}return e}function pd(b){function a(a,b){b&&!f[a]?(l.addClass(e,a),f[a]=!0):!b&&f[a]&&(l.removeClass(e,a),f[a]=!1)}function c(b,c){b=b?"-"+Mb(b,"-"):"";a(ib+b,!0===c);a(sd+b,!1===c)}var d=b.ctrl,e=b.$element,f={},g=b.set,h=b.unset,k=b.parentForm,l=b.$animate;f[sd]=!(f[ib]=e.hasClass(ib));d.$setValidity=function(b,e,f){e===u?(d.$pending||(d.$pending={}),g(d.$pending, +b,f)):(d.$pending&&h(d.$pending,b,f),td(d.$pending)&&(d.$pending=u));Ua(e)?e?(h(d.$error,b,f),g(d.$$success,b,f)):(g(d.$error,b,f),h(d.$$success,b,f)):(h(d.$error,b,f),h(d.$$success,b,f));d.$pending?(a(ud,!0),d.$valid=d.$invalid=u,c("",null)):(a(ud,!1),d.$valid=td(d.$error),d.$invalid=!d.$valid,c("",d.$valid));e=d.$pending&&d.$pending[b]?u:d.$error[b]?!1:d.$$success[b]?!0:null;c(b,e);k.$setValidity(b,e,d)}}function td(b){if(b)for(var a in b)return!1;return!0}function ic(b,a){b="ngClass"+b;return["$animate", +function(c){function d(a,b){var c=[],d=0;a:for(;d(?:<\/\1>|)$/,Pb=/<|&#?\w+;/,af=/<([\w:]+)/,bf=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, +ja={option:[1,'"],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ja.optgroup=ja.option;ja.tbody=ja.tfoot=ja.colgroup=ja.caption=ja.thead;ja.th=ja.td;var Ka=S.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===V.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),S(U).on("load",a))}, +toString:function(){var b=[];r(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?A(this[b]):A(this[this.length+b])},length:0,push:Kf,sort:[].sort,splice:[].splice},yb={};r("multiple selected checked disabled readOnly required open".split(" "),function(b){yb[R(b)]=b});var Kc={};r("input select option textarea button form details".split(" "),function(b){Kc[b]=!0});var Lc={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"}; +r({data:Sb,removeData:ub},function(b,a){S[a]=b});r({data:Sb,inheritedData:xb,scope:function(b){return A.data(b,"$scope")||xb(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return A.data(b,"$isolateScope")||A.data(b,"$isolateScopeNoTemplate")},controller:Gc,injector:function(b){return xb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Tb,css:function(b,a,c){a=bb(a);if(y(c))b.style[a]=c;else return b.style[a]},attr:function(b,a,c){var d=R(a);if(yb[d])if(y(c))c? +(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||x).specified?d:u;else if(y(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?u:b},prop:function(b,a,c){if(y(c))b[a]=c;else return b[a]},text:function(){function b(a,b){if(G(b)){var d=a.nodeType;return d===na||d===mb?a.textContent:""}a.textContent=b}b.$dv="";return b}(),val:function(b,a){if(G(a)){if(b.multiple&&"select"===ta(b)){var c=[];r(b.options,function(a){a.selected&& +c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(G(a))return b.innerHTML;tb(b,!0);b.innerHTML=a},empty:Hc},function(b,a){S.prototype[a]=function(a,d){var e,f,g=this.length;if(b!==Hc&&(2==b.length&&b!==Tb&&b!==Gc?a:d)===u){if(K(a)){for(e=0;e":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"!":function(a,c,d){return!d(a,c)},"=":!0,"|":!0}),Sf={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},gc=function(a){this.options=a};gc.prototype={constructor:gc,lex:function(a){this.text=a;this.index=0;for(this.tokens= +[];this.index=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"=== +a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d=d||this.index;c=y(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw la("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.indexa){a=this.tokens[a];var g=a.text;if(g===c||g===d||g===e||g===f||!(c||d||e||f))return a}return!1},expect:function(a,c,d,e){return(a=this.peek(a,c,d,e))?(this.tokens.shift(),a):!1},consume:function(a){if(0===this.tokens.length)throw la("ueoe",this.text);var c=this.expect(a);c||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return c},unaryFn:function(a,c){var d=jb[a];return C(function(a,f){return d(a, +f,c)},{constant:c.constant,inputs:[c]})},binaryFn:function(a,c,d,e){var f=jb[c];return C(function(c,e){return f(c,e,a,d)},{constant:a.constant&&d.constant,inputs:!e&&[a,d]})},identifier:function(){for(var a=this.consume().text;this.peek(".")&&this.peekAhead(1).identifier&&!this.peekAhead(2,"(");)a+=this.consume().text+this.consume().text;return Ib[a]||bd(a,this.options,this.text)},constant:function(){var a=this.consume().value;return C(function(){return a},{constant:!0,literal:!0})},statements:function(){for(var a= +[];;)if(0","<=",">="))a=this.binaryFn(a,c.text,this.relational());return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.text,this.multiplicative());return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.text,this.unary());return a},unary:function(){var a;return this.expect("+")? +this.primary():(a=this.expect("-"))?this.binaryFn(fb.ZERO,a.text,this.unary()):(a=this.expect("!"))?this.unaryFn(a.text,this.unary()):this.primary()},fieldAccess:function(a){var c=this.text,d=this.consume().text,e=bd(d,this.options,c);return C(function(c,d,h){return e(h||a(c,d))},{assign:function(e,g,h){(h=a(e,h))||a.assign(e,h={});return Oa(h,d,g,c)}})},objectIndex:function(a){var c=this.text,d=this.expression();this.consume("]");return C(function(e,f){var g=a(e,f),h=d(e,f);ra(h,c);return g?sa(g[h], +c):u},{assign:function(e,f,g){var h=ra(d(e,g),c);(g=sa(a(e,g),c))||a.assign(e,g={});return g[h]=f}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression());while(this.expect(","))}this.consume(")");var e=this.text,f=d.length?[]:null;return function(g,h){var k=c?c(g,h):g,l=a(g,h,k)||x;if(f)for(var m=d.length;m--;)f[m]=sa(d[m](g,h),e);sa(k,e);if(l){if(l.constructor===l)throw la("isecfn",e);if(l===Pf||l===Qf||l===Rf)throw la("isecff",e);}k=l.apply?l.apply(k, +f):l(f[0],f[1],f[2],f[3],f[4]);return sa(k,e)}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return C(function(c,d){for(var e=[],f=0,g=a.length;fa.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<= +a?"+":"")+(Cb(Math[0=h};d.$observe("min",function(a){y(a)&&!X(a)&&(a=parseFloat(a,10));h=X(a)&&!isNaN(a)?a:u;e.$validate()})}if(d.max||d.ngMax){var k;e.$validators.max=function(a){return e.$isEmpty(a)||G(k)||a<=k};d.$observe("max",function(a){y(a)&&!X(a)&&(a=parseFloat(a,10));k=X(a)&&!isNaN(a)?a:u;e.$validate()})}},url:function(a,c,d,e,f,g){gb(a,c,d,e,f,g);hc(e);e.$$parserName="url";e.$validators.url=function(a, +c){var d=a||c;return e.$isEmpty(d)||Tf.test(d)}},email:function(a,c,d,e,f,g){gb(a,c,d,e,f,g);hc(e);e.$$parserName="email";e.$validators.email=function(a,c){var d=a||c;return e.$isEmpty(d)||Uf.test(d)}},radio:function(a,c,d,e){G(d.name)&&c.attr("name",++kb);c.on("click",function(a){c[0].checked&&e.$setViewValue(d.value,a&&a.type)});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e,f,g,h,k){var l=rd(k,a,"ngTrueValue",d.ngTrueValue,!0), +m=rd(k,a,"ngFalseValue",d.ngFalseValue,!1);c.on("click",function(a){e.$setViewValue(c[0].checked,a&&a.type)});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return!1===a};e.$formatters.push(function(a){return pa(a,l)});e.$parsers.push(function(a){return a?l:m})},hidden:x,button:x,submit:x,reset:x,file:x},vc=["$browser","$sniffer","$filter","$parse",function(a,c,d,e){return{restrict:"E",require:["?ngModel"],link:{pre:function(f,g,h,k){k[0]&&(Ad[R(h.type)]||Ad.text)(f,g,h,k[0], +c,a,d,e)}}}}],ib="ng-valid",sd="ng-invalid",Qa="ng-pristine",Fb="ng-dirty",ud="ng-pending",Xf=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,c,d,e,f,g,h,k,l,m){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=u;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid= +!1;this.$error={};this.$$success={};this.$pending=u;this.$name=m(d.name||"",!1)(a);var p=f(d.ngModel),s=p.assign,t=p,q=s,N=null,n=this;this.$$setOptions=function(a){if((n.$options=a)&&a.getterSetter){var c=f(d.ngModel+"()"),g=f(d.ngModel+"($$$p)");t=function(a){var d=p(a);F(d)&&(d=c(a));return d};q=function(a,c){F(p(a))?g(a,{$$$p:n.$modelValue}):s(a,n.$modelValue)}}else if(!p.assign)throw Hb("nonassign",d.ngModel,va(e));};this.$render=x;this.$isEmpty=function(a){return G(a)||""===a||null===a||a!== +a};var v=e.inheritedData("$formController")||Eb,w=0;pd({ctrl:this,$element:e,set:function(a,c){a[c]=!0},unset:function(a,c){delete a[c]},parentForm:v,$animate:g});this.$setPristine=function(){n.$dirty=!1;n.$pristine=!0;g.removeClass(e,Fb);g.addClass(e,Qa)};this.$setDirty=function(){n.$dirty=!0;n.$pristine=!1;g.removeClass(e,Qa);g.addClass(e,Fb);v.$setDirty()};this.$setUntouched=function(){n.$touched=!1;n.$untouched=!0;g.setClass(e,"ng-untouched","ng-touched")};this.$setTouched=function(){n.$touched= +!0;n.$untouched=!1;g.setClass(e,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){h.cancel(N);n.$viewValue=n.$$lastCommittedViewValue;n.$render()};this.$validate=function(){if(!X(n.$modelValue)||!isNaN(n.$modelValue)){var a=n.$$rawModelValue,c=n.$valid,d=n.$modelValue,e=n.$options&&n.$options.allowInvalid;n.$$runValidators(n.$error[n.$$parserName||"parse"]?!1:u,a,n.$$lastCommittedViewValue,function(f){e||c===f||(n.$modelValue=f?a:u,n.$modelValue!==d&&n.$$writeModelToScope())})}};this.$$runValidators= +function(a,c,d,e){function f(){var a=!0;r(n.$validators,function(e,f){var g=e(c,d);a=a&&g;h(f,g)});return a?!0:(r(n.$asyncValidators,function(a,c){h(c,null)}),!1)}function g(){var a=[],e=!0;r(n.$asyncValidators,function(f,g){var k=f(c,d);if(!k||!F(k.then))throw Hb("$asyncValidators",k);h(g,u);a.push(k.then(function(){h(g,!0)},function(a){e=!1;h(g,!1)}))});a.length?l.all(a).then(function(){k(e)},x):k(!0)}function h(a,c){m===w&&n.$setValidity(a,c)}function k(a){m===w&&e(a)}w++;var m=w;(function(a){var c= +n.$$parserName||"parse";if(a===u)h(c,null);else if(h(c,a),!a)return r(n.$validators,function(a,c){h(c,null)}),r(n.$asyncValidators,function(a,c){h(c,null)}),!1;return!0})(a)?f()?g():k(!1):k(!1)};this.$commitViewValue=function(){var a=n.$viewValue;h.cancel(N);if(n.$$lastCommittedViewValue!==a||""===a&&n.$$hasNativeValidators)n.$$lastCommittedViewValue=a,n.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var c=n.$$lastCommittedViewValue,d=c,e=G(d)?u:!0;if(e)for(var f= +0;ff||e.$isEmpty(a)||c.length<=f}}}}},yc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("minlength",function(a){f=$(a)||0;e.$validate()});e.$validators.minlength=function(a,c){return e.$isEmpty(c)||c.length>=f}}}}},te=function(){return{restrict:"A",priority:100,require:"ngModel", +link:function(a,c,d,e){var f=c.attr(d.$attr.ngList)||", ",g="false"!==d.ngTrim,h=g?P(f):f;e.$parsers.push(function(a){if(!G(a)){var c=[];a&&r(a.split(h),function(a){a&&c.push(g?P(a):a)});return c}});e.$formatters.push(function(a){return D(a)?a.join(f):u});e.$isEmpty=function(a){return!a||!a.length}}}},Yf=/^(true|false|\d+)$/,ve=function(){return{restrict:"A",priority:100,compile:function(a,c){return Yf.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue, +function(a){f.$set("value",a)})}}}},we=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,c){var d=this;this.$options=a.$eval(c.ngModelOptions);this.$options.updateOn!==u?(this.$options.updateOnDefault=!1,this.$options.updateOn=P(this.$options.updateOn.replace(Wf,function(){d.$options.updateOnDefault=!0;return" "}))):this.$options.updateOnDefault=!0}]}},Wd=["$compile",function(a){return{restrict:"AC",compile:function(c){a.$$addBindingClass(c);return function(c,e,f){a.$$addBindingInfo(e, +f.ngBind);e=e[0];c.$watch(f.ngBind,function(a){e.textContent=a===u?"":a})}}}}],Yd=["$interpolate","$compile",function(a,c){return{compile:function(d){c.$$addBindingClass(d);return function(d,f,g){d=a(f.attr(g.$attr.ngBindTemplate));c.$$addBindingInfo(f,d.expressions);f=f[0];g.$observe("ngBindTemplate",function(a){f.textContent=a===u?"":a})}}}}],Xd=["$sce","$parse","$compile",function(a,c,d){return{restrict:"A",compile:function(e,f){var g=c(f.ngBindHtml),h=c(f.ngBindHtml,function(a){return(a||"").toString()}); +d.$$addBindingClass(e);return function(c,e,f){d.$$addBindingInfo(e,f.ngBindHtml);c.$watch(h,function(){e.html(a.getTrustedHtml(g(c))||"")})}}}}],Zd=ic("",!0),ae=ic("Odd",0),$d=ic("Even",1),be=Ia({compile:function(a,c){c.$set("ngCloak",u);a.removeClass("ng-cloak")}}),ce=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],Ac={},Zf={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "), +function(a){var c=wa("ng-"+a);Ac[c]=["$parse","$rootScope",function(d,e){return{restrict:"A",compile:function(f,g){var h=d(g[c],null,!0);return function(c,d){d.on(a,function(d){var f=function(){h(c,{$event:d})};Zf[a]&&e.$$phase?c.$evalAsync(f):c.$apply(f)})}}}}]});var fe=["$animate",function(a){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,f,g){var h,k,l;c.$watch(e.ngIf,function(c){c?k||g(function(c,f){k=f;c[c.length++]=V.createComment(" end ngIf: "+ +e.ngIf+" ");h={clone:c};a.enter(c,d.parent(),d)}):(l&&(l.remove(),l=null),k&&(k.$destroy(),k=null),h&&(l=qb(h.clone),a.leave(l).then(function(){l=null}),h=null))})}}}],ge=["$templateRequest","$anchorScroll","$animate","$sce",function(a,c,d,e){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ha.noop,compile:function(f,g){var h=g.ngInclude||g.src,k=g.onload||"",l=g.autoscroll;return function(f,g,s,r,q){var u=0,n,v,w,O=function(){v&&(v.remove(),v=null);n&&(n.$destroy(), +n=null);w&&(d.leave(w).then(function(){v=null}),v=w,w=null)};f.$watch(e.parseAsResourceUrl(h),function(e){var h=function(){!y(l)||l&&!f.$eval(l)||c()},s=++u;e?(a(e,!0).then(function(a){if(s===u){var c=f.$new();r.template=a;a=q(c,function(a){O();d.enter(a,null,g).then(h)});n=c;w=a;n.$emit("$includeContentLoaded",e);f.$eval(k)}},function(){s===u&&(O(),f.$emit("$includeContentError",e))}),f.$emit("$includeContentRequested",e)):(O(),r.template=null)})}}}}],xe=["$compile",function(a){return{restrict:"ECA", +priority:-400,require:"ngInclude",link:function(c,d,e,f){/SVG/.test(d[0].toString())?(d.empty(),a(Dc(f.template,V).childNodes)(c,function(a){d.append(a)},{futureParentElement:d})):(d.html(f.template),a(d.contents())(c))}}}],he=Ia({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),ie=Ia({terminal:!0,priority:1E3}),je=["$locale","$interpolate",function(a,c){var d=/{}/g,e=/^when(Minus)?(.+)$/;return{restrict:"EA",link:function(f,g,h){function k(a){g.text(a||"")}var l= +h.count,m=h.$attr.when&&g.attr(h.$attr.when),p=h.offset||0,s=f.$eval(m)||{},t={},m=c.startSymbol(),q=c.endSymbol(),u=m+l+"-"+p+q,n=ha.noop,v;r(h,function(a,c){var d=e.exec(c);d&&(d=(d[1]?"-":"")+R(d[2]),s[d]=g.attr(h.$attr[c]))});r(s,function(a,e){t[e]=c(a.replace(d,u))});f.$watch(l,function(c){c=parseFloat(c);var d=isNaN(c);d||c in s||(c=a.pluralCat(c-p));c===v||d&&isNaN(v)||(n(),n=f.$watch(t[c],k),v=c)})}}}],ke=["$parse","$animate",function(a,c){var d=z("ngRepeat"),e=function(a,c,d,e,l,m,p){a[d]= +e;l&&(a[l]=m);a.$index=c;a.$first=0===c;a.$last=c===p-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(c&1))};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,g){var h=g.ngRepeat,k=V.createComment(" end ngRepeat: "+h+" "),l=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!l)throw d("iexp",h);var m=l[1],p=l[2],s=l[3],t=l[4],l=m.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); +if(!l)throw d("iidexp",m);var q=l[3]||l[1],y=l[2];if(s&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(s)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent)$/.test(s)))throw d("badident",s);var n,v,w,z,E={$id:Ma};t?n=a(t):(w=function(a,c){return Ma(c)},z=function(a){return a});return function(a,f,g,l,m){n&&(v=function(c,d,e){y&&(E[y]=c);E[q]=d;E.$index=e;return n(a,E)});var t=ia();a.$watchCollection(p,function(g){var l,n,p=f[0],B,E=ia(),C,x,G,T,D,F,I;s&&(a[s]=g);if(Ra(g))D=g,n=v|| +w;else{n=v||z;D=[];for(I in g)g.hasOwnProperty(I)&&"$"!=I.charAt(0)&&D.push(I);D.sort()}C=D.length;I=Array(C);for(l=0;lD;)d=q.pop(),m(Q,d.label,!1),d.element.remove();r(Q,function(a,c){0a&&s.removeOption(c)})}for(;R.length>x;)R.pop()[0].element.remove()}var p;if(!(p=q.match(d)))throw $f("iexp",q,va(f));var E=c(p[2]|| +p[1]),x=p[4]||p[6],A=/ as /.test(p[0])&&p[1],C=A?c(A):null,G=p[5],I=c(p[3]||""),H=c(p[2]?p[1]:x),M=c(p[7]),K=p[8]?c(p[8]):null,S={},R=[[{element:f,label:""}]],U={};z&&(a(z)(e),z.removeClass("ng-scope"),z.remove());f.empty();f.on("change",function(){e.$apply(function(){var a=M(e)||[],c;if(t)c=[],r(f.val(),function(d){d=K?S[d]:d;c.push("?"===d?u:""===d?null:h(C?C:H,d,a[d]))});else{var d=K?S[f.val()]:f.val();c="?"===d?u:""===d?null:h(C?C:H,d,a[d])}g.$setViewValue(c);n()})});g.$render=n;e.$watchCollection(M, +l);e.$watchCollection(function(){var a=M(e),c;if(a&&D(a)){c=Array(a.length);for(var d=0,f=a.length;d@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}'); +//# sourceMappingURL=angular.min.js.map + +/* + AngularJS v1.3.4 + (c) 2010-2014 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(p,d,C){'use strict';function v(r,h,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,y){function z(){k&&(g.cancel(k),k=null);l&&(l.$destroy(),l=null);m&&(k=g.leave(m),k.then(function(){k=null}),m=null)}function x(){var b=r.current&&r.current.locals;if(d.isDefined(b&&b.$template)){var b=a.$new(),f=r.current;m=y(b,function(b){g.enter(b,null,m||c).then(function(){!d.isDefined(t)||t&&!a.$eval(t)||h()});z()});l=f.scope=b;l.$emit("$viewContentLoaded"); +l.$eval(w)}else z()}var l,m,k,t=b.autoscroll,w=b.onload||"";a.$on("$routeChangeSuccess",x);x()}}}function A(d,h,g){return{restrict:"ECA",priority:-400,link:function(a,c){var b=g.current,f=b.locals;c.html(f.$template);var y=d(c.contents());b.controller&&(f.$scope=a,f=h(b.controller,f),b.controllerAs&&(a[b.controllerAs]=f),c.data("$ngControllerController",f),c.children().data("$ngControllerController",f));y(a)}}}p=d.module("ngRoute",["ng"]).provider("$route",function(){function r(a,c){return d.extend(Object.create(a), +c)}function h(a,d){var b=d.caseInsensitiveMatch,f={originalPath:a,regexp:a},g=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,d,b,c){a="?"===c?c:null;c="*"===c?c:null;g.push({name:b,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=new RegExp("^"+a+"$",b?"i":"");return f}var g={};this.when=function(a,c){var b=d.copy(c);d.isUndefined(b.reloadOnSearch)&&(b.reloadOnSearch=!0); +d.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);g[a]=d.extend(b,a&&h(a,b));if(a){var f="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";g[f]=d.extend({redirectTo:a},h(f,b))}return this};this.caseInsensitiveMatch=!1;this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,c,b,f,h,p,x){function l(b){var e=s.current; +(v=(n=k())&&e&&n.$$route===e.$$route&&d.equals(n.pathParams,e.pathParams)&&!n.reloadOnSearch&&!w)||!e&&!n||a.$broadcast("$routeChangeStart",n,e).defaultPrevented&&b&&b.preventDefault()}function m(){var u=s.current,e=n;if(v)u.params=e.params,d.copy(u.params,b),a.$broadcast("$routeUpdate",u);else if(e||u)w=!1,(s.current=e)&&e.redirectTo&&(d.isString(e.redirectTo)?c.path(t(e.redirectTo,e.params)).search(e.params).replace():c.url(e.redirectTo(e.pathParams,c.path(),c.search())).replace()),f.when(e).then(function(){if(e){var a= +d.extend({},e.resolve),b,c;d.forEach(a,function(b,e){a[e]=d.isString(b)?h.get(b):h.invoke(b,null,null,e)});d.isDefined(b=e.template)?d.isFunction(b)&&(b=b(e.params)):d.isDefined(c=e.templateUrl)&&(d.isFunction(c)&&(c=c(e.params)),c=x.getTrustedResourceUrl(c),d.isDefined(c)&&(e.loadedTemplateUrl=c,b=p(c)));d.isDefined(b)&&(a.$template=b);return f.all(a)}}).then(function(c){e==s.current&&(e&&(e.locals=c,d.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,u))},function(b){e==s.current&&a.$broadcast("$routeChangeError", +e,u,b)})}function k(){var a,b;d.forEach(g,function(f,g){var q;if(q=!b){var h=c.path();q=f.keys;var l={};if(f.regexp)if(h=f.regexp.exec(h)){for(var k=1,m=h.length;k> 1; + if (value < data[mid][0]) + high = mid; + else + low = mid + 1; + } + return low; + } + }; + + /** + * Initialises a new TimeSeries with optional data options. + * + * Options are of the form (defaults shown): + * + *
+   * {
+   *   resetBounds: true,        // enables/disables automatic scaling of the y-axis
+   *   resetBoundsInterval: 3000 // the period between scaling calculations, in millis
+   * }
+   * 
+ * + * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries. + * + * @constructor + */ + function TimeSeries(options) { + this.options = Util.extend({}, TimeSeries.defaultOptions, options); + this.clear(); + } + + TimeSeries.defaultOptions = { + resetBoundsInterval: 3000, + resetBounds: true + }; + + /** + * Clears all data and state from this TimeSeries object. + */ + TimeSeries.prototype.clear = function() { + this.data = []; + this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries. + this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries. + }; + + /** + * Recalculate the min/max values for this TimeSeries object. + * + * This causes the graph to scale itself in the y-axis. + */ + TimeSeries.prototype.resetBounds = function() { + if (this.data.length) { + // Walk through all data points, finding the min/max value + this.maxValue = this.data[0][1]; + this.minValue = this.data[0][1]; + for (var i = 1; i < this.data.length; i++) { + var value = this.data[i][1]; + if (value > this.maxValue) { + this.maxValue = value; + } + if (value < this.minValue) { + this.minValue = value; + } + } + } else { + // No data exists, so set min/max to NaN + this.maxValue = Number.NaN; + this.minValue = Number.NaN; + } + }; + + /** + * Adds a new data point to the TimeSeries, preserving chronological order. + * + * @param timestamp the position, in time, of this data point + * @param value the value of this data point + * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls + * whether it is replaced, or the values summed (defaults to false.) + */ + TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) { + // Rewind until we hit an older timestamp + var i = this.data.length - 1; + while (i >= 0 && this.data[i][0] > timestamp) { + i--; + } + + if (i === -1) { + // This new item is the oldest data + this.data.splice(0, 0, [timestamp, value]); + } else if (this.data.length > 0 && this.data[i][0] === timestamp) { + // Update existing values in the array + if (sumRepeatedTimeStampValues) { + // Sum this value into the existing 'bucket' + this.data[i][1] += value; + value = this.data[i][1]; + } else { + // Replace the previous value + this.data[i][1] = value; + } + } else if (i < this.data.length - 1) { + // Splice into the correct position to keep timestamps in order + this.data.splice(i + 1, 0, [timestamp, value]); + } else { + // Add to the end of the array + this.data.push([timestamp, value]); + } + + this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); + this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); + }; + + TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) { + // We must always keep one expired data point as we need this to draw the + // line that comes into the chart from the left, but any points prior to that can be removed. + var removeCount = 0; + while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { + removeCount++; + } + if (removeCount !== 0) { + this.data.splice(0, removeCount); + } + }; + + /** + * Initialises a new SmoothieChart. + * + * Options are optional, and should be of the form below. Just specify the values you + * need and the rest will be given sensible defaults as shown: + * + *
+   * {
+   *   minValue: undefined,                      // specify to clamp the lower y-axis to a given value
+   *   maxValue: undefined,                      // specify to clamp the upper y-axis to a given value
+   *   maxValueScale: 1,                         // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
+   *   minValueScale: 1,                         // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
+   *   yRangeFunction: undefined,                // function({min: , max: }) { return {min: , max: }; }
+   *   scaleSmoothing: 0.125,                    // controls the rate at which y-value zoom animation occurs
+   *   millisPerPixel: 20,                       // sets the speed at which the chart pans by
+   *   enableDpiScaling: true,                   // support rendering at different DPI depending on the device
+   *   yMinFormatter: function(min, precision) { // callback function that formats the min y value label
+   *     return parseFloat(min).toFixed(precision);
+   *   },
+   *   yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
+   *     return parseFloat(max).toFixed(precision);
+   *   },
+   *   maxDataSetLength: 2,
+   *   interpolation: 'bezier'                   // one of 'bezier', 'linear', or 'step'
+   *   timestampFormatter: null,                 // optional function to format time stamps for bottom of chart
+   *                                             // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
+   *   scrollBackwards: false,                   // reverse the scroll direction of the chart
+   *   horizontalLines: [],                      // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
+   *   grid:
+   *   {
+   *     fillStyle: '#000000',                   // the background colour of the chart
+   *     lineWidth: 1,                           // the pixel width of grid lines
+   *     strokeStyle: '#777777',                 // colour of grid lines
+   *     millisPerLine: 1000,                    // distance between vertical grid lines
+   *     sharpLines: false,                      // controls whether grid lines are 1px sharp, or softened
+   *     verticalSections: 2,                    // number of vertical sections marked out by horizontal grid lines
+   *     borderVisible: true                     // whether the grid lines trace the border of the chart or not
+   *   },
+   *   labels
+   *   {
+   *     disabled: false,                        // enables/disables labels showing the min/max values
+   *     fillStyle: '#ffffff',                   // colour for text of labels,
+   *     fontSize: 15,
+   *     fontFamily: 'sans-serif',
+   *     precision: 2
+   *   },
+   *   tooltip: false                            // show tooltip when mouse is over the chart
+   *   tooltipLine: {                            // properties for a vertical line at the cursor position
+   *     lineWidth: 1,
+   *     strokeStyle: '#BBBBBB'
+   *   },
+   *   tooltipFormatter: SmoothieChart.tooltipFormatter, // formatter function for tooltip text
+   *   responsive: false,                        // whether the chart should adapt to the size of the canvas
+   *   limitFPS: 0                         // maximum frame rate the chart will render at, in FPS (zero means no limit)
+   * }
+   * 
+ * + * @constructor + */ + function SmoothieChart(options) { + this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); + this.seriesSet = []; + this.currentValueRange = 1; + this.currentVisMinValue = 0; + this.lastRenderTimeMillis = 0; + + this.mousemove = this.mousemove.bind(this); + this.mouseout = this.mouseout.bind(this); + } + + /** Formats the HTML string content of the tooltip. */ + SmoothieChart.tooltipFormatter = function (timestamp, data) { + var timestampFormatter = this.options.timestampFormatter || SmoothieChart.timeFormatter, + lines = [timestampFormatter(new Date(timestamp))]; + + for (var i = 0; i < data.length; ++i) { + lines.push('' + + this.options.yMaxFormatter(data[i].value, this.options.labels.precision) + ''); + } + + return lines.join('
'); + }; + + SmoothieChart.defaultChartOptions = { + millisPerPixel: 20, + enableDpiScaling: true, + yMinFormatter: function(min, precision) { + return parseFloat(min).toFixed(precision); + }, + yMaxFormatter: function(max, precision) { + return parseFloat(max).toFixed(precision); + }, + maxValueScale: 1, + minValueScale: 1, + interpolation: 'bezier', + scaleSmoothing: 0.125, + maxDataSetLength: 2, + scrollBackwards: false, + grid: { + fillStyle: '#000000', + strokeStyle: '#777777', + lineWidth: 1, + sharpLines: false, + millisPerLine: 1000, + verticalSections: 2, + borderVisible: true + }, + labels: { + fillStyle: '#ffffff', + disabled: false, + fontSize: 10, + fontFamily: 'monospace', + precision: 2 + }, + horizontalLines: [], + tooltip: false, + tooltipLine: { + lineWidth: 1, + strokeStyle: '#BBBBBB' + }, + tooltipFormatter: SmoothieChart.tooltipFormatter, + responsive: false, + limitFPS: 0 + }; + + // Based on http://inspirit.github.com/jsfeat/js/compatibility.js + SmoothieChart.AnimateCompatibility = (function() { + var requestAnimationFrame = function(callback, element) { + var requestAnimationFrame = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(function() { + callback(Date.now()); + }, 16); + }; + return requestAnimationFrame.call(window, callback, element); + }, + cancelAnimationFrame = function(id) { + var cancelAnimationFrame = + window.cancelAnimationFrame || + function(id) { + clearTimeout(id); + }; + return cancelAnimationFrame.call(window, id); + }; + + return { + requestAnimationFrame: requestAnimationFrame, + cancelAnimationFrame: cancelAnimationFrame + }; + })(); + + SmoothieChart.defaultSeriesPresentationOptions = { + lineWidth: 1, + strokeStyle: '#ffffff' + }; + + /** + * Adds a TimeSeries to this chart, with optional presentation options. + * + * Presentation options should be of the form (defaults shown): + * + *
+   * {
+   *   lineWidth: 1,
+   *   strokeStyle: '#ffffff',
+   *   fillStyle: undefined
+   * }
+   * 
+ */ + SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { + this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)}); + if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { + timeSeries.resetBoundsTimerId = setInterval( + function() { + timeSeries.resetBounds(); + }, + timeSeries.options.resetBoundsInterval + ); + } + }; + + /** + * Removes the specified TimeSeries from the chart. + */ + SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + this.seriesSet.splice(i, 1); + break; + } + } + // If a timer was operating for that timeseries, remove it + if (timeSeries.resetBoundsTimerId) { + // Stop resetting the bounds, if we were + clearInterval(timeSeries.resetBoundsTimerId); + } + }; + + /** + * Gets render options for the specified TimeSeries. + * + * As you may use a single TimeSeries in multiple charts with different formatting in each usage, + * these settings are stored in the chart. + */ + SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + return this.seriesSet[i].options; + } + } + }; + + /** + * Brings the specified TimeSeries to the top of the chart. It will be rendered last. + */ + SmoothieChart.prototype.bringToFront = function(timeSeries) { + // Find the correct timeseries to remove, and remove it + var numSeries = this.seriesSet.length; + for (var i = 0; i < numSeries; i++) { + if (this.seriesSet[i].timeSeries === timeSeries) { + var set = this.seriesSet.splice(i, 1); + this.seriesSet.push(set[0]); + break; + } + } + }; + + /** + * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay. + * + * @param canvas the target canvas element + * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series + * from appearing on screen, with new values flashing into view, at the expense of some latency. + */ + SmoothieChart.prototype.streamTo = function(canvas, delayMillis) { + this.canvas = canvas; + this.delay = delayMillis; + this.start(); + }; + + SmoothieChart.prototype.getTooltipEl = function () { + // Create the tool tip element lazily + if (!this.tooltipEl) { + this.tooltipEl = document.createElement('div'); + this.tooltipEl.className = 'smoothie-chart-tooltip'; + this.tooltipEl.style.position = 'absolute'; + this.tooltipEl.style.display = 'none'; + document.body.appendChild(this.tooltipEl); + } + return this.tooltipEl; + }; + + SmoothieChart.prototype.updateTooltip = function () { + var el = this.getTooltipEl(); + + if (!this.mouseover || !this.options.tooltip) { + el.style.display = 'none'; + return; + } + + var time = this.lastRenderTimeMillis - (this.delay || 0); + + // Round time down to pixel granularity, so motion appears smoother. + time -= time % this.options.millisPerPixel; + + // x pixel to time + var t = this.options.scrollBackwards + ? time - this.mouseX * this.options.millisPerPixel + : time - (this.canvas.offsetWidth - this.mouseX) * this.options.millisPerPixel; + + var data = []; + + // For each data set... + for (var d = 0; d < this.seriesSet.length; d++) { + var timeSeries = this.seriesSet[d].timeSeries, + // find datapoint closest to time 't' + closeIdx = Util.binarySearch(timeSeries.data, t); + + if (closeIdx > 0 && closeIdx < timeSeries.data.length) { + data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] }); + } + } + + if (data.length) { + el.innerHTML = this.options.tooltipFormatter.call(this, t, data); + el.style.display = 'block'; + } else { + el.style.display = 'none'; + } + }; + + SmoothieChart.prototype.mousemove = function (evt) { + this.mouseover = true; + this.mouseX = evt.offsetX; + this.mouseY = evt.offsetY; + this.mousePageX = evt.pageX; + this.mousePageY = evt.pageY; + + var el = this.getTooltipEl(); + el.style.top = Math.round(this.mousePageY) + 'px'; + el.style.left = Math.round(this.mousePageX) + 'px'; + this.updateTooltip(); + }; + + SmoothieChart.prototype.mouseout = function () { + this.mouseover = false; + this.mouseX = this.mouseY = -1; + if (SmoothieChart.tooltipEl) + SmoothieChart.tooltipEl.style.display = 'none'; + }; + + /** + * Make sure the canvas has the optimal resolution for the device's pixel ratio. + */ + SmoothieChart.prototype.resize = function () { + var dpr = !this.options.enableDpiScaling || !window ? 1 : window.devicePixelRatio, + width, height; + if (this.options.responsive) { + // Newer behaviour: Use the canvas's size in the layout, and set the internal + // resolution according to that size and the device pixel ratio (eg: high DPI) + width = this.canvas.offsetWidth; + height = this.canvas.offsetHeight; + + if (width !== this.lastWidth) { + this.lastWidth = width; + this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); + } + if (height !== this.lastHeight) { + this.lastHeight = height; + this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); + } + } else if (dpr !== 1) { + // Older behaviour: use the canvas's inner dimensions and scale the element's size + // according to that size and the device pixel ratio (eg: high DPI) + width = parseInt(this.canvas.getAttribute('width')); + height = parseInt(this.canvas.getAttribute('height')); + + if (!this.originalWidth || (Math.floor(this.originalWidth * dpr) !== width)) { + this.originalWidth = width; + this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); + this.canvas.style.width = width + 'px'; + this.canvas.getContext('2d').scale(dpr, dpr); + } + + if (!this.originalHeight || (Math.floor(this.originalHeight * dpr) !== height)) { + this.originalHeight = height; + this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); + this.canvas.style.height = height + 'px'; + this.canvas.getContext('2d').scale(dpr, dpr); + } + } + }; + + /** + * Starts the animation of this chart. + */ + SmoothieChart.prototype.start = function() { + if (this.frame) { + // We're already running, so just return + return; + } + + this.canvas.addEventListener('mousemove', this.mousemove); + this.canvas.addEventListener('mouseout', this.mouseout); + + // Renders a frame, and queues the next frame for later rendering + var animate = function() { + this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() { + this.render(); + animate(); + }.bind(this)); + }.bind(this); + + animate(); + }; + + /** + * Stops the animation of this chart. + */ + SmoothieChart.prototype.stop = function() { + if (this.frame) { + SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); + delete this.frame; + this.canvas.removeEventListener('mousemove', this.mousemove); + this.canvas.removeEventListener('mouseout', this.mouseout); + } + }; + + SmoothieChart.prototype.updateValueRange = function() { + // Calculate the current scale of the chart, from all time series. + var chartOptions = this.options, + chartMaxValue = Number.NaN, + chartMinValue = Number.NaN; + + for (var d = 0; d < this.seriesSet.length; d++) { + // TODO(ndunn): We could calculate / track these values as they stream in. + var timeSeries = this.seriesSet[d].timeSeries; + if (!isNaN(timeSeries.maxValue)) { + chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; + } + + if (!isNaN(timeSeries.minValue)) { + chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; + } + } + + // Scale the chartMaxValue to add padding at the top if required + if (chartOptions.maxValue != null) { + chartMaxValue = chartOptions.maxValue; + } else { + chartMaxValue *= chartOptions.maxValueScale; + } + + // Set the minimum if we've specified one + if (chartOptions.minValue != null) { + chartMinValue = chartOptions.minValue; + } else { + chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue); + } + + // If a custom range function is set, call it + if (this.options.yRangeFunction) { + var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue}); + chartMinValue = range.min; + chartMaxValue = range.max; + } + + if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { + var targetValueRange = chartMaxValue - chartMinValue; + var valueRangeDiff = (targetValueRange - this.currentValueRange); + var minValueDiff = (chartMinValue - this.currentVisMinValue); + this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1; + this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff; + this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff; + } + + this.valueRange = { min: chartMinValue, max: chartMaxValue }; + }; + + SmoothieChart.prototype.render = function(canvas, time) { + var nowMillis = Date.now(); + + // Respect any frame rate limit. + if (this.options.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < (1000/this.options.limitFPS)) + return; + + if (!this.isAnimatingScale) { + // We're not animating. We can use the last render time and the scroll speed to work out whether + // we actually need to paint anything yet. If not, we can return immediately. + + // Render at least every 1/6th of a second. The canvas may be resized, which there is + // no reliable way to detect. + var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel); + + if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) { + return; + } + } + + this.resize(); + this.updateTooltip(); + + this.lastRenderTimeMillis = nowMillis; + + canvas = canvas || this.canvas; + time = time || nowMillis - (this.delay || 0); + + // Round time down to pixel granularity, so motion appears smoother. + time -= time % this.options.millisPerPixel; + + var context = canvas.getContext('2d'), + chartOptions = this.options, + dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight }, + // Calculate the threshold time for the oldest data points. + oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel), + valueToYPixel = function(value) { + var offset = value - this.currentVisMinValue; + return this.currentValueRange === 0 + ? dimensions.height + : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height)); + }.bind(this), + timeToXPixel = function(t) { + if(chartOptions.scrollBackwards) { + return Math.round((time - t) / chartOptions.millisPerPixel); + } + return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel)); + }; + + this.updateValueRange(); + + context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; + + // Save the state of the canvas context, any transformations applied in this method + // will get removed from the stack at the end of this method when .restore() is called. + context.save(); + + // Move the origin. + context.translate(dimensions.left, dimensions.top); + + // Create a clipped rectangle - anything we draw will be constrained to this rectangle. + // This prevents the occasional pixels from curves near the edges overrunning and creating + // screen cheese (that phrase should need no explanation). + context.beginPath(); + context.rect(0, 0, dimensions.width, dimensions.height); + context.clip(); + + // Clear the working area. + context.save(); + context.fillStyle = chartOptions.grid.fillStyle; + context.clearRect(0, 0, dimensions.width, dimensions.height); + context.fillRect(0, 0, dimensions.width, dimensions.height); + context.restore(); + + // Grid lines... + context.save(); + context.lineWidth = chartOptions.grid.lineWidth; + context.strokeStyle = chartOptions.grid.strokeStyle; + // Vertical (time) dividers. + if (chartOptions.grid.millisPerLine > 0) { + context.beginPath(); + for (var t = time - (time % chartOptions.grid.millisPerLine); + t >= oldestValidTime; + t -= chartOptions.grid.millisPerLine) { + var gx = timeToXPixel(t); + if (chartOptions.grid.sharpLines) { + gx -= 0.5; + } + context.moveTo(gx, 0); + context.lineTo(gx, dimensions.height); + } + context.stroke(); + context.closePath(); + } + + // Horizontal (value) dividers. + for (var v = 1; v < chartOptions.grid.verticalSections; v++) { + var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections); + if (chartOptions.grid.sharpLines) { + gy -= 0.5; + } + context.beginPath(); + context.moveTo(0, gy); + context.lineTo(dimensions.width, gy); + context.stroke(); + context.closePath(); + } + // Bounding rectangle. + if (chartOptions.grid.borderVisible) { + context.beginPath(); + context.strokeRect(0, 0, dimensions.width, dimensions.height); + context.closePath(); + } + context.restore(); + + // Draw any horizontal lines... + if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { + for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { + var line = chartOptions.horizontalLines[hl], + hly = Math.round(valueToYPixel(line.value)) - 0.5; + context.strokeStyle = line.color || '#ffffff'; + context.lineWidth = line.lineWidth || 1; + context.beginPath(); + context.moveTo(0, hly); + context.lineTo(dimensions.width, hly); + context.stroke(); + context.closePath(); + } + } + + // For each data set... + for (var d = 0; d < this.seriesSet.length; d++) { + context.save(); + var timeSeries = this.seriesSet[d].timeSeries, + dataSet = timeSeries.data, + seriesOptions = this.seriesSet[d].options; + + // Delete old data that's moved off the left of the chart. + timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); + + // Set style for this dataSet. + context.lineWidth = seriesOptions.lineWidth; + context.strokeStyle = seriesOptions.strokeStyle; + // Draw the line... + context.beginPath(); + // Retain lastX, lastY for calculating the control points of bezier curves. + var firstX = 0, lastX = 0, lastY = 0; + for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) { + var x = timeToXPixel(dataSet[i][0]), + y = valueToYPixel(dataSet[i][1]); + + if (i === 0) { + firstX = x; + context.moveTo(x, y); + } else { + switch (chartOptions.interpolation) { + case "linear": + case "line": { + context.lineTo(x,y); + break; + } + case "bezier": + default: { + // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves + // + // Assuming A was the last point in the line plotted and B is the new point, + // we draw a curve with control points P and Q as below. + // + // A---P + // | + // | + // | + // Q---B + // + // Importantly, A and P are at the same y coordinate, as are B and Q. This is + // so adjacent curves appear to flow as one. + // + context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop + Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) + Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) + x, y); // endPoint (B) + break; + } + case "step": { + context.lineTo(x,lastY); + context.lineTo(x,y); + break; + } + } + } + + lastX = x; lastY = y; + } + + if (dataSet.length > 1) { + if (seriesOptions.fillStyle) { + // Close up the fill region. + context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); + context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); + context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); + context.fillStyle = seriesOptions.fillStyle; + context.fill(); + } + + if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') { + context.stroke(); + } + context.closePath(); + } + context.restore(); + } + + if (chartOptions.tooltip && this.mouseX >= 0) { + // Draw vertical bar to show tooltip position + context.lineWidth = chartOptions.tooltipLine.lineWidth; + context.strokeStyle = chartOptions.tooltipLine.strokeStyle; + context.beginPath(); + context.moveTo(this.mouseX, 0); + context.lineTo(this.mouseX, dimensions.height); + context.closePath(); + context.stroke(); + this.updateTooltip(); + } + + // Draw the axis values on the chart. + if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { + var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision), + minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision), + maxLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2, + minLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(minValueString).width - 2; + context.fillStyle = chartOptions.labels.fillStyle; + context.fillText(maxValueString, maxLabelPos, chartOptions.labels.fontSize); + context.fillText(minValueString, minLabelPos, dimensions.height - 2); + } + + // Display timestamps along x-axis at the bottom of the chart. + if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) { + var textUntilX = chartOptions.scrollBackwards + ? context.measureText(minValueString).width + : dimensions.width - context.measureText(minValueString).width + 4; + for (var t = time - (time % chartOptions.grid.millisPerLine); + t >= oldestValidTime; + t -= chartOptions.grid.millisPerLine) { + var gx = timeToXPixel(t); + // Only draw the timestamp if it won't overlap with the previously drawn one. + if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) { + // Formats the timestamp based on user specified formatting function + // SmoothieChart.timeFormatter function above is one such formatting option + var tx = new Date(t), + ts = chartOptions.timestampFormatter(tx), + tsWidth = context.measureText(ts).width; + + textUntilX = chartOptions.scrollBackwards + ? gx + tsWidth + 2 + : gx - tsWidth - 2; + + context.fillStyle = chartOptions.labels.fillStyle; + if(chartOptions.scrollBackwards) { + context.fillText(ts, gx, dimensions.height - 2); + } else { + context.fillText(ts, gx - tsWidth, dimensions.height - 2); + } + } + } + } + + context.restore(); // See .save() above. + }; + + // Sample timestamp formatting function + SmoothieChart.timeFormatter = function(date) { + function pad2(number) { return (number < 10 ? '0' : '') + number } + return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); + }; + + exports.TimeSeries = TimeSeries; + exports.SmoothieChart = SmoothieChart; + +})(typeof exports === 'undefined' ? this : exports); + +/*! Sortable 1.7.0 - MIT | git://github.com/rubaxa/Sortable.git */ +!function(a){"use strict";"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports=a():window.Sortable=a()}(function(){"use strict";function a(b,c){if(!b||!b.nodeType||1!==b.nodeType)throw"Sortable: `el` must be HTMLElement, and not "+{}.toString.call(b);this.el=b,this.options=c=t({},c),b[V]=this;var d={group:Math.random(),sort:!0,disabled:!1,store:null,handle:null,scroll:!0,scrollSensitivity:30,scrollSpeed:10,draggable:/[uo]l/i.test(b.nodeName)?"li":">*",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,setData:function(a,b){a.setData("Text",b.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:a.supportPointer!==!1};for(var e in d)!(e in c)&&(c[e]=d[e]);ka(c);for(var g in this)"_"===g.charAt(0)&&"function"==typeof this[g]&&(this[g]=this[g].bind(this));this.nativeDraggable=!c.forceFallback&&ca,f(b,"mousedown",this._onTapStart),f(b,"touchstart",this._onTapStart),c.supportPointer&&f(b,"pointerdown",this._onTapStart),this.nativeDraggable&&(f(b,"dragover",this),f(b,"dragenter",this)),ia.push(this._onDragOver),c.store&&this.sort(c.store.get(this))}function b(a,b){"clone"!==a.lastPullMode&&(b=!0),B&&B.state!==b&&(i(B,"display",b?"none":""),b||B.state&&(a.options.group.revertClone?(C.insertBefore(B,D),a._animate(y,B)):C.insertBefore(B,y)),B.state=b)}function c(a,b,c){if(a){c=c||X;do if(">*"===b&&a.parentNode===c||r(a,b))return a;while(a=d(a))}return null}function d(a){var b=a.host;return b&&b.nodeType?b:a.parentNode}function e(a){a.dataTransfer&&(a.dataTransfer.dropEffect="move"),a.preventDefault()}function f(a,b,c){a.addEventListener(b,c,aa)}function g(a,b,c){a.removeEventListener(b,c,aa)}function h(a,b,c){if(a)if(a.classList)a.classList[c?"add":"remove"](b);else{var d=(" "+a.className+" ").replace(T," ").replace(" "+b+" "," ");a.className=(d+(c?" "+b:"")).replace(T," ")}}function i(a,b,c){var d=a&&a.style;if(d){if(void 0===c)return X.defaultView&&X.defaultView.getComputedStyle?c=X.defaultView.getComputedStyle(a,""):a.currentStyle&&(c=a.currentStyle),void 0===b?c:c[b];b in d||(b="-webkit-"+b),d[b]=c+("string"==typeof c?"":"px")}}function j(a,b,c){if(a){var d=a.getElementsByTagName(b),e=0,f=d.length;if(c)for(;e5||b.clientX-(d.left+d.width)>5}function p(a){for(var b=a.tagName+a.className+a.src+a.href+a.textContent,c=b.length,d=0;c--;)d+=b.charCodeAt(c);return d.toString(36)}function q(a,b){var c=0;if(!a||!a.parentNode)return-1;for(;a&&(a=a.previousElementSibling);)"TEMPLATE"===a.nodeName.toUpperCase()||">*"!==b&&!r(a,b)||c++;return c}function r(a,b){if(a){b=b.split(".");var c=b.shift().toUpperCase(),d=new RegExp("\\s("+b.join("|")+")(?=\\s)","g");return!(""!==c&&a.nodeName.toUpperCase()!=c||b.length&&((" "+a.className+" ").match(d)||[]).length!=b.length)}return!1}function s(a,b){var c,d;return function(){void 0===c&&(c=arguments,d=this,Z(function(){1===c.length?a.call(d,c[0]):a.apply(d,c),c=void 0},b))}}function t(a,b){if(a&&b)for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return a}function u(a){return _&&_.dom?_.dom(a).cloneNode(!0):$?$(a).clone(!0)[0]:a.cloneNode(!0)}function v(a){for(var b=a.getElementsByTagName("input"),c=b.length;c--;){var d=b[c];d.checked&&ha.push(d)}}function w(a){return Z(a,0)}function x(a){return clearTimeout(a)}if("undefined"==typeof window||!window.document)return function(){throw new Error("Sortable.js requires a window with a document")};var y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S={},T=/\s+/g,U=/left|right|inline/,V="Sortable"+(new Date).getTime(),W=window,X=W.document,Y=W.parseInt,Z=W.setTimeout,$=W.jQuery||W.Zepto,_=W.Polymer,aa=!1,ba=!1,ca="draggable"in X.createElement("div"),da=function(a){return!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)&&(a=X.createElement("x"),a.style.cssText="pointer-events:auto","auto"===a.style.pointerEvents)}(),ea=!1,fa=Math.abs,ga=Math.min,ha=[],ia=[],ja=s(function(a,b,c){if(c&&b.scroll){var d,e,f,g,h,i,j=c[V],k=b.scrollSensitivity,l=b.scrollSpeed,m=a.clientX,n=a.clientY,o=window.innerWidth,p=window.innerHeight;if(G!==c&&(F=b.scroll,G=c,H=b.scrollFn,F===!0)){F=c;do if(F.offsetWidth-1:e==a)}}var c={},d=a.group;d&&"object"==typeof d||(d={name:d}),c.name=d.name,c.checkPull=b(d.pull,!0),c.checkPut=b(d.put),c.revertClone=d.revertClone,a.group=c};try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:function(){ba=!1,aa={capture:!1,passive:ba}}}))}catch(a){}return a.prototype={constructor:a,_onTapStart:function(a){var b,d=this,e=this.el,f=this.options,g=f.preventOnFilter,h=a.type,i=a.touches&&a.touches[0],j=(i||a).target,l=a.target.shadowRoot&&a.path&&a.path[0]||j,m=f.filter;if(v(e),!y&&!(/mousedown|pointerdown/.test(h)&&0!==a.button||f.disabled)&&!l.isContentEditable&&(j=c(j,f.draggable,e),j&&E!==j)){if(b=q(j,f.draggable),"function"==typeof m){if(m.call(this,a,j,this))return k(d,l,"filter",j,e,e,b),void(g&&a.preventDefault())}else if(m&&(m=m.split(",").some(function(a){if(a=c(l,a.trim(),e))return k(d,a,"filter",j,e,e,b),!0})))return void(g&&a.preventDefault());f.handle&&!c(l,f.handle,e)||this._prepareDragStart(a,i,j,b)}},_prepareDragStart:function(a,b,c,d){var e,g=this,i=g.el,l=g.options,n=i.ownerDocument;c&&!y&&c.parentNode===i&&(P=a,C=i,y=c,z=y.parentNode,D=y.nextSibling,E=c,N=l.group,L=d,this._lastX=(b||a).clientX,this._lastY=(b||a).clientY,y.style["will-change"]="all",e=function(){g._disableDelayedDrag(),y.draggable=g.nativeDraggable,h(y,l.chosenClass,!0),g._triggerDragStart(a,b),k(g,C,"choose",y,C,C,L)},l.ignore.split(",").forEach(function(a){j(y,a.trim(),m)}),f(n,"mouseup",g._onDrop),f(n,"touchend",g._onDrop),f(n,"touchcancel",g._onDrop),f(n,"selectstart",g),l.supportPointer&&f(n,"pointercancel",g._onDrop),l.delay?(f(n,"mouseup",g._disableDelayedDrag),f(n,"touchend",g._disableDelayedDrag),f(n,"touchcancel",g._disableDelayedDrag),f(n,"mousemove",g._disableDelayedDrag),f(n,"touchmove",g._disableDelayedDrag),l.supportPointer&&f(n,"pointermove",g._disableDelayedDrag),g._dragStartTimer=Z(e,l.delay)):e())},_disableDelayedDrag:function(){var a=this.el.ownerDocument;clearTimeout(this._dragStartTimer),g(a,"mouseup",this._disableDelayedDrag),g(a,"touchend",this._disableDelayedDrag),g(a,"touchcancel",this._disableDelayedDrag),g(a,"mousemove",this._disableDelayedDrag),g(a,"touchmove",this._disableDelayedDrag),g(a,"pointermove",this._disableDelayedDrag)},_triggerDragStart:function(a,b){b=b||("touch"==a.pointerType?a:null),b?(P={target:y,clientX:b.clientX,clientY:b.clientY},this._onDragStart(P,"touch")):this.nativeDraggable?(f(y,"dragend",this),f(C,"dragstart",this._onDragStart)):this._onDragStart(P,!0);try{X.selection?w(function(){X.selection.empty()}):window.getSelection().removeAllRanges()}catch(a){}},_dragStarted:function(){if(C&&y){var b=this.options;h(y,b.ghostClass,!0),h(y,b.dragClass,!1),a.active=this,k(this,C,"start",y,C,C,L)}else this._nulling()},_emulateDragOver:function(){if(Q){if(this._lastX===Q.clientX&&this._lastY===Q.clientY)return;this._lastX=Q.clientX,this._lastY=Q.clientY,da||i(A,"display","none");var a=X.elementFromPoint(Q.clientX,Q.clientY),b=a,c=ia.length;if(a&&a.shadowRoot&&(a=a.shadowRoot.elementFromPoint(Q.clientX,Q.clientY),b=a),b)do{if(b[V]){for(;c--;)ia[c]({clientX:Q.clientX,clientY:Q.clientY,target:a,rootEl:b});break}a=b}while(b=b.parentNode);da||i(A,"display","")}},_onTouchMove:function(b){if(P){var c=this.options,d=c.fallbackTolerance,e=c.fallbackOffset,f=b.touches?b.touches[0]:b,g=f.clientX-P.clientX+e.x,h=f.clientY-P.clientY+e.y,j=b.touches?"translate3d("+g+"px,"+h+"px,0)":"translate("+g+"px,"+h+"px)";if(!a.active){if(d&&ga(fa(f.clientX-this._lastX),fa(f.clientY-this._lastY))y.offsetWidth,x=e.offsetHeight>y.offsetHeight,E=(v?(d.clientX-g.left)/t:(d.clientY-g.top)/u)>.5,F=e.nextElementSibling,G=!1;if(v){var H=y.offsetTop,L=e.offsetTop;G=H===L?e.previousElementSibling===y&&!w||E&&w:e.previousElementSibling===y||y.previousElementSibling===e?(d.clientY-g.top)/u>.5:L>H}else r||(G=F!==y&&!x||E&&x);var M=l(C,j,y,f,e,g,d,G);M!==!1&&(1!==M&&M!==-1||(G=1===M),ea=!0,Z(n,30),b(p,q),y.contains(j)||(G&&!F?j.appendChild(y):e.parentNode.insertBefore(y,G?F:e)),z=y.parentNode,this._animate(f,y),this._animate(g,e))}}},_animate:function(a,b){var c=this.options.animation;if(c){var d=b.getBoundingClientRect();1===a.nodeType&&(a=a.getBoundingClientRect()),i(b,"transition","none"),i(b,"transform","translate3d("+(a.left-d.left)+"px,"+(a.top-d.top)+"px,0)"),b.offsetWidth,i(b,"transition","all "+c+"ms"),i(b,"transform","translate3d(0,0,0)"),clearTimeout(b.animated),b.animated=Z(function(){i(b,"transition",""),i(b,"transform",""),b.animated=!1},c)}},_offUpEvents:function(){var a=this.el.ownerDocument;g(X,"touchmove",this._onTouchMove),g(X,"pointermove",this._onTouchMove),g(a,"mouseup",this._onDrop),g(a,"touchend",this._onDrop),g(a,"pointerup",this._onDrop),g(a,"touchcancel",this._onDrop),g(a,"pointercancel",this._onDrop),g(a,"selectstart",this)},_onDrop:function(b){var c=this.el,d=this.options;clearInterval(this._loopId),clearInterval(S.pid),clearTimeout(this._dragStartTimer),x(this._cloneId),x(this._dragStartId),g(X,"mouseover",this),g(X,"mousemove",this._onTouchMove),this.nativeDraggable&&(g(X,"drop",this),g(c,"dragstart",this._onDragStart)),this._offUpEvents(),b&&(R&&(b.preventDefault(),!d.dropBubble&&b.stopPropagation()),A&&A.parentNode&&A.parentNode.removeChild(A),C!==z&&"clone"===a.active.lastPullMode||B&&B.parentNode&&B.parentNode.removeChild(B),y&&(this.nativeDraggable&&g(y,"dragend",this),m(y),y.style["will-change"]="",h(y,this.options.ghostClass,!1),h(y,this.options.chosenClass,!1),k(this,C,"unchoose",y,z,C,L),C!==z?(M=q(y,d.draggable),M>=0&&(k(null,z,"add",y,z,C,L,M),k(this,C,"remove",y,z,C,L,M),k(null,z,"sort",y,z,C,L,M),k(this,C,"sort",y,z,C,L,M))):y.nextSibling!==D&&(M=q(y,d.draggable),M>=0&&(k(this,C,"update",y,z,C,L,M),k(this,C,"sort",y,z,C,L,M))),a.active&&(null!=M&&M!==-1||(M=L),k(this,C,"end",y,z,C,L,M),this.save()))),this._nulling()},_nulling:function(){C=y=z=A=D=B=E=F=G=P=Q=R=M=I=J=O=N=a.active=null,ha.forEach(function(a){a.checked=!0}),ha.length=0},handleEvent:function(a){switch(a.type){case"drop":case"dragend":this._onDrop(a);break;case"dragover":case"dragenter":y&&(this._onDragOver(a),e(a));break;case"mouseover":this._onDrop(a);break;case"selectstart":a.preventDefault()}},toArray:function(){for(var a,b=[],d=this.el.children,e=0,f=d.length,g=this.options;e', + '', + 'Loading...', + '
', + ].join(''), + controller: ['$scope', '$location', '$rootScope', appLoadController], + }) + + .when('/system-status', { + template: [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ].join(''), + }) + + .when('/basic-info', { + template: [ + '', + '', + '', + '', + '', + '', + ].join(''), + }) + + .when('/network', { + template: [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ].join(''), + }) + + .when('/accounts', { + template: [ + ' ', + ' ', + ' ', + ].join(''), + }) + + .when('/apps', { + template: [ + '', + '', + '', + '', + ].join(''), + }) + .otherwise({ + redirectTo: '/loading' + }) +} + +angular.module('linuxDash').config(['$routeProvider', routesFn]) + +angular + .module('linuxDash') + .service('server', [ + '$http', '$rootScope', '$location', + function($http, $rootScope, $location) { + + var websocket = { + connection: null, + onMessageEventHandlers: {} + }; + + /** + * @description: + * Establish a websocket connection with server + * + * @return Null + */ + var establishWebsocketConnection = function() { + + var websocketUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.hostname + ':' + window.location.port; + + if (websocket.connection === null) { + + websocket.connection = new WebSocket(websocketUrl); + + websocket.connection.onopen = function() { + $rootScope.$broadcast("start-linux-dash", {}); + $rootScope.$apply(); + console.info('Websocket connection is open'); + }; + + websocket.connection.onmessage = function(event) { + + var response = JSON.parse(event.data); + var moduleName = response.moduleName; + var moduleData = JSON.parse(response.output); + + if (!!websocket.onMessageEventHandlers[moduleName]) { + websocket.onMessageEventHandlers[moduleName](moduleData); + } else { + console.info("Websocket could not find module", moduleName, "in:", websocket.onMessageEventHandlers); + } + + }; + + websocket.connection.onclose = function() { + websocket.connection = null; + } + } + + }; + + /** + * @description: + * Check if websockets are supported + * If so, call establishWebsocketConnection() + * + * @return Null + */ + this.checkIfWebsocketsAreSupported = function() { + + var websocketSupport = { + browser: null, + server: null, + }; + + // does browser support websockets? + if (window.WebSocket) { + + websocketSupport.browser = true; + + // does backend support websockets? + $http.get("/websocket").then(function(response) { + + // if websocket_support property exists and is trurthy + // websocketSupport.server will equal true. + websocketSupport.server = !!response.data["websocket_support"]; + + }).catch(function websocketNotSupportedByServer() { + + websocketSupport.server = false; + $rootScope.$broadcast("start-linux-dash", {}); + + }).then(function finalDecisionOnWebsocket() { + + if (websocketSupport.browser && websocketSupport.server) { + + establishWebsocketConnection(); + + } else { + $rootScope.$broadcast("start-linux-dash", {}); + } + + }); + + } + + }; + + /** + * Handles requests from modules for data from server + * + * @param {String} moduleName + * @param {Function} callback + * @return {[ Null || callback(server response) ]} + */ + this.get = function(moduleName, callback) { + + // if we have a websocket connection + if (websocket.connection) { + + // and the connection is ready + if (websocket.connection.readyState === 1) { + + // set the callback as the event handler + // for server response. + // + // Callback instance needs to be overwritten + // each time for this to work. Not sure why. + websocket.onMessageEventHandlers[moduleName] = callback; + + // + websocket.connection.send(moduleName); + + } else { + console.log("Websocket not ready yet.", moduleName); + } + + } + // otherwise + else { + + var moduleAddress = 'server/?module=' + moduleName; + + return $http.get(moduleAddress).then(function(response) { + return callback(response.data); + }); + + } + + }; + + } +]) + +angular.module('linuxDash').directive('cpuAvgLoadChart', ['server', function(server) { + return { + restrict: 'E', + scope: {}, + template: '\ + \ + \ + ', + link: function(scope) { + scope.units = '%' + } + } +}]) + +angular.module('linuxDash').directive('cpuTemp', ['server', function(server) { + return { + restrict: 'E', + scope: {}, + template: ' \ + \ + \ + ', + link: function(scope) { + scope.min = 0 + scope.max = 100 + + scope.displayValue = function (serverResponseData) { + return serverResponseData + } + + scope.utilMetrics = [{ + name: 'Temprature', + generate: function (serverResponseData) { + return serverResponseData + ' °C' + } + }] + + } + } +}]) + +angular.module('linuxDash').directive('cpuUtilizationChart', ['server', function(server) { + return { + restrict: 'E', + scope: {}, + template: ' \ + \ + \ + ', + link: function(scope) { + scope.min = 0 + scope.max = 100 + + scope.displayValue = function(serverResponseData) { + return serverResponseData + } + + scope.utilMetrics = [{ + name: 'Usage', + generate: function(serverResponseData) { + return serverResponseData + ' %' + } + }] + + } + } +}]) + +angular.module('linuxDash').directive('downloadTransferRateChart', ['server', function(server) { + return { + restrict: 'E', + scope: {}, + template: ' \ + \ + \ + ', + link: function(scope) { + scope.delay = 2000 + scope.units = 'KB/s' + } + } +}]) + +angular.module('linuxDash').directive('ramChart', ['server', function (server) { + return { + restrict: 'E', + scope: {}, + template: '\ + \ + \ + ', + link: function(scope) { + + // get max ram available on machine before we + // can start charting + server.get('current_ram', function(resp) { + scope.maxRam = resp.total + scope.minRam = 0 + }) + + scope.ramToDisplay = function(serverResponseData) { + return serverResponseData.used + } + + var humanizeRam = function (ramInMB) { + var ram = { + value: parseInt(ramInMB, 10), + unit: 'MB', + } + + // if ram > 1,000 MB, use GB + if (ram.value > 1000) { + ram = { + value: (ramInMB/1024).toFixed(2), + unit: 'GB', + } + } + + return ram.value + ' ' + ram.unit + } + + scope.ramMetrics = [{ + name: 'Used', + generate: function(serverResponseData) { + var ratio = serverResponseData.used / serverResponseData.total + var percentage = parseInt(ratio * 100) + + var usedRam = humanizeRam(serverResponseData.used) + return usedRam + ' (' + percentage.toString() + '%)' + } + }, + { + name: 'Available', + generate: function(serverResponseData) { + + var availableRam = humanizeRam(serverResponseData.available) + var totalRam = humanizeRam(serverResponseData.total) + return availableRam + ' of ' + totalRam + } + }] + } + } +}]) + +var simpleTableModules = [ + { + name: 'machineInfo', + template: '' + }, + { + name: 'ipAddresses', + template: '' + }, + { + name: 'ramIntensiveProcesses', + template: '' + }, + { + name: 'cpuIntensiveProcesses', + template: '' + }, + { + name: 'dockerProcesses', + template: '' + }, + { + name: 'networkConnections', + template: '' + }, + { + name: 'serverAccounts', + template: '' + }, + { + name: 'loggedInAccounts', + template: '' + }, + { + name: 'recentLogins', + template: '' + }, + { + name: 'arpCacheTable', + template: '' + }, + { + name: 'commonApplications', + template: '' + }, + { + name: 'pingSpeeds', + template: '' + }, + { + name: 'bandwidth', + template: '' + }, + { + name: 'swapUsage', + template: '' + }, + { + name: 'internetSpeed', + template: '' + }, + { + name: 'memcached', + template: '' + }, + { + name: 'redis', + template: '' + }, + { + name: 'pm2', + template: '' + }, + { + name: 'memoryInfo', + template: '' + }, + { + name: 'cpuInfo', + template: '' + }, + { + name: 'ioStats', + template: '' + }, + { + name: 'scheduledCrons', + template: '' + }, + { + name: 'cronHistory', + template: '' + } +] + +simpleTableModules.forEach(function(module, key) { + + angular.module('linuxDash').directive(module.name, ['server', function(server) { + + var moduleDirective = { + restrict: 'E', + scope: {} + } + + moduleDirective['template'] = module.template + + return moduleDirective + }]) + +}) + +angular.module('linuxDash').directive('uploadTransferRateChart', ['server', function(server) { + return { + restrict: 'E', + scope: {}, + template: ' \ + \ + \ + ', + link: function(scope) { + scope.delay = 2000 + scope.units = 'KB/s' + } + } +}]) + +angular + .module('linuxDash') + .run(['$rootScope', '$location', function ($rootScope, $location) { + + var key = 'hiddenPlugins' + + var getHiddenPlugins = function () { + var hiddenPluginsCSV = localStorage.getItem(key) || '' + return hiddenPluginsCSV.split(',') + } + + var updateHiddenPlugins = function (hiddenPlugins) { + localStorage.setItem(key, hiddenPlugins.join(',')) + } + + $rootScope.$on('hide-plugin', function (e, m) { + var hiddenPlugins = getHiddenPlugins() + + if(hiddenPlugins.indexOf(m) < 0) + hiddenPlugins.push(m) + + updateHiddenPlugins(hiddenPlugins) + }) + + $rootScope.$on('show-plugin', function (e, m) { + var hiddenPlugins = getHiddenPlugins() + var indexOfPlugin = hiddenPlugins.indexOf(m) + + if(indexOfPlugin > -1) + hiddenPlugins.splice(indexOfPlugin, 1) + + updateHiddenPlugins(hiddenPlugins) + }) + + $rootScope.hiddenPlugins = getHiddenPlugins() + + }]) + +angular + .module('linuxDash') + .run(['$rootScope', '$location', function ($rootScope, $location) { + + $rootScope.$on('$routeChangeSuccess', function () { + + var intervalId = setInterval(function () { + + var el = document.getElementById('plugins') + + if (el) { + + var sortable = Sortable.create(el, { + group: 'plugin-order-' + $location.path().replace('/', ''), + handle: '.heading', + ghostClass: 'ld-ghost', + chosenClass: 'ld-chosen', + dataIdAttr: 'sortablejs-id', + animation: 1050, + store: { + get: function (sortable) { + var order = localStorage.getItem(sortable.options.group.name); + return order ? order.split('|') : []; + }, + set: function (sortable) { + var order = sortable.toArray(); + localStorage.setItem(sortable.options.group.name, order.join('|')); + } + } + }) + + clearInterval(intervalId) + } + }) + }) + + }]) + +angular.module('linuxDash').directive('diskSpace', ['server', function(server) { + return { + restrict: 'E', + scope: {}, + templateUrl: 'src/js/plugins/disk-space/disk-space.html', + link: function(scope) { + + var getKBMultiplierFn = function (size, power) { + return function () { + return size * Math.pow(1024, power) + } + } + + var kbDictionary = { + 'M': function () { return getKBMultiplierFn(size, 1) }, + 'G': function () { return getKBMultiplierFn(size, 2) }, + 'T': function () { return getKBMultiplierFn(size, 3) }, + 'P': function () { return getKBMultiplierFn(size, 4) }, + 'E': function () { return getKBMultiplierFn(size, 5) }, + 'Z': function () { return getKBMultiplierFn(size, 6) }, + 'Y': function () { return getKBMultiplierFn(size, 7) }, + } + + scope.heading = "Disk Partitions" + scope.moduleName = 'disk_partitions' + + scope.getData = function() { + server.get(scope.moduleName, function(serverResponseData) { + scope.diskSpaceData = serverResponseData + }) + + scope.lastGet = new Date().getTime() + } + + scope.getData() + + scope.getKB = function(stringSize) { + + var lastChar = stringSize.slice(-1) + var size = parseFloat(stringSize.replace(",", ".")) + + try { + return kbDictionary[lastChar](size) + } catch (err) { + return size + } + } + + } + } +}]) + +angular.module('linuxDash').directive('keyValueList', ['server', '$rootScope', function (server, $rootScope) { + return { + scope: { + heading: '@', + info: '@', + moduleName: '@', + }, + templateUrl: 'src/js/core/features/key-value-list/key-value-list.html', + link: function(scope, element) { + + scope.getData = function() { + delete scope.tableRows + + server.get(scope.moduleName, function(serverResponseData) { + scope.tableRows = serverResponseData + scope.lastGet = new Date().getTime() + + if (Object.keys(serverResponseData).length === 0) { + scope.emptyResult = true + } + + if (!scope.$$phase && !$rootScope.$$phase) scope.$digest() + }) + } + + scope.getData() + } + } +}]) + +angular.module('linuxDash').directive('lineChartPlugin', [ + '$interval', '$compile', 'server', '$window', + function ($interval, $compile, server, $window) { + return { + scope: { + heading: '@', + moduleName: '@', + refreshRate: '=', + maxValue: '=', + minValue: '=', + getDisplayValue: '=', + metrics: '=', + color: '@' + }, + templateUrl: 'src/js/core/features/line-chart/line-chart-plugin.html', + link: function(scope, element) { + + scope.initializing = true + + // wrap the entire plugin into an initializing function + var start_rendering_line_chart = function () { + + if (!scope.color) + scope.color = '0, 255, 0' + + var series, w, h, canvas + + angular.element($window).bind('resize', function() { + canvas.width = w + canvas.height = h + }) + + // smoothieJS - Create new chart + var chart = new SmoothieChart({ + borderVisible: false, + sharpLines: true, + grid: { + fillStyle: '#ffffff', + strokeStyle: 'rgba(232,230,230,0.93)', + sharpLines: true, + millisPerLine: 3000, + borderVisible: false + }, + labels: { + fontSize: 11, + precision: 0, + fillStyle: '#0f0e0e' + }, + maxValue: parseInt(scope.maxValue), + minValue: parseInt(scope.minValue), + horizontalLines: [{ + value: 5, + color: '#eff', + lineWidth: 1 + }] + }) + + var initializeChart = function () { + // smoothieJS - set up canvas element for chart + var checkForCanvasReadyState = $interval(function () { + if (element.find('canvas')[0]) { + canvas = element.find('canvas')[0] + series = series || new TimeSeries() + w = canvas.width + h = canvas.height + + if (chart.seriesSet.length > 0) + chart.removeTimeSeries(chart.seriesSet[0].timeSeries) + + chart.addTimeSeries(series, { + strokeStyle: 'rgba(' + scope.color + ', 1)', + fillStyle: 'rgba(' + scope.color + ', 0.2)', + lineWidth: 2 + }) + + chart.streamTo(canvas, 1000) + $interval.cancel(checkForCanvasReadyState) + } + }, 100) + } + + scope.reInitializeChart = function () { + initializeChart() + } + + if (!scope.isHidden) + initializeChart() + + var dataCallInProgress = false + + // update data on chart + scope.getData = function() { + + if(scope.initializing) + scope.initializing = false + + if (dataCallInProgress || !element.find('canvas')[0]) return + + dataCallInProgress = true + + server.get(scope.moduleName, function(serverResponseData) { + + if (serverResponseData.length < 1) { + scope.emptyResult = true + return + } + + dataCallInProgress = false + scope.lastGet = new Date().getTime() + + // change graph colour depending on usage + if (scope.maxValue / 4 * 3 < scope.getDisplayValue(serverResponseData)) { + chart.seriesSet[0].options.strokeStyle = 'rgba(255, 89, 0, 1)' + chart.seriesSet[0].options.fillStyle = 'rgba(255, 89, 0, 0.2)' + } else if (scope.maxValue / 3 < scope.getDisplayValue(serverResponseData)) { + chart.seriesSet[0].options.strokeStyle = 'rgba(255, 238, 0, 1)' + chart.seriesSet[0].options.fillStyle = 'rgba(255, 238, 0, 0.2)' + } else { + chart.seriesSet[0].options.strokeStyle = 'rgba(' + scope.color + ', 1)' + chart.seriesSet[0].options.fillStyle = 'rgba(' + scope.color + ', 0.2)' + } + + scope.newData = scope.getDisplayValue(serverResponseData) + + // update chart with this response + series.append(scope.lastGet, scope.newData) + + // update the metrics for this chart + scope.metrics.forEach(function(metricObj) { + metricObj.data = metricObj.generate(serverResponseData) + }) + + }) + } + + // set the directive-provided interval + // at which to run the chart update + var intervalRef = $interval(scope.getData, scope.refreshRate) + var removeInterval = function() { + $interval.cancel(intervalRef) + } + + element.on("$destroy", removeInterval) + } + + // only start rendering plugin when we know the scale of max/min for the canvas chart (smoothie) + var stopWatching = scope.$watch('maxValue', function (n, o) { + if (n) { + start_rendering_line_chart() + stopWatching() + } + }) + + + } + } + } +]) + +angular.module('linuxDash').directive('loader', function() { + return { + scope: { + width: '@' + }, + template: '\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + ' + } +}) + +angular.module('linuxDash').directive('multiLineChartPlugin', [ + '$interval', '$compile', 'server', '$window', + function ($interval, $compile, server, $window) { + return { + scope: { + heading: '@', + moduleName: '@', + refreshRate: '=', + getDisplayValue: '=', + units: '=', + delay: '=' + }, + templateUrl: 'src/js/core/features/multi-line-chart/multi-line-chart-plugin.html', + link: function(scope, element) { + + var w, h, canvas + + angular.element($window).bind('resize', function() { + canvas.width = w + canvas.height = h + }) + + // smoothieJS - Create new chart + var chart = new SmoothieChart({ + borderVisible: false, + sharpLines: true, + grid: { + fillStyle: '#ffffff', + strokeStyle: 'rgba(232,230,230,0.93)', + sharpLines: true, + borderVisible: false + }, + labels: { + fontSize: 12, + precision: 0, + fillStyle: '#0f0e0e' + }, + maxValue: 100, + minValue: 0, + horizontalLines: [{ + value: 1, + color: '#ecc', + lineWidth: 1 + }] + }) + + var seriesOptions = [ + { + strokeStyle: 'rgba(255, 0, 0, 1)', + lineWidth: 2 + }, + { + strokeStyle: 'rgba(0, 255, 0, 1)', + lineWidth: 2 + }, + { + strokeStyle: 'rgba(0, 0, 255, 1)', + lineWidth: 2 + }, + { + strokeStyle: 'rgba(255, 255, 0, 1)', + lineWidth: 1 + } + ] + + // smoothieJS - set up canvas element for chart + scope.seriesArray = [] + scope.metricsArray = [] + + var delay = 1000 + + if (angular.isDefined(scope.delay)) + delay = scope.delay + + var initializeChart = function () { + // smoothieJS - set up canvas element for chart + var checkForCanvasReadyState = $interval(function () { + if (element.find('canvas')[0]) { + canvas = element.find('canvas')[0] + w = canvas.width + h = canvas.height + + // get the data once to set up # of lines on chart + server.get(scope.moduleName, function(serverResponseData) { + + var numberOfLines = Object.keys(serverResponseData).length + + for (var x = 0; x < numberOfLines; x++) { + + var keyForThisLine = Object.keys(serverResponseData)[x]; + + scope.seriesArray[x] = new TimeSeries(); + chart.addTimeSeries(scope.seriesArray[x], seriesOptions[x]); + scope.metricsArray[x] = { + name: keyForThisLine, + color: seriesOptions[x].strokeStyle, + } + } + + }) + + chart.streamTo(canvas, delay) + $interval.cancel(checkForCanvasReadyState) + } + }, 100) + } + + scope.reInitializeChart = function () { + chart.seriesSet.forEach(function (ts) { + chart.removeTimeSeries(ts.timeSeries) + }) + + initializeChart() + } + + if (!scope.isHidden) + initializeChart() + + var dataCallInProgress = false + + // update data on chart + scope.getData = function() { + + if (dataCallInProgress) return + + if (!scope.seriesArray.length) return + + dataCallInProgress = true + + server.get(scope.moduleName, function(serverResponseData) { + + dataCallInProgress = false + scope.lastGet = new Date().getTime() + var keyCount = 0 + var maxAvg = 100 + + // update chart with current response + for (var key in serverResponseData) { + scope.seriesArray[keyCount].append(scope.lastGet, serverResponseData[key]) + keyCount++ + maxAvg = Math.max(maxAvg, serverResponseData[key]) + } + + // update the metrics for this chart + scope.metricsArray.forEach(function(metricObj) { + metricObj.data = serverResponseData[metricObj.name].toString() + ' ' + scope.units + }) + + // round up the average and set the maximum scale + var len = parseInt(Math.log(maxAvg) / Math.log(10)) + var div = Math.pow(10, len) + chart.options.maxValue = Math.ceil(maxAvg / div) * div + + }) + + } + + var refreshRate = (angular.isDefined(scope.refreshRate)) ? scope.refreshRate : 1000 + var intervalRef = $interval(scope.getData, refreshRate) + var removeInterval = function() { + $interval.cancel(intervalRef) + } + + element.on("$destroy", removeInterval) + } + } +}]) + +angular.module('linuxDash').directive('navBar', ['$location', function($location) { + return { + template: '\ + \ + Linux Dash\ + \ +
    \ +
  • \ + \ +
  • \ +
\ + \ + Resources:\ + GitHub | \ + Gitter Chat Room | \ + Docs \ + \ + ', + link: function(scope) { + scope.items = [ + 'system-status', + 'basic-info', + 'network', + 'accounts', + 'apps' + ] + + scope.getNavItemName = function(url) { + return url.replace('-', ' ') + } + + scope.isActive = function(route) { + return '/' + route === $location.path() + } + } + } +}]) + +angular.module('linuxDash').directive('plugin', ['$rootScope', function($rootScope) { + return { + transclude: true, + templateUrl: 'src/js/core/features/plugin/plugin.html', + link: function (s, el, attr) { + + if (attr.hasOwnProperty('chartPlugin')) + s.isChartPlugin = true + + if ($rootScope.hiddenPlugins.indexOf(s.moduleName) > -1) + s.isHidden = true + + s.toggleWidth = function () { + el.find('div')[0].removeAttribute('style') + s.enlarged = !s.enlarged + } + + var setPluginVisibility = function (shouldShow) { + s.isHidden = !shouldShow + + if (shouldShow) { + $rootScope.$emit('show-plugin', s.moduleName) + if (s.isChartPlugin) s.reInitializeChart() + } else { + $rootScope.$emit('hide-plugin', s.moduleName) + } + } + + s.toggleVisibility = function () { + setPluginVisibility(s.isHidden) + } + + + s.$watch('emptyResult', function (n, o) { + if (n) { + setPluginVisibility(false) + } + }) + } + } +}]) + +angular.module('linuxDash').directive('progressBarPlugin', function() { + return { + scope: { + width: '@', + moduleName: '@', + name: '@', + value: '@', + max: '@' + }, + template: '\ +
\ +
\ +
\ +
\ +
\ + ' + } +}) + +angular.module('linuxDash').directive('topBar', ['$rootScope', function($rootScope) { + return { + scope: { + heading: '=', + refresh: '&', + lastUpdated: '=', + toggleVisibility: '&', + isHidden: '=', + toggleWidth: '&', + isChart: '=', + info: '=', // not being used; needs a good ui solution + }, + template: '\ +
\ + ☰ {{ heading }} \ + \ + \ + \ + \ + \ + \ +
\ + ', + } +}]) + +angular.module('linuxDash').directive('tableData', ['server', '$rootScope', function (server, $rootScope) { + return { + scope: { + heading: '@', + info: '@', + moduleName: '@', + width: '@', + height: '@' + }, + templateUrl: 'src/js/core/features/table-data/table-data.html', + link: function(scope, element) { + + scope.sortByColumn = null + scope.sortReverse = null + + // set the column to sort by + scope.setSortColumn = function(column) { + + // if the column is already being sorted + // reverse the order + if (column === scope.sortByColumn) { + scope.sortReverse = !scope.sortReverse + } else { + scope.sortByColumn = column + } + + scope.sortTableRows() + } + + scope.sortTableRows = function() { + scope.tableRows.sort(function(currentRow, nextRow) { + + var sortResult = 0 + + if (currentRow[scope.sortByColumn] < nextRow[scope.sortByColumn]) { + sortResult = -1 + } else if (currentRow[scope.sortByColumn] === nextRow[scope.sortByColumn]) { + sortResult = 0 + } else { + sortResult = 1 + } + + if (scope.sortReverse) { + sortResult = -1 * sortResult + } + + return sortResult + }) + } + + scope.getData = function() { + delete scope.tableRows + + server.get(scope.moduleName, function(serverResponseData) { + + if (serverResponseData.length > 0) { + scope.tableHeaders = Object.keys(serverResponseData[0]) + } + + scope.tableRows = serverResponseData + + if (scope.sortByColumn) { + scope.sortTableRows() + } + + scope.lastGet = new Date().getTime() + + if (serverResponseData.length < 1) { + scope.emptyResult = true + } + + if (!scope.$$phase && !$rootScope.$$phase) scope.$digest() + }) + } + + scope.getData() + } + } +}]) + +angular.module("linuxDash").run(["$templateCache", function($templateCache) {$templateCache.put("src/js/plugins/disk-space/disk-space.html","\n\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
NameStatsUsedMount
{{partition[\'file_system\']}}\n \n \n \n {{ partition[\'used\'] }} / {{ partition[\'size\'] }}\n \n {{ partition[\'used%\'] }}\n {{ partition[\'mounted\'] }}
\n\n\n"); +$templateCache.put("src/js/core/features/key-value-list/key-value-list.html","\n\n \n\n
\n \n \n \n \n \n \n \n
{{ name }}{{ value }}
\n\n
\n\n No data\n\n"); +$templateCache.put("src/js/core/features/line-chart/line-chart-plugin.html","\n\n \n\n \n \n\n \n \n \n \n \n \n \n
{{ metric.name }}{{ metric.data }}
\n\n No data\n\n
\n"); +$templateCache.put("src/js/core/features/multi-line-chart/multi-line-chart-plugin.html","\n\n \n\n \n \n \n \n \n \n \n \n
\n \n \n {{ metric.name }}{{ metric.data }}
\n\n
\n"); +$templateCache.put("src/js/core/features/plugin/plugin.html","\n\n \n \n\n \n \n\n\n"); +$templateCache.put("src/js/core/features/table-data/table-data.html","\n\n \n\n
\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n
\n {{ header }}\n \n {{ (header === sortByColumn && !sortReverse) ? \'▲\': \'\'; }}\n {{ (header === sortByColumn && sortReverse) ? \'▼\': \'\'; }}\n \n
\n {{ row[header] }}\n
\n\n
\n\n No data\n\n");}]); \ No newline at end of file diff --git a/app/server/config/ping_hosts b/app/server/config/ping_hosts new file mode 100644 index 00000000..a3884882 --- /dev/null +++ b/app/server/config/ping_hosts @@ -0,0 +1,3 @@ +google.com +yahoo.com +twitter.com diff --git a/app/server/index.go b/app/server/index.go new file mode 100644 index 00000000..3827425f --- /dev/null +++ b/app/server/index.go @@ -0,0 +1,50 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "net/http" + "os" + "os/exec" +) + +var ( + listenAddress = flag.String("listen", "0.0.0.0:80", "Where the server listens for connections. [interface]:port") + staticPath = flag.String("static", "../", "Location of static files.") +) + +func init() { + flag.Parse() +} + +func main() { + http.Handle("/", http.FileServer(http.Dir(*staticPath))) + http.HandleFunc("/server/", func(w http.ResponseWriter, r *http.Request) { + module := r.URL.Query().Get("module") + if module == "" { + http.Error(w, "No module specified, or requested module doesn't exist.", 406) + return + } + + // Execute the command + cmd := exec.Command("./linux_json_api.sh", module) + var output bytes.Buffer + cmd.Stdout = &output + err := cmd.Run() + if err != nil { + fmt.Printf("Error executing '%s': %s\n\tScript output: %s\n", module, err.Error(), output.String()) + http.Error(w, "Unable to execute module.", http.StatusInternalServerError) + return + } + + w.Write(output.Bytes()) + }) + + fmt.Println("Starting http server at:", *listenAddress) + err := http.ListenAndServe(*listenAddress, nil) + if err != nil { + fmt.Println("Error starting http server:", err) + os.Exit(1) + } +} diff --git a/app/server/index.js b/app/server/index.js new file mode 100644 index 00000000..e4da5c21 --- /dev/null +++ b/app/server/index.js @@ -0,0 +1,77 @@ +var express = require('express') +var app = require('express')() +var server = require('http').Server(app) +var path = require('path') +var spawn = require('child_process').spawn +var fs = require('fs') +var ws = require('websocket').server +var args = require('yargs').argv +var host = args.host || process.env.LINUX_DASH_SERVER_HOST || '0.0.0.0' +var port = args.port || process.env.LINUX_DASH_SERVER_PORT || 80 + +server.listen(port, host, function() { + console.log('Linux Dash Server Started on ' + host + ':' + port + '!'); +}) + +app.use(express.static(path.resolve(__dirname + '/../'))) + +app.get('/', function (req, res) { + res.sendFile(path.resolve(__dirname + '/../index.html')) +}) + +app.get('/websocket', function (req, res) { + + res.send({ + websocket_support: true, + }) + +}) + +wsServer = new ws({ + httpServer: server +}) + +var nixJsonAPIScript = __dirname + '/linux_json_api.sh' + +function getPluginData(pluginName, callback) { + var command = spawn(nixJsonAPIScript, [ pluginName, '' ]) + var output = [] + + command.stdout.on('data', function(chunk) { + output.push(chunk.toString()) + }) + + command.on('close', function (code) { + callback(code, output) + }) +} + +wsServer.on('request', function(request) { + + var wsClient = request.accept('', request.origin) + + wsClient.on('message', function(wsReq) { + + var moduleName = wsReq.utf8Data + var sendDataToClient = function(code, output) { + if (code === 0) { + var wsResponse = '{ "moduleName": "' + moduleName + '", "output": "'+ output.join('') +'" }' + wsClient.sendUTF(wsResponse) + } + } + + getPluginData(moduleName, sendDataToClient) + + }) + +}) + +app.get('/server/', function (req, res) { + + var respondWithData = function(code, output) { + if (code === 0) res.send(output.toString()) + else res.sendStatus(500) + } + + getPluginData(req.query.module, respondWithData) +}) diff --git a/app/server/index.php b/app/server/index.php new file mode 100644 index 00000000..add9587d --- /dev/null +++ b/app/server/index.php @@ -0,0 +1,8 @@ + to stop') + server.serve_forever() diff --git a/app/server/linux_json_api.sh b/app/server/linux_json_api.sh new file mode 100755 index 00000000..16f573e5 --- /dev/null +++ b/app/server/linux_json_api.sh @@ -0,0 +1,657 @@ +#!/bin/bash + +ECHO=$(type -P echo) +SED=$(type -P sed) +GREP=$(type -P grep) +TR=$(type -P tr) +AWK=$(type -P awk) +CAT=$(type -P cat) +HEAD=$(type -P head) +CUT=$(type -P cut) +PS=$(type -P ps) + +_parseAndPrint() { + while read data; do + $ECHO -n "$data" | $SED -r 's/\\//g' | $TR -d "\n"; + done; +} + +arp_cache() { + local arpCommand=$(type -P arp) + + result=$($arpCommand | $AWK 'BEGIN {print "["} NR>1 \ + {print "{ \"addr\": \"" $1 "\", " \ + "\"hw_type\": \"" $2 "\", " \ + "\"hw_addr.\": \"" $3 "\", " \ + "\"mask\": \"" $5 "\" }, " \ + } \ + END {print "]"}' \ + | $SED 'N;$s/},/}/;P;D') + + if [ -z "$result" ]; then + $ECHO {} + else + $ECHO $result | _parseAndPrint + fi +} + +bandwidth() { + + $CAT /proc/net/dev \ + | $AWK 'BEGIN {print "["} NR>2 {print "{ \"interface\": \"" $1 "\"," \ + " \"tx\": " $2 "," \ + " \"rx\": " $10 " }," } END {print "]"}' \ + | $SED 'N;$s/,\n/\n/;P;D' \ + | _parseAndPrint +} + +common_applications() { + result=$(whereis php node mysql mongo vim python ruby java apache2 nginx openssl vsftpd make \ + | $AWK -F: '{if(length($2)==0) { installed="false"; } else { installed="true"; } \ + print \ + "{ \ + \"binary\": \""$1"\", \ + \"location\": \""$2"\", \ + \"installed\": "installed" \ + },"}') + + $ECHO "[" ${result%?} "]" | _parseAndPrint +} + +cpu_info() { + local lscpuCommand=$(type -P lscpu) + + result=$($lscpuCommand \ + | $AWK -F: '{print "\""$1"\": \""$2"\"," } '\ + ) + + $ECHO "{" ${result%?} "}" | _parseAndPrint +} + +cpu_intensive_processes() { + + result=$($PS axo pid,user,pcpu,rss,vsz,comm --sort -pcpu,-rss,-vsz \ + | $HEAD -n 15 \ + | $AWK 'BEGIN{OFS=":"} NR>1 {print "{ \"pid\": " $1 \ + ", \"user\": \"" $2 "\"" \ + ", \"cpu%\": " $3 \ + ", \"rss\": " $4 \ + ", \"vsz\": " $5 \ + ", \"cmd\": \"" $6 "\"" "},"\ + }') + + $ECHO "[" ${result%?} "]" | _parseAndPrint +} + +cpu_temp() { + local ID=* + [ -f /etc/os-release ] && source /etc/os-release + case "$ID" in + "raspbian") + cpu=$(/dev/null; then + returnString=`sensors` + #amd + if [[ "${returnString/"k10"}" != "${returnString}" ]] ; then + $ECHO ${returnString##*k10} | $CUT -d ' ' -f 6 | $CUT -c 2- | $CUT -c 1-4 + #intel + elif [[ "${returnString/"core"}" != "${returnString}" ]] ; then + fromcore=${returnString##*"coretemp"} + $ECHO ${fromcore##*Physical} | $CUT -d ' ' -f 3 | $CUT -c 2-5 | _parseAndPrint + fi + else + $ECHO "[]" | _parseAndPrint + fi + ;; + esac +} + +# by Paul Colby (http://colby.id.au), no rights reserved ;) +cpu_utilization() { + + PREV_TOTAL=0 + PREV_IDLE=0 + iteration=0 + + while [[ iteration -lt 2 ]]; do + # Get the total CPU statistics, discarding the 'cpu ' prefix. + CPU=(`$SED -n 's/^cpu\s//p' /proc/stat`) + IDLE=${CPU[3]} # Just the idle CPU time. + + # Calculate the total CPU time. + TOTAL=0 + for VALUE in "${CPU[@]}"; do + let "TOTAL=$TOTAL+$VALUE" + done + + # Calculate the CPU usage since we last checked. + let "DIFF_IDLE=$IDLE-$PREV_IDLE" + let "DIFF_TOTAL=$TOTAL-$PREV_TOTAL" + let "DIFF_USAGE=(1000*($DIFF_TOTAL-$DIFF_IDLE)/$DIFF_TOTAL+5)/10" + #echo -en "\rCPU: $DIFF_USAGE% \b\b" + + # Remember the total and idle CPU times for the next check. + PREV_TOTAL="$TOTAL" + PREV_IDLE="$IDLE" + + # Wait before checking again. + sleep 1 + iteration="$iteration+1" + done + $ECHO -en "$DIFF_USAGE" +} + +cron_history() { + + local cronLog='/var/log/syslog' + local numberOfLines='50' + + # Month, Day, Time, Hostname, tag, user, + + result=$($GREP -m $numberOfLines CRON $cronLog \ + | $AWK '{ s = ""; for (i = 6; i <= NF; i++) s = s $i " "; \ + print "{\"time\" : \"" $1" "$2" "$3 "\"," \ + "\"user\" : \"" $6 "\"," \ + "\"message\" : \"" $5" "gensub("\"", "\\\\\"", "g", s) "\"" \ + "}," + }' + ) + + $ECHO [${result%?}] | _parseAndPrint +} + +current_ram() { + + local memInfoFile="/proc/meminfo" + + # References: + # Calculations: http://zcentric.com/2012/05/29/mapping-procmeminfo-to-output-of-free-command/ + # Fields: https://www.kernel.org/doc/Documentation/filesystems/proc.txt + + memInfo=$($CAT $memInfoFile | $GREP 'MemTotal\|MemFree\|Buffers\|Cached') + + $ECHO $memInfo | $AWK '{print "{ \"total\": " ($2/1024) ", \"used\": " ( ($2-($5+$8+$11))/1024 ) ", \"available\": " (($5+$8+$11)/1024) " }" }' | _parseAndPrint +} + +disk_partitions() { + local dfCommand=$(type -P df) + + result=$($dfCommand -Ph | $AWK 'NR>1 {print "{\"file_system\": \"" $1 "\", \"size\": \"" $2 "\", \"used\": \"" $3 "\", \"avail\": \"" $4 "\", \"used%\": \"" $5 "\", \"mounted\": \"" $6 "\"},"}') + + $ECHO [ ${result%?} ] | _parseAndPrint +} + +docker_processes() { + + local result="" + local dockerCommand=$(type -P docker) + local containers="$($dockerCommand ps | $AWK '{if(NR>1) print $NF}')" + + for i in $containers; do + result="$result $($dockerCommand top $i axo pid,user,pcpu,pmem,comm --sort -pcpu,-pmem \ + | $HEAD -n 15 \ + | $AWK -v cnt="$i" 'BEGIN{OFS=":"} NR>1 {print "{ \"cname\": \"" cnt \ + "\", \"pid\": " $1 \ + ", \"user\": \"" $2 "\"" \ + ", \"cpu%\": " $3 \ + ", \"mem%\": " $4 \ + ", \"cmd\": \"" $5 "\"" "},"\ + }')" + done + + $ECHO "[" ${result%?} "]" | _parseAndPrint +} + +download_transfer_rate() { + + local files=(/sys/class/net/*) + local pos=$(( ${#files[*]} - 1 )) + local last=${files[$pos]} + + local json_output="{" + + for interface in "${files[@]}" + do + basename=$(basename "$interface") + + # find the number of bytes transfered for this interface + in1=$($CAT /sys/class/net/"$basename"/statistics/rx_bytes) + + # wait a second + sleep 1 + + # check same interface again + in2=$($CAT /sys/class/net/"$basename"/statistics/rx_bytes) + + # get the difference (transfer rate) + in_bytes=$((in2 - in1)) + + # convert transfer rate to KB + in_kbytes=$((in_bytes / 1024)) + + # convert transfer rate to KB + json_output="$json_output \"$basename\": $in_kbytes" + + # if it is not the last line + if [[ ! $interface == $last ]] + then + # add a comma to the line (JSON formatting) + json_output="$json_output," + fi + done + + # close the JSON object & print to screen + $ECHO "$json_output}" | _parseAndPrint +} + +general_info() { + local lsbRelease=$(type -P lsb_release) + local uName=$(type -P uname) + local hostName=$(type -P hostname) + + function displaytime { + local T=$1 + local D=$((T/60/60/24)) + local H=$((T/60/60%24)) + local M=$((T/60%60)) + local S=$((T%60)) + [[ $D > 0 ]] && printf '%d days ' $D + [[ $H > 0 ]] && printf '%d hours ' $H + [[ $M > 0 ]] && printf '%d minutes ' $M + [[ $D > 0 || $H > 0 || $M > 0 ]] && printf 'and ' + printf '%d seconds\n' $S + } + + local lsbRelease=$($lsbRelease -ds | $SED -e 's/^"//' -e 's/"$//') + local uname=$($uName -r | $SED -e 's/^"//' -e 's/"$//') + local os=$($ECHO $lsbRelease $uname) + local hostname=$($hostName) + local uptime_seconds=$($CAT /proc/uptime | awk '{print $1}') + local server_time=$(date) + + $ECHO "{ \"OS\": \"$os\", \"Hostname\": \"$hostname\", \"Uptime\": \" $(displaytime ${uptime_seconds%.*}) \", \"Server Time\": \"$server_time\" }" | _parseAndPrint +} + +io_stats() { + + result=$($CAT /proc/diskstats | $AWK \ + '{ if($4==0 && $8==0 && $12==0 && $13==0) next } \ + {print "{ \"device\": \"" $3 "\", \"reads\": \""$4"\", \"writes\": \"" $8 "\", \"in_prog.\": \"" $12 "\", \"time\": \"" $13 "\"},"}' + ) + + $ECHO [ ${result%?} ] | _parseAndPrint +} + +ip_addresses() { + + local ifconfigCmd=$(type -P ifconfig) + local digCmd=$(type -P dig) + + externalIp=$($digCmd +short myip.opendns.com @resolver1.opendns.com) + + $ECHO -n "[" + + for item in $($ifconfigCmd | $GREP -oP "^[a-zA-Z0-9:]*(?=:)") + do + $ECHO -n "{\"interface\" : \""$item"\", \"ip\" : \"$( $ifconfigCmd $item | $GREP "inet" | $AWK '{match($0,"inet (addr:)?([0-9.]*)",a)}END{ if (NR != 0){print a[2]; exit}{print "none"}}')\"}, " + done + + $ECHO "{ \"interface\": \"external\", \"ip\": \"$externalIp\" } ]" | _parseAndPrint +} + +load_avg() { + + local numberOfCores=$($GREP -c 'processor' /proc/cpuinfo) + + if [ $numberOfCores -eq 0 ]; then + numberOfCores=1 + fi + + result=$($CAT /proc/loadavg | $AWK '{print "{ \"1_min_avg\": " ($1*100)/'$numberOfCores' ", \"5_min_avg\": " ($2*100)/'$numberOfCores' ", \"15_min_avg\": " ($3*100)/'$numberOfCores' "}," }') + + $ECHO ${result%?} | _parseAndPrint +} + +logged_in_users() { + local whoCommand=$(type -P w) + + result=$(COLUMNS=300 $whoCommand -h | $AWK '{print "{\"user\": \"" $1 "\", \"from\": \"" $3 "\", \"when\": \"" $4 "\"},"}') + + $ECHO [ ${result%?} ] | _parseAndPrint +} + +memcached() { + local ncCommand=$(type -P nc) + + $ECHO "stats" \ + | $ncCommand -w 1 127.0.0.1 11211 \ + | $GREP 'bytes' \ + | $AWK 'BEGIN {print "{"} {print "\"" $2 "\": " $3 } END {print "}"}' \ + | $TR '\r' ',' \ + | $SED 'N;$s/,\n/\n/;P;D' \ + | _parseAndPrint +} + +memory_info() { + + $CAT /proc/meminfo \ + | $AWK -F: 'BEGIN {print "{"} {print "\"" $1 "\": \"" $2 "\"," } END {print "}"}' \ + | $SED 'N;$s/,\n/\n/;P;D' \ + | _parseAndPrint +} + +network_connections() { + + local netstatCmd=$(type -P netstat) + local sortCmd=$(type -P sort) + local uniqCmd=$(type -P uniq) + + $netstatCmd -ntu \ + | $AWK 'NR>2 {print $5}' \ + | $sortCmd \ + | $uniqCmd -c \ + | $AWK 'BEGIN {print "["} {print "{ \"connections\": " $1 ", \"address\": \"" $2 "\" }," } END {print "]"}' \ + | $SED 'N;$s/},/}/;P;D' \ + | _parseAndPrint +} + +number_of_cpu_cores() { + + local numberOfCPUCores=$($GREP -c 'model name' /proc/cpuinfo) + + if [ -z $numberOfCPUCores ]; then + echo "cannot be found"; + fi +} + +# http://askubuntu.com/questions/413367/ping-multiple-ips-using-bash +ping() { + + # get absolute path to config file + local SCRIPTPATH=$(dirname $(readlink -f $0)) + local CONFIG_PATH=$SCRIPTPATH"/config/ping_hosts" + + local pingCmd=$(type -P ping) + local numOfLinesInConfig=$($SED -n '$=' $CONFIG_PATH) + local result='[' + + $CAT $CONFIG_PATH \ + | while read output + do + singlePing=$($pingCmd -qc 2 $output \ + | $AWK -F/ 'BEGIN { endLine="}," } /^rtt/ { if ('$numOfLinesInConfig'==1){endLine="}"} print "{" "\"host\": \"'$output'\", \"ping\": " $5 " " endLine }' \ + ) + numOfLinesInConfig=$(($numOfLinesInConfig-1)) + result=$result$singlePing + if [ $numOfLinesInConfig -eq 0 ] + then + $ECHO $result"]" + fi + done \ + | $SED 's/\},]/}]/g' \ + | _parseAndPrint +} + +pm2_stats() { + + #get data + local data="$(pm2 list)" + local tailCommand=$(type -P tail) + + #only process data if variable has a length + #this should handle cases where pm2 is not installed + if [ -n "$data" ]; then + + #start processing data on line 4 + #don't process last 2 lines + json=$( $ECHO "$data" | $tailCommand -n +4 | $HEAD -n +2 \ + | $AWK '{print "{"}\ + {print "\"appName\":\"" $2 "\","} \ + {print "\"id\":\"" $4 "\","} \ + {print "\"mode\":\"" $6 "\","} \ + {print "\"pid\":\"" $8 "\","}\ + {print "\"status\":\"" $10 "\","}\ + {print "\"restart\":\"" $12 "\","}\ + {print "\"uptime\":\"" $14 "\","}\ + {print "\"memory\":\"" $16 $17 "\","}\ + {print "\"watching\":\"" $19 "\""}\ + {print "},"}') + #make sure to remove last comma and print in array + $ECHO "[" ${json%?} "]" | _parseAndPrint + else + #no data found + $ECHO "[]" | _parseAndPrint + fi +} + +ram_intensive_processes() { + + local psCommand=$(type -P ps) + + result=$($psCommand axo pid,user,pmem,rss,vsz,comm --sort -pmem,-rss,-vsz \ + | $HEAD -n 15 \ + | $AWK 'NR>1 {print "{ \"pid\": " $1 \ + ", \"user\": \"" $2 \ + "\", \"mem%\": " $3 \ + ", \"rss\": " $4 \ + ", \"vsz\": " $5 \ + ", \"cmd\": \"" $6 \ + "\"},"}') + + $ECHO [ ${result%?} ] | _parseAndPrint +} + +recent_account_logins() { + + local lastLogCommand=$(type -p lastlog) + + result=$($lastLogCommand -t 365 \ + | $AWK 'NR>1 {\ + print "{ \ + \"user\": \"" $1 "\", \ + \"ip\": \"" $3 "\","" \ + \"date\": \"" $5" "$6" "$7" "$8" "$9 "\"}," + }' + ) + $ECHO [ ${result%?} ] | _parseAndPrint +} + +redis() { + + ########### Enter Your Redis Password HERE ######### + local redisPassword='' + ########### Enter Your Redis Password HERE ######### + + local redisCommand=$(type -P redis-cli); + + if [ -n "$redisPassword" ]; then + redisCommand="$redisCommand -a $redisPassword" + fi + + result=$($redisCommand INFO \ + | $GREP 'redis_version\|connected_clients\|connected_slaves\|used_memory_human\|total_connections_received\|total_commands_processed' \ + | $AWK -F: '{print "\"" $1 "\":" "\"" $2 }' \ + | $TR '\r' '"' | $TR '\n' ',' + ) + $ECHO { ${result%?} } | _parseAndPrint +} + +scheduled_crons() { + + ###### + # Credit: http://stackoverflow.com/questions/134906/how-do-i-list-all-cron-jobs-for-all-users#answer-137173 + ###### + + local egrepCmd=$(type -P egrep) + local crontabCmd=$(type -P crontab) + + # System-wide crontab file and cron job directory. Change these for your system. + CRONTAB='/etc/crontab' + CRONDIR='/etc/cron.d' + + # Single tab character. Annoyingly necessary. + tab=$($ECHO -en "\t") + + # Given a stream of crontab lines, exclude non-cron job lines, replace + # whitespace characters with a single space, and remove any spaces from the + # beginning of each line. + function clean_cron_lines() { + while read line ; do + $ECHO "${line}" | + $egrepCmd --invert-match '^($|\s*#|\s*[[:alnum:]_]+=)' | + $SED --regexp-extended "s/\s+/ /g" | + $SED --regexp-extended "s/^ //" + done; + } + + # Given a stream of cleaned crontab lines, $echoCmd any that don't include the + # run-parts command, and for those that do, show each job file in the run-parts + # directory as if it were scheduled explicitly. + function lookup_run_parts() { + while read line ; do + match=$($ECHO "${line}" | $egrepCmd -o 'run-parts (-{1,2}\S+ )*\S+') + + if [[ -z "${match}" ]] ; then + $echoCmd "${line}" + else + cron_fields=$($ECHO "${line}" | $CUT -f1-6 -d' ') + cron_job_dir=$($ECHO "${match}" | $AWK '{print $NF}') + + if [[ -d "${cron_job_dir}" ]] ; then + for cron_job_file in "${cron_job_dir}"/* ; do # */ + [[ -f "${cron_job_file}" ]] && $ECHO "${cron_fields} ${cron_job_file}" + done + fi + fi + done; + } + + # Temporary file for crontab lines. + temp=$(mktemp) || exit 1 + + # Add all of the jobs from the system-wide crontab file. + $CAT "${CRONTAB}" | clean_cron_lines | lookup_run_parts >"${temp}" + + # Add all of the jobs from the system-wide cron directory. + $CAT "${CRONDIR}"/* | clean_cron_lines >>"${temp}" # */ + + # Add each user's crontab (if it exists). Insert the user's name between the + # five time fields and the command. + while read user ; do + $crontabCmd -l -u "${user}" 2>/dev/null | + clean_cron_lines | + $SED --regexp-extended "s/^((\S+ +){5})(.+)$/\1${user} \3/" >>"${temp}" + done < <($CUT --fields=1 --delimiter=: /etc/passwd) + + # Output the collected crontab lines. + + ## Changes: Parses output into JSON + + $CAT "${temp}" \ + | $AWK 'BEGIN {print "["} \ + {print "{ \"min\": \"" $1 \ + "\", \"hrs\": \"" $2 "\", " \ + " \"day\": \"" $3 "\", " \ + " \"month\": \"" $4 "\", " \ + " \"wkday\": \"" $5 "\", " \ + " \"user\": \"" $6 "\", " \ + " \"CMD\": \""} \ + {for(i=7;i<=NF;++i) printf("%s ", gensub("\"", "\\\\\"", "g", $i) ) } \ + {print "\" " \ + "}," } \ + END {print "]"}' \ + | $SED 'N;$s/,\n//;P;D' \ + | _parseAndPrint + + rm --force "${temp}" +} + +swap() { + + local wcCmd=$(which wc) + + local swapLineCount=$($CAT /proc/swaps | $wcCmd -l) + + if [ "$swapLineCount" -gt 1 ]; then + + result=$($CAT /proc/swaps \ + | $AWK 'NR>1 {print "{ \"filename\": \"" $1"\", \"type\": \""$2"\", \"size\": \""$3"\", \"used\": \""$4"\", \"priority\": \""$5"\"}," }' + ) + + $ECHO [ ${result%?} ] | _parseAndPrint + + else + $ECHO [] | _parseAndPrint + fi +} + +upload_transfer_rate() { + + local files=(/sys/class/net/*) + local pos=$(( ${#files[*]} - 1 )) + local last=${files[$pos]} + + local json_output="{" + + for interface in "${files[@]}" + do + basename=$(basename "$interface") + + # find the number of bytes transfered for this interface + out1=$($CAT /sys/class/net/"$basename"/statistics/tx_bytes) + + # wait a second + sleep 1 + + # check same interface again + out2=$($CAT /sys/class/net/"$basename"/statistics/tx_bytes) + + # get the difference (transfer rate) + out_bytes=$((out2 - out1)) + + # convert transfer rate to KB + out_kbytes=$((out_bytes / 1024)) + + # convert transfer rate to KB + json_output="$json_output \"$basename\": $out_kbytes" + + # if it is not the last line + if [[ ! $interface == $last ]] + then + # add a comma to the line (JSON formatting) + json_output="$json_output," + fi + done + + # close the JSON object & print to screen + $ECHO "$json_output}" | _parseAndPrint +} + +user_accounts() { + result=$($AWK -F: '{ \ + if ($3<=499){userType="system";} \ + else {userType="user";} \ + print "{ \"type\": \"" userType "\"" ", \"user\": \"" $1 "\", \"home\": \"" $6 "\" }," }' < /etc/passwd + ) + + length=$($ECHO ${#result}) + + if [ $length -eq 0 ]; then + result=$(getent passwd | $AWK -F: '{ if ($3<=499){userType="system";} else {userType="user";} print "{ \"type\": \"" userType "\"" ", \"user\": \"" $1 "\", \"home\": \"" $6 "\" }," }') + fi + + $ECHO [ ${result%?} ] | _parseAndPrint +} + +fnCalled="$1" + +# Check if the function call is indeed a function. +if [ -n "$(type -t $fnCalled)" ] && [ "$(type -t $fnCalled)" = function ]; then + ${fnCalled} +else + echo '{\"success\":false,\"status\":\"Invalid module\"}' +fi + diff --git a/bin/linux-dash b/bin/linux-dash new file mode 100755 index 00000000..591c4230 --- /dev/null +++ b/bin/linux-dash @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../app/server/index.js') diff --git a/compressor.php b/compressor.php deleted file mode 100644 index bf53c87b..00000000 --- a/compressor.php +++ /dev/null @@ -1,674 +0,0 @@ - - * @copyright Abhimanyu Sharma [abhimanyusharma003] - * @licence http://codecanyon.net/licenses/regular [ for single application ] - * @version 2.1 - */ - - -// Main settings - -$setting = array( - 'compress' => TRUE, - 'cache-folder' => 'AUTO', - 'minify-css-files' => TRUE, - 'minify-js-files' => TRUE, -); - - -// MIME type settings read more on wikipedia about mime type - -$mimetype = array( - 'css' => 'text/css', - 'js' => 'text/javascript' -); - - -// js and css file headers settings - -$headers = array( - 'expire' => (24 * 60 * 60) * 7, // 7 days in seconds - 'cache-control' => 'must-revalidate', - 'vary' => 'Accept-Encoding' -); - - -//---------------------------------------------------------------------------------------------- -// DO NOT MODIFY ANY THING BELOW THIS -//---------------------------------------------------------------------------------------------- - -if ($setting['cache-folder'] == 'AUTO') { - $cacheDir = dirname(__FILE__) . '/dwccache'; -} else { - $cacheDir = $setting['cache-folder']; -} - -if (!empty($_GET['dwc-debug']) && isset($_GET['dwc-debug'])) { - if ($_GET['dwc-debug'] == TRUE) { - $setting['compress'] = FALSE; - } -} - -$GLOBALS['compressor']['clear'] = FALSE; -if (!empty($_GET['dwc-clear-cache']) && isset($_GET['dwc-clear-cache'])) { - if ($_GET['dwc-clear-cache'] == TRUE) { - $setting['compress'] = FALSE; - $GLOBALS['compressor']['clear'] = TRUE; - } -} - -// Main cache dir 'dwccache' -if (!is_dir($cacheDir)) { - mkdir($cacheDir, 0777) or die('Unable to create dwccache cache folder'); -} - -// JS file cache dir -if (!is_dir($cacheDir . '/js')) { - mkdir($cacheDir . '/js', 0777) or die('Unable to create css cache folder'); -} - -// CSS file cache dir -if (!is_dir($cacheDir . '/css')) { - mkdir($cacheDir . '/css', 0777) or die('Unable to create css cache folder'); -} - - -if (preg_match('/.css/', $_SERVER['QUERY_STRING']) && $setting['minify-css-files'] == true) { - cssFileCreate($_SERVER['QUERY_STRING']); -} - -if (preg_match('/.js/', $_SERVER['QUERY_STRING']) && $setting['minify-js-files'] == true) { - jsFileCreate($_SERVER['QUERY_STRING']); -} - - -function jsFileCreate($queryString) -{ - global $cacheDir, $mimetype; - - $content = explode('?', urldecode($queryString)); - @$content = file_get_contents($content[0]); - if($content == FALSE){ - header("content-type: text/javascript; charset: UTF-8"); - $content = 'alert("DWC: Sorry, On the fly minification of JavaScript FILES will not work on your script, Please disable it. Rest all will work fine")'; - echo $content; - exit(); - } - $cache = new DwcCache($cacheDir . '/js'); - $md = md5($content); - - $data = $cache->get($md); - if ($data == FALSE) { - $data = jsFileOptimizer($content); - $cache->set($md, $data); - } - _compress($data, $mimetype['js'], $cache->getTime($md)); - -} - -function jsFileOptimizer($js) -{ - $comp = new CompJS($js); - return trim($comp->compressor_function($js)); -} - - -function cssFileCreate($queryString) -{ - global $cacheDir, $mimetype; - - $content = explode('?', urldecode($queryString)); - @$content = file_get_contents($content[0]); - if($content == FALSE){ - header("content-type: text/css; charset: UTF-8"); - $content = '*{ background:red;}.body{background:red;}'; - echo $content; - exit(); - } - $cache = new DwcCache($cacheDir . '/css'); - $md = md5($content); - - $data = $cache->get($md); - if ($data == FALSE) { - $data = cssFileOptimizer($content); - $cache->set($md, $data); - } - _compress($data, $mimetype['css'], $cache->getTime($md)); - -} - -function cssFileOptimizer($str) -{ - $str = preg_replace('!/\*.*?\*/!s', '', $str); - $str = preg_replace('/\s*([{}|:;,])\s+/', '$1', $str); - $str = preg_replace('/\s\s+(.*)/', '$1', $str); - $str = str_replace(';}', '}', $str); - - return trim($str); -} - -function _compress($data, $mime, $timestamp) -{ - - global $headers; - - $expire = "expires: " . gmdate("D, d M Y H:i:s", time() + $headers['expire']) . " GMT"; - - ob_start("ob_gzhandler"); - header("content-type: {$mime}; charset: UTF-8"); - header("cache-control: {$headers['cache-control']}"); - header($expire); - header("Vary: {$headers['vary']}"); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $timestamp) . ' GMT'); - - echo $data; - -} - - -class CompJS { - const LF = 10; - const SP = 32; - const KPA = 1; - const DEA = 2; - const DEAB = 3; - protected $a = ''; - protected $b = ''; - protected $inp = ''; - protected $inpI = 0; - protected $inpL = 0; - protected $la = null; - protected $op = ''; - - public function __construct($input) - { - $this->inp = str_replace("\r\n", "\n", $input); - $this->inpL = strlen($this->inp); - } - - function compressor_function($js) - { - $jsmin = new CompJS($js); - return $jsmin->min(); - } - - protected function min() - { - if (0 == strncmp($this->pk(), "\xef", 1)) { - $this->put(); - $this->put(); - $this->put(); - } - - $this->a = "\n"; - $this->act(self::DEAB); - - while ($this->a !== null) { - switch ($this->a) { - case ' ': - if ($this->isAN($this->b)) { - $this->act(self::KPA); - } else { - $this->act(self::DEA); - } - break; - - case "\n": - switch ($this->b) { - case '{': - case '[': - case '(': - case '+': - case '-': - case '!': - case '~': - $this->act(self::KPA); - break; - - case ' ': - $this->act(self::DEAB); - break; - - default: - if ($this->isAN($this->b)) { - $this->act(self::KPA); - } else { - $this->act(self::DEA); - } - } - break; - - default: - switch ($this->b) { - case ' ': - if ($this->isAN($this->a)) { - $this->act(self::KPA); - break; - } - - $this->act(self::DEAB); - break; - - case "\n": - switch ($this->a) { - case '}': - case ']': - case ')': - case '+': - case '-': - case '"': - case "'": - $this->act(self::KPA); - break; - - default: - if ($this->isAN($this->a)) { - $this->act(self::KPA); - } else { - $this->act(self::DEAB); - } - } - break; - - default: - $this->act(self::KPA); - break; - } - } - } - - return $this->op; - } - - protected function pk() - { - $this->la = $this->put(); - return $this->la; - } - - protected function put() - { - $c = $this->la; - $this->la = null; - - if ($c === null) { - if ($this->inpI < $this->inpL) { - $c = substr($this->inp, $this->inpI, 1); - $this->inpI += 1; - } else { - $c = null; - } - } - - if ($c === "\r") { - return "\n"; - } - - if ($c === null || $c === "\n" || ord($c) >= self::SP) { - return $c; - } - - return ' '; - } - - protected function act($command) - { - switch ($command) { - case self::KPA: - $this->op .= $this->a; - - case self::DEA: - $this->a = $this->b; - - if ($this->a === "'" || $this->a === '"') { - for (; ;) { - $this->op .= $this->a; - $this->a = $this->put(); - - if ($this->a === $this->b) { - break; - } - - if (ord($this->a) <= self::LF) { - throw new CompJSException('Unterminated string literal.'); - } - - if ($this->a === '\\') { - $this->op .= $this->a; - $this->a = $this->put(); - } - } - } - - case self::DEAB: - $this->b = $this->nt(); - - if ($this->b === '/' && ( - $this->a === '(' || $this->a === ',' || $this->a === '=' || - $this->a === ':' || $this->a === '[' || $this->a === '!' || - $this->a === '&' || $this->a === '|' || $this->a === '?' || - $this->a === '{' || $this->a === '}' || $this->a === ';' || - $this->a === "\n") - ) { - - $this->op .= $this->a . $this->b; - - for (; ;) { - $this->a = $this->put(); - - if ($this->a === '[') { - for (; ;) { - $this->op .= $this->a; - $this->a = $this->put(); - - if ($this->a === ']') { - break; - } elseif ($this->a === '\\') { - $this->op .= $this->a; - $this->a = $this->put(); - } elseif (ord($this->a) <= self::LF) { - throw new CompJSException('Regex literal.'); - } - } - } elseif ($this->a === '/') { - break; - } elseif ($this->a === '\\') { - $this->op .= $this->a; - $this->a = $this->put(); - } elseif (ord($this->a) <= self::LF) { - throw new CompJSException('Regex literal.'); - } - - $this->op .= $this->a; - } - - $this->b = $this->nt(); - } - } - } - - protected function nt() - { - $c = $this->put(); - - if ($c === '/') { - switch ($this->pk()) { - case '/': - for (; ;) { - $c = $this->put(); - - if (ord($c) <= self::LF) { - return $c; - } - } - - case '*': - $this->put(); - - for (; ;) { - switch ($this->put()) { - case '*': - if ($this->pk() === '/') { - $this->put(); - return ' '; - } - break; - - case null: - throw new CompJSException('Unterminated comment'); - } - } - - default: - return $c; - } - } - - return $c; - } - - protected function isAN($c) - { - return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1; - } -} - -class DwcCache { - - function __construct($dir) - { - $this->dir = $dir; - } - - public function get($key) - { - - if (!is_dir($this->dir)) { - return FALSE; - } - - $cache_path = $this->_name($key); - - if (!@file_exists($cache_path)) { - return FALSE; - } - - if (!$fp = @fopen($cache_path, 'rb')) { - return FALSE; - } - - flock($fp, LOCK_SH); - - - if (filesize($cache_path) > 0) { - $cache = unserialize(fread($fp, filesize($cache_path))); - } else { - $cache = NULL; - } - - flock($fp, LOCK_UN); - fclose($fp); - - return $cache; - } - - private function _name($key) - { - return sprintf("%s/%s", $this->dir, sha1($key)); - } - - public function getTime($key) - { - $cache_path = $this->_name($key); - return filemtime($cache_path); - } - - public function set($key, $data) - { - - if (!is_dir($this->dir) OR !is_writable($this->dir)) { - return FALSE; - } - - $cache_path = $this->_name($key); - - if (!$fp = fopen($cache_path, 'wb')) { - return FALSE; - } - - if (flock($fp, LOCK_EX)) { - fwrite($fp, serialize($data)); - flock($fp, LOCK_UN); - } else { - return FALSE; - } - fclose($fp); - @chmod($cache_path, 0777); - return TRUE; - } -} - - -function compressor_ob_handler($start_buffer) -{ - static $file_html = false; - if (!$file_html) { - if (stripos($start_buffer, ']*>(\s+)?(\s+)?<\/script>/', '', $start_buffer); - - // CDATA - $start_buffer = preg_replace('/^(?:\s*\/\*\s*\s*\*\/|\/\/\s*\]\]>)\s*$/', ' ', $start_buffer); - $start_buffer = preg_replace('/\/\/]](>|>)/', '', $start_buffer); - $start_buffer = preg_replace('/\/\*\*\//', ' ', $start_buffer); - //CDATA - - $start_buffer = preg_replace('/(<|<)!--(\s){0,1}?(?!<)(?!(\s+)?.(\s+)?(<)?[ifIFIfiF|>)/s', ' ', $start_buffer); - $start_buffer = preg_replace('/]*>/', ' + + + + diff --git a/index.php b/index.php deleted file mode 100755 index fbf10b23..00000000 --- a/index.php +++ /dev/null @@ -1,388 +0,0 @@ - - - - - -Server Monitoring Dashboard - - - - - - - - - - - - - - - -Fork me on GitHub - - - - - -
-
-
- -
-
-
- -
-
- A simple web dashboard to monitor your server. -
- -
- -
-
-
-

General Info

- -
-   Refresh - -
- -
- -
-
-
- OS:
- Uptime: Hours
- Hostname:
-

-
-
- -
- -
- -
-
-
-

Load average

- -
-   Refresh - -
- -
- -
-
-
- Number of cores: 0 -
-
- -
- 1 min - %
- -
- - -
- 5 min - %
- -
- - -
- 15 min - %
- -
- -
-
-
- -
- -
- -
-
-
- -

RAM

-
-   Refresh -
-
- -
-
-
- -
-
- Total - MB -
- - -
- Used - MB -
% -
- - -
- Free - MB -
% - -
- - - -
-
- - -
-
-
-
- -
- -
-
-
-
-

Disk Usage

-
-   Refresh -
-
- -
- -
-
- -
- -
- -
-
-
-

Users

-
-   Refresh -
-
- -
- - -
- -
- -
- - -
- - - -
- - - -
-
-
-
-

Software

-
-   Refresh -
-
- -
- - -
-
- -
- -
- -
-
-
-

IP

-
-   Refresh -
-
- -
- -
-
- -
- -
- -
-
-
-

Internet Speed

-
-   Refresh -
-
- -
-
- 0 - Mbps -
-
- -
- -
- - -
-
-
-

Online

-
-   Refresh -
-
- -
- - -
- -
- -
- -
-
- - - - -
-
-
-
-

Processes

-
-   Refresh -
-
- -
-
- -
- - -
-
- -
- -
- - -
- - -
- -
- -
- - - - - - - - - - - - - - - - - diff --git a/js/base.js b/js/base.js deleted file mode 100644 index fdca2aee..00000000 --- a/js/base.js +++ /dev/null @@ -1,68 +0,0 @@ -$(document).ready(function() { - - // enable popovers - $(".pop").popover(); - - // Smooth scrolling for links. - $(".mainnav").on("click", ".js-smoothscroll", function(event) { - event.preventDefault(); - var target = $(this.hash).parent(); - pulseElement(target, 8, 400); - - $("html").animate({ - scrollTop: target.offset().top - 130 - }, 1000); - }); - - // refresh all widgets - $('#refresh-all').on('click',function(){ - dashboard.getAll(); - }); - - dashboard.getAll(); -}).on("click", ".js-refresh-info", function(event) { - event.preventDefault(); - var item = event.target.id.split("-").splice(-1)[0]; - dashboard.fnMap[item](); -}); - -// Handle for cancelling active effect. -var pulsing = { - element: null, - timeoutIDs: [], - resetfn: function() { - pulsing.element = null; - pulsing.timeoutIDs = []; - } -}; - -/** - * Applies a pulse effect to the specified element. If triggered while already - * active the ongoing effect is cancelled immediately. - * - * @param {HTMLElement} element The element to apply the effect to. - * @param {Number} times How many pulses. - * @param {Number} interval Milliseconds between pulses. - */ -function pulseElement(element, times, interval) { - if (pulsing.element) { - pulsing.element.removeClass("pulse"). - parent().removeClass("pulse-border"); - pulsing.timeoutIDs.forEach(function(ID) { - clearTimeout(ID); - }); - pulsing.timeoutIDs = []; - } - pulsing.element = element; - var parent = element.parent(); - var f = function() { - element.toggleClass("pulse"); - parent.toggleClass("pulse-border"); - }; - - pulsing.timeoutIDs.push(setTimeout(pulsing.resetfn, - (times + 1) * interval)); - for (; times > 0; --times) { - pulsing.timeoutIDs.push(setTimeout(f, times * interval)); - } -} diff --git a/js/bootstrap.js b/js/bootstrap.js deleted file mode 100644 index ca868671..00000000 --- a/js/bootstrap.js +++ /dev/null @@ -1,1726 +0,0 @@ -/* =================================================== - * bootstrap-transition.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#transitions - * =================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - -!function( $ ) { - - $(function () { - - "use strict" - - /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) - * ======================================================= */ - - $.support.transition = (function () { - var thisBody = document.body || document.documentElement - , thisStyle = thisBody.style - , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined - - return support && { - end: (function () { - var transitionEnd = "TransitionEnd" - if ( $.browser.webkit ) { - transitionEnd = "webkitTransitionEnd" - } else if ( $.browser.mozilla ) { - transitionEnd = "transitionend" - } else if ( $.browser.opera ) { - transitionEnd = "oTransitionEnd" - } - return transitionEnd - }()) - } - })() - - }) - -}( window.jQuery );/* ========================================================== - * bootstrap-alert.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#alerts - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function( $ ){ - - "use strict" - - /* ALERT CLASS DEFINITION - * ====================== */ - - var dismiss = '[data-dismiss="alert"]' - , Alert = function ( el ) { - $(el).on('click', dismiss, this.close) - } - - Alert.prototype = { - - constructor: Alert - - , close: function ( e ) { - var $this = $(this) - , selector = $this.attr('data-target') - , $parent - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = $(selector) - $parent.trigger('close') - - e && e.preventDefault() - - $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) - - $parent - .trigger('close') - .removeClass('in') - - function removeElement() { - $parent - .trigger('closed') - .remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent.on($.support.transition.end, removeElement) : - removeElement() - } - - } - - - /* ALERT PLUGIN DEFINITION - * ======================= */ - - $.fn.alert = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('alert') - if (!data) $this.data('alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.alert.Constructor = Alert - - - /* ALERT DATA-API - * ============== */ - - $(function () { - $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) - }) - -}( window.jQuery );/* ============================================================ - * bootstrap-button.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#buttons - * ============================================================ - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - -!function( $ ){ - - "use strict" - - /* BUTTON PUBLIC CLASS DEFINITION - * ============================== */ - - var Button = function ( element, options ) { - this.$element = $(element) - this.options = $.extend({}, $.fn.button.defaults, options) - } - - Button.prototype = { - - constructor: Button - - , setState: function ( state ) { - var d = 'disabled' - , $el = this.$element - , data = $el.data() - , val = $el.is('input') ? 'val' : 'html' - - state = state + 'Text' - data.resetText || $el.data('resetText', $el[val]()) - - $el[val](data[state] || this.options[state]) - - // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : - $el.removeClass(d).removeAttr(d) - }, 0) - } - - , toggle: function () { - var $parent = this.$element.parent('[data-toggle="buttons-radio"]') - - $parent && $parent - .find('.active') - .removeClass('active') - - this.$element.toggleClass('active') - } - - } - - - /* BUTTON PLUGIN DEFINITION - * ======================== */ - - $.fn.button = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('button') - , options = typeof option == 'object' && option - if (!data) $this.data('button', (data = new Button(this, options))) - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - $.fn.button.defaults = { - loadingText: 'loading...' - } - - $.fn.button.Constructor = Button - - - /* BUTTON DATA-API - * =============== */ - - $(function () { - $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - $btn.button('toggle') - }) - }) - -}( window.jQuery );/* ========================================================== - * bootstrap-carousel.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#carousel - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function( $ ){ - - "use strict" - - /* CAROUSEL CLASS DEFINITION - * ========================= */ - - var Carousel = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.carousel.defaults, options) - this.options.slide && this.slide(this.options.slide) - this.options.pause == 'hover' && this.$element - .on('mouseenter', $.proxy(this.pause, this)) - .on('mouseleave', $.proxy(this.cycle, this)) - } - - Carousel.prototype = { - - cycle: function () { - this.interval = setInterval($.proxy(this.next, this), this.options.interval) - return this - } - - , to: function (pos) { - var $active = this.$element.find('.active') - , children = $active.parent().children() - , activePos = children.index($active) - , that = this - - if (pos > (children.length - 1) || pos < 0) return - - if (this.sliding) { - return this.$element.one('slid', function () { - that.to(pos) - }) - } - - if (activePos == pos) { - return this.pause().cycle() - } - - return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) - } - - , pause: function () { - clearInterval(this.interval) - this.interval = null - return this - } - - , next: function () { - if (this.sliding) return - return this.slide('next') - } - - , prev: function () { - if (this.sliding) return - return this.slide('prev') - } - - , slide: function (type, next) { - var $active = this.$element.find('.active') - , $next = next || $active[type]() - , isCycling = this.interval - , direction = type == 'next' ? 'left' : 'right' - , fallback = type == 'next' ? 'first' : 'last' - , that = this - - this.sliding = true - - isCycling && this.pause() - - $next = $next.length ? $next : this.$element.find('.item')[fallback]() - - if ($next.hasClass('active')) return - - if (!$.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger('slide') - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid') - } else { - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - this.$element.trigger('slide') - this.$element.one($.support.transition.end, function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { that.$element.trigger('slid') }, 0) - }) - } - - isCycling && this.cycle() - - return this - } - - } - - - /* CAROUSEL PLUGIN DEFINITION - * ========================== */ - - $.fn.carousel = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('carousel') - , options = typeof option == 'object' && option - if (!data) $this.data('carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (typeof option == 'string' || (option = options.slide)) data[option]() - else data.cycle() - }) - } - - $.fn.carousel.defaults = { - interval: 5000 - , pause: 'hover' - } - - $.fn.carousel.Constructor = Carousel - - - /* CAROUSEL DATA-API - * ================= */ - - $(function () { - $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { - var $this = $(this), href - , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) - $target.carousel(options) - e.preventDefault() - }) - }) - -}( window.jQuery );/* ============================================================= - * bootstrap-collapse.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#collapse - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - -!function( $ ){ - - "use strict" - - var Collapse = function ( element, options ) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) - - if (this.options["parent"]) { - this.$parent = $(this.options["parent"]) - } - - this.options.toggle && this.toggle() - } - - Collapse.prototype = { - - constructor: Collapse - - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - , show: function () { - var dimension = this.dimension() - , scroll = $.camelCase(['scroll', dimension].join('-')) - , actives = this.$parent && this.$parent.find('.in') - , hasData - - if (actives && actives.length) { - hasData = actives.data('collapse') - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', 'show', 'shown') - this.$element[dimension](this.$element[0][scroll]) - - } - - , hide: function () { - var dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', 'hide', 'hidden') - this.$element[dimension](0) - } - - , reset: function ( size ) { - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size ? 'addClass' : 'removeClass']('collapse') - - return this - } - - , transition: function ( method, startEvent, completeEvent ) { - var that = this - , complete = function () { - if (startEvent == 'show') that.reset() - that.$element.trigger(completeEvent) - } - - this.$element - .trigger(startEvent) - [method]('in') - - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() - } - - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - } - - /* COLLAPSIBLE PLUGIN DEFINITION - * ============================== */ - - $.fn.collapse = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = typeof option == 'object' && option - if (!data) $this.data('collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.defaults = { - toggle: true - } - - $.fn.collapse.Constructor = Collapse - - - /* COLLAPSIBLE DATA-API - * ==================== */ - - $(function () { - $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) { - var $this = $(this), href - , target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $(target).collapse(option) - }) - }) - -}( window.jQuery );/* ============================================================ - * bootstrap-dropdown.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#dropdowns - * ============================================================ - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function( $ ){ - - "use strict" - - /* DROPDOWN CLASS DEFINITION - * ========================= */ - - var toggle = '[data-toggle="dropdown"]' - , Dropdown = function ( element ) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } - - Dropdown.prototype = { - - constructor: Dropdown - - , toggle: function ( e ) { - var $this = $(this) - , selector = $this.attr('data-target') - , $parent - , isActive - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = $(selector) - $parent.length || ($parent = $this.parent()) - - isActive = $parent.hasClass('open') - - clearMenus() - !isActive && $parent.toggleClass('open') - - return false - } - - } - - function clearMenus() { - $(toggle).parent().removeClass('open') - } - - - /* DROPDOWN PLUGIN DEFINITION - * ========================== */ - - $.fn.dropdown = function ( option ) { - return this.each(function () { - var $this = $(this) - , data = $this.data('dropdown') - if (!data) $this.data('dropdown', (data = new Dropdown(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.dropdown.Constructor = Dropdown - - - /* APPLY TO STANDARD DROPDOWN ELEMENTS - * =================================== */ - - $(function () { - $('html').on('click.dropdown.data-api', clearMenus) - $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) - }) - -}( window.jQuery );/* ========================================================= - * bootstrap-modal.js v2.0.2 - * http://twitter.github.com/bootstrap/javascript.html#modals - * ========================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================= */ - - -!function( $ ){ - - "use strict" - - /* MODAL CLASS DEFINITION - * ====================== */ - - var Modal = function ( content, options ) { - this.options = options - this.$element = $(content) - .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) - } - - Modal.prototype = { - - constructor: Modal - - , toggle: function () { - return this[!this.isShown ? 'show' : 'hide']() - } - - , show: function () { - var that = this - - if (this.isShown) return - - $('body').addClass('modal-open') - - this.isShown = true - this.$element.trigger('show') - - escape.call(this) - backdrop.call(this, function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position - - that.$element - .show() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element.addClass('in') - - transition ? - that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : - that.$element.trigger('shown') - - }) - } - - , hide: function ( e ) { - e && e.preventDefault() - - if (!this.isShown) return - - var that = this - this.isShown = false - - $('body').removeClass('modal-open') - - escape.call(this) - - this.$element - .trigger('hide') - .removeClass('in') - - $.support.transition && this.$element.hasClass('fade') ? - hideWithTransition.call(this) : - hideModal.call(this) - } - - } - - - /* MODAL PRIVATE METHODS - * ===================== */ - - function hideWithTransition() { - var that = this - , timeout = setTimeout(function () { - that.$element.off($.support.transition.end) - hideModal.call(that) - }, 500) - - this.$element.one($.support.transition.end, function () { - clearTimeout(timeout) - hideModal.call(that) - }) - } - - function hideModal( that ) { - this.$element - .hide() - .trigger('hidden') - - backdrop.call(this) - } - - function backdrop( callback ) { - var that = this - , animate = this.$element.hasClass('fade') ? 'fade' : '' - - if (this.isShown && this.options.backdrop) { - var doAnimate = $.support.transition && animate - - this.$backdrop = $('