Permalink
Browse files

Logs now outputting to clients

  • Loading branch information...
1 parent edb92c0 commit b7950bb7baa4d054aaac164831a6483840e17bf1 @adamcbrewer committed Oct 31, 2012
Showing with 160 additions and 51 deletions.
  1. +50 −0 models/stalker.js
  2. +1 −1 public/assets/css/core.css
  3. +30 −4 public/assets/js/script.js
  4. +1 −1 public/assets/js/script.min.js
  5. +25 −5 public/assets/less/core.less
  6. +34 −39 server.js
  7. +1 −1 server.min.js
  8. +18 −0 view/layout.tmpl
View
@@ -0,0 +1,50 @@
+/*
+* Stalker.js
+*
+* A new instance of this should be created each time a new client connect to the server
+*
+**/
+module.exports = Stalker;
+
+
+var Stalker = function (params) {
+
+ params = params || {};
+
+ return stalker;
+
+};
+
+Stalker.prototype.stalkFile = function (fileObj) {
+
+ fileObj = fileObj || false;
+
+ var interval = fileObj.interval || 10000, // 10 senconds default
+ i = 0,
+ lines = [];
+
+ fs.watchFile(fileObj.path, { interval: interval }, function (curr, prev) {
+ console.log('-- LOG - '+fileObj.name+' changed at: ' + curr.mtime);
+ console.log('-- LOG - The previous mtime was: ' + prev.mtime);
+ fs.readFile(fileObj.path, 'utf8', function (err, data) {
+ if (err) throw err;
+ console.log(data);
+
+ });
+ });
+
+ var dataReader = new reader.DataReader(fileObj.path, {encoding: "utf8"})
+ .on('error', function (error) {
+ console.log('error reading file: ' + error);
+ })
+ .on('line', function (line, nextByteOffset) {
+ i++;
+ lines.push(line);
+ // console.log(i + ': ' + line);
+ })
+ .on('end', function () {
+ console.log(i + ' lines in ' + fileObj.name);
+ })
+ .read();
+
+}
@@ -1 +1 @@
-.filter{-webkit-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-moz-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-ms-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-o-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65))}.hover-filter{-webkit-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-moz-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-ms-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-o-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95))}.drop-shadow{-webkit-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-moz--box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-ms-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-o-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08)}.hover-drop-shadow{-webkit-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-moz--box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-ms-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-o-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2)}.light-border{border-radius:4px;border:2px solid rgba(127,255,255,0.35)}.hover-light-border{border:2px solid rgba(127,255,255,0.8)}*{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;background-repeat:no-repeat;vertical-align:middle}::-moz-selection{background:#333;color:#f8f8f8;text-shadow:none}::selection{background:#333;color:#f8f8f8;text-shadow:none}html,body{-web-font-smoothing:antialiased;width:100%;height:100%}body{background-color:#0a0a0a;color:rgba(127,255,255,0.9);font-family:Menlo}[type=search]{-webkit-appearance:none}::-webkit-validation-bubble-message{padding:40px}#wrapper{padding:1%;display:-webkit-flex}#details{display:-webkit-flex;-webkit-flex:1}#terminals{display:-webkit-flex;-webkit-flex:2}.terminal{border-radius:4px;border:2px solid rgba(127,255,255,0.35);-webkit-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-moz--box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-ms-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-o-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-webkit-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-moz-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-ms-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-o-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));color:rgba(127,255,255,0.8);margin:10px;background-color:rgba(0,127,127,0.08);padding:4% 2%;text-align:center;-webkit-flex:2;-webkit-transform-style:preserve-3d;-webkit-transform:rotatey(20deg) skewy(30deg);-webkit-transition:.8s border-color ease,0.2s box-shadow ease 0s,0.4s -webkit-transform ease 0s}.terminal ul{display:-webkit-flex}.terminal li{-webkit-flex:1;margin:0 2%}.p-title{font-size:1em;text-transform:uppercase}.p-output{font-size:2em;margin:.5em 0}.terminal:hover,.terminal.active{-webkit-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-moz--box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-ms-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-o-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);border:2px solid rgba(127,255,255,0.8);-webkit-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-moz-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-ms-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-o-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));color:#7fffff}.terminal.active{-webkit-transform:scale(1.4)}@media print{*{background:transparent!important;color:black!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}
+.filter{-webkit-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-moz-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-ms-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-o-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65))}.hover-filter{-webkit-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-moz-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-ms-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-o-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95))}.drop-shadow{-webkit-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-moz--box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-ms-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-o-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08)}.hover-drop-shadow{-webkit-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-moz--box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-ms-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-o-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2)}.light-border{border-radius:4px;border:2px solid rgba(127,255,255,0.35)}.hover-light-border{border:2px solid rgba(127,255,255,0.8)}*{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;background-repeat:no-repeat;vertical-align:middle}::-moz-selection{background:#333;color:#f8f8f8;text-shadow:none}::selection{background:#333;color:#f8f8f8;text-shadow:none}html,body{-web-font-smoothing:antialiased;width:100%;height:100%}body{background-color:#0a0a0a;color:rgba(127,255,255,0.9);font-family:Menlo}[type=search]{-webkit-appearance:none}::-webkit-validation-bubble-message{padding:40px}#wrapper{padding:1%;display:-webkit-flex}#details{display:-webkit-flex;-webkit-flex:1}#terminals{display:-webkit-flex;-webkit-flex:2}#logs{display:-webkit-flex;-webkit-flex:2}.terminal,.log{border-radius:4px;border:2px solid rgba(127,255,255,0.35);-webkit-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-moz--box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-ms-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-o-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.08);-webkit-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-moz-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-ms-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));-o-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));filter:drop-shadow(0px 0 20px rgba(0,255,255,0.65));color:rgba(127,255,255,0.8);margin:10px;background-color:rgba(0,127,127,0.08);padding:4% 2%;text-align:center;-webkit-flex:2;-webkit-transition:.8s border-color ease,0.2s box-shadow ease 0s,0.4s -webkit-transform ease 0s}.terminal ul,.log ul{display:-webkit-flex}.terminal li,.log li{-webkit-flex:1;margin:0 2%}.p-title,.log-title{font-size:1em;text-transform:uppercase}.p-output,.log-output{font-size:2em;margin:.5em 0}.bar-graph{width:100%;height:20px;background-color:rgba(0,127,127,0.11)}.bar-graph .bar{background-color:#7fffff;color:#DDD;height:100%;font-size:14px}.terminal:hover,.terminal.active{-webkit-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-moz--box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-ms-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);-o-box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);box-shadow:0 0 40px rgba(0,255,255,0.25),inset 0 0 40px rgba(0,255,255,0.2);border:2px solid rgba(127,255,255,0.8);-webkit-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-moz-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-ms-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));-o-filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));filter:drop-shadow(0px 0 20px rgba(0,255,255,0.95));color:#7fffff}.terminal.active{-webkit-transform:scale(1.4)}@media print{*{background:transparent!important;color:black!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}
@@ -34,7 +34,9 @@
// =========================================
//
App.socket.on('broadcast', function (data) {
+ console.log(data);
if (data.results && data.results.osData) App.updateTerminals(data.results.osData);
+ if (data.results && data.results.logs) App.updateLogs(data.results.logs);
});
@@ -60,11 +62,13 @@
}
// Free memory
- if (data.freemem) {
+ if (data.freemem && data.totalmem) {
terminal = $('[data-os="freemem"]');
- var niceMem = Math.round(data.freemem/1024/1024) + ' MB',
- rawMem = data.freemem + ' bytes';
- terminal.find('[data-output="main"]').html(niceMem);
+ var niceFreeMem = Math.round(data.freemem/1024/1024) + ' MB',
+ rawMem = data.freemem + ' bytes',
+ percentFree = Math.round(data.freemem / data.totalmem * 100) + '%';
+ terminal.find('[data-output="main"]').html(niceFreeMem + ' / ' + percentFree);
+ terminal.find('[data-bar="free"]').css('width', percentFree);
terminal.find('[data-output="raw"]').html(rawMem);
}
@@ -96,6 +100,28 @@
+
+ //
+ // Update the front-end panels with data from the server
+ //
+ // =========================================
+ //
+ App.updateLogs = function (logs) {
+
+ var log = null;
+
+ // Apache Error logs
+ if (logs.apacheErr) {
+ log = $('[data-log="apacheErr"]');
+
+ log.find('[data-output="main"]').html(logs.apacheErr.latestLines.join('<br />'));
+ }
+
+
+ };
+
+
+
//
//
//
@@ -1 +1 @@
-(function(e,t){e=e||{};Modernizr.load({load:[e.siteUrl+"/assets/js/libs/jq-1.8.2.min.js",e.siteUrl+"/assets/js/libs/moment-1.7.2.js"],complete:function(){var t=$("#terminals"),n=$(".terminal");t.on("click",".terminal",function(e){e.preventDefault();n.not(this).removeClass("active");this.classList.add("active")});$(window).on("keyup",function(e){e.keyCode===27&&n.removeClass("active")});e.socket.on("broadcast",function(t){t.results&&t.results.osData&&e.updateTerminals(t.results.osData)});e.updateTerminals=function(e){console.log(e);var t;if(e.uptime){t=$('[data-os="uptime"]');var n=moment.duration(e.uptime*1e3).humanize(),r=e.uptime+" seconds";t.find('[data-output="main"]').html(n);t.find('[data-output="raw"]').html(r)}if(e.freemem){t=$('[data-os="freemem"]');var i=Math.round(e.freemem/1024/1024)+" MB",s=e.freemem+" bytes";t.find('[data-output="main"]').html(i);t.find('[data-output="raw"]').html(s)}if(e.loadavg){t=$('[data-os="loadavg"]');var o=0,u="",a="";type="";for(o;o<e.loadavg.length;o++){o===0?type="1min":o===1?type="5min":type="15min";u+="<li>"+e.loadavg[o].toFixed(2)+"</li>";a+="<li>"+type+"</li>"}t.find('[data-output="main"]').html(u);t.find('[data-output="raw"]').html(a)}};e.displayDetails=function(e){}}})})(App,Modernizr);
+(function(e,t){e=e||{};Modernizr.load({load:[e.siteUrl+"/assets/js/libs/jq-1.8.2.min.js",e.siteUrl+"/assets/js/libs/moment-1.7.2.js"],complete:function(){var t=$("#terminals"),n=$(".terminal");t.on("click",".terminal",function(e){e.preventDefault();n.not(this).removeClass("active");this.classList.add("active")});$(window).on("keyup",function(e){e.keyCode===27&&n.removeClass("active")});e.socket.on("broadcast",function(t){console.log(t);t.results&&t.results.osData&&e.updateTerminals(t.results.osData);t.results&&t.results.logs&&e.updateLogs(t.results.logs)});e.updateTerminals=function(e){console.log(e);var t;if(e.uptime){t=$('[data-os="uptime"]');var n=moment.duration(e.uptime*1e3).humanize(),r=e.uptime+" seconds";t.find('[data-output="main"]').html(n);t.find('[data-output="raw"]').html(r)}if(e.freemem&&e.totalmem){t=$('[data-os="freemem"]');var i=Math.round(e.freemem/1024/1024)+" MB",s=e.freemem+" bytes",o=Math.round(e.freemem/e.totalmem*100)+"%";t.find('[data-output="main"]').html(i+" / "+o);t.find('[data-bar="free"]').css("width",o);t.find('[data-output="raw"]').html(s)}if(e.loadavg){t=$('[data-os="loadavg"]');var u=0,a="",f="";type="";for(u;u<e.loadavg.length;u++){u===0?type="1min":u===1?type="5min":type="15min";a+="<li>"+e.loadavg[u].toFixed(2)+"</li>";f+="<li>"+type+"</li>"}t.find('[data-output="main"]').html(a);t.find('[data-output="raw"]').html(f)}};e.updateLogs=function(e){var t=null;if(e.apacheErr){t=$('[data-log="apacheErr"]');t.find('[data-output="main"]').html(e.apacheErr.latestLines.join("<br />"))}};e.displayDetails=function(e){}}})})(App,Modernizr);
@@ -156,6 +156,12 @@ input:indeterminate {} /* un-selected radio buttons :) */
/*-webkit-flow-into: details-space;*/
}
+#logs {
+ display: -webkit-flex;
+ -webkit-flex: 2;
+ /*-webkit-flow-into: details-space;*/
+}
+
/* ====================================================
@@ -164,7 +170,8 @@ input:indeterminate {} /* un-selected radio buttons :) */
Modules are the reusable, modular parts of our design.
They are the callouts, the sidebar sections, the product lists and so on.
==================================================== */
-.terminal {
+.terminal,
+.log {
.light-border;
.drop-shadow;
.filter;
@@ -175,9 +182,9 @@ input:indeterminate {} /* un-selected radio buttons :) */
text-align: center;
-webkit-flex: 2;
- -webkit-transform-style: preserve-3d;
+ /*-webkit-transform-style: preserve-3d;
-webkit-transform: rotateY(20deg)
- skewY(30deg);
+ skewY(30deg);*/
/*-webkit-transform: scale(0.5);*/
-webkit-transition: 0.8s border-color ease,
@@ -192,14 +199,27 @@ input:indeterminate {} /* un-selected radio buttons :) */
margin: 0 2%;
}
}
-.p-title {
+.p-title,
+.log-title {
font-size: 1em;
text-transform: uppercase;
}
-.p-output {
+.p-output,
+.log-output {
font-size: 2em;
margin: 0.5em 0;
}
+.bar-graph {
+ width: 100%;
+ height: 20px;
+ background-color: @c-glow-1;
+ .bar {
+ background-color: @c-copy-3;
+ color: #DDD;
+ height: 100%;
+ font-size: 14px;
+ }
+}
/* ====================================================
STATES STYLES
View
@@ -1,7 +1,7 @@
var http = require('http'),
https = require('https'),
fs = require('fs'),
- lazy = require('lazy'),
+ reader = require('buffered-reader'),
express = require('express'),
handlebars = require('handlebars'),
os = require('os'),
@@ -31,7 +31,7 @@ var http = require('http'),
this.stalkFiles({ files: opts.files });
// TODO: move the interval time to config.js
- // this.stalkSystem(2000);
+ this.stalkSystem(1000);
},
@@ -116,44 +116,39 @@ var http = require('http'),
fileObj = fileObj || false;
var interval = fileObj.interval || 10000, // 10 senconds default
- i = 0;
+ i = 0,
+ lines = [],
+ that = this,
+
+ dataReader = new reader.DataReader(fileObj.path, {encoding: "utf8"})
+ .on('error', function (error) {
+ console.log('error reading file: ' + error);
+ })
+ .on('line', function (line, nextByteOffset) {
+ i++;
+ lines.push('LINE: ' + i + ' - ' + line);
+ })
+ .on('end', function () {
+
+ var logs = {};
+ logs[fileObj.slug] = fileObj; // the original file data we were supplied
- // fs.watchFile(fileObj.path, { interval: interval }, function (curr, prev) {
- // console.log('-- LOG - '+fileObj.name+' changed at: ' + curr.mtime);
- // console.log('-- LOG - The previous mtime was: ' + prev.mtime);
- // fs.readFile(fileObj.path, 'utf8', function (err, data) {
- // if (err) throw err;
- // console.log(data);
-
- // });
- // });
-
-
- // var stream = fs.createReadStream(fileObj.path, { encoding: 'utf8' });
- // stream.on('data', function (data) {
- // i++;
- // console.log('here');
- // console.log('LINE: ' + i + ': ' + data + '\n');
- // });
- // console.log(stream);
- // stream.on('error', function (err) {
- // console.log(err);
- // });
- // stream.on('end', function () {
- // console.log('this is the end for ' + fileObj.name);
- // });
-
- var j = 0;
- new lazy(fs.createReadStream(fileObj.path))
- .lines
- .forEach(function (line) {
- j++;
- console.log(j);
- // console.log(++i + ': ' + line.toString());
- });
- console.log('totoal lines = ' + j);
-
- console.log('here');
+ logs[fileObj.slug].latestLines = lines.slice(lines.length - 10); // last 10 lines
+ logs[fileObj.slug].lineCount = lines.length;
+
+ that.broadcast({
+ logs: logs
+ }, false, true);
+
+ });
+
+ fs.watchFile(fileObj.path, { interval: interval }, function (curr, prev) {
+ dataReader.read();
+ });
+ //
+ // setInterval(function () {
+ // dataReader.read();
+ // }, 4000)
},
Oops, something went wrong.

0 comments on commit b7950bb

Please sign in to comment.