├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.yml
├── .gitignore
├── .moniteurrc.default.yml
├── .moniteurrc.development.yml
├── .travis.yml
├── CHANGELOG.md
├── Procfile
├── README.md
├── __tests__
└── utils-test.js
├── app.json
├── bin
├── moniteur
└── moniteur.js
├── client
├── javascripts
│ ├── app.js
│ └── assets-graph-theme.js
└── stylesheets
│ └── style.css
├── docs
├── screenshot.png
├── welcome-process.key
└── welcome-process
│ ├── welcome-process.001.png
│ ├── welcome-process.002.png
│ ├── welcome-process.003.png
│ ├── welcome-process.004.png
│ └── welcome-process.005.png
├── fixtures
├── abc.js
├── main.css
├── main2.css
└── xyz.js
├── lib
├── db.js
├── jsparser.js
├── record.js
├── sensors.js
└── utils.js
├── package.json
├── routes
├── index.js
├── metrics.js
└── settings.js
├── views
├── _faq.pug
├── error.pug
├── index.pug
├── layout.pug
├── settings.pug
├── support.pug
└── welcome.pug
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["es2015", {"modules": false}]]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; This file is for unifying the coding style for different editors and IDEs
2 | ; See editorconfig.org
3 |
4 | ; top-most EditorConfig file
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | __tests__/fixtures/*
2 | dist
3 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: "standard"
2 | plugins:
3 | - jasmine
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | bower_components
5 | .moniteurdb # local database
6 | .vscode
7 | .moniteurdb
8 | dist
9 |
10 | # Comment this line if you'd like to version your custom configuration
11 | /.moniteurrc.yml
12 |
--------------------------------------------------------------------------------
/.moniteurrc.default.yml:
--------------------------------------------------------------------------------
1 | # Default configuration
2 | #
3 | # To override these values, multiple options:
4 | # - Environment variable:
5 | # DB__REDIS_URL="redis://localhost:6379" npm start
6 | # - Arguments:
7 | # npm start -- --db:redis_url "redis://localhost:6379"
8 | # - .moniteurrc.yml (override file):
9 | # copy and paste the contents of this file to .moniteurrc.yml
10 | # and tweak the values as desired
11 | db:
12 | directory: ".moniteurdb"
13 |
--------------------------------------------------------------------------------
/.moniteurrc.development.yml:
--------------------------------------------------------------------------------
1 | # Development configuration
2 | assets:
3 | FT desktop CSS bundle:
4 | - "http://s1.ft-static.com/m/style/90975546/bundles/core.css"
5 | # Commented until https://github.com/t32k/stylestats/issues/158 is fixed
6 | # - "http://navigation.webservices.ft.com/v1/navigation/ft/css/style.min.css"
7 | # - "http://s1.ft-static.com/m/style/5c37627a/bundles/nonArticle.css"
8 | Guardian's CSS:
9 | - "http://assets.guim.co.uk/stylesheets/global.css"
10 | # Commented until https://github.com/t32k/stylestats/issues/158 is fixed
11 | # - "http://assets.guim.co.uk/stylesheets/head.default.css"
12 | Main CSS: "fixtures/main.css"
13 | Another CSS: "fixtures/main2.css"
14 | ABC Script: "fixtures/abc.js"
15 | My XYZ Script: "fixtures/xyz.js"
16 | My Bundle of Scripts:
17 | - "fixtures/abc.js"
18 | - "fixtures/xyz.js"
19 | A remote Script: "http://assets.guim.co.uk/javascripts/bootstraps/app.js"
20 | # Uncomment to use Redis instead of the filesystem
21 | # db:
22 | # redis_url: redis://localhost:6379
23 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 8.9.4
4 | before_install: yarn global add greenkeeper-lockfile@1
5 | before_script: greenkeeper-lockfile-update
6 | after_script: greenkeeper-lockfile-upload
7 | env:
8 | global:
9 | secure: g+LI0n3NmGqXlrFPX3gwjf+EOjrDtdl2gpsBFTt7rL3mik8xdEhVxMu4kFBI+CByHP/JiwvYSaVgqw12bf2ATPLcWDwwmjmc3dPJUIHMAkGicLMBYuJRZdQIfB6j8OY1jA8fqCs9xEU691V3N8hOYYsNWY5d4ySMiGaRtvIWM5Q=
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | ## [0.7.15](https://github.com/kaelig/moniteur/compare/v0.7.14...v0.7.15) (2017-05-03)
7 |
8 |
9 | ### Bug Fixes
10 |
11 | * **package:** update yargs to version 8.0.1 ([#22](https://github.com/kaelig/moniteur/issues/22)) ([f211aed](https://github.com/kaelig/moniteur/commit/f211aed))
12 | * **yarn:** upgrade yarn dependency tree ([cc87fd7](https://github.com/kaelig/moniteur/commit/cc87fd7))
13 |
14 |
15 |
16 |
17 | ## [0.7.14](https://github.com/kaelig/moniteur/compare/v0.7.13...v0.7.14) (2017-05-03)
18 |
19 |
20 | ### Bug Fixes
21 |
22 | * **build:** Use Yarn to fix an issue where some dependency tree relationship would fail ([b1572a6](https://github.com/kaelig/moniteur/commit/b1572a6))
23 | * **utils:** Fix an issue where querystrings would make extension recognition fail ([40209c0](https://github.com/kaelig/moniteur/commit/40209c0))
24 |
25 |
26 |
27 |
28 | ## [0.7.13](https://github.com/kaelig/moniteur/compare/v0.7.12...v0.7.13) (2017-05-03)
29 |
30 |
31 | ### Bug Fixes
32 |
33 | * **package:** update babel-loader to version 7.0.0 ([d005e60](https://github.com/kaelig/moniteur/commit/d005e60))
34 |
35 |
36 |
37 |
38 | ## [0.7.12](https://github.com/kaelig/moniteur/compare/v0.7.11...v0.7.12) (2017-03-05)
39 |
40 |
41 | ### Bug Fixes
42 |
43 | * **assets.json:** Fix an issue where /assets.json wouldn't work ([68413ba](https://github.com/kaelig/moniteur/commit/68413ba))
44 |
45 |
46 |
47 |
48 | ## [0.7.11](https://github.com/kaelig/moniteur/compare/v0.7.10...v0.7.11) (2017-03-05)
49 |
50 |
51 | ### Bug Fixes
52 |
53 | * **route:** Fix routes for static pages ([c2a293b](https://github.com/kaelig/moniteur/commit/c2a293b))
54 |
55 |
56 |
57 |
58 | ## [0.7.10](https://github.com/kaelig/moniteur/compare/v0.7.9...v0.7.10) (2017-03-05)
59 |
60 |
61 |
62 |
63 | ## [0.7.9](https://github.com/kaelig/moniteur/compare/v0.7.8...v0.7.9) (2017-03-05)
64 |
65 |
66 | ### Bug Fixes
67 |
68 | * **package:** update babel-preset-babili to version 0.0.11 ([b0f3468](https://github.com/kaelig/moniteur/commit/b0f3468))
69 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm start
2 | record: npm run record
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # moniteur [](https://travis-ci.org/kaelig/moniteur) [](http://badge.fury.io/js/moniteur) [](https://greenkeeper.io/) [](https://github.com/conventional-changelog/standard-version)
2 |
3 | For people who care about keeping an eye on their CSS and JavaScript file sizes.
4 |
5 | [](https://heroku.com/deploy)
6 |
7 | **[Demo & Documentation](https://moniteur.herokuapp.com/)**
8 |
9 | 
10 |
11 | ----
12 |
13 | ## CLI
14 |
15 | Moniteur is also available as a command line interface:
16 |
17 | ```bash
18 | npm install -g moniteur
19 | ```
20 |
21 | ### Usage:
22 |
23 | ```bash
24 | Usage: moniteur [options] [command]
25 |
26 |
27 | Commands:
28 |
29 | record record a snapshot of all asset metrics
30 | serve start the server to show metrics in the browser
31 | assets display the list of assets loaded by moniteur
32 | help display this helpful message
33 |
34 | Options:
35 |
36 | -h, --help output usage information
37 | -V, --version output the version number
38 | ```
39 |
40 | Configuration: edit the `.moniteurrc.yml` file in the current directory.
41 |
42 | ### Database configuration
43 |
44 | For now, two types of storage are supported: Redis and local filesystem.
45 |
46 | A Redis URL can be passed through an environment variable,
47 | instead of having it stored in the configuration file:
48 |
49 | ```
50 | DB__REDIS_URL=redis://rediscloud:XXXX@pub-redis-XXXX.us-east-X-X.X.ec2.garantiadata.com:13714
51 | ```
52 |
53 | Run your application like this:
54 | ```
55 | DB__REDIS_URL=redis://url moniteur [options]
56 | ```
57 |
58 | Note that `REDIS_URL` and `REDISCLOUD_URL` are also valid environment variables.
59 |
60 | ### Development
61 |
62 | Clone the repository and run:
63 |
64 | ```bash
65 | npm run dev
66 | ```
67 |
68 | ## Asset monitor API
69 |
70 | ### Record data
71 |
72 | Takes a snapshot of asset metrics and stores them in the `.moniteur/`
73 | directory.
74 |
75 | ```bash
76 | moniteur record
77 | ```
78 |
79 |
80 | ## HTTP API
81 |
82 | ### View a JSON representation of all loaded assets
83 |
84 | `/assets.json`
85 |
86 | #### JSON data object for HighCharts (providing the asset name's hash)
87 |
88 | Since forever:
89 | `/metrics/stylesheets/adf6e9c154cb57a818f7fb407085bff6`
90 |
91 | Between two dates:
92 | `/metrics/stylesheets/adf6e9c154cb57a818f7fb407085bff6/1015711104475..1415711104475`
93 |
94 |
95 | ## License
96 |
97 | MIT
98 |
99 | ## Acknowledgments
100 |
101 | _Merci_ to https://github.com/t32k/stylestats, which has been
102 | a great source of inspiration.
103 |
104 | And thanks to @oncletom for helping building the early versions of moniteur.
105 |
106 | ## Roadmap
107 |
108 | - [x] Make moniteur a working node module
109 | - [x] Run as some sort of daemon that monitors asset metrics every X seconds
110 | - [x] Monitor JavaScript files
111 | - [ ] Unit / Integration tests
112 | - [ ] Option to filter graphs by time range (last 7 days, last 30 days, last year)
113 | - [ ] Slack Bot
114 |
115 | ### Ideas
116 |
117 | - [ ] Providing a page's URL, scrape all assets out of it and analyse them
118 | - [ ] Parse all assets in a particular directory
119 | - [ ] Asset size budget limits
120 | - [ ] Email alert when budget is almost reached or exceeded
121 | - [ ] Weekly email recaps
122 |
--------------------------------------------------------------------------------
/__tests__/utils-test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, expect, jasmine */
2 | const utils = require('../lib/utils')
3 | const md5 = require('md5')
4 |
5 | const assets = {
6 | SingleResource: 'http://foo.com/something.css',
7 | Bundle: [
8 | 'http://bar.com/something.css'
9 | ]
10 | }
11 |
12 | describe('hashedAssets', () => {
13 | it('should hash assets', () => {
14 | const hashedAssets = utils.hashAssets(assets)
15 | expect(hashedAssets[md5('SingleResource')]).toEqual(jasmine.any(String))
16 | expect(hashedAssets[md5('Bundle')]).toEqual(jasmine.any(Array))
17 | })
18 | })
19 |
20 | describe('assetType', () => {
21 | it('returns the correct type', () => {
22 | expect(utils.getAssetType('/path/something.css')).toEqual('css')
23 | expect(utils.getAssetType('/path/something.css?foo=bar')).toEqual('css')
24 | expect(utils.getAssetType('/path/to.html')).toEqual('html')
25 | expect(utils.getAssetType('http://js.com/something.css')).toEqual('css')
26 | expect(utils.getAssetType(['http://bar.com/something.css'])).toEqual('css')
27 | expect(utils.getAssetType(['http://bar.com/something.css?foo=bar'])).toEqual('css')
28 | })
29 | })
30 |
31 | describe('getAssetTypeFromHash', () => {
32 | it('returns the correct asset type', () => {
33 | const hash = md5(Object.keys(assets)[0])
34 | expect(utils.getAssetTypeFromHash(hash, assets)).toEqual('css')
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Moniteur",
3 | "description": "Monitor your asset size over time, in your browser, or using the provided HTTP API.",
4 | "keywords": [
5 | "performance",
6 | "tracking"
7 | ],
8 | "website": "http://moniteur.herokuapp.com/",
9 | "repository": "https://github.com/kaelig/moniteur",
10 | "success_url": "/welcome",
11 | "scripts": {
12 | "postdeploy": "npm run record"
13 | },
14 | "env": {
15 | "ASSETS": {
16 | "description": "Assets to track in YAML (alternatively, use the configuration files)",
17 | "value": "Moniteur's CSS: https://moniteur.herokuapp.com/stylesheets/style.css\nMoniteur's JavaScript: https://moniteur.herokuapp.com/js/bundle.js",
18 | "required": false
19 | },
20 | "USERNAME": {
21 | "description": "Username for basic authentication protection (optional)",
22 | "required": false
23 | },
24 | "PASSWORD": {
25 | "description": "Password for basic authentication protection (optional)",
26 | "required": false
27 | }
28 | },
29 | "addons": [
30 | "rediscloud",
31 | "scheduler:standard"
32 | ],
33 | "buildpacks": [
34 | {
35 | "url": "heroku/nodejs"
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/bin/moniteur:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require('./moniteur.js')
4 |
--------------------------------------------------------------------------------
/bin/moniteur.js:
--------------------------------------------------------------------------------
1 | const debug = require('debug')
2 | const program = require('commander')
3 | const db = require('../lib/db')
4 | const Record = require('../lib/record')
5 | const nconf = require('nconf')
6 | const compression = require('compression')
7 | const express = require('express')
8 | const auth = require('http-auth')
9 | const slashes = require('express-slashes')
10 | const path = require('path')
11 | const lem = require('lem')
12 | const yaml = require('js-yaml')
13 | const fs = require('fs')
14 | nconf.formats.yaml = require('nconf-yaml')
15 | const log = debug('moniteur:log')
16 |
17 | program
18 | .version(require('../package.json').version)
19 |
20 | nconf
21 | .env({
22 | separator: '__',
23 | lowerCase: true,
24 | whitelist: ['REDISCLOUD_URL', 'REDIS_URL', 'DB__REDIS_URL']
25 | })
26 | .argv()
27 |
28 | if (process.env.NODE_ENV !== 'production') {
29 | nconf.file('environment', { file: path.join(__dirname, '/../.moniteurrc.development.yml'), format: nconf.formats.yaml })
30 | }
31 | nconf.file('environment', { file: '.moniteurrc.yml', dir: process.cwd(), search: true, format: nconf.formats.yaml })
32 |
33 | nconf.defaults(yaml.safeLoad(fs.readFileSync(path.join(__dirname, '/../.moniteurrc.default.yml'), 'utf8')))
34 |
35 | nconf
36 | .set('assets', process.env.ASSETS ? yaml.safeLoad(process.env.ASSETS) : nconf.get('assets'))
37 |
38 | // nconf evaluates the : in the protocol as a key:value pair
39 | // so we're restoring the colon in the URL protocols
40 | nconf
41 | .set('db:redis_url',
42 | process.env.REDIS_URL ? process.env.REDIS_URL.replace(/redis\/\//, 'redis://')
43 | : (process.env.REDISCLOUD_URL ? process.env.REDISCLOUD_URL.replace(/redis\/\//, 'redis://')
44 | : (nconf.get('db:redis_url') ? nconf.get('db:redis_url').replace(/redis\/\//, 'redis://')
45 | : null)))
46 |
47 | program
48 | .command('record')
49 | .description('record a snapshot of all asset metrics')
50 | .action((cmd, env) => {
51 | log(nconf.get('assets'))
52 | log(nconf.get('db'))
53 | const dbinstance = db(nconf.get('db'))
54 |
55 | const record = new Record(nconf.get('assets'), lem(dbinstance))
56 | return Promise.all(record.init()).then((data) => {
57 | return Promise.all(record.recordDataPoints()).then((data) => {
58 | dbinstance.close()
59 | return log('DataPoints:', JSON.stringify(data, null, 4))
60 | }, (reason) => console.log(reason))
61 | }, (reason) => console.log(reason))
62 | })
63 |
64 | program
65 | .command('serve')
66 | .description('start the server to show metrics in the browser')
67 | .action(() => {
68 | const app = express()
69 |
70 | // Basic auth
71 | // Set USERNAME and PASSWORD environment variables
72 | const basic = auth.basic({
73 | realm: 'Moniteur'
74 | }, (username, password, next) => {
75 | next(username === process.env.USERNAME && password === process.env.PASSWORD)
76 | })
77 |
78 | if (process.env.USERNAME && process.env.PASSWORD) {
79 | app.use(function (req, res, next) {
80 | // Exclude /metrics so we can fetch() them
81 | if (req.path.startsWith('/metrics')) {
82 | next()
83 | } else {
84 | (auth.connect(basic))(req, res, next)
85 | }
86 | })
87 | }
88 |
89 | app.set('strict routing', true)
90 | const router = express.Router({
91 | caseSensitive: app.get('case sensitive routing'),
92 | strict: app.get('strict routing')
93 | })
94 | app.use(compression())
95 |
96 | app.use(router)
97 | app.use(slashes())
98 |
99 | log(nconf.get('db'))
100 | const dbinstance = db(nconf.get('db'))
101 |
102 | router.use((req, res, next) => {
103 | res.locals.assets = nconf.get('assets')
104 | res.locals.db = dbinstance
105 | next()
106 | })
107 |
108 | // JS Setup
109 | if (app.get('env') === 'development') {
110 | const webpack = require('webpack')
111 | const webpackDevMiddleware = require('webpack-dev-middleware')
112 | const webpackHotMiddleware = require('webpack-hot-middleware')
113 | const webpackConfig = require('../webpack.config')
114 | const bundler = webpack(webpackConfig)
115 |
116 | app.use(webpackDevMiddleware(bundler, {
117 | publicPath: '/js/',
118 | stats: { colors: true }
119 | }))
120 | app.use(webpackHotMiddleware(bundler, {
121 | log: console.log
122 | }))
123 | }
124 |
125 | // view engine setup
126 | app.set('views', path.join(__dirname, '../views'))
127 | app.set('view engine', 'pug')
128 | app.set('view cache', true)
129 |
130 | router.use('/js', express.static(path.join(__dirname, '../dist/js')))
131 | router.use('/stylesheets', express.static(path.join(__dirname, '../client/stylesheets')))
132 | router.use('/docs', express.static(path.join(__dirname, '../docs')))
133 |
134 | router.use('/', require('../routes/index'))
135 | router.get('/welcome', (req, res) => res.render('welcome', { title: 'moniteur: welcome' }))
136 | router.get('/support', (req, res) => res.render('support', { title: 'moniteur: support' }))
137 | router.use('/metrics', require('../routes/metrics'))
138 | app.use('/settings', require('../routes/settings'))
139 | router.get('/assets.json', (req, res) => {
140 | res.json(res.locals.assets)
141 | })
142 |
143 | // Hide from crawlers
144 | router.get('/robots.txt', (req, res) => {
145 | res.type('text/plain')
146 | res.send('User-agent: *\nDisallow: /')
147 | })
148 |
149 | // Catch 404 and forward to error handler
150 | app.use((req, res, next) => {
151 | let err = new Error('Not Found')
152 | err.status = 404
153 | next(err)
154 | })
155 |
156 | // Error handlers
157 | if (app.get('env') === 'development') {
158 | // development error handler
159 | // will print stacktrace
160 | app.use((err, req, res, next) => {
161 | res.status(err.status || 500)
162 | res.render('error', {
163 | message: err.message,
164 | error: err
165 | })
166 | })
167 | } else {
168 | // production error handler
169 | // no stacktraces leaked to user
170 | app.use((err, req, res, next) => {
171 | res.status(err.status || 500)
172 | res.render('error', {
173 | message: err.message,
174 | error: {}
175 | })
176 | })
177 | }
178 |
179 | app.set('port', process.env.PORT || 3000)
180 |
181 | if (app.get('env') === 'development') {
182 | const browserSync = require('browser-sync')
183 |
184 | browserSync({
185 | server: {
186 | port: app.get('port'),
187 | baseDir: './',
188 | middleware: [app]
189 | },
190 | open: false,
191 | logFileChanges: false,
192 | notify: false,
193 | files: [
194 | 'views/*.pug',
195 | 'client/stylesheets/*.css'
196 | ]
197 | })
198 | } else {
199 | app.listen(app.get('port'))
200 | }
201 | })
202 |
203 | program
204 | .command('assets')
205 | .description('display the list of assets loaded by moniteur')
206 | .action(() => console.log(nconf.get('assets')))
207 |
208 | program.command('help', null, {isDefault: true})
209 | .description('display this helpful message')
210 | .action(() => program.outputHelp())
211 |
212 | program.command('*', null, {noHelp: true})
213 | .action(function (cmd) {
214 | console.error(`${cmd} is not a moniteur command. See usage below`)
215 | program.outputHelp()
216 | })
217 |
218 | program.parse(process.argv)
219 |
--------------------------------------------------------------------------------
/client/javascripts/app.js:
--------------------------------------------------------------------------------
1 | /* global Highcharts */
2 | /* eslint-env browser */
3 | require('./assets-graph-theme')
4 | const prettyBytes = require('pretty-bytes')
5 |
6 | Array.from(document.querySelectorAll('.js-asset')).map(assetContainer => {
7 | const assetHash = assetContainer.dataset.assetHash
8 |
9 | return fetch(`/metrics/${assetHash}`)
10 | .then(res => res.json())
11 | .then(series => {
12 | const sizes = series[0].data
13 | if (!sizes.length) {
14 | assetContainer.querySelector('#js-asset-chart-' + assetHash).innerHTML =
15 | `
16 |
17 |
Could not find any data for this asset.
18 |
19 | You may need to trigger a recording
20 | so that moniteur has some data to show.
21 |
22 |
23 | `
24 | return
25 | }
26 | const firstSize = sizes[0][1]
27 | const lastSize = sizes[sizes.length - 1][1]
28 |
29 | const difference = lastSize - firstSize
30 | const prettyDifference = prettyBytes(difference)
31 | const trend = (difference < 0) ? 'down' : 'up'
32 | const trendSign = (difference > 0) ? '+' : ''
33 | const $trendElement = document.querySelector('.js-trend-' + assetHash)
34 |
35 | if (difference !== 0) {
36 | $trendElement.querySelector('.js-trend-sign').textContent = trendSign
37 | $trendElement.classList.add('trend--' + trend)
38 | $trendElement.querySelector('.js-trend-value').textContent = prettyDifference
39 | $trendElement.setAttribute('title', `${trendSign}${prettyDifference} (uncompressed) compared to the beginning of the period`)
40 | }
41 |
42 | Highcharts.chart(
43 | 'js-asset-chart-' + assetHash,
44 | {
45 | chart: {
46 | type: 'spline'
47 | },
48 | yAxis: [
49 | {
50 | title: {
51 | text: 'Size'
52 | },
53 | labels: {
54 | formatter: function () {
55 | return prettyBytes(this.value)
56 | }
57 | }
58 | },
59 | {
60 | title: {
61 | text: 'Count'
62 | },
63 | opposite: true
64 | }
65 | ],
66 | tooltip: {
67 | crosshairs: [false, true],
68 | formatter: function () {
69 | return '' + this.series.name + '
' + Highcharts.dateFormat('%b %e, %H:%M', this.x) + ': ' + (this.series.area ? prettyBytes(this.y) : this.y) + ''
70 | }
71 | },
72 | series: series
73 | }
74 | )
75 | })
76 | })
77 |
--------------------------------------------------------------------------------
/client/javascripts/assets-graph-theme.js:
--------------------------------------------------------------------------------
1 | /* global Highcharts */
2 |
3 | /**
4 | * Assets graph Theme for Highcharts JS
5 | * @author Kaelig Deloumeau-Prigent
6 | * Based on Dark theme for Highcharts JS
7 | * @author Torstein Honsi
8 | */
9 |
10 | Highcharts.theme = {
11 | colors: ['#2b908f', '#90ee7e', '#f45b5b', 'rgba(119, 152, 192, .33)', 'rgba(170, 238, 238, .33)', '#ff0066', '#eeaaee',
12 | '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'],
13 | chart: {
14 | backgroundColor: '#2a2a2b',
15 | style: {
16 | fontFamily: 'sans-serif'
17 | },
18 | plotBorderColor: '#606063'
19 | },
20 | title: {
21 | style: {
22 | display: 'none'
23 | }
24 | },
25 | credits: {
26 | enabled: false
27 | },
28 | subtitle: {
29 | style: {
30 | color: '#E0E0E3',
31 | textTransform: 'uppercase'
32 | }
33 | },
34 | xAxis: {
35 | gridLineColor: '#707073',
36 | labels: {
37 | style: {
38 | color: '#E0E0E3',
39 | fontSize: '12px'
40 | }
41 | },
42 | lineColor: '#707073',
43 | minorGridLineColor: '#505053',
44 | tickColor: '#707073',
45 | type: 'datetime',
46 | dateTimeLabelFormats: {
47 | month: '%e. %b',
48 | year: '%b'
49 | },
50 | title: {
51 | enabled: false
52 | }
53 | },
54 | yAxis: {
55 | gridLineColor: '#707073',
56 | labels: {
57 | style: {
58 | color: '#E0E0E3'
59 | }
60 | },
61 | lineColor: '#707073',
62 | minorGridLineColor: '#505053',
63 | tickColor: '#707073',
64 | tickWidth: 1,
65 | title: {
66 | style: {
67 | color: '#A0A0A3'
68 | }
69 | }
70 | },
71 | tooltip: {
72 | backgroundColor: 'rgba(0, 0, 0, 0.85)',
73 | style: {
74 | color: '#F0F0F0'
75 | }
76 | },
77 | plotOptions: {
78 | series: {
79 | dataLabels: {
80 | color: '#B0B0B3'
81 | },
82 | marker: {
83 | lineColor: '#333'
84 | }
85 | },
86 | boxplot: {
87 | fillColor: '#505053'
88 | },
89 | candlestick: {
90 | lineColor: 'white'
91 | },
92 | errorbar: {
93 | color: 'white'
94 | },
95 | areaspline: {
96 | marker: {
97 | symbol: 'circle',
98 | radius: 7
99 | },
100 | lineWidth: 5,
101 | states: {
102 | hover: {
103 | lineWidth: 6
104 | }
105 | }
106 | },
107 | spline: {
108 | marker: {
109 | symbol: 'circle',
110 | radius: 4
111 | },
112 | lineWidth: 3,
113 | states: {
114 | hover: {
115 | lineWidth: 4
116 | }
117 | }
118 | }
119 | },
120 | legend: {
121 | align: 'center',
122 | itemMarginBottom: 4,
123 | itemStyle: {
124 | color: '#E0E0E3',
125 | fontSize: '14px',
126 | fontWeight: 'lighter'
127 | },
128 | itemHoverStyle: {
129 | color: '#FFF'
130 | },
131 | itemHiddenStyle: {
132 | color: '#606063'
133 | }
134 | },
135 | labels: {
136 | style: {
137 | color: '#707073'
138 | }
139 | },
140 |
141 | drilldown: {
142 | activeAxisLabelStyle: {
143 | color: '#F0F0F3'
144 | },
145 | activeDataLabelStyle: {
146 | color: '#F0F0F3'
147 | }
148 | },
149 |
150 | navigation: {
151 | buttonOptions: {
152 | enabled: false
153 | }
154 | },
155 |
156 | // scroll charts
157 | rangeSelector: {
158 | buttonTheme: {
159 | fill: '#505053',
160 | stroke: '#000000',
161 | style: {
162 | color: '#CCC'
163 | },
164 | states: {
165 | hover: {
166 | fill: '#707073',
167 | stroke: '#000000',
168 | style: {
169 | color: 'white'
170 | }
171 | },
172 | select: {
173 | fill: '#000003',
174 | stroke: '#000000',
175 | style: {
176 | color: 'white'
177 | }
178 | }
179 | }
180 | },
181 | inputBoxBorderColor: '#505053',
182 | inputStyle: {
183 | backgroundColor: '#333',
184 | color: 'silver'
185 | },
186 | labelStyle: {
187 | color: 'silver'
188 | }
189 | },
190 |
191 | navigator: {
192 | handles: {
193 | backgroundColor: '#666',
194 | borderColor: '#AAA'
195 | },
196 | outlineColor: '#CCC',
197 | maskFill: 'rgba(255,255,255,0.1)',
198 | series: {
199 | color: '#7798BF',
200 | lineColor: '#A6C7ED'
201 | },
202 | xAxis: {
203 | gridLineColor: '#505053'
204 | }
205 | },
206 |
207 | scrollbar: {
208 | barBackgroundColor: '#808083',
209 | barBorderColor: '#808083',
210 | buttonArrowColor: '#CCC',
211 | buttonBackgroundColor: '#606063',
212 | buttonBorderColor: '#606063',
213 | rifleColor: '#FFF',
214 | trackBackgroundColor: '#404043',
215 | trackBorderColor: '#404043'
216 | },
217 |
218 | // special colors for some of the
219 | legendBackgroundColor: 'rgba(0, 0, 0, 0.5)',
220 | background2: '#505053',
221 | dataLabelsColor: '#B0B0B3',
222 | textColor: '#C0C0C0',
223 | contrastTextColor: '#F0F0F3',
224 | maskColor: 'rgba(255,255,255,0.3)'
225 | }
226 |
227 | // Apply the theme
228 | Highcharts.setOptions(Highcharts.theme)
229 |
--------------------------------------------------------------------------------
/client/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | margin: 0;
3 | background: #2a2a2b;
4 | color: white;
5 | font-family: -apple-system, BlinkMacSystemFont, Arial, sans-serif;
6 | line-height: 1.5;
7 | text-rendering: optimizeLegibility;
8 | -webkit-font-smoothing: antialiased;
9 | }
10 |
11 | body {
12 | margin: 0;
13 | }
14 |
15 | img {
16 | border: 0;
17 | max-width: 100%;
18 | }
19 |
20 | a {
21 | color: #ddd;
22 | }
23 |
24 | .l-container {
25 | box-sizing: border-box;
26 | margin: 0 auto;
27 | padding: 0 1em;
28 | max-width: 1400px;
29 | }
30 |
31 | .l-header,
32 | .l-footer {
33 | background: rgba(0, 0, 0, .3);
34 | }
35 | .l-header,
36 | .l-footer,
37 | .l-content {
38 | margin: 0;
39 | padding: 1rem 2rem;
40 | }
41 | .l-footer {
42 | margin-top: 2em;
43 | }
44 | .l-footer a {
45 | margin-right: 1.5em;
46 | }
47 |
48 | h1 {
49 | margin: 0;
50 | }
51 | h1 a {
52 | text-decoration: none;
53 | }
54 | h1 a:hover,
55 | h1 a:focus {
56 | text-decoration: underline;
57 | }
58 |
59 | .asset {
60 | box-sizing: border-box;
61 | padding-top: 1rem;
62 | padding-bottom: 1rem;
63 | border-top: .5rem solid rgba(0, 0, 0, .1);
64 | }
65 | @media (min-width: 600px) {
66 | .asset {
67 | padding-top: 2em;
68 | padding-bottom: 2em;
69 | }
70 | }
71 | .asset:first-child {
72 | border-top: 0;
73 | }
74 | .asset__name {
75 | font-size: 2em;
76 | margin-left: 1.33em;
77 | }
78 | .asset__type {
79 | position: relative;
80 | }
81 | .asset__type::before {
82 | content: '{}';
83 | display: inline;
84 | position: absolute;
85 | top: -.7em;
86 | display: inline-block;
87 | left: -.5em;
88 | letter-spacing: .15em;
89 | font-weight: bolder;
90 | font-size: 2.5em;
91 | opacity: .07;
92 | }
93 | [data-asset-type=js] .asset__type::before {
94 | content: '❮❯';
95 | }
96 | .asset__linkto {
97 | text-decoration: none;
98 | opacity: 0;
99 | transition: opacity .5s 0;
100 | padding: .3em;
101 | }
102 | .asset__linkto:hover,
103 | .asset__linkto:focus {
104 | opacity: .6 !important;
105 | }
106 | .asset__name:hover .asset__linkto {
107 | opacity: .3;
108 | }
109 | .asset__chart {
110 | min-height: 400px;
111 | width: 100%;
112 | }
113 | .trend {
114 | position: relative;
115 | top: 2px;
116 | float: right;
117 | margin-right: 1em;
118 | padding-right: 12px;
119 | padding-left: 12px;
120 | border-radius: 4px;
121 | background: rgba(255, 255, 255, .1);
122 | font-weight: normal;
123 | font-size: 20px;
124 | line-height: 32px;
125 | font-family: monospace;
126 | }
127 | .trend--down {
128 | background: #399914;
129 | }
130 | .trend--up {
131 | background: #99222F;
132 | }
133 |
134 | .table {
135 | text-align: left;
136 | width: 100%;
137 | }
138 | .table th,
139 | .table td {
140 | padding: .75em 1em;
141 | vertical-align: top;
142 | }
143 | .table tbody th,
144 | .table tbody td {
145 | border-top: 1px solid rgba(255, 255, 255, .1);
146 | }
147 | .table th:first-child {
148 | padding-left: 0;
149 | }
150 | .table__assetname a {
151 | color: #fff;
152 | text-decoration: none;
153 | }
154 | .table__assetresources a {
155 | text-decoration: none;
156 | font-family: Monaco, monospace;
157 | font-size: 14px;
158 | }
159 |
160 | .table__assetname a:hover,
161 | .table__assetname a:focus,
162 | .table__assetresources a:hover,
163 | .table__assetresources a:focus {
164 | text-decoration: underline;
165 | }
166 |
167 | .code {
168 | font-family: Monaco, monospace;
169 | font-size: 16px;
170 | }
171 |
172 | .user-select {
173 | -webkit-user-select: all;
174 | user-select: all;
175 | }
176 |
177 | .welcome-message {
178 | margin-top: .5em;
179 | font-size: 6em;
180 | line-height: 1;
181 | text-align: center;
182 | font-weight: 100;
183 | }
184 |
185 | .align-center {
186 | text-align: center;
187 | }
188 |
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaelig/moniteur/ea28dfd9b58a0febae0a598925e72e84bf9ff067/docs/screenshot.png
--------------------------------------------------------------------------------
/docs/welcome-process.key:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaelig/moniteur/ea28dfd9b58a0febae0a598925e72e84bf9ff067/docs/welcome-process.key
--------------------------------------------------------------------------------
/docs/welcome-process/welcome-process.001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaelig/moniteur/ea28dfd9b58a0febae0a598925e72e84bf9ff067/docs/welcome-process/welcome-process.001.png
--------------------------------------------------------------------------------
/docs/welcome-process/welcome-process.002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaelig/moniteur/ea28dfd9b58a0febae0a598925e72e84bf9ff067/docs/welcome-process/welcome-process.002.png
--------------------------------------------------------------------------------
/docs/welcome-process/welcome-process.003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaelig/moniteur/ea28dfd9b58a0febae0a598925e72e84bf9ff067/docs/welcome-process/welcome-process.003.png
--------------------------------------------------------------------------------
/docs/welcome-process/welcome-process.004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaelig/moniteur/ea28dfd9b58a0febae0a598925e72e84bf9ff067/docs/welcome-process/welcome-process.004.png
--------------------------------------------------------------------------------
/docs/welcome-process/welcome-process.005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaelig/moniteur/ea28dfd9b58a0febae0a598925e72e84bf9ff067/docs/welcome-process/welcome-process.005.png
--------------------------------------------------------------------------------
/fixtures/abc.js:
--------------------------------------------------------------------------------
1 | var foo = 'bar'
2 | export default foo
3 |
--------------------------------------------------------------------------------
/fixtures/main.css:
--------------------------------------------------------------------------------
1 | div {
2 | background: red;
3 | }
4 | h1 {
5 | color: black;
6 | }
7 | .element {
8 | font-size: 2em;
9 | }
--------------------------------------------------------------------------------
/fixtures/main2.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:"BentonSans";src:url("http://s1.ft-static.com/m/font/ft-velcro/bentonsans-regular.eot");src:url("http://s1.ft-static.com/m/font/ft-velcro/bentonsans-regular.eot?#iefix") format("embedded-opentype"),url("http://s1.ft-static.com/m/font/ft-velcro/bentonsans-regular.woff") format("woff");font-style:normal;font-weight:normal}@font-face{font-family:"BentonSans";src:url("http://s1.ft-static.com/m/font/ft-velcro/bentonsans-bold.eot");src:url("http://s1.ft-static.com/m/font/ft-velcro/bentonsans-bold.eot?#iefix") format("embedded-opentype"),url("http://s1.ft-static.com/m/font/ft-velcro/bentonsans-bold.woff") format("woff");font-style:normal;font-weight:bold}body{margin:0;width:100%;background-color:#fff1e0;font-size:16px;font-family:Arial,Helvetica,sans-serif;line-height:18px}.font-default{font-family:Arial,Helvetica,sans-serif}.font-editorial{font-family:Georgia,"Times New Roman",serif}.nojs .jsOnly{display:none}.forceUppercase{text-transform:uppercase}.forceLowercase{text-transform:lowercase}:link,:visited{text-decoration:none}ul,ol{list-style:none}h1,h2,h3,h4,h5,h6,pre,code{font-weight:normal;font-size:16px}ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,body,html,p,blockquote,fieldset,input{margin:0;padding:0}acronym,a img,:link img,:visited img,fieldset{border:0}address{font-style:normal}a{color:#2e6e9e;-webkit-font-smoothing:antialiased}a:hover{color:#000}textarea{font-family:Arial,Helvetica,sans-serif}button{cursor:pointer}.clearfix{display:inline-block}.clearfix:after{visibility:hidden;display:block;clear:both;height:0;content:""}/*\*/* html .clearfix{height:1%}.clearfix{display:block}/**/.clear{clear:both}.container{position:relative;margin:0 auto;padding:0 10px;width:973px;font-size:12px}.master-row{float:left;width:972px;z-index:1400}.master-column{float:left;width:972px}.middleSection{margin:12px 0 0;width:973px;z-index:1200;background:url("http://im.ft-static.com/m/img/railBg.gif") repeat-y}.noRailBg{background-image:none}.contentSection{margin-right:20px;width:600px}.msie div.contentSection{margin-top:-1px}.wideContentSection{width:972px}.editorialSection{width:600px}.wideContentSection .editorialSection{width:972px}.railSection{width:352px;background-color:#f6e9d8;font-family:BentonSans,Helvetica,Arial,sans-serif}.msie6 .railSection{margin-right:-3px}.marketingSection{padding-bottom:60px;width:621px}.wideContentSection .marketingSection{padding-bottom:60px;width:972px}.msie6 .railSection,.msie6 .editorialSection,.msie6 .contentSection{overflow:hidden}.topAds{position:absolute;top:3px;left:10px;z-index:10005}.wrapperContentTwoColumn .editorialSection{float:left;margin-right:28px;width:367px}.wrapperContentTwoColumn .marketingSection{float:left;width:213px}.topSection #header{position:relative;clear:both;z-index:300;margin:0;padding-top:10px}.no-banlb div .topSection #header{margin-top:8px}.topSection #header #searchbox{position:absolute;bottom:0;right:0;margin:0;width:270px;height:42px;overflow:hidden}.topSection .colright{position:relative;float:left;width:362px;height:90px}.msie .topSection .colright{margin-top:-1px;zoom:1}#ft-search{position:relative;float:right;margin:0;width:208px;z-index:1}#ft-search .searchContainer{float:left;width:138px;background:transparent}#ft-search .text{border:1px solid #f7ecdd;padding:4px;height:20px;width:130px;vertical-align:top;font-size:14px;background:white url("http://im.ft-static.com/m/img/blank.gif");z-index:1}#ft-search .ft-button{float:right;padding:0!important;width:63px!important;text-align:center}#ft-search .searchType{float:left;margin-right:5px}#ft-search .searchType a{color:#2e6e9e}#ft-search .searchType a:hover{color:#000;text-decoration:underline}#ft-search .searchType input{float:none;margin:-3px 2px -3px 0;padding:0;vertical-align:middle}.msi #ft-search .searchType input{vertical-align:10%}#ft-search .searchType.withRadios li{float:left;clear:left;line-height:14px;min-height:14px;vertical-align:top}#ft-search .searchType li.nojs{display:none}.msie #ft-search .searchType{margin-right:3px;margin-left:-4px}.nojs #ft-search li.js{display:none}.nojs #ft-search li.nojs{display:list-item;width:55px}#ftLogin{position:absolute;top:6px;right:219px;min-height:18px;width:345px;text-align:right;color:#777;font-size:14px;z-index:3}#ftLogin a{padding-left:.5em;color:#2e6e9e}#ftLogin a:hover{color:#000}#ftLogin .ft-link{margin-right:5px;padding:0}.msie6 #ftLogin{height:18px}#ftLogin-user{display:inline-block;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom;text-align:left}.ftLogin-loggedOut{display:block}.ftLogin-loggedIn{display:none}#ftLogin-box{display:none;position:absolute;right:150px;top:30px;padding:9px;text-align:left;background-image:url("http://im.ft-static.com/m/img/shadow-9x9.png");line-height:30px;vertical-align:middle;z-index:400}#ftLogin-box a{padding:0}#ftLogin-box form{position:relative;padding:0 10px 10px;width:300px;background-color:#fff}#ftLogin-box input{vertical-align:middle}#ftLogin-box .text{display:block;position:relative;margin-bottom:6px;border:1px solid #a7a49b;padding:6px;width:286px}#ftLogin-box .checkbox{margin:0 3px 0 0;padding:0}#ftLogin-box .ft-button{position:absolute;right:12px}.msie6 #ftLogin-box .ft-button,.msie7 #ftLogin-box .ft-button{width:64px!important;margin-top:-3px;right:0}.msie6 #ftLogin-box .ft-button{margin-top:-8px;right:16px}#ftLogin-box .closeButton{display:block;position:absolute;top:0;right:0;margin:10px 8px 0 0;height:12px;width:12px;background-image:url("http://im.ft-static.com/m/img/menu_close.gif");cursor:pointer}#ftLogin.open #ftLogin-box{display:block}.ftLogin-rememberMe{padding-bottom:41px}.ftLogin-rememberMe span{float:left}.ftLogin-rememberMe input.button{float:right}.msie6 .ftLogin-rememberMe,.msie7 .ftLogin-rememberMe{padding-bottom:27px}.ftLogin-cookiePolicy{line-height:18px;padding-bottom:3px}.ftLogin-forgotPassword{clear:both;padding-top:1em}#page-title{float:left;width:610px;padding-top:5px}#page-title .bar .bc{display:inline;float:left;margin-right:10px;font-size:16px;line-height:18px;color:#777;text-transform:lowercase}#page-title .bar .bc a,#page-title .bar .bc a:visited,#page-title .bar .bc a:link{color:#777}#page-title .bar .bc a:hover{color:#000}#page-title .bar .bc span{color:#000}.msie #page-title .bar .bc{color:#777}.msie #page-title .bar{zoom:1}#page-title h1,#page-title span.ftlogo{display:block;clear:left;padding:7px 0 1px}#page-title .pagename h1{display:inline;font-size:100%;padding:0;line-height:100%}#page-title span.timezone{float:left;margin-left:16px;padding-top:1px;color:#777}#page-title .section .heading{display:inline-block;background:url("http://im.ft-static.com/m/img/masthead_small.gif") no-repeat 0 2px}#page-title .section img{margin:2px 0 4px 0;visibility:hidden;width:164px;height:14px}.msie #page-title .section img{margin-bottom:2px}#page-title .section .bc{display:block;float:none;margin-bottom:6px;width:400px;font-size:16px;color:#000}#page-title .section .bc a{color:#777}#page-title .section .bc span{font-weight:bold;font-size:14px}#page-title .section .pagename{margin:0;padding:2px 0 6px;min-height:49px;font-size:42px;line-height:100%}#page-title .section .pagename .ftdotcom,#page-title .section .pagename .ftdotcom a,#page-title .section .pagename .ftdotcom a:hover{color:#a7a59b}#page-title .section .pagename a,#page-title .section .pagename a:hover{color:#000}#page-title .section .pagename i{font-size:12px}.msie6 #page-title .section .pagename{height:49px}.edition{float:left;position:relative;padding:1px 22px 0 0}.edition a{color:#2e6e9e}#editionChanger{float:right;position:absolute;right:0;top:2px;min-width:16px;min-height:16px;cursor:pointer}.nojs #editionChanger.basic,#editionChanger.enhanced{background:url("http://im.ft-static.com/m/img/menu_open.gif") right top no-repeat}#editionChanger p{display:none;position:relative;border:1px solid #e9decf;border-bottom:0;padding:6px 6px 0 6px;min-width:132px;color:#777;background-color:#fff;cursor:default}.nojs #editionChanger.basic:hover p{display:block}#editionChanger.open p{display:block}#editionChanger ul{display:none;padding:6px;width:132px;border:1px solid #e9decf;border-top:0;background-color:#fff;cursor:default}#editionChanger ul li{padding-left:22px}#editionChanger ul li.selected{font-weight:bold;color:#000;background:url("http://im.ft-static.com/m/img/menu_tick.gif") no-repeat 5px center}#editionChanger ul li a:hover{color:#000}#editionChanger.basic:hover ul{display:block}#editionChanger.open ul{display:block}#editionChanger span.closeButton{position:absolute;top:10px;right:10px;width:12px;height:12px;background-image:url("http://im.ft-static.com/m/img/menu_close.gif");cursor:pointer}.msie div#editionChanger{top:3px}.msie6 div#editionChanger{width:16px;height:16px}.msie6 div#editionChanger p{width:132px}.subscribeTile{float:right;margin-top:7px;font-size:14px;line-height:26px;text-align:right}.subscribeTile a{color:#2e6e9e}.subscribeTile a:hover{color:#000}.bottomSection{clear:left;position:relative}#renderPageOverlay{position:absolute;left:0;top:0;width:100%;height:100%;z-index:99000}#renderPageOverlay.loading{filter:alpha(opacity=30);opacity:.3;background:black url("http://im.ft-static.com/m/img/loading_big_white.gif") 470px 230px no-repeat}.roundedCorners{-moz-border-radius:8px;-webkit-border-radius:8px;border-radius:8px}.roundedCorners4-all{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.roundedCorners4-topleft{-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px}.roundedCorners4-topright{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px}.roundedCorners4-bottomleft{-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px}.roundedCorners4-bottomright{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px}.overlay{padding:8px;background-image:url("http://im.ft-static.com/m/img/overlayBg.png")}.overlay .innerBox{background-color:#fff}.overlayArrow{width:34px;height:21px;background:url("http://im.ft-static.com/m/img/overlay_arrow.png") no-repeat}.overlayTopArrow{background-position:0 0}.overlayBottomArrow{background-position:0 -21px}#dev-env{position:fixed;top:0;right:0;width:150px;background-color:#f00;z-index:1000;text-align:center}#dev-env .info{font-size:10px}.twitter-overlay-container{display:none}.nojs #navigation li a,.nojs #navigation li.on .subnav li a,#navigation li li a,#navigation li.on .subnav li li a{visibility:visible}.nojs #navigation .subnav .subnav{display:none}.nojs #navigation li{margin:0 7px}.msie7 #navigation{float:none}#navigation.basic li:hover .subnav{display:none}#navigation.basic li.on:hover .subnav{display:block}#navigation.basic li.on:hover .subnav .subnav{display:none}#navigation li a{visibility:hidden}#navigation{position:relative;float:left;clear:left;border-top:8px solid #9e2f50;border-bottom:12px solid #fff;padding:4px 0 0 0;width:972px;margin-bottom:19px;background-color:#fff;z-index:2;height:29px}#navigation li{float:left;margin:0;display:inline;font-size:14px;height:24px}#navigation li.first-child{margin-left:12px}#navigation li a{float:left;position:relative;padding:0 0 0 4px;color:#000;height:24px;line-height:24px}#navigation li a span{padding-right:3px}#navigation li.nosub a em{right:-5px;width:5px}#navigation li a em{display:block;position:absolute;right:-15px;top:0;margin-bottom:5px;padding:0;height:24px;width:15px;cursor:pointer}#navigation li a.on{background:#a7a59b left -240px no-repeat;color:#fff;padding-bottom:5px}#navigation li a.on em{background:white right -180px no-repeat}#navigation li.nosub a.on em{background:#a7a59b right -240px no-repeat;padding-bottom:5px}#navigation li a:hover{background:#e4eef5 left top no-repeat}#navigation li.navopen a,#navigation li.navopen a:hover{background:#4781aa left -60px no-repeat;color:#fff;border-bottom:2px solid #4781aa}#navigation li a:hover em{background:right top no-repeat}#navigation li a:hover em:hover{background:white right -60px no-repeat}#navigation li.navopen a:hover em,#navigation li.navopen a em{background:white right -180px no-repeat;border-bottom:2px solid #4781aa}#navigation li.nosub a:hover em{background:#e4eef5 right -120px no-repeat}#navigation li.navopen .subnav{display:block}#navigation>li.first-child>.subnav{margin-left:16px}#navigation>li.on>.subnav{margin-left:0}#navigation li.navopen .subnav a{border-bottom:0 none}#navigation li.navopen .subnav .subnav{display:none}#navigation li ul{display:none;position:absolute;top:30px;margin-left:0;border:2px solid #4781aa;border-bottom:0 none;padding:0;width:214px;background:#fff;z-index:2}.nojs #navigation li ul{top:28px}#navigation li ul li{display:block;float:none;clear:left;margin:0;border-top:1px solid #dfded8;padding:0;font-size:12px;line-height:13px;background:#fff none;height:auto}#navigation .on .nav-highlighted{padding:3px 10px 3px 9px}#navigation .nav-highlight-left,#navigation .nav-highlight-right{display:block;padding:0 0 0 30px;color:#FFF;background:transparent url("http://im.ft-static.com/m/img/nav/highlight-nav.png") no-repeat 0 0}#navigation .nav-highlight-right{padding:2px 4px 2px;background-position:100% 100%}#navigation .nav-highlight-dropdown{padding:0 20px 0 0;background-position:97% -283px}#navigation .subnav .nosub .nav-highlight-dropdown{padding:0;background:0}#navigation .navopen .nav-highlight-left,#navigation .navopen .nav-highlight-right,#navigation .navopen .nav-highlight-dropdown{padding:0;background:0;color:#777}#navigation .on .subnav .subnav li.dummy-child a.nav-highlighted,#navigation .on .subnav .subnav li.dummy-child a.nav-highlighted:hover{padding-right:63px}#navigation li .subnav li.last-child{border-bottom:2px solid #4781aa;margin-bottom:-4px}#navigation li.on .subnav li.last-child{border-bottom:0 none;margin-bottom:0}#navigation li .subnav .subnav li.last-child{border-bottom:2px solid #a7a59b;margin-bottom:-4px}#navigation li ul li a,#navigation .navopen .nav-highlighted{float:none;display:block;padding:10px 13px;height:auto;line-height:14px}#navigation li.navopen ul li a{background:#fff none;color:#777}#navigation li.navopen ul li a:hover{background:#efeeeb none;color:#777;border-bottom:0 none}#navigation li ul li.first-child,#navigation li ul li.first-child a{border-top:0;margin-left:0}#navigation .on a:hover{background-color:#a7a59b}#navigation .on em{background-color:#a7a59b}#navigation .on .subnav{display:block;visibility:visible;padding:0;background-color:#a7a59b;left:0;width:972px;top:32px;border:0 none;position:absolute;z-index:1;margin-left:0}#navigation .on .subnav li{float:left;clear:none;background-color:#fff;color:#fff;border:0 none;margin:0;position:relative}#navigation .on a{margin-left:-3px;margin-right:0;padding-left:7px;padding-right:0}#navigation .on li a{color:#fff;width:auto;padding:5px 22px 5px 9px;margin:0}#navigation .on li.last-child{float:right}#navigation .on li.tools{float:right}#navigation .on li.tools .subnav{margin-left:-156px}#navigation .on li.tools .subnav li.dummy-child{right:-2px;left:auto}#navigation li.on li.nosub a:hover.rss{background-image:url("http://im.ft-static.com/m/img/nav/rss_link_nav.gif")}#navigation .on li.tools .subnav li.section a{color:#2e6e9e}#navigation li.on a:hover{background:#a7a59b left -240px no-repeat}#navigation li.on a:hover em{background:#a7a59b right -240px no-repeat}#navigation li.on li a{background:#a7a59b right -279px no-repeat;color:#fff}#navigation li.on li a:hover{background:#74736c right -303px no-repeat;color:#fff}#navigation li.on li.on a{background:#4780ab right -327px no-repeat;color:#fff}#navigation li.on li.rss a{padding:5px 8px 0 2px}#navigation li.on li.rss a:hover{background-color:#a7a59b}#navigation li.on li li a,#navigation li.on li li a:hover,#navigation li.on li.on li a,#navigation li.on li.on li a:hover{background:#fff none}#navigation li.on li.on{padding-bottom:0}#navigation .on .subnav .subnav li.dummy-child{position:absolute;top:-23px;left:-2px;border:2px solid #a7a59b;border-bottom:0 none;padding:0 3px;background-color:#fff;height:21px;z-index:2}#navigation .on .subnav .subnav li.dummy-child a:hover,#navigation .on .subnav .subnav li.dummy-child a{padding:4px 17px 2px 4px;background:white right -352px no-repeat}#navigation .on .subnav .subnav li.dummy-child:hover,#navigation .on .subnav .subnav li.dummy-child:hover a,#navigation .on .subnav .subnav li.dummy-child a:hover{background-color:#fff}#navigation .on .subnav .subnav{display:none;width:214px;top:21px;background-color:#fff;border:2px solid #a7a59b;padding:0}#navigation .on .subnav li.hover .subnav,#navigation .on .subnav li:hover .subnav{display:block}.editor #navigation .on .subnav li.hover .subnav,.editor #navigation .on .subnav li:hover .subnav{display:none}#navigation .on .subnav .subnav li{float:none;clear:left;font-weight:normal;padding:4px 2px;margin:0;border-top:1px solid #dfded8}#navigation .on .subnav .subnav li.first-child{border-top:0 none}#navigation .on .subnav .subnav li a{color:#777;padding:5px}#navigation .on .subnav .subnav li.hover,#navigation .on .subnav .subnav li.hover a,#navigation .on .subnav .subnav li:hover,#navigation .on .subnav .subnav li:hover a,#navigation .on .subnav .subnav li a:hover{color:#777;background-color:#efeeeb}#navigation .on .subnav .subnav li.dummy-child,#navigation .on .subnav .subnav li.dummy-child a{background-color:#fff}#navigation .on .subnav .subnav li a:hover{padding:5px;border:0 none}#navigation .subnav li.nosub:hover .subnav,#navigation .subnav li.nosub.hover .subnav{display:none}#navigation .on .nodisp{display:none}#navigation .subnav .tr,#navigation .subnav .tl,#navigation .subnav .bl,#navigation .subnav .br{position:absolute;height:5px;width:5px;z-index:2}#navigation .subnav .tr,#navigation .on .subnav .tr{right:-2px;top:-2px;background:white -5px 0 no-repeat}#navigation .subnav li:hover .tr,#navigation .subnav li.hover .tr{background-position:-5px -10px}#navigation .last-child .subnav .tr,#navigation .on .last-child .subnav .tr{display:none}#navigation .last-child .subnav .tl,#navigation .on .last-child .subnav .tl{top:-2px;left:-2px;background:#fff 0 0 no-repeat}#navigation .last-child .subnav li:hover .tl,#navigation .last-child .subnav li.hover .tl{background-position:0 -10px}#navigation .subnav .bl,#navigation .on .subnav .bl{bottom:-4px;left:-2px;background:transparent 0 -5px no-repeat}#navigation .subnav .br,#navigation .on .subnav .br{right:-2px;bottom:-4px;background:transparent -5px -5px no-repeat}#navigation .subnav .subnav .bl,#navigation .on .subnav .subnav .bl,#navigation .subnav .subnav .br,#navigation .on .subnav .subnav .br{bottom:-2px}#navigation .subnav li:hover .bl,#navigation .subnav li.hover .bl{background-position:0 -15px}#navigation .subnav li:hover .br,#navigation .subnav li.hover .br{background-position:-5px -15px}.msie #navigation li .subnav li.last-child{position:relative;z-index:2}.msie #navigation .subnav li.last-child .bl,.msie #navigation .subnav li.last-child .br{bottom:-2px}#navigation .on .subnav .tr,#navigation .on .subnav .tl,#navigation .on .subnav .bl,#navigation .on .subnav .br{display:none}#navigation .on .subnav .subnav .tr{display:none}#navigation .on .subnav .subnav .bl{display:block;background-position:0 -23px}#navigation .on .subnav .subnav .br{display:block;background-position:-5px -23px}#navigation .on .subnav .subnav li:hover .bl{background-position:0 -28px}#navigation .on .subnav .subnav li:hover .br{background-position:-5px -28px}#navigation .on .subnav .subnav li.on .bl{background-position:0 -33px}#navigation .on .subnav .subnav li.on .br{background-position:-5px -33px}#navigation .on .subnav .subnav .dummy-child .tl{display:block;background:white 0 -20px no-repeat;width:2px;height:2px;top:0;left:0}#navigation .on .subnav .subnav .dummy-child .tr{display:block;background-position:-8px -20px;width:2px;height:2px;width:2px;height:2px;top:0;right:0}#navigation .on .subnav .subnav .dummy-child .bl,#navigation .on .subnav .subnav .dummy-child .br{display:none}#navigation .subnav .bl,#navigation .on .subnav .bl,#navigation .subnav .br,#navigation .on .subnav .br,#navigation .last-child .subnav .tl,#navigation .on .last-child .subnav .tl,#navigation .subnav .tr,#navigation .on .subnav .tr,#navigation .on .subnav .subnav .dummy-child .tl{background-image:url("http://im.ft-static.com/m/img/nav/navigation_subnav.gif")}#navigation li a.on,#navigation li a.on em,#navigation li a:hover em,#navigation li a:hover,#navigation li a:hover em:hover,#navigation li.nosub a.on em,#navigation li.nosub a:hover em,#navigation li.navopen a,#navigation li.navopen a:hover,#navigation li.navopen a em,#navigation li.navopen a:hover em,#navigation li.on a:hover,#navigation li.on a:hover em,#navigation li.on li a,#navigation li.on li a:hover,#navigation li.on li.on a,#navigation .on .subnav .subnav li.dummy-child a:hover,#navigation .on .subnav .subnav li.dummy-child a,#navigation .nav-highlight-dropdown{background-image:url("http://im.ft-static.com/m/img/nav/nav.gif")}#navigation li.on li.nosub a,#navigation li.on li.nosub a:hover{background-image:none;padding-right:10px}#navigation .on .subnav .subnav li.on,#navigation .on .subnav .subnav li.on a{background-color:#4780ab;color:#fff;background-image:none}.msie6 #navigation .bl,.msie6 #navigation .br,.msie6 #navigation .tl,.msie6 #navigation .tr{font-size:0;line-height:0}.msie6 #navigation li.navopen ul li,.msie6 #navigation .subnav .subnav li{float:left;width:100%}.msie6 #navigation .subnav .br,.msie6 #navigation .subnav .bl{margin-bottom:0}.msie6 #navigation .subnav .subnav .br,.msie6 #navigation .subnav .subnav .bl{margin-bottom:-1px}.msie6 #navigation .subnav li.last-child{margin-bottom:-5px}.msie6 #navigation .subnav li.last-child a{padding-bottom:11px}.msie6 #navigation .on .subnav li.last-child a{padding-bottom:5px}.msie6 #navigation .subnav .subnav li.last-child{margin-bottom:-4px}.msie6 #navigation .subnav .subnav li.last-child a{padding-bottom:5px}.msie6 #navigation .on .subnav .subnav .dummy-child{position:relative;width:auto;margin-bottom:-23px}.msie6 #navigation .on .subnav .subnav .dummy-child a{padding-bottom:4px}.msie6 #navigation .on .subnav .last-child .subnav .dummy-child{margin-left:78px}.msie6 #navigation .subnav .last-child .subnav{margin-left:-160px}.jobsBoxSearchButton span,.ePaperButton span,.ePaperButton span span,.linkButton a,.linkButton form,.linkButton a span,.linkButton form span{background-image:url("http://im.ft-static.com/m/img/button_sprite.png")}.promobox,.pocket{background-image:url("http://im.ft-static.com/m/img/fade.gif")}.button-pdf .pdf-icon{background-image:url("http://im.ft-static.com/m/img/pdf-icon.gif")}.conkerOverlay{position:absolute;top:0;left:0;width:100%;height:100%;background:#000;filter:alpha(opacity=15);opacity:.15;z-index:9998}.conkerContainer{position:absolute;top:55px;left:355px;padding:12px;width:480px;background-image:url("http://im.ft-static.com/m/img/shadow-9x9.png");font-family:Arial,Helvetica,sans-serif;z-index:9999}.conkerFrame{float:left;padding-bottom:20px;width:480px;background:white url(http://media.ft.com/FTCOM/Images/warningExclamationBlack.gif) 23px 73px no-repeat}.conkerFrame h2{clear:left;margin:20px 25px 20px 25px;border-bottom:1px solid #a7a59b;padding:0 0 8px 0;font-size:17px;font-weight:bold}.conkerFrame p{margin:10px 25px 15px 110px;font-weight:bold;font-size:14px;line-height:1.3em}.conkerFrame a{color:#2e6e9e;text-decoration:none}.conkerFrame a:hover{color:#000}.ft-search-autocomplete-placeholder{font-size:10px!important;line-height:20px;color:#999}.ft-autocomplete{position:absolute;display:none;top:34px;width:100%;border:1px solid #999;background-color:#fff;z-index:3}.ft-autocomplete.open{display:block}.ft-autocomplete-heading{display:block;padding:0 12px;font-size:11px;text-transform:uppercase;color:#fff;background-color:#a7a59b}.ft-autocomplete-item{padding:0}.ft-autocomplete-item a{display:block;position:relative;padding:6px 12px;cursor:pointer;color:#2e6e9e}.ft-autocomplete-item-alt a{background-color:#f8f8f8}.ft-autocomplete-news .ft-autocomplete-item a{font-size:14px;line-height:17px}.ft-autocomplete-quotes .ft-autocomplete-item a{font-size:12px;line-height:15px}.ft-autocomplete-item.highlight a{color:#FFF;background-color:#2e6e9e}.ft-autocomplete-item.highlight .detail{color:#FFF}.ft-autocomplete-item .match{font-weight:bold}.ft-autocomplete-item .detail{position:relative;float:right;margin-left:10px;padding-left:23px;font-size:11px;text-align:left;color:#000;white-space:nowrap;overflow:hidden}.ft-autocomplete-item .wsod-flag{position:absolute;top:2px;left:0}.ft-autocomplete-morelink a{padding-right:12px;text-align:center}.wsod-flag{height:11px;width:16px;background:url("http://im.ft-static.com/m/img/country-flag.bg.gif") no-repeat 50px 50px;overflow:hidden}.flag-ad{background-position:0 0}.flag-ae{background-position:-16px 0}.flag-af{background-position:-32px 0}.flag-ag{background-position:-48px 0}.flag-ai{background-position:-64px 0}.flag-al{background-position:-80px 0}.flag-am{background-position:-96px 0}.flag-an{background-position:-112px 0}.flag-ao{background-position:-128px 0}.flag-ar{background-position:-144px 0}.flag-as{background-position:-160px 0}.flag-at{background-position:-176px 0}.flag-au{background-position:-192px 0}.flag-aw{background-position:-208px 0}.flag-ax{background-position:-224px 0}.flag-az{background-position:-240px 0}.flag-ba{background-position:-256px 0}.flag-bb{background-position:-272px 0}.flag-bd{background-position:-288px 0}.flag-be{background-position:0 -11px}.flag-bf{background-position:-16px -11px}.flag-bg{background-position:-32px -11px}.flag-bh{background-position:-48px -11px}.flag-bi{background-position:-64px -11px}.flag-bj{background-position:-80px -11px}.flag-bm{background-position:-96px -11px}.flag-bn{background-position:-112px -11px}.flag-bo{background-position:-128px -11px}.flag-br{background-position:-144px -11px}.flag-bs{background-position:-160px -11px}.flag-bt{background-position:-176px -11px}.flag-bv{background-position:-192px -11px}.flag-bw{background-position:-208px -11px}.flag-by{background-position:-224px -11px}.flag-bz{background-position:-240px -11px}.flag-ca{background-position:-256px -11px}.flag-catalonia{background-position:-272px -11px}.flag-cc{background-position:-288px -11px}.flag-cd{background-position:0 -22px}.flag-cf{background-position:-16px -22px}.flag-cg{background-position:-32px -22px}.flag-ch{background-position:-48px -22px}.flag-ci{background-position:-64px -22px}.flag-ck{background-position:-80px -22px}.flag-cl{background-position:-96px -22px}.flag-cm{background-position:-112px -22px}.flag-cn{background-position:-128px -22px}.flag-co{background-position:-144px -22px}.flag-cr{background-position:-160px -22px}.flag-cs{background-position:-176px -22px}.flag-cu{background-position:-192px -22px}.flag-cv{background-position:-208px -22px}.flag-cx{background-position:-224px -22px}.flag-cy{background-position:-240px -22px}.flag-cz{background-position:-256px -22px}.flag-de{background-position:-272px -22px}.flag-dj{background-position:-288px -22px}.flag-dk{background-position:0 -33px}.flag-dm{background-position:-16px -33px}.flag-do{background-position:-32px -33px}.flag-dz{background-position:-48px -33px}.flag-ec{background-position:-64px -33px}.flag-ee{background-position:-80px -33px}.flag-eg{background-position:-96px -33px}.flag-eh{background-position:-112px -33px}.flag-england{background-position:-128px -33px}.flag-er{background-position:-144px -33px}.flag-es{background-position:-160px -33px}.flag-et{background-position:-176px -33px}.flag-europeanunion{background-position:-192px -33px}.flag-fam{background-position:-208px -33px}.flag-fi{background-position:-224px -33px}.flag-fj{background-position:-240px -33px}.flag-fk{background-position:-256px -33px}.flag-fm{background-position:-272px -33px}.flag-fo{background-position:-288px -33px}.flag-fr{background-position:0 -44px}.flag-ga{background-position:-16px -44px}.flag-gb{background-position:-32px -44px}.flag-gd{background-position:-48px -44px}.flag-ge{background-position:-64px -44px}.flag-gf{background-position:-80px -44px}.flag-gh{background-position:-96px -44px}.flag-gi{background-position:-112px -44px}.flag-gl{background-position:-128px -44px}.flag-gm{background-position:-144px -44px}.flag-gn{background-position:-160px -44px}.flag-gp{background-position:-176px -44px}.flag-gq{background-position:-192px -44px}.flag-gr{background-position:-208px -44px}.flag-gs{background-position:-224px -44px}.flag-gt{background-position:-240px -44px}.flag-gu{background-position:-256px -44px}.flag-gw{background-position:-272px -44px}.flag-gy{background-position:-288px -44px}.flag-hk{background-position:0 -55px}.flag-hm{background-position:-16px -55px}.flag-hn{background-position:-32px -55px}.flag-hr{background-position:-48px -55px}.flag-ht{background-position:-64px -55px}.flag-hu{background-position:-80px -55px}.flag-id{background-position:-96px -55px}.flag-ie{background-position:-112px -55px}.flag-il{background-position:-128px -55px}.flag-in{background-position:-144px -55px}.flag-io{background-position:-160px -55px}.flag-iq{background-position:-176px -55px}.flag-ir{background-position:-192px -55px}.flag-is{background-position:-208px -55px}.flag-it{background-position:-224px -55px}.flag-jm{background-position:-240px -55px}.flag-jo{background-position:-256px -55px}.flag-jp{background-position:-272px -55px}.flag-ke{background-position:-288px -55px}.flag-kg{background-position:0 -66px}.flag-kh{background-position:-16px -66px}.flag-ki{background-position:-32px -66px}.flag-km{background-position:-48px -66px}.flag-kn{background-position:-64px -66px}.flag-kp{background-position:-80px -66px}.flag-kr{background-position:-96px -66px}.flag-kw{background-position:-112px -66px}.flag-ky{background-position:-128px -66px}.flag-kz{background-position:-144px -66px}.flag-la{background-position:-160px -66px}.flag-lb{background-position:-176px -66px}.flag-lc{background-position:-192px -66px}.flag-li{background-position:-208px -66px}.flag-lk{background-position:-224px -66px}.flag-lr{background-position:-240px -66px}.flag-ls{background-position:-256px -66px}.flag-lt{background-position:-272px -66px}.flag-lu{background-position:-288px -66px}.flag-lv{background-position:0 -77px}.flag-ly{background-position:-16px -77px}.flag-ma{background-position:-32px -77px}.flag-mc{background-position:-48px -77px}.flag-md{background-position:-64px -77px}.flag-me{background-position:-80px -77px}.flag-mg{background-position:-96px -77px}.flag-mh{background-position:-112px -77px}.flag-mk{background-position:-128px -77px}.flag-ml{background-position:-144px -77px}.flag-mm{background-position:-160px -77px}.flag-mn{background-position:-176px -77px}.flag-mo{background-position:-192px -77px}.flag-mp{background-position:-208px -77px}.flag-mq{background-position:-224px -77px}.flag-mr{background-position:-240px -77px}.flag-ms{background-position:-256px -77px}.flag-mt{background-position:-272px -77px}.flag-mu{background-position:-288px -77px}.flag-mv{background-position:0 -88px}.flag-mw{background-position:-16px -88px}.flag-mx{background-position:-32px -88px}.flag-my{background-position:-48px -88px}.flag-mz{background-position:-64px -88px}.flag-na{background-position:-80px -88px}.flag-nc{background-position:-96px -88px}.flag-ne{background-position:-112px -88px}.flag-nf{background-position:-128px -88px}.flag-ng{background-position:-144px -88px}.flag-ni{background-position:-160px -88px}.flag-nl{background-position:-176px -88px}.flag-no{background-position:-192px -88px}.flag-np{background-position:-208px -88px}.flag-nr{background-position:-224px -88px}.flag-nu{background-position:-240px -88px}.flag-nz{background-position:-256px -88px}.flag-om{background-position:-272px -88px}.flag-pa{background-position:-288px -88px}.flag-pe{background-position:0 -99px}.flag-pf{background-position:-16px -99px}.flag-pg{background-position:-32px -99px}.flag-ph{background-position:-48px -99px}.flag-pk{background-position:-64px -99px}.flag-pl{background-position:-80px -99px}.flag-pm{background-position:-96px -99px}.flag-pn{background-position:-112px -99px}.flag-pr{background-position:-128px -99px}.flag-ps{background-position:-144px -99px}.flag-pt{background-position:-160px -99px}.flag-pw{background-position:-176px -99px}.flag-py{background-position:-192px -99px}.flag-qa{background-position:-208px -99px}.flag-re{background-position:-224px -99px}.flag-ro{background-position:-240px -99px}.flag-rs{background-position:-256px -99px}.flag-ru{background-position:-272px -99px}.flag-rw{background-position:-288px -99px}.flag-sa{background-position:0 -110px}.flag-sb{background-position:-16px -110px}.flag-sc{background-position:-32px -110px}.flag-scotland{background-position:-48px -110px}.flag-sd{background-position:-64px -110px}.flag-se{background-position:-80px -110px}.flag-sg{background-position:-96px -110px}.flag-sh{background-position:-112px -110px}.flag-si{background-position:-128px -110px}.flag-sj{background-position:-144px -110px}.flag-sk{background-position:-160px -110px}.flag-sl{background-position:-176px -110px}.flag-sm{background-position:-192px -110px}.flag-sn{background-position:-208px -110px}.flag-so{background-position:-224px -110px}.flag-sr{background-position:-240px -110px}.flag-st{background-position:-256px -110px}.flag-sv{background-position:-272px -110px}.flag-sy{background-position:-288px -110px}.flag-sz{background-position:0 -121px}.flag-tc{background-position:-16px -121px}.flag-td{background-position:-32px -121px}.flag-tf{background-position:-48px -121px}.flag-tg{background-position:-64px -121px}.flag-th{background-position:-80px -121px}.flag-tj{background-position:-96px -121px}.flag-tk{background-position:-112px -121px}.flag-tl{background-position:-128px -121px}.flag-tm{background-position:-144px -121px}.flag-tn{background-position:-160px -121px}.flag-to{background-position:-176px -121px}.flag-tr{background-position:-192px -121px}.flag-tt{background-position:-208px -121px}.flag-tv{background-position:-224px -121px}.flag-tw{background-position:-240px -121px}.flag-tz{background-position:-256px -121px}.flag-ua{background-position:-272px -121px}.flag-ug{background-position:-288px -121px}.flag-um{background-position:0 -132px}.flag-us{background-position:-16px -132px}.flag-uy{background-position:-32px -132px}.flag-uz{background-position:-48px -132px}.flag-va{background-position:-64px -132px}.flag-vc{background-position:-80px -132px}.flag-ve{background-position:-96px -132px}.flag-vg{background-position:-112px -132px}.flag-vi{background-position:-128px -132px}.flag-vn{background-position:-144px -132px}.flag-vu{background-position:-160px -132px}.flag-wales{background-position:-176px -132px}.flag-wf{background-position:-192px -132px}.flag-ws{background-position:-208px -132px}.flag-ye{background-position:-224px -132px}.flag-yt{background-position:-240px -132px}.flag-za{background-position:-256px -132px}.flag-zm{background-position:-272px -132px}.flag-zw{background-position:-288px -132px}.box-topDivider-container{float:left;clear:left;border-top:8px solid #a7a59b;width:342px;margin-bottom:32px;padding-top:3px}.box-topDivider-container.advertising{margin-bottom:13px}.box-topDivider-content{padding:16px 0 0 0}.msie6 .box-topDivider-container,.msie7 .box-topDivider-container{margin-bottom:29px;padding-top:5px}.msie .box-topDivider-container.advertising{margin-bottom:28px}.msie .box-topDivider-content{padding-top:13px}.heading-simple-large{font-size:18px;line-height:22px;margin-top:2px}.heading-simple-large a{color:#000}.heading-simple-large a:hover{color:#2e6e9e}.heading-simple-normal{font-size:16px;margin-top:4px}.heading-simple-normal a{color:#000}.heading-simple-normal a:hover{color:#2e6e9e}.brandPagePromo img.brandPagePromoImage{float:right;padding:0 0 14px 12px}.brandPagePromo .twitterFollowButton{padding-top:13px}.brandPagePromo .box-topDivider-content{padding-left:1px;line-height:17px}#footer{width:972px;background-color:#fff1e0}#footer div{border-top:solid 9px #e9decf;padding:14px 0 0 0}#footer span{color:#999;line-height:1}#footer .copyright{color:#000;text-transform:uppercase}#footer ul{margin:0 0 2px 0}#footer ul li{display:inline;margin:2px 3px 0 0}#footer ul li a{display:inline;margin:0 0 0 5px;color:#2e6e9e}#footer ul li:first-child a{margin-left:0}#footer ul li a:hover{color:#000}.msie6 #footer ul{margin-left:-0.5em}#footer div.pearson{height:18px;background-color:#e9decf;padding:13px 17px 13px 16px;margin-top:14px;border:0}#footer div.pearson div{border:0;padding:0}#footer div.pearson .tagline{width:139px;height:10px;float:left;margin-top:4px;background:url("http://im.ft-static.com/m/img/pearson_always_learning_small.gif") no-repeat}#footer div.pearson .companyName{width:114px;height:18px;float:right;background:url("http://im.ft-static.com/m/img/pearson_logo_small.gif") no-repeat}span.offscreen{position:absolute;left:-5000px;top:auto;width:1px;height:1px;overflow:hidden}#deviceSwitchingLink{background:url("http://im.ft-static.com/m/img/mobile-icon.gif") no-repeat 0 1px transparent;padding-left:13px;position:absolute;right:0;top:23px}.msie #deviceSwitchingLink{background-position:0 2px}.hidden,#page-title .section .hidden{display:none}#print-footer{display:none}#print-footer p{margin-bottom:5px}#print-footer .company{text-transform:uppercase}#print-footer #print-from{margin-bottom:20px}@media print{body{background-color:transparent;width:auto}#page-container{padding-right:0;padding-left:0;z-index:1000}.storyvideo,#DRMUpsell,#header .pagename,#header .colright,#page-title .section .bc,#navigation,.railSection,.storyTools,div.topAds,.topAds,#mpu,.adRequests,div.marketingSection,#footer,#renderPageOverlay,.bottomSection,#inferno-input,#inferno-comments,#ft-article-comments,.ft-livefyre-comments,#print-footer .links,.story-package,#annotationstool,.assankainpagemarkerwrapper,.insideArticleShare,#expandableimage .enlargeIcon{display:none}#page-title .section .heading{display:inline-block}.promobox{background:0;border-top-color:#ccc}#print-footer{display:block;font-size:75%}div#mpu,div#mpu2,div#mpu3{width:1px;height:1px;overflow:hidden;visibility:hidden;min-height:1px}div.topSection #header,div.topSection #page-title,div.topSection #page-title .bar,div.topSection #header img{margin:0;padding:0}.overlay{padding-left:0}div.topSection #header{border-bottom:4px solid #9e2f50;padding-bottom:8px;margin-bottom:16px}div.topSection #header img{width:auto;height:auto;visibility:visible}#page-title .section .heading{background-image:none}#footer #content{border-top:0 none;padding-top:5px}#footer span{color:#000}.bottomSection .freestyle{margin-top:75px}div.container,div.middleSection,div.contentSection,div.editorialSection,div.master-row,div.wideContentSection,div.wideContentSection .editorialSection{width:auto;float:none}div.contentSection{border-right:0 none}#publicationDate{color:#000}.fullstoryHeader{padding-left:0;padding-top:0}.fullstoryBody{border-top-color:#999;padding-left:0;padding-right:0;padding-bottom:0;margin-left:0}a,a:hover,a:link,a:visited,a:active{color:#000;text-decoration:none}.screen-copy{display:none}.slideshow-viewer{display:none}.slideshow-head ul{display:none}.slideshow .slideshow-slide img{display:block}.slideshow .slideshow-caption{display:block}.fullstoryBody caption{background-color:#fff;color:#333}}.ft-badge{display:inline-block;border-radius:5px;padding:2px 4px 0 4px;font-family:BentonSans,Helvetica,Arial,sans-serif;font-size:10px;font-weight:normal;line-height:13px;letter-spacing:1px;text-transform:uppercase}.ft-badge-red{background-color:#c00;color:#fff}.ft-button{display:inline-block;margin:0;padding:0 8px!important;border-width:0!important;border-radius:.3em;width:auto!important;background:#4e8fbd;background:-webkit-gradient(linear,left top,left bottom,from(#549ccf),to(#4782ab));background:-moz-linear-gradient(#549ccf,#4782ab);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#549ccf,endColorstr=#4782ab);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#549ccf, endColorstr=#4782ab)";zoom:1;box-shadow:inset 0 1px 0 #90bfe0;-moz-box-shadow:inset 0 1px 0 #90bfe0;-webkit-box-shadow:inset 0 1px 0 #90bfe0;font-family:Arial,Helvetica,sans-serif;color:#fff;text-shadow:0 0 0 #FFF;text-decoration:none;white-space:nowrap;cursor:pointer}.ft-button:hover{color:#ddd}.ft-button-large{min-height:30px;line-height:30px;font-size:16px}.ft-button-medium{min-height:24px;line-height:24px;font-size:14px}.ft-button-small{min-height:16px;line-height:16px;font-size:12px}.ft-button-negative{background:#b7315c;background:-webkit-gradient(linear,left top,left bottom,from(#c36),to(#9f2f50));background:-moz-linear-gradient(#c36,#9f2f50);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#cc3366,endColorstr=#9f2f50);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#cc3366, endColorstr=#9f2f50)";box-shadow:inset 0 1px 0 #d89aad;-moz-box-shadow:inset 0 1px 0 #d89aad;-webkit-box-shadow:inset 0 1px 0 #d89aad}.ft-button.ft-state-disabled,.ft-button[disabled]{background:#acacac;background:-webkit-gradient(linear,left top,left bottom,from(#b5b5b5),to(#a5a5a5));background:-moz-linear-gradient(#b5b5b5,#a5a5a5);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#b5b5b5,endColorstr=#a5a5a5);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#b5b5b5, endColorstr=#a5a5a5)";box-shadow:inset 0 1px 0 #a5a5a5;-moz-box-shadow:inset 0 1px 0 #a5a5a5;-webkit-box-shadow:inset 0 1px 0 #a5a5a5;cursor:default;color:#fff}.ft-divider{clear:both;margin:0;border-width:0;border-radius:4px;height:9px;background-color:#e9decf}.ft-footer-small{clear:left;height:1px;background:url("http://im.ft-static.com/m/img/ft-velcro/border-horizontal-dotted.png") bottom left repeat-x}.ft-header-small{position:relative;border-radius:4px;background-color:#e9decf;height:9px;line-height:8px;font-size:14px}.ft-header-small-title{display:inline-block;padding:0 10px;font-family:BentonSans,Helvetica,Arial,sans-serif;font-weight:bold;font-size:12px;line-height:10px;background-color:#fff1e0;color:#333}.msie7 .ft-header-small-title{display:inline;zoom:1}.msie6 .ft-header-small-title{position:static;background-color:transparent}.ft-header-small-titleleft{border-top-left-radius:0;border-bottom-left-radius:0}.ft-header-small-titleleft .ft-header-small-title{padding-left:0}.ft-header-small-titlecenter{text-align:center}.ft-header-medium{clear:left;font-family:BentonSans,Helvetica,Arial,sans-serif;overflow:hidden;border-top:4px solid #a7a59b;background:url("http://im.ft-static.com/m/img/ft-velcro/border-horizontal-dotted.png") bottom left repeat-x}.ft-header-medium-title{float:left;margin:8px 0 8px 10px;font-size:12px;line-height:16px;font-weight:bold;font-style:normal;color:#434343}a.ft-header-medium-title:hover{color:#2e6e9e}.msie6 .ft-header-medium{height:34px;overflow:visible}.msie6 .ft-header-medium-title{display:inline}.msie7 .ft-header-medium{min-height:34px}.ft-header-medium-tabs{border-top:0;background-color:#e9decf;background-image:none}.ft-header-medium-tint{background-color:#e9decf}.ft-tabs-large{background-image:none}.ft-tab-large{float:left;margin-left:2px;border-top:4px solid #a8a79c;width:175px;text-align:center;background:#e9decf url("http://im.ft-static.com/m/img/ft-velcro/border-horizontal-dotted.png") bottom left repeat-x}.ft-tab-large:first-child{margin-left:0}.msie6 .ft-tab-large{margin-left:0;width:176px}.ft-tab-title{display:block;white-space:nowrap;color:#434343;text-overflow:ellipsis}.ft-tab-title img{vertical-align:middle}a.ft-tab-title:hover{color:#2e6e9e}.ft-tabs-large .ft-tab-title{font-size:12px;line-height:34px;font-weight:bold;opacity:.5}.ft-tabs-large .ft-state-selected{border-top-color:#a7294d;background:#f6e9d8}.ft-tabs-large .ft-state-selected .ft-tab-title{color:#333;opacity:1}.ft-tabs-large .ft-state-selected .ft-tab-title:hover{cursor:text}.ft-tabs-small{padding-left:20px;background:url("http://im.ft-static.com/m/img/ft-velcro/border-horizontal-dotted.png") bottom left repeat-x;overflow:hidden}.ft-header-medium-title+.ft-tabs-small{float:right}.ft-tabs-large+.ft-tabs-small{clear:left;background-color:#f6e9d8}.msie6 .ft-tabs-small{zoom:1}.ft-tab-small{float:left;margin-right:10px}.ft-tabs-small .ft-tab-title{font-size:11px;line-height:34px}.ft-tabs-small .ft-state-selected{background:url("http://im.ft-static.com/m/img/ft-velcro/mod-tab-selected.gif") bottom center no-repeat}.ft-tabs-small .ft-state-selected .ft-tab-title{color:#000}.ft-tabs-small .ft-state-selected .ft-tab-title:hover{cursor:text}.ft-heading{font-family:BentonSans,Helvetica,Arial,sans-serif;color:#000}.ft-heading a{color:#2e6e9e}.ft-heading a:hover{color:#000}.ft-heading-medium{text-transform:uppercase;font-size:12px;font-weight:bold;line-height:14px}.ft-heading-small{font-size:14px;line-height:18px}.ft-image{display:inline-block;position:relative;margin:0;padding:0;overflow:hidden}.msie7 .ft-image{float:left;display:inline}.ft-image-article{width:600px;height:338px}.ft-image-primary{width:272px;height:153px}.ft-image-secondary{width:167px;height:94px}.ft-image-tertiary{width:114px;height:64px}.ft-image-leader{width:414px;height:233px}.ft-image-fullwidth{width:972px;height:547px}.ft-overlay{position:absolute;padding:5px 10px}.ft-overlay-topleft{top:0;left:0}.ft-overlay-bottom{bottom:0;left:0;right:0}.ft-overlay-bottomright-credit{bottom:0;right:0;padding:2px 2px 1px 2px}.ft-overlay-bg20{background-color:black;background-color:rgba(0,0,0,0.2)}.ft-overlay-bg30{background-color:black;background-color:rgba(0,0,0,0.3)}.ft-overlay-bg50{background-color:black;background-color:rgba(0,0,0,0.5)}.ft-overlay-heading{font-family:BentonSans,Helvetica,Arial,sans-serif;font-size:11px;font-weight:bold;line-height:14px;color:#fff}.ft-overlay-title{font-family:BentonSans,Helvetica,Arial,sans-serif;font-size:20px;line-height:24px;color:#fff}.ft-overlay-caption{font-family:BentonSans,Helvetica,Arial,sans-serif;font-size:12px;line-height:14px;color:#fff}.ft-overlay-credit{display:block;font-size:9px;line-height:9px;text-decoration:none;white-space:nowrap;color:#fff}.ft-link{color:#2e6e9e;text-decoration:none}.ft-link:hover{color:#000}.ft-link-stealth{color:inherit;text-decoration:none}.msie7 .ft-link-stealth{color:expression(this.parentNode.currentStyle["color"])}.ft-link-stealth:hover{color:#2e6e9e}.ft-link-alternative{color:#9b164f}.ft-link-kicker{color:#000}.ft-list{margin:0;padding:0;font-family:BentonSans,Helvetica,Arial,sans-serif;font-size:13px;line-height:21px;color:#777}.ft-list-item{margin:0;padding:0}.ft-list-bulletted .ft-list-item,.ft-list-numbered .ft-list-item{margin-left:18px}.ft-list-plain .ft-list-item{list-style-type:none}.ft-list-bulletted .ft-list-item{list-style-type:disc}.ft-list-numbered .ft-list-item{list-style-type:decimal}.ft-list-wrapping:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}.ft-list-wrapping .ft-list-item{float:left;clear:none;margin-right:18px}body,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,body,html,p,blockquote,fieldset,input{margin:0;padding:0}a{text-decoration:none}h1,h2,h3,h4,h5,h6,pre,code{font-weight:normal}ul,ol{list-style:none}acronym,a img,:link img,:visited img,fieldset{border:0}address{font-style:normal}.ft-spc-btm-full{margin-bottom:20px}.ft-spc-btm-half{margin-bottom:10px}.ft-spc-btm-qtr{margin-bottom:5px}.ft-state-hidden{display:none}.ft-timestamp{font-family:BentonSans,Helvetica,Arial,sans-serif;font-size:12px;font-weight:normal;line-height:14px;color:#777}.ft-title{font-family:Georgia,"Times New Roman",serif;color:#000}.ft-title-large{font-size:32px;line-height:36px}.ft-title-medium{font-size:21px;line-height:26px}.ft-title-small{font-size:15px;font-weight:bold;line-height:17px}.ft-lead{font-family:Georgia,"Times New Roman",serif;color:#505050}.ft-lead-large{font-size:21px;line-height:26px}.ft-lead-medium{font-size:17px;line-height:21px;color:#777}.ft-lead-small{font-family:BentonSans,Helvetica,Arial,sans-serif;font-size:13px;line-height:17px}.ft-byline{font-family:BentonSans,Helvetica,Arial,sans-serif;clear:left;color:#666}.carousel{position:relative;overflow:hidden}.carousel .carousel-collection{position:absolute;top:0;left:0;margin:0;padding:0;list-style-type:none}.carousel .carousel-item{float:left}.carousel-nav{position:absolute;cursor:pointer}.msie .carousel-nav{background:url("http://im.ft-static.com/m/img/blank.gif") transparent repeat}.carousel-nav span{display:block}.carousel-prev{left:0}.carousel-next{right:0}.carousel-hidden{display:none}.comp-header{border-top:4px solid #a7a59b;background:#e9decf url("http://im.ft-static.com/m/img/components/common/comp-header-border.png") bottom left repeat-x;overflow:hidden}.comp-header-title{float:left;margin:8px 0 8px 10px;font-size:12px;line-height:16px;font-weight:bold;font-style:normal;color:#434343}.comp-header-title a{color:#434343}.comp-header-title a:hover{color:#2e6e9e}.comp-header-tabs{float:right;font-size:11px;line-height:34px}.comp-header-tab{float:left;margin-right:10px;white-space:nowrap}.comp-state-selected{background:url("http://im.ft-static.com/m/img/components/common/comp-tab-selected.gif") bottom center no-repeat}.comp-state-selected a{color:#000}.msie6 .comp-header{height:34px;overflow:visible}.msie6 .comp-header-title{display:inline}.msie7 .comp-header{min-height:34px}.story-image{display:inline;position:relative;float:left;margin:0;overflow:hidden}.assassination .story-image{width:466px;height:280px}.primary .story-image{width:272px;min-height:153px;max-height:193px}.secondary .story-image{width:167px;height:96px}.story-image a,.story-image img{display:block}.story-image .credit{position:absolute;right:2px;bottom:2px;font-family:Arial,Helvetica,sans-serif;font-size:9px;line-height:1;color:#dfded8}.fullstoryBody .story-image .credit{line-height:2}.manualSource{cursor:default}.img-overlay{position:absolute;top:4px;left:4px;background-image:url("http://im.ft-static.com/m/img/sprites/image-overlay-icons-sprite.png");height:24px;width:24px}.img-overlay-icon-video{background-position:0 0}.img-overlay-icon-interactive{background-position:left 28px}.img-overlay-icon-qanda{background-position:left 58px}.img-overlay-icon-slideshow{background-position:left 87px}.img-overlay-icon-audio{background-position:left 116px}.msie6 .primary .story-image{height:153px}.ftbf-syndicationIndicator{display:none;background:url("http://im.ft-static.com/m/img/checked.png") no-repeat;height:16px;width:19px;vertical-align:middle;margin:0 0 4px 0;position:relative}.ftbf-syndicationIndicator a{display:none;position:absolute;left:-108px;margin-left:17px;color:#FFF;font-weight:bold;font-family:BentonSans,Helvetica,Arial,sans-serif;font-size:13px;line-height:19px;background:#74736c;width:auto;padding:0 15px 2px;border-radius:4px}.top5_standalone .ft-image{overflow:inherit}.top5_standalone .ft-image.carousel{overflow:hidden}.top5_standalone .ft-image-leader:hover .ft-overlay-title .ftbf-syndicationIndicator a{color:#FFF}.ftbf-syndicationIndicator-notPublic{display:none;background:url("http://im.ft-static.com/m/img/stop.png") no-repeat;height:16px;width:19px;vertical-align:middle;margin:0 0 4px 0}.doublet a{color:#000}.doublet a:hover{color:#2e6e9e}.doublet div .label{display:block;min-height:12px;font-size:12px;line-height:14px;font-weight:bold;text-transform:uppercase}.doublet div a.label{color:#2e6e9e}.doublet div a.label:hover{color:#000}.doublet div .story-image{margin:5px 0;width:161px;height:93px}.doublet div span.content{font-size:13px;line-height:16px}.doublet .doublet-wrapper.position-error,.doublet .position-error div span.image{width:163px;overflow:hidden}.doublet .position-error .doublet-content{margin-top:-2px}.editorialDoublet{margin:0 0 32px 0;overflow:hidden;clear:both}.doublet{display:inline;float:left;margin-top:10px;width:352px}.nojs .doublet{display:none}.doublet-column{display:inline;float:left;margin:0 0 0 10px;width:161px;overflow:hidden}.doublet-column .doublet-content{clear:both}.doublet-column .story-image img{width:161px}.doublet h4{font-size:12px;line-height:13px}.doublet h4 a{color:#2e6e9e}.doublet h4 a:hover{color:#000}.doublet p{margin:10px 0 15px 0;font-size:13px;line-height:18px}.doublet p a{display:block;color:#000}.doublet p a:hover{color:#2e6e9e}.freestyle{clear:both}.railComponent{clear:left;margin-bottom:27px;border-top:8px solid #a7a59b;padding-top:5px;overflow:hidden}.railComponent.advertising{margin-bottom:13px}.railComponent h3,.railComponentHeading{font-size:16px;margin-top:2px}.railComponent h3 a,.railComponentHeading a{color:#000}.railComponent h3 a:hover,.railComponentHeading a:hover{color:#2e6e9e}.mkAd,.tradeCenter{font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:16px}.mkAd .moreLink{float:right;margin-right:15px}div.mkAd p{margin:12px 0 0 16px;padding-bottom:9px}.tradeCenter li{background-color:#999;display:block;float:left;height:60px;margin:14px 21px 2px 18px;width:120px}.tradeCenter h3{margin-top:5px}.partnerLinks{clear:left;margin-bottom:32px;overflow:hidden}.partnerLinks .railLinks{display:inline;float:left;margin:6px 10px 0 10px;list-style:none;width:332px}.partnerLinks .railLinks li{float:left;clear:left;width:332px}.partnerLinks .railLinks li.moreLink{float:none;margin-right:0;clear:both;width:auto}.msie6 .partnerLinks .railLinks{margin-left:8px}.jobsBox{clear:left;margin-bottom:32px}.jobsBoxSearch{margin:12px 0 15px 10px}.jobsBoxSearch legend{display:none}.jobsBoxKeywords{border:1px solid #fff;border-radius:3px;padding:3px 4px 3px 4px;width:243px;height:16px;color:#8c8c8c}.jobsBoxSearchButton{border:0;height:24px;background:transparent;color:#fff;padding:0 0 0 6px;font-family:Arial,Helvetica,sans-serif;cursor:pointer;white-space:nowrap;overflow:visible}.jobsBoxSearchButton:hover{color:#ddd}.jobsBoxSearchButton span{display:inline-block;padding:0 4px 0 0;font-size:14px;line-height:24px;background:#4781aa no-repeat scroll right -64px url("http://im.ft-static.com/m/img/button_sprite.png")}.jobsBoxSearchButton span span{padding:0 4px 0 8px;background-position:left -90px;background-color:transparent}.jobsBoxLinks li{float:left;margin:0 10px 15px 0;width:161px;color:#8a8a8a;font-size:13px;line-height:18px;overflow:hidden}.jobsBoxLinks li.odd{clear:left;margin-left:10px}.jobsBoxLinks li.fullwidth{width:100%;margin-left:0}.jobsBoxLinks a{color:#000}.jobsBoxLinks a:hover{color:#2e6e9e}.jobsBoxOptionalLink{margin:0 10px;font-size:11px}.msie6 .jobsBoxSearch,.msie7 .jobsBoxSearch{margin-left:5px}.msie6 .jobsBoxLinks li{display:inline}.railList{margin:0 0 30px 0;width:352px}.railList .comp-body{padding:15px 10px 0}.railLinks{margin:0 0 0 23px;font-weight:bold;color:#777}ol.railLinks{list-style:decimal}.railLinks li{display:list-item;padding:2px 0;font-size:13px;line-height:18px}.msie ul.railLinks li{padding:3px 0 4px 0}.railLinks a{color:#2e6e9e;font-weight:normal}.railList .moreLink{padding-right:0;text-align:right}.railList .moreLink a{color:#2e6e9e}.railLinks a:hover,.railList .moreLink a:hover{color:#000}.topics a{color:#9b164f}.railMiniVideo{margin-bottom:32px}.editorialSection .railMiniVideo{margin-bottom:20px}.railMiniVideo-player{margin:10px 0 0 10px;width:332px;height:225px;-webkit-transform-style:preserve-3d}.editorialSection .railMiniVideo-player{margin:10px 0 0;width:600px;height:338px}.nojs .railMiniVideo-player{display:none}.railMiniVideo-player-nojs{margin:10px 5px 0 5px;width:342px}#wsodHomepagePlaceholder,#wsodCommoditiesPlaceholder{min-height:181px}.wsodRailModule{margin:0 0 30px 0;overflow:auto}.railSection .ft-header-medium-title a{color:#434343}.railSection .ft-header-medium-title a:hover{color:#2e6e9e}.slideshow{-webkit-font-smoothing:antialiased;-webkit-perspective:1000;-webkit-backface-visibility:hidden;clear:both}.slideshow ul{-webkit-transform:translate3d(0,0,0)}.slideshow .carousel-item{position:relative;background-color:#000}.slideshow .carousel-item img{display:block;margin:0 auto}.hoverable .slideshow .carousel-nav{display:none}.slideshow .carousel-nav span{text-indent:-200px;overflow:hidden}.slideshow:hover .carousel-nav{display:block}.slideshow:hover .carousel-hidden{display:none}.slideshow-narrow,.slideshow-narrow .carousel-item{width:414px;height:233px}.slideshow-standard,.slideshow-standard .carousel-item{width:600px;height:338px}.slideshow-wide,.slideshow-wide .carousel-item{width:972px;height:547px}.nojs .thumbnail-carousel{display:none}.thumbnail-carousel{margin-top:-42px;margin-bottom:42px;height:72px;background-color:#a7a59b}.thumbnails-narrow{width:414px}.thumbnails-standard{width:600px}.thumbnails-wide{width:972px}.thumbnail-carousel .carousel-item{position:relative}.thumbnail-carousel .carousel-item img{display:block;cursor:pointer}.msie6 .carousel-item{display:inline;overflow:hidden}.thumbnails-standard .carousel-item{margin-left:20px;margin-top:4px}.thumbnails-wide .carousel-item{margin-left:16px;margin-top:4px}.thumbnail-carousel .carousel-nav{width:26px;height:72px;background-image:url("http://im.ft-static.com/m/img/slideshow/nav_sprite_thumbnails.png")}.thumbnail-carousel .carousel-prev{background-position:0 0}.thumbnail-carousel .carousel-prev:hover{background-position:0 72px}.thumbnail-carousel .carousel-next{background-position:26px 0}.thumbnail-carousel .carousel-next:hover{background-position:26px 72px}.thumbnails-standard li:first-child,.thumbnails-standard .carousel-group-start{margin-left:42px}.thumbnails-wide li:first-child,.thumbnails-wide .carousel-group-start{margin-left:39px}.thumbnails-standard .carousel-group-end{margin-right:42px}.thumbnails-wide .carousel-group-end{margin-right:39px}.thumbnail-carousel .imageset-selected div.indicate-imageset-selected{background:url("http://im.ft-static.com/m/img/slideshow/select-thumbV3.png") no-repeat;position:absolute;top:0;left:0;width:100%;height:100%}.toolsandservices{clear:left;margin-bottom:18px}.tools-column{float:left;margin:11px 0 12px 0;width:176px;display:inline}.tools-heading{margin:0 0 3px 10px;padding:0;font-weight:bold;font-size:11px;color:#434343}.tools-column ul{padding-bottom:20px;list-style-type:none}.tools-column li{padding:2px 0 2px 10px;color:#74736c;font-weight:bold;font-size:13px;line-height:18px}.tools-column+.tools-column li{padding-right:10px}.tools-column li a{font-weight:normal}.tools-column img{vertical-align:middle}.tools-column a.rss,.tools-column a.rss:visited{padding-left:18px;color:#fa9d3a;background:url("http://im.ft-static.com/m/img/rss.gif") left 50% no-repeat}.tools-column a.rss:hover{color:#000}.msie6 .toolsandservices{overflow:hidden}
--------------------------------------------------------------------------------
/fixtures/xyz.js:
--------------------------------------------------------------------------------
1 | // var md5 = require('md5')
2 | // var $ = require('jquery')
3 | // var Highcharts = require('highcharts')
4 | // require('./assets-graph-theme')
5 | // var prettyBytes = require('pretty-bytes')
6 |
7 | // function setTrend (assetHash, firstMetric, secondMetric) {
8 | // var difference = secondMetric - firstMetric
9 | // var trend = (difference < 0) ? 'down' : 'up'
10 | // var trendSign = (difference > 0) ? '+' : ''
11 |
12 | // $trendElement = $('.js-trend-' + assetHash)
13 |
14 | // if (difference !== 0) {
15 | // $trendElement.addClass('trend--' + trend)
16 | // $trendElement.find('.js-trend-sign').text(trendSign)
17 | // $trendElement.find('.js-trend-value').text(prettyBytes(difference))
18 | // }
19 | // }
20 |
21 | // function graphStylesheets () {
22 | // var sizes
23 | // var firstSize
24 | // var lastSize
25 |
26 | // $('.js-asset').each(function (assetContainer) {
27 | // var assetHash = $(this).data('asset-hash')
28 |
29 | // $.getJSON('/metrics/stylesheets/' + assetHash, function (series) {
30 | // sizes = series[0].data
31 | // firstSize = sizes[0][1]
32 | // lastSize = sizes[sizes.length - 1][1]
33 | // setTrend(assetHash, firstSize, lastSize)
34 |
35 | // new Highcharts.Chart({
36 | // chart: {
37 | // type: 'spline',
38 | // renderTo: 'js-asset-chart-' + assetHash
39 | // },
40 | // yAxis: [
41 | // {
42 | // title: {
43 | // text: 'Size'
44 | // },
45 | // labels: {
46 | // formatter: function () {
47 | // return prettyBytes(this.value)
48 | // }
49 | // }
50 | // },
51 | // {
52 | // title: {
53 | // text: 'Count'
54 | // },
55 | // opposite: true
56 | // }
57 | // ],
58 | // tooltip: {
59 | // crosshairs: [false, true],
60 | // formatter: function () {
61 | // return '' + this.series.name + '
' + Highcharts.dateFormat('%b %e, %H:%M', this.x) + ': ' + (this.series.type === 'area' ? prettyBytes(this.y) : this.y) + ''
62 | // }
63 | // },
64 | // series: series
65 | // })
66 | // })
67 | // })
68 | // }
69 |
70 | // $(function () {
71 | // graphStylesheets()
72 | // })
73 | // var md5 = require('md5')
74 | // var $ = require('jquery')
75 | // var Highcharts = require('highcharts')
76 | // require('./assets-graph-theme')
77 | // var prettyBytes = require('pretty-bytes')
78 |
79 | // function setTrend (assetHash, firstMetric, secondMetric) {
80 | // var difference = secondMetric - firstMetric
81 | // var trend = (difference < 0) ? 'down' : 'up'
82 | // var trendSign = (difference > 0) ? '+' : ''
83 |
84 | // $trendElement = $('.js-trend-' + assetHash)
85 |
86 | // if (difference !== 0) {
87 | // $trendElement.addClass('trend--' + trend)
88 | // $trendElement.find('.js-trend-sign').text(trendSign)
89 | // $trendElement.find('.js-trend-value').text(prettyBytes(difference))
90 | // }
91 | // }
92 |
93 | // function graphStylesheets () {
94 | // var sizes
95 | // var firstSize
96 | // var lastSize
97 |
98 | // $('.js-asset').each(function (assetContainer) {
99 | // var assetHash = $(this).data('asset-hash')
100 |
101 | // $.getJSON('/metrics/stylesheets/' + assetHash, function (series) {
102 | // sizes = series[0].data
103 | // firstSize = sizes[0][1]
104 | // lastSize = sizes[sizes.length - 1][1]
105 | // setTrend(assetHash, firstSize, lastSize)
106 |
107 | // new Highcharts.Chart({
108 | // chart: {
109 | // type: 'spline',
110 | // renderTo: 'js-asset-chart-' + assetHash
111 | // },
112 | // yAxis: [
113 | // {
114 | // title: {
115 | // text: 'Size'
116 | // },
117 | // labels: {
118 | // formatter: function () {
119 | // return prettyBytes(this.value)
120 | // }
121 | // }
122 | // },
123 | // {
124 | // title: {
125 | // text: 'Count'
126 | // },
127 | // opposite: true
128 | // }
129 | // ],
130 | // tooltip: {
131 | // crosshairs: [false, true],
132 | // formatter: function () {
133 | // return '' + this.series.name + '
' + Highcharts.dateFormat('%b %e, %H:%M', this.x) + ': ' + (this.series.type === 'area' ? prettyBytes(this.y) : this.y) + ''
134 | // }
135 | // },
136 | // series: series
137 | // })
138 | // })
139 | // })
140 | // }
141 |
142 | // $(function () {
143 | // graphStylesheets()
144 | // })
145 | // var md5 = require('md5')
146 | // var $ = require('jquery')
147 | // var Highcharts = require('highcharts')
148 | // require('./assets-graph-theme')
149 | // var prettyBytes = require('pretty-bytes')
150 |
151 | // function setTrend (assetHash, firstMetric, secondMetric) {
152 | // var difference = secondMetric - firstMetric
153 | // var trend = (difference < 0) ? 'down' : 'up'
154 | // var trendSign = (difference > 0) ? '+' : ''
155 |
156 | // $trendElement = $('.js-trend-' + assetHash)
157 |
158 | // if (difference !== 0) {
159 | // $trendElement.addClass('trend--' + trend)
160 | // $trendElement.find('.js-trend-sign').text(trendSign)
161 | // $trendElement.find('.js-trend-value').text(prettyBytes(difference))
162 | // }
163 | // }
164 |
165 | // function graphStylesheets () {
166 | // var sizes
167 | // var firstSize
168 | // var lastSize
169 |
170 | // $('.js-asset').each(function (assetContainer) {
171 | // var assetHash = $(this).data('asset-hash')
172 |
173 | // $.getJSON('/metrics/stylesheets/' + assetHash, function (series) {
174 | // sizes = series[0].data
175 | // firstSize = sizes[0][1]
176 | // lastSize = sizes[sizes.length - 1][1]
177 | // setTrend(assetHash, firstSize, lastSize)
178 |
179 | // new Highcharts.Chart({
180 | // chart: {
181 | // type: 'spline',
182 | // renderTo: 'js-asset-chart-' + assetHash
183 | // },
184 | // yAxis: [
185 | // {
186 | // title: {
187 | // text: 'Size'
188 | // },
189 | // labels: {
190 | // formatter: function () {
191 | // return prettyBytes(this.value)
192 | // }
193 | // }
194 | // },
195 | // {
196 | // title: {
197 | // text: 'Count'
198 | // },
199 | // opposite: true
200 | // }
201 | // ],
202 | // tooltip: {
203 | // crosshairs: [false, true],
204 | // formatter: function () {
205 | // return '' + this.series.name + '
' + Highcharts.dateFormat('%b %e, %H:%M', this.x) + ': ' + (this.series.type === 'area' ? prettyBytes(this.y) : this.y) + ''
206 | // }
207 | // },
208 | // series: series
209 | // })
210 | // })
211 | // })
212 | // }
213 |
214 | // $(function () {
215 | // graphStylesheets()
216 | // })
217 | // var md5 = require('md5')
218 | // var $ = require('jquery')
219 | // var Highcharts = require('highcharts')
220 | // require('./assets-graph-theme')
221 | // var prettyBytes = require('pretty-bytes')
222 |
223 | // function setTrend (assetHash, firstMetric, secondMetric) {
224 | // var difference = secondMetric - firstMetric
225 | // var trend = (difference < 0) ? 'down' : 'up'
226 | // var trendSign = (difference > 0) ? '+' : ''
227 |
228 | // $trendElement = $('.js-trend-' + assetHash)
229 |
230 | // if (difference !== 0) {
231 | // $trendElement.addClass('trend--' + trend)
232 | // $trendElement.find('.js-trend-sign').text(trendSign)
233 | // $trendElement.find('.js-trend-value').text(prettyBytes(difference))
234 | // }
235 | // }
236 |
237 | // function graphStylesheets () {
238 | // var sizes
239 | // var firstSize
240 | // var lastSize
241 |
242 | // $('.js-asset').each(function (assetContainer) {
243 | // var assetHash = $(this).data('asset-hash')
244 |
245 | // $.getJSON('/metrics/stylesheets/' + assetHash, function (series) {
246 | // sizes = series[0].data
247 | // firstSize = sizes[0][1]
248 | // lastSize = sizes[sizes.length - 1][1]
249 | // setTrend(assetHash, firstSize, lastSize)
250 |
251 | // new Highcharts.Chart({
252 | // chart: {
253 | // type: 'spline',
254 | // renderTo: 'js-asset-chart-' + assetHash
255 | // },
256 | // yAxis: [
257 | // {
258 | // title: {
259 | // text: 'Size'
260 | // },
261 | // labels: {
262 | // formatter: function () {
263 | // return prettyBytes(this.value)
264 | // }
265 | // }
266 | // },
267 | // {
268 | // title: {
269 | // text: 'Count'
270 | // },
271 | // opposite: true
272 | // }
273 | // ],
274 | // tooltip: {
275 | // crosshairs: [false, true],
276 | // formatter: function () {
277 | // return '' + this.series.name + '
' + Highcharts.dateFormat('%b %e, %H:%M', this.x) + ': ' + (this.series.type === 'area' ? prettyBytes(this.y) : this.y) + ''
278 | // }
279 | // },
280 | // series: series
281 | // })
282 | // })
283 | // })
284 | // }
285 |
286 | // $(function () {
287 | // graphStylesheets()
288 | // })
289 |
--------------------------------------------------------------------------------
/lib/db.js:
--------------------------------------------------------------------------------
1 | const level = require('level')
2 | const levelup = require('levelup')
3 | const encode = require('encoding-down')
4 |
5 | module.exports = function (db) {
6 | if (db.redis_url) {
7 | const RedisDown = require('redisdown')
8 | return levelup(encode(new RedisDown('moniteur')), {
9 | url: db.redis_url
10 | })
11 | }
12 |
13 | // By default, save the database on the filesystem
14 | return level('./' + db.directory)
15 | }
16 |
--------------------------------------------------------------------------------
/lib/jsparser.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const request = require('request')
3 | const gzipSize = require('gzip-size')
4 |
5 | /**
6 | * Get promised request
7 | * @param {Object} options
8 | * @returns {Promise}
9 | */
10 | function requestSync (options) {
11 | return new Promise(function (resolve, reject) {
12 | request(options, function (error, response) {
13 | if (!error && response.statusCode === 200) {
14 | resolve(response)
15 | } else if (!error) {
16 | reject(new Error('Status code is ' + response.statusCode))
17 | } else {
18 | reject(error)
19 | }
20 | })
21 | })
22 | }
23 |
24 | /**
25 | * JSParser class
26 | * @param {Array} urls
27 | * @param {Array} files
28 | * @constructor
29 | */
30 | function JSParser (urls, files) {
31 | this.urls = urls
32 | this.files = files
33 | this.scripts = []
34 | this.options = {}
35 | }
36 |
37 | /**
38 | * Parse JS data
39 | */
40 | JSParser.prototype.parse = function parse () {
41 | // object to return
42 | let parsedData = {
43 | jsString: '',
44 | size: 0,
45 | gzippedSize: 0
46 | }
47 | // remote file requests
48 | const requestPromises = this.urls.map(url =>
49 | requestSync({url: url}))
50 |
51 | // JS string array from arguments
52 | // they will be joined into JS string
53 | this.files.forEach((jsFile) => {
54 | // push local js data
55 | this.scripts.push(fs.readFileSync(jsFile, {
56 | encoding: 'utf8'
57 | }))
58 | })
59 |
60 | // get remote files
61 | return new Promise((resolve, reject) => {
62 | Promise.all(requestPromises).then((results) => {
63 | // requests to scripts defined in html
64 | let requestPromisesInner = []
65 |
66 | results.forEach((result) => {
67 | // push remote js data
68 | if (result.headers['content-type'].indexOf('javascript') > -1) {
69 | parsedData.files += 1
70 | this.scripts.push(result.body)
71 | } else {
72 | throw new Error('Content type is not JavaScript!')
73 | }
74 | })
75 |
76 | if (requestPromisesInner.length > 0) {
77 | return Promise.all(requestPromisesInner)
78 | } else {
79 | return true
80 | }
81 | }).then((results) => {
82 | if (Array.isArray(results)) {
83 | results.forEach((result) => {
84 | this.scripts.push(result.body)
85 | })
86 | }
87 |
88 | // join all JS string
89 | parsedData.jsString = this.scripts.join('')
90 | parsedData.size = Buffer.byteLength(parsedData.jsString, 'utf8')
91 | parsedData.gzippedSize = gzipSize.sync(parsedData.jsString)
92 | return resolve(parsedData)
93 | })
94 | .catch(reason => reject(reason))
95 | })
96 | }
97 |
98 | // export
99 | module.exports = JSParser
100 |
--------------------------------------------------------------------------------
/lib/record.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const StyleStats = require('stylestats')
3 | const debug = require('debug')
4 | const validUrl = require('valid-url')
5 | const sensors = require('./sensors')
6 | const JSParser = require('./jsparser')
7 | const utils = require('./utils')
8 |
9 | const log = debug('moniteur:log')
10 |
11 | /**
12 | * Argument is file path or not
13 | * @param {String} file
14 | * @returns {Boolean}
15 | */
16 | function isFile (file) {
17 | try {
18 | return fs.existsSync(file) && fs.statSync(file).isFile()
19 | } catch (error) {
20 | return false
21 | }
22 | }
23 |
24 | // const setRecorder = (key, )
25 |
26 | module.exports = class Record {
27 | constructor (assets, db) {
28 | this.assets = assets
29 | this.recorders = {}
30 | this.db = db
31 | }
32 |
33 | init () {
34 | return Object.keys(this.assets).map(assetName =>
35 | new Promise((resolve, reject) => {
36 | this.recorders = this.recorders || {}
37 | const assetHash = utils.getAssetHash(assetName)
38 | const assetEndpoints = this.assets[assetName]
39 | const assetType = utils.getAssetType(assetEndpoints)
40 | this.recorders[assetHash] = {}
41 |
42 | if (!assetEndpoints) {
43 | reject(console.error('No asset names were found'))
44 | }
45 |
46 | Object.keys(sensors[assetType]).map(metric => {
47 | resolve(log('Set up recorder:', assetName, metric))
48 | const key = 'assets.' + assetHash + '.' + metric
49 | this.db.index(key, metric)
50 | this.recorders[assetHash][metric] = this.db.recorder(key)
51 | })
52 | })
53 | )
54 | }
55 |
56 | recordDataPoints () {
57 | return Object.keys(this.assets).map(assetName => {
58 | const assetHash = utils.getAssetHash(assetName)
59 | const assetEndpoints = this.assets[assetName]
60 | const assetType = utils.getAssetType(assetEndpoints)
61 | switch (assetType) {
62 | case 'js':
63 | const assetToParse = !Array.isArray(assetEndpoints) ? assetEndpoints.split(',') : assetEndpoints
64 | const urls = assetToParse.filter(validUrl.isUri)
65 | const files = assetToParse.filter(isFile)
66 |
67 | return new Promise((resolve, reject) => {
68 | const jsparser = new JSParser(urls, files)
69 |
70 | return jsparser.parse()
71 | .then(stats =>
72 | Object.keys(sensors['js']).map(metric =>
73 | this.recorders[assetHash][metric](stats[metric], () =>
74 | resolve(log(`Recorded: ${assetHash} 'js' ${assetEndpoints} ${metric} ${stats[metric]}`))
75 | )
76 | )
77 | )
78 | .catch(reason => reject(reason))
79 | })
80 | case 'css':
81 | return new Promise((resolve, reject) => {
82 | const stats = new StyleStats(assetEndpoints)
83 |
84 | // returns a Promise
85 | // each promise performs an asynchronous StyleStats.parse operation
86 | stats.parse()
87 | .then(result =>
88 | Object.keys(sensors['css']).map(metric =>
89 | this.recorders[assetHash][metric](result[metric], () =>
90 | resolve(log(`Recorded: ${assetHash} css ${assetEndpoints} ${metric} ${result[metric]}`))
91 | )
92 | )
93 | )
94 | .catch(reason => reject(reason))
95 | })
96 | }
97 | })
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/lib/sensors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Telemetry sensors, by asset category
3 | *
4 | * Sensors have properties mapped to the HighCharts API:
5 | * http://api.highcharts.com/highcharts#plotOptions.areaspline
6 | * http://api.highcharts.com/highcharts#plotOptions.spline
7 | */
8 |
9 | module.exports = {
10 | js: {
11 | size: {
12 | name: 'Size',
13 | yAxis: 0,
14 | type: 'areaspline',
15 | fillOpacity: 0.1
16 | },
17 | gzippedSize: {
18 | name: 'Size (gzipped)',
19 | yAxis: 0,
20 | type: 'areaspline',
21 | fillOpacity: 0.1
22 | }
23 | },
24 | css: {
25 | size: {
26 | name: 'Size',
27 | yAxis: 0,
28 | type: 'areaspline',
29 | fillOpacity: 0.1
30 | },
31 | gzippedSize: {
32 | name: 'Size (gzipped)',
33 | yAxis: 0,
34 | type: 'areaspline',
35 | fillOpacity: 0.1
36 | },
37 | dataUriSize: {
38 | name: 'Data URI Size',
39 | yAxis: 0,
40 | type: 'areaspline',
41 | fillOpacity: 0.1
42 | },
43 | rules: {
44 | name: 'Rules',
45 | yAxis: 1,
46 | type: 'spline'
47 | },
48 | selectors: {
49 | name: 'Selectors',
50 | yAxis: 1,
51 | type: 'spline'
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | const md5 = require('md5')
2 | const path = require('path')
3 | const url = require('url')
4 |
5 | /**
6 | * @param {String} name name of the asset to hash
7 | * @returns {String} hashed name
8 | */
9 | const getAssetHash = md5
10 |
11 | /**
12 | * @param {Object} assets
13 | * @returns {Object} asset object with their names hashed
14 | */
15 | const hashAssets = assets =>
16 | Object.assign(
17 | {},
18 | ...Object.keys(assets).map(assetName =>
19 | ({[getAssetHash(assetName)]: assets[assetName]})
20 | ))
21 |
22 | /**
23 | * @param {String} p path to a file
24 | * @returns {String} file extension without the dot
25 | */
26 | const getExtension = p => path.extname(url.parse(p).pathname).slice(1)
27 |
28 | /**
29 | * Get the type of an asset or array of assets
30 | *
31 | * @param {(String|Array)} assetPath path to an asset or array of paths
32 | * @returns String
33 | */
34 | const getAssetType = assetPath =>
35 | /* eslint no-useless-call: 0 */
36 | // 1. transform assetPath into an array,
37 | // 2. flatten the array
38 | // 3. take its first element
39 | // 4. put it into an array
40 | [[].concat.apply([], [assetPath]).shift()]
41 | .map(getExtension)
42 | .shift()
43 |
44 | /**
45 | * @param {String} assetHash md5 hash of an asset
46 | * @param {Object} assets assets containing the asset
47 | * @returns {String}
48 | */
49 | const getAssetTypeFromHash = (assetHash, assets) =>
50 | getAssetType(hashAssets(assets)[assetHash])
51 |
52 | module.exports = {
53 | getAssetHash: getAssetHash,
54 | hashAssets: hashAssets,
55 | getAssetType: getAssetType,
56 | getAssetTypeFromHash: getAssetTypeFromHash
57 | }
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "moniteur",
3 | "description": "Monitor your asset size over time, in your browser, or using the provided HTTP API.",
4 | "version": "0.8.2",
5 | "bin": {
6 | "moniteur": "./bin/moniteur"
7 | },
8 | "engines": {
9 | "node": ">= 8.9.4",
10 | "npm": "latest"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/kaelig/moniteur.git"
15 | },
16 | "keywords": [
17 | "webperf",
18 | "performance"
19 | ],
20 | "author": "Kaelig Deloumeau-Prigent ",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/kaelig/moniteur/issues"
24 | },
25 | "homepage": "https://github.com/kaelig/moniteur",
26 | "devDependencies": {
27 | "browser-sync": "^2.23.5",
28 | "cz-conventional-changelog": "^2.1.0",
29 | "eslint": "^4.15.0",
30 | "eslint-config-standard": "^11.0.0",
31 | "eslint-plugin-import": "^2.8.0",
32 | "eslint-plugin-jasmine": "^2.9.1",
33 | "eslint-plugin-node": "^6.0.0",
34 | "eslint-plugin-promise": "^4.0.0",
35 | "eslint-plugin-standard": "^3.0.1",
36 | "jest-cli": "^22.0.6",
37 | "nodemon": "^1.14.11",
38 | "standard-version": "^4.3.0",
39 | "webpack-dev-middleware": "^2.0.4",
40 | "webpack-hot-middleware": "^2.21.0"
41 | },
42 | "scripts": {
43 | "test": "jest",
44 | "prestart": "webpack -p",
45 | "start": "NODE_ENV=production ./bin/moniteur serve",
46 | "record": "./bin/moniteur record",
47 | "release": "standard-version",
48 | "dev": "DEBUG=moniteur:* nodemon -e js,pug --watch views --watch routes --watch lib --watch .moniteurrc.development.yml --watch .moniteurrc.default.yml --watch .moniteurrc.yml --watch webpack.config.js --watch bin/moniteur.js ./bin/moniteur serve",
49 | "lint": "eslint . --format codeframe --ext .js,.jsx"
50 | },
51 | "dependencies": {
52 | "babel-core": "^6.26.0",
53 | "babel-loader": "^7.1.2",
54 | "babel-preset-es2015": "^6.24.1",
55 | "commander": "^2.13.0",
56 | "compression": "^1.7.1",
57 | "debug": "^3.1.0",
58 | "express": "^4.16.2",
59 | "express-slashes": "^0.1.1",
60 | "glob": "^7.1.2",
61 | "gzip-size": "^4.1.0",
62 | "highcharts": "^6.0.4",
63 | "highland": "^3.0.0-beta.2",
64 | "http-auth": "^3.2.3",
65 | "js-yaml": "^3.10.0",
66 | "lem": "1.0.0",
67 | "level": "^3.0.0",
68 | "leveldown": "^3.0.0",
69 | "md5": "^2.2.1",
70 | "nconf": "^0.10.0",
71 | "nconf-yaml": "^1.0.2",
72 | "pretty-bytes": "^4.0.2",
73 | "pug": "^2.0.0-rc.4",
74 | "redis": "^2.8.0",
75 | "redisdown": "^0.1.12",
76 | "request": "^2.83.0",
77 | "stylestats": "^7.0.1",
78 | "valid-url": "^1.0.9",
79 | "webpack": "^3.10.0",
80 | "yargs": "^10.1.1"
81 | },
82 | "config": {
83 | "commitizen": {
84 | "path": "./node_modules/cz-conventional-changelog"
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const utils = require('../lib/utils')
3 |
4 | const router = express.Router()
5 |
6 | module.exports = router.get('/', (req, res) => {
7 | const assets = Object.keys(res.locals.assets).map(asset =>
8 | ({
9 | name: asset,
10 | hash: utils.getAssetHash(asset),
11 | type: utils.getAssetType(res.locals.assets[asset])
12 | })
13 | )
14 | res.render('index', { title: 'moniteur', assets })
15 | })
16 |
--------------------------------------------------------------------------------
/routes/metrics.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const debug = require('debug')
3 | const lem = require('lem')
4 | const sensors = require('../lib/sensors')
5 | const highland = require('highland')
6 | const utils = require('../lib/utils')
7 |
8 | const _ = highland
9 | const router = express.Router()
10 | const log = debug('moniteur:log')
11 |
12 | // Example:
13 | // Series (since forever): /metrics/adf6e9c154cb57a818f7fb407085bff6
14 | // Series between two dates: /metrics/adf6e9c154cb57a818f7fb407085bff6/1015711104475..1415711104475
15 | module.exports = router.get(/^\/(\w+)(\/(\d+)\.\.(\d+))?$/, (req, res) => {
16 | const assetHash = req.params[0]
17 | const assetType = utils.getAssetTypeFromHash(assetHash, res.locals.assets)
18 | const start = req.params[2] || (Date.now() - 3600 * 24 * 30 * 1000).toString()
19 | const end = req.params[3] || Date.now().toString()
20 | const db = lem(res.locals.db)
21 |
22 | // Get Array of values for metric
23 | _(Object.keys(sensors[assetType])).flatMap(metric => {
24 | const key = `assets.${assetHash}.${metric}`
25 | log('Opening ValueStream for:', key)
26 | return _(db.valuestream(
27 | key,
28 | {
29 | start: start,
30 | end: end
31 | }
32 | ))
33 | .filter(data => data.value !== 0)
34 | .map(data =>
35 | [
36 | data.key,
37 | data.value
38 | ]
39 | )
40 | .collect()
41 | .map(data =>
42 | Object.assign(
43 | {},
44 | sensors[assetType][metric],
45 | { data }))
46 | }).toArray(data => res.json(data))
47 | })
48 |
--------------------------------------------------------------------------------
/routes/settings.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const utils = require('../lib/utils')
3 |
4 | const router = express.Router()
5 |
6 | module.exports = router.get('/', (req, res) => {
7 | const assets = Object.keys(res.locals.assets).map(asset => {
8 | const resources = !Array.isArray(res.locals.assets[asset])
9 | ? Array.of(res.locals.assets[asset])
10 | : res.locals.assets[asset]
11 |
12 | return {
13 | name: asset,
14 | hash: utils.getAssetHash(asset),
15 | resources: resources
16 | }
17 | })
18 | res.render('settings', { title: 'moniteur: current settings', assets })
19 | })
20 |
--------------------------------------------------------------------------------
/views/_faq.pug:
--------------------------------------------------------------------------------
1 | h1 How do I…
2 | h2#faq-scheduling Schedule daily data recordings?
3 | p Moniteur checks assets frequently and generates graphs for you.
4 | img(src='/docs/welcome-process/welcome-process.001.png')
5 | img(src='/docs/welcome-process/welcome-process.002.png')
6 | img(src='/docs/welcome-process/welcome-process.003.png')
7 | img(src='/docs/welcome-process/welcome-process.004.png')
8 | img(src='/docs/welcome-process/welcome-process.005.png')
9 |
10 | h2#faq-assets See what assets are being monitored?
11 | p Here's a full list of the assets being monitored and the URLs that moniteur will check:
12 | p
13 | strong
14 | a(href='/settings') Current settings
15 |
16 | h2#faq-trigger Trigger a new recording?
17 | p Typically, moniteur records asset sizes on a daily basis, but you don't have to wait that long to see how big your CSS and JavaScript files got.
18 | p You can trigger a new recording at any time by executing:
19 | pre.code
20 | code.user-select heroku run record -a moniteur
21 | p Note: you'll need the #[a(href="https://toolbelt.heroku.com/") Heroku toolbelt] to execute this command.
22 |
--------------------------------------------------------------------------------
/views/error.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | .l-content
5 | .l-container
6 | h1= message
7 | h2= error.status
8 | pre #{error.stack}
9 |
--------------------------------------------------------------------------------
/views/index.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | .assets
5 | each asset in assets
6 | - var hash = asset.hash
7 | - var name = asset.name
8 | - var type = asset.type
9 | .js-asset.asset.l-container(id='asset-'+ hash, data-asset-hash=hash, data-asset-type=type)
10 | h2.asset__name
11 | span.asset__type
12 | = name
13 | a.asset__linkto(href='#asset-' + hash) ¶
14 | span(title='Evolution over time', class='trend js-trend js-trend-' + hash)
15 | span.js-trend-sign =
16 | span.js-trend-value
17 | .js-asset-chart.asset__chart(id='js-asset-chart-'+ hash)
18 |
--------------------------------------------------------------------------------
/views/layout.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang='en')
3 | head
4 | meta(charset='utf-8')
5 | meta(http-equiv='x-ua-compatible' content='ie=edge')
6 | title= title
7 | link(rel='stylesheet', href='/stylesheets/style.css')
8 | body
9 | header.l-header
10 | .l-container
11 | h1
12 | a(href='/') moniteur
13 | block content
14 | footer.l-footer
15 | .l-container
16 | a(href='http://git.io/moniteur') Fork on GitHub
17 | a(href='https://github.com/kaelig/moniteur/issues') Report an issue
18 | a(href='/support') FAQ
19 | a(href='/welcome') Welcome screen
20 | a(href='/settings') Current settings
21 | script(src='https://cdn.polyfill.io/v2/polyfill.min.js')
22 | script(src='https://code.highcharts.com/5/highcharts.js')
23 | script(src='/js/bundle.js')
24 |
--------------------------------------------------------------------------------
/views/settings.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | .l-content
5 | .l-container
6 | h2 Assets
7 | p These #{assets.length} assets are currently being monitored:
8 | table.table
9 | thead
10 | tr
11 | th Asset
12 | th Resource(s)
13 | tbody
14 | each asset in assets
15 | tr
16 | th.table__assetname
17 | a(href='/#asset-' + asset.hash)=asset.name
18 | td.table__assetresources
19 | - var resources = asset.resources.map(r => `${r}`).join('
');
20 | !=resources
21 |
--------------------------------------------------------------------------------
/views/support.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | .l-content
5 | .l-container
6 | include ./_faq.pug
7 |
--------------------------------------------------------------------------------
/views/welcome.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | .l-content
5 | .l-container
6 | p.welcome-message Welcome to moniteur
7 | p.align-center Here are a few steps you might want to follow…
8 | include ./_faq.pug
9 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | devtool: 'source-map',
5 | entry: {
6 | filename: './client/javascripts/app.js'
7 | },
8 | output: {
9 | filename: 'bundle.js',
10 | path: path.resolve(__dirname, 'dist/js/'),
11 | publicPath: '/js/'
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.js$/,
17 | use: ['babel-loader']
18 | }
19 | ]
20 | },
21 | stats: {
22 | // Nice colored output
23 | colors: true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------