├── .gitignore ├── app ├── favicon.ico ├── robots.txt ├── styles │ ├── .DS_Store │ └── home.scss ├── humans.txt └── scripts │ └── main.js ├── views ├── .DS_Store ├── layouts │ ├── .DS_Store │ └── main.hbs ├── settings.hbs └── home.hbs ├── .jscsrc ├── config └── config.sample.js ├── .jshintrc ├── db └── db-helper.js ├── README.md ├── app.js ├── index.js ├── package.json ├── model └── runs-model.js ├── controller └── RequestController.js └── gulpfile.babel.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config/config.js 3 | settings.json 4 | dist/ 5 | .tmp/ 6 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauntface/webperf-monitor-frontend/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /views/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauntface/webperf-monitor-frontend/HEAD/views/.DS_Store -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /app/styles/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauntface/webperf-monitor-frontend/HEAD/app/styles/.DS_Store -------------------------------------------------------------------------------- /views/layouts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gauntface/webperf-monitor-frontend/HEAD/views/layouts/.DS_Store -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "validateLineBreaks": "LF", 4 | "validateIndentation": 2, 5 | "excludeFiles": ["node_modules/**"] 6 | } 7 | -------------------------------------------------------------------------------- /config/config.sample.js: -------------------------------------------------------------------------------- 1 | exports.dbURL = { 2 | host : 'localhost', 3 | user : '', 4 | password : '', 5 | port: 8889 6 | }; 7 | exports.dbName = 'webperfmonitor'; -------------------------------------------------------------------------------- /app/humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org/ 2 | # The humans responsible & technology colophon 3 | 4 | # TEAM 5 | 6 | -- -- 7 | 8 | # THANKS 9 | 10 | 11 | 12 | # TECHNOLOGY COLOPHON 13 | 14 | HTML5, CSS3 15 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node" : true, 3 | "browser" : true, 4 | "esnext" : true, 5 | "bitwise" : true, 6 | "camelcase" : true, 7 | "curly" : true, 8 | "eqeqeq" : true, 9 | "immed" : true, 10 | "indent" : 2, 11 | "newcap" : true, 12 | "noarg" : true, 13 | "quotmark" : "single", 14 | "undef" : true, 15 | "unused" : "vars", 16 | "strict" : true, 17 | "globalstrict" : true, 18 | "globals": { 19 | "d3": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /db/db-helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mysql = require('mysql'); 4 | 5 | exports.openDb = function(cb) { 6 | var config = require('../' + GLOBAL.configFile); 7 | if (config === null) { 8 | cb('No Config Available to load the database'); 9 | return; 10 | } 11 | 12 | var connection = mysql.createConnection(config.dbURL); 13 | 14 | // a connection can also be implicitly established by invoking a query 15 | connection.query('USE ' + config.dbName, function(err) { 16 | if (err) { 17 | cb('Unable to use the database [' + config.dbName + ']'); 18 | return; 19 | } 20 | 21 | cb(null, connection); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | webperf-monitor-frontend 2 | ======================== 3 | 4 | This is a web based front end for [webperf-monitor](https://github.com/gauntface/webperf-monitor). 5 | 6 | To start the server, grab the source and run it with the following commands: 7 | 8 | sudo npm install 9 | 10 | gulp 11 | 12 | node index.js -c 13 | 14 | The config file is the same as webperf-monitor, but there is also a copy in this repo in config/config.sample.js: 15 | 16 | exports.dbURL = { 17 | host : 'localhost', 18 | user : '', 19 | password : '', 20 | port: 8889 21 | }; 22 | exports.dbName = 'webperfmonitor'; 23 | 24 | If you want to set this task up to run constantly then I'd recommend using the forever task: 25 | 26 | sudo npm install forever -g 27 | 28 | forever start index.js -c -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function startServer () { 4 | var configFilePath = 'config/config.js'; 5 | var express = require('express'); 6 | var exphbs = require('express3-handlebars'); 7 | var RequestController = require('./controller/RequestController.js'); 8 | var app = express(); 9 | var server; 10 | 11 | GLOBAL.configFile = configFilePath; 12 | 13 | app.engine('hbs', exphbs({extname:'.hbs', defaultLayout: 'main'})); 14 | app.set('view engine', 'hbs'); 15 | 16 | var bodyParser = require('body-parser'); 17 | app.use(bodyParser.json()); 18 | app.use(bodyParser.urlencoded({ 19 | extended: true 20 | })); 21 | 22 | app.get('/', RequestController.getIndexRequest); 23 | app.get('/settings', RequestController.getSettingsRequest); 24 | app.post('/settings/save', RequestController.getSettingsSubmitRequest); 25 | 26 | app.use('/styles', express.static(__dirname + '/dist/styles')); 27 | app.use('/scripts', express.static(__dirname + '/dist/scripts')); 28 | 29 | server = app.listen(3000, function() { 30 | console.log('Listening on port %d', server.address().port); 31 | }); 32 | } 33 | 34 | startServer(); 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pkg = require( './package.json' ); 4 | 5 | var argv = require('minimist')(process.argv.slice(2)); 6 | 7 | var printHelp = function() { 8 | console.log([ 9 | 'webperf-monitor', 10 | pkg.description, 11 | '', 12 | 'Usage:', 13 | ' $ webperf-monitor -c ' 14 | ].join('\n')); 15 | }; 16 | 17 | if(argv.v || argv.version) { 18 | console.log(pkg.version); 19 | return; 20 | } 21 | 22 | if(argv.h || argv.help) { 23 | printHelp(); 24 | return; 25 | } 26 | 27 | var configFilePath = './config/config.js'; 28 | 29 | if(argv.c || argv.config) { 30 | configFilePath = argv.c || argv.config; 31 | } 32 | 33 | if(configFilePath.indexOf('.') === 0) { 34 | configFilePath = configFilePath.substring(1); 35 | configFilePath = __dirname + configFilePath; 36 | } 37 | 38 | console.log('Looking for config file at '+configFilePath); 39 | 40 | var config; 41 | try { 42 | config = require(configFilePath); 43 | } catch(exception) {} 44 | 45 | if(!config) { 46 | console.error('No config file could be found.'); 47 | //process.exit(); 48 | //return; 49 | //require('./setup.js'); 50 | } 51 | 52 | require('./app.js'); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "apache-server-configs": "^2.7.1", 4 | "babel": "^5.4.7", 5 | "browser-sync": "^2.6.4", 6 | "del": "^1.1.0", 7 | "gulp": "^3.9.0", 8 | "gulp-autoprefixer": "^2.0.0", 9 | "gulp-cache": "0.2.2", 10 | "gulp-changed": "^1.0.0", 11 | "gulp-concat": "^2.5.2", 12 | "gulp-csso": "^1.0.0", 13 | "gulp-flatten": "0.0.4", 14 | "gulp-if": "^1.2.1", 15 | "gulp-imagemin": "^2.0.0", 16 | "gulp-jshint": "^1.6.3", 17 | "gulp-load-plugins": "^0.10.0", 18 | "gulp-minify-html": "0.1.5", 19 | "gulp-replace": "^0.5.3", 20 | "gulp-sass": "^2.0.0", 21 | "gulp-size": "^1.0.0", 22 | "gulp-sourcemaps": "^1.3.0", 23 | "gulp-uglify": "^1.0.1", 24 | "gulp-uncss": "^1.0.0", 25 | "gulp-useref": "^1.0.1", 26 | "jshint-stylish": "^1.0.0", 27 | "opn": "^1.0.0", 28 | "psi": "^1.0.4", 29 | "require-dir": "^0.3.0", 30 | "run-sequence": "^1.0.1", 31 | "sw-precache": "^1.3.2" 32 | }, 33 | "engines": { 34 | "node": ">=0.10.0" 35 | }, 36 | "private": true, 37 | "scripts": { 38 | "test": "gulp && git status | grep 'working directory clean' >/dev/null || (echo 'Please commit all changes generated by building'; exit 1)" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /views/settings.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{message}}

