├── web
├── public
│ └── .gitkeep
├── img
│ ├── favicon.ico
│ ├── digital-ocean-badge.png
│ └── gps.svg
├── octicons
│ ├── octicons.eot
│ ├── octicons.ttf
│ ├── octicons.woff
│ ├── octicons-local.ttf
│ ├── octicons.css
│ └── octicons.less
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.woff2
├── templates
│ ├── user.repositories.html
│ ├── search-form.html
│ ├── bsod.html
│ ├── user.common.html
│ └── user.events.html
├── css
│ ├── site.css
│ ├── loader.css
│ └── bootstrap-theme.css
├── js
│ ├── stats.controller.js
│ ├── services.js
│ ├── search.controller.js
│ ├── bsod.controller.js
│ ├── app.js
│ ├── ie10-viewport-bug-workaround.js
│ ├── error-handler.js
│ ├── routes.js
│ ├── user.controller.js
│ ├── event-octicon.js
│ ├── truncate.js
│ └── angular-ui-router.min.js
├── package.json
├── gulpfile.js
└── index.html
├── conf
├── galaxy-roles.txt
├── ssmtp-revaliases
├── github-contributions-process.service
├── lets-encrypt-config.ini
├── github-contributions-process.timer
├── inventory
├── ssmtp.conf
├── mongod.conf
├── build-ghc-app.sh
├── github-contributions-app.service
├── ghc-env.sh
├── server-monitor
├── secrets.yml
├── process-job-w32.bat
├── process-job
├── lets-encrypt-renew-certificate.sh
├── github-contributions.nginx.conf
└── provision.yml
├── .agignore
├── util
├── mongo-tunnel
├── mongo
│ ├── mongo-date-fix.js
│ └── find-duplicate-events.js
├── usernames
└── archive-processor
├── app
└── github_contributions.py
├── .gitignore
├── requirements-w32.txt
├── requirements.txt
├── manage
├── ghc-app
├── models.go
├── controller.go
└── main.go
├── README.md
└── fixtures
└── 2990966171.json
/web/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/conf/galaxy-roles.txt:
--------------------------------------------------------------------------------
1 | kamaln7.swapfile
2 |
--------------------------------------------------------------------------------
/.agignore:
--------------------------------------------------------------------------------
1 | web/node_modules
2 | web/js/templates.js
3 | web/public
4 |
--------------------------------------------------------------------------------
/conf/ssmtp-revaliases:
--------------------------------------------------------------------------------
1 | root:postmaster@githubcontributions.io:smtp.mailgun.org:587
2 |
--------------------------------------------------------------------------------
/web/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/img/favicon.ico
--------------------------------------------------------------------------------
/web/octicons/octicons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/octicons/octicons.eot
--------------------------------------------------------------------------------
/web/octicons/octicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/octicons/octicons.ttf
--------------------------------------------------------------------------------
/web/octicons/octicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/octicons/octicons.woff
--------------------------------------------------------------------------------
/web/img/digital-ocean-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/img/digital-ocean-badge.png
--------------------------------------------------------------------------------
/web/octicons/octicons-local.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/octicons/octicons-local.ttf
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleladd/github-contributions/master/web/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/conf/github-contributions-process.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=GitHub Contributions Process Task
3 |
4 | [Service]
5 | ExecStart=/srv/github-contributions/conf/process-job
6 |
--------------------------------------------------------------------------------
/conf/lets-encrypt-config.ini:
--------------------------------------------------------------------------------
1 | rsa-key-size = 4096
2 | email = liam@tenex.tech
3 | domains = githubcontributions.io
4 | text = True
5 | webroot-path = /github-contributions/web/public
6 |
--------------------------------------------------------------------------------
/util/mongo-tunnel:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | SERVER="ibm704.githubcontributions.io"
3 | PORT="27017"
4 | echo "Tunneling to production MongoDB"
5 | ssh -N -v "-L${PORT}:localhost:${PORT}" "${SERVER}"
6 |
--------------------------------------------------------------------------------
/app/github_contributions.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from flask.ext.pymongo import PyMongo
3 | app = Flask(__name__)
4 | app.config['MONGO_DBNAME'] = 'contributions'
5 | mongo = PyMongo(app)
6 |
--------------------------------------------------------------------------------
/conf/github-contributions-process.timer:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Run GHC archive processor hourly
3 |
4 | [Timer]
5 | Persistent=true
6 | OnCalendar=hourly
7 |
8 | [Install]
9 | WantedBy=timers.target
--------------------------------------------------------------------------------
/conf/inventory:
--------------------------------------------------------------------------------
1 | [github-contributions]
2 | [github-contributions:children]
3 | web
4 | database
5 |
6 | [web]
7 | univac.githubcontributions.io
8 |
9 | [database]
10 | univac.githubcontributions.io
11 |
--------------------------------------------------------------------------------
/web/templates/user.repositories.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ghc-app/bindata.go
2 | ghc-app/ghc-app
3 | /event-digest/event-digest
4 | *.exe
5 | *.pyc
6 | venv
7 | /web/node_modules
8 | /web/public
9 | !/web/public/.gitkeep
10 | /web/js/templates.js
11 | *.test
12 |
13 |
--------------------------------------------------------------------------------
/requirements-w32.txt:
--------------------------------------------------------------------------------
1 | filelock==1.0.3
2 | Flask==0.10.1
3 | Flask-PyMongo==0.4.0
4 | Flask-Script==2.0.5
5 | MarkupSafe==0.23
6 | pymongo==3.1.1
7 | python-dateutil==2.4.2
8 | requests==2.7.0
9 | six==1.9.0
10 | termcolor==1.1.0
11 | rollbar=0.11.2
12 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | filelock==1.0.3
2 | Flask==0.10.1
3 | Flask-PyMongo==0.4.0
4 | Flask-Script==2.0.5
5 | pymongo==3.1.1
6 | python-dateutil==2.4.2
7 | python-rapidjson==0.0.6
8 | requests==2.7.0
9 | six==1.9.0
10 | termcolor==1.1.0
11 | rollbar==0.11.2
12 |
--------------------------------------------------------------------------------
/util/mongo/mongo-date-fix.js:
--------------------------------------------------------------------------------
1 | db.contributions.find().snapshot().forEach(
2 | function (e) {
3 | if (typeof e.created_at === 'string') {
4 | e.created_at = ISODate(e.created_at);
5 | db.contributions.save(e);
6 | }
7 | }
8 | )
9 |
--------------------------------------------------------------------------------
/conf/ssmtp.conf:
--------------------------------------------------------------------------------
1 | root=postmaster@githubcontributions.io
2 | mailhub=smtp.mailgun.org:587
3 | rewriteDomain=githubcontributions.io
4 | hostname={{ inventory_hostname }}
5 | UseTLS=Yes
6 | UseSTARTTLS=Yes
7 | AuthUser=postmaster@githubcontributions.io
8 | AuthPass={{ smtp_auth_pass }}
9 |
--------------------------------------------------------------------------------
/conf/mongod.conf:
--------------------------------------------------------------------------------
1 | storage:
2 | dbPath: "/srv/db"
3 | engine: "wiredTiger"
4 | wiredTiger:
5 | collectionConfig:
6 | blockCompressor: snappy
7 |
8 | systemLog:
9 | destination: file
10 | path: "/var/log/mongodb/mongodb.log"
11 | logAppend: true
12 | timeStampFormat: iso8601-utc
13 |
14 | net:
15 | bindIp: "127.0.0.1"
16 | port: 27017
17 |
--------------------------------------------------------------------------------
/conf/build-ghc-app.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | export GOPATH="${HOME}/go"
4 | export PATH="$PATH:/usr/local/go/bin:$GOPATH/bin"
5 | export GHC_APP_GOPATH="${GOPATH}/src/ghc-app"
6 | pushd "${GHC_APP_GOPATH}" > /dev/null
7 | go get -u github.com/jteeuwen/go-bindata/...
8 | go generate ghc-app # run go-bindata
9 | go get -d ./...
10 | export GOBIN='/srv/bin'
11 | go install ghc-app
12 | popd
13 |
--------------------------------------------------------------------------------
/conf/github-contributions-app.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=GitHub Contributions Application Server
3 |
4 | [Service]
5 | Type=simple
6 | User=ghc
7 | Group=ghc
8 | Environment=GHC_ENV=production
9 | Environment=GHC_ROLLBAR_TOKEN={{ rollbar_token }}
10 | Environment=GHC_EVENTS_PATH=/github-archive/events
11 | ExecStart=/srv/bin/ghc-app
12 | Restart=always
13 |
14 | [Install]
15 | WantedBy=multi-user.target
16 |
--------------------------------------------------------------------------------
/conf/ghc-env.sh:
--------------------------------------------------------------------------------
1 | export GHC_ENV="production"
2 | export GHC_LOCKFILE_PATH="/tmp/archive-processor.lock"
3 | export GHC_EVENTS_PATH="/srv/github-archive/events"
4 | export GHC_TIMELINE_PATH="/srv/github-archive/timeline"
5 | export GHC_TRANSFORMED_PATH="/srv/github-archive/transformed"
6 | export GHC_LOADED_PATH="/srv/github-archive/loaded"
7 | export GHC_LOG_PATH="/srv/github-archive/logs"
8 | export GHC_ROLLBAR_TOKEN="{{ rollbar_token }}"
9 |
--------------------------------------------------------------------------------
/web/css/site.css:
--------------------------------------------------------------------------------
1 | html
2 | {
3 | overflow: -moz-scrollbars-vertical;
4 | overflow: scroll;
5 | min-height: 100%;
6 | position:relative;
7 | }
8 |
9 | body
10 | {
11 | margin-top:60px;
12 | margin-bottom:60px;
13 | padding-bottom: 70px;
14 | }
15 |
16 | .bsod
17 | {
18 | font-family: 'VT323';
19 | background-color: #0000aa;
20 | color: #ffffff;
21 | font-size: 1.2em;
22 | padding: 20px;
23 | }
24 |
25 | .bsod-title
26 | {
27 | background-color: #aaaaaa;
28 | color: #0000aa;
29 | }
30 |
--------------------------------------------------------------------------------
/web/js/stats.controller.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | angular
3 | .module('ghca')
4 | .controller("StatisticsController", StatisticsController);
5 |
6 | StatisticsController.$inject = ["$scope", "$log", "moment", "Statistics"];
7 |
8 | function StatisticsController($scope, $log, moment, Statistics) {
9 | $scope.stats = Statistics.get(
10 | {}, function(statsData) {
11 | $scope.retrieved = true;
12 | }
13 | );
14 | }
15 | })();
16 |
--------------------------------------------------------------------------------
/web/js/services.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var app = angular.module('ghca.services', [
3 | require('angular-resource')
4 | ]);
5 | app.factory('User', ['$resource', function($resource) {
6 | return $resource('/api/user/:username');
7 | }]);
8 | app.factory('Event', ['$resource', function($resource) {
9 | return $resource('/api/user/:username/events/:page');
10 | }]);
11 | app.factory('Statistics', ['$resource', function($resource) {
12 | return $resource('/api/stats');
13 | }]);
14 | })();
15 |
--------------------------------------------------------------------------------
/web/templates/search-form.html:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/web/js/search.controller.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | angular
3 | .module('ghca')
4 | .controller('SearchController', SearchController);
5 |
6 | SearchController.$inject = ['$scope', '$state'];
7 |
8 | function SearchController($scope, $state) {
9 | var vm = this;
10 |
11 | vm.username = '';
12 | vm.doSearch = doSearch;
13 |
14 | //////////
15 |
16 | function doSearch() {
17 | $state.go('root.user.repositories', {
18 | username: vm.username
19 | });
20 | }
21 | }
22 | })();
23 |
--------------------------------------------------------------------------------
/web/js/bsod.controller.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | angular
3 | .module('ghca')
4 | .controller('BsodInstanceCtrl', BsodInstanceController);
5 |
6 | BsodInstanceController.$inject = [
7 | '$scope', '$uibModalInstance', 'errorDescription'
8 | ];
9 |
10 | function BsodInstanceController($scope, $uibModalInstance, errorDescription) {
11 | $scope.errorDescription = errorDescription;
12 | $scope.ok = function() {
13 | $uibModalInstance.close();
14 | };
15 | }
16 | })();
17 |
--------------------------------------------------------------------------------
/web/templates/bsod.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Windows
4 |
5 |
An error has occured. To continue:
6 |
Press Enter to return to Windows, or
7 |
Press CTRL_ALT_DEL to restart your computer. If you do this, you will lose any unsaved information in all open applications.
8 |
Error: {{ $root.errorDescription }}
9 |
Press any key to continue
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/web/js/app.js:
--------------------------------------------------------------------------------
1 | var angular = require('angular');
2 |
3 | require('./services.js');
4 | require('./truncate.js');
5 | require('./templates.js');
6 |
7 | (function() {
8 | angular.module('ghca', [
9 | 'ghca.services',
10 | 'truncate',
11 | 'templates',
12 | require('angular-moment'),
13 | require('angular-ui-bootstrap'),
14 | require('angular-ui-router')
15 | ]).config(function($locationProvider) {
16 | $locationProvider.html5Mode(true).hashPrefix('!');
17 | });
18 | })();
19 |
20 | require('./routes.js');
21 | require('./error-handler.js');
22 | require('./bsod.controller.js');
23 | require('./search.controller.js');
24 | require('./user.controller.js');
25 | require('./stats.controller.js');
26 |
--------------------------------------------------------------------------------
/conf/server-monitor:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | HOST="https://githubcontributions.io/api/stats"
3 | MAX_AGE="7500" # seconds
4 | SCAPEGOAT="engineers@tenex.tech"
5 |
6 | function notify_failure()
7 | {
8 | if [ -t 1 ]
9 | then
10 | echo "old records detected. notifying ${SCAPEGOAT}"
11 | fi
12 | age=$1
13 | msg="Error: recent events are not being entered into the database.\n"
14 | msg="${msg} Maximum age of newest record is ${MAX_AGE} seconds\n"
15 | msg="${msg} Currently the newest record is ${age} seconds old.\n"
16 | printf "${msg}" | mail -s "ERROR: GitHub Contributions" "${SCAPEGOAT}"
17 | }
18 |
19 | age="$(curl --location "${HOST}" 2>/dev/null | jq '.latestEventAge')"
20 | (($age > $MAX_AGE)) && notify_failure "${age}"
21 |
--------------------------------------------------------------------------------
/conf/secrets.yml:
--------------------------------------------------------------------------------
1 | $ANSIBLE_VAULT;1.1;AES256
2 | 65386238626365316338613766626133336230646661636530653465323233356665343537643265
3 | 6164646138346366303261353739616461633461373430610a376662616162333136373931346361
4 | 61383066376132653133343138646538306563633561386131373233366433383134303538623934
5 | 3631356432356530350a313334623837303339653932363765373838666137396533313164383732
6 | 34323630303139383663396432666666353038343163303336356535303466393936353838333838
7 | 34393835346131303864303432383232343033343934333136333534626432306235313139666464
8 | 35633838313239393430356363393831643932633334313464336261623539333133333861643933
9 | 36653837333966336132373832363130383338326535303532626233623334316266383833653361
10 | 66653363653130363165663239303033636238303864393938346465373935373837
11 |
--------------------------------------------------------------------------------
/web/js/ie10-viewport-bug-workaround.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * IE10 viewport hack for Surface/desktop Windows 8 bug
3 | * Copyright 2014 Twitter, Inc.
4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. For
5 | * details, see http://creativecommons.org/licenses/by/3.0/.
6 | */
7 |
8 | // See the Getting Started docs for more information:
9 | // http://getbootstrap.com/getting-started/#support-ie10-width
10 |
11 | (function () {
12 | 'use strict';
13 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
14 | var msViewportStyle = document.createElement('style')
15 | msViewportStyle.appendChild(
16 | document.createTextNode(
17 | '@-ms-viewport{width:auto!important}'
18 | )
19 | )
20 | document.querySelector('head').appendChild(msViewportStyle)
21 | }
22 | })();
23 |
--------------------------------------------------------------------------------
/conf/process-job-w32.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 | SET WORKING_PATH=D:\github-contributions
3 | SET PROCESSOR=%WORKING_PATH%\util\archive-processor
4 | SET DATA_PATH=D:\github-archive
5 |
6 | SET GHC_LOCKFILE_PATH=%DATA_PATH%\archive-processor.lock
7 | SET GHC_EVENTS_PATH=%DATA_PATH%\events
8 | SET GHC_TIMELINE_PATH=%DATA_PATH%\timeline
9 | SET GHC_TRANSFORMED_PATH=%DATA_PATH%\transformed
10 | SET GHC_LOADED_PATH=%DATA_PATH%\loaded
11 | SET GHC_LOG_PATH=%DATA_PATH%\logs
12 |
13 | ECHO GHCA Archive Processor on Windows
14 |
15 | REM Don't even try to run multiple copies
16 | REM If we do, they'll just get queued, but then we might have a lot of processes.
17 | IF EXIST %GHC_LOCKFILE_PATH% (
18 | ECHO "archive-processor already running (lockfile: %GHC_LOCKFILE_PATH%)"
19 | EXIT
20 | )
21 |
22 | CD %WORKING_PATH%
23 |
24 | python -Wd %PROCESSOR% process
25 |
--------------------------------------------------------------------------------
/util/usernames:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | from __future__ import print_function
3 | import os
4 | import sys
5 | import glob
6 | import gzip
7 | import json
8 |
9 |
10 | def usernames():
11 | """ generate list of distinct usernames from transformed data
12 | """
13 | users = set()
14 | files = list(glob.glob(os.path.join(
15 | os.environ['GHC_TRANSFORMED_PATH'], '*.json.gz')))
16 | print("looking in", len(files), "files")
17 | for ix, path in enumerate(files):
18 | with gzip.open(path, 'rt', encoding='utf-8') as f:
19 | events = (json.loads(line) for line in f)
20 | users |= set((e.get('_user_lower') for e in events))
21 | print(ix, "\t", len(files))
22 |
23 | users = sorted(users)
24 |
25 | with open('users.txt', 'wt') as f:
26 | for u in users: print(u, file=f)
27 |
28 | print("there are", len(users), "users")
29 |
30 | if __name__=='__main__':
31 | usernames()
32 |
--------------------------------------------------------------------------------
/util/mongo/find-duplicate-events.js:
--------------------------------------------------------------------------------
1 | // This shouldn't ever need to be run unless the existing data needs re-importing
2 | // Takes 40 minutes to clear 10 records :(
3 | db.contributions.aggregate(
4 | [
5 | {
6 | "$match": {
7 | "_event_id": { "$exists" : true }
8 | },
9 | },
10 | {
11 | "$group": {
12 | "_id": { "_event_id": "$_event_id" },
13 | "uniqueIds": { "$push": "$_id" },
14 | "count": { "$sum": 1 }
15 | },
16 | },
17 | {
18 | "$match": {
19 | "count": { "$gt": 1 }
20 | }
21 | },
22 | {
23 | $out : "duplicates"
24 | }
25 | ],
26 | {
27 | "allowDiskUse": true
28 | }
29 | );
30 | // .forEach(function(doc) {
31 | // doc.uniqueIds.shift();
32 | // printjson(doc);
33 | // var wRes = db.contributions.remove(
34 | // {
35 | // "_id": {
36 | // "$in": doc.uniqueIds,
37 | // },
38 | // },
39 | // {
40 | // "justOne": true,
41 | // });
42 | // print(wRes);
43 | // });
44 |
--------------------------------------------------------------------------------
/web/img/gps.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/manage:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | from __future__ import print_function
3 | import os, sys
4 | abspath = os.path.abspath(__file__)
5 | dname = os.path.dirname(abspath)
6 | sys.path.insert(0, os.path.join(dname, "app"))
7 |
8 | from flask.ext.script import Manager, Server, Shell
9 | from app.github_contributions import app, mongo
10 |
11 | def _make_context():
12 | return dict(
13 | app=app,
14 | mongo=mongo
15 | )
16 |
17 | manager = Manager(app)
18 | manager.add_command('shell', Shell(
19 | make_context=_make_context,
20 | use_ipython=True,
21 | banner='GitHub Contributions Shell'
22 | ))
23 |
24 | @manager.command
25 | def ensure_indexes():
26 | """ ensure contributions are indexed
27 | """
28 | collection = mongo.db.contributions
29 | print("indexing _event_id")
30 | collection.create_index("_event_id",
31 | sparse=True,
32 | unique=True)
33 | print("indexing _user_lower")
34 | collection.create_index("_user_lower")
35 | print("indexing created_at")
36 | collection.create_index("created_at")
37 |
38 | if __name__=='__main__':
39 | manager.run()
40 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-contributions-www",
3 | "private": true,
4 | "version": "0.0.1",
5 | "description": "GitHub Contributions Archive",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "github.com/tenex/github-contributions"
13 | },
14 | "keywords": [
15 | "github",
16 | "git"
17 | ],
18 | "author": "Tenex Developers",
19 | "license": "ISC",
20 | "devDependencies": {
21 | "browserify": "^13.0.0",
22 | "connect": "^3.4.1",
23 | "del": "^2.2.0",
24 | "gulp": "^3.9.1",
25 | "gulp-angular-templatecache": "^1.8.0",
26 | "gulp-concat": "^2.6.0",
27 | "gulp-connect": "^2.3.1",
28 | "gulp-imagemin": "^2.4.0",
29 | "gulp-minify-css": "^1.2.3",
30 | "gulp-sourcemaps": "^1.6.0",
31 | "gulp-uglify": "^1.5.2",
32 | "gulp-util": "^3.0.7",
33 | "vinyl-source-stream": "^1.1.0"
34 | },
35 | "dependencies": {
36 | "angular": "^1.5.0",
37 | "angular-moment": "^1.0.0-beta.4",
38 | "angular-resource": "^1.5.0",
39 | "angular-ui-bootstrap": "^1.1.2",
40 | "angular-ui-router": "^0.2.18"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/conf/process-job:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | MAILTO="engineers@tenex.tech"
3 | WORKING_PATH="/srv/github-contributions"
4 | PROCESSOR="/srv/github-contributions/util/archive-processor"
5 | PYTHON="/srv/venv/bin/python3"
6 | source /etc/ghc-env.sh
7 |
8 | echo "GHCA Archive Processor"
9 | /usr/bin/uptime
10 |
11 | # All loaded files should be hard links to files in
12 | # ${GHC_TRANSFORMED_PATH}. If they are not, it will cause
13 | # the archive processor to be unable to figure out what to load.
14 | LOADED_UNLINKED=$(find "${GHC_LOADED_PATH}" -type f -links 1)
15 | if [[ -n $LOADED_UNLINKED ]]
16 | then
17 | ERR="files found in ${GHC_LOADED_PATH} which are orphaned:"
18 | ERR="${ERR}\n${LOADED_UNLINKED}"
19 | echo "${ERR}"
20 | echo "${ERR}" | mail -s "GHC: ERROR: Orphaned loaded events!" "${MAILTO}"
21 | exit 1
22 | fi
23 |
24 |
25 | # Don't even try to run multiple copies
26 | # If we do, they'll just get queued, but then we might have a lot of processes.
27 | if [[ -f "${GHC_LOCKFILE_PATH}" ]];
28 | then
29 | echo "archive-processor already running (lockfile: ${GHC_LOCKFILE_PATH})"
30 | exit 1
31 | fi
32 |
33 | /bin/mkdir -p "${WORKING_PATH}"
34 | cd "${WORKING_PATH}"
35 |
36 | ${PYTHON} ${PROCESSOR} 'process'
37 |
38 | /usr/bin/uptime
39 |
--------------------------------------------------------------------------------
/web/js/error-handler.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | angular
3 | .module('ghca')
4 | .config(ConfigureErrorHandler);
5 |
6 | ConfigureErrorHandler.$inject = ['$httpProvider'];
7 |
8 | function ConfigureErrorHandler($httpProvider) {
9 | $httpProvider.interceptors.push(function($q, $rootScope, $log, $injector) {
10 | return {
11 | 'responseError': function(rejection) {
12 | $log.debug(rejection);
13 | $rootScope.errorDescription = rejection.data.error;
14 | $injector.get('$uibModal').open({
15 | templateUrl: 'bsod.html',
16 | controller: 'BsodInstanceCtrl',
17 | keyboard: true,
18 | windowClass: 'bsod',
19 | size: 'lg',
20 | resolve: {
21 | errorDescription: function() {
22 | return rejection.data.error;
23 | }
24 | }
25 | });
26 | return $q.reject(rejection);
27 | }
28 | };
29 | });
30 | }
31 | })();
32 |
--------------------------------------------------------------------------------
/conf/lets-encrypt-renew-certificate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | web_service='nginx'
4 | config_file='/github-contributions/conf/lets-encrypt-config.ini'
5 | le_path='/opt/letsencrypt'
6 | exp_limit=30;
7 |
8 | if [ ! -f $config_file ]; then
9 | echo "[ERROR] config file does not exist: $config_file"
10 | exit 1;
11 | fi
12 |
13 | domain=githubcontributions.io
14 | cert_file="/etc/letsencrypt/live/$domain/fullchain.pem"
15 |
16 | if [ ! -f $cert_file ]; then
17 | echo "[ERROR] certificate file not found for domain $domain."
18 | fi
19 |
20 | exp=$(date -d "`openssl x509 -in $cert_file -text -noout|grep "Not After"|cut -c 25-`" +%s)
21 | datenow=$(date -d "now" +%s)
22 | days_exp=$(echo \( $exp - $datenow \) / 86400 |bc)
23 |
24 | echo "Checking expiration date for $domain..."
25 |
26 | if [ "$days_exp" -gt "$exp_limit" ] ; then
27 | echo "The certificate is up to date, no need for renewal ($days_exp days left)."
28 | exit 0;
29 | else
30 | echo "The certificate for $domain is about to expire soon. Starting webroot renewal script..."
31 | $le_path/letsencrypt-auto certonly -a webroot --agree-tos --renew-by-default --config $config_file
32 | echo "Reloading $web_service"
33 | /usr/sbin/service $web_service reload
34 | echo "Renewal process finished for domain $domain"
35 | exit 0;
36 | fi
37 |
--------------------------------------------------------------------------------
/web/js/routes.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | angular
3 | .module('ghca')
4 | .config(ConfigureRoutes);
5 |
6 | ConfigureRoutes.$inject = ['$stateProvider', '$urlRouterProvider'];
7 |
8 | function ConfigureRoutes($stateProvider, $urlRouterProvider) {
9 | $urlRouterProvider.otherwise('/');
10 | $stateProvider
11 | .state('root', {
12 | url: '/',
13 | views: {
14 | search: {
15 | templateUrl: 'search-form.html',
16 | controller: 'SearchController',
17 | controllerAs: 'search'
18 | }
19 | }
20 | })
21 | .state('root.user', {
22 | url: 'user/{username}',
23 | abstract: true,
24 | views: {
25 | "@": {
26 | controller: 'UserController',
27 | controllerAs: 'userVm',
28 | templateUrl: 'user.common.html',
29 | resolve: {
30 | "username": function($stateParams) {
31 | return $stateParams.username;
32 | }
33 | }
34 | }
35 | }
36 | })
37 | .state('root.user.repositories', {
38 | url: '',
39 | templateUrl: 'user.repositories.html'
40 | })
41 | .state('root.user.events', {
42 | url: '/events/{page:int}',
43 | templateUrl: 'user.events.html'
44 | });
45 | }
46 | })();
47 |
--------------------------------------------------------------------------------
/conf/github-contributions.nginx.conf:
--------------------------------------------------------------------------------
1 | # ; -*-mode: nginx;-*-
2 | server {
3 | listen 443 ssl;
4 | server_name githubcontributions.io;
5 | charset utf-8;
6 | client_max_body_size 100M;
7 |
8 | root /srv/github-contributions/web/public;
9 |
10 | ssl_certificate /etc/letsencrypt/live/githubcontributions.io/fullchain.pem;
11 | ssl_certificate_key /etc/letsencrypt/live/githubcontributions.io/privkey.pem;
12 |
13 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
14 | ssl_prefer_server_ciphers on;
15 | ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
16 |
17 | location /user/ {
18 | try_files $uri $uri/ /index.html =404; # HTML5 Push State
19 | }
20 |
21 | location /api/ {
22 | proxy_set_header X-Real-IP $remote_addr;
23 | proxy_set_header X-Forwarded-For $remote_addr;
24 | proxy_set_header Host $host;
25 | proxy_pass http://127.0.0.1:5000/;
26 | proxy_read_timeout 600s;
27 | }
28 |
29 | location /archive/events {
30 | alias /srv/github-archive/events/;
31 | autoindex on;
32 | }
33 |
34 | location /archive/timeline {
35 | alias /srv/github-archive/timeline/;
36 | autoindex on;
37 | }
38 |
39 | location ~ /.well-known {
40 | allow all;
41 | }
42 | }
43 |
44 | server {
45 | listen 80;
46 | server_name githubcontributions.io;
47 | return 301 https://$host$request_uri;
48 | }
49 |
--------------------------------------------------------------------------------
/web/js/user.controller.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | angular
3 | .module('ghca')
4 | .controller("UserController",
5 | UserController);
6 |
7 | UserController.$inject = [
8 | "$log", "User", "Event", "username"
9 | ];
10 |
11 | function UserController($log, User, Event, username) {
12 | var vm = this;
13 |
14 | // User stuff
15 | vm.processing = true;
16 | vm.username = username;
17 | vm.userUrl = "";
18 | vm.user = User.get({username: username}, function(user) {
19 | vm.processing = false;
20 | vm.userUrl = "https://github.com/" + user.username;
21 | });
22 |
23 | // Event stuff
24 | vm.loadingEvents = false;
25 | vm.eventPages = {};
26 | vm.eventData = {};
27 | vm.currentEventPage = 1;
28 | vm.eventPageSize = 50;
29 | vm.eventPageChanged = function() {
30 | fetchEventPage(vm.currentEventPage);
31 | };
32 |
33 | // Preload
34 | fetchEventPage(1);
35 |
36 | function fetchEventPage(pageNumber) {
37 | vm.loadingEvents = true;
38 | vm.eventData = Event.get({
39 | username: username,
40 | page: pageNumber
41 | }, function(events) {
42 | vm.eventPages[pageNumber] = events;
43 | vm.loadingEvents = false;
44 | });
45 | }
46 | }
47 | })();
48 |
--------------------------------------------------------------------------------
/web/templates/user.common.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{userVm.user.username}}
6 |
7 | made
8 | {{userVm.user.eventCount}} contributions
9 | to
10 | {{userVm.user.repos.length}} repositories
11 |
12 |
13 |
14 |
17 |
19 |
20 | No results for {{ userVm.username }}.
21 |
22 |
23 |
24 |
40 |
41 |
42 |
43 |
52 |
--------------------------------------------------------------------------------
/web/js/event-octicon.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | angular
3 | .module("ghca")
4 | .directive("eventOcticon", EventOcticon);
5 |
6 | function EventOcticon() {
7 | var octiconMap = {
8 | "GollumEvent": "book",
9 | "IssuesEvent": "issue-opened",
10 | "PushEvent": "repo-push",
11 | "CommitCommentEvent": "comment",
12 | "ReleaseEvent": "tag",
13 | "PublicEvent": "megaphone",
14 | "MemberEvent": "person",
15 | "IssueCommentEvent": "comment-discussion"
16 | };
17 |
18 | var eventDescriptionMap = {
19 | "GollumEvent": "Wiki",
20 | "IssuesEvent": "Issue",
21 | "PushEvent": "Push",
22 | "CommitCommentEvent": "Commit Comment",
23 | "ReleaseEvent": "Release",
24 | "PublicEvent": "Repository made public",
25 | "MemberEvent": "Membership change",
26 | "IssueCommentEvent": "Issue comment"
27 | };
28 |
29 | return {
30 | restrict: "A",
31 | require: "^ngModel",
32 | scope: {
33 | ngModel: '='
34 | },
35 | template: '',
36 | link: function(scope, element, attrs) {
37 | element.addClass("octicon");
38 | element.addClass("octicon-" + octiconMap[scope.ngModel]);
39 | element.attr("data-toggle", "tooltip");
40 | element.attr("data-placement", "left");
41 | element.attr("title", eventDescriptionMap[scope.ngModel]);
42 | $(element).tooltip();
43 | }
44 | };
45 | }
46 | })();
47 |
--------------------------------------------------------------------------------
/web/js/truncate.js:
--------------------------------------------------------------------------------
1 | angular.module('truncate', [])
2 | .filter('characters', function () {
3 | return function (input, chars, breakOnWord) {
4 | if (isNaN(chars)) return input;
5 | if (chars <= 0) return '';
6 | if (input && input.length > chars) {
7 | input = input.substring(0, chars);
8 |
9 | if (!breakOnWord) {
10 | var lastspace = input.lastIndexOf(' ');
11 | //get last space
12 | if (lastspace !== -1) {
13 | input = input.substr(0, lastspace);
14 | }
15 | }else{
16 | while(input.charAt(input.length-1) === ' '){
17 | input = input.substr(0, input.length -1);
18 | }
19 | }
20 | return input + '…';
21 | }
22 | return input;
23 | };
24 | })
25 | .filter('splitcharacters', function() {
26 | return function (input, chars) {
27 | if (isNaN(chars)) return input;
28 | if (chars <= 0) return '';
29 | if (input && input.length > chars) {
30 | var prefix = input.substring(0, chars/2);
31 | var postfix = input.substring(input.length-chars/2, input.length);
32 | return prefix + '...' + postfix;
33 | }
34 | return input;
35 | };
36 | })
37 | .filter('words', function () {
38 | return function (input, words) {
39 | if (isNaN(words)) return input;
40 | if (words <= 0) return '';
41 | if (input) {
42 | var inputWords = input.split(/\s+/);
43 | if (inputWords.length > words) {
44 | input = inputWords.slice(0, words).join(' ') + '…';
45 | }
46 | }
47 | return input;
48 | };
49 | });
50 |
--------------------------------------------------------------------------------
/web/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var browserify = require('browserify');
3 | var connect = require('connect');
4 | var source = require('vinyl-source-stream');
5 | var concat = require('gulp-concat');
6 | var uglify = require('gulp-uglify');
7 | var minifyCss = require('gulp-minify-css');
8 | var imagemin = require('gulp-imagemin');
9 | var sourcemaps = require('gulp-sourcemaps');
10 | var templateCache = require('gulp-angular-templatecache');
11 | var del = require('del');
12 |
13 | var paths = {
14 | static: ['index.html', 'octicons/**/*', 'img/**/*'],
15 | fonts: ['fonts/*'],
16 | scripts: 'js/*.js',
17 | stylesheets: 'css/*.css',
18 | templates: 'templates/**/*.html'
19 | };
20 |
21 | gulp.task('browserify', ['templates'], function() {
22 | return browserify('js/app.js')
23 | .bundle()
24 | .pipe(source('main.js'))
25 | .pipe(gulp.dest('public/'));
26 | });
27 |
28 | gulp.task('stylesheets', function() {
29 | return gulp.src(paths.stylesheets)
30 | .pipe(sourcemaps.init())
31 | .pipe(minifyCss())
32 | .pipe(sourcemaps.write())
33 | .pipe(gulp.dest('public'));
34 | });
35 |
36 | gulp.task('static', function() {
37 | return gulp.src(paths.static)
38 | .pipe(gulp.dest('public'));
39 | });
40 |
41 | gulp.task('fonts', function() {
42 | return gulp.src(paths.fonts)
43 | .pipe(gulp.dest('public/fonts'));
44 | });
45 |
46 | gulp.task('clean', function() {
47 | return del(['public/**/*']);
48 | });
49 |
50 | gulp.task('templates', function() {
51 | return gulp.src(paths.templates)
52 | .pipe(templateCache('templates.js', {
53 | standalone: true
54 | }))
55 | .pipe(gulp.dest('js/'));
56 | });
57 |
58 | gulp.task('watch', ['browserify', 'static', 'stylesheets'], function() {
59 | gulp.watch([paths.templates, paths.scripts], ['browserify']);
60 | gulp.watch(paths.static, ['static']);
61 | gulp.watch(paths.stylesheets, ['stylesheets']);
62 | });
63 |
64 | gulp.task('connect', function () {
65 | connect.server({
66 | root: 'public',
67 | port: 4000
68 | });
69 | });
70 |
71 | gulp.task('default', [
72 | 'browserify',
73 | 'static',
74 | 'stylesheets',
75 | 'fonts'
76 | ]);
77 |
--------------------------------------------------------------------------------
/ghc-app/models.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sort"
5 | "strings"
6 | "time"
7 |
8 | "gopkg.in/mgo.v2"
9 | "gopkg.in/mgo.v2/bson"
10 | )
11 |
12 | // PageSize set the size of the result set where applicable
13 | const PageSize = 50
14 |
15 | // UserContributionsFunc returns raw BSON records of a user's contributions
16 | type UserContributionsFunc func(string, int) ([]bson.M, error)
17 |
18 | // UserContributionsFactory returns a UserContributionsFunc that can be used
19 | // to retrieve a user's contributions given their username and a zero-based skip
20 | func UserContributionsFactory(c *mgo.Collection) UserContributionsFunc {
21 | return func(username string, skip int) ([]bson.M, error) {
22 | username = strings.ToLower(username)
23 |
24 | var events []bson.M
25 | query := c.Find(bson.M{"_user_lower": username})
26 | query = query.Sort("-created_at")
27 | query = query.Skip(skip).Limit(PageSize)
28 | err := query.All(&events)
29 | if err != nil {
30 | return nil, err
31 | }
32 | return events, nil
33 | }
34 | }
35 |
36 | //////////////////
37 | // User Summary //
38 | //////////////////
39 |
40 | // UserSummary describes a brief summary of a user's contributions
41 | type UserSummary struct {
42 | Username string `json:"username"`
43 | Repositories []string `json:"repos"`
44 | EventCount int `json:"eventCount"`
45 | }
46 |
47 | // UserSummaryFunc returns an instance of UserSummary
48 | // given a username
49 | type UserSummaryFunc func(string) (*UserSummary, error)
50 |
51 | // UserSummaryFactory returns a UserSummaryFunc that can be used
52 | // to retrieve a user's summary
53 | func UserSummaryFactory(c *mgo.Collection) UserSummaryFunc {
54 | return func(username string) (*UserSummary, error) {
55 | username = strings.ToLower(username)
56 | query := c.Find(bson.M{"_user_lower": username})
57 |
58 | repoList := []string{}
59 | err := query.Distinct("repo", &repoList)
60 | if err != nil {
61 | return nil, err
62 | }
63 | sort.Strings(repoList)
64 |
65 | ct, err := query.Count()
66 | if err != nil {
67 | return nil, err
68 | }
69 |
70 | return &UserSummary{
71 | Username: username,
72 | Repositories: repoList,
73 | EventCount: ct,
74 | }, nil
75 | }
76 | }
77 |
78 | ////////////////
79 | // Statistics //
80 | ////////////////
81 |
82 | // GHCStats describes statistics about the project's database
83 | type GHCStats struct {
84 | EventCount int `json:"eventCount"`
85 | LatestEvent time.Time `json:"latestEvent"`
86 | LatestEventAge int64 `json:"latestEventAge"`
87 | }
88 |
89 | // GHCStatsFunc returns the latest statistics
90 | type GHCStatsFunc func() (*GHCStats, error)
91 |
92 | // GHCStatsFactory returns a GHCStatsFunc which can be used
93 | // to reteurn statistics about the project
94 | func GHCStatsFactory(c *mgo.Collection) GHCStatsFunc {
95 | return func() (*GHCStats, error) {
96 | ct, err := c.Count()
97 | if err != nil {
98 | return nil, err
99 | }
100 |
101 | var latestEvt bson.M
102 | err = c.Find(nil).Sort("-created_at").One(&latestEvt)
103 | if err != nil {
104 | return nil, err
105 | }
106 | latestEvtTime, err := time.Parse(
107 | time.RFC3339,
108 | latestEvt["created_at"].(string))
109 | latestEvtAge := int64(
110 | time.Now().UTC().Sub(latestEvtTime).Seconds())
111 |
112 | return &GHCStats{
113 | EventCount: ct,
114 | LatestEvent: latestEvtTime,
115 | LatestEventAge: latestEvtAge,
116 | }, nil
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/ghc-app/controller.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "io"
7 | "net/http"
8 | "os"
9 | "path/filepath"
10 | "strconv"
11 |
12 | "github.com/gorilla/mux"
13 | "gopkg.in/mgo.v2"
14 | "gopkg.in/mgo.v2/bson"
15 | )
16 |
17 | // GHCController is the master controller for this application
18 | type GHCController struct {
19 | *mux.Router
20 |
21 | userContributions UserContributionsFunc
22 | userSummary UserSummaryFunc
23 | ghcStats GHCStatsFunc
24 | }
25 |
26 | // NewGHCController is the constructor for GHCController
27 | func NewGHCController(contributions *mgo.Collection) *GHCController {
28 | c := &GHCController{
29 | userContributions: UserContributionsFactory(contributions),
30 | userSummary: UserSummaryFactory(contributions),
31 | ghcStats: GHCStatsFactory(contributions),
32 | Router: mux.NewRouter(),
33 | }
34 | c.HandleFunc("/user/{username}", c.UserSummary)
35 | c.HandleFunc("/user/{username}/events", c.UserEvents)
36 | c.HandleFunc("/user/{username}/events/{page:[0-9]+}", c.UserEvents)
37 | c.HandleFunc("/stats", c.Stats)
38 | c.HandleFunc("/error", c.Error)
39 | c.HandleFunc("/aggregates", c.Aggregates)
40 | return c
41 | }
42 |
43 | func (c *GHCController) Error(_ http.ResponseWriter, _ *http.Request) {
44 | panic(errors.New("error successful"))
45 | }
46 |
47 | // Aggregates serves /aggregates
48 | func (c *GHCController) Aggregates(rw http.ResponseWriter, r *http.Request) {
49 | summaryPath := filepath.Join(
50 | os.Getenv("GHC_EVENTS_PATH"), "summary.json")
51 | f, err := os.Open(summaryPath)
52 | if err != nil {
53 | panic(err)
54 | }
55 | defer f.Close()
56 | rw.WriteHeader(http.StatusOK)
57 | _, err = io.Copy(rw, f)
58 | }
59 |
60 | // UserEventsPage includes <= PageSize number of events and metadata about
61 | // all of the events corresponding to the user
62 | type UserEventsPage struct {
63 | Events []bson.M `json:"events"`
64 | Start int `json:"start"`
65 | End int `json:"end"`
66 | CurrentPage int `json:"currentPage"`
67 | PageCount int `json:"size"`
68 | }
69 |
70 | // UserEvents is a controller action for:
71 | // /user/{username}/events
72 | // /user/{username}/events/[page]
73 | func (c *GHCController) UserEvents(rw http.ResponseWriter, r *http.Request) {
74 | v := mux.Vars(r)
75 | username := v["username"]
76 | page, err := strconv.Atoi(v["page"])
77 | if err != nil {
78 | page = 1
79 | }
80 | skip := (page - 1) * PageSize
81 | contributionBSON, err := c.userContributions(username, skip)
82 | if err != nil {
83 | panic(err) // TODO: Fix
84 | }
85 | eventsPage := UserEventsPage{
86 | Events: contributionBSON,
87 | Start: skip,
88 | End: len(contributionBSON) + skip,
89 | CurrentPage: page,
90 | PageCount: len(contributionBSON),
91 | }
92 | err = serveJSON(rw, eventsPage)
93 | if err != nil {
94 | panic(err) // TODO: Fix
95 | }
96 | }
97 |
98 | // UserSummary is a controller action for /user/{username}
99 | func (c *GHCController) UserSummary(rw http.ResponseWriter, r *http.Request) {
100 | username := mux.Vars(r)["username"]
101 | summary, err := c.userSummary(username)
102 | if err != nil {
103 | panic(err) // TODO: Fix
104 | }
105 | err = serveJSON(rw, summary)
106 | if err != nil {
107 | panic(err) // TODO: Fix
108 | }
109 | }
110 |
111 | // Stats is a controller action for /stats
112 | func (c *GHCController) Stats(rw http.ResponseWriter, r *http.Request) {
113 | stats, err := c.ghcStats()
114 | if err != nil {
115 | panic(err)
116 | }
117 | serveJSON(rw, stats)
118 | }
119 |
120 | func serveJSON(rw http.ResponseWriter, obj interface{}) error {
121 | rw.Header().Set("Content-Type", "application/json")
122 | return json.NewEncoder(rw).Encode(obj)
123 | }
124 |
--------------------------------------------------------------------------------
/ghc-app/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "net"
7 | "net/http"
8 | "os"
9 | "syscall"
10 | "time"
11 |
12 | log "github.com/Sirupsen/logrus"
13 | "github.com/heroku/rollrus"
14 | "gopkg.in/mgo.v2"
15 | "gopkg.in/natefinch/lumberjack.v2"
16 | )
17 |
18 | var (
19 | // AppEnv is: production, staging, development
20 | AppEnv string
21 | )
22 |
23 | func init() {
24 | // This is used to generate Request IDs
25 | rand.Seed(time.Now().UnixNano())
26 |
27 | AppEnv = os.Getenv("GHC_ENV")
28 | if AppEnv == "" {
29 | AppEnv = "development"
30 | }
31 | logDest := os.Getenv("GHC_APP_LOG_PATH")
32 | if logDest == "" {
33 | logDest = "/var/log/ghc/ghc.log"
34 | }
35 | log.SetOutput(&lumberjack.Logger{
36 | Filename: logDest,
37 | MaxSize: 100, // MB
38 | })
39 | if AppEnv == "production" {
40 | rollrus.SetupLogging(os.Getenv("GHC_ROLLBAR_TOKEN"), AppEnv)
41 | }
42 | // PUT THIS AFTER ROLLRUS!
43 | // https://github.com/heroku/rollrus/issues/4
44 | log.SetFormatter(&log.JSONFormatter{})
45 | }
46 |
47 | func makeRequestID() string {
48 | return fmt.Sprintf("%08X", rand.Uint32())
49 | }
50 |
51 | func logHandler(h http.Handler) http.Handler {
52 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53 | rID := makeRequestID()
54 | log.WithFields(log.Fields{
55 | "requestID": rID,
56 | "referer": r.Referer(),
57 | "remoteAddr": r.RemoteAddr,
58 | "url": r.URL.String(),
59 | "userAgent": r.UserAgent(),
60 | "method": r.Method,
61 | }).Info("request")
62 | w.Header().Add("X-GHC-Request-ID", rID)
63 |
64 | startTime := time.Now()
65 | h.ServeHTTP(w, r)
66 | elapsedTime := time.Now().Sub(startTime).Seconds()
67 |
68 | log.WithFields(log.Fields{
69 | "requestID": rID,
70 | "elapsed": elapsedTime,
71 | }).Info("response")
72 | })
73 | }
74 |
75 | // Stops panics with no panic-worthy cause
76 | // Stops:
77 | // - EPIPE, which occurs when a client stops loading a page
78 | func xanax(v interface{}) error {
79 | if v == nil {
80 | return nil
81 | }
82 | var err error
83 |
84 | switch cause := v.(type) {
85 | case *net.OpError:
86 | err = cause
87 | if opErrorCause, ok := cause.Err.(*os.SyscallError); ok {
88 | if opErrorCause.Err == syscall.EPIPE {
89 | err = nil
90 | }
91 | }
92 | case error:
93 | err = cause
94 | default:
95 | err = fmt.Errorf("%v", v)
96 | }
97 | return err
98 | }
99 |
100 | func recoverHandler(h http.Handler) http.Handler {
101 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
102 | defer func() {
103 | if err := xanax(recover()); err != nil {
104 | rawStr := fmt.Sprintf("%#v", err)
105 | log.WithField("raw", rawStr).Error(err.Error())
106 | http.Error(rw, err.Error(), 500)
107 | }
108 | }()
109 | h.ServeHTTP(rw, r)
110 | })
111 | }
112 |
113 | func remoteAddrHandler(h http.Handler) http.Handler {
114 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
115 | remoteAddr := r.Header.Get("X-Forwarded-For")
116 | if remoteAddr != "" {
117 | r.RemoteAddr = remoteAddr
118 | }
119 | h.ServeHTTP(rw, r)
120 | })
121 | }
122 |
123 | func mainHandler(globalSession *mgo.Session) http.Handler {
124 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
125 | session := globalSession.Copy()
126 | defer session.Close()
127 | collection := session.DB("contributions").C("contributions")
128 | NewGHCController(collection).ServeHTTP(rw, r)
129 | })
130 | }
131 |
132 | func main() {
133 | session, err := mgo.Dial("localhost")
134 | if err != nil {
135 | panic(err)
136 | }
137 | defer session.Close()
138 | session.SetSafe(nil) // we never write
139 | session.SetSocketTimeout(15 * time.Minute) // cheaper than SSDs
140 | session.SetMode(mgo.Monotonic, true)
141 | port := os.Getenv("PORT")
142 | if port == "" {
143 | port = "5000"
144 | }
145 | bindEndpoint := net.JoinHostPort("", port)
146 |
147 | handler := mainHandler(session)
148 | handler = recoverHandler(handler)
149 | handler = logHandler(handler)
150 | handler = remoteAddrHandler(handler)
151 |
152 | http.ListenAndServe(bindEndpoint, handler)
153 | }
154 |
--------------------------------------------------------------------------------
/web/css/loader.css:
--------------------------------------------------------------------------------
1 | .tetrominos {
2 | position: absolute;
3 | top: 50%;
4 | left: 50%;
5 | -webkit-transform: translate(-112px, -96px);
6 | -ms-transform: translate(-112px, -96px);
7 | transform: translate(-112px, -96px);
8 | }
9 |
10 | .tetromino {
11 | width: 96px;
12 | height: 112px;
13 | position: absolute;
14 | -webkit-transition: all ease .3s;
15 | transition: all ease .3s;
16 | background: url('data:image/svg+xml;utf-8,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 684"%3E%3Cpath fill="%23010101" d="M305.7 0L0 170.9v342.3L305.7 684 612 513.2V170.9L305.7 0z"/%3E%3Cpath fill="%23fff" d="M305.7 80.1l-233.6 131 233.6 131 234.2-131-234.2-131"/%3E%3C/svg%3E') no-repeat top center;
17 | }
18 |
19 | .box1 {
20 | -webkit-animation: tetromino1 1.5s ease-out infinite;
21 | animation: tetromino1 1.5s ease-out infinite;
22 | }
23 |
24 | .box2 {
25 | -webkit-animation: tetromino2 1.5s ease-out infinite;
26 | animation: tetromino2 1.5s ease-out infinite;
27 | }
28 |
29 | .box3 {
30 | -webkit-animation: tetromino3 1.5s ease-out infinite;
31 | animation: tetromino3 1.5s ease-out infinite;
32 | z-index: 2;
33 | }
34 |
35 | .box4 {
36 | -webkit-animation: tetromino4 1.5s ease-out infinite;
37 | animation: tetromino4 1.5s ease-out infinite;
38 | }
39 |
40 | @-webkit-keyframes tetromino1 {
41 | 0%, 40% {
42 | /* compose logo */
43 | /* 1 on 3 */
44 | /* L-shape */
45 | -webkit-transform: translate(0, 0);
46 | transform: translate(0, 0);
47 | }
48 | 50% {
49 | /* pre-box */
50 | -webkit-transform: translate(48px, -27px);
51 | transform: translate(48px, -27px);
52 | }
53 | 60%, 100% {
54 | /* box */
55 | /* compose logo */
56 | -webkit-transform: translate(96px, 0);
57 | transform: translate(96px, 0);
58 | }
59 | }
60 |
61 | @keyframes tetromino1 {
62 | 0%, 40% {
63 | /* compose logo */
64 | /* 1 on 3 */
65 | /* L-shape */
66 | -webkit-transform: translate(0, 0);
67 | transform: translate(0, 0);
68 | }
69 | 50% {
70 | /* pre-box */
71 | -webkit-transform: translate(48px, -27px);
72 | transform: translate(48px, -27px);
73 | }
74 | 60%, 100% {
75 | /* box */
76 | /* compose logo */
77 | -webkit-transform: translate(96px, 0);
78 | transform: translate(96px, 0);
79 | }
80 | }
81 | @-webkit-keyframes tetromino2 {
82 | 0%, 20% {
83 | /* compose logo */
84 | /* 1 on 3 */
85 | -webkit-transform: translate(96px, 0px);
86 | transform: translate(96px, 0px);
87 | }
88 | 40%, 100% {
89 | /* L-shape */
90 | /* box */
91 | /* compose logo */
92 | -webkit-transform: translate(144px, 27px);
93 | transform: translate(144px, 27px);
94 | }
95 | }
96 | @keyframes tetromino2 {
97 | 0%, 20% {
98 | /* compose logo */
99 | /* 1 on 3 */
100 | -webkit-transform: translate(96px, 0px);
101 | transform: translate(96px, 0px);
102 | }
103 | 40%, 100% {
104 | /* L-shape */
105 | /* box */
106 | /* compose logo */
107 | -webkit-transform: translate(144px, 27px);
108 | transform: translate(144px, 27px);
109 | }
110 | }
111 | @-webkit-keyframes tetromino3 {
112 | 0% {
113 | /* compose logo */
114 | -webkit-transform: translate(144px, 27px);
115 | transform: translate(144px, 27px);
116 | }
117 | 20%, 60% {
118 | /* 1 on 3 */
119 | /* L-shape */
120 | /* box */
121 | -webkit-transform: translate(96px, 54px);
122 | transform: translate(96px, 54px);
123 | }
124 | 90%, 100% {
125 | /* compose logo */
126 | -webkit-transform: translate(48px, 27px);
127 | transform: translate(48px, 27px);
128 | }
129 | }
130 | @keyframes tetromino3 {
131 | 0% {
132 | /* compose logo */
133 | -webkit-transform: translate(144px, 27px);
134 | transform: translate(144px, 27px);
135 | }
136 | 20%, 60% {
137 | /* 1 on 3 */
138 | /* L-shape */
139 | /* box */
140 | -webkit-transform: translate(96px, 54px);
141 | transform: translate(96px, 54px);
142 | }
143 | 90%, 100% {
144 | /* compose logo */
145 | -webkit-transform: translate(48px, 27px);
146 | transform: translate(48px, 27px);
147 | }
148 | }
149 | @-webkit-keyframes tetromino4 {
150 | 0%, 60% {
151 | /* compose logo */
152 | /* 1 on 3 */
153 | /* L-shape */
154 | /* box */
155 | -webkit-transform: translate(48px, 27px);
156 | transform: translate(48px, 27px);
157 | }
158 | 90%, 100% {
159 | /* compose logo */
160 | -webkit-transform: translate(0, 0);
161 | transform: translate(0, 0);
162 | }
163 | }
164 | @keyframes tetromino4 {
165 | 0%, 60% {
166 | /* compose logo */
167 | /* 1 on 3 */
168 | /* L-shape */
169 | /* box */
170 | -webkit-transform: translate(48px, 27px);
171 | transform: translate(48px, 27px);
172 | }
173 | 90%, 100% {
174 | /* compose logo */
175 | -webkit-transform: translate(0, 0);
176 | transform: translate(0, 0);
177 | }
178 | }
179 |
180 |
--------------------------------------------------------------------------------
/web/templates/user.events.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |

14 |
15 |
16 |
136 |
137 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | GitHub Contributions Archive
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
42 |
43 |
96 |
97 |
106 |
107 |
120 |
121 |
123 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitHub Contributions
2 |
3 | [GitHubContributions.io](http://githubcontributions.io)
4 |
5 | This is a utility to find a list of all contributions a user has made to any public repository on GitHub from 2011-01-01 through yesterday.
6 |
7 | The data from 2015-01-01 - present is found on [GitHub Archive](https://www.githubarchive.org). The data from before this uses a different schema and was obtained from Google's BigQuery (see below)
8 |
9 | As of 2015-08-28, it tracks a total of
10 | ```sh
11 | % cd /github-archive/processed
12 | % gzip -l *.json.gz | awk 'END{print $2}' | numfmt --to=iec-i --suffix=B --format="%3f"
13 | 93GiB
14 | % zcat *.json.gz | wc -l
15 | 253027947
16 | ```
17 | events.
18 |
19 | `db.contributions.stats()`:
20 |
21 | ```json
22 | {
23 | "ns" : "contributions.contributions",
24 | "count" : 284048099,
25 | "size" : 113714359272,
26 | "avgObjSize" : 400,
27 | "storageSize" : 47820357632,
28 | "capped" : false,
29 | "nindexes" : 4,
30 | "totalIndexSize" : 8810385408,
31 | "indexSizes" : {
32 | "_id_" : 2804744192,
33 | "_user_lower_1" : 2275647488,
34 | "_event_id_1" : 1029251072,
35 | "created_at_1" : 2700742656
36 | },
37 | "ok" : 1
38 | }
39 | ```
40 | (WiredTiger stats omitted)
41 |
42 | ### Processing data archives
43 |
44 | Processing the data archives involves 3 steps:
45 |
46 | 1. Download the raw events files from [GitHub Archive](https://www.githubarchive.org) into the events directory
47 | 2. Transform the events files by filtering non-contribution events (e.g., starring a repository) and adding necessary indexable keys (e.g., lowercased username)
48 | 3. Load the transformed data into MongoDB
49 |
50 | The `archive-processor` tool in the `util` directory handles all of this.
51 |
52 | The transformed data from step 2 is compressed and saved just in case we need to re-load the entire database (these files are much smaller than the raw data).
53 |
54 | All of this can be done automatically by setting the correct environment variables, then running `archive-processor process`, or it can be invoked differently to separate the steps or change the working directories. Run `archive-processor --help` for details.
55 |
56 | | Environment Variable | Meaning
57 | |----------------------|----------------------------------------------------------|
58 | | GHC_EVENTS_PATH | Contains data from 2015-01-01 to present (.json.gz) |
59 | | GHC_TIMELINE_PATH | Contains data before 2015-01-01 (.csv.gz) |
60 | | GHC_TRANSFORMED_PATH | Contains output of "transform" operation (.json.gz) |
61 | | GHC_LOADED_PATH | Links to files in GHC_TRANSFORMED_PATH when loaded to DB |
62 | | GHC_LOG_PATH | Each invocation of `archive-processor` logs to here |
63 |
64 |
65 | ## BigQuery Data Sets
66 |
67 | For the data from 2011-2014 (actually, 2008-08-25 01:07:06 to 2014-12-31 23:59:59), the GitHub Archive project recorded data from the (now deprecated) Timeline API. This is in a different format and has many more quirks than the new [GitHub Events API](https://developer.github.com/v3/activity/events/). To obtain this data, the following BigTable query was used (which took only 47.5s to run):
68 |
69 | ```sql
70 | SELECT
71 | -- common fields
72 | created_at, actor, repository_owner, repository_name, repository_organization, type, url,
73 | -- specific to type
74 | payload_page_html_url, -- GollumEvent
75 | payload_page_summary, -- GollumEvent
76 | payload_page_page_name, -- GollumEvent
77 | payload_page_action, -- GollumEvent
78 | payload_page_title, -- GollumEvent
79 | payload_page_sha, -- GollumEvent
80 | payload_number, -- IssuesEvent
81 | payload_action, -- MemberEvent, IssuesEvent, ReleaseEvent, IssueCommentEvent
82 | payload_member_login, -- MemberEvent
83 | payload_commit_msg, -- PushEvent
84 | payload_commit_email, -- PushEvent
85 | payload_commit_id, -- PushEvent
86 | payload_head, -- PushEvent
87 | payload_ref, -- PushEvent
88 | payload_comment_commit_id, -- CommitCommentEvent
89 | payload_comment_path, -- CommitCommentEvent
90 | payload_comment_body, -- CommitCommentEvent
91 | payload_issue_id, -- IssueCommentEvent
92 | payload_comment_id -- IssueCommentEvent
93 | FROM (
94 | TABLE_QUERY(githubarchive:year,'true') -- All the years!
95 | )
96 | WHERE type IN (
97 | "GollumEvent",
98 | "IssuesEvent",
99 | "PushEvent",
100 | "CommitCommentEvent",
101 | "ReleaseEvent",
102 | "PublicEvent",
103 | "MemberEvent",
104 | "IssueCommentEvent"
105 | )
106 |
107 | ```
108 |
109 | If you actually want to use this data, there's no need to run that query; just ask me for the CSVs. When gzipped, they are about 19GB.
110 |
111 | ### Erroneous data
112 |
113 | There is lots of data in the archives that just doesn't make sense. Where I can, I've worked around it, for example by parsing needed data out of the event's URL. Here are some issues:
114 |
115 | #### BigQuery exports CSV nulls weird?
116 |
117 | Example:
118 |
119 | ```sql
120 | SELECT *
121 | FROM [githubarchive:year.2014]
122 | LIMIT 1000
123 | ```
124 |
125 | you will note that in the results pane of Google's BigQuery page, there is the string "null" where it really means a real null value. That makes its way into the exported CSV. So you should export the table the real way, or you will have the string "null" for almost every value.
126 |
127 | #### PushEvent with no repository name (Timeline API)
128 |
129 | Example:
130 |
131 | ```sql
132 | SELECT *
133 | FROM [githubarchive:year.2014]
134 | WHERE payload_head='8824ed4d86f587a2a556248d9abfac790a1cbd3f'
135 | LIMIT 1
136 | ```
137 |
138 | It seems like sometimes, the only way to get the real repository name (`owner/project`) is to parse it from the URL.
139 |
140 | #### PushEvent with no way of figuring out the repository (Timeline API)
141 |
142 | Example:
143 |
144 | ```sql
145 | SELECT *
146 | FROM [githubarchive:year.2011]
147 | WHERE payload_head='32b2177f05be005df3542c14d9a9985be2b553f7'
148 | LIMIT 5
149 | ```
150 |
151 | `repository_url` is `https://github.com//` and `repository_name` is `/` for each of these. They actually push to:
152 | https://github.com/Jiyambi/WoW-Pro-Guides but I only know that by reading the commit messages.
153 |
--------------------------------------------------------------------------------
/conf/provision.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: provision server
3 | hosts: all
4 | user: root
5 | roles:
6 | - role: kamaln7.swapfile
7 | swapfile_use_dd: True
8 | swapfile_size: 2048
9 | tasks:
10 | - name: update installed packages
11 | apt: upgrade=dist update_cache=yes cache_valid_time=3600
12 |
13 | - name: create ghc user
14 | user: name=ghc generate_ssh_key=yes shell=/bin/bash
15 |
16 | - name: install server base packages
17 | apt: name={{ item }} state=latest update_cache=yes cache_valid_time=3600
18 | with_items:
19 | - apt-file
20 | - apticron
21 | - build-essential
22 | - cowsay
23 | - curl
24 | - dnsutils
25 | - emacs24
26 | - git
27 | - htop
28 | - iftop
29 | - iotop
30 | - jq
31 | - mailutils
32 | - mc
33 | - ncdu
34 | - ncurses-term
35 | - ntp
36 | - pcregrep
37 | - python-dev
38 | - python-pip
39 | - python-virtualenv
40 | - python3-dev
41 | - python3-pip
42 | - rsync
43 | - silversearcher-ag
44 | - ssmtp
45 | - tig
46 | - tmux
47 | - toilet
48 | - virtualenvwrapper
49 | - wget
50 | - zsh
51 |
52 | - name: set tmp permissions
53 | file: path=/tmp mode=a+w
54 |
55 | - name: enable agent forwarding
56 | lineinfile: dest=/etc/ssh/sshd_config
57 | state=present
58 | regexp='^AllowAgentForwarding'
59 | line='AllowAgentForwarding yes'
60 | notify: restart sshd
61 |
62 | - name: configure ssh keys for root
63 | authorized_key: user=root key=https://github.com/hut8.keys
64 |
65 | - name: configure ssh keys for ghc
66 | authorized_key: user=ghc key=https://github.com/hut8.keys
67 |
68 | handlers:
69 | - name: restart sshd
70 | service: name=ssh state=restarted
71 |
72 | - name: install go
73 | hosts: all
74 | user: root
75 | tasks:
76 | - name: set go version
77 | set_fact: go_version="1.6.2.linux-amd64"
78 |
79 | - name: download go binary distribution
80 | get_url: dest=/tmp
81 | url="https://storage.googleapis.com/golang/go{{go_version}}.tar.gz"
82 |
83 | - name: extract tarball
84 | unarchive: src="/tmp/go{{go_version}}.tar.gz"
85 | dest=/usr/local/
86 | copy=no
87 |
88 | - name: install and configure mongodb
89 | user: root
90 | hosts: database
91 | tasks:
92 | - apt_key: keyserver=keyserver.ubuntu.com id=EA312927
93 |
94 | - name: add mongodb repo (debian wheezy has better package for systemd)
95 | apt_repository: repo='deb http://repo.mongodb.org/apt/debian wheezy/mongodb-org/3.2 main'
96 | state=present
97 |
98 | - name: install mongodb
99 | apt: name=mongodb-org state=latest update_cache=yes
100 |
101 | - name: configure data directory
102 | file: path=/srv/db
103 | state=directory
104 | owner=mongodb
105 | group=daemon
106 |
107 | - name: install config file
108 | copy: src=mongod.conf
109 | dest=/etc/mongod.conf
110 | notify: restart mongodb
111 |
112 | handlers:
113 | - name: restart mongodb
114 | service: name=mongod state=restarted
115 |
116 | - name: install and configure nginx and frontend utilities
117 | user: root
118 | hosts: web
119 | tags:
120 | - deploy
121 | tasks:
122 | - name: install nginx
123 | apt: name=nginx-extras state=latest update_cache=yes
124 |
125 | - name: uninstall default site config
126 | file: path=/etc/nginx/sites-enabled/default
127 | state=absent
128 |
129 | - name: install nginx.conf
130 | template: src=github-contributions.nginx.conf
131 | dest=/etc/nginx/sites-enabled/github-contributions.nginx.conf
132 | notify: restart nginx
133 |
134 | - name: install node repo
135 | shell: curl -sL https://deb.nodesource.com/setup_5.x | bash -
136 |
137 | - name: install node + npm
138 | apt: name=nodejs state=latest update_cache=yes
139 |
140 | - name: install gulp-cli
141 | command: npm install -g gulp-cli
142 | # DO NOT USE -- THIS DOESN'T INSTALL IT ?!
143 | # npm: name=gulp-cli global=yes state=latest
144 |
145 | handlers:
146 | - name: restart nginx
147 | service: name=nginx state=restarted
148 |
149 |
150 | - name: make common directory structures
151 | user: root
152 | hosts: github-contributions
153 | tags:
154 | - deploy
155 | tasks:
156 | - name: archive directories
157 | file: path={{ item }} state=directory owner=ghc group=ghc
158 | with_items:
159 | - /srv/github-contributions
160 | - /srv
161 | - /srv/bin
162 | - /home/ghc/go
163 | - /home/ghc/go/src
164 | - /var/log/ghc
165 |
166 | - name: make archive directory structures
167 | user: root
168 | hosts: database
169 | tasks:
170 | - name: archive directories
171 | file: path={{ item }} state=directory owner=ghc group=ghc
172 | with_items:
173 | - /srv/github-archive
174 | - /srv/github-archive/events
175 | - /srv/github-archive/loaded
176 | - /srv/github-archive/logs
177 | - /srv/github-archive/timeline
178 | - /srv/github-archive/transformed
179 |
180 |
181 | - name: install ghc server configuration
182 | user: root
183 | hosts: web
184 | tags:
185 | - deploy
186 | tasks:
187 | - include_vars: secrets.yml
188 |
189 | - name: write environment variables
190 | template: src=ghc-env.sh
191 | dest=/etc/ghc-env.sh
192 |
193 | - name: install ghc app systemd script
194 | template: src=github-contributions-app.service
195 | dest=/etc/systemd/system/github-contributions-app.service
196 | notify: enable systemd service
197 |
198 | handlers:
199 | - name: enable systemd service
200 | shell: systemctl daemon-reload && systemctl enable github-contributions-app.service
201 |
202 | - name: configure ssmtp
203 | user: root
204 | hosts: github-contributions
205 | tasks:
206 | - include_vars: secrets.yml
207 |
208 | - name: copy revaliases
209 | template: src=ssmtp-revaliases dest=/etc/ssmtp/revaliases
210 |
211 | - name: copy ssmtp.conf
212 | template: src=ssmtp.conf dest=/etc/ssmtp/ssmtp.conf mode=0644
213 |
214 | - name: build and deploy app and archive-processor
215 | user: ghc
216 | hosts: github-contributions
217 | tags:
218 | - deploy
219 | vars:
220 | git_branch: master
221 | tasks:
222 | - name: pull ghc repository
223 | git: repo=ssh://git@github.com/tenex/github-contributions.git
224 | dest=/srv/github-contributions
225 | accept_hostkey=yes
226 | version="{{ git_branch }}"
227 | register: git_status
228 |
229 | - debug: msg="deploying ghc at {{ git_status.after }}"
230 |
231 | - name: install python dependencies
232 | pip: requirements=/srv/github-contributions/requirements.txt
233 | virtualenv=/srv/venv-{{ git_status.after }}
234 | virtualenv_command="virtualenv --python=/usr/bin/python3"
235 |
236 | - name: commit python venv
237 | file: path=/srv/venv
238 | src=/srv/venv-{{ git_status.after }}
239 | state=link
240 |
241 | - name: commit archive-processor
242 | file: path=/srv/app
243 | src=/srv/github-contributions/app
244 | state=link
245 |
246 | - name: gopath trick for ghc-app
247 | file: path=/home/ghc/go/src/ghc-app
248 | src=/srv/github-contributions/ghc-app
249 | state=link
250 |
251 | - name: build ghc-app
252 | script: build-ghc-app.sh
253 |
254 | - name: install dependencies
255 | shell: npm install
256 | args:
257 | chdir: /srv/github-contributions/web
258 |
259 | - name: build assets
260 | shell: gulp
261 | args:
262 | chdir: /srv/github-contributions/web
263 |
264 | - name: set service states
265 | user: root
266 | hosts: web
267 | tags:
268 | - deploy
269 | tasks:
270 | - name: ensure server is running
271 | service: name=github-contributions-app state=restarted
272 |
273 | - name: install cron job
274 | user: root
275 | hosts: database
276 | tags:
277 | - deploy
278 | tasks:
279 | - name: github archive processor cron job
280 | cron: job=/srv/github-contributions/conf/process-job
281 | name=archive-processor
282 | minute=5
283 | user=ghc
284 |
285 | - name: install monitor cron job
286 | user: root
287 | hosts: database
288 | tags:
289 | - deploy
290 | tasks:
291 | - name: github archive archive processor monitor cron job
292 | cron: job=/srv/github-contributions/conf/server-monitor
293 | name=server-monitor
294 | minute=55
295 | user=ghc
296 |
297 | - name: install lets-encrypt and certificate cron job
298 | user: root
299 | hosts: web
300 | tasks:
301 | - name: install lets-encrypt from source
302 | git: repo=https://github.com/letsencrypt/letsencrypt
303 | dest=/opt/letsencrypt
304 | update=no # It updates itself without Git apparently?
305 | - name: lets-encrypt certificate renewal cron job
306 | cron: job=/srv/github-contributions/conf/lets-encrypt-renew-certificate.sh
307 | special_time=weekly
308 | name=renew-certificate
309 |
310 | - name: tell rollbar we deployed
311 | user: ghc
312 | hosts: web
313 | tasks:
314 | - name: figure out who i am
315 | local_action: command whoami
316 | register: local_user
317 |
318 | - name: rollbar notification
319 | run_once: true
320 | rollbar_deployment: environment=production
321 | revision={{ git_status.after }}
322 | token={{ rollbar_token }}
323 | user={{ local_user.stdout }}
324 |
--------------------------------------------------------------------------------
/web/octicons/octicons.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'octicons';
3 | src: url('octicons.eot?#iefix') format('embedded-opentype'),
4 | url('octicons.woff') format('woff'),
5 | url('octicons.ttf') format('truetype'),
6 | url('octicons.svg#octicons') format('svg');
7 | font-weight: normal;
8 | font-style: normal;
9 | }
10 |
11 | /*
12 |
13 | .octicon is optimized for 16px.
14 | .mega-octicon is optimized for 32px but can be used larger.
15 |
16 | */
17 | .octicon, .mega-octicon {
18 | font: normal normal normal 16px/1 octicons;
19 | display: inline-block;
20 | text-decoration: none;
21 | text-rendering: auto;
22 | -webkit-font-smoothing: antialiased;
23 | -moz-osx-font-smoothing: grayscale;
24 | -webkit-user-select: none;
25 | -moz-user-select: none;
26 | -ms-user-select: none;
27 | user-select: none;
28 | }
29 | .mega-octicon { font-size: 32px; }
30 |
31 | .octicon-alert:before { content: '\f02d'} /* */
32 | .octicon-alignment-align:before { content: '\f08a'} /* */
33 | .octicon-alignment-aligned-to:before { content: '\f08e'} /* */
34 | .octicon-alignment-unalign:before { content: '\f08b'} /* */
35 | .octicon-arrow-down:before { content: '\f03f'} /* */
36 | .octicon-arrow-left:before { content: '\f040'} /* */
37 | .octicon-arrow-right:before { content: '\f03e'} /* */
38 | .octicon-arrow-small-down:before { content: '\f0a0'} /* */
39 | .octicon-arrow-small-left:before { content: '\f0a1'} /* */
40 | .octicon-arrow-small-right:before { content: '\f071'} /* */
41 | .octicon-arrow-small-up:before { content: '\f09f'} /* */
42 | .octicon-arrow-up:before { content: '\f03d'} /* */
43 | .octicon-beer:before { content: '\f069'} /* */
44 | .octicon-book:before { content: '\f007'} /* */
45 | .octicon-bookmark:before { content: '\f07b'} /* */
46 | .octicon-briefcase:before { content: '\f0d3'} /* */
47 | .octicon-broadcast:before { content: '\f048'} /* */
48 | .octicon-browser:before { content: '\f0c5'} /* */
49 | .octicon-bug:before { content: '\f091'} /* */
50 | .octicon-calendar:before { content: '\f068'} /* */
51 | .octicon-check:before { content: '\f03a'} /* */
52 | .octicon-checklist:before { content: '\f076'} /* */
53 | .octicon-chevron-down:before { content: '\f0a3'} /* */
54 | .octicon-chevron-left:before { content: '\f0a4'} /* */
55 | .octicon-chevron-right:before { content: '\f078'} /* */
56 | .octicon-chevron-up:before { content: '\f0a2'} /* */
57 | .octicon-circle-slash:before { content: '\f084'} /* */
58 | .octicon-circuit-board:before { content: '\f0d6'} /* */
59 | .octicon-clippy:before { content: '\f035'} /* */
60 | .octicon-clock:before { content: '\f046'} /* */
61 | .octicon-cloud-download:before { content: '\f00b'} /* */
62 | .octicon-cloud-upload:before { content: '\f00c'} /* */
63 | .octicon-code:before { content: '\f05f'} /* */
64 | .octicon-color-mode:before { content: '\f065'} /* */
65 | .octicon-comment-add:before,
66 | .octicon-comment:before { content: '\f02b'} /* */
67 | .octicon-comment-discussion:before { content: '\f04f'} /* */
68 | .octicon-credit-card:before { content: '\f045'} /* */
69 | .octicon-dash:before { content: '\f0ca'} /* */
70 | .octicon-dashboard:before { content: '\f07d'} /* */
71 | .octicon-database:before { content: '\f096'} /* */
72 | .octicon-device-camera:before { content: '\f056'} /* */
73 | .octicon-device-camera-video:before { content: '\f057'} /* */
74 | .octicon-device-desktop:before { content: '\f27c'} /* */
75 | .octicon-device-mobile:before { content: '\f038'} /* */
76 | .octicon-diff:before { content: '\f04d'} /* */
77 | .octicon-diff-added:before { content: '\f06b'} /* */
78 | .octicon-diff-ignored:before { content: '\f099'} /* */
79 | .octicon-diff-modified:before { content: '\f06d'} /* */
80 | .octicon-diff-removed:before { content: '\f06c'} /* */
81 | .octicon-diff-renamed:before { content: '\f06e'} /* */
82 | .octicon-ellipsis:before { content: '\f09a'} /* */
83 | .octicon-eye-unwatch:before,
84 | .octicon-eye-watch:before,
85 | .octicon-eye:before { content: '\f04e'} /* */
86 | .octicon-file-binary:before { content: '\f094'} /* */
87 | .octicon-file-code:before { content: '\f010'} /* */
88 | .octicon-file-directory:before { content: '\f016'} /* */
89 | .octicon-file-media:before { content: '\f012'} /* */
90 | .octicon-file-pdf:before { content: '\f014'} /* */
91 | .octicon-file-submodule:before { content: '\f017'} /* */
92 | .octicon-file-symlink-directory:before { content: '\f0b1'} /* */
93 | .octicon-file-symlink-file:before { content: '\f0b0'} /* */
94 | .octicon-file-text:before { content: '\f011'} /* */
95 | .octicon-file-zip:before { content: '\f013'} /* */
96 | .octicon-flame:before { content: '\f0d2'} /* */
97 | .octicon-fold:before { content: '\f0cc'} /* */
98 | .octicon-gear:before { content: '\f02f'} /* */
99 | .octicon-gift:before { content: '\f042'} /* */
100 | .octicon-gist:before { content: '\f00e'} /* */
101 | .octicon-gist-secret:before { content: '\f08c'} /* */
102 | .octicon-git-branch-create:before,
103 | .octicon-git-branch-delete:before,
104 | .octicon-git-branch:before { content: '\f020'} /* */
105 | .octicon-git-commit:before { content: '\f01f'} /* */
106 | .octicon-git-compare:before { content: '\f0ac'} /* */
107 | .octicon-git-merge:before { content: '\f023'} /* */
108 | .octicon-git-pull-request-abandoned:before,
109 | .octicon-git-pull-request:before { content: '\f009'} /* */
110 | .octicon-globe:before { content: '\f0b6'} /* */
111 | .octicon-graph:before { content: '\f043'} /* */
112 | .octicon-heart:before { content: '\2665'} /* ♥ */
113 | .octicon-history:before { content: '\f07e'} /* */
114 | .octicon-home:before { content: '\f08d'} /* */
115 | .octicon-horizontal-rule:before { content: '\f070'} /* */
116 | .octicon-hourglass:before { content: '\f09e'} /* */
117 | .octicon-hubot:before { content: '\f09d'} /* */
118 | .octicon-inbox:before { content: '\f0cf'} /* */
119 | .octicon-info:before { content: '\f059'} /* */
120 | .octicon-issue-closed:before { content: '\f028'} /* */
121 | .octicon-issue-opened:before { content: '\f026'} /* */
122 | .octicon-issue-reopened:before { content: '\f027'} /* */
123 | .octicon-jersey:before { content: '\f019'} /* */
124 | .octicon-jump-down:before { content: '\f072'} /* */
125 | .octicon-jump-left:before { content: '\f0a5'} /* */
126 | .octicon-jump-right:before { content: '\f0a6'} /* */
127 | .octicon-jump-up:before { content: '\f073'} /* */
128 | .octicon-key:before { content: '\f049'} /* */
129 | .octicon-keyboard:before { content: '\f00d'} /* */
130 | .octicon-law:before { content: '\f0d8'} /* */
131 | .octicon-light-bulb:before { content: '\f000'} /* */
132 | .octicon-link:before { content: '\f05c'} /* */
133 | .octicon-link-external:before { content: '\f07f'} /* */
134 | .octicon-list-ordered:before { content: '\f062'} /* */
135 | .octicon-list-unordered:before { content: '\f061'} /* */
136 | .octicon-location:before { content: '\f060'} /* */
137 | .octicon-gist-private:before,
138 | .octicon-mirror-private:before,
139 | .octicon-git-fork-private:before,
140 | .octicon-lock:before { content: '\f06a'} /* */
141 | .octicon-logo-github:before { content: '\f092'} /* */
142 | .octicon-mail:before { content: '\f03b'} /* */
143 | .octicon-mail-read:before { content: '\f03c'} /* */
144 | .octicon-mail-reply:before { content: '\f051'} /* */
145 | .octicon-mark-github:before { content: '\f00a'} /* */
146 | .octicon-markdown:before { content: '\f0c9'} /* */
147 | .octicon-megaphone:before { content: '\f077'} /* */
148 | .octicon-mention:before { content: '\f0be'} /* */
149 | .octicon-microscope:before { content: '\f089'} /* */
150 | .octicon-milestone:before { content: '\f075'} /* */
151 | .octicon-mirror-public:before,
152 | .octicon-mirror:before { content: '\f024'} /* */
153 | .octicon-mortar-board:before { content: '\f0d7'} /* */
154 | .octicon-move-down:before { content: '\f0a8'} /* */
155 | .octicon-move-left:before { content: '\f074'} /* */
156 | .octicon-move-right:before { content: '\f0a9'} /* */
157 | .octicon-move-up:before { content: '\f0a7'} /* */
158 | .octicon-mute:before { content: '\f080'} /* */
159 | .octicon-no-newline:before { content: '\f09c'} /* */
160 | .octicon-octoface:before { content: '\f008'} /* */
161 | .octicon-organization:before { content: '\f037'} /* */
162 | .octicon-package:before { content: '\f0c4'} /* */
163 | .octicon-paintcan:before { content: '\f0d1'} /* */
164 | .octicon-pencil:before { content: '\f058'} /* */
165 | .octicon-person-add:before,
166 | .octicon-person-follow:before,
167 | .octicon-person:before { content: '\f018'} /* */
168 | .octicon-pin:before { content: '\f041'} /* */
169 | .octicon-playback-fast-forward:before { content: '\f0bd'} /* */
170 | .octicon-playback-pause:before { content: '\f0bb'} /* */
171 | .octicon-playback-play:before { content: '\f0bf'} /* */
172 | .octicon-playback-rewind:before { content: '\f0bc'} /* */
173 | .octicon-plug:before { content: '\f0d4'} /* */
174 | .octicon-repo-create:before,
175 | .octicon-gist-new:before,
176 | .octicon-file-directory-create:before,
177 | .octicon-file-add:before,
178 | .octicon-plus:before { content: '\f05d'} /* */
179 | .octicon-podium:before { content: '\f0af'} /* */
180 | .octicon-primitive-dot:before { content: '\f052'} /* */
181 | .octicon-primitive-square:before { content: '\f053'} /* */
182 | .octicon-pulse:before { content: '\f085'} /* */
183 | .octicon-puzzle:before { content: '\f0c0'} /* */
184 | .octicon-question:before { content: '\f02c'} /* */
185 | .octicon-quote:before { content: '\f063'} /* */
186 | .octicon-radio-tower:before { content: '\f030'} /* */
187 | .octicon-repo-delete:before,
188 | .octicon-repo:before { content: '\f001'} /* */
189 | .octicon-repo-clone:before { content: '\f04c'} /* */
190 | .octicon-repo-force-push:before { content: '\f04a'} /* */
191 | .octicon-gist-fork:before,
192 | .octicon-repo-forked:before { content: '\f002'} /* */
193 | .octicon-repo-pull:before { content: '\f006'} /* */
194 | .octicon-repo-push:before { content: '\f005'} /* */
195 | .octicon-rocket:before { content: '\f033'} /* */
196 | .octicon-rss:before { content: '\f034'} /* */
197 | .octicon-ruby:before { content: '\f047'} /* */
198 | .octicon-screen-full:before { content: '\f066'} /* */
199 | .octicon-screen-normal:before { content: '\f067'} /* */
200 | .octicon-search-save:before,
201 | .octicon-search:before { content: '\f02e'} /* */
202 | .octicon-server:before { content: '\f097'} /* */
203 | .octicon-settings:before { content: '\f07c'} /* */
204 | .octicon-log-in:before,
205 | .octicon-sign-in:before { content: '\f036'} /* */
206 | .octicon-log-out:before,
207 | .octicon-sign-out:before { content: '\f032'} /* */
208 | .octicon-split:before { content: '\f0c6'} /* */
209 | .octicon-squirrel:before { content: '\f0b2'} /* */
210 | .octicon-star-add:before,
211 | .octicon-star-delete:before,
212 | .octicon-star:before { content: '\f02a'} /* */
213 | .octicon-steps:before { content: '\f0c7'} /* */
214 | .octicon-stop:before { content: '\f08f'} /* */
215 | .octicon-repo-sync:before,
216 | .octicon-sync:before { content: '\f087'} /* */
217 | .octicon-tag-remove:before,
218 | .octicon-tag-add:before,
219 | .octicon-tag:before { content: '\f015'} /* */
220 | .octicon-telescope:before { content: '\f088'} /* */
221 | .octicon-terminal:before { content: '\f0c8'} /* */
222 | .octicon-three-bars:before { content: '\f05e'} /* */
223 | .octicon-thumbsdown:before { content: '\f0db'} /* */
224 | .octicon-thumbsup:before { content: '\f0da'} /* */
225 | .octicon-tools:before { content: '\f031'} /* */
226 | .octicon-trashcan:before { content: '\f0d0'} /* */
227 | .octicon-triangle-down:before { content: '\f05b'} /* */
228 | .octicon-triangle-left:before { content: '\f044'} /* */
229 | .octicon-triangle-right:before { content: '\f05a'} /* */
230 | .octicon-triangle-up:before { content: '\f0aa'} /* */
231 | .octicon-unfold:before { content: '\f039'} /* */
232 | .octicon-unmute:before { content: '\f0ba'} /* */
233 | .octicon-versions:before { content: '\f064'} /* */
234 | .octicon-remove-close:before,
235 | .octicon-x:before { content: '\f081'} /* */
236 | .octicon-zap:before { content: '\26A1'} /* ⚡ */
237 |
--------------------------------------------------------------------------------
/web/octicons/octicons.less:
--------------------------------------------------------------------------------
1 | @octicons-font-path: ".";
2 | @octicons-version: "675c3211eac589bbda193fdb306ce567a2c4569f";
3 |
4 | @font-face {
5 | font-family: 'octicons';
6 | src: ~"url('@{octicons-font-path}/octicons.eot?#iefix&v=@{octicons-version}') format('embedded-opentype')",
7 | ~"url('@{octicons-font-path}/octicons.woff?v=@{octicons-version}') format('woff')",
8 | ~"url('@{octicons-font-path}/octicons.ttf?v=@{octicons-version}') format('truetype')",
9 | ~"url('@{octicons-font-path}/octicons.svg?v=@{octicons-version}#octicons') format('svg')";
10 | font-weight: normal;
11 | font-style: normal;
12 | }
13 |
14 | // .octicon is optimized for 16px.
15 | // .mega-octicon is optimized for 32px but can be used larger.
16 | .octicon, .mega-octicon {
17 | font: normal normal normal 16px/1 octicons;
18 | display: inline-block;
19 | text-decoration: none;
20 | text-rendering: auto;
21 | -webkit-font-smoothing: antialiased;
22 | -moz-osx-font-smoothing: grayscale;
23 | -webkit-user-select: none;
24 | -moz-user-select: none;
25 | -ms-user-select: none;
26 | user-select: none;
27 | }
28 | .mega-octicon { font-size: 32px; }
29 |
30 | .octicon-alert:before { content: '\f02d'} /* */
31 | .octicon-alignment-align:before { content: '\f08a'} /* */
32 | .octicon-alignment-aligned-to:before { content: '\f08e'} /* */
33 | .octicon-alignment-unalign:before { content: '\f08b'} /* */
34 | .octicon-arrow-down:before { content: '\f03f'} /* */
35 | .octicon-arrow-left:before { content: '\f040'} /* */
36 | .octicon-arrow-right:before { content: '\f03e'} /* */
37 | .octicon-arrow-small-down:before { content: '\f0a0'} /* */
38 | .octicon-arrow-small-left:before { content: '\f0a1'} /* */
39 | .octicon-arrow-small-right:before { content: '\f071'} /* */
40 | .octicon-arrow-small-up:before { content: '\f09f'} /* */
41 | .octicon-arrow-up:before { content: '\f03d'} /* */
42 | .octicon-beer:before { content: '\f069'} /* */
43 | .octicon-book:before { content: '\f007'} /* */
44 | .octicon-bookmark:before { content: '\f07b'} /* */
45 | .octicon-briefcase:before { content: '\f0d3'} /* */
46 | .octicon-broadcast:before { content: '\f048'} /* */
47 | .octicon-browser:before { content: '\f0c5'} /* */
48 | .octicon-bug:before { content: '\f091'} /* */
49 | .octicon-calendar:before { content: '\f068'} /* */
50 | .octicon-check:before { content: '\f03a'} /* */
51 | .octicon-checklist:before { content: '\f076'} /* */
52 | .octicon-chevron-down:before { content: '\f0a3'} /* */
53 | .octicon-chevron-left:before { content: '\f0a4'} /* */
54 | .octicon-chevron-right:before { content: '\f078'} /* */
55 | .octicon-chevron-up:before { content: '\f0a2'} /* */
56 | .octicon-circle-slash:before { content: '\f084'} /* */
57 | .octicon-circuit-board:before { content: '\f0d6'} /* */
58 | .octicon-clippy:before { content: '\f035'} /* */
59 | .octicon-clock:before { content: '\f046'} /* */
60 | .octicon-cloud-download:before { content: '\f00b'} /* */
61 | .octicon-cloud-upload:before { content: '\f00c'} /* */
62 | .octicon-code:before { content: '\f05f'} /* */
63 | .octicon-color-mode:before { content: '\f065'} /* */
64 | .octicon-comment-add:before,
65 | .octicon-comment:before { content: '\f02b'} /* */
66 | .octicon-comment-discussion:before { content: '\f04f'} /* */
67 | .octicon-credit-card:before { content: '\f045'} /* */
68 | .octicon-dash:before { content: '\f0ca'} /* */
69 | .octicon-dashboard:before { content: '\f07d'} /* */
70 | .octicon-database:before { content: '\f096'} /* */
71 | .octicon-device-camera:before { content: '\f056'} /* */
72 | .octicon-device-camera-video:before { content: '\f057'} /* */
73 | .octicon-device-desktop:before { content: '\f27c'} /* */
74 | .octicon-device-mobile:before { content: '\f038'} /* */
75 | .octicon-diff:before { content: '\f04d'} /* */
76 | .octicon-diff-added:before { content: '\f06b'} /* */
77 | .octicon-diff-ignored:before { content: '\f099'} /* */
78 | .octicon-diff-modified:before { content: '\f06d'} /* */
79 | .octicon-diff-removed:before { content: '\f06c'} /* */
80 | .octicon-diff-renamed:before { content: '\f06e'} /* */
81 | .octicon-ellipsis:before { content: '\f09a'} /* */
82 | .octicon-eye-unwatch:before,
83 | .octicon-eye-watch:before,
84 | .octicon-eye:before { content: '\f04e'} /* */
85 | .octicon-file-binary:before { content: '\f094'} /* */
86 | .octicon-file-code:before { content: '\f010'} /* */
87 | .octicon-file-directory:before { content: '\f016'} /* */
88 | .octicon-file-media:before { content: '\f012'} /* */
89 | .octicon-file-pdf:before { content: '\f014'} /* */
90 | .octicon-file-submodule:before { content: '\f017'} /* */
91 | .octicon-file-symlink-directory:before { content: '\f0b1'} /* */
92 | .octicon-file-symlink-file:before { content: '\f0b0'} /* */
93 | .octicon-file-text:before { content: '\f011'} /* */
94 | .octicon-file-zip:before { content: '\f013'} /* */
95 | .octicon-flame:before { content: '\f0d2'} /* */
96 | .octicon-fold:before { content: '\f0cc'} /* */
97 | .octicon-gear:before { content: '\f02f'} /* */
98 | .octicon-gift:before { content: '\f042'} /* */
99 | .octicon-gist:before { content: '\f00e'} /* */
100 | .octicon-gist-secret:before { content: '\f08c'} /* */
101 | .octicon-git-branch-create:before,
102 | .octicon-git-branch-delete:before,
103 | .octicon-git-branch:before { content: '\f020'} /* */
104 | .octicon-git-commit:before { content: '\f01f'} /* */
105 | .octicon-git-compare:before { content: '\f0ac'} /* */
106 | .octicon-git-merge:before { content: '\f023'} /* */
107 | .octicon-git-pull-request-abandoned:before,
108 | .octicon-git-pull-request:before { content: '\f009'} /* */
109 | .octicon-globe:before { content: '\f0b6'} /* */
110 | .octicon-graph:before { content: '\f043'} /* */
111 | .octicon-heart:before { content: '\2665'} /* ♥ */
112 | .octicon-history:before { content: '\f07e'} /* */
113 | .octicon-home:before { content: '\f08d'} /* */
114 | .octicon-horizontal-rule:before { content: '\f070'} /* */
115 | .octicon-hourglass:before { content: '\f09e'} /* */
116 | .octicon-hubot:before { content: '\f09d'} /* */
117 | .octicon-inbox:before { content: '\f0cf'} /* */
118 | .octicon-info:before { content: '\f059'} /* */
119 | .octicon-issue-closed:before { content: '\f028'} /* */
120 | .octicon-issue-opened:before { content: '\f026'} /* */
121 | .octicon-issue-reopened:before { content: '\f027'} /* */
122 | .octicon-jersey:before { content: '\f019'} /* */
123 | .octicon-jump-down:before { content: '\f072'} /* */
124 | .octicon-jump-left:before { content: '\f0a5'} /* */
125 | .octicon-jump-right:before { content: '\f0a6'} /* */
126 | .octicon-jump-up:before { content: '\f073'} /* */
127 | .octicon-key:before { content: '\f049'} /* */
128 | .octicon-keyboard:before { content: '\f00d'} /* */
129 | .octicon-law:before { content: '\f0d8'} /* */
130 | .octicon-light-bulb:before { content: '\f000'} /* */
131 | .octicon-link:before { content: '\f05c'} /* */
132 | .octicon-link-external:before { content: '\f07f'} /* */
133 | .octicon-list-ordered:before { content: '\f062'} /* */
134 | .octicon-list-unordered:before { content: '\f061'} /* */
135 | .octicon-location:before { content: '\f060'} /* */
136 | .octicon-gist-private:before,
137 | .octicon-mirror-private:before,
138 | .octicon-git-fork-private:before,
139 | .octicon-lock:before { content: '\f06a'} /* */
140 | .octicon-logo-github:before { content: '\f092'} /* */
141 | .octicon-mail:before { content: '\f03b'} /* */
142 | .octicon-mail-read:before { content: '\f03c'} /* */
143 | .octicon-mail-reply:before { content: '\f051'} /* */
144 | .octicon-mark-github:before { content: '\f00a'} /* */
145 | .octicon-markdown:before { content: '\f0c9'} /* */
146 | .octicon-megaphone:before { content: '\f077'} /* */
147 | .octicon-mention:before { content: '\f0be'} /* */
148 | .octicon-microscope:before { content: '\f089'} /* */
149 | .octicon-milestone:before { content: '\f075'} /* */
150 | .octicon-mirror-public:before,
151 | .octicon-mirror:before { content: '\f024'} /* */
152 | .octicon-mortar-board:before { content: '\f0d7'} /* */
153 | .octicon-move-down:before { content: '\f0a8'} /* */
154 | .octicon-move-left:before { content: '\f074'} /* */
155 | .octicon-move-right:before { content: '\f0a9'} /* */
156 | .octicon-move-up:before { content: '\f0a7'} /* */
157 | .octicon-mute:before { content: '\f080'} /* */
158 | .octicon-no-newline:before { content: '\f09c'} /* */
159 | .octicon-octoface:before { content: '\f008'} /* */
160 | .octicon-organization:before { content: '\f037'} /* */
161 | .octicon-package:before { content: '\f0c4'} /* */
162 | .octicon-paintcan:before { content: '\f0d1'} /* */
163 | .octicon-pencil:before { content: '\f058'} /* */
164 | .octicon-person-add:before,
165 | .octicon-person-follow:before,
166 | .octicon-person:before { content: '\f018'} /* */
167 | .octicon-pin:before { content: '\f041'} /* */
168 | .octicon-playback-fast-forward:before { content: '\f0bd'} /* */
169 | .octicon-playback-pause:before { content: '\f0bb'} /* */
170 | .octicon-playback-play:before { content: '\f0bf'} /* */
171 | .octicon-playback-rewind:before { content: '\f0bc'} /* */
172 | .octicon-plug:before { content: '\f0d4'} /* */
173 | .octicon-repo-create:before,
174 | .octicon-gist-new:before,
175 | .octicon-file-directory-create:before,
176 | .octicon-file-add:before,
177 | .octicon-plus:before { content: '\f05d'} /* */
178 | .octicon-podium:before { content: '\f0af'} /* */
179 | .octicon-primitive-dot:before { content: '\f052'} /* */
180 | .octicon-primitive-square:before { content: '\f053'} /* */
181 | .octicon-pulse:before { content: '\f085'} /* */
182 | .octicon-puzzle:before { content: '\f0c0'} /* */
183 | .octicon-question:before { content: '\f02c'} /* */
184 | .octicon-quote:before { content: '\f063'} /* */
185 | .octicon-radio-tower:before { content: '\f030'} /* */
186 | .octicon-repo-delete:before,
187 | .octicon-repo:before { content: '\f001'} /* */
188 | .octicon-repo-clone:before { content: '\f04c'} /* */
189 | .octicon-repo-force-push:before { content: '\f04a'} /* */
190 | .octicon-gist-fork:before,
191 | .octicon-repo-forked:before { content: '\f002'} /* */
192 | .octicon-repo-pull:before { content: '\f006'} /* */
193 | .octicon-repo-push:before { content: '\f005'} /* */
194 | .octicon-rocket:before { content: '\f033'} /* */
195 | .octicon-rss:before { content: '\f034'} /* */
196 | .octicon-ruby:before { content: '\f047'} /* */
197 | .octicon-screen-full:before { content: '\f066'} /* */
198 | .octicon-screen-normal:before { content: '\f067'} /* */
199 | .octicon-search-save:before,
200 | .octicon-search:before { content: '\f02e'} /* */
201 | .octicon-server:before { content: '\f097'} /* */
202 | .octicon-settings:before { content: '\f07c'} /* */
203 | .octicon-log-in:before,
204 | .octicon-sign-in:before { content: '\f036'} /* */
205 | .octicon-log-out:before,
206 | .octicon-sign-out:before { content: '\f032'} /* */
207 | .octicon-split:before { content: '\f0c6'} /* */
208 | .octicon-squirrel:before { content: '\f0b2'} /* */
209 | .octicon-star-add:before,
210 | .octicon-star-delete:before,
211 | .octicon-star:before { content: '\f02a'} /* */
212 | .octicon-steps:before { content: '\f0c7'} /* */
213 | .octicon-stop:before { content: '\f08f'} /* */
214 | .octicon-repo-sync:before,
215 | .octicon-sync:before { content: '\f087'} /* */
216 | .octicon-tag-remove:before,
217 | .octicon-tag-add:before,
218 | .octicon-tag:before { content: '\f015'} /* */
219 | .octicon-telescope:before { content: '\f088'} /* */
220 | .octicon-terminal:before { content: '\f0c8'} /* */
221 | .octicon-three-bars:before { content: '\f05e'} /* */
222 | .octicon-thumbsdown:before { content: '\f0db'} /* */
223 | .octicon-thumbsup:before { content: '\f0da'} /* */
224 | .octicon-tools:before { content: '\f031'} /* */
225 | .octicon-trashcan:before { content: '\f0d0'} /* */
226 | .octicon-triangle-down:before { content: '\f05b'} /* */
227 | .octicon-triangle-left:before { content: '\f044'} /* */
228 | .octicon-triangle-right:before { content: '\f05a'} /* */
229 | .octicon-triangle-up:before { content: '\f0aa'} /* */
230 | .octicon-unfold:before { content: '\f039'} /* */
231 | .octicon-unmute:before { content: '\f0ba'} /* */
232 | .octicon-versions:before { content: '\f064'} /* */
233 | .octicon-remove-close:before,
234 | .octicon-x:before { content: '\f081'} /* */
235 | .octicon-zap:before { content: '\26A1'} /* ⚡ */
236 |
--------------------------------------------------------------------------------
/web/css/bootstrap-theme.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.3.4 (http://getbootstrap.com)
3 | * Copyright 2011-2015 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 |
7 | .btn-default,
8 | .btn-primary,
9 | .btn-success,
10 | .btn-info,
11 | .btn-warning,
12 | .btn-danger {
13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
16 | }
17 | .btn-default:active,
18 | .btn-primary:active,
19 | .btn-success:active,
20 | .btn-info:active,
21 | .btn-warning:active,
22 | .btn-danger:active,
23 | .btn-default.active,
24 | .btn-primary.active,
25 | .btn-success.active,
26 | .btn-info.active,
27 | .btn-warning.active,
28 | .btn-danger.active {
29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
31 | }
32 | .btn-default .badge,
33 | .btn-primary .badge,
34 | .btn-success .badge,
35 | .btn-info .badge,
36 | .btn-warning .badge,
37 | .btn-danger .badge {
38 | text-shadow: none;
39 | }
40 | .btn:active,
41 | .btn.active {
42 | background-image: none;
43 | }
44 | .btn-default {
45 | text-shadow: 0 1px 0 #fff;
46 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
47 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
48 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
49 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
50 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
51 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
52 | background-repeat: repeat-x;
53 | border-color: #dbdbdb;
54 | border-color: #ccc;
55 | }
56 | .btn-default:hover,
57 | .btn-default:focus {
58 | background-color: #e0e0e0;
59 | background-position: 0 -15px;
60 | }
61 | .btn-default:active,
62 | .btn-default.active {
63 | background-color: #e0e0e0;
64 | border-color: #dbdbdb;
65 | }
66 | .btn-default.disabled,
67 | .btn-default:disabled,
68 | .btn-default[disabled] {
69 | background-color: #e0e0e0;
70 | background-image: none;
71 | }
72 | .btn-primary {
73 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
74 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
75 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
76 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
79 | background-repeat: repeat-x;
80 | border-color: #245580;
81 | }
82 | .btn-primary:hover,
83 | .btn-primary:focus {
84 | background-color: #265a88;
85 | background-position: 0 -15px;
86 | }
87 | .btn-primary:active,
88 | .btn-primary.active {
89 | background-color: #265a88;
90 | border-color: #245580;
91 | }
92 | .btn-primary.disabled,
93 | .btn-primary:disabled,
94 | .btn-primary[disabled] {
95 | background-color: #265a88;
96 | background-image: none;
97 | }
98 | .btn-success {
99 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
100 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
101 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
102 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
103 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
104 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
105 | background-repeat: repeat-x;
106 | border-color: #3e8f3e;
107 | }
108 | .btn-success:hover,
109 | .btn-success:focus {
110 | background-color: #419641;
111 | background-position: 0 -15px;
112 | }
113 | .btn-success:active,
114 | .btn-success.active {
115 | background-color: #419641;
116 | border-color: #3e8f3e;
117 | }
118 | .btn-success.disabled,
119 | .btn-success:disabled,
120 | .btn-success[disabled] {
121 | background-color: #419641;
122 | background-image: none;
123 | }
124 | .btn-info {
125 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
126 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
127 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
128 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
129 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
130 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
131 | background-repeat: repeat-x;
132 | border-color: #28a4c9;
133 | }
134 | .btn-info:hover,
135 | .btn-info:focus {
136 | background-color: #2aabd2;
137 | background-position: 0 -15px;
138 | }
139 | .btn-info:active,
140 | .btn-info.active {
141 | background-color: #2aabd2;
142 | border-color: #28a4c9;
143 | }
144 | .btn-info.disabled,
145 | .btn-info:disabled,
146 | .btn-info[disabled] {
147 | background-color: #2aabd2;
148 | background-image: none;
149 | }
150 | .btn-warning {
151 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
152 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
153 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
154 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
155 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
156 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
157 | background-repeat: repeat-x;
158 | border-color: #e38d13;
159 | }
160 | .btn-warning:hover,
161 | .btn-warning:focus {
162 | background-color: #eb9316;
163 | background-position: 0 -15px;
164 | }
165 | .btn-warning:active,
166 | .btn-warning.active {
167 | background-color: #eb9316;
168 | border-color: #e38d13;
169 | }
170 | .btn-warning.disabled,
171 | .btn-warning:disabled,
172 | .btn-warning[disabled] {
173 | background-color: #eb9316;
174 | background-image: none;
175 | }
176 | .btn-danger {
177 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
178 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
179 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
180 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
182 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
183 | background-repeat: repeat-x;
184 | border-color: #b92c28;
185 | }
186 | .btn-danger:hover,
187 | .btn-danger:focus {
188 | background-color: #c12e2a;
189 | background-position: 0 -15px;
190 | }
191 | .btn-danger:active,
192 | .btn-danger.active {
193 | background-color: #c12e2a;
194 | border-color: #b92c28;
195 | }
196 | .btn-danger.disabled,
197 | .btn-danger:disabled,
198 | .btn-danger[disabled] {
199 | background-color: #c12e2a;
200 | background-image: none;
201 | }
202 | .thumbnail,
203 | .img-thumbnail {
204 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
205 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
206 | }
207 | .dropdown-menu > li > a:hover,
208 | .dropdown-menu > li > a:focus {
209 | background-color: #e8e8e8;
210 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
211 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
212 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
213 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
214 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
215 | background-repeat: repeat-x;
216 | }
217 | .dropdown-menu > .active > a,
218 | .dropdown-menu > .active > a:hover,
219 | .dropdown-menu > .active > a:focus {
220 | background-color: #2e6da4;
221 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
222 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
223 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
224 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
225 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
226 | background-repeat: repeat-x;
227 | }
228 | .navbar-default {
229 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
230 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
231 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
232 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
233 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
234 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
235 | background-repeat: repeat-x;
236 | border-radius: 4px;
237 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
238 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
239 | }
240 | .navbar-default .navbar-nav > .open > a,
241 | .navbar-default .navbar-nav > .active > a {
242 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
243 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
244 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
245 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
246 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
247 | background-repeat: repeat-x;
248 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
249 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
250 | }
251 | .navbar-brand,
252 | .navbar-nav > li > a {
253 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
254 | }
255 | .navbar-inverse {
256 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
257 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
258 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
259 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
260 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
261 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
262 | background-repeat: repeat-x;
263 | }
264 | .navbar-inverse .navbar-nav > .open > a,
265 | .navbar-inverse .navbar-nav > .active > a {
266 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
267 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
268 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
269 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
270 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
271 | background-repeat: repeat-x;
272 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
273 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
274 | }
275 | .navbar-inverse .navbar-brand,
276 | .navbar-inverse .navbar-nav > li > a {
277 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
278 | }
279 | .navbar-static-top,
280 | .navbar-fixed-top,
281 | .navbar-fixed-bottom {
282 | border-radius: 0;
283 | }
284 | @media (max-width: 767px) {
285 | .navbar .navbar-nav .open .dropdown-menu > .active > a,
286 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
287 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
288 | color: #fff;
289 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
290 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
291 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
292 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
293 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
294 | background-repeat: repeat-x;
295 | }
296 | }
297 | .alert {
298 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
299 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
300 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
301 | }
302 | .alert-success {
303 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
304 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
305 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
306 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
307 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
308 | background-repeat: repeat-x;
309 | border-color: #b2dba1;
310 | }
311 | .alert-info {
312 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
313 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
314 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
315 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
316 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
317 | background-repeat: repeat-x;
318 | border-color: #9acfea;
319 | }
320 | .alert-warning {
321 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
322 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
323 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
324 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
325 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
326 | background-repeat: repeat-x;
327 | border-color: #f5e79e;
328 | }
329 | .alert-danger {
330 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
331 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
332 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
333 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
334 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
335 | background-repeat: repeat-x;
336 | border-color: #dca7a7;
337 | }
338 | .progress {
339 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
340 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
342 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
344 | background-repeat: repeat-x;
345 | }
346 | .progress-bar {
347 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
348 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);
349 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
350 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);
351 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
352 | background-repeat: repeat-x;
353 | }
354 | .progress-bar-success {
355 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
356 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
357 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
358 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
359 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
360 | background-repeat: repeat-x;
361 | }
362 | .progress-bar-info {
363 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
364 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
365 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
366 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
367 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
368 | background-repeat: repeat-x;
369 | }
370 | .progress-bar-warning {
371 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
372 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
373 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
374 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
375 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
376 | background-repeat: repeat-x;
377 | }
378 | .progress-bar-danger {
379 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
380 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
381 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
382 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
383 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
384 | background-repeat: repeat-x;
385 | }
386 | .progress-bar-striped {
387 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
388 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
389 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
390 | }
391 | .list-group {
392 | border-radius: 4px;
393 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
394 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
395 | }
396 | .list-group-item.active,
397 | .list-group-item.active:hover,
398 | .list-group-item.active:focus {
399 | text-shadow: 0 -1px 0 #286090;
400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
405 | background-repeat: repeat-x;
406 | border-color: #2b669a;
407 | }
408 | .list-group-item.active .badge,
409 | .list-group-item.active:hover .badge,
410 | .list-group-item.active:focus .badge {
411 | text-shadow: none;
412 | }
413 | .panel {
414 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
415 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
416 | }
417 | .panel-default > .panel-heading {
418 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
419 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
420 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
421 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
422 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
423 | background-repeat: repeat-x;
424 | }
425 | .panel-primary > .panel-heading {
426 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
427 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
428 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
429 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
430 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
431 | background-repeat: repeat-x;
432 | }
433 | .panel-success > .panel-heading {
434 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
435 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
436 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
437 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
438 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
439 | background-repeat: repeat-x;
440 | }
441 | .panel-info > .panel-heading {
442 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
443 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
444 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
445 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
446 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
447 | background-repeat: repeat-x;
448 | }
449 | .panel-warning > .panel-heading {
450 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
451 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
453 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
455 | background-repeat: repeat-x;
456 | }
457 | .panel-danger > .panel-heading {
458 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
459 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
461 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
463 | background-repeat: repeat-x;
464 | }
465 | .well {
466 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
467 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
469 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
471 | background-repeat: repeat-x;
472 | border-color: #dcdcdc;
473 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
474 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
475 | }
476 | /*# sourceMappingURL=bootstrap-theme.css.map */
477 |
--------------------------------------------------------------------------------
/web/js/angular-ui-router.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * State-based routing for AngularJS
3 | * @version v0.2.15
4 | * @link http://angular-ui.github.com/
5 | * @license MIT License, http://www.opensource.org/licenses/MIT
6 | */
7 | "undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="ui.router"),function(a,b,c){"use strict";function d(a,b){return N(new(N(function(){},{prototype:a})),b)}function e(a){return M(arguments,function(b){b!==a&&M(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)})}),a}function f(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function g(a){if(Object.keys)return Object.keys(a);var b=[];return M(a,function(a,c){b.push(c)}),b}function h(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function i(a,b,c,d){var e,i=f(c,d),j={},k=[];for(var l in i)if(i[l].params&&(e=g(i[l].params),e.length))for(var m in e)h(k,e[m])>=0||(k.push(e[m]),j[e[m]]=a[e[m]]);return N({},j,b)}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e "));if(s[c]=d,J(a))q.push(c,[function(){return b.get(a)}],j);else{var e=b.annotate(a);M(e,function(a){a!==c&&i.hasOwnProperty(a)&&n(i[a],a)}),q.push(c,a,e)}r.pop(),s[c]=f}}function o(a){return K(a)&&a.then&&a.$$promises}if(!K(i))throw new Error("'invocables' must be an object");var p=g(i||{}),q=[],r=[],s={};return M(i,n),i=r=s=null,function(d,f,g){function h(){--u||(v||e(t,f.$$values),r.$$values=t,r.$$promises=r.$$promises||!0,delete r.$$inheritedValues,n.resolve(t))}function i(a){r.$$failure=a,n.reject(a)}function j(c,e,f){function j(a){l.reject(a),i(a)}function k(){if(!H(r.$$failure))try{l.resolve(b.invoke(e,g,t)),l.promise.then(function(a){t[c]=a,h()},j)}catch(a){j(a)}}var l=a.defer(),m=0;M(f,function(a){s.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,s[a].then(function(b){t[a]=b,--m||k()},j))}),m||k(),s[c]=l.promise}if(o(d)&&g===c&&(g=f,f=d,d=null),d){if(!K(d))throw new Error("'locals' must be an object")}else d=k;if(f){if(!o(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=l;var n=a.defer(),r=n.promise,s=r.$$promises={},t=N({},d),u=1+q.length/3,v=!1;if(H(f.$$failure))return i(f.$$failure),r;f.$$inheritedValues&&e(t,m(f.$$inheritedValues,p)),N(s,f.$$promises),f.$$values?(v=e(t,m(f.$$values,p)),r.$$inheritedValues=m(f.$$values,p),h()):(f.$$inheritedValues&&(r.$$inheritedValues=m(f.$$inheritedValues,p)),f.then(h,i));for(var w=0,x=q.length;x>w;w+=3)d.hasOwnProperty(q[w])?h():j(q[w],q[w+1],q[w+2]);return r}},this.resolve=function(a,b,c,d){return this.study(a)(b,c,d)}}function q(a,b,c){this.fromConfig=function(a,b,c){return H(a.template)?this.fromString(a.template,b):H(a.templateUrl)?this.fromUrl(a.templateUrl,b):H(a.templateProvider)?this.fromProvider(a.templateProvider,b,c):null},this.fromString=function(a,b){return I(a)?a(b):a},this.fromUrl=function(c,d){return I(c)&&(c=c(d)),null==c?null:a.get(c,{cache:b,headers:{Accept:"text/html"}}).then(function(a){return a.data})},this.fromProvider=function(a,b,d){return c.invoke(a,null,d||{params:b})}}function r(a,b,e){function f(b,c,d,e){if(q.push(b),o[b])return o[b];if(!/^\w+(-+\w+)*(?:\[\])?$/.test(b))throw new Error("Invalid parameter name '"+b+"' in pattern '"+a+"'");if(p[b])throw new Error("Duplicate parameter name '"+b+"' in pattern '"+a+"'");return p[b]=new P.Param(b,c,d,e),p[b]}function g(a,b,c,d){var e=["",""],f=a.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!b)return f;switch(c){case!1:e=["(",")"+(d?"?":"")];break;case!0:e=["?(",")?"];break;default:e=["("+c+"|",")?"]}return f+e[0]+b+e[1]}function h(e,f){var g,h,i,j,k;return g=e[2]||e[3],k=b.params[g],i=a.substring(m,e.index),h=f?e[4]:e[4]||("*"==e[1]?".*":null),j=P.type(h||"string")||d(P.type("string"),{pattern:new RegExp(h,b.caseInsensitive?"i":c)}),{id:g,regexp:h,segment:i,type:j,cfg:k}}b=N({params:{}},K(b)?b:{});var i,j=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,k=/([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,l="^",m=0,n=this.segments=[],o=e?e.params:{},p=this.params=e?e.params.$$new():new P.ParamSet,q=[];this.source=a;for(var r,s,t;(i=j.exec(a))&&(r=h(i,!1),!(r.segment.indexOf("?")>=0));)s=f(r.id,r.type,r.cfg,"path"),l+=g(r.segment,s.type.pattern.source,s.squash,s.isOptional),n.push(r.segment),m=j.lastIndex;t=a.substring(m);var u=t.indexOf("?");if(u>=0){var v=this.sourceSearch=t.substring(u);if(t=t.substring(0,u),this.sourcePath=a.substring(0,m+u),v.length>0)for(m=0;i=k.exec(v);)r=h(i,!0),s=f(r.id,r.type,r.cfg,"search"),m=j.lastIndex}else this.sourcePath=a,this.sourceSearch="";l+=g(t)+(b.strict===!1?"/?":"")+"$",n.push(t),this.regexp=new RegExp(l,b.caseInsensitive?"i":c),this.prefix=n[0],this.$$paramNames=q}function s(a){N(this,a)}function t(){function a(a){return null!=a?a.toString().replace(/\//g,"%2F"):a}function e(a){return null!=a?a.toString().replace(/%2F/g,"/"):a}function f(){return{strict:p,caseInsensitive:m}}function i(a){return I(a)||L(a)&&I(a[a.length-1])}function j(){for(;w.length;){var a=w.shift();if(a.pattern)throw new Error("You cannot override a type's .pattern at runtime.");b.extend(u[a.name],l.invoke(a.def))}}function k(a){N(this,a||{})}P=this;var l,m=!1,p=!0,q=!1,u={},v=!0,w=[],x={string:{encode:a,decode:e,is:function(a){return null==a||!H(a)||"string"==typeof a},pattern:/[^/]*/},"int":{encode:a,decode:function(a){return parseInt(a,10)},is:function(a){return H(a)&&this.decode(a.toString())===a},pattern:/\d+/},bool:{encode:function(a){return a?1:0},decode:function(a){return 0!==parseInt(a,10)},is:function(a){return a===!0||a===!1},pattern:/0|1/},date:{encode:function(a){return this.is(a)?[a.getFullYear(),("0"+(a.getMonth()+1)).slice(-2),("0"+a.getDate()).slice(-2)].join("-"):c},decode:function(a){if(this.is(a))return a;var b=this.capture.exec(a);return b?new Date(b[1],b[2]-1,b[3]):c},is:function(a){return a instanceof Date&&!isNaN(a.valueOf())},equals:function(a,b){return this.is(a)&&this.is(b)&&a.toISOString()===b.toISOString()},pattern:/[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,capture:/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/},json:{encode:b.toJson,decode:b.fromJson,is:b.isObject,equals:b.equals,pattern:/[^/]*/},any:{encode:b.identity,decode:b.identity,equals:b.equals,pattern:/.*/}};t.$$getDefaultValue=function(a){if(!i(a.value))return a.value;if(!l)throw new Error("Injectable functions cannot be called at configuration time");return l.invoke(a.value)},this.caseInsensitive=function(a){return H(a)&&(m=a),m},this.strictMode=function(a){return H(a)&&(p=a),p},this.defaultSquashPolicy=function(a){if(!H(a))return q;if(a!==!0&&a!==!1&&!J(a))throw new Error("Invalid squash policy: "+a+". Valid policies: false, true, arbitrary-string");return q=a,a},this.compile=function(a,b){return new r(a,N(f(),b))},this.isMatcher=function(a){if(!K(a))return!1;var b=!0;return M(r.prototype,function(c,d){I(c)&&(b=b&&H(a[d])&&I(a[d]))}),b},this.type=function(a,b,c){if(!H(b))return u[a];if(u.hasOwnProperty(a))throw new Error("A type named '"+a+"' has already been defined.");return u[a]=new s(N({name:a},b)),c&&(w.push({name:a,def:c}),v||j()),this},M(x,function(a,b){u[b]=new s(N({name:b},a))}),u=d(u,{}),this.$get=["$injector",function(a){return l=a,v=!1,j(),M(x,function(a,b){u[b]||(u[b]=new s(a))}),this}],this.Param=function(a,b,d,e){function f(a){var b=K(a)?g(a):[],c=-1===h(b,"value")&&-1===h(b,"type")&&-1===h(b,"squash")&&-1===h(b,"array");return c&&(a={value:a}),a.$$fn=i(a.value)?a.value:function(){return a.value},a}function j(b,c,d){if(b.type&&c)throw new Error("Param '"+a+"' has two type configurations.");return c?c:b.type?b.type instanceof s?b.type:new s(b.type):"config"===d?u.any:u.string}function k(){var b={array:"search"===e?"auto":!1},c=a.match(/\[\]$/)?{array:!0}:{};return N(b,c,d).array}function m(a,b){var c=a.squash;if(!b||c===!1)return!1;if(!H(c)||null==c)return q;if(c===!0||J(c))return c;throw new Error("Invalid squash policy: '"+c+"'. Valid policies: false, true, or arbitrary string")}function p(a,b,d,e){var f,g,i=[{from:"",to:d||b?c:""},{from:null,to:d||b?c:""}];return f=L(a.replace)?a.replace:[],J(e)&&f.push({from:e,to:c}),g=o(f,function(a){return a.from}),n(i,function(a){return-1===h(g,a.from)}).concat(f)}function r(){if(!l)throw new Error("Injectable functions cannot be called at configuration time");var a=l.invoke(d.$$fn);if(null!==a&&a!==c&&!w.type.is(a))throw new Error("Default value ("+a+") for parameter '"+w.id+"' is not an instance of Type ("+w.type.name+")");return a}function t(a){function b(a){return function(b){return b.from===a}}function c(a){var c=o(n(w.replace,b(a)),function(a){return a.to});return c.length?c[0]:a}return a=c(a),H(a)?w.type.$normalize(a):r()}function v(){return"{Param:"+a+" "+b+" squash: '"+z+"' optional: "+y+"}"}var w=this;d=f(d),b=j(d,b,e);var x=k();b=x?b.$asArray(x,"search"===e):b,"string"!==b.name||x||"path"!==e||d.value!==c||(d.value="");var y=d.value!==c,z=m(d,y),A=p(d,x,y,z);N(this,{id:a,type:b,location:e,array:x,squash:z,replace:A,isOptional:y,value:t,dynamic:c,config:d,toString:v})},k.prototype={$$new:function(){return d(this,N(new k,{$$parent:this}))},$$keys:function(){for(var a=[],b=[],c=this,d=g(k.prototype);c;)b.push(c),c=c.$$parent;return b.reverse(),M(b,function(b){M(g(b),function(b){-1===h(a,b)&&-1===h(d,b)&&a.push(b)})}),a},$$values:function(a){var b={},c=this;return M(c.$$keys(),function(d){b[d]=c[d].value(a&&a[d])}),b},$$equals:function(a,b){var c=!0,d=this;return M(d.$$keys(),function(e){var f=a&&a[e],g=b&&b[e];d[e].type.equals(f,g)||(c=!1)}),c},$$validates:function(a){var d,e,f,g,h,i=this.$$keys();for(d=0;de;e++)if(b(j[e]))return;k&&b(k)}}function n(){return i=i||e.$on("$locationChangeSuccess",m)}var o,p=g.baseHref(),q=d.url();return l||n(),{sync:function(){m()},listen:function(){return n()},update:function(a){return a?void(q=d.url()):void(d.url()!==q&&(d.url(q),d.replace()))},push:function(a,b,e){var f=a.format(b||{});null!==f&&b&&b["#"]&&(f+="#"+b["#"]),d.url(f),o=e&&e.$$avoidResync?d.url():c,e&&e.replace&&d.replace()},href:function(c,e,f){if(!c.validates(e))return null;var g=a.html5Mode();b.isObject(g)&&(g=g.enabled);var i=c.format(e);if(f=f||{},g||null===i||(i="#"+a.hashPrefix()+i),null!==i&&e&&e["#"]&&(i+="#"+e["#"]),i=h(i,g,f.absolute),!f.absolute||!i)return i;var j=!g&&i?"/":"",k=d.port();return k=80===k||443===k?"":":"+k,[d.protocol(),"://",d.host(),k,j,i].join("")}}}var i,j=[],k=null,l=!1;this.rule=function(a){if(!I(a))throw new Error("'rule' must be a function");return j.push(a),this},this.otherwise=function(a){if(J(a)){var b=a;a=function(){return b}}else if(!I(a))throw new Error("'rule' must be a function");return k=a,this},this.when=function(a,b){var c,h=J(b);if(J(a)&&(a=d.compile(a)),!h&&!I(b)&&!L(b))throw new Error("invalid 'handler' in when()");var i={matcher:function(a,b){return h&&(c=d.compile(b),b=["$match",function(a){return c.format(a)}]),N(function(c,d){return g(c,b,a.exec(d.path(),d.search()))},{prefix:J(a.prefix)?a.prefix:""})},regex:function(a,b){if(a.global||a.sticky)throw new Error("when() RegExp must not be global or sticky");return h&&(c=b,b=["$match",function(a){return f(c,a)}]),N(function(c,d){return g(c,b,a.exec(d.path()))},{prefix:e(a)})}},j={matcher:d.isMatcher(a),regex:a instanceof RegExp};for(var k in j)if(j[k])return this.rule(i[k](a,b));throw new Error("invalid 'what' in when()")},this.deferIntercept=function(a){a===c&&(a=!0),l=a},this.$get=h,h.$inject=["$location","$rootScope","$injector","$browser"]}function v(a,e){function f(a){return 0===a.indexOf(".")||0===a.indexOf("^")}function m(a,b){if(!a)return c;var d=J(a),e=d?a:a.name,g=f(e);if(g){if(!b)throw new Error("No reference point given for path '"+e+"'");b=m(b);for(var h=e.split("."),i=0,j=h.length,k=b;j>i;i++)if(""!==h[i]||0!==i){if("^"!==h[i])break;if(!k.parent)throw new Error("Path '"+e+"' not valid for state '"+b.name+"'");k=k.parent}else k=b;h=h.slice(i).join("."),e=k.name+(k.name&&h?".":"")+h}var l=z[e];return!l||!d&&(d||l!==a&&l.self!==a)?c:l}function n(a,b){A[a]||(A[a]=[]),A[a].push(b)}function p(a){for(var b=A[a]||[];b.length;)q(b.shift())}function q(b){b=d(b,{self:b,resolve:b.resolve||{},toString:function(){return this.name}});var c=b.name;if(!J(c)||c.indexOf("@")>=0)throw new Error("State must have a valid name");if(z.hasOwnProperty(c))throw new Error("State '"+c+"'' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):J(b.parent)?b.parent:K(b.parent)&&J(b.parent.name)?b.parent.name:"";if(e&&!z[e])return n(e,b.self);for(var f in C)I(C[f])&&(b[f]=C[f](b,C.$delegates[f]));return z[c]=b,!b[B]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){y.$current.navigable==b&&j(a,c)||y.transitionTo(b,a,{inherit:!0,location:!1})}]),p(c),b}function r(a){return a.indexOf("*")>-1}function s(a){for(var b=a.split("."),c=y.$current.name.split("."),d=0,e=b.length;e>d;d++)"*"===b[d]&&(c[d]="*");return"**"===b[0]&&(c=c.slice(h(c,b[1])),c.unshift("**")),"**"===b[b.length-1]&&(c.splice(h(c,b[b.length-2])+1,Number.MAX_VALUE),c.push("**")),b.length!=c.length?!1:c.join("")===b.join("")}function t(a,b){return J(a)&&!H(b)?C[a]:I(b)&&J(a)?(C[a]&&!C.$delegates[a]&&(C.$delegates[a]=C[a]),C[a]=b,this):this}function u(a,b){return K(a)?b=a:b.name=a,q(b),this}function v(a,e,f,h,l,n,p,q,t){function u(b,c,d,f){var g=a.$broadcast("$stateNotFound",b,c,d);if(g.defaultPrevented)return p.update(),D;if(!g.retry)return null;if(f.$retry)return p.update(),E;var h=y.transition=e.when(g.retry);return h.then(function(){return h!==y.transition?A:(b.options.$retry=!0,y.transitionTo(b.to,b.toParams,b.options))},function(){return D}),p.update(),h}function v(a,c,d,g,i,j){function m(){var c=[];return M(a.views,function(d,e){var g=d.resolve&&d.resolve!==a.resolve?d.resolve:{};g.$template=[function(){return f.load(e,{view:d,locals:i.globals,params:n,notify:j.notify})||""}],c.push(l.resolve(g,i.globals,i.resolve,a).then(function(c){if(I(d.controllerProvider)||L(d.controllerProvider)){var f=b.extend({},g,i.globals);c.$$controller=h.invoke(d.controllerProvider,null,f)}else c.$$controller=d.controller;c.$$state=a,c.$$controllerAs=d.controllerAs,i[e]=c}))}),e.all(c).then(function(){return i.globals})}var n=d?c:k(a.params.$$keys(),c),o={$stateParams:n};i.resolve=l.resolve(a.resolve,o,i.resolve,a);var p=[i.resolve.then(function(a){i.globals=a})];return g&&p.push(g),e.all(p).then(m).then(function(a){return i})}var A=e.reject(new Error("transition superseded")),C=e.reject(new Error("transition prevented")),D=e.reject(new Error("transition aborted")),E=e.reject(new Error("transition failed"));return x.locals={resolve:null,globals:{$stateParams:{}}},y={params:{},current:x.self,$current:x,transition:null},y.reload=function(a){return y.transitionTo(y.current,n,{reload:a||!0,inherit:!1,notify:!0})},y.go=function(a,b,c){return y.transitionTo(a,b,N({inherit:!0,relative:y.$current},c))},y.transitionTo=function(b,c,f){c=c||{},f=N({location:!0,inherit:!1,relative:null,notify:!0,reload:!1,$retry:!1},f||{});var g,j=y.$current,l=y.params,o=j.path,q=m(b,f.relative),r=c["#"];if(!H(q)){var s={to:b,toParams:c,options:f},t=u(s,j.self,l,f);if(t)return t;if(b=s.to,c=s.toParams,f=s.options,q=m(b,f.relative),!H(q)){if(!f.relative)throw new Error("No such state '"+b+"'");throw new Error("Could not resolve '"+b+"' from state '"+f.relative+"'")}}if(q[B])throw new Error("Cannot transition to abstract state '"+b+"'");if(f.inherit&&(c=i(n,c||{},y.$current,q)),!q.params.$$validates(c))return E;c=q.params.$$values(c),b=q;var z=b.path,D=0,F=z[D],G=x.locals,I=[];if(f.reload){if(J(f.reload)||K(f.reload)){if(K(f.reload)&&!f.reload.name)throw new Error("Invalid reload state object");var L=f.reload===!0?o[0]:m(f.reload);if(f.reload&&!L)throw new Error("No such reload state '"+(J(f.reload)?f.reload:f.reload.name)+"'");for(;F&&F===o[D]&&F!==L;)G=I[D]=F.locals,D++,F=z[D]}}else for(;F&&F===o[D]&&F.ownParams.$$equals(c,l);)G=I[D]=F.locals,D++,F=z[D];if(w(b,c,j,l,G,f))return r&&(c["#"]=r),y.params=c,O(y.params,n),f.location&&b.navigable&&b.navigable.url&&(p.push(b.navigable.url,c,{$$avoidResync:!0,replace:"replace"===f.location}),p.update(!0)),y.transition=null,e.when(y.current);if(c=k(b.params.$$keys(),c||{}),f.notify&&a.$broadcast("$stateChangeStart",b.self,c,j.self,l).defaultPrevented)return a.$broadcast("$stateChangeCancel",b.self,c,j.self,l),p.update(),C;for(var M=e.when(G),P=D;P=D;d--)g=o[d],g.self.onExit&&h.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=D;d=0?e:e+"@"+(f?f.state.name:"")}function B(a,b){var c,d=a.match(/^\s*({[^}]*})\s*$/);if(d&&(a=b+"("+d[1]+")"),c=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/),!c||4!==c.length)throw new Error("Invalid state ref '"+a+"'");return{state:c[1],paramExpr:c[3]||null}}function C(a){var b=a.parent().inheritedData("$uiView");return b&&b.state&&b.state.name?b.state:void 0}function D(a,c){var d=["location","inherit","reload","absolute"];return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(e,f,g,h){var i=B(g.uiSref,a.current.name),j=null,k=C(f)||a.$current,l="[object SVGAnimatedString]"===Object.prototype.toString.call(f.prop("href"))?"xlink:href":"href",m=null,n="A"===f.prop("tagName").toUpperCase(),o="FORM"===f[0].nodeName,p=o?"action":l,q=!0,r={relative:k,inherit:!0},s=e.$eval(g.uiSrefOpts)||{};b.forEach(d,function(a){a in s&&(r[a]=s[a])});var t=function(c){if(c&&(j=b.copy(c)),q){m=a.href(i.state,j,r);var d=h[1]||h[0];return d&&d.$$addStateInfo(i.state,j),null===m?(q=!1,!1):void g.$set(p,m)}};i.paramExpr&&(e.$watch(i.paramExpr,function(a,b){a!==j&&t(a)},!0),j=b.copy(e.$eval(i.paramExpr))),t(),o||f.bind("click",function(b){var d=b.which||b.button;if(!(d>1||b.ctrlKey||b.metaKey||b.shiftKey||f.attr("target"))){var e=c(function(){a.go(i.state,j,r)});b.preventDefault();var g=n&&!m?1:0;b.preventDefault=function(){g--<=0&&c.cancel(e)}}})}}}function E(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs",function(b,d,e){function f(){g()?d.addClass(i):d.removeClass(i)}function g(){for(var a=0;ae;e++){g=h[e];var l=this.params[g],m=d[e+1];for(f=0;fe;e++)g=h[e],k[g]=this.params[g].value(b[g]);return k},r.prototype.parameters=function(a){return H(a)?this.params[a]||null:this.$$paramNames},r.prototype.validates=function(a){return this.params.$$validates(a)},r.prototype.format=function(a){function b(a){return encodeURIComponent(a).replace(/-/g,function(a){return"%5C%"+a.charCodeAt(0).toString(16).toUpperCase()})}a=a||{};var c=this.segments,d=this.parameters(),e=this.params;if(!this.validates(a))return null;var f,g=!1,h=c.length-1,i=d.length,j=c[0];for(f=0;i>f;f++){var k=h>f,l=d[f],m=e[l],n=m.value(a[l]),p=m.isOptional&&m.type.equals(m.value(),n),q=p?m.squash:!1,r=m.type.encode(n);if(k){var s=c[f+1];if(q===!1)null!=r&&(j+=L(r)?o(r,b).join("-"):encodeURIComponent(r)),j+=s;else if(q===!0){var t=j.match(/\/$/)?/\/?(.*)/:/(.*)/;j+=s.match(t)[1]}else J(q)&&(j+=q+s)}else{if(null==r||p&&q!==!1)continue;L(r)||(r=[r]),r=o(r,encodeURIComponent).join("&"+l+"="),j+=(g?"&":"?")+(l+"="+r),g=!0}}return j},s.prototype.is=function(a,b){return!0},s.prototype.encode=function(a,b){return a},s.prototype.decode=function(a,b){return a},s.prototype.equals=function(a,b){return a==b},s.prototype.$subPattern=function(){var a=this.pattern.toString();return a.substr(1,a.length-2)},s.prototype.pattern=/.*/,s.prototype.toString=function(){return"{Type:"+this.name+"}"},s.prototype.$normalize=function(a){return this.is(a)?a:this.decode(a)},s.prototype.$asArray=function(a,b){function d(a,b){function d(a,b){return function(){return a[b].apply(a,arguments)}}function e(a){return L(a)?a:H(a)?[a]:[]}function f(a){switch(a.length){case 0:return c;case 1:return"auto"===b?a[0]:a;default:return a}}function g(a){return!a}function h(a,b){return function(c){c=e(c);var d=o(c,a);return b===!0?0===n(d,g).length:f(d)}}function i(a){return function(b,c){var d=e(b),f=e(c);if(d.length!==f.length)return!1;for(var g=0;g