summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Van Doorn <vandoorn.nick@gmail.com>2016-11-19 01:02:01 -0800
committerNick Van Doorn <vandoorn.nick@gmail.com>2016-11-19 01:02:01 -0800
commit1fa1933463a0fd38e7312f3f1a4441e6410008fa (patch)
tree5d4915b379dda4e7920c38c3cc8522d85dfb5a0a
First commit
-rw-r--r--.gitignore3
-rw-r--r--app.js64
-rwxr-xr-xbin/www91
-rw-r--r--controllers/log.js146
-rw-r--r--helpers/helpers.js7
-rw-r--r--package.json23
-rw-r--r--public/stylesheets/lib/normalize.css427
-rw-r--r--public/stylesheets/lib/skeleton.css418
-rw-r--r--public/stylesheets/style.css27
-rw-r--r--scss/stylesheets/style.scss37
-rw-r--r--views/error.jade6
-rw-r--r--views/layout.jade10
-rw-r--r--views/loglines.jade3
-rw-r--r--views/logs.jade8
-rw-r--r--views/sidebar.jade5
15 files changed, 1275 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2bcb02d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+.idea/
+.DS_store
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..2f9b369
--- /dev/null
+++ b/app.js
@@ -0,0 +1,64 @@
+const express = require('express');
+const path = require('path');
+const favicon = require('serve-favicon');
+const logger = require('morgan');
+const cookieParser = require('cookie-parser');
+const bodyParser = require('body-parser');
+const sass = require('node-sass-middleware');
+
+const logs = require('./controllers/log');
+
+const app = express();
+
+// view engine setup
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'jade');
+app.use(sass({
+ src: path.join(__dirname, 'scss'),
+ dest: path.join(__dirname, 'public'),
+ debug: process.env.NODE_ENV === "development" || false
+}));
+
+// uncomment after placing your favicon in /public
+//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
+app.use(logger('dev'));
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(express.static(path.join(__dirname, 'public')));
+
+app.use('/logs', logs);
+
+// catch 404 and forward to error handler
+app.use(function(req, res, next) {
+ var err = new Error('Not Found');
+ err.status = 404;
+ next(err);
+});
+
+// error handlers
+
+// development error handler
+// will print stacktrace
+if (app.get('env') === 'development') {
+ app.use(function(err, req, res, next) {
+ res.status(err.status || 500);
+ res.render('error', {
+ message: err.message,
+ error: err
+ });
+ });
+}
+
+// production error handler
+// no stacktraces leaked to user
+app.use(function(err, req, res, next) {
+ res.status(err.status || 500);
+ res.render('error', {
+ message: err.message,
+ error: {}
+ });
+});
+
+
+module.exports = app;
diff --git a/bin/www b/bin/www
new file mode 100755
index 0000000..35be9a9
--- /dev/null
+++ b/bin/www
@@ -0,0 +1,91 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var app = require('../app');
+var debug = require('debug')('logclient:server');
+var http = require('http');
+
+/**
+ * Get port from environment and store in Express.
+ */
+
+var port = normalizePort(process.env.PORT || '3000');
+app.set('port', port);
+
+/**
+ * Create HTTP server.
+ */
+
+var server = http.createServer(app);
+
+/**
+ * Listen on provided port, on all network interfaces.
+ */
+
+server.listen(port);
+server.on('error', onError);
+server.on('listening', onListening);
+server.timeout = 100000;
+
+/**
+ * Normalize a port into a number, string, or false.
+ */
+
+function normalizePort(val) {
+ var port = parseInt(val, 10);
+
+ if (isNaN(port)) {
+ // named pipe
+ return val;
+ }
+
+ if (port >= 0) {
+ // port number
+ return port;
+ }
+
+ return false;
+}
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+
+function onError(error) {
+ if (error.syscall !== 'listen') {
+ throw error;
+ }
+
+ var bind = typeof port === 'string'
+ ? 'Pipe ' + port
+ : 'Port ' + port;
+
+ // handle specific listen errors with friendly messages
+ switch (error.code) {
+ case 'EACCES':
+ console.error(bind + ' requires elevated privileges');
+ process.exit(1);
+ break;
+ case 'EADDRINUSE':
+ console.error(bind + ' is already in use');
+ process.exit(1);
+ break;
+ default:
+ throw error;
+ }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+
+function onListening() {
+ var addr = server.address();
+ var bind = typeof addr === 'string'
+ ? 'pipe ' + addr
+ : 'port ' + addr.port;
+ debug('Listening on ' + bind);
+}
diff --git a/controllers/log.js b/controllers/log.js
new file mode 100644
index 0000000..987988d
--- /dev/null
+++ b/controllers/log.js
@@ -0,0 +1,146 @@
+const fs = require('fs');
+const async = require('async');
+const events = require('events');
+const helpers = require('../helpers/helpers');
+const express = require('express');
+const path = require('path');
+const util = require('util');
+const walk = require('walk');
+const router = express.Router();
+
+const LOG_DIR = '/var/log/clientlog';
+const DEFAULT_START_TIME_HR = 8;
+
+exports.levels = {
+ ERROR: 0,
+ WARN: 1,
+ INFO: 2,
+ VERBOSE: 3,
+ DEBUG: 4
+};
+
+exports.getClass = (level) => {
+ let levelStr;
+ if(helpers.isUndefined(level)){
+ levelStr = 'debug';
+ }
+ else{
+ levelStr = level;
+ }
+ return util.format('logline-%s', levelStr.toLowerCase());
+};
+
+exports.generateLogEntries = (logPath) => {
+ return new Promise((resolve, reject) => {
+ async.waterfall([
+ (next) => {
+ fs.readFile(path.join(LOG_DIR, logPath), (err, data) => {
+ next(null, data.toString());
+ });
+ },
+ (data, next) => {
+ next(null, data.split('\n'));
+ },
+ (data, done) => {
+ let logEntries = [];
+ for(let logEntry of data){
+ let parts = logEntry.split(/[+ ]+/);
+ let isoTime, timestamp, levelKey, level;
+ try{
+ isoTime = parts[0].substring(1, parts[0].length - 1);
+ timestamp = new Date(isoTime).getTime();
+ levelKey = parts[1].substring(1, parts[1].length - 2);
+ level = helpers.isUndefined(levelKey) ? undefined : exports.levels[levelKey];
+ }
+ catch(err){
+
+ }
+ logEntries.push({
+ text: logEntry,
+ time: timestamp,
+ level: level,
+ classStr: exports.getClass(levelKey)
+ });
+ }
+ done(null, logEntries)
+ }],
+ (err, results) => {
+ resolve(results);
+ }
+ );
+ });
+};
+
+exports.getLogFileNames = (logDir) => {
+ return new Promise((resolve, reject) => {
+ let logFileNames = [];
+ let walker = walk.walk(logDir);
+ walker.on('file', (root, fileStats, next) => {
+ logFileNames.push(fileStats.name);
+ next();
+ });
+
+ walker.on('end', () => {
+ resolve(logFileNames);
+ });
+ });
+
+};
+
+exports.LogFile = function(logPath){
+ const self = this;
+ exports.generateLogEntries(logPath).then((data) => {
+ self.logEntries = data;
+ self.emit('ready');
+ });
+};
+
+exports.LogFile.prototype.__proto__ = events.EventEmitter.prototype;
+
+exports.LogFile.prototype.query = function(query){
+ const self = this;
+ return new Promise((resolve, reject) => {
+ let startTimestamp = helpers.isUndefined(query.startdt) ? new Date().getTime() - 60 * 60 * 1000 * DEFAULT_START_TIME_HR
+ : new Date(query.startdt).getTime();
+ let endTimestamp = helpers.isUndefined(query.enddt) ? new Date().getTime() : new Date(query.enddt).getTime();
+ let startLine = query.startline || 0;
+ let endLine = query.endline || self.logEntries.length;
+ let level = parseInt(query.level) || exports.levels.DEBUG;
+ let filtered = self.logEntries.filter((entry) => {
+ let timeMatch = entry.time >= startTimestamp && entry.time <= endTimestamp;
+ let levelMatch = entry.level <= level;
+ let lineMatch = self.logEntries.indexOf(entry) >= startLine && self.logEntries.indexOf(entry) <= endLine;
+ return timeMatch && levelMatch && lineMatch;
+ });
+ resolve(filtered);
+ });
+};
+
+exports.LogFile.prototype.getAll = function(){
+ return this.logEntries;
+};
+
+router.get('/:logfile', (req, res, next) => {
+ let logFile = new exports.LogFile(req.params.logfile);
+ logFile.on('ready', () => {
+ let promises = [
+ logFile.query(req.query),
+ exports.getLogFileNames(LOG_DIR)
+ ];
+ Promise.all(promises).then((values) => {
+ res.render('logs', {
+ title: req.params.logfile,
+ logEntries: values[0],
+ logFiles: values[1]
+ });
+ });
+
+ });
+});
+
+module.exports = router;
+
+exports.getLogFileNames(LOG_DIR);
+
+
+
diff --git a/helpers/helpers.js b/helpers/helpers.js
new file mode 100644
index 0000000..2b7d124
--- /dev/null
+++ b/helpers/helpers.js
@@ -0,0 +1,7 @@
+exports.isUndefined = (property) => {
+ return typeof property === 'undefined';
+};
+
+exports.isNumber = (number) => {
+ return typeof number === 'number';
+}; \ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..98fc355
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "logclient",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "start": "node ./bin/www"
+ },
+ "dependencies": {
+ "async": "^2.1.2",
+ "body-parser": "~1.15.1",
+ "cookie-parser": "~1.4.3",
+ "debug": "~2.2.0",
+ "events": "^1.1.1",
+ "express": "~4.13.4",
+ "fs": "0.0.2",
+ "jade": "~1.11.0",
+ "morgan": "~1.7.0",
+ "node-sass-middleware": "^0.10.0",
+ "path": "^0.12.7",
+ "serve-favicon": "~2.3.0",
+ "walk": "^2.3.9"
+ }
+}
diff --git a/public/stylesheets/lib/normalize.css b/public/stylesheets/lib/normalize.css
new file mode 100644
index 0000000..81c6f31
--- /dev/null
+++ b/public/stylesheets/lib/normalize.css
@@ -0,0 +1,427 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; /* 2 */
+ box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+} \ No newline at end of file
diff --git a/public/stylesheets/lib/skeleton.css b/public/stylesheets/lib/skeleton.css
new file mode 100644
index 0000000..f28bf6c
--- /dev/null
+++ b/public/stylesheets/lib/skeleton.css
@@ -0,0 +1,418 @@
+/*
+* Skeleton V2.0.4
+* Copyright 2014, Dave Gamache
+* www.getskeleton.com
+* Free to use under the MIT license.
+* http://www.opensource.org/licenses/mit-license.php
+* 12/29/2014
+*/
+
+
+/* Table of contents
+––––––––––––––––––––––––––––––––––––––––––––––––––
+- Grid
+- Base Styles
+- Typography
+- Links
+- Buttons
+- Forms
+- Lists
+- Code
+- Tables
+- Spacing
+- Utilities
+- Clearing
+- Media Queries
+*/
+
+
+/* Grid
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.container {
+ position: relative;
+ width: 100%;
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 0 20px;
+ box-sizing: border-box; }
+.column,
+.columns {
+ width: 100%;
+ float: left;
+ box-sizing: border-box; }
+
+/* For devices larger than 400px */
+@media (min-width: 400px) {
+ .container {
+ width: 85%;
+ padding: 0; }
+}
+
+/* For devices larger than 550px */
+@media (min-width: 550px) {
+ .container {
+ width: 80%; }
+ .column,
+ .columns {
+ margin-left: 4%; }
+ .column:first-child,
+ .columns:first-child {
+ margin-left: 0; }
+
+ .one.column,
+ .one.columns { width: 4.66666666667%; }
+ .two.columns { width: 13.3333333333%; }
+ .three.columns { width: 22%; }
+ .four.columns { width: 30.6666666667%; }
+ .five.columns { width: 39.3333333333%; }
+ .six.columns { width: 48%; }
+ .seven.columns { width: 56.6666666667%; }
+ .eight.columns { width: 65.3333333333%; }
+ .nine.columns { width: 74.0%; }
+ .ten.columns { width: 82.6666666667%; }
+ .eleven.columns { width: 91.3333333333%; }
+ .twelve.columns { width: 100%; margin-left: 0; }
+
+ .one-third.column { width: 30.6666666667%; }
+ .two-thirds.column { width: 65.3333333333%; }
+
+ .one-half.column { width: 48%; }
+
+ /* Offsets */
+ .offset-by-one.column,
+ .offset-by-one.columns { margin-left: 8.66666666667%; }
+ .offset-by-two.column,
+ .offset-by-two.columns { margin-left: 17.3333333333%; }
+ .offset-by-three.column,
+ .offset-by-three.columns { margin-left: 26%; }
+ .offset-by-four.column,
+ .offset-by-four.columns { margin-left: 34.6666666667%; }
+ .offset-by-five.column,
+ .offset-by-five.columns { margin-left: 43.3333333333%; }
+ .offset-by-six.column,
+ .offset-by-six.columns { margin-left: 52%; }
+ .offset-by-seven.column,
+ .offset-by-seven.columns { margin-left: 60.6666666667%; }
+ .offset-by-eight.column,
+ .offset-by-eight.columns { margin-left: 69.3333333333%; }
+ .offset-by-nine.column,
+ .offset-by-nine.columns { margin-left: 78.0%; }
+ .offset-by-ten.column,
+ .offset-by-ten.columns { margin-left: 86.6666666667%; }
+ .offset-by-eleven.column,
+ .offset-by-eleven.columns { margin-left: 95.3333333333%; }
+
+ .offset-by-one-third.column,
+ .offset-by-one-third.columns { margin-left: 34.6666666667%; }
+ .offset-by-two-thirds.column,
+ .offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
+
+ .offset-by-one-half.column,
+ .offset-by-one-half.columns { margin-left: 52%; }
+
+}
+
+
+/* Base Styles
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/* NOTE
+html is set to 62.5% so that all the REM measurements throughout Skeleton
+are based on 10px sizing. So basically 1.5rem = 15px :) */
+html {
+ font-size: 62.5%; }
+body {
+ font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
+ line-height: 1.6;
+ font-weight: 400;
+ font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color: #222; }
+
+
+/* Typography
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+h1, h2, h3, h4, h5, h6 {
+ margin-top: 0;
+ margin-bottom: 2rem;
+ font-weight: 300; }
+h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
+h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
+h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
+h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
+h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
+h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
+
+/* Larger than phablet */
+@media (min-width: 550px) {
+ h1 { font-size: 5.0rem; }
+ h2 { font-size: 4.2rem; }
+ h3 { font-size: 3.6rem; }
+ h4 { font-size: 3.0rem; }
+ h5 { font-size: 2.4rem; }
+ h6 { font-size: 1.5rem; }
+}
+
+p {
+ margin-top: 0; }
+
+
+/* Links
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+a {
+ color: #1EAEDB; }
+a:hover {
+ color: #0FA0CE; }
+
+
+/* Buttons
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.button,
+button,
+input[type="submit"],
+input[type="reset"],
+input[type="button"] {
+ display: inline-block;
+ height: 38px;
+ padding: 0 30px;
+ color: #555;
+ text-align: center;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 38px;
+ letter-spacing: .1rem;
+ text-transform: uppercase;
+ text-decoration: none;
+ white-space: nowrap;
+ background-color: transparent;
+ border-radius: 4px;
+ border: 1px solid #bbb;
+ cursor: pointer;
+ box-sizing: border-box; }
+.button:hover,
+button:hover,
+input[type="submit"]:hover,
+input[type="reset"]:hover,
+input[type="button"]:hover,
+.button:focus,
+button:focus,
+input[type="submit"]:focus,
+input[type="reset"]:focus,
+input[type="button"]:focus {
+ color: #333;
+ border-color: #888;
+ outline: 0; }
+.button.button-primary,
+button.button-primary,
+input[type="submit"].button-primary,
+input[type="reset"].button-primary,
+input[type="button"].button-primary {
+ color: #FFF;
+ background-color: #33C3F0;
+ border-color: #33C3F0; }
+.button.button-primary:hover,
+button.button-primary:hover,
+input[type="submit"].button-primary:hover,
+input[type="reset"].button-primary:hover,
+input[type="button"].button-primary:hover,
+.button.button-primary:focus,
+button.button-primary:focus,
+input[type="submit"].button-primary:focus,
+input[type="reset"].button-primary:focus,
+input[type="button"].button-primary:focus {
+ color: #FFF;
+ background-color: #1EAEDB;
+ border-color: #1EAEDB; }
+
+
+/* Forms
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="text"],
+input[type="tel"],
+input[type="url"],
+input[type="password"],
+textarea,
+select {
+ height: 38px;
+ padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
+ background-color: #fff;
+ border: 1px solid #D1D1D1;
+ border-radius: 4px;
+ box-shadow: none;
+ box-sizing: border-box; }
+/* Removes awkward default styles on some inputs for iOS */
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="text"],
+input[type="tel"],
+input[type="url"],
+input[type="password"],
+textarea {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none; }
+textarea {
+ min-height: 65px;
+ padding-top: 6px;
+ padding-bottom: 6px; }
+input[type="email"]:focus,
+input[type="number"]:focus,
+input[type="search"]:focus,
+input[type="text"]:focus,
+input[type="tel"]:focus,
+input[type="url"]:focus,
+input[type="password"]:focus,
+textarea:focus,
+select:focus {
+ border: 1px solid #33C3F0;
+ outline: 0; }
+label,
+legend {
+ display: block;
+ margin-bottom: .5rem;
+ font-weight: 600; }
+fieldset {
+ padding: 0;
+ border-width: 0; }
+input[type="checkbox"],
+input[type="radio"] {
+ display: inline; }
+label > .label-body {
+ display: inline-block;
+ margin-left: .5rem;
+ font-weight: normal; }
+
+
+/* Lists
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+ul {
+ list-style: circle inside; }
+ol {
+ list-style: decimal inside; }
+ol, ul {
+ padding-left: 0;
+ margin-top: 0; }
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+ margin: 1.5rem 0 1.5rem 3rem;
+ font-size: 90%; }
+li {
+ margin-bottom: 1rem; }
+
+
+/* Code
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+code {
+ padding: .2rem .5rem;
+ margin: 0 .2rem;
+ font-size: 90%;
+ white-space: nowrap;
+ background: #F1F1F1;
+ border: 1px solid #E1E1E1;
+ border-radius: 4px; }
+pre > code {
+ display: block;
+ padding: 1rem 1.5rem;
+ white-space: pre; }
+
+
+/* Tables
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+th,
+td {
+ padding: 12px 15px;
+ text-align: left;
+ border-bottom: 1px solid #E1E1E1; }
+th:first-child,
+td:first-child {
+ padding-left: 0; }
+th:last-child,
+td:last-child {
+ padding-right: 0; }
+
+
+/* Spacing
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+button,
+.button {
+ margin-bottom: 1rem; }
+input,
+textarea,
+select,
+fieldset {
+ margin-bottom: 1.5rem; }
+pre,
+blockquote,
+dl,
+figure,
+table,
+p,
+ul,
+ol,
+form {
+ margin-bottom: 2.5rem; }
+
+
+/* Utilities
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.u-full-width {
+ width: 100%;
+ box-sizing: border-box; }
+.u-max-full-width {
+ max-width: 100%;
+ box-sizing: border-box; }
+.u-pull-right {
+ float: right; }
+.u-pull-left {
+ float: left; }
+
+
+/* Misc
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+hr {
+ margin-top: 3rem;
+ margin-bottom: 3.5rem;
+ border-width: 0;
+ border-top: 1px solid #E1E1E1; }
+
+
+/* Clearing
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+
+/* Self Clearing Goodness */
+.container:after,
+.row:after,
+.u-cf {
+ content: "";
+ display: table;
+ clear: both; }
+
+
+/* Media Queries
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/*
+Note: The best way to structure the use of media queries is to create the queries
+near the relevant code. For example, if you wanted to change the styles for buttons
+on small devices, paste the mobile query code up in the buttons section and style it
+there.
+*/
+
+
+/* Larger than mobile */
+@media (min-width: 400px) {}
+
+/* Larger than phablet (also point when grid becomes active) */
+@media (min-width: 550px) {}
+
+/* Larger than tablet */
+@media (min-width: 750px) {}
+
+/* Larger than desktop */
+@media (min-width: 1000px) {}
+
+/* Larger than Desktop HD */
+@media (min-width: 1200px) {}
diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css
new file mode 100644
index 0000000..5dfc188
--- /dev/null
+++ b/public/stylesheets/style.css
@@ -0,0 +1,27 @@
+body {
+ padding: 50px;
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; }
+
+a {
+ color: #00B7FF; }
+
+.logline-error {
+ color: #d9534f; }
+
+.logline-warn {
+ color: #f0ad4e; }
+
+.logline-info {
+ color: #5bc0de; }
+
+.logline-verbose {
+ color: #428bca; }
+
+.logline-debug {
+ color: #5cb85c; }
+
+#loglines {
+ border-radius: 5px;
+ background: #e6e6e6;
+ padding: 20px;
+ overflow: scroll; }
diff --git a/scss/stylesheets/style.scss b/scss/stylesheets/style.scss
new file mode 100644
index 0000000..1722462
--- /dev/null
+++ b/scss/stylesheets/style.scss
@@ -0,0 +1,37 @@
+$loglines-bg: lighten(#000, 90%);
+
+body {
+ padding: 50px;
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+
+a {
+ color: #00B7FF;
+}
+
+.logline-error{
+ color: #d9534f;
+}
+
+.logline-warn{
+ color: #f0ad4e;
+}
+
+.logline-info{
+ color: #5bc0de;
+}
+
+.logline-verbose{
+ color: #428bca;
+}
+
+.logline-debug{
+ color: #5cb85c;
+}
+
+#loglines{
+ border-radius: 5px;
+ background: $loglines-bg;
+ padding: 20px;
+ overflow: scroll;
+} \ No newline at end of file
diff --git a/views/error.jade b/views/error.jade
new file mode 100644
index 0000000..51ec12c
--- /dev/null
+++ b/views/error.jade
@@ -0,0 +1,6 @@
+extends layout
+
+block content
+ h1= message
+ h2= error.status
+ pre #{error.stack}
diff --git a/views/layout.jade b/views/layout.jade
new file mode 100644
index 0000000..d985021
--- /dev/null
+++ b/views/layout.jade
@@ -0,0 +1,10 @@
+doctype html
+html
+ head
+ title= title
+ link(rel='stylesheet', href='/stylesheets/style.css')
+ link(rel='stylesheet', href='/stylesheets/lib/skeleton.css')
+ link(rel='stylesheet', href='/stylesheets/lib/normalize.css')
+
+ body
+ block content
diff --git a/views/loglines.jade b/views/loglines.jade
new file mode 100644
index 0000000..e69193d
--- /dev/null
+++ b/views/loglines.jade
@@ -0,0 +1,3 @@
+#loglines.columns.nine
+ for logEntry in logEntries
+ p(class=logEntry.classStr)= logEntry.text \ No newline at end of file
diff --git a/views/logs.jade b/views/logs.jade
new file mode 100644
index 0000000..f113cf5
--- /dev/null
+++ b/views/logs.jade
@@ -0,0 +1,8 @@
+extends layout
+
+block content
+
+ .row
+ include sidebar
+ include loglines
+
diff --git a/views/sidebar.jade b/views/sidebar.jade
new file mode 100644
index 0000000..1a894c4
--- /dev/null
+++ b/views/sidebar.jade
@@ -0,0 +1,5 @@
+#sidebar.columns.three
+ ul
+ each logFileName in logFiles
+ li
+ a(href=logFileName)= logFileName \ No newline at end of file