diff --git a/.jshintrc b/.jshintrc
index c16505dd07..d1064e9ba9 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -8,5 +8,5 @@
"asi": true,
"boss": true,
- "newcap": false,
+ "newcap": false
}
diff --git a/Gruntfile.js b/Gruntfile.js
index 3376be4c61..85bb31be74 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -110,7 +110,7 @@ module.exports = function(grunt) {
//Load build files from public/manifest.json
grunt.registerTask('loadManifestFiles', 'Load all build files from public/manifest.json', function(){
- var files = grunt.file.readJSON('./public/manifest.json');
+ var files = grunt.file.readJSON('./website/public/manifest.json');
var uglify = {};
var cssmin = {};
@@ -130,6 +130,8 @@ module.exports = function(grunt) {
});
});
+ console.log(uglify);
+ console.log(cssmin);
grunt.config.set('uglify.build.files', uglify);
grunt.config.set('uglify.build.options', {compress: false});
@@ -142,6 +144,7 @@ module.exports = function(grunt) {
// Register tasks.
grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']);
grunt.registerTask('build:dev', ['stylus']);
+ grunt.registerTask('test', ['loadManifestFiles', 'uglify', 'cssmin']);
grunt.registerTask('run:dev', [ 'build:dev', 'concurrent' ]);
diff --git a/bower.json b/bower.json
index da34bcb073..4581445b88 100644
--- a/bower.json
+++ b/bower.json
@@ -26,7 +26,7 @@
"angular-loading-bar": "~0.6.0",
"bootstrap": "~3.1.0",
"bootstrap-growl": "git://github.com/ifightcrime/bootstrap-growl.git#master",
- "bootstrap-tour": "~0.8.1",
+ "bootstrap-tour": "~0.10.1",
"BrowserQuest": "git://github.com/browserquest/BrowserQuest.git",
"github-buttons": "git://github.com/mdo/github-buttons.git",
"marked": "~0.2.9",
diff --git a/gulpfile.js b/gulpfile.js
index 02736c7720..a9dff50ab3 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -21,6 +21,7 @@ var gulp = require('gulp'),
pkg = require('./package');
var paths = {
+ build: "./website/build",
stylus: {
src: {
app: './website/public/css/index.styl',
@@ -165,6 +166,7 @@ gulp.task('sprite', function(cb) {
STEP++;
console.log("Finished spritesmith" + key + ".png");
if(STEP >= COUNT) {
+ console.log(paths.sprites.cssminSrc);
gulp.src(paths.sprites.cssminSrc)
.pipe(concat('habitrpg-shared.css'))
.pipe(cssmin())
@@ -185,6 +187,45 @@ gulp.task('browserify', function() {
.pipe(gulp.dest(paths.common.dest))
})
+gulp.task('build', function() {
+ var files = require('./website/public/manifest');
+ var uglifySrc = {};
+ var cssminSrc = {};
+
+ _.each(files, function(val, key){
+
+ var js = uglifySrc[key + '.js'] = [];
+
+ _.each(files[key]['js'], function(val){
+ js.push('./website/public/' + val);
+ });
+
+ var css = cssminSrc[key + '.css'] = [];
+
+ _.each(files[key]['css'], function(val){
+ var path = (val == 'app.css' || val == 'static.css') ? paths.build : './website/public/';
+ css.push(path + val)
+ });
+
+ });
+
+ // Concat CSS
+ _.each(cssminSrc, function(val, key) {
+ gulp.src(val)
+ .pipe(concat(key))
+ .pipe(cssmin())
+ .pipe(gulp.dest(paths.build))
+ });
+
+ // Uglify JS
+ _.each(uglifySrc, function(val, key) {
+ gulp.src(val)
+ .pipe(concat(key))
+ .pipe(uglify())
+ .pipe(gulp.dest(paths.build))
+ });
+});
+
gulp.task('watch', ['stylus', 'browserify'], function() {
gulp.watch(paths.stylus.watch, ['stylus']);
gulp.watch(paths.common.watch, ['browserify']);
diff --git a/src/controllers/auth.js b/src/controllers/auth.js
new file mode 100644
index 0000000000..68c2b7c066
--- /dev/null
+++ b/src/controllers/auth.js
@@ -0,0 +1,307 @@
+var _ = require('lodash');
+var validator = require('validator');
+var passport = require('passport');
+var shared = require('habitrpg-shared');
+var async = require('async');
+var utils = require('../utils');
+var nconf = require('nconf');
+var request = require('request');
+var User = require('../models/user').model;
+var ga = require('./../utils').ga;
+var i18n = require('./../i18n');
+
+var isProd = nconf.get('NODE_ENV') === 'production';
+
+var api = module.exports;
+
+var NO_TOKEN_OR_UID = { err: "You must include a token and uid (user id) in your request"};
+var NO_USER_FOUND = {err: "No user found."};
+var NO_SESSION_FOUND = { err: "You must be logged in." };
+var accountSuspended = function(uuid){
+ return {
+ err: 'Account has been suspended, please contact leslie@habitrpg.com with your UUID ('+uuid+') for assistance.',
+ code: 'ACCOUNT_SUSPENDED'
+ };
+}
+// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790
+var RegexEscape = function(s){
+ return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
+}
+
+api.auth = function(req, res, next) {
+ var uid = req.headers['x-api-user'];
+ var token = req.headers['x-api-key'];
+ if (!(uid && token)) return res.json(401, NO_TOKEN_OR_UID);
+ User.findOne({_id: uid,apiToken: token}, function(err, user) {
+ if (err) return next(err);
+ if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
+ if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
+
+ res.locals.wasModified = req.query._v ? +user._v !== +req.query._v : true;
+ res.locals.user = user;
+ req.session.userId = user._id;
+ return next();
+ });
+};
+
+api.authWithSession = function(req, res, next) { //[todo] there is probably a more elegant way of doing this...
+ if (!(req.session && req.session.userId))
+ return res.json(401, NO_SESSION_FOUND);
+ User.findOne({_id: req.session.userId}, function(err, user) {
+ if (err) return next(err);
+ if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
+ res.locals.user = user;
+ next();
+ });
+};
+
+api.authWithUrl = function(req, res, next) {
+ User.findOne({_id:req.query._id, apiToken:req.query.apiToken}, function(err,user){
+ if (err) return next(err);
+ if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
+ res.locals.user = user;
+ next();
+ })
+}
+
+api.registerUser = function(req, res, next) {
+ async.auto({
+ validate: function(cb) {
+ if (!(req.body.username && req.body.password && req.body.email))
+ return cb({code:401, err: ":username, :email, :password, :confirmPassword required"});
+ if (req.body.password !== req.body.confirmPassword)
+ return cb({code:401, err: ":password and :confirmPassword don't match"});
+ if (!validator.isEmail(req.body.email))
+ return cb({code:401, err: ":email invalid"});
+ cb();
+ },
+ findEmail: function(cb) {
+ User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {_id:1}, cb);
+ },
+ findUname: function(cb) {
+ User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {_id:1}, cb);
+ },
+ findFacebook: function(cb){
+ User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
+ },
+ register: ['validate', 'findEmail', 'findUname', 'findFacebook', function(cb, data) {
+ if (data.findEmail) return cb({code:401, err:"Email already taken"});
+ if (data.findUname) return cb({code:401, err:"Username already taken"});
+ var salt = utils.makeSalt();
+ var newUser = {
+ auth: {
+ local: {
+ username: req.body.username,
+ email: req.body.email,
+ salt: salt,
+ hashed_password: utils.encryptPassword(req.body.password, salt)
+ },
+ timestamps: {created: +new Date(), loggedIn: +new Date()}
+ }
+ };
+ // existing user, allow them to add local authentication
+ if (data.findFacebook) {
+ data.findFacebook.auth.local = newUser.auth.local;
+ data.findFacebook.save(cb);
+ // new user, register them
+ } else {
+ newUser.preferences = newUser.preferences || {};
+ newUser.preferences.language = req.language; // User language detected from browser, not saved
+ var user = new User(newUser);
+ utils.txnEmail(user, 'welcome');
+ ga.event('register', 'Local').send();
+ user.save(cb);
+ }
+ }]
+ }, function(err, data) {
+ if (err) return err.code ? res.json(err.code, err) : next(err);
+ res.json(200, data.register[0]);
+ });
+};
+
+/*
+ Register new user with uname / password
+ */
+
+
+api.loginLocal = function(req, res, next) {
+ var username = req.body.username;
+ var password = req.body.password;
+ if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'});
+ var login = validator.isEmail(username) ? {'auth.local.email':username} : {'auth.local.username':username};
+ User.findOne(login, {auth:1}, function(err, user){
+ if (err) return next(err);
+ if (!user) return res.json(401, {err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"});
+ if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
+ // We needed the whole user object first so we can get his salt to encrypt password comparison
+ User.findOne(
+ {$and: [login, {'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt)}]}
+ , {_id:1, apiToken:1}
+ , function(err, user){
+ if (err) return next(err);
+ if (!user) return res.json(401,{err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"});
+ res.json({id: user._id,token: user.apiToken});
+ password = null;
+ });
+ });
+};
+
+/*
+ POST /user/auth/social
+ */
+api.loginSocial = function(req, res, next) {
+ var access_token = req.body.authResponse.access_token,
+ network = req.body.network;
+ if (network!=='facebook')
+ return res.json(401, {err:"Only Facebook supported currently."});
+ async.auto({
+ profile: function (cb) {
+ passport._strategies[network].userProfile(access_token, cb);
+ },
+ user: ['profile', function (cb, results) {
+ var q = {};
+ q['auth.' + network + '.id'] = results.profile.id;
+ User.findOne(q, {_id: 1, apiToken: 1, auth: 1}, cb);
+ }],
+ register: ['profile', 'user', function (cb, results) {
+ if (results.user) return cb(null, results.user);
+ // Create new user
+ var prof = results.profile;
+ var user = {
+ preferences: {
+ language: req.language // User language detected from browser, not saved
+ },
+ auth: {
+ timestamps: {created: +new Date(), loggedIn: +new Date()}
+ }
+ };
+ user.auth[network] = prof;
+ user = new User(user);
+ user.save(cb);
+
+ utils.txnEmail(user, 'welcome');
+ ga.event('register', network).send();
+ }]
+ }, function(err, results){
+ if (err) return res.json(401, {err: err.toString ? err.toString() : err});
+ var acct = results.register[0] ? results.register[0] : results.register;
+ if (acct.auth.blocked) return res.json(401, accountSuspended(acct._id));
+ return res.json(200, {id:acct._id, token:acct.apiToken});
+ })
+};
+
+/**
+ * DELETE /user/auth/social
+ */
+api.deleteSocial = function(req,res,next){
+ if (!res.locals.user.auth.local.username)
+ return res.json(401, {err:"Account lacks another authentication method, can't detach Facebook"});
+ //FIXME for some reason, the following gives https://gist.github.com/lefnire/f93eb306069b9089d123
+ //res.locals.user.auth.facebook = null;
+ //res.locals.user.auth.save(function(err, saved){
+ User.update({_id:res.locals.user._id}, {$unset:{'auth.facebook':1}}, function(err){
+ if (err) return next(err);
+ res.send(200);
+ })
+}
+
+api.resetPassword = function(req, res, next){
+ var email = req.body.email,
+ salt = utils.makeSalt(),
+ newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
+ hashed_password = utils.encryptPassword(newPassword, salt);
+
+ User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){
+ if (err) return next(err);
+ if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
+ user.auth.local.salt = salt;
+ user.auth.local.hashed_password = hashed_password;
+ utils.txnEmail(user, 'reset-password', [
+ {name: "NEW_PASSWORD", content: newPassword},
+ {name: "USERNAME", content: user.auth.local.username}
+ ]);
+ user.save(function(err){
+ if(err) return next(err);
+ res.send('New password sent to '+ email);
+ email = salt = newPassword = hashed_password = null;
+ });
+ });
+};
+
+var invalidPassword = function(user, password){
+ var hashed_password = utils.encryptPassword(password, user.auth.local.salt);
+ if (hashed_password !== user.auth.local.hashed_password)
+ return {code:401, err:"Incorrect password"};
+ return false;
+}
+
+api.changeUsername = function(req, res, next) {
+ async.waterfall([
+ function(cb){
+ User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb);
+ },
+ function(found, cb){
+ if (found) return cb({code:401, err: "Username already taken"});
+ if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
+ res.locals.user.auth.local.username = req.body.username;
+ res.locals.user.save(cb);
+ }
+ ], function(err){
+ if (err) return err.code ? res.json(err.code, err) : next(err);
+ res.send(200);
+ })
+}
+
+api.changeEmail = function(req, res, next){
+ async.waterfall([
+ function(cb){
+ User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb);
+ },
+ function(found, cb){
+ if(found) return cb({code:401, err: "Email already taken"});
+ if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
+ res.locals.user.auth.local.email = req.body.email;
+ res.locals.user.save(cb);
+ }
+ ], function(err){
+ if (err) return err.code ? res.json(err.code,err) : next(err);
+ res.send(200);
+ })
+}
+
+api.changePassword = function(req, res, next) {
+ var user = res.locals.user,
+ oldPassword = req.body.oldPassword,
+ newPassword = req.body.newPassword,
+ confirmNewPassword = req.body.confirmNewPassword;
+
+ if (newPassword != confirmNewPassword)
+ return res.json(401, {err: "Password & Confirm don't match"});
+
+ var salt = user.auth.local.salt,
+ hashed_old_password = utils.encryptPassword(oldPassword, salt),
+ hashed_new_password = utils.encryptPassword(newPassword, salt);
+
+ if (hashed_old_password !== user.auth.local.hashed_password)
+ return res.json(401, {err:"Old password doesn't match"});
+
+ user.auth.local.hashed_password = hashed_new_password;
+ user.save(function(err, saved){
+ if (err) next(err);
+ res.send(200);
+ })
+}
+
+/*
+ Registers a new user. Only accepting username/password registrations, no Facebook
+ */
+
+api.setupPassport = function(router) {
+
+ router.get('/logout', i18n.getUserLanguage, function(req, res) {
+ req.logout();
+ delete req.session.userId;
+ res.redirect('/');
+ })
+
+};
diff --git a/website/public/js/controllers/groupsCtrl.js b/website/public/js/controllers/groupsCtrl.js
index 376c25d810..c911b1e2f2 100644
--- a/website/public/js/controllers/groupsCtrl.js
+++ b/website/public/js/controllers/groupsCtrl.js
@@ -111,7 +111,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
.controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', 'Groups',
function($scope, $rootScope, Members, Shared, $http, Notification, Groups) {
$scope.timestamp = function(timestamp){
- return moment(timestamp).format('MM/DD/YYYY');
+ return moment(timestamp).format($rootScope.User.user.preferences.dateFormat.toUpperCase());
}
// We watch Members.selectedMember because it's asynchronously set, so would be a hassle to handle updates here
$scope.$watch( function() { return Members.selectedMember; }, function (member) {
diff --git a/website/public/js/controllers/rootCtrl.js b/website/public/js/controllers/rootCtrl.js
index efe2862967..65e1188e77 100644
--- a/website/public/js/controllers/rootCtrl.js
+++ b/website/public/js/controllers/rootCtrl.js
@@ -166,7 +166,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
}
matrix = [[env.t('date'), env.t('score')]];
_.each(history, function(obj) {
- matrix.push([moment(obj.date).format('MM/DD/YY'), obj.value]);
+ matrix.push([moment(obj.date).format(User.user.preferences.dateFormat.toUpperCase().replace('YYYY','YY') ), obj.value]);
});
data = google.visualization.arrayToDataTable(matrix);
options = {
diff --git a/website/public/js/services/guideServices.js b/website/public/js/services/guideServices.js
index 8e4eb1bc60..74d710dbe5 100644
--- a/website/public/js/services/guideServices.js
+++ b/website/public/js/services/guideServices.js
@@ -5,8 +5,8 @@
*/
angular.module('habitrpg').factory('Guide',
-['$rootScope', 'User', '$timeout',
-function($rootScope, User, $timeout) {
+['$rootScope', 'User', '$timeout', '$state',
+function($rootScope, User, $timeout, $state) {
/**
* Init and show the welcome tour. Note we do it listening to a $rootScope broadcasted 'userLoaded' message,
* this because we need to determine whether to show the tour *after* the user has been pulled from the server,
@@ -17,7 +17,7 @@ function($rootScope, User, $timeout) {
if (User.user.flags.showTour === false) return;
var tourSteps = [
{
- element: ".main-herobox",
+ orphan:true,
title: window.env.t('welcomeHabit'),
content: window.env.t('welcomeHabitT1') + " Justin, " + window.env.t('welcomeHabitT2'),
}, {
@@ -29,11 +29,11 @@ function($rootScope, User, $timeout) {
title: window.env.t('avatarCustom'),
content: window.env.t('avatarCustomText'),
}, {
- element: "#bars",
+ element: ".hero-stats",
title: window.env.t('hitPoints'),
content: window.env.t('hitPointsText'),
}, {
- element: "#bars",
+ element: ".hero-stats",
title: window.env.t('expPoints'),
content: window.env.t('expPointsText'),
}, {
@@ -55,7 +55,7 @@ function($rootScope, User, $timeout) {
element: "ul.todos",
title: window.env.t('todos'),
content: window.env.t('tourTodos'),
- placement: "top"
+ placement: "top",
}, {
element: "ul.main-list.rewards",
title: window.env.t('rewards'),
@@ -67,34 +67,32 @@ function($rootScope, User, $timeout) {
content: window.env.t('hoverOverText'),
placement: "right"
}, {
- element: "ul.habits li:first-child",
+ orphan:true,
title: window.env.t('unlockFeatures'),
content: window.env.t('unlockFeaturesT1') + " " + window.env.t('habitWiki') + " " + window.env.t('unlockFeaturesT2'),
placement: "right"
}
];
- _.each(tourSteps, function(step){
- if (env.worldDmg.guide) {
- step.content = "
";
- } else {
- step.content = "";
- }
- });
$('.main-herobox').popover('destroy');
var tour = new Tour({
- template: "" +
- "
" +
- "
" +
- "" +
- "" +
- "
" +
- "
",
+ backdrop: true,
+ //orphan: true,
+ //keyboard: false,
+ template: '
',
onEnd: function(){
User.set({'flags.showTour': false});
}
});
- tourSteps.forEach(function(step) {
- tour.addStep(_.defaults(step, {html: true}));
+ _.each(tourSteps, function(step) {
+ step.content = "";
+ step.onShow = function(){
+ // Since all the steps are currently on the tasks page, ensure we go back there for each step in case they
+ // clicked elsewhere during the tour. FIXME: $state.go() returns a promise, necessary for async tour steps;
+ // however, that's not working here - have to use timeout instead :/
+ if (!$state.is('tasks')) return $timeout(function(){$state.go('tasks');}, 0)
+ }
+ step.html = true;
+ tour.addStep(step);
});
tour.restart(); // Tour doesn't quite mesh with our handling of flags.showTour, just restart it on page load
//tour.start(true);
diff --git a/website/public/manifest.json b/website/public/manifest.json
index 9592271a9e..16a2b35758 100644
--- a/website/public/manifest.json
+++ b/website/public/manifest.json
@@ -73,6 +73,7 @@
"bower_components/pnotify/jquery.pnotify.default.css",
"bower_components/pnotify/jquery.pnotify.default.icons.css",
"common/sprites/habitrpg-shared.css",
+ "bower_components/bootstrap-tour/build/css/bootstrap-tour.css",
"fontello/css/fontelico.css"
]
},
diff --git a/website/src/controllers/auth.js b/website/src/controllers/auth.js
index a798666d62..e400c07a06 100644
--- a/website/src/controllers/auth.js
+++ b/website/src/controllers/auth.js
@@ -23,9 +23,9 @@ var accountSuspended = function(uuid){
code: 'ACCOUNT_SUSPENDED'
};
}
-// escape email for regex, then search case-insensitive. See http://stackoverflow.com/a/3561711/362790
-var mongoEmailRegex = function(email){
- return new RegExp('^' + email.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
+// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790
+var RegexEscape = function(s){
+ return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
}
api.auth = function(req, res, next) {
@@ -76,10 +76,10 @@ api.registerUser = function(req, res, next) {
cb();
},
findEmail: function(cb) {
- User.findOne({'auth.local.email': req.body.email}, cb);
+ User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {_id:1}, cb);
},
findUname: function(cb) {
- User.findOne({'auth.local.username': req.body.username}, cb);
+ User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {_id:1}, cb);
},
findFacebook: function(cb){
User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
@@ -211,7 +211,7 @@ api.resetPassword = function(req, res, next){
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
hashed_password = utils.encryptPassword(newPassword, salt);
- User.findOne({'auth.local.email':mongoEmailRegex(email)}, function(err, user){
+ User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){
if (err) return next(err);
if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
user.auth.local.salt = salt;
@@ -238,7 +238,7 @@ var invalidPassword = function(user, password){
api.changeUsername = function(req, res, next) {
async.waterfall([
function(cb){
- User.findOne({'auth.local.username': req.body.username}, {auth:1}, cb);
+ User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb);
},
function(found, cb){
if (found) return cb({code:401, err: "Username already taken"});
@@ -255,7 +255,7 @@ api.changeUsername = function(req, res, next) {
api.changeEmail = function(req, res, next){
async.waterfall([
function(cb){
- User.findOne({'auth.local.email': mongoEmailRegex(req.body.email)}, {auth:1}, cb);
+ User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb);
},
function(found, cb){
if(found) return cb({code:401, err: "Email already taken"});
diff --git a/website/views/index.jade b/website/views/index.jade
index 5698a42884..8c93d28e04 100644
--- a/website/views/index.jade
+++ b/website/views/index.jade
@@ -10,6 +10,11 @@ html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":a
meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name='apple-mobile-web-app-capable', content='yes')
+ if(env.NODE_ENV == 'production')
+ script(type='text/javascript').
+ window.NREUM||(NREUM={}),__nr_require=function(t,e,n){function r(n){if(!e[n]){var o=e[n]={exports:{}};t[n][0].call(o.exports,function(e){var o=t[n][1][e];return r(o?o:e)},o,o.exports)}return e[n].exports}if("function"==typeof __nr_require)return __nr_require;for(var o=0;od;d++)c[d].apply(u,n);return u}function a(t,e){f[t]=s(t).concat(e)}function s(t){return f[t]||[]}function c(){return n(e)}var f={};return{on:a,emit:e,create:c,listeners:s,_events:f}}function r(){return{}}var o="nr@context",i=t("gos");e.exports=n()},{gos:"7eSDFh"}],ee:[function(t,e){e.exports=t("QJf3ax")},{}],3:[function(t){function e(t,e,n,i,s){try{c?c-=1:r("err",[s||new UncaughtException(t,e,n)])}catch(f){try{r("ierr",[f,(new Date).getTime(),!0])}catch(u){}}return"function"==typeof a?a.apply(this,o(arguments)):!1}function UncaughtException(t,e,n){this.message=t||"Uncaught error with no additional information",this.sourceURL=e,this.line=n}function n(t){r("err",[t,(new Date).getTime()])}var r=t("handle"),o=t(5),i=t("ee"),a=window.onerror,s=!1,c=0;t("loader").features.err=!0,window.onerror=e,NREUM.noticeError=n;try{throw new Error}catch(f){"stack"in f&&(t(1),t(4),"addEventListener"in window&&t(2),window.XMLHttpRequest&&XMLHttpRequest.prototype&&XMLHttpRequest.prototype.addEventListener&&t(3),s=!0)}i.on("fn-start",function(){s&&(c+=1)}),i.on("fn-err",function(t,e,r){s&&(this.thrown=!0,n(r))}),i.on("fn-end",function(){s&&!this.thrown&&c>0&&(c-=1)}),i.on("internal-error",function(t){r("ierr",[t,(new Date).getTime(),!0])})},{1:8,2:5,3:9,4:7,5:21,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],4:[function(t){function e(){}if(window.performance&&window.performance.timing&&window.performance.getEntriesByType){var n=t("ee"),r=t("handle"),o=t(2);t("loader").features.stn=!0,t(1),n.on("fn-start",function(t){var e=t[0];e instanceof Event&&(this.bstStart=Date.now())}),n.on("fn-end",function(t,e){var n=t[0];n instanceof Event&&r("bst",[n,e,this.bstStart,Date.now()])}),o.on("fn-start",function(t,e,n){this.bstStart=Date.now(),this.bstType=n}),o.on("fn-end",function(t,e){r("bstTimer",[e,this.bstStart,Date.now(),this.bstType])}),n.on("pushState-start",function(){this.time=Date.now(),this.startPath=location.pathname+location.hash}),n.on("pushState-end",function(){r("bstHist",[location.pathname+location.hash,this.startPath,this.time])}),"addEventListener"in window.performance&&(window.performance.addEventListener("webkitresourcetimingbufferfull",function(){r("bstResource",[window.performance.getEntriesByType("resource")]),window.performance.webkitClearResourceTimings()},!1),window.performance.addEventListener("resourcetimingbufferfull",function(){r("bstResource",[window.performance.getEntriesByType("resource")]),window.performance.clearResourceTimings()},!1)),document.addEventListener("scroll",e,!1),document.addEventListener("keypress",e,!1),document.addEventListener("click",e,!1)}},{1:6,2:8,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],5:[function(t,e){function n(t){i.inPlace(t,["addEventListener","removeEventListener"],"-",r)}function r(t){return t[1]}var o=(t(1),t("ee").create()),i=t(2)(o),a=t("gos");if(e.exports=o,n(window),"getPrototypeOf"in Object){for(var s=document;s&&!s.hasOwnProperty("addEventListener");)s=Object.getPrototypeOf(s);s&&n(s);for(var c=XMLHttpRequest.prototype;c&&!c.hasOwnProperty("addEventListener");)c=Object.getPrototypeOf(c);c&&n(c)}else XMLHttpRequest.prototype.hasOwnProperty("addEventListener")&&n(XMLHttpRequest.prototype);o.on("addEventListener-start",function(t){if(t[1]){var e=t[1];"function"==typeof e?this.wrapped=t[1]=a(e,"nr@wrapped",function(){return i(e,"fn-",null,e.name||"anonymous")}):"function"==typeof e.handleEvent&&i.inPlace(e,["handleEvent"],"fn-")}}),o.on("removeEventListener-start",function(t){var e=this.wrapped;e&&(t[1]=e)})},{1:21,2:22,ee:"QJf3ax",gos:"7eSDFh"}],6:[function(t,e){var n=(t(2),t("ee").create()),r=t(1)(n);e.exports=n,r.inPlace(window.history,["pushState"],"-")},{1:22,2:21,ee:"QJf3ax"}],7:[function(t,e){var n=(t(2),t("ee").create()),r=t(1)(n);e.exports=n,r.inPlace(window,["requestAnimationFrame","mozRequestAnimationFrame","webkitRequestAnimationFrame","msRequestAnimationFrame"],"raf-"),n.on("raf-start",function(t){t[0]=r(t[0],"fn-")})},{1:22,2:21,ee:"QJf3ax"}],8:[function(t,e){function n(t,e,n){var r=t[0];"string"==typeof r&&(r=new Function(r)),t[0]=o(r,"fn-",null,n)}var r=(t(2),t("ee").create()),o=t(1)(r);e.exports=r,o.inPlace(window,["setTimeout","setInterval","setImmediate"],"setTimer-"),r.on("setTimer-start",n)},{1:22,2:21,ee:"QJf3ax"}],9:[function(t,e){function n(){c.inPlace(this,d,"fn-")}function r(t,e){c.inPlace(e,["onreadystatechange"],"fn-")}function o(t,e){return e}var i=t("ee").create(),a=t(1),s=t(2),c=s(i),f=s(a),u=window.XMLHttpRequest,d=["onload","onerror","onabort","onloadstart","onloadend","onprogress","ontimeout"];e.exports=i,window.XMLHttpRequest=function(t){var e=new u(t);try{i.emit("new-xhr",[],e),f.inPlace(e,["addEventListener","removeEventListener"],"-",function(t,e){return e}),e.addEventListener("readystatechange",n,!1)}catch(r){try{i.emit("internal-error",[r])}catch(o){}}return e},window.XMLHttpRequest.prototype=u.prototype,c.inPlace(XMLHttpRequest.prototype,["open","send"],"-xhr-",o),i.on("send-xhr-start",r),i.on("open-xhr-start",r)},{1:5,2:22,ee:"QJf3ax"}],10:[function(t){function e(t){if("string"==typeof t&&t.length)return t.length;if("object"!=typeof t)return void 0;if("undefined"!=typeof ArrayBuffer&&t instanceof ArrayBuffer&&t.byteLength)return t.byteLength;if("undefined"!=typeof Blob&&t instanceof Blob&&t.size)return t.size;if("undefined"!=typeof FormData&&t instanceof FormData)return void 0;try{return JSON.stringify(t).length}catch(e){return void 0}}function n(t){var n=this.params,r=this.metrics;if(!this.ended){this.ended=!0;for(var i=0;c>i;i++)t.removeEventListener(s[i],this.listener,!1);if(!n.aborted){if(r.duration=(new Date).getTime()-this.startTime,4===t.readyState){n.status=t.status;var a=t.responseType,f="arraybuffer"===a||"blob"===a||"json"===a?t.response:t.responseText,u=e(f);if(u&&(r.rxSize=u),this.sameOrigin){var d=t.getResponseHeader("X-NewRelic-App-Data");d&&(n.cat=d.split(", ").pop())}}else n.status=0;r.cbTime=this.cbTime,o("xhr",[n,r,this.startTime])}}}function r(t,e){var n=i(e),r=t.params;r.host=n.hostname+":"+n.port,r.pathname=n.pathname,t.sameOrigin=n.sameOrigin}if(window.XMLHttpRequest&&XMLHttpRequest.prototype&&XMLHttpRequest.prototype.addEventListener&&!/CriOS/.test(navigator.userAgent)){t("loader").features.xhr=!0;var o=t("handle"),i=t(2),a=t("ee"),s=["load","error","abort","timeout"],c=s.length,f=t(1);t(4),t(3),a.on("new-xhr",function(){this.totalCbs=0,this.called=0,this.cbTime=0,this.end=n,this.ended=!1,this.xhrGuids={}}),a.on("open-xhr-start",function(t){this.params={method:t[0]},r(this,t[1]),this.metrics={}}),a.on("open-xhr-end",function(t,e){"loader_config"in NREUM&&"xpid"in NREUM.loader_config&&this.sameOrigin&&e.setRequestHeader("X-NewRelic-ID",NREUM.loader_config.xpid)}),a.on("send-xhr-start",function(t,n){var r=this.metrics,o=t[0],i=this;if(r&&o){var f=e(o);f&&(r.txSize=f)}this.startTime=(new Date).getTime(),this.listener=function(t){try{"abort"===t.type&&(i.params.aborted=!0),("load"!==t.type||i.called===i.totalCbs&&(i.onloadCalled||"function"!=typeof n.onload))&&i.end(n)}catch(e){try{a.emit("internal-error",[e])}catch(r){}}};for(var u=0;c>u;u++)n.addEventListener(s[u],this.listener,!1)}),a.on("xhr-cb-time",function(t,e,n){this.cbTime+=t,e?this.onloadCalled=!0:this.called+=1,this.called!==this.totalCbs||!this.onloadCalled&&"function"==typeof n.onload||this.end(n)}),a.on("xhr-load-added",function(t,e){var n=""+f(t)+!!e;this.xhrGuids&&!this.xhrGuids[n]&&(this.xhrGuids[n]=!0,this.totalCbs+=1)}),a.on("xhr-load-removed",function(t,e){var n=""+f(t)+!!e;this.xhrGuids&&this.xhrGuids[n]&&(delete this.xhrGuids[n],this.totalCbs-=1)}),a.on("addEventListener-end",function(t,e){e instanceof XMLHttpRequest&&"load"===t[0]&&a.emit("xhr-load-added",[t[1],t[2]],e)}),a.on("removeEventListener-end",function(t,e){e instanceof XMLHttpRequest&&"load"===t[0]&&a.emit("xhr-load-removed",[t[1],t[2]],e)}),a.on("fn-start",function(t,e,n){e instanceof XMLHttpRequest&&("onload"===n&&(this.onload=!0),("load"===(t[0]&&t[0].type)||this.onload)&&(this.xhrCbStart=(new Date).getTime()))}),a.on("fn-end",function(t,e){this.xhrCbStart&&a.emit("xhr-cb-time",[(new Date).getTime()-this.xhrCbStart,this.onload,e],e)})}},{1:"XL7HBI",2:11,3:9,4:5,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],11:[function(t,e){e.exports=function(t){var e=document.createElement("a"),n=window.location,r={};e.href=t,r.port=e.port;var o=e.href.split("://");return!r.port&&o[1]&&(r.port=o[1].split("/")[0].split("@").pop().split(":")[1]),r.port&&"0"!==r.port||(r.port="https"===o[0]?"443":"80"),r.hostname=e.hostname||n.hostname,r.pathname=e.pathname,r.protocol=o[0],"/"!==r.pathname.charAt(0)&&(r.pathname="/"+r.pathname),r.sameOrigin=!e.hostname||e.hostname===document.domain&&e.port===n.port&&e.protocol===n.protocol,r}},{}],gos:[function(t,e){e.exports=t("7eSDFh")},{}],"7eSDFh":[function(t,e){function n(t,e,n){if(r.call(t,e))return t[e];var o=n();if(Object.defineProperty&&Object.keys)try{return Object.defineProperty(t,e,{value:o,writable:!0,enumerable:!1}),o}catch(i){}return t[e]=o,o}var r=Object.prototype.hasOwnProperty;e.exports=n},{}],D5DuLP:[function(t,e){function n(t,e,n){return r.listeners(t).length?r.emit(t,e,n):(o[t]||(o[t]=[]),void o[t].push(e))}var r=t("ee").create(),o={};e.exports=n,n.ee=r,r.q=o},{ee:"QJf3ax"}],handle:[function(t,e){e.exports=t("D5DuLP")},{}],XL7HBI:[function(t,e){function n(t){var e=typeof t;return!t||"object"!==e&&"function"!==e?-1:t===window?0:i(t,o,function(){return r++})}var r=1,o="nr@id",i=t("gos");e.exports=n},{gos:"7eSDFh"}],id:[function(t,e){e.exports=t("XL7HBI")},{}],loader:[function(t,e){e.exports=t("G9z0Bl")},{}],G9z0Bl:[function(t,e){function n(){var t=l.info=NREUM.info;if(t&&t.licenseKey&&t.applicationID&&f&&f.body){s(h,function(e,n){e in t||(t[e]=n)}),l.proto="https"===p.split(":")[0]||t.sslForHttp?"https://":"http://",a("mark",["onload",i()]);var e=f.createElement("script");e.src=l.proto+t.agent,f.body.appendChild(e)}}function r(){"complete"===f.readyState&&o()}function o(){a("mark",["domContent",i()])}function i(){return(new Date).getTime()}var a=t("handle"),s=t(1),c=window,f=c.document,u="addEventListener",d="attachEvent",p=(""+location).split("?")[0],h={beacon:"bam.nr-data.net",errorBeacon:"bam.nr-data.net",agent:"js-agent.newrelic.com/nr-515.min.js"},l=e.exports={offset:i(),origin:p,features:{}};f[u]?(f[u]("DOMContentLoaded",o,!1),c[u]("load",n,!1)):(f[d]("onreadystatechange",r),c[d]("onload",n)),a("mark",["firstbyte",i()])},{1:20,handle:"D5DuLP"}],20:[function(t,e){function n(t,e){var n=[],o="",i=0;for(o in t)r.call(t,o)&&(n[i]=e(o,t[o]),i+=1);return n}var r=Object.prototype.hasOwnProperty;e.exports=n},{}],21:[function(t,e){function n(t,e,n){e||(e=0),"undefined"==typeof n&&(n=t?t.length:0);for(var r=-1,o=n-e||0,i=Array(0>o?0:o);++rd;d++)c[d].apply(u,n);return u}function a(t,e){f[t]=s(t).concat(e)}function s(t){return f[t]||[]}function c(){return n(e)}var f={};return{on:a,emit:e,create:c,listeners:s,_events:f}}function r(){return{}}var o="nr@context",i=t("gos");e.exports=n()},{gos:"7eSDFh"}],ee:[function(t,e){e.exports=t("QJf3ax")},{}],3:[function(t){function e(t,e,n,i,s){try{c?c-=1:r("err",[s||new UncaughtException(t,e,n)])}catch(f){try{r("ierr",[f,(new Date).getTime(),!0])}catch(u){}}return"function"==typeof a?a.apply(this,o(arguments)):!1}function UncaughtException(t,e,n){this.message=t||"Uncaught error with no additional information",this.sourceURL=e,this.line=n}function n(t){r("err",[t,(new Date).getTime()])}var r=t("handle"),o=t(5),i=t("ee"),a=window.onerror,s=!1,c=0;t("loader").features.err=!0,window.onerror=e,NREUM.noticeError=n;try{throw new Error}catch(f){"stack"in f&&(t(1),t(4),"addEventListener"in window&&t(2),window.XMLHttpRequest&&XMLHttpRequest.prototype&&XMLHttpRequest.prototype.addEventListener&&t(3),s=!0)}i.on("fn-start",function(){s&&(c+=1)}),i.on("fn-err",function(t,e,r){s&&(this.thrown=!0,n(r))}),i.on("fn-end",function(){s&&!this.thrown&&c>0&&(c-=1)}),i.on("internal-error",function(t){r("ierr",[t,(new Date).getTime(),!0])})},{1:8,2:5,3:9,4:7,5:21,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],4:[function(t){function e(){}if(window.performance&&window.performance.timing&&window.performance.getEntriesByType){var n=t("ee"),r=t("handle"),o=t(2);t("loader").features.stn=!0,t(1),n.on("fn-start",function(t){var e=t[0];e instanceof Event&&(this.bstStart=Date.now())}),n.on("fn-end",function(t,e){var n=t[0];n instanceof Event&&r("bst",[n,e,this.bstStart,Date.now()])}),o.on("fn-start",function(t,e,n){this.bstStart=Date.now(),this.bstType=n}),o.on("fn-end",function(t,e){r("bstTimer",[e,this.bstStart,Date.now(),this.bstType])}),n.on("pushState-start",function(){this.time=Date.now(),this.startPath=location.pathname+location.hash}),n.on("pushState-end",function(){r("bstHist",[location.pathname+location.hash,this.startPath,this.time])}),"addEventListener"in window.performance&&(window.performance.addEventListener("webkitresourcetimingbufferfull",function(){r("bstResource",[window.performance.getEntriesByType("resource")]),window.performance.webkitClearResourceTimings()},!1),window.performance.addEventListener("resourcetimingbufferfull",function(){r("bstResource",[window.performance.getEntriesByType("resource")]),window.performance.clearResourceTimings()},!1)),document.addEventListener("scroll",e,!1),document.addEventListener("keypress",e,!1),document.addEventListener("click",e,!1)}},{1:6,2:8,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],5:[function(t,e){function n(t){i.inPlace(t,["addEventListener","removeEventListener"],"-",r)}function r(t){return t[1]}var o=(t(1),t("ee").create()),i=t(2)(o),a=t("gos");if(e.exports=o,n(window),"getPrototypeOf"in Object){for(var s=document;s&&!s.hasOwnProperty("addEventListener");)s=Object.getPrototypeOf(s);s&&n(s);for(var c=XMLHttpRequest.prototype;c&&!c.hasOwnProperty("addEventListener");)c=Object.getPrototypeOf(c);c&&n(c)}else XMLHttpRequest.prototype.hasOwnProperty("addEventListener")&&n(XMLHttpRequest.prototype);o.on("addEventListener-start",function(t){if(t[1]){var e=t[1];"function"==typeof e?this.wrapped=t[1]=a(e,"nr@wrapped",function(){return i(e,"fn-",null,e.name||"anonymous")}):"function"==typeof e.handleEvent&&i.inPlace(e,["handleEvent"],"fn-")}}),o.on("removeEventListener-start",function(t){var e=this.wrapped;e&&(t[1]=e)})},{1:21,2:22,ee:"QJf3ax",gos:"7eSDFh"}],6:[function(t,e){var n=(t(2),t("ee").create()),r=t(1)(n);e.exports=n,r.inPlace(window.history,["pushState"],"-")},{1:22,2:21,ee:"QJf3ax"}],7:[function(t,e){var n=(t(2),t("ee").create()),r=t(1)(n);e.exports=n,r.inPlace(window,["requestAnimationFrame","mozRequestAnimationFrame","webkitRequestAnimationFrame","msRequestAnimationFrame"],"raf-"),n.on("raf-start",function(t){t[0]=r(t[0],"fn-")})},{1:22,2:21,ee:"QJf3ax"}],8:[function(t,e){function n(t,e,n){var r=t[0];"string"==typeof r&&(r=new Function(r)),t[0]=o(r,"fn-",null,n)}var r=(t(2),t("ee").create()),o=t(1)(r);e.exports=r,o.inPlace(window,["setTimeout","setInterval","setImmediate"],"setTimer-"),r.on("setTimer-start",n)},{1:22,2:21,ee:"QJf3ax"}],9:[function(t,e){function n(){c.inPlace(this,d,"fn-")}function r(t,e){c.inPlace(e,["onreadystatechange"],"fn-")}function o(t,e){return e}var i=t("ee").create(),a=t(1),s=t(2),c=s(i),f=s(a),u=window.XMLHttpRequest,d=["onload","onerror","onabort","onloadstart","onloadend","onprogress","ontimeout"];e.exports=i,window.XMLHttpRequest=function(t){var e=new u(t);try{i.emit("new-xhr",[],e),f.inPlace(e,["addEventListener","removeEventListener"],"-",function(t,e){return e}),e.addEventListener("readystatechange",n,!1)}catch(r){try{i.emit("internal-error",[r])}catch(o){}}return e},window.XMLHttpRequest.prototype=u.prototype,c.inPlace(XMLHttpRequest.prototype,["open","send"],"-xhr-",o),i.on("send-xhr-start",r),i.on("open-xhr-start",r)},{1:5,2:22,ee:"QJf3ax"}],10:[function(t){function e(t){if("string"==typeof t&&t.length)return t.length;if("object"!=typeof t)return void 0;if("undefined"!=typeof ArrayBuffer&&t instanceof ArrayBuffer&&t.byteLength)return t.byteLength;if("undefined"!=typeof Blob&&t instanceof Blob&&t.size)return t.size;if("undefined"!=typeof FormData&&t instanceof FormData)return void 0;try{return JSON.stringify(t).length}catch(e){return void 0}}function n(t){var n=this.params,r=this.metrics;if(!this.ended){this.ended=!0;for(var i=0;c>i;i++)t.removeEventListener(s[i],this.listener,!1);if(!n.aborted){if(r.duration=(new Date).getTime()-this.startTime,4===t.readyState){n.status=t.status;var a=t.responseType,f="arraybuffer"===a||"blob"===a||"json"===a?t.response:t.responseText,u=e(f);if(u&&(r.rxSize=u),this.sameOrigin){var d=t.getResponseHeader("X-NewRelic-App-Data");d&&(n.cat=d.split(", ").pop())}}else n.status=0;r.cbTime=this.cbTime,o("xhr",[n,r,this.startTime])}}}function r(t,e){var n=i(e),r=t.params;r.host=n.hostname+":"+n.port,r.pathname=n.pathname,t.sameOrigin=n.sameOrigin}if(window.XMLHttpRequest&&XMLHttpRequest.prototype&&XMLHttpRequest.prototype.addEventListener&&!/CriOS/.test(navigator.userAgent)){t("loader").features.xhr=!0;var o=t("handle"),i=t(2),a=t("ee"),s=["load","error","abort","timeout"],c=s.length,f=t(1);t(4),t(3),a.on("new-xhr",function(){this.totalCbs=0,this.called=0,this.cbTime=0,this.end=n,this.ended=!1,this.xhrGuids={}}),a.on("open-xhr-start",function(t){this.params={method:t[0]},r(this,t[1]),this.metrics={}}),a.on("open-xhr-end",function(t,e){"loader_config"in NREUM&&"xpid"in NREUM.loader_config&&this.sameOrigin&&e.setRequestHeader("X-NewRelic-ID",NREUM.loader_config.xpid)}),a.on("send-xhr-start",function(t,n){var r=this.metrics,o=t[0],i=this;if(r&&o){var f=e(o);f&&(r.txSize=f)}this.startTime=(new Date).getTime(),this.listener=function(t){try{"abort"===t.type&&(i.params.aborted=!0),("load"!==t.type||i.called===i.totalCbs&&(i.onloadCalled||"function"!=typeof n.onload))&&i.end(n)}catch(e){try{a.emit("internal-error",[e])}catch(r){}}};for(var u=0;c>u;u++)n.addEventListener(s[u],this.listener,!1)}),a.on("xhr-cb-time",function(t,e,n){this.cbTime+=t,e?this.onloadCalled=!0:this.called+=1,this.called!==this.totalCbs||!this.onloadCalled&&"function"==typeof n.onload||this.end(n)}),a.on("xhr-load-added",function(t,e){var n=""+f(t)+!!e;this.xhrGuids&&!this.xhrGuids[n]&&(this.xhrGuids[n]=!0,this.totalCbs+=1)}),a.on("xhr-load-removed",function(t,e){var n=""+f(t)+!!e;this.xhrGuids&&this.xhrGuids[n]&&(delete this.xhrGuids[n],this.totalCbs-=1)}),a.on("addEventListener-end",function(t,e){e instanceof XMLHttpRequest&&"load"===t[0]&&a.emit("xhr-load-added",[t[1],t[2]],e)}),a.on("removeEventListener-end",function(t,e){e instanceof XMLHttpRequest&&"load"===t[0]&&a.emit("xhr-load-removed",[t[1],t[2]],e)}),a.on("fn-start",function(t,e,n){e instanceof XMLHttpRequest&&("onload"===n&&(this.onload=!0),("load"===(t[0]&&t[0].type)||this.onload)&&(this.xhrCbStart=(new Date).getTime()))}),a.on("fn-end",function(t,e){this.xhrCbStart&&a.emit("xhr-cb-time",[(new Date).getTime()-this.xhrCbStart,this.onload,e],e)})}},{1:"XL7HBI",2:11,3:9,4:5,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],11:[function(t,e){e.exports=function(t){var e=document.createElement("a"),n=window.location,r={};e.href=t,r.port=e.port;var o=e.href.split("://");return!r.port&&o[1]&&(r.port=o[1].split("/")[0].split("@").pop().split(":")[1]),r.port&&"0"!==r.port||(r.port="https"===o[0]?"443":"80"),r.hostname=e.hostname||n.hostname,r.pathname=e.pathname,r.protocol=o[0],"/"!==r.pathname.charAt(0)&&(r.pathname="/"+r.pathname),r.sameOrigin=!e.hostname||e.hostname===document.domain&&e.port===n.port&&e.protocol===n.protocol,r}},{}],gos:[function(t,e){e.exports=t("7eSDFh")},{}],"7eSDFh":[function(t,e){function n(t,e,n){if(r.call(t,e))return t[e];var o=n();if(Object.defineProperty&&Object.keys)try{return Object.defineProperty(t,e,{value:o,writable:!0,enumerable:!1}),o}catch(i){}return t[e]=o,o}var r=Object.prototype.hasOwnProperty;e.exports=n},{}],D5DuLP:[function(t,e){function n(t,e,n){return r.listeners(t).length?r.emit(t,e,n):(o[t]||(o[t]=[]),void o[t].push(e))}var r=t("ee").create(),o={};e.exports=n,n.ee=r,r.q=o},{ee:"QJf3ax"}],handle:[function(t,e){e.exports=t("D5DuLP")},{}],XL7HBI:[function(t,e){function n(t){var e=typeof t;return!t||"object"!==e&&"function"!==e?-1:t===window?0:i(t,o,function(){return r++})}var r=1,o="nr@id",i=t("gos");e.exports=n},{gos:"7eSDFh"}],id:[function(t,e){e.exports=t("XL7HBI")},{}],loader:[function(t,e){e.exports=t("G9z0Bl")},{}],G9z0Bl:[function(t,e){function n(){var t=l.info=NREUM.info;if(t&&t.licenseKey&&t.applicationID&&f&&f.body){s(h,function(e,n){e in t||(t[e]=n)}),l.proto="https"===p.split(":")[0]||t.sslForHttp?"https://":"http://",a("mark",["onload",i()]);var e=f.createElement("script");e.src=l.proto+t.agent,f.body.appendChild(e)}}function r(){"complete"===f.readyState&&o()}function o(){a("mark",["domContent",i()])}function i(){return(new Date).getTime()}var a=t("handle"),s=t(1),c=window,f=c.document,u="addEventListener",d="attachEvent",p=(""+location).split("?")[0],h={beacon:"bam.nr-data.net",errorBeacon:"bam.nr-data.net",agent:"js-agent.newrelic.com/nr-515.min.js"},l=e.exports={offset:i(),origin:p,features:{}};f[u]?(f[u]("DOMContentLoaded",o,!1),c[u]("load",n,!1)):(f[d]("onreadystatechange",r),c[d]("onload",n)),a("mark",["firstbyte",i()])},{1:20,handle:"D5DuLP"}],20:[function(t,e){function n(t,e){var n=[],o="",i=0;for(o in t)r.call(t,o)&&(n[i]=e(o,t[o]),i+=1);return n}var r=Object.prototype.hasOwnProperty;e.exports=n},{}],21:[function(t,e){function n(t,e,n){e||(e=0),"undefined"==typeof n&&(n=t?t.length:0);for(var r=-1,o=n-e||0,i=Array(0>o?0:o);++r