3 | 4 |
5 |

6 |
7 | 8 |

9 | 10 |

11 |
12 | 13 |

14 | 15 |

16 |
17 | 18 |

19 | 20 |

21 |
22 | 23 |

24 | 25 |

26 |
27 | 28 |

29 | 30 |

31 |
32 | 33 |

34 | 35 |

36 |
37 | 38 |

39 | 40 |

41 |
42 |
43 | -------------------------------------------------------------------------------- /app/styles/home.scss: -------------------------------------------------------------------------------- 1 | /** 2 | This is all MDL 3 | **/ 4 | 5 | .avatar { 6 | width: 48px; 7 | height: 48px; 8 | border-radius: 24px; 9 | } 10 | 11 | .mdl-layout__header { 12 | padding: 0 16px; 13 | overflow: visible; 14 | } 15 | .mdl-layout__drawer { 16 | overflow: visible; 17 | } 18 | .mdl-layout__drawer .avatar { 19 | margin-bottom: 16px; 20 | } 21 | .mdl-layout__drawer > header { 22 | display: flex; 23 | flex-direction: column; 24 | padding: 16px; 25 | } 26 | .mdl-layout__drawer > header > div { 27 | display: flex; 28 | flex-direction: row; 29 | align-items: center; 30 | } 31 | .mdl-layout__drawer nav a { 32 | color: inherit; 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | } 37 | .mdl-layout__drawer nav a .material-icons { 38 | font-size: 1.5em; 39 | margin-right: 16px; 40 | } 41 | .mdl-layout__drawer .mdl-navigation { 42 | flex-grow: 1; 43 | } 44 | 45 | .mdl-layout__content { 46 | padding: 0; 47 | display: flex; 48 | } 49 | .charts { 50 | align-items: center; 51 | } 52 | .charts svg:nth-child(1) { 53 | fill: #ACEC00; 54 | } 55 | .charts svg:nth-child(2) { 56 | fill: #00BBD6; 57 | } 58 | .charts svg:nth-child(3) { 59 | fill: #BA65C9; 60 | } 61 | .charts svg:nth-child(4) { 62 | fill: #EF3C79; 63 | } 64 | .graphs { 65 | padding: 32px; 66 | } 67 | .graphs img { 68 | width: 100%; 69 | height: auto; 70 | margin-bottom: 80px; 71 | } 72 | .graph { 73 | width:100%; 74 | height: auto; 75 | } 76 | .graph:nth-child(1) { 77 | fill: #00b9d8; 78 | } 79 | .graph:nth-child(2) { 80 | fill: #d9006e; 81 | } 82 | 83 | .cards { 84 | padding: 0; 85 | margin: 0; 86 | } 87 | .cards .mdl-card { 88 | height: auto; 89 | display: flex; 90 | flex-direction: column; 91 | } 92 | .cards .mdl-card__title { 93 | display: flex; 94 | flex-direction: row; 95 | box-sizing: border-box; 96 | flex-grow: 1; 97 | } 98 | .cards .mdl-card__supporting-text { 99 | height: auto; 100 | flex-grow: 0; 101 | } 102 | .cards .mdl-card__title h2 { 103 | height: auto; 104 | align-self: flex-end; 105 | } 106 | .cards ul { 107 | padding: 0; 108 | } 109 | .cards h3 { 110 | font-size: 1em; 111 | } 112 | .cards .dog { 113 | height: 200px; 114 | background-image: url('images/dog.png'); 115 | background-position: 90% 100%; 116 | background-repeat: no-repeat; 117 | } 118 | -------------------------------------------------------------------------------- /app/scripts/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Web Starter Kit 4 | * Copyright 2014 Google Inc. All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License 17 | * 18 | */ 19 | 'use strict'; 20 | 21 | (function() { 22 | var querySelector = document.querySelector.bind(document); 23 | 24 | var navdrawerContainer = querySelector('.navdrawer-container'); 25 | var body = document.body; 26 | var appbarElement = querySelector('.app-bar'); 27 | var menuBtn = querySelector('.menu'); 28 | var main = querySelector('main'); 29 | 30 | function closeMenu() { 31 | body.classList.remove('open'); 32 | appbarElement.classList.remove('open'); 33 | navdrawerContainer.classList.remove('open'); 34 | } 35 | 36 | function toggleMenu() { 37 | body.classList.toggle('open'); 38 | appbarElement.classList.toggle('open'); 39 | navdrawerContainer.classList.toggle('open'); 40 | } 41 | 42 | main.addEventListener('click', closeMenu); 43 | menuBtn.addEventListener('click', toggleMenu); 44 | navdrawerContainer.addEventListener('click', function(event) { 45 | if (event.target.nodeName === 'A' || event.target.nodeName === 'LI') { 46 | closeMenu(); 47 | } 48 | }); 49 | })(); 50 | 51 | window.addEventListener('load', function() { 52 | console.log('Starting up graphs hopefully...'); 53 | var data = [{ 54 | sale: '202', 55 | year: '2000' 56 | }, { 57 | sale: '215', 58 | year: '2001' 59 | }, { 60 | sale: '179', 61 | year: '2002' 62 | }, { 63 | sale: '199', 64 | year: '2003' 65 | }, { 66 | sale: '134', 67 | year: '2003' 68 | }, { 69 | sale: '176', 70 | year: '2010' 71 | }]; 72 | 73 | var data2 = [{ 74 | sale: '152', 75 | year: '2000' 76 | }, { 77 | sale: '189', 78 | year: '2002' 79 | }, { 80 | sale: '179', 81 | year: '2004' 82 | }, { 83 | sale: '199', 84 | year: '2006' 85 | }, { 86 | sale: '134', 87 | year: '2008' 88 | }, { 89 | sale: '176', 90 | year: '2010' 91 | }]; 92 | 93 | var vis = d3.select('#visualisation'); 94 | var WIDTH = 1000; 95 | var HEIGHT = 500; 96 | var MARGINS = { 97 | top: 20, 98 | right: 20, 99 | bottom: 20, 100 | left: 50 101 | }; 102 | var xScale = d3.scale.linear() 103 | .range([MARGINS.left, WIDTH - MARGINS.right]) 104 | .domain([2000, 2010]); 105 | var yScale = d3.scale.linear() 106 | .range([HEIGHT - MARGINS.top, MARGINS.bottom]) 107 | .domain([134, 215]); 108 | var xAxis = d3.svg.axis().scale(xScale); 109 | var yAxis = d3.svg.axis() 110 | .scale(yScale) 111 | .orient('left'); 112 | 113 | vis.append('svg:g') 114 | .attr('class', 'axis') 115 | .attr('transform', 'translate(0,' + (HEIGHT - MARGINS.bottom) + ')') 116 | .call(xAxis); 117 | vis.append('svg:g') 118 | .attr('class', 'axis') 119 | .attr('transform', 'translate(' + (MARGINS.left) + ')') 120 | .call(yAxis); 121 | 122 | var lineGen = d3.svg.line() 123 | .x(function(d) { 124 | return xScale(d.year); 125 | }) 126 | .y(function(d) { 127 | return yScale(d.sale); 128 | }) 129 | .interpolate('basis'); 130 | 131 | vis.append('svg:path') 132 | .attr('d', lineGen(data)) 133 | .attr('stroke', 'green') 134 | .attr('stroke-width', 2) 135 | .attr('fill', 'none'); 136 | 137 | vis.append('svg:path') 138 | .attr('d', lineGen(data2)) 139 | .attr('stroke', 'blue') 140 | .attr('stroke-width', 2) 141 | .attr('fill', 'none'); 142 | }); 143 | -------------------------------------------------------------------------------- /model/runs-model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dbHelper = require('./../db/db-helper.js'); 4 | var when = require('when'); 5 | 6 | function RunsModel() { 7 | 8 | } 9 | 10 | RunsModel.prototype.getLatestCompleteRun = function() { 11 | return when.promise(function(resolve, reject, notify) { 12 | dbHelper.openDb(function(err, dbConnection) { 13 | if (err) { 14 | reject('runs-model.js Unable to get db connection: ' + err); 15 | return; 16 | } 17 | 18 | dbConnection.query('SELECT * FROM runs WHERE status = ? ORDER BY ' + 19 | 'end_time DESC', ['successful'], function(err, result) { 20 | dbConnection.destroy(); 21 | if (err) { 22 | reject('runs-model.js Unable to select latest run: ' + err); 23 | return; 24 | } 25 | 26 | if (result.length === 0) { 27 | reject('runs-model.js No runs to select'); 28 | return; 29 | } 30 | 31 | var latestRun = result[0]; 32 | resolve(latestRun); 33 | }); 34 | }); 35 | }); 36 | }; 37 | 38 | RunsModel.prototype.getTopResultsForRun = function(runId, numOfResults) { 39 | return when.promise(function(resolve, reject, notify) { 40 | dbHelper.openDb(function(err, dbConnection) { 41 | if (err) { 42 | reject('runs-model.js Unable to get db connection: ' + err); 43 | return; 44 | } 45 | 46 | dbConnection.query('SELECT run_entries.entry_id, run_entries.run_id, ' + 47 | 'run_entries.speed_score, urls.url FROM run_entries INNER JOIN urls ON ' + 48 | 'run_entries.url_id = urls.url_id WHERE run_id = ? ORDER BY speed_score ' + 49 | 'DESC LIMIT ?', 50 | [runId, numOfResults], function(err, result) { 51 | dbConnection.destroy(); 52 | if (err) { 53 | reject('runs-model.js Unable to select the top results: ' + err); 54 | return; 55 | } 56 | 57 | if (result.length === 0) { 58 | reject('runs-model.js No sites in the run to select'); 59 | return; 60 | } 61 | 62 | resolve(result); 63 | }); 64 | }); 65 | }); 66 | }; 67 | 68 | RunsModel.prototype.getWorstResultsForRun = function(runId, numOfResults) { 69 | return when.promise(function(resolve, reject, notify) { 70 | dbHelper.openDb(function(err, dbConnection) { 71 | if (err) { 72 | reject('runs-model.js Unable to get db connection: ' + err); 73 | return; 74 | } 75 | 76 | dbConnection.query('SELECT run_entries.entry_id, run_entries.run_id, ' + 77 | 'run_entries.speed_score, urls.url FROM run_entries INNER JOIN urls ON ' + 78 | 'run_entries.url_id = urls.url_id WHERE run_id = ? ORDER BY speed_score ' + 79 | 'ASC LIMIT ?', 80 | [runId, numOfResults], function(err, result) { 81 | dbConnection.destroy(); 82 | if (err) { 83 | reject('runs-model.js Unable to select the top results: ' + err); 84 | return; 85 | } 86 | 87 | if (result.length === 0) { 88 | reject('runs-model.js No sites in the run to select'); 89 | return; 90 | } 91 | 92 | resolve(result); 93 | }); 94 | }); 95 | }); 96 | }; 97 | 98 | RunsModel.prototype.getPreviousScoreAverages = function(numOfDays) { 99 | return when.promise(function(resolve, reject, notify) { 100 | dbHelper.openDb(function(err, dbConnection) { 101 | if (err) { 102 | reject('runs-model.js Unable to get db connection: ' + err); 103 | return; 104 | } 105 | 106 | dbConnection.query('SELECT run_id, end_time, mean_score, median_score ' + 107 | 'FROM runs WHERE status = ? AND end_time > DATE_SUB(now(), INTERVAL ? ' + 108 | 'DAY) ORDER BY end_time DESC', 109 | ['successful', numOfDays], function(err, result) { 110 | dbConnection.destroy(); 111 | if (err) { 112 | reject('runs-model.js Unable to select the top results: ' + err); 113 | return; 114 | } 115 | 116 | if (result.length === 0) { 117 | reject('runs-model.js No sites in the run to select'); 118 | return; 119 | } 120 | 121 | resolve(result); 122 | }); 123 | }); 124 | }); 125 | }; 126 | 127 | RunsModel.prototype.getBiggestPagesByTotalResources = 128 | function(runId, numOfResults) { 129 | return when.promise(function(resolve, reject, notify) { 130 | dbHelper.openDb(function(err, dbConnection) { 131 | if (err) { 132 | reject('runs-model.js Unable to get db connection: ' + err); 133 | return; 134 | } 135 | 136 | dbConnection.query( 137 | 'SELECT run_entries.entry_id, run_entries.run_id, ' + 138 | 'run_entries.total_request_bytes, urls.url FROM run_entries INNER JOIN urls ON ' + 139 | 'run_entries.url_id = urls.url_id WHERE run_id = ? ORDER BY total_request_bytes ' + 140 | 'DESC LIMIT ?', 141 | [runId, numOfResults], 142 | function(err, result) { 143 | dbConnection.destroy(); 144 | if (err) { 145 | reject('runs-model.js Unable to select the top results: ' + err); 146 | return; 147 | } 148 | 149 | if (result.length === 0) { 150 | reject('runs-model.js No sites in the run to select'); 151 | return; 152 | } 153 | 154 | resolve(result); 155 | }); 156 | }); 157 | }); 158 | }; 159 | 160 | module.exports = new RunsModel(); 161 | -------------------------------------------------------------------------------- /controller/RequestController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*global configFilePath */ 4 | 5 | var SETTINGS_FILENAME = 'settings.json'; 6 | var DEFAULT_DB_NAME = 'WebPerfMonitor'; 7 | var DEFAULT_PORT_NUMBER = 3306; 8 | 9 | var RunsModel = require('./../model/runs-model.js'); 10 | var fs = require('fs'); 11 | 12 | exports.getIndexRequest = function(req, res) { 13 | var config; 14 | try { 15 | config = require('../' + GLOBAL.configFile); 16 | } catch (exception) {} 17 | if (!config) { 18 | exports.getSettingsRequest(req, res, true); 19 | return; 20 | } 21 | 22 | RunsModel.getLatestCompleteRun() 23 | .then(function(result) { 24 | // jscs:disable 25 | var runId = result['run_id']; 26 | // jscs:enable 27 | getTopResultsForRun(runId, 3, function(err, topPagesResults) { 28 | if (err) { 29 | console.error(err); 30 | res.status(500).send('Something broke! ' + err); 31 | return; 32 | } 33 | 34 | getWorstResultsForRun(runId, 3, function(err, worstPagesResults) { 35 | if (err) { 36 | console.error(err); 37 | res.status(500).send('Something broke! ' + err); 38 | return; 39 | } 40 | 41 | getPreviousScoreAverages(function(err, scoreAverages) { 42 | if (err) { 43 | console.error(err); 44 | res.status(500).send('Something broke! ' + err); 45 | return; 46 | } 47 | 48 | getBiggestPagesByTotalResources(runId, 3, 49 | function(err, biggestTotalResourcePages) { 50 | if (err) { 51 | console.error(err); 52 | res.status(500).send('Something broke! ' + err); 53 | return; 54 | } 55 | 56 | res.render('home', { 57 | cssfile: 'styles/home.css', 58 | topSites: topPagesResults, 59 | worstSites: worstPagesResults, 60 | scoreAverages: scoreAverages, 61 | biggestTotalResourcePages: biggestTotalResourcePages 62 | }); 63 | }); 64 | }); 65 | }); 66 | }); 67 | }) 68 | .catch(function(err) { 69 | console.error(err); 70 | res.status(500).send('Something broke! ' + err); 71 | }); 72 | }; 73 | 74 | exports.getSettingsRequest = function(req, res, isIndexPage) { 75 | // TODO: Better way to show intro message? 76 | var message = null; 77 | if (isIndexPage) { 78 | message = 'You\'ll need to set-up your database settings '; 79 | message += 'before anything can start working.'; 80 | } 81 | 82 | var settings = {}; 83 | if (fs.existsSync(SETTINGS_FILENAME)) { 84 | settings = JSON.parse(fs.readFileSync(SETTINGS_FILENAME)); 85 | } 86 | 87 | console.log(JSON.stringify(settings)); 88 | 89 | res.render('settings', { 90 | cssfile: 'styles/settings.css', 91 | message: message, 92 | databaseHostname: settings.databaseHostname || '', 93 | databasePortNum: settings.databasePortNum || DEFAULT_PORT_NUMBER, 94 | databaseUsername: settings.databaseUsername || '', 95 | databasePassword: settings.databasePassword || '', 96 | databaseName: settings.databaseName || DEFAULT_DB_NAME, 97 | sitemapUrl: settings.sitemapUrl || '', 98 | webpagetestAPIKey: settings.webpagetestAPIKey || '' 99 | }); 100 | }; 101 | 102 | exports.getSettingsSubmitRequest = function(req, res) { 103 | var params = req.body; 104 | if (!params) { 105 | res.redirect('/'); 106 | } 107 | 108 | fs.writeFileSync(SETTINGS_FILENAME, JSON.stringify(params)); 109 | 110 | if (!params.databaseName) { 111 | params.databaseName = DEFAULT_DB_NAME; 112 | } 113 | 114 | if (typeof(params.databasePortNum) === 'undefined' || 115 | params.databasePortNum === null) { 116 | params.databasePortNum = DEFAULT_PORT_NUMBER; 117 | } 118 | 119 | var fieldErrors = validateFormResults(params); 120 | if (fieldErrors) { 121 | res.redirect('/'); 122 | } 123 | 124 | res.redirect('/'); 125 | }; 126 | 127 | function validateFormResults(params) { 128 | var fieldErrors = []; 129 | if (!isValidFormTest(params.databaseHostname)) { 130 | fieldErrors.push({ 131 | errorKey: 'databaseHostname', 132 | errorMsg: 'You need to supply a Database hostname' 133 | }); 134 | } 135 | 136 | if (!isValidFormTest(params.databaseUsername)) { 137 | fieldErrors.push({ 138 | errorKey: 'databaseUsername', 139 | errorMsg: 'You need to supply a Database username' 140 | }); 141 | } 142 | 143 | if (!isValidFormTest(params.databasePassword)) { 144 | fieldErrors.push({ 145 | errorKey: 'databasePassword', 146 | errorMsg: 'You need to supply a Database password' 147 | }); 148 | } 149 | 150 | if (!isValidFormTest(params.sitemapUrl)) { 151 | fieldErrors.push({ 152 | errorKey: 'sitemapUrl', 153 | errorMsg: 'You need to supply a sitemap URL' 154 | }); 155 | } 156 | 157 | return fieldErrors; 158 | } 159 | 160 | function isValidFormTest(field) { 161 | if (field === null || typeof(field) === 'undefined') { 162 | return false; 163 | } 164 | 165 | return field.length > 0; 166 | } 167 | 168 | function getTopResultsForRun(runId, numOfResults, cb) { 169 | if (!cb) { 170 | return; 171 | } 172 | 173 | numOfResults = numOfResults || 10; 174 | RunsModel.getTopResultsForRun(runId, numOfResults) 175 | .then(function(result) { 176 | cb(null, result); 177 | }) 178 | .catch(function(err) { 179 | cb(err); 180 | }); 181 | } 182 | 183 | function getWorstResultsForRun(runId, numOfResults, cb) { 184 | if (!cb) { 185 | return; 186 | } 187 | 188 | numOfResults = numOfResults || 10; 189 | RunsModel.getWorstResultsForRun(runId, numOfResults) 190 | .then(function(result) { 191 | cb(null, result); 192 | }) 193 | .catch(function(err) { 194 | cb(err); 195 | }); 196 | } 197 | 198 | function getPreviousScoreAverages(cb) { 199 | if (!cb) { 200 | return; 201 | } 202 | var numberOfDays = 30 * 3; 203 | RunsModel.getPreviousScoreAverages(numberOfDays) 204 | .then(function(result) { 205 | cb(null, result); 206 | }) 207 | .catch(function(err) { 208 | cb(err); 209 | }); 210 | } 211 | 212 | function getBiggestPagesByTotalResources(runId, numOfResults, cb) { 213 | if (!cb) { 214 | return; 215 | } 216 | 217 | numOfResults = numOfResults || 10; 218 | RunsModel.getBiggestPagesByTotalResources(runId, numOfResults) 219 | .then(function(result) { 220 | cb(null, result); 221 | }) 222 | .catch(function(err) { 223 | cb(err); 224 | }); 225 | } 226 | -------------------------------------------------------------------------------- /views/home.hbs: -------------------------------------------------------------------------------- 1 | 2 |

Averages

3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 |

Best for PSI Speed

14 | 15 |
16 | {{#each topSites}} 17 |
18 |
19 |
20 |

{{this.speed_score}}

21 |
22 |
23 | {{this.url}} 24 |
25 |
26 | Read More 27 |
28 |
29 |
30 | {{/each}} 31 |
32 | 33 | 34 |

PageSpeed Insights - Worst Pages

35 | 36 |
37 | {{#each worstSites}} 38 |
39 |
40 |
41 |

{{this.speed_score}}

42 |
43 |
44 | {{this.url}} 45 |
46 |
47 | Read More 48 |
49 |
50 |
51 | {{/each}} 52 |
53 | 54 | 55 |

Biggest Pages

56 | 57 |
58 | {{#each biggestTotalResourcePages}} 59 |
60 |
61 |
62 |

{{this.total_request_bytes}}

63 |
64 |
65 | {{this.url}} 66 |
67 |
68 | Read More 69 |
70 |
71 |
72 | {{/each}} 73 |
74 | 75 | 76 |
77 | 78 | 79 | 82% 80 | 81 | 82 | 83 | 82% 84 | 85 | 86 | 87 | 82% 88 | 89 | 90 | 91 | 82% 92 | 93 |
94 | 95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 | 105 |
106 |
107 |
108 |

Updates

109 |
110 |
111 | Non dolore elit adipisicing ea reprehenderit consectetur culpa. 112 |
113 |
114 | Read More 115 |
116 |
117 |
118 |
119 |

View options

120 |
    121 | 125 | 129 | 133 | 137 |
138 |
139 |
140 | Change location 141 |
142 | location_on 143 |
144 |
145 |
146 | 147 | 148 |
149 | 150 |

Run Score Averages

151 | 152 |
153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | {{#each scoreAverages}} 164 | 165 | 166 | 167 | 168 | 169 | {{/each}} 170 | 171 |
End TimeMean ScoreMedian Score
{{this.end_time}}{{this.mean_score}} {{this.median_score}}
172 |
173 |
174 | -------------------------------------------------------------------------------- /views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Web Perf Monitor 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 | 52 |
53 | 54 | 55 |
56 |
57 | Home 58 |
59 | 62 |
    63 |
  • What is
  • 64 |
  • supposed to
  • 65 |
  • be here?
  • 66 |
67 |
68 |
69 | 70 |
71 |
72 | 73 |
74 | hello@email.com 75 |
76 | 79 |
    80 | 81 | 82 | 83 |
84 |
85 |
86 | 99 |
100 | 101 |
102 | {{{body}}} 103 |
104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 500 130 | 400 131 | 300 132 | 200 133 | 100 134 | 1 135 | 2 136 | 3 137 | 4 138 | 5 139 | 6 140 | 7 141 | 142 | 143 | 145 | 146 | 147 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Web Starter Kit 4 | * Copyright 2015 Google Inc. All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License 17 | * 18 | */ 19 | 20 | // This gulpfile makes use of new JavaScript features. 21 | // Babel handles this without us having to do anything. It just works. 22 | // You can read more about the new JavaScript features here: 23 | // https://babeljs.io/docs/learn-es2015/ 24 | 25 | 'use strict'; 26 | 27 | import fs from 'fs'; 28 | import path from 'path'; 29 | import gulp from 'gulp'; 30 | import del from 'del'; 31 | import runSequence from 'run-sequence'; 32 | import browserSync from 'browser-sync'; 33 | import swPrecache from 'sw-precache'; 34 | import gulpLoadPlugins from 'gulp-load-plugins'; 35 | import {output as pagespeed} from 'psi'; 36 | import pkg from './package.json'; 37 | 38 | const $ = gulpLoadPlugins(); 39 | const reload = browserSync.reload; 40 | 41 | // Lint JavaScript 42 | gulp.task('jshint', () => { 43 | return gulp.src('app/scripts/**/*.js') 44 | .pipe(reload({stream: true, once: true})) 45 | .pipe($.jshint()) 46 | .pipe($.jshint.reporter('jshint-stylish')) 47 | .pipe($.if(!browserSync.active, $.jshint.reporter('fail'))); 48 | }); 49 | 50 | // Optimize images 51 | gulp.task('images', () => { 52 | return gulp.src('app/images/**/*') 53 | .pipe($.cache($.imagemin({ 54 | progressive: true, 55 | interlaced: true 56 | }))) 57 | .pipe(gulp.dest('dist/images')) 58 | .pipe($.size({title: 'images'})); 59 | }); 60 | 61 | // Copy all files at the root level (app) 62 | gulp.task('copy', () => { 63 | return gulp.src([ 64 | 'app/*', 65 | '!app/*.html', 66 | 'node_modules/apache-server-configs/dist/.htaccess' 67 | ], { 68 | dot: true 69 | }).pipe(gulp.dest('dist')) 70 | .pipe($.size({title: 'copy'})); 71 | }); 72 | 73 | // Copy web fonts to dist 74 | gulp.task('fonts', () => { 75 | return gulp.src(['app/fonts/**']) 76 | .pipe(gulp.dest('dist/fonts')) 77 | .pipe($.size({title: 'fonts'})); 78 | }); 79 | 80 | // Compile and automatically prefix stylesheets 81 | gulp.task('styles', () => { 82 | const AUTOPREFIXER_BROWSERS = [ 83 | 'ie >= 10', 84 | 'ie_mob >= 10', 85 | 'ff >= 30', 86 | 'chrome >= 34', 87 | 'safari >= 7', 88 | 'opera >= 23', 89 | 'ios >= 7', 90 | 'android >= 4.4', 91 | 'bb >= 10' 92 | ]; 93 | 94 | // For best performance, don't add Sass partials to `gulp.src` 95 | return gulp.src([ 96 | 'app/**/*.scss', 97 | 'app/styles/**/*.css' 98 | ]) 99 | .pipe($.changed('.tmp/styles', {extension: '.css'})) 100 | .pipe($.sourcemaps.init()) 101 | .pipe($.sass({ 102 | precision: 10 103 | }).on('error', $.sass.logError)) 104 | .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) 105 | .pipe(gulp.dest('.tmp')) 106 | // Concatenate and minify styles 107 | .pipe($.if('*.css', $.csso())) 108 | .pipe($.sourcemaps.write()) 109 | .pipe(gulp.dest('dist')) 110 | .pipe($.size({title: 'styles'})); 111 | }); 112 | 113 | // Concatenate and minify JavaScript 114 | gulp.task('scripts', () => { 115 | return gulp.src(['./app/scripts/main.js']) 116 | .pipe($.concat('main.min.js')) 117 | .pipe($.uglify({preserveComments: 'some'})) 118 | // Output files 119 | .pipe(gulp.dest('dist/scripts')) 120 | .pipe($.size({title: 'scripts'})); 121 | }); 122 | 123 | // Scan your HTML for assets & optimize them 124 | gulp.task('html', () => { 125 | const assets = $.useref.assets({searchPath: '{.tmp,app}'}); 126 | 127 | return gulp.src('app/**/**/*.html') 128 | .pipe(assets) 129 | // Remove any unused CSS 130 | // Note: If not using the Style Guide, you can delete it from 131 | // the next line to only include styles your project uses. 132 | .pipe($.if('*.css', $.uncss({ 133 | html: [ 134 | 'app/index.html' 135 | ], 136 | // CSS Selectors for UnCSS to ignore 137 | ignore: [ 138 | /.navdrawer-container.open/, 139 | /.app-bar.open/ 140 | ] 141 | }))) 142 | 143 | // Concatenate and minify styles 144 | // In case you are still using useref build blocks 145 | .pipe($.if('*.css', $.csso())) 146 | .pipe(assets.restore()) 147 | .pipe($.useref()) 148 | 149 | // Minify any HTML 150 | .pipe($.if('*.html', $.minifyHtml())) 151 | // Output files 152 | .pipe(gulp.dest('dist')) 153 | .pipe($.size({title: 'html'})); 154 | }); 155 | 156 | // Clean output directory 157 | gulp.task('clean', () => del(['.tmp', 'dist/*', '!dist/.git'], {dot: true})); 158 | 159 | // Watch files for changes & reload 160 | gulp.task('serve', ['styles'], () => { 161 | browserSync({ 162 | notify: false, 163 | // Customize the BrowserSync console logging prefix 164 | logPrefix: 'WSK', 165 | // Run as an https by uncommenting 'https: true' 166 | // Note: this uses an unsigned certificate which on first access 167 | // will present a certificate warning in the browser. 168 | // https: true, 169 | server: ['.tmp', 'app'] 170 | }); 171 | 172 | gulp.watch(['app/**/*.html'], reload); 173 | gulp.watch(['app/styles/**/*.{scss,css}'], ['styles', reload]); 174 | gulp.watch(['app/scripts/**/*.js'], ['jshint']); 175 | gulp.watch(['app/images/**/*'], reload); 176 | }); 177 | 178 | // Build and serve the output from the dist build 179 | gulp.task('serve:dist', ['default'], () => { 180 | browserSync({ 181 | notify: false, 182 | logPrefix: 'WSK', 183 | // Run as an https by uncommenting 'https: true' 184 | // Note: this uses an unsigned certificate which on first access 185 | // will present a certificate warning in the browser. 186 | // https: true, 187 | server: 'dist', 188 | baseDir: 'dist' 189 | }); 190 | }); 191 | 192 | // Build production files, the default task 193 | gulp.task('default', ['clean'], cb => { 194 | runSequence( 195 | 'styles', 196 | ['jshint', 'html', 'scripts', 'images', 'fonts', 'copy'], 197 | 'generate-service-worker', 198 | cb 199 | ); 200 | }); 201 | 202 | // Run PageSpeed Insights 203 | gulp.task('pagespeed', cb => { 204 | // Update the below URL to the public URL of your site 205 | pagespeed('example.com', { 206 | strategy: 'mobile', 207 | // By default we use the PageSpeed Insights free (no API key) tier. 208 | // Use a Google Developer API key if you have one: http://goo.gl/RkN0vE 209 | // key: 'YOUR_API_KEY' 210 | }, cb); 211 | }); 212 | 213 | // See http://www.html5rocks.com/en/tutorials/service-worker/introduction/ for 214 | // an in-depth explanation of what service workers are and why you should care. 215 | // Generate a service worker file that will provide offline functionality for 216 | // local resources. This should only be done for the 'dist' directory, to allow 217 | // live reload to work as expected when serving from the 'app' directory. 218 | gulp.task('generate-service-worker', cb => { 219 | const rootDir = 'dist'; 220 | 221 | swPrecache({ 222 | // Used to avoid cache conflicts when serving on localhost. 223 | cacheId: pkg.name || 'web-starter-kit', 224 | // URLs that don't directly map to single static files can be defined here. 225 | // If any of the files a URL depends on changes, then the URL's cache entry 226 | // is invalidated and it will be refetched. 227 | // Generally, URLs that depend on multiple files (such as layout templates) 228 | // should list all the files; a change in any will invalidate the cache. 229 | // In this case, './' is the top-level relative URL, and its response 230 | // depends on the contents of the file 'dist/index.html'. 231 | dynamicUrlToDependencies: { 232 | './': [path.join(rootDir, 'index.html')] 233 | }, 234 | staticFileGlobs: [ 235 | // Add/remove glob patterns to match your directory setup. 236 | `${rootDir}/fonts/**/*.woff`, 237 | `${rootDir}/images/**/*`, 238 | `${rootDir}/scripts/**/*.js`, 239 | `${rootDir}/styles/**/*.css`, 240 | `${rootDir}/*.{html,json}` 241 | ], 242 | // Translates a static file path to the relative URL that it's served from. 243 | stripPrefix: path.join(rootDir, path.sep) 244 | }, (err, swFileContents) => { 245 | if (err) { 246 | cb(err); 247 | return; 248 | } 249 | 250 | const filepath = path.join(rootDir, 'service-worker.js'); 251 | 252 | fs.writeFile(filepath, swFileContents, err => { 253 | if (err) { 254 | cb(err); 255 | return; 256 | } 257 | 258 | cb(); 259 | }); 260 | }); 261 | }); 262 | 263 | // Load custom tasks from the `tasks` directory 264 | // try { require('require-dir')('tasks'); } catch (err) { console.error(err); } 265 | --------------------------------------------------------------------------------