├── log └── README ├── views ├── log │ ├── README │ ├── frigga.log.20130502 │ ├── frigga.log │ └── frigga.log.20130501 ├── deploy │ └── README ├── conf │ ├── ip.yml │ ├── base.god │ ├── frigga.yml │ ├── frigga.god │ └── passport-agent.god.sample ├── vendor │ └── cache │ │ ├── rack-1.5.2.gem │ │ ├── thin-1.5.1.gem │ │ ├── tilt-1.3.7.gem │ │ ├── tins-0.7.2.gem │ │ ├── thor-0.18.1.gem │ │ ├── builder-3.2.0.gem │ │ ├── daemons-1.1.9.gem │ │ ├── net-ldap-0.3.1.gem │ │ ├── rack-rpc-0.0.6.gem │ │ ├── sinatra-1.4.2.gem │ │ ├── addressable-2.3.3.gem │ │ ├── file-tail-1.0.12.gem │ │ ├── god-xiaomi-1.13.7.gem │ │ ├── em-websocket-0.3.8.gem │ │ ├── eventmachine-1.0.3.gem │ │ ├── rack-protection-1.5.0.gem │ │ └── sinatra-websocket-0.3.0.gem ├── static │ └── vendor │ │ └── bootstrap │ │ ├── img │ │ ├── glyphicons-halflings.png │ │ └── glyphicons-halflings-white.png │ │ ├── js │ │ ├── bootstrap-tooltip.js │ │ └── bootstrap.min.js │ │ └── css │ │ └── bootstrap-responsive.min.css ├── notfound.erb ├── error.erb ├── login_form.erb ├── Gemfile ├── lib │ ├── frigga │ │ ├── log.rb │ │ ├── rpc │ │ │ ├── god.rb │ │ │ ├── ctmgr.rb │ │ │ ├── server.rb │ │ │ └── deploy.rb │ │ ├── rpc.rb │ │ ├── auth.rb │ │ └── talk.rb │ └── frigga.rb ├── about.erb ├── README.md ├── bin │ ├── frigga.rb │ └── testrpc.rb ├── tail.erb ├── layout.erb ├── index.erb ├── ctmgr.erb └── script │ └── run.rb ├── conf ├── ip.yml ├── frigga.yml ├── base.god └── frigga.god ├── .gitignore ├── vendor └── cache │ ├── rack-1.5.2.gem │ ├── thin-1.5.1.gem │ ├── thor-0.18.1.gem │ ├── tilt-1.3.7.gem │ ├── tins-0.7.2.gem │ ├── builder-3.2.0.gem │ ├── daemons-1.1.9.gem │ ├── sinatra-1.4.2.gem │ ├── file-tail-1.0.12.gem │ ├── rack-rpc-0.0.6.gem │ ├── addressable-2.3.3.gem │ ├── em-websocket-0.3.8.gem │ ├── eventmachine-1.0.3.gem │ ├── god-xiaomi-1.13.7.gem │ ├── rack-protection-1.5.0.gem │ └── sinatra-websocket-0.3.0.gem ├── static └── vendor │ └── bootstrap │ ├── img │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png │ ├── js │ ├── bootstrap-tooltip.js │ └── bootstrap.min.js │ └── css │ └── bootstrap-responsive.min.css ├── Gemfile ├── lib ├── frigga │ ├── rpc │ │ ├── server.rb │ │ └── god.rb │ ├── log.rb │ ├── auth.rb │ ├── rpc.rb │ └── talk.rb └── frigga.rb ├── bin └── frigga.rb ├── script └── run.rb └── README.md /log/README: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/log/README: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/deploy/README: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conf/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 127.0.0.1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gods/ 2 | log/frigga.* 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /conf/frigga.yml: -------------------------------------------------------------------------------- 1 | --- 2 | port: 9001 3 | http_auth: ["admin", "123"] 4 | -------------------------------------------------------------------------------- /views/conf/ip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 172.30.251.110 3 | - 10.0.9.10 4 | - 192.168.1.106 5 | -------------------------------------------------------------------------------- /vendor/cache/rack-1.5.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/rack-1.5.2.gem -------------------------------------------------------------------------------- /vendor/cache/thin-1.5.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/thin-1.5.1.gem -------------------------------------------------------------------------------- /vendor/cache/thor-0.18.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/thor-0.18.1.gem -------------------------------------------------------------------------------- /vendor/cache/tilt-1.3.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/tilt-1.3.7.gem -------------------------------------------------------------------------------- /vendor/cache/tins-0.7.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/tins-0.7.2.gem -------------------------------------------------------------------------------- /vendor/cache/builder-3.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/builder-3.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/daemons-1.1.9.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/daemons-1.1.9.gem -------------------------------------------------------------------------------- /vendor/cache/sinatra-1.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/sinatra-1.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/file-tail-1.0.12.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/file-tail-1.0.12.gem -------------------------------------------------------------------------------- /vendor/cache/rack-rpc-0.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/rack-rpc-0.0.6.gem -------------------------------------------------------------------------------- /views/vendor/cache/rack-1.5.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/rack-1.5.2.gem -------------------------------------------------------------------------------- /views/vendor/cache/thin-1.5.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/thin-1.5.1.gem -------------------------------------------------------------------------------- /views/vendor/cache/tilt-1.3.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/tilt-1.3.7.gem -------------------------------------------------------------------------------- /views/vendor/cache/tins-0.7.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/tins-0.7.2.gem -------------------------------------------------------------------------------- /vendor/cache/addressable-2.3.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/addressable-2.3.3.gem -------------------------------------------------------------------------------- /vendor/cache/em-websocket-0.3.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/em-websocket-0.3.8.gem -------------------------------------------------------------------------------- /vendor/cache/eventmachine-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/eventmachine-1.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/god-xiaomi-1.13.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/god-xiaomi-1.13.7.gem -------------------------------------------------------------------------------- /views/vendor/cache/thor-0.18.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/thor-0.18.1.gem -------------------------------------------------------------------------------- /vendor/cache/rack-protection-1.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/rack-protection-1.5.0.gem -------------------------------------------------------------------------------- /views/vendor/cache/builder-3.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/builder-3.2.0.gem -------------------------------------------------------------------------------- /views/vendor/cache/daemons-1.1.9.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/daemons-1.1.9.gem -------------------------------------------------------------------------------- /views/vendor/cache/net-ldap-0.3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/net-ldap-0.3.1.gem -------------------------------------------------------------------------------- /views/vendor/cache/rack-rpc-0.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/rack-rpc-0.0.6.gem -------------------------------------------------------------------------------- /views/vendor/cache/sinatra-1.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/sinatra-1.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/sinatra-websocket-0.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/vendor/cache/sinatra-websocket-0.3.0.gem -------------------------------------------------------------------------------- /views/vendor/cache/addressable-2.3.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/addressable-2.3.3.gem -------------------------------------------------------------------------------- /views/vendor/cache/file-tail-1.0.12.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/file-tail-1.0.12.gem -------------------------------------------------------------------------------- /views/vendor/cache/god-xiaomi-1.13.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/god-xiaomi-1.13.7.gem -------------------------------------------------------------------------------- /views/vendor/cache/em-websocket-0.3.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/em-websocket-0.3.8.gem -------------------------------------------------------------------------------- /views/vendor/cache/eventmachine-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/eventmachine-1.0.3.gem -------------------------------------------------------------------------------- /views/vendor/cache/rack-protection-1.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/rack-protection-1.5.0.gem -------------------------------------------------------------------------------- /views/vendor/cache/sinatra-websocket-0.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/vendor/cache/sinatra-websocket-0.3.0.gem -------------------------------------------------------------------------------- /static/vendor/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/static/vendor/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /conf/base.god: -------------------------------------------------------------------------------- 1 | God::Contacts::Email.defaults do |d| 2 | d.from_email = 'god@xiaomi.com' 3 | d.from_name = 'God' 4 | d.delivery_method = :sendmail 5 | end 6 | -------------------------------------------------------------------------------- /static/vendor/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/static/vendor/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /views/conf/base.god: -------------------------------------------------------------------------------- 1 | God::Contacts::Email.defaults do |d| 2 | d.from_email = 'god@xiaomi.com' 3 | d.from_name = 'God' 4 | d.delivery_method = :sendmail 5 | end 6 | -------------------------------------------------------------------------------- /views/static/vendor/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/static/vendor/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /views/notfound.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | 错误! 页面不存在. 4 |
-------------------------------------------------------------------------------- /views/error.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | 错误! <%= @error %> 4 |
5 | -------------------------------------------------------------------------------- /views/static/vendor/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomi-sa/frigga/HEAD/views/static/vendor/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | #gem 'god', :git => 'git://github.com/WilburJ/god-xiaomi.git' 2 | gem 'god-xiaomi' 3 | gem 'rack-rpc' 4 | gem 'sinatra' 5 | gem 'thin' 6 | gem 'thor' 7 | gem 'file-tail' 8 | gem 'sinatra-websocket' 9 | -------------------------------------------------------------------------------- /views/login_form.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /views/conf/frigga.yml: -------------------------------------------------------------------------------- 1 | --- 2 | port: 9001 3 | 4 | white_list: 5 | - 127.0.0.1 6 | - 10.0.0.1 7 | 8 | ldap: 9 | host: 10.0.0.1 10 | port: 389 11 | base: OU=xxx,DC=xxx,DC=net 12 | username: user 13 | password: pass 14 | -------------------------------------------------------------------------------- /views/Gemfile: -------------------------------------------------------------------------------- 1 | #gem 'god', :git => 'git://github.com/WilburJ/god-xiaomi.git' 2 | gem 'god-xiaomi' 3 | gem 'rack-rpc' 4 | gem 'sinatra' 5 | gem 'thin' 6 | gem 'thor' 7 | gem 'net-ldap' 8 | gem 'file-tail' 9 | gem 'sinatra-websocket' 10 | -------------------------------------------------------------------------------- /lib/frigga/rpc/server.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | module RPC 3 | module Server 4 | RPC_LIST = %w(ver) 5 | def ver 6 | VER 7 | end 8 | 9 | end # module Server finish 10 | end # module RPC finish 11 | end # module Frigga finish 12 | -------------------------------------------------------------------------------- /lib/frigga/log.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | require 'logger' 3 | require 'singleton' 4 | class Log 5 | include Singleton 6 | attr_accessor :logger 7 | def initialize 8 | @logger = Logger.new('log/frigga.log', 'daily', 10) 9 | @logger.formatter = proc { |severity, datetime, progname, msg| "#{datetime} #{severity} -- : #{msg}\n"} 10 | @logger.datetime_format = "%Y-%m-%d %H:%M:%S" 11 | @logger.level = eval("Logger::#{LOG_LEVEL.upcase}") 12 | end 13 | end 14 | Logger = Log.instance.logger 15 | 16 | end -------------------------------------------------------------------------------- /views/lib/frigga/log.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | require 'logger' 3 | require 'singleton' 4 | class Log 5 | include Singleton 6 | attr_accessor :logger 7 | def initialize 8 | @logger = Logger.new('log/frigga.log', 'daily', 10) 9 | @logger.formatter = proc { |severity, datetime, progname, msg| "#{datetime} #{severity} -- : #{msg}\n"} 10 | @logger.datetime_format = "%Y-%m-%d %H:%M:%S" 11 | @logger.level = eval("Logger::#{LOG_LEVEL.upcase}") 12 | end 13 | end 14 | Logger = Log.instance.logger 15 | 16 | end -------------------------------------------------------------------------------- /lib/frigga/auth.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | module Auth 3 | require 'open-uri' 4 | def authorized? 5 | @auth ||= Rack::Auth::Basic::Request.new(request.env) 6 | @auth.provided? && @auth.basic? && @auth.credentials && @auth.credentials == [Http_auth_user, Http_auth_passwd] 7 | end 8 | 9 | def protected! 10 | unless authorized? 11 | response['WWW-Authenticate'] = %(Basic realm="Restricted Area") 12 | throw(:halt, [401, "Oops... we need your login name & password\n"]) 13 | end 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /conf/frigga.god: -------------------------------------------------------------------------------- 1 | frigga_path = File.expand_path("./bin") 2 | God.watch do |w| 3 | w.name = "frigga" 4 | w.start = "ruby #{frigga_path}/frigga.rb" 5 | w.keepalive(:memory_max => 100.megabytes, :cpu_max => 50.percent) 6 | w.behavior(:clean_pid_file) 7 | 8 | w.lifecycle do |on| 9 | on.condition(:flapping) do |c| 10 | c.to_state = [:start, :restart] 11 | c.times = 3 12 | c.within = 3.minute 13 | c.transition = :unmonitored 14 | c.retry_in = 10.minutes 15 | c.retry_times = 2 16 | c.retry_within = 1.hours 17 | end 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /views/conf/frigga.god: -------------------------------------------------------------------------------- 1 | frigga_path = File.expand_path("./bin") 2 | God.watch do |w| 3 | w.name = "frigga" 4 | w.start = "ruby #{frigga_path}/frigga.rb" 5 | w.keepalive(:memory_max => 100.megabytes, :cpu_max => 50.percent) 6 | w.behavior(:clean_pid_file) 7 | 8 | w.lifecycle do |on| 9 | on.condition(:flapping) do |c| 10 | c.to_state = [:start, :restart] 11 | c.times = 3 12 | c.within = 3.minute 13 | c.transition = :unmonitored 14 | c.retry_in = 10.minutes 15 | c.retry_times = 2 16 | c.retry_within = 1.hours 17 | end 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/frigga/rpc/god.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | module RPC 3 | module God 4 | #must have RPC_LIST for regsiter rpc_call 5 | RPC_LIST = %w(status restart start stop) 6 | def status 7 | hi = Frigga::Talk.god('status') 8 | hi.map {|k, v| [k, v[:status], v[:start_time], v[:pid], v[:start]] } 9 | end 10 | def restart(str) 11 | Frigga::Talk.god('restart', str) 12 | end 13 | 14 | def start(str) 15 | Frigga::Talk.god('start', str) 16 | end 17 | 18 | def stop(str) 19 | Frigga::Talk.god('stop', str) 20 | end 21 | 22 | end #God 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /views/lib/frigga/rpc/god.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | module RPC 3 | module God 4 | #must have RPC_LIST for regsiter rpc_call 5 | RPC_LIST = %w(status restart start stop) 6 | def status 7 | hi = Frigga::Talk.god('status') 8 | hi.map {|k, v| [k, v[:status], v[:start_time], v[:pid], v[:start]] } 9 | end 10 | def restart(str) 11 | Frigga::Talk.god('restart', str) 12 | end 13 | 14 | def start(str) 15 | Frigga::Talk.god('start', str) 16 | end 17 | 18 | def stop(str) 19 | Frigga::Talk.god('stop', str) 20 | end 21 | 22 | end #God 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /views/log/frigga.log.20130502: -------------------------------------------------------------------------------- 1 | # Logfile created on 2013-05-02 23:56:06 +0800 by logger.rb/31641 2 | 2013-05-02 23:56:06 +0800 INFO -- : [192.168.1.106] / 3 | 2013-05-02 23:56:06 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 4 | 2013-05-02 23:56:07 +0800 INFO -- : [192.168.1.106] /favicon.ico 5 | 2013-05-02 23:56:07 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 6 | 2013-05-02 23:56:13 +0800 INFO -- : [192.168.1.106] /ctmgr 7 | 2013-05-02 23:56:13 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 8 | 2013-05-02 23:56:13 +0800 INFO -- : [192.168.1.106] /favicon.ico 9 | 2013-05-02 23:56:13 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 10 | -------------------------------------------------------------------------------- /views/about.erb: -------------------------------------------------------------------------------- 1 |

Frigga 版本: <%= @ver %>

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 15 | 18 | <% else %> 19 | <%= @process['frigga'][:status] %> 20 | <% end %> 21 | 22 | 23 | 24 | 25 |
ProcessStatusStart Time
13 | Frigga 14 | 16 | <% if @process['frigga'][:status] == 'running' %> 17 | <%= @process['frigga'][:status] %><%= @process['frigga'][:start_time] %>
26 | -------------------------------------------------------------------------------- /views/conf/passport-agent.god.sample: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = "passport-agent" 3 | w.start = "/home/work/opdir/passport/release/run.sh" 4 | w.log = "/home/work/opdir/passport/release/run.log" 5 | w.dir = "/home/work/opdir/passport/release" 6 | w.env = {"PATH"=>"/home/work/jdk/bin/:/bin:/usr/bin:/sbin", "JAVA_HOME"=>"/home/work/jdk", "CLASSPATH"=>".:/home/work/jdk/lib/tools.jar:/home/work/jdk/lib/rt.jar"} 7 | w.keepalive( 8 | 9 | 10 | ) 11 | w.behavior(:clean_pid_file) 12 | w.stop_timeout = 60.seconds 13 | w.process_log = "/home/work/opdir/passport.yaml" 14 | 15 | w.lifecycle do |on| 16 | on.condition(:flapping) do |c| 17 | c.to_state = [:start, :restart] 18 | c.times = 3 19 | c.within = 10.minute 20 | c.transition = :unmonitored 21 | c.retry_in = 20.minutes 22 | c.retry_times = 2 23 | c.retry_within = 1.hours 24 | c.notify = 'proc_down' 25 | end 26 | end 27 | 28 | end 29 | 30 | God.contact(:email) do |c| 31 | c.name = 'proc_down' 32 | c.group = 'developers' 33 | 34 | c.to_email = "fangshaosen@xiaomi.com" 35 | 36 | end 37 | 38 | -------------------------------------------------------------------------------- /views/README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | Frigga是一个针对程序部署以及监控的自动化工具 4 | 5 | 在北欧神话中,frigga是神后,odin的妻子;掌管婚姻和家庭;负责纺织云彩 6 | 7 | frigga是部署的client端,相配合的是odin,部署的控制端。在一次部署任务中frigga负责调用thor进行部署 8 | 在部署功能中,frigga的上游系统是odin,下游系统是thor 9 | 10 | ## 功能: 11 | 12 | - 作为client端发起一次程序部署 13 | - 提供xmlrpc接口来进行部署任务的查询 14 | - 集成了god,用来作为程序的supervise 15 | - 支持web化的god 16 | - 支持日志 17 | - 支持添加自定义的xmlrpc接口 18 | - 支持自升级 19 | 20 | 21 | ## 环境依赖 22 | 23 | - Ruby 1.9.3 24 | 25 | ## 安装 26 | 27 | ``` 28 | git clone 29 | ``` 30 | 31 | ## 使用 32 | 33 | ### 基本用法 34 | 35 | 36 | `启动frigga god 以及需要启动的supervise程序` 37 | 38 | 39 | ``` 40 | cd script/ && ./run.rb start 41 | ``` 42 | 43 | 44 | `用god管理进程` 45 | 46 | 47 | ``` 48 | god start/stop/restart frigga 49 | ``` 50 | 51 | 52 | ### 使用god管理进程 53 | 54 | `添加god配置文件到 conf/ 目录下, 配置文件以.god作为后缀` 55 | 56 | 57 | ### 为frigga添加白名单 58 | 59 | `在conf/下添加ip.yml文件,将需要信任的机器加入` 60 | 61 | 62 | ``` 63 | --- 64 | - 10.237.37.23 65 | - 10.237.37.24 66 | ``` 67 | 68 | ### 高级功能 69 | 70 | 71 | `添加自定义xmlrpc接口` 72 | 73 | 74 | ## Halp! 75 | 联系 xiedanbo <xiedanbo@xiaomi.com> 76 | -------------------------------------------------------------------------------- /views/bin/frigga.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #ecoding: utf-8 3 | $: << "../lib" << "./lib" 4 | 5 | require "pathname" 6 | require 'yaml' 7 | 8 | Dir.chdir Pathname.new(__FILE__).realpath + "../.." 9 | 10 | DIR = File.expand_path("") 11 | 12 | def load_yml(yml_path) 13 | yaml_context = nil 14 | begin 15 | yaml_context = YAML::load_file(yml_path) if File.exist?(yml_path) 16 | rescue Exception => e 17 | abort "Read yaml file[#{yml_path}] error! -> #{e}" 18 | end 19 | yaml_context 20 | end 21 | 22 | #log white_ip from ip.yml 23 | if File.exist?("conf/ip.yml") 24 | white_ip = load_yml("conf/ip.yml") 25 | abort "ip.yml must be array!" unless white_ip.kind_of?(Array) 26 | end 27 | #load main config 28 | conf = load_yml("conf/frigga.yml") 29 | #http-server port 30 | Http_port = conf.fetch "port", 9001 31 | #http-server ip white_list 32 | White_list = (conf.fetch "white_list", []) + (white_ip.nil? ? [] : white_ip) 33 | #ldap config 34 | Ldap = conf.fetch "ldap", {} 35 | 36 | 37 | #god's sock 38 | GOD_SOCK = "/tmp/god.17165.sock" 39 | #log level: debug > info > warn > fatal 40 | LOG_LEVEL = 'info' 41 | VER = '0.0.5' 42 | 43 | require 'frigga' 44 | #ok, let's go 45 | Frigga::WebServer.run! 46 | -------------------------------------------------------------------------------- /views/lib/frigga/rpc/ctmgr.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | 3 | module RPC 4 | 5 | require "digest/md5" 6 | require "fileutils" 7 | require "pathname" 8 | 9 | BASE_PATH = Pathname.new(File.dirname(__FILE__)).realpath.to_s.split("/")[0..-4].join("/") + "/deploy/" 10 | CT_USERS = ["root", "work"] 11 | 12 | module Ctmgr 13 | 14 | RPC_LIST = %w( getCtstatus ) 15 | 16 | def getCtstatus 17 | ct_all = [] 18 | 19 | CT_USERS.each do |u| 20 | ct_detail = `crontab -l -u #{u}`.split("\n") 21 | counter = 1 22 | if ct_detail.empty? or ct_detail =~ /^no crontab for/ 23 | ct_all << ["user : #{u}", "no crontab for #{u}"] 24 | else 25 | ct_array = ["user : #{u}"] 26 | ct_detail.each do |i| 27 | if i =~ /^#/ 28 | ct_array << i 29 | else 30 | ct_array << [counter, i] 31 | counter += 1 32 | end 33 | end 34 | ct_all << ct_array 35 | end 36 | end 37 | return ct_all 38 | end 39 | 40 | 41 | end # end of Ctmgr module 42 | end # end of RPC module 43 | end # end of Frigga module 44 | -------------------------------------------------------------------------------- /bin/frigga.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #ecoding: utf-8 3 | $: << "../lib" << "./lib" 4 | 5 | require "pathname" 6 | require 'yaml' 7 | 8 | Dir.chdir Pathname.new(__FILE__).realpath + "../.." 9 | 10 | DIR = File.expand_path("") 11 | 12 | def load_yml(yml_path) 13 | yaml_context = nil 14 | begin 15 | yaml_context = YAML::load_file(yml_path) if File.exist?(yml_path) 16 | rescue Exception => e 17 | abort "Read yaml file[#{yml_path}] error! -> #{e}" 18 | end 19 | yaml_context 20 | end 21 | 22 | #log white_ip from ip.yml 23 | if File.exist?("conf/ip.yml") 24 | white_ip = load_yml("conf/ip.yml") 25 | abort "ip.yml must be array!" unless white_ip.kind_of?(Array) 26 | end 27 | #load main config 28 | conf = load_yml("conf/frigga.yml") 29 | #http-server basic auth 30 | Http_auth_user, Http_auth_passwd = conf.fetch "http_auth", nil 31 | #http-server port 32 | Http_port = conf.fetch "port", 9001 33 | #http-server ip white_list 34 | White_list = (white_ip.nil? ? [] : white_ip) 35 | #ldap config 36 | Ldap = conf.fetch "ldap", {} 37 | 38 | 39 | #god's sock 40 | GOD_SOCK = "/tmp/god.17165.sock" 41 | #log level: debug > info > warn > fatal 42 | LOG_LEVEL = 'info' 43 | VER = '1.0.0' 44 | 45 | require 'frigga' 46 | #ok, let's go 47 | Frigga::WebServer.run! 48 | -------------------------------------------------------------------------------- /views/tail.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 注意: 点击stop按钮停止tail log,继续tail log请刷新本页面 5 |
6 |
 7 | 
8 |
9 |
10 | 11 | 12 |
13 | 14 | 42 | -------------------------------------------------------------------------------- /views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Frigga 进程管理 <%= @ver %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 40 | 41 | 42 |
43 | <%unless @msg.nil? %> 44 | 45 |
<%= @msg %>
46 | <% end %> 47 | 48 |
49 |
50 | <%= yield %> 51 |
52 | 53 | -------------------------------------------------------------------------------- /lib/frigga/rpc.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | module RPC 3 | require 'rack/rpc' 4 | require 'sinatra/base' 5 | Dir.glob(File.join(File.dirname(__FILE__), 'rpc', "*.rb")) do |file| 6 | require file 7 | end 8 | class Runner < Rack::RPC::Server 9 | #include rpc/*.rb and regsiter rpc call 10 | #eg. rpc/god.rb god.hello 11 | @@rpc_list = [] 12 | Dir.glob(File.join(File.dirname(__FILE__), 'rpc', "*.rb")) do |file| 13 | rpc_class = File.basename(file).split('.rb')[0].capitalize 14 | rpc_list = [] 15 | eval "include Frigga::RPC::#{rpc_class}" 16 | eval "rpc_list = Frigga::RPC::#{rpc_class}::RPC_LIST" 17 | rpc_list.each do |rpc_name| 18 | eval "alias :old_#{rpc_class.downcase}_#{rpc_name} :#{rpc_name}" 19 | define_method "#{rpc_class.downcase}_#{rpc_name}".to_sym do |*arg| 20 | Logger.info "[#{request.ip}] called #{rpc_class.downcase}.#{rpc_name} #{arg.join(', ')}" 21 | eval "old_#{rpc_class.downcase}_#{rpc_name} *arg" 22 | end 23 | rpc "#{rpc_class.downcase}.#{rpc_name}" => "#{rpc_class.downcase}_#{rpc_name}".to_sym 24 | @@rpc_list << "#{rpc_class.downcase}.#{rpc_name}" 25 | end 26 | end 27 | 28 | def help 29 | rpc_methods = (['help'] + @@rpc_list.sort).join("\n") 30 | end 31 | rpc "help" => :help 32 | 33 | before_filter :check_auth 34 | 35 | def check_auth 36 | unless White_list.include?(request.ip) 37 | Logger.info "[#{request.ip}] Not authorized" 38 | raise "Not authorized" 39 | end 40 | end 41 | 42 | end 43 | end #RPC 44 | end 45 | -------------------------------------------------------------------------------- /views/lib/frigga/rpc.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | module RPC 3 | require 'rack/rpc' 4 | require 'sinatra/base' 5 | Dir.glob(File.join(File.dirname(__FILE__), 'rpc', "*.rb")) do |file| 6 | require file 7 | end 8 | class Runner < Rack::RPC::Server 9 | #include rpc/*.rb and regsiter rpc call 10 | #eg. rpc/god.rb god.hello 11 | @@rpc_list = [] 12 | Dir.glob(File.join(File.dirname(__FILE__), 'rpc', "*.rb")) do |file| 13 | rpc_class = File.basename(file).split('.rb')[0].capitalize 14 | rpc_list = [] 15 | eval "include Frigga::RPC::#{rpc_class}" 16 | eval "rpc_list = Frigga::RPC::#{rpc_class}::RPC_LIST" 17 | rpc_list.each do |rpc_name| 18 | eval "alias :old_#{rpc_class.downcase}_#{rpc_name} :#{rpc_name}" 19 | define_method "#{rpc_class.downcase}_#{rpc_name}".to_sym do |*arg| 20 | Logger.info "[#{request.ip}] called #{rpc_class.downcase}.#{rpc_name} #{arg.join(', ')}" 21 | eval "old_#{rpc_class.downcase}_#{rpc_name} *arg" 22 | end 23 | rpc "#{rpc_class.downcase}.#{rpc_name}" => "#{rpc_class.downcase}_#{rpc_name}".to_sym 24 | @@rpc_list << "#{rpc_class.downcase}.#{rpc_name}" 25 | end 26 | end 27 | 28 | def help 29 | rpc_methods = (['help'] + @@rpc_list.sort).join("\n") 30 | end 31 | rpc "help" => :help 32 | 33 | before_filter :check_auth 34 | 35 | def check_auth 36 | unless White_list.include?(request.ip) 37 | Logger.info "[#{request.ip}] Not authorized" 38 | raise "Not authorized" 39 | end 40 | end 41 | 42 | end 43 | end #RPC 44 | end 45 | -------------------------------------------------------------------------------- /views/bin/testrpc.rb: -------------------------------------------------------------------------------- 1 | $: << 'lib' << '../lib' 2 | require "rubygems" 3 | require "xmlrpc/client" 4 | require "pp" 5 | 6 | #server = XMLRPC::Client.new2("http://foo:bar@0.0.0.0:9001/rpc") 7 | server = XMLRPC::Client.new2("http://127.0.0.1:9001/rpc") 8 | #result = server.call('system.listMethods') 9 | #result = server.call('supervisor.getAllProcessInfo') 10 | #result = server.call('deployment.getAllStatus') 11 | #result = server.call('help') 12 | puts server.call('help') 13 | puts "-----------------------" 14 | loop do 15 | begin 16 | x = gets.strip.split(/\s/) 17 | next if x.empty? 18 | result = server.call(*x) 19 | pp result 20 | puts "-----------------------" 21 | rescue => e 22 | puts e.message 23 | puts e.backtrace.join("\n") 24 | next 25 | end 26 | end 27 | #result = server.call('help') 28 | #puts result 29 | #result = server.call('god.status') 30 | #result = server.call('deploy.startDeploy', '8081', 'first_deploy-1.0.0.0.tar.gz') 31 | #puts result 32 | 33 | __END__ 34 | options = { 35 | :backtrace => true, 36 | :dir_mode => :script, 37 | :dir => 'pids', 38 | :monitor => true 39 | } 40 | 41 | current_dir = Dir.pwd 42 | 43 | Daemons.run_proc('tower', options) do 44 | loop do 45 | Dir.chdir(current_dir) 46 | logger = Log.instance 47 | EM.run do 48 | # hit Control + C to stop 49 | # Signal.trap("INT") { EM.stop } 50 | # Signal.trap("TERM") { EM.stop } 51 | 52 | #load conf file 53 | $conf = load_config('../conf/dm-server.yaml') 54 | EM.stop if $conf.nil? 55 | 56 | $logger.info "start dm-server..." 57 | EM.start_server($conf['ip'], $conf['port'], DomainReportServer) 58 | $DB = Sequel.connect($conf['db'], :max_connections => 10) 59 | end 60 | sleep 3 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /views/lib/frigga/auth.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | module Auth 3 | require 'net/ldap' 4 | require 'open-uri' 5 | def authorized? 6 | @auth ||= Rack::Auth::Basic::Request.new(request.env) 7 | @auth.provided? && @auth.basic? && @auth.credentials && check_ldap 8 | end 9 | 10 | def protected! 11 | unless authorized? 12 | response['WWW-Authenticate'] = %(Basic realm="Restricted Area") 13 | throw(:halt, [401, "Oops... we need your login name & password\n"]) 14 | end 15 | end 16 | 17 | def check_ldap 18 | ldap = Net::LDAP.new({ :host => Ldap['host'], 19 | :port => Ldap['port'], 20 | :base => Ldap['base'], 21 | :auth => { :method => :simple, 22 | :username => Ldap['username'], 23 | :password => Ldap['password'] 24 | } 25 | }) 26 | 27 | filter = Net::LDAP::Filter.eq("mail", @auth.credentials[0]) 28 | ldap_entry = nil 29 | ldap.search(:filter => filter) {|entry| ldap_entry = entry} 30 | return false if ldap_entry.nil? 31 | ldap.auth(ldap_entry.dn, @auth.credentials[1]) 32 | if ldap.bind 33 | door_god = get_doorgod_list(@auth.credentials[0].split('@')[0]) 34 | ip_list = `ip -f inet addr | grep global | awk '{print $2}' | awk -F/ '{print $1}'`.split("\n").select {|f| f =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d+{1,3}/} 35 | return ip_list.map {|f| door_god.scan("(#{f})").empty? ? false : true}.any? 36 | else 37 | return false 38 | end 39 | end #check_ldap 40 | 41 | def get_doorgod_list(user) 42 | host_list = open "http://krb1.xiaomi.net/getmyinfo.php?user=#{user}" do |f| 43 | f.read 44 | end 45 | end 46 | 47 | end 48 | end -------------------------------------------------------------------------------- /script/run.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #ecoding: utf-8 3 | require "pathname" 4 | 5 | Dir.chdir Pathname.new(__FILE__).realpath + "../.." 6 | 7 | # update gem 8 | 9 | gem_lock =Pathname.new(File.dirname(__FILE__)).realpath.to_s.split("/")[0..-2].join("/") + "/Gemfile.lock" 10 | unless File.exist?(gem_lock) 11 | `bundle update --local` 12 | abort "Install gem failed!" if $? != 0 13 | end 14 | 15 | DIR = File.expand_path("") 16 | abort "God does not exist..." unless system("which god > /dev/null") 17 | 18 | require "thor" 19 | class Cli < Thor 20 | desc "start", "Start God, Frigga and #{DIR}/gods/*.god" 21 | def start 22 | #wake up god 23 | wakeup_god = %W(god --no-events --log-level info -c #{DIR}/conf/base.god) 24 | abort "Start God failed!" unless system *wakeup_god 25 | 26 | #use god to load frigga.god for wakeing frigga up 27 | wakeup_frigga = "god load conf/frigga.god" 28 | abort "Start Frigga failed!" unless system *wakeup_frigga 29 | 30 | #start process 31 | Dir.glob(File.join(Dir.pwd, 'gods', "*.god")) do |god| 32 | start_process = "god load #{god}" 33 | `#{start_process}` 34 | warn "Start process[#{god}] failed!" unless $? == 0 35 | end 36 | 37 | #check process status 38 | puts "Command: god status" 39 | system("god status") 40 | end 41 | 42 | desc "stop", "Stop God and Frigga, Don't stop *.god" 43 | def stop 44 | #stop frigg 45 | stop_frigg = "god stop frigga" 46 | `#{stop_frigg}` 47 | warn "Stop Frigga failed!" unless $? == 0 48 | 49 | #stop god 50 | stop_god = %W(god quit) 51 | abort "Stop God failed!" unless system *stop_god 52 | end 53 | 54 | desc "nuke", "Stop God,Frigga and *.god" 55 | def nuke 56 | #terminate god 57 | nuke_god = %W(god terminate) 58 | abort "Nuke all gods failed!" unless system *nuke_god 59 | end 60 | 61 | desc "stop_god", "Stop God only, update god gem." 62 | def stop_god 63 | # update god 64 | stop_god = %W(god quit) 65 | abort "Stop God failed!" unless system *stop_god 66 | end 67 | 68 | end 69 | 70 | Cli.start(ARGV) 71 | 72 | -------------------------------------------------------------------------------- /lib/frigga/talk.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | require "drb" 3 | class Talk 4 | def self.god(command, task = "") 5 | new.action(command, task) 6 | end 7 | 8 | def initialize 9 | @server = DRbObject.new(nil, "drbunix://#{GOD_SOCK}") 10 | end 11 | 12 | def ping 13 | begin 14 | @server.ping 15 | rescue DRb::DRbConnError 16 | raise "God server is not available, #{GOD_SOCK}" 17 | end 18 | end 19 | 20 | def action(command, task = "") 21 | if %w{status}.include?(command) 22 | ping 23 | status 24 | elsif %w{start stop restart monitor unmonitor remove}.include?(command) 25 | ping 26 | @server.control(task, command) 27 | else 28 | raise "Command '#{command}' is not valid." 29 | end 30 | end 31 | 32 | def status 33 | hi = @server.status 34 | process = {} 35 | unless hi.nil? || hi.empty? 36 | hi.each do |k, v| 37 | 38 | start_time = "" 39 | status = v[:state] == :up ? 'running' : (v[:state] == :unmonitored ? 'stop' : 'something wrong!') 40 | pid = v.fetch :pid, nil 41 | if v[:state] == :up 42 | if ! pid.nil? && File.exist?("/proc/#{pid}") 43 | jiffies = IO.read("/proc//#{pid}/stat").split(/\s/)[21].to_i 44 | uptime = IO.readlines("/proc/stat").find {|t| t =~ /^btime/ }.split(/\s/)[1].strip.to_i 45 | start_time = Time.at(uptime + jiffies / 100).strftime("%Y-%m-%d %H:%M:%S") 46 | else 47 | status = 'flapping' 48 | start_time = "Can't find pid:#{pid}" 49 | end 50 | 51 | end 52 | process[k] = {:status => status, 53 | :start_time => start_time, 54 | :start => v[:start], 55 | :pid => pid, 56 | :http_url => v[:http_url], 57 | :all_log => { :process_log => v[:process_log], 58 | :log => v[:log], 59 | :err_log => v[:err_log] 60 | } 61 | } 62 | end 63 | end 64 | process 65 | end 66 | end #talk 67 | 68 | end 69 | -------------------------------------------------------------------------------- /views/lib/frigga/talk.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | require "drb" 3 | #require 'singleton' 4 | class Talk 5 | #include Singleton 6 | def self.god(command, task = "") 7 | new.action(command, task) 8 | end 9 | 10 | def initialize 11 | @server = DRbObject.new(nil, "drbunix://#{GOD_SOCK}") 12 | #ping 13 | end 14 | 15 | def ping 16 | begin 17 | @server.ping 18 | rescue DRb::DRbConnError 19 | raise "God server is not available, #{GOD_SOCK}" 20 | end 21 | end 22 | 23 | def action(command, task = "") 24 | if %w{status}.include?(command) 25 | ping 26 | status 27 | elsif %w{start stop restart monitor unmonitor remove}.include?(command) 28 | ping 29 | @server.control(task, command) 30 | else 31 | raise "Command '#{command}' is not valid." 32 | end 33 | end 34 | 35 | def status 36 | hi = @server.status 37 | process = {} 38 | unless hi.nil? || hi.empty? 39 | hi.each do |k, v| 40 | 41 | start_time = "" 42 | status = v[:state] == :up ? 'running' : (v[:state] == :unmonitored ? 'stop' : 'something wrong!') 43 | pid = v.fetch :pid, nil 44 | if v[:state] == :up 45 | if ! pid.nil? && File.exist?("/proc/#{pid}") 46 | jiffies = IO.read("/proc//#{pid}/stat").split(/\s/)[21].to_i 47 | uptime = IO.readlines("/proc/stat").find {|t| t =~ /^btime/ }.split(/\s/)[1].strip.to_i 48 | start_time = Time.at(uptime + jiffies / 100).strftime("%Y-%m-%d %H:%M:%S") 49 | else 50 | status = 'flapping' 51 | start_time = "Can't find pid:#{pid}" 52 | end 53 | 54 | end 55 | process[k] = {:status => status, 56 | :start_time => start_time, 57 | :start => v[:start], 58 | :pid => pid, 59 | :http_url => v[:http_url], 60 | :all_log => { :process_log => v[:process_log], 61 | :log => v[:log], 62 | :err_log => v[:err_log] 63 | } 64 | } 65 | end 66 | end 67 | process 68 | end 69 | end #talk 70 | #Talk_to = Frigga::Talk.instance 71 | 72 | end 73 | -------------------------------------------------------------------------------- /views/lib/frigga/rpc/server.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | module RPC 3 | module Server 4 | RPC_LIST = %w(ver update) 5 | def ver 6 | VER 7 | end 8 | 9 | def update 10 | restart_flag = false 11 | if system("which git > /dev/null") && File.directory?(".git") 12 | clean = `git status --porcelain`.empty? 13 | current_branch = `git symbolic-ref HEAD`.chomp 14 | master = current_branch == "refs/heads/master" 15 | no_new_commits = system('git diff --exit-code --quiet origin/master master') 16 | 17 | short_branch = current_branch.split('/').last 18 | 19 | if !master 20 | return ["fail", "Frigga on a non-master branch '#{short_branch}', won't auto-update!"] 21 | elsif !no_new_commits 22 | return ["fail", "Frigga has unpushed commits on master, won't auto-update!"] 23 | elsif !clean 24 | return ["fail", "Frigga has a dirty tree, won't auto-update!"] 25 | end 26 | 27 | if clean && master && no_new_commits 28 | quietly = "> /dev/null 2>&1" 29 | fetch = "(git fetch origin #{quietly})" 30 | reset = "(git reset --hard origin/master #{quietly})" 31 | reclean = "(git clean -df #{quietly})" 32 | 33 | if system "#{fetch}" 34 | origin_log = `git log origin/master -1 --oneline` 35 | master_log = `git log master -1 --oneline` 36 | restart_flag = (origin_log == master_log ? false : true) 37 | else 38 | return ["fail", "Git fetch failed."] 39 | end 40 | 41 | unless system "#{reset} && #{reclean}" 42 | return ["fail", "Auto-update of Frigga Failed!"] 43 | end 44 | 45 | if restart_flag 46 | Thread.new { sleep 1; exit} 47 | return ["succ", "Auto-update of Frigga Succeed, ready to restart"] 48 | else 49 | return ["succ", "Frigga is already up to date, Don't need restart."] 50 | end 51 | end 52 | 53 | else 54 | return ["fail", "Don't have git and .git, won't auto-update!"] 55 | end 56 | 57 | end # update 58 | 59 | def update_god 60 | Dir.chdir Pathname.new(__FILE__).realpath + "../../../../script" 61 | `./run.rb stop_god` 62 | sleep(1) 63 | `./run.rb start` 64 | end 65 | 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /views/index.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | <% if @process.empty? %> 12 | 13 | 14 | 15 | 16 | 17 | 18 | <% end %> 19 | <% @process.each do |k, v| %> 20 | <% next if k =~ /Frigga/i %> 21 | 22 | 25 | 28 | <% else %> 29 | <%= v[:status] %> 30 | <% end %> 31 | 32 | 71 | 72 | <% end %> 73 | 74 |
ProcessStatusStart TimeAction
N/A
23 | data-toggle="tooltip" data-placement="bottom" title="" data-original-title="<%= v[:start] %>"><%= k %> 24 | 26 | <% if v[:status] == 'running' %> 27 | <%= v[:status] %><%= v[:start_time] %> 33 | <% if v[:status] == 'stop' %> 34 |
35 | 36 | 37 |
38 | <% else %> 39 |
40 | 41 | 42 |
43 | <% end %> 44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 | Log 53 | 54 | 55 | 68 |
69 | 70 |
75 | 76 | 83 | -------------------------------------------------------------------------------- /views/ctmgr.erb: -------------------------------------------------------------------------------- 1 | 2 | @ctstatus 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | <% if @process.empty? %> 13 | 14 | 15 | 16 | 17 | 18 | 19 | <% end %> 20 | <% @process.each do |k, v| %> 21 | <% next if k =~ /Frigga/i %> 22 | 23 | 26 | 29 | <% else %> 30 | <%= v[:status] %> 31 | <% end %> 32 | 33 | 72 | 73 | <% end %> 74 | 75 |
ProcessStatusStart TimeAction
N/A
24 | data-toggle="tooltip" data-placement="bottom" title="" data-original-title="<%= v[:start] %>"><%= k %> 25 | 27 | <% if v[:status] == 'running' %> 28 | <%= v[:status] %><%= v[:start_time] %> 34 | <% if v[:status] == 'stop' %> 35 |
36 | 37 | 38 |
39 | <% else %> 40 |
41 | 42 | 43 |
44 | <% end %> 45 | 46 |
47 | 48 | 49 |
50 | 51 |
52 | 53 | Log 54 | 55 | 56 | 69 |
70 | 71 |
76 | 77 | 84 | -------------------------------------------------------------------------------- /views/script/run.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #ecoding: utf-8 3 | require "pathname" 4 | 5 | Dir.chdir Pathname.new(__FILE__).realpath + "../.." 6 | 7 | # Auto-update code 8 | if system("which git > /dev/null") && File.directory?(".git") 9 | clean = `git status --porcelain`.empty? 10 | current_branch = `git symbolic-ref HEAD`.chomp 11 | master = current_branch == "refs/heads/master" 12 | no_new_commits = system('git diff --exit-code --quiet origin/master master') 13 | 14 | short_branch = current_branch.split('/').last 15 | 16 | if !master 17 | warn "Frigga on a non-master branch '#{short_branch}', won't auto-update!" 18 | elsif !no_new_commits 19 | warn "Frigga has unpushed commits on master, won't auto-update!" 20 | elsif !clean 21 | warn "Frigga has a dirty tree, won't auto-update!" 22 | end 23 | 24 | if clean && master && no_new_commits 25 | quietly = "> /dev/null 2>&1" 26 | fetch = "(git fetch origin #{quietly})" 27 | reset = "(git reset --hard origin/master #{quietly})" 28 | reclean = "(git clean -df #{quietly})" 29 | 30 | unless system "#{fetch} && #{reset} && #{reclean}" 31 | warn "Auto-update of Frigga FAILED, continuing." 32 | end 33 | end 34 | else 35 | warn "Don't have git or .git, won't auto-update!" 36 | end 37 | 38 | # update gem 39 | 40 | gem_lock =Pathname.new(File.dirname(__FILE__)).realpath.to_s.split("/")[0..-2].join("/") + "/Gemfile.lock" 41 | `rm -rf #{gem_lock}` 42 | `bundle update --local` 43 | abort "Install gem failed!" if $? != 0 44 | 45 | DIR = File.expand_path("") 46 | abort "God does not exist..." unless system("which god > /dev/null") 47 | 48 | require "thor" 49 | class Cli < Thor 50 | desc "start", "Start God, Frigga and #{DIR}/gods/*.god" 51 | def start 52 | #wake up god 53 | wakeup_god = %W(god --no-events --log-level info -c #{DIR}/conf/base.god) 54 | abort "Start God failed!" unless system *wakeup_god 55 | 56 | #use god to load frigga.god for wakeing frigga up 57 | wakeup_frigga = "god load conf/frigga.god" 58 | abort "Start Frigga failed!" unless system *wakeup_frigga 59 | 60 | #start process 61 | Dir.glob(File.join(Dir.pwd, 'gods', "*.god")) do |god| 62 | start_process = "god load #{god}" 63 | `#{start_process}` 64 | warn "Start process[#{god}] failed!" unless $? == 0 65 | end 66 | 67 | #check process status 68 | puts "Command: god status" 69 | system("god status") 70 | end 71 | 72 | desc "stop", "Stop God and Frigga, Don't stop *.god" 73 | def stop 74 | #stop frigg 75 | stop_frigg = "god stop frigga" 76 | `#{stop_frigg}` 77 | warn "Stop Frigga failed!" unless $? == 0 78 | 79 | #stop god 80 | stop_god = %W(god quit) 81 | abort "Stop God failed!" unless system *stop_god 82 | end 83 | 84 | desc "nuke", "Stop God,Frigga and *.god" 85 | def nuke 86 | #terminate god 87 | nuke_god = %W(god terminate) 88 | abort "Nuke all gods failed!" unless system *nuke_god 89 | end 90 | 91 | desc "stop_god", "Stop God only, update god gem." 92 | def stop_god 93 | # update god 94 | stop_god = %W(god quit) 95 | abort "Stop God failed!" unless system *stop_god 96 | end 97 | 98 | end 99 | 100 | Cli.start(ARGV) 101 | 102 | -------------------------------------------------------------------------------- /lib/frigga.rb: -------------------------------------------------------------------------------- 1 | $: << "./" 2 | require 'sinatra/base' 3 | require 'rack/rpc' 4 | require 'frigga/log' 5 | require 'frigga/auth' 6 | require 'frigga/talk' 7 | require 'frigga/rpc' 8 | require 'sinatra-websocket' 9 | require "file/tail" 10 | 11 | module Frigga 12 | class Tail < File 13 | include File::Tail 14 | end 15 | 16 | class WebServer < Sinatra::Base 17 | include Frigga::Auth 18 | configure do 19 | set :public_folder, Proc.new { File.join(File.expand_path(""), "static") } 20 | set :views, Proc.new { File.join(File.expand_path(""), "views") } 21 | enable :sessions 22 | set :port => Http_port 23 | set :bind => '0.0.0.0' 24 | set :show_exceptions => false 25 | end 26 | 27 | before '*' do 28 | Logger.info "[#{request.ip}] #{request.path}" 29 | unless Http_auth_user.nil? 30 | if White_list.include?(request.ip) 31 | Logger.info "[#{request.ip}] in white-list, skip Auth!" 32 | else 33 | protected! unless request.websocket? 34 | end 35 | end 36 | @ver = VER 37 | end 38 | 39 | not_found do 40 | erb :notfound 41 | end 42 | 43 | error do 44 | @error = env['sinatra.error'].message 45 | erb :error 46 | end 47 | 48 | get '/' do 49 | if session[:notice] 50 | @msg = session[:notice].dup 51 | session[:notice] = nil 52 | end 53 | @process = Frigga::Talk.god('status') 54 | erb :index 55 | end 56 | 57 | get '/about' do 58 | @process = Frigga::Talk.god('status') 59 | raise "Frigga hasn't watched by God" if @process.empty? 60 | erb :about 61 | end 62 | 63 | get '/log/*' do 64 | #check log file 65 | name, type, index = params[:splat][0].split('/') 66 | raise "url error" if name.nil? or type.nil? 67 | log = nil 68 | log_file = nil 69 | thr = nil 70 | if name == "god" 71 | if File.exist?("/var/log/messages") 72 | log_file = "/var/log/messages" 73 | elsif File.exist?("/var/log/syslog") 74 | log_file = "/var/log/syslog" 75 | end 76 | else 77 | process = Frigga::Talk.god('status') 78 | raise "Don't have name:#{name}" unless process.key?(name) 79 | if !process[name][:all_log][type.to_sym].nil? && !process[name][:all_log][type.to_sym].eql?("/dev/null") 80 | if process[name][:all_log][type.to_sym].kind_of?(Array) 81 | if !index.nil? && (0 ... process[name][:all_log][type.to_sym].size).include?(index.to_i) 82 | log_file = process[name][:all_log][type.to_sym][index.to_i] if File.exist?(process[name][:all_log][type.to_sym][index.to_i]) 83 | end 84 | else 85 | log_file = process[name][:all_log][type.to_sym] if File.exist?(process[name][:all_log][type.to_sym]) 86 | end 87 | end 88 | end 89 | raise "We can't find log file, sorry..." if log_file.nil? 90 | 91 | #start websocet 92 | if !request.websocket? 93 | erb :tail 94 | else 95 | request.websocket do |ws| 96 | ws.onopen do 97 | log = Tail.new(log_file) 98 | log.interval = 5 99 | log.backward(5) 100 | log.return_if_eof = true 101 | thr = Thread.new { 102 | loop do 103 | unless log.closed? 104 | log.tail { |line| ws.send line } 105 | end 106 | sleep 0.25 107 | end 108 | } 109 | end 110 | 111 | ws.onmessage do |msg| 112 | if msg =~ /END/i 113 | thr.kill if thr.alive? 114 | log.close unless log.closed? 115 | ws.close_connection 116 | end 117 | end 118 | 119 | ws.onclose do 120 | thr.kill if thr.alive? 121 | log.close unless log.closed? 122 | end 123 | end 124 | end 125 | end 126 | 127 | post '/god/:action' do |action| 128 | unless %w(restart start stop).include?(action) 129 | raise "Don't know action[#{action}]" 130 | end 131 | hi = Frigga::Talk.god(action, params[:name]) 132 | if hi.empty? 133 | raise "#{action.capitalize} #{params[:name]} failed! #{hi[1]}" 134 | end 135 | session[:notice] = "Action: #{action} process[#{params[:name]}] success!" 136 | sleep 0.5 137 | redirect to '/' 138 | end 139 | 140 | use Rack::RPC::Endpoint, Frigga::RPC::Runner.new 141 | end 142 | end 143 | 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | Frigga是一款使用简单、极具扩展的进程监控的框架。她基于开源的god,修改和添加了web界面和rpc接口,以满足大集群服务管理的需求。 4 | 5 | 在北欧神话中,frigga是神后,odin的妻子;掌管婚姻和家庭;负责纺织云彩 6 | 7 | ![图片](http://noops.me/wp-content/uploads/2013/05/frigga.png) 8 | 9 | ## 功能 10 | 11 | - 集成了god,用来作为程序的supervise程序 12 | - C/S结构,并且集成了多种认证方式,以支持大集群运维管理 13 | - 基本功能均提供api接口,方便扩展 14 | - 支持单机web化的god,方便查看和管理 15 | - 支持日志查看 16 | - 支持添加自定义的xmlrpc接口,方便进行二次开发 17 | 18 | 19 | ## 环境依赖 20 | 21 | - Ruby 1.9.3 22 | - bundle 23 | 24 | ## 安装 25 | 26 | ``` 27 | git clone git@github.com:xiaomi-sa/frigga.git 28 | ``` 29 | 30 | ## 使用 31 | 32 | ### 基本用法 33 | 34 | 启动frigga god 以及需要启动的supervise程序 35 | 36 | ``` 37 | cd script/ && ./run.rb start 38 | ``` 39 | - 第一次使用会使用bundle安装vendor/cache/*.gem到系统 40 | - 在run.sh中,调用`god --no-events --log-level info -c #{DIR}/conf/base.god`启动god 41 | - 在run.sh中,通过god启动的frigga `god load conf/frigga.god` 42 | 43 | 通过浏览器链接http://localhost:9901, 默认用户名: admin, 默认密码: 123,可以在web查看 44 | 45 | ### 用god管理进程 46 | - [god官方地址](http://godrb.com) 47 | - [noops.me](http://noops.me/?p=133)上有god的使用介绍和其他进程管理工具的对比 48 | 49 | 查看启动的进程: `god status` 50 | 51 | 52 | ``` 53 | god start/stop/restart process_name 54 | ``` 55 | 56 | ### 使用god管理进程 57 | 58 | `添加god配置文件到 gods/ 目录下, 配置文件以.god作为后缀`,使用script/run.sh start时,会批量加载该目录下的*.god文件 59 | 60 | ### 建议的god配置 61 | ``` ruby 62 | God.watch do |w| 63 | w.name = "agent" 64 | w.start = "/home/work/opdir/agent/release/run.sh" 65 | w.log = "/home/work/opdir/agent/release/run.log" 66 | w.process_log = "/home/work/opdir/agent/log/agent.log" 67 | w.dir = "/home/work/opdir/agent/release" 68 | w.env = {"PATH"=>"/home/work/jdk/bin/:/bin:/usr/bin:/sbin", "JAVA_HOME"=>"/home/work/jdk", "CLASSPATH"=>".:/home/work/jdk/lib/tools.jar:/home/work/jdk/lib/rt.jar"} 69 | w.keepalive 70 | w.behavior(:clean_pid_file) 71 | w.stop_timeout = 60.seconds 72 | 73 | w.lifecycle do |on| 74 | on.condition(:flapping) do |c| 75 | c.to_state = [:start, :restart] 76 | c.times = 3 77 | c.within = 10.minute 78 | c.transition = :unmonitored 79 | c.retry_in = 20.minutes 80 | c.retry_times = 2 81 | c.retry_within = 1.hours 82 | c.notify = 'proc_down' 83 | end 84 | end 85 | end 86 | 87 | God.contact(:email) do |c| 88 | c.name = 'proc_down' 89 | c.group = 'developers' 90 | 91 | c.to_email = "god@xiaomi.com" 92 | end 93 | ``` 94 | 95 | > 其中http_url, process_log为frigga支持的新参数 96 | > w.http_url = "www.xiaomi.com", 在web端点击process name,可以跳转到指定的url地址 97 | 98 | > w.process_log 支持数组或字符串配置 99 | >> w.process_log = "/home/work/xxx/log/xxx.log" 100 | 101 | >> w.process_log = ["/home/work/xxx/log/xxx.log", "/home/work/xxx/log/xxx1.log"] 102 | 103 | 配置好process_log后,可以通过god log process_name或web端查看日志, 如进程名为w.name = "frigga" 104 | 105 | `god log frigga [1/2/3...]` 106 | 107 | ### 为frigga添加白名单 108 | 109 | `在conf/下添加ip.yml文件,将需要信任的机器加入` 110 | 111 | 112 | ``` 113 | --- 114 | - 10.237.37.23 115 | - 10.237.37.24 116 | ``` 117 | 118 | ### 高级功能 119 | 120 | 121 | `添加自定义xmlrpc接口` 122 | 123 | > rpc接口的添加可以参照lib/frigga/rpc/god.rb 124 | 125 | > 假设我们要添加一个叫做call_somethg的接口,支持一个叫做do_somethg的方法,可以按照下面方法开发: 126 | >> 首先在lib/frigga/rpc/下添加一个call_somethg.rb的文件 127 | 128 | >> call_somethg.rb文件中的namespace应该遵循以下结构。需要注意的是要将需要调用的function写入到RPC_LIST数组中,作为方法的注册。 129 | 130 | ``` 131 | module Frigga 132 | module RPC 133 | module call_somethg 134 | 135 | RPC_LIST = %w(do_somethg) 136 | 137 | def do_somethg 138 | do_somethg 139 | end 140 | 141 | end 142 | end 143 | end 144 | 145 | ``` 146 | 147 | `使用rpc接口,遵循标准的xmlrpc协议.以下是一个调用demo` 148 | 149 | python的调用方法 150 | ``` python 151 | import xmlrpclib 152 | server_ip = "your_ip" 153 | server_port = "9001" 154 | uri = "http://" + server_ip + ":" + server_port + "/rpc" 155 | server = xmlrpclib.ServerProxy(uri) 156 | print server.call_somethg.do_somethg() 157 | ``` 158 | 159 | ruby的调用方法 160 | ``` ruby 161 | require "xmlrpc/client" 162 | require "pp" 163 | server = XMLRPC::Client.new2("http://127.0.0.1:9001/rpc") 164 | puts server.call('help') 165 | puts "-----------------------" 166 | loop do 167 | begin 168 | x = gets.strip.split(/\s/) 169 | next if x.empty? 170 | result = server.call(*x) 171 | pp result 172 | puts "-----------------------" 173 | rescue => e 174 | puts e.message 175 | puts e.backtrace.join("\n") 176 | next 177 | end 178 | end 179 | ``` 180 | 调用`help`方法,会列出frigga支持的所有rpc call 181 | 182 | ## 注意事项 183 | 184 | - 由于script/run.rb启动god时关闭了event,所以不能使用god event配置 185 | 186 | ## Help! 187 | 联系 xiedanbo <xiedanbo@xiaomi.com> 188 | -------------------------------------------------------------------------------- /views/log/frigga.log: -------------------------------------------------------------------------------- 1 | # Logfile created on 2013-05-03 00:00:34 +0800 by logger.rb/31641 2 | 2013-05-03 00:00:34 +0800 INFO -- : [192.168.1.106] /ctmgr 3 | 2013-05-03 00:00:34 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 4 | 2013-05-03 00:00:34 +0800 INFO -- : [192.168.1.106] /favicon.ico 5 | 2013-05-03 00:00:34 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 6 | 2013-05-03 00:00:35 +0800 INFO -- : [192.168.1.106] /ctmgr 7 | 2013-05-03 00:00:35 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 8 | 2013-05-03 00:00:35 +0800 INFO -- : [192.168.1.106] /favicon.ico 9 | 2013-05-03 00:00:35 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 10 | 2013-05-03 00:00:37 +0800 INFO -- : [192.168.1.106] /ctmgr 11 | 2013-05-03 00:00:37 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 12 | 2013-05-03 00:00:37 +0800 INFO -- : [192.168.1.106] /favicon.ico 13 | 2013-05-03 00:00:37 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 14 | 2013-05-03 00:00:42 +0800 INFO -- : [192.168.1.106] /ctmgr 15 | 2013-05-03 00:00:42 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 16 | 2013-05-03 00:00:42 +0800 INFO -- : [192.168.1.106] /favicon.ico 17 | 2013-05-03 00:00:42 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 18 | 2013-05-03 00:00:45 +0800 INFO -- : [192.168.1.106] /ctmgr 19 | 2013-05-03 00:00:45 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 20 | 2013-05-03 00:00:46 +0800 INFO -- : [192.168.1.106] /favicon.ico 21 | 2013-05-03 00:00:46 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 22 | 2013-05-03 00:00:53 +0800 INFO -- : [192.168.1.106] /ctmgr 23 | 2013-05-03 00:00:53 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 24 | 2013-05-03 00:00:53 +0800 INFO -- : [192.168.1.106] /favicon.ico 25 | 2013-05-03 00:00:53 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 26 | 2013-05-03 00:01:44 +0800 INFO -- : [192.168.1.106] /ctmgr 27 | 2013-05-03 00:01:44 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 28 | 2013-05-03 00:01:45 +0800 INFO -- : [192.168.1.106] /favicon.ico 29 | 2013-05-03 00:01:45 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 30 | 2013-05-03 00:04:42 +0800 INFO -- : [192.168.1.106] /ctmgr 31 | 2013-05-03 00:04:42 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 32 | 2013-05-03 00:04:42 +0800 INFO -- : [192.168.1.106] /favicon.ico 33 | 2013-05-03 00:04:42 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 34 | 2013-05-03 00:04:44 +0800 INFO -- : [192.168.1.106] /ctmgr 35 | 2013-05-03 00:04:44 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 36 | 2013-05-03 00:04:44 +0800 INFO -- : [192.168.1.106] /favicon.ico 37 | 2013-05-03 00:04:44 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 38 | 2013-05-03 00:05:37 +0800 INFO -- : [192.168.1.106] /ctmgr 39 | 2013-05-03 00:05:37 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 40 | 2013-05-03 00:05:37 +0800 INFO -- : [192.168.1.106] /favicon.ico 41 | 2013-05-03 00:05:37 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 42 | 2013-05-03 00:05:41 +0800 INFO -- : [192.168.1.106] /ctmgr 43 | 2013-05-03 00:05:41 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 44 | 2013-05-03 00:05:41 +0800 INFO -- : [192.168.1.106] /favicon.ico 45 | 2013-05-03 00:05:41 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 46 | 2013-05-03 00:05:44 +0800 INFO -- : [192.168.1.106] /ctmgr 47 | 2013-05-03 00:05:44 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 48 | 2013-05-03 00:05:44 +0800 INFO -- : [192.168.1.106] /favicon.ico 49 | 2013-05-03 00:05:44 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 50 | 2013-05-03 00:06:27 +0800 INFO -- : [192.168.1.106] /ctmgr 51 | 2013-05-03 00:06:27 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 52 | 2013-05-03 00:06:27 +0800 INFO -- : [192.168.1.106] /favicon.ico 53 | 2013-05-03 00:06:27 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 54 | 2013-05-03 00:07:07 +0800 INFO -- : [10.180.2.13] called ctmgr.getCtstatus 55 | 2013-05-03 00:08:45 +0800 INFO -- : [10.180.2.13] called ctmgr.getCtstatus 56 | 2013-05-03 00:18:55 +0800 INFO -- : [192.168.1.106] /ctmgr 57 | 2013-05-03 00:18:55 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 58 | 2013-05-03 00:18:56 +0800 INFO -- : [192.168.1.106] /favicon.ico 59 | 2013-05-03 00:18:56 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 60 | 2013-05-03 00:28:08 +0800 INFO -- : [192.168.1.106] /ctmgr 61 | 2013-05-03 00:28:08 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 62 | 2013-05-03 00:28:09 +0800 INFO -- : [192.168.1.106] /favicon.ico 63 | 2013-05-03 00:28:09 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 64 | 2013-05-03 00:28:10 +0800 INFO -- : [192.168.1.106] /ctmgr 65 | 2013-05-03 00:28:10 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 66 | 2013-05-03 00:28:10 +0800 INFO -- : [192.168.1.106] /favicon.ico 67 | 2013-05-03 00:28:10 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 68 | -------------------------------------------------------------------------------- /views/log/frigga.log.20130501: -------------------------------------------------------------------------------- 1 | # Logfile created on 2013-05-01 18:05:45 +0800 by logger.rb/31641 2 | 2013-05-01 18:05:45 +0800 INFO -- : Init env. Mkdir /home/xiedanbo/frigga/deploy/log/ 3 | 2013-05-01 18:05:45 +0800 INFO -- : Init env. Mkdir /home/xiedanbo/frigga/deploy/tmp/ 4 | 2013-05-01 18:06:07 +0800 INFO -- : [10.180.2.13] called ctmgr.getCtstatus 5 | 2013-05-01 18:07:47 +0800 INFO -- : [192.168.1.106] / 6 | 2013-05-01 18:07:52 +0800 INFO -- : [192.168.1.106] /favicon.ico 7 | 2013-05-01 18:08:45 +0800 INFO -- : [192.168.1.106] / 8 | 2013-05-01 18:08:45 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 9 | 2013-05-01 18:08:45 +0800 INFO -- : [192.168.1.106] /favicon.ico 10 | 2013-05-01 18:08:45 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 11 | 2013-05-01 18:10:08 +0800 INFO -- : [192.168.1.106] /log/god/0 12 | 2013-05-01 18:10:08 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 13 | 2013-05-01 18:10:08 +0800 INFO -- : [192.168.1.106] /log/god/0 14 | 2013-05-01 18:10:08 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 15 | 2013-05-01 18:10:08 +0800 INFO -- : [192.168.1.106] /favicon.ico 16 | 2013-05-01 18:10:08 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 17 | 2013-05-01 18:10:09 +0800 INFO -- : [192.168.1.106] / 18 | 2013-05-01 18:10:09 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 19 | 2013-05-01 18:10:09 +0800 INFO -- : [192.168.1.106] /favicon.ico 20 | 2013-05-01 18:10:09 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 21 | 2013-05-01 18:10:10 +0800 INFO -- : [192.168.1.106] /about 22 | 2013-05-01 18:10:10 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 23 | 2013-05-01 18:10:10 +0800 INFO -- : [192.168.1.106] /favicon.ico 24 | 2013-05-01 18:10:10 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 25 | 2013-05-01 18:10:11 +0800 INFO -- : [192.168.1.106] /log/god/0 26 | 2013-05-01 18:10:11 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 27 | 2013-05-01 18:10:12 +0800 INFO -- : [192.168.1.106] /favicon.ico 28 | 2013-05-01 18:10:12 +0800 INFO -- : [192.168.1.106] /log/god/0 29 | 2013-05-01 18:10:12 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 30 | 2013-05-01 18:10:12 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 31 | 2013-05-01 18:10:13 +0800 INFO -- : [192.168.1.106] / 32 | 2013-05-01 18:10:13 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 33 | 2013-05-01 18:10:13 +0800 INFO -- : [192.168.1.106] /favicon.ico 34 | 2013-05-01 18:10:13 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 35 | 2013-05-01 18:11:30 +0800 INFO -- : [192.168.1.106] /log/god/0 36 | 2013-05-01 18:11:30 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 37 | 2013-05-01 18:11:30 +0800 INFO -- : [192.168.1.106] /log/god/0 38 | 2013-05-01 18:11:30 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 39 | 2013-05-01 18:11:30 +0800 INFO -- : [192.168.1.106] /favicon.ico 40 | 2013-05-01 18:11:30 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 41 | 2013-05-01 18:15:48 +0800 INFO -- : [192.168.1.106] / 42 | 2013-05-01 18:15:48 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 43 | 2013-05-01 18:15:48 +0800 INFO -- : [192.168.1.106] /favicon.ico 44 | 2013-05-01 18:15:48 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 45 | 2013-05-01 18:16:04 +0800 INFO -- : [192.168.1.106] /about 46 | 2013-05-01 18:16:04 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 47 | 2013-05-01 18:16:04 +0800 INFO -- : [192.168.1.106] /favicon.ico 48 | 2013-05-01 18:16:04 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 49 | 2013-05-01 18:16:08 +0800 INFO -- : [192.168.1.106] /ctmgr 50 | 2013-05-01 18:16:08 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 51 | 2013-05-01 18:16:09 +0800 INFO -- : [192.168.1.106] /favicon.ico 52 | 2013-05-01 18:16:09 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 53 | 2013-05-01 18:17:57 +0800 INFO -- : [192.168.1.106] /ctmgr 54 | 2013-05-01 18:17:57 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 55 | 2013-05-01 18:17:57 +0800 INFO -- : [192.168.1.106] /favicon.ico 56 | 2013-05-01 18:17:57 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 57 | 2013-05-01 18:17:58 +0800 INFO -- : [192.168.1.106] /ctmgr 58 | 2013-05-01 18:17:58 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 59 | 2013-05-01 18:17:58 +0800 INFO -- : [192.168.1.106] /favicon.ico 60 | 2013-05-01 18:17:58 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 61 | 2013-05-01 18:17:59 +0800 INFO -- : [192.168.1.106] /ctmgr 62 | 2013-05-01 18:17:59 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 63 | 2013-05-01 18:17:59 +0800 INFO -- : [192.168.1.106] /favicon.ico 64 | 2013-05-01 18:17:59 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 65 | 2013-05-01 18:18:21 +0800 INFO -- : [192.168.1.106] /ctmgr 66 | 2013-05-01 18:18:21 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 67 | 2013-05-01 18:18:21 +0800 INFO -- : [192.168.1.106] /favicon.ico 68 | 2013-05-01 18:18:21 +0800 INFO -- : [192.168.1.106] in white-list, skip Auth! 69 | -------------------------------------------------------------------------------- /views/lib/frigga.rb: -------------------------------------------------------------------------------- 1 | $: << "./" 2 | require 'sinatra/base' 3 | require 'rack/rpc' 4 | require 'frigga/log' 5 | require 'frigga/auth' 6 | require 'frigga/talk' 7 | require 'frigga/rpc' 8 | require 'sinatra-websocket' 9 | require "file/tail" 10 | 11 | module Frigga 12 | class Tail < File 13 | include File::Tail 14 | end 15 | 16 | class WebServer < Sinatra::Base 17 | include Frigga::Auth 18 | configure do 19 | set :public_folder, Proc.new { File.join(File.expand_path(""), "static") } 20 | set :views, Proc.new { File.join(File.expand_path(""), "views") } 21 | enable :sessions 22 | set :port => Http_port 23 | set :bind => '0.0.0.0' 24 | set :show_exceptions => false 25 | end 26 | # use Rack::Auth::Basic, "Protected Area" do |username, password| 27 | # username == 'foo' && password == 'bar' 28 | # end 29 | before '*' do 30 | Logger.info "[#{request.ip}] #{request.path}" 31 | if White_list.include?(request.ip) 32 | Logger.info "[#{request.ip}] in white-list, skip Auth!" 33 | else 34 | protected! unless request.websocket? 35 | end 36 | @ver = VER 37 | end 38 | 39 | not_found do 40 | erb :notfound 41 | end 42 | 43 | error do 44 | @error = env['sinatra.error'].message 45 | erb :error 46 | end 47 | 48 | get '/' do 49 | if session[:notice] 50 | @msg = session[:notice].dup 51 | session[:notice] = nil 52 | end 53 | @process = Frigga::Talk.god('status') 54 | erb :index 55 | end 56 | 57 | get '/about' do 58 | @process = Frigga::Talk.god('status') 59 | raise "Frigga hasn't watched by God" if @process.empty? 60 | erb :about 61 | end 62 | 63 | get '/log/*' do 64 | #check log file 65 | name, type, index = params[:splat][0].split('/') 66 | raise "url error" if name.nil? or type.nil? 67 | log = nil 68 | log_file = nil 69 | thr = nil 70 | if name == "god" 71 | if File.exist?("/var/log/messages") 72 | log_file = "/var/log/messages" 73 | elsif File.exist?("/var/log/syslog") 74 | log_file = "/var/log/syslog" 75 | end 76 | else 77 | process = Frigga::Talk.god('status') 78 | raise "Don't have name:#{name}" unless process.key?(name) 79 | if !process[name][:all_log][type.to_sym].nil? && !process[name][:all_log][type.to_sym].eql?("/dev/null") 80 | if process[name][:all_log][type.to_sym].kind_of?(Array) 81 | if !index.nil? && (0 .. process[name][:all_log][type.to_sym].size-1).include?(index.to_i) 82 | log_file = process[name][:all_log][type.to_sym][index.to_i] if File.exist?(process[name][:all_log][type.to_sym][index.to_i]) 83 | end 84 | else 85 | log_file = process[name][:all_log][type.to_sym] if File.exist?(process[name][:all_log][type.to_sym]) 86 | end 87 | end 88 | end 89 | raise "We can't find log file, sorry..." if log_file.nil? 90 | 91 | #start websocet 92 | if !request.websocket? 93 | erb :tail 94 | else 95 | request.websocket do |ws| 96 | ws.onopen do 97 | log = Tail.new(log_file) 98 | log.interval = 5 99 | log.backward(5) 100 | log.return_if_eof = true 101 | thr = Thread.new { 102 | loop do 103 | unless log.closed? 104 | log.tail { |line| ws.send line } 105 | end 106 | sleep 0.25 107 | end 108 | } 109 | end 110 | 111 | ws.onmessage do |msg| 112 | if msg =~ /END/i 113 | thr.kill if thr.alive? 114 | log.close unless log.closed? 115 | ws.close_connection 116 | end 117 | end 118 | 119 | ws.onclose do 120 | thr.kill if thr.alive? 121 | log.close unless log.closed? 122 | end 123 | end 124 | end 125 | end 126 | 127 | post '/god/:action' do |action| 128 | unless %w(restart start stop).include?(action) 129 | raise "Don't know action[#{action}]" 130 | end 131 | hi = Frigga::Talk.god(action, params[:name]) 132 | if hi.empty? 133 | raise "#{action.capitalize} #{params[:name]} failed! #{hi[1]}" 134 | end 135 | session[:notice] = "Action: #{action} process[#{params[:name]}] success!" 136 | sleep 0.5 137 | redirect to '/' 138 | end 139 | 140 | #get '/ctmgr' do 141 | # if session[:notice] 142 | # @msg = session[:notice].dup 143 | # session[:notice] = nil 144 | # end 145 | # include Frigga::RPC::Ctmgr 146 | # @ctstatus = getCtstatus() 147 | # erb :ctmgr 148 | #end 149 | 150 | use Rack::RPC::Endpoint, Frigga::RPC::Runner.new 151 | end 152 | end 153 | 154 | -------------------------------------------------------------------------------- /static/vendor/bootstrap/js/bootstrap-tooltip.js: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | * bootstrap-tooltip.js v2.3.1 3 | * http://twitter.github.com/bootstrap/javascript.html#tooltips 4 | * Inspired by the original jQuery.tipsy by Jason Frame 5 | * =========================================================== 6 | * Copyright 2012 Twitter, Inc. 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * ========================================================== */ 20 | 21 | 22 | !function ($) { 23 | 24 | "use strict"; // jshint ;_; 25 | 26 | 27 | /* TOOLTIP PUBLIC CLASS DEFINITION 28 | * =============================== */ 29 | 30 | var Tooltip = function (element, options) { 31 | this.init('tooltip', element, options) 32 | } 33 | 34 | Tooltip.prototype = { 35 | 36 | constructor: Tooltip 37 | 38 | , init: function (type, element, options) { 39 | var eventIn 40 | , eventOut 41 | , triggers 42 | , trigger 43 | , i 44 | 45 | this.type = type 46 | this.$element = $(element) 47 | this.options = this.getOptions(options) 48 | this.enabled = true 49 | 50 | triggers = this.options.trigger.split(' ') 51 | 52 | for (i = triggers.length; i--;) { 53 | trigger = triggers[i] 54 | if (trigger == 'click') { 55 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) 56 | } else if (trigger != 'manual') { 57 | eventIn = trigger == 'hover' ? 'mouseenter' : 'focus' 58 | eventOut = trigger == 'hover' ? 'mouseleave' : 'blur' 59 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) 60 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) 61 | } 62 | } 63 | 64 | this.options.selector ? 65 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : 66 | this.fixTitle() 67 | } 68 | 69 | , getOptions: function (options) { 70 | options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options) 71 | 72 | if (options.delay && typeof options.delay == 'number') { 73 | options.delay = { 74 | show: options.delay 75 | , hide: options.delay 76 | } 77 | } 78 | 79 | return options 80 | } 81 | 82 | , enter: function (e) { 83 | var defaults = $.fn[this.type].defaults 84 | , options = {} 85 | , self 86 | 87 | this._options && $.each(this._options, function (key, value) { 88 | if (defaults[key] != value) options[key] = value 89 | }, this) 90 | 91 | self = $(e.currentTarget)[this.type](options).data(this.type) 92 | 93 | if (!self.options.delay || !self.options.delay.show) return self.show() 94 | 95 | clearTimeout(this.timeout) 96 | self.hoverState = 'in' 97 | this.timeout = setTimeout(function() { 98 | if (self.hoverState == 'in') self.show() 99 | }, self.options.delay.show) 100 | } 101 | 102 | , leave: function (e) { 103 | var self = $(e.currentTarget)[this.type](this._options).data(this.type) 104 | 105 | if (this.timeout) clearTimeout(this.timeout) 106 | if (!self.options.delay || !self.options.delay.hide) return self.hide() 107 | 108 | self.hoverState = 'out' 109 | this.timeout = setTimeout(function() { 110 | if (self.hoverState == 'out') self.hide() 111 | }, self.options.delay.hide) 112 | } 113 | 114 | , show: function () { 115 | var $tip 116 | , pos 117 | , actualWidth 118 | , actualHeight 119 | , placement 120 | , tp 121 | , e = $.Event('show') 122 | 123 | if (this.hasContent() && this.enabled) { 124 | this.$element.trigger(e) 125 | if (e.isDefaultPrevented()) return 126 | $tip = this.tip() 127 | this.setContent() 128 | 129 | if (this.options.animation) { 130 | $tip.addClass('fade') 131 | } 132 | 133 | placement = typeof this.options.placement == 'function' ? 134 | this.options.placement.call(this, $tip[0], this.$element[0]) : 135 | this.options.placement 136 | 137 | $tip 138 | .detach() 139 | .css({ top: 0, left: 0, display: 'block' }) 140 | 141 | this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) 142 | 143 | pos = this.getPosition() 144 | 145 | actualWidth = $tip[0].offsetWidth 146 | actualHeight = $tip[0].offsetHeight 147 | 148 | switch (placement) { 149 | case 'bottom': 150 | tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} 151 | break 152 | case 'top': 153 | tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} 154 | break 155 | case 'left': 156 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} 157 | break 158 | case 'right': 159 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} 160 | break 161 | } 162 | 163 | this.applyPlacement(tp, placement) 164 | this.$element.trigger('shown') 165 | } 166 | } 167 | 168 | , applyPlacement: function(offset, placement){ 169 | var $tip = this.tip() 170 | , width = $tip[0].offsetWidth 171 | , height = $tip[0].offsetHeight 172 | , actualWidth 173 | , actualHeight 174 | , delta 175 | , replace 176 | 177 | $tip 178 | .offset(offset) 179 | .addClass(placement) 180 | .addClass('in') 181 | 182 | actualWidth = $tip[0].offsetWidth 183 | actualHeight = $tip[0].offsetHeight 184 | 185 | if (placement == 'top' && actualHeight != height) { 186 | offset.top = offset.top + height - actualHeight 187 | replace = true 188 | } 189 | 190 | if (placement == 'bottom' || placement == 'top') { 191 | delta = 0 192 | 193 | if (offset.left < 0){ 194 | delta = offset.left * -2 195 | offset.left = 0 196 | $tip.offset(offset) 197 | actualWidth = $tip[0].offsetWidth 198 | actualHeight = $tip[0].offsetHeight 199 | } 200 | 201 | this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') 202 | } else { 203 | this.replaceArrow(actualHeight - height, actualHeight, 'top') 204 | } 205 | 206 | if (replace) $tip.offset(offset) 207 | } 208 | 209 | , replaceArrow: function(delta, dimension, position){ 210 | this 211 | .arrow() 212 | .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '') 213 | } 214 | 215 | , setContent: function () { 216 | var $tip = this.tip() 217 | , title = this.getTitle() 218 | 219 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) 220 | $tip.removeClass('fade in top bottom left right') 221 | } 222 | 223 | , hide: function () { 224 | var that = this 225 | , $tip = this.tip() 226 | , e = $.Event('hide') 227 | 228 | this.$element.trigger(e) 229 | if (e.isDefaultPrevented()) return 230 | 231 | $tip.removeClass('in') 232 | 233 | function removeWithAnimation() { 234 | var timeout = setTimeout(function () { 235 | $tip.off($.support.transition.end).detach() 236 | }, 500) 237 | 238 | $tip.one($.support.transition.end, function () { 239 | clearTimeout(timeout) 240 | $tip.detach() 241 | }) 242 | } 243 | 244 | $.support.transition && this.$tip.hasClass('fade') ? 245 | removeWithAnimation() : 246 | $tip.detach() 247 | 248 | this.$element.trigger('hidden') 249 | 250 | return this 251 | } 252 | 253 | , fixTitle: function () { 254 | var $e = this.$element 255 | if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { 256 | $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') 257 | } 258 | } 259 | 260 | , hasContent: function () { 261 | return this.getTitle() 262 | } 263 | 264 | , getPosition: function () { 265 | var el = this.$element[0] 266 | return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { 267 | width: el.offsetWidth 268 | , height: el.offsetHeight 269 | }, this.$element.offset()) 270 | } 271 | 272 | , getTitle: function () { 273 | var title 274 | , $e = this.$element 275 | , o = this.options 276 | 277 | title = $e.attr('data-original-title') 278 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 279 | 280 | return title 281 | } 282 | 283 | , tip: function () { 284 | return this.$tip = this.$tip || $(this.options.template) 285 | } 286 | 287 | , arrow: function(){ 288 | return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow") 289 | } 290 | 291 | , validate: function () { 292 | if (!this.$element[0].parentNode) { 293 | this.hide() 294 | this.$element = null 295 | this.options = null 296 | } 297 | } 298 | 299 | , enable: function () { 300 | this.enabled = true 301 | } 302 | 303 | , disable: function () { 304 | this.enabled = false 305 | } 306 | 307 | , toggleEnabled: function () { 308 | this.enabled = !this.enabled 309 | } 310 | 311 | , toggle: function (e) { 312 | var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this 313 | self.tip().hasClass('in') ? self.hide() : self.show() 314 | } 315 | 316 | , destroy: function () { 317 | this.hide().$element.off('.' + this.type).removeData(this.type) 318 | } 319 | 320 | } 321 | 322 | 323 | /* TOOLTIP PLUGIN DEFINITION 324 | * ========================= */ 325 | 326 | var old = $.fn.tooltip 327 | 328 | $.fn.tooltip = function ( option ) { 329 | return this.each(function () { 330 | var $this = $(this) 331 | , data = $this.data('tooltip') 332 | , options = typeof option == 'object' && option 333 | if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) 334 | if (typeof option == 'string') data[option]() 335 | }) 336 | } 337 | 338 | $.fn.tooltip.Constructor = Tooltip 339 | 340 | $.fn.tooltip.defaults = { 341 | animation: true 342 | , placement: 'top' 343 | , selector: false 344 | , template: '
' 345 | , trigger: 'hover focus' 346 | , title: '' 347 | , delay: 0 348 | , html: false 349 | , container: false 350 | } 351 | 352 | 353 | /* TOOLTIP NO CONFLICT 354 | * =================== */ 355 | 356 | $.fn.tooltip.noConflict = function () { 357 | $.fn.tooltip = old 358 | return this 359 | } 360 | 361 | }(window.jQuery); 362 | -------------------------------------------------------------------------------- /views/static/vendor/bootstrap/js/bootstrap-tooltip.js: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | * bootstrap-tooltip.js v2.3.1 3 | * http://twitter.github.com/bootstrap/javascript.html#tooltips 4 | * Inspired by the original jQuery.tipsy by Jason Frame 5 | * =========================================================== 6 | * Copyright 2012 Twitter, Inc. 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * ========================================================== */ 20 | 21 | 22 | !function ($) { 23 | 24 | "use strict"; // jshint ;_; 25 | 26 | 27 | /* TOOLTIP PUBLIC CLASS DEFINITION 28 | * =============================== */ 29 | 30 | var Tooltip = function (element, options) { 31 | this.init('tooltip', element, options) 32 | } 33 | 34 | Tooltip.prototype = { 35 | 36 | constructor: Tooltip 37 | 38 | , init: function (type, element, options) { 39 | var eventIn 40 | , eventOut 41 | , triggers 42 | , trigger 43 | , i 44 | 45 | this.type = type 46 | this.$element = $(element) 47 | this.options = this.getOptions(options) 48 | this.enabled = true 49 | 50 | triggers = this.options.trigger.split(' ') 51 | 52 | for (i = triggers.length; i--;) { 53 | trigger = triggers[i] 54 | if (trigger == 'click') { 55 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) 56 | } else if (trigger != 'manual') { 57 | eventIn = trigger == 'hover' ? 'mouseenter' : 'focus' 58 | eventOut = trigger == 'hover' ? 'mouseleave' : 'blur' 59 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) 60 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) 61 | } 62 | } 63 | 64 | this.options.selector ? 65 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : 66 | this.fixTitle() 67 | } 68 | 69 | , getOptions: function (options) { 70 | options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options) 71 | 72 | if (options.delay && typeof options.delay == 'number') { 73 | options.delay = { 74 | show: options.delay 75 | , hide: options.delay 76 | } 77 | } 78 | 79 | return options 80 | } 81 | 82 | , enter: function (e) { 83 | var defaults = $.fn[this.type].defaults 84 | , options = {} 85 | , self 86 | 87 | this._options && $.each(this._options, function (key, value) { 88 | if (defaults[key] != value) options[key] = value 89 | }, this) 90 | 91 | self = $(e.currentTarget)[this.type](options).data(this.type) 92 | 93 | if (!self.options.delay || !self.options.delay.show) return self.show() 94 | 95 | clearTimeout(this.timeout) 96 | self.hoverState = 'in' 97 | this.timeout = setTimeout(function() { 98 | if (self.hoverState == 'in') self.show() 99 | }, self.options.delay.show) 100 | } 101 | 102 | , leave: function (e) { 103 | var self = $(e.currentTarget)[this.type](this._options).data(this.type) 104 | 105 | if (this.timeout) clearTimeout(this.timeout) 106 | if (!self.options.delay || !self.options.delay.hide) return self.hide() 107 | 108 | self.hoverState = 'out' 109 | this.timeout = setTimeout(function() { 110 | if (self.hoverState == 'out') self.hide() 111 | }, self.options.delay.hide) 112 | } 113 | 114 | , show: function () { 115 | var $tip 116 | , pos 117 | , actualWidth 118 | , actualHeight 119 | , placement 120 | , tp 121 | , e = $.Event('show') 122 | 123 | if (this.hasContent() && this.enabled) { 124 | this.$element.trigger(e) 125 | if (e.isDefaultPrevented()) return 126 | $tip = this.tip() 127 | this.setContent() 128 | 129 | if (this.options.animation) { 130 | $tip.addClass('fade') 131 | } 132 | 133 | placement = typeof this.options.placement == 'function' ? 134 | this.options.placement.call(this, $tip[0], this.$element[0]) : 135 | this.options.placement 136 | 137 | $tip 138 | .detach() 139 | .css({ top: 0, left: 0, display: 'block' }) 140 | 141 | this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) 142 | 143 | pos = this.getPosition() 144 | 145 | actualWidth = $tip[0].offsetWidth 146 | actualHeight = $tip[0].offsetHeight 147 | 148 | switch (placement) { 149 | case 'bottom': 150 | tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} 151 | break 152 | case 'top': 153 | tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} 154 | break 155 | case 'left': 156 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} 157 | break 158 | case 'right': 159 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} 160 | break 161 | } 162 | 163 | this.applyPlacement(tp, placement) 164 | this.$element.trigger('shown') 165 | } 166 | } 167 | 168 | , applyPlacement: function(offset, placement){ 169 | var $tip = this.tip() 170 | , width = $tip[0].offsetWidth 171 | , height = $tip[0].offsetHeight 172 | , actualWidth 173 | , actualHeight 174 | , delta 175 | , replace 176 | 177 | $tip 178 | .offset(offset) 179 | .addClass(placement) 180 | .addClass('in') 181 | 182 | actualWidth = $tip[0].offsetWidth 183 | actualHeight = $tip[0].offsetHeight 184 | 185 | if (placement == 'top' && actualHeight != height) { 186 | offset.top = offset.top + height - actualHeight 187 | replace = true 188 | } 189 | 190 | if (placement == 'bottom' || placement == 'top') { 191 | delta = 0 192 | 193 | if (offset.left < 0){ 194 | delta = offset.left * -2 195 | offset.left = 0 196 | $tip.offset(offset) 197 | actualWidth = $tip[0].offsetWidth 198 | actualHeight = $tip[0].offsetHeight 199 | } 200 | 201 | this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') 202 | } else { 203 | this.replaceArrow(actualHeight - height, actualHeight, 'top') 204 | } 205 | 206 | if (replace) $tip.offset(offset) 207 | } 208 | 209 | , replaceArrow: function(delta, dimension, position){ 210 | this 211 | .arrow() 212 | .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '') 213 | } 214 | 215 | , setContent: function () { 216 | var $tip = this.tip() 217 | , title = this.getTitle() 218 | 219 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) 220 | $tip.removeClass('fade in top bottom left right') 221 | } 222 | 223 | , hide: function () { 224 | var that = this 225 | , $tip = this.tip() 226 | , e = $.Event('hide') 227 | 228 | this.$element.trigger(e) 229 | if (e.isDefaultPrevented()) return 230 | 231 | $tip.removeClass('in') 232 | 233 | function removeWithAnimation() { 234 | var timeout = setTimeout(function () { 235 | $tip.off($.support.transition.end).detach() 236 | }, 500) 237 | 238 | $tip.one($.support.transition.end, function () { 239 | clearTimeout(timeout) 240 | $tip.detach() 241 | }) 242 | } 243 | 244 | $.support.transition && this.$tip.hasClass('fade') ? 245 | removeWithAnimation() : 246 | $tip.detach() 247 | 248 | this.$element.trigger('hidden') 249 | 250 | return this 251 | } 252 | 253 | , fixTitle: function () { 254 | var $e = this.$element 255 | if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { 256 | $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') 257 | } 258 | } 259 | 260 | , hasContent: function () { 261 | return this.getTitle() 262 | } 263 | 264 | , getPosition: function () { 265 | var el = this.$element[0] 266 | return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { 267 | width: el.offsetWidth 268 | , height: el.offsetHeight 269 | }, this.$element.offset()) 270 | } 271 | 272 | , getTitle: function () { 273 | var title 274 | , $e = this.$element 275 | , o = this.options 276 | 277 | title = $e.attr('data-original-title') 278 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 279 | 280 | return title 281 | } 282 | 283 | , tip: function () { 284 | return this.$tip = this.$tip || $(this.options.template) 285 | } 286 | 287 | , arrow: function(){ 288 | return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow") 289 | } 290 | 291 | , validate: function () { 292 | if (!this.$element[0].parentNode) { 293 | this.hide() 294 | this.$element = null 295 | this.options = null 296 | } 297 | } 298 | 299 | , enable: function () { 300 | this.enabled = true 301 | } 302 | 303 | , disable: function () { 304 | this.enabled = false 305 | } 306 | 307 | , toggleEnabled: function () { 308 | this.enabled = !this.enabled 309 | } 310 | 311 | , toggle: function (e) { 312 | var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this 313 | self.tip().hasClass('in') ? self.hide() : self.show() 314 | } 315 | 316 | , destroy: function () { 317 | this.hide().$element.off('.' + this.type).removeData(this.type) 318 | } 319 | 320 | } 321 | 322 | 323 | /* TOOLTIP PLUGIN DEFINITION 324 | * ========================= */ 325 | 326 | var old = $.fn.tooltip 327 | 328 | $.fn.tooltip = function ( option ) { 329 | return this.each(function () { 330 | var $this = $(this) 331 | , data = $this.data('tooltip') 332 | , options = typeof option == 'object' && option 333 | if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) 334 | if (typeof option == 'string') data[option]() 335 | }) 336 | } 337 | 338 | $.fn.tooltip.Constructor = Tooltip 339 | 340 | $.fn.tooltip.defaults = { 341 | animation: true 342 | , placement: 'top' 343 | , selector: false 344 | , template: '
' 345 | , trigger: 'hover focus' 346 | , title: '' 347 | , delay: 0 348 | , html: false 349 | , container: false 350 | } 351 | 352 | 353 | /* TOOLTIP NO CONFLICT 354 | * =================== */ 355 | 356 | $.fn.tooltip.noConflict = function () { 357 | $.fn.tooltip = old 358 | return this 359 | } 360 | 361 | }(window.jQuery); 362 | -------------------------------------------------------------------------------- /static/vendor/bootstrap/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.0 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /views/static/vendor/bootstrap/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.0 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /views/lib/frigga/rpc/deploy.rb: -------------------------------------------------------------------------------- 1 | module Frigga 2 | 3 | module RPC 4 | 5 | require "digest/md5" 6 | require "fileutils" 7 | require "pathname" 8 | 9 | BASE_PATH = Pathname.new(File.dirname(__FILE__)).realpath.to_s.split("/")[0..-4].join("/") + "/deploy/" 10 | DATA_PATH = BASE_PATH + "data/" 11 | TMP_PATH = BASE_PATH + "tmp/" 12 | LOG_PATH = BASE_PATH + "log/" 13 | HM_PATH = "/home/xbox/aesir/thor/thor/bin/thor.py" 14 | GOD_PATH = "/home/xbox/aesir/frigga/gods/" 15 | DEPLOY_LCK= LOG_PATH + "deploy.lck" 16 | LOG_SYMBOL= "=" 17 | 18 | if not File.directory?LOG_PATH 19 | Logger.info "Init env. Mkdir #{LOG_PATH}" 20 | `mkdir -p #{LOG_PATH}` 21 | end 22 | 23 | if not File.directory?TMP_PATH 24 | Logger.info "Init env. Mkdir #{TMP_PATH}" 25 | `mkdir -p #{TMP_PATH}` 26 | end 27 | 28 | module Deploy 29 | 30 | RPC_LIST = %w( startDeploy getDeployStatus getDeployTaskStatus getDeployLog4Mod getDeployLog ) 31 | 32 | # Hostname`s hash 33 | def _mk_id_part_hostname 34 | hostname = `hostname` 35 | checksum = Digest::MD5.hexdigest(hostname) 36 | return checksum[0,8] 37 | end 38 | 39 | # Create random string 40 | def _random_str(len=8) 41 | chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a 42 | rand_str = "" 43 | 1.upto(len) { |i| rand_str << chars[rand(chars.size-1)] } 44 | return rand_str 45 | end 46 | 47 | # Download file ,return the abs path. 48 | def _down_file(path, url, file_name) 49 | 50 | down_cmd = "cd %s && wget %s -q -O ./%s" %[path, url, file_name] 51 | r = system(down_cmd) 52 | file_path = path + "/" + file_name 53 | 54 | if FileTest::exists?(file_path) and not FileTest::zero?(file_path) 55 | Logger.info "Download succ . file_path : #{file_path} url : #{url}" 56 | return ['task_id', 'succ', file_path] 57 | else 58 | Logger.fatal "Download fail. file_path : #{file_path} url : #{url}" 59 | return ['task_id', 'fail', file_path] 60 | end 61 | end 62 | 63 | # Unzip file. 64 | def _do_unzip(gz_file) 65 | 66 | # The zip_file like 67 | # "/home/xiedanbo/frigg/lib/deploy/tmp/eca12e6anPDHQkRE1364449531/cluster.tar.gz" 68 | # The filename_before like "cluster.tar.gz" 69 | # The filename_bef_tar like "cluster" 70 | # The task_id like "eca12e6anPDHQkRE1364449531" 71 | # The file_path like "/home/xiedanbo/frigg/lib/deploy/tmp/eca12e6anPDHQkRE1364449531" 72 | 73 | task_id, filename_before = gz_file.split("/")[-2..-1] 74 | filename_bef_tar = filename_before.split(".tar.gz")[0] 75 | file_path = gz_file.split("/")[0..-2].join("/") 76 | file_path_af_unzip = file_path + "/" + filename_bef_tar 77 | 78 | tar_cmd = "cd %s && tar zxf %s" %[file_path, filename_before] 79 | r = system(tar_cmd) 80 | 81 | if FileTest::exists?(file_path_af_unzip) 82 | Logger.info "Unzip #{gz_file} succ . file_path_af_unzip : #{file_path_af_unzip}" 83 | return [task_id, "succ", file_path_af_unzip] 84 | else 85 | Logger.fatal "Unzip #{gz_file} fail. file_path_af_unzip : #{file_path_af_unzip}" 86 | return [task_id, 'fail', 'Unzip the data failed.'] 87 | end 88 | 89 | end 90 | 91 | # Through the shell execute the thor script. 92 | def _execute_thor(thor_data_path) 93 | 94 | task_id = thor_data_path.split("/")[-2] 95 | thor_log_path = LOG_PATH + "thor_" + task_id + ".log" 96 | deploy_cmd = "nohup /usr/bin/python -u %s -p %s -s %s &>%s &" \ 97 | %[HM_PATH, thor_data_path, GOD_PATH, thor_log_path] 98 | r = system(deploy_cmd) 99 | 100 | Logger.info "Execute the thor succ. deploy_cmd >>> #{deploy_cmd} <<<" 101 | return [task_id, 'succ', 'hahaha'] 102 | 103 | end 104 | 105 | # Do deploy task. 106 | def _do_deploy( task_id, port, path, host_ip) 107 | 108 | if path.start_with?("/") 109 | url = "http://%s:%s%s" %[host_ip.to_s, port.to_s, path.to_s] 110 | else 111 | url = "http://%s:%s/%s" %[host_ip.to_s, port.to_s, path.to_s] 112 | end 113 | 114 | tmp_file_path = TMP_PATH + task_id 115 | Logger.info "Task_id : #{task_id} url : #{url} tmp_file_path : #{tmp_file_path}" 116 | 117 | if FileTest::exists?(tmp_file_path) 118 | Logger.fatal "The tmp path for task was exists. #{tmp_file_path}" 119 | return [task_id, "fail","The tmp path for task was exists."] 120 | end 121 | 122 | begin 123 | FileUtils.mkdir(tmp_file_path) 124 | rescue => e 125 | Logger.fatal "Mkdir #{tmp_file_path} failed." 126 | return [task_id, "fail",e] 127 | end 128 | 129 | # Check file format, support "tar.gz" 130 | # _file_name_prefix like "first_deploy-1.0.0.2" 131 | _file_name_prefix = url.split("/")[-1].split(".tar.gz")[0] 132 | _suffix = path.split(".")[-2..-1].join(".") 133 | 134 | if _suffix != "tar.gz" 135 | Logger.fatal "Unsupport file type. #{_suffix}" 136 | return [task_id, "fail","Unsupport file."] 137 | end 138 | 139 | # Download by url. 140 | deploy_file_name = _file_name_prefix + ".tar.gz" 141 | down_file_result = _down_file(tmp_file_path, url, deploy_file_name) 142 | if down_file_result[1] == "fail" 143 | return down_file_result 144 | else 145 | deploy_file = down_file_result[2] 146 | end 147 | 148 | # Unzip the package 149 | unzip_result = _do_unzip(deploy_file) 150 | if unzip_result[1] == "fail" 151 | return unzip_result 152 | else 153 | thor_data_path = unzip_result[2] 154 | end 155 | 156 | # Execute the thor script. 157 | ex_thor_result = _execute_thor(thor_data_path) 158 | return ex_thor_result 159 | 160 | end 161 | 162 | # Use this method, wen can start a deploy task. 163 | def _startDeploy(task_id, port, path, host_ip) 164 | 165 | begin 166 | 167 | Logger.info "[ Detach task #{task_id} ] Start......" 168 | 169 | # Make sure only one task runs at a time. 170 | lckself = File.new __FILE__ 171 | unless lckself.flock File::LOCK_EX | File::LOCK_NB 172 | Logger.fatal "[ Detach task #{task_id} ] A task was running. Get LOCK #{__FILE__} failed,exit." 173 | return 1 174 | end 175 | 176 | Logger.info "[ Detach task #{task_id} ] Get LOCK #{__FILE__} succ. Start _do_deploy function." 177 | 178 | # Execute the deploy. 179 | result = _do_deploy(task_id, port, path, host_ip) 180 | return 0 181 | 182 | rescue => e 183 | Logger.fatal "[ Detach task #{task_id} ] Deploy #{task_id} exception : #{e} , exit." 184 | return 1 185 | 186 | ensure 187 | lckself.flock File::LOCK_UN 188 | 189 | Logger.info "[ Detach task #{task_id} ] Deploy finish. Result : #{result}\n#{LOG_SYMBOL*20} ENDTASK : #{task_id} #{LOG_SYMBOL*20}" 190 | 191 | end 192 | 193 | end 194 | 195 | # The RPC method 196 | def startDeploy(port, path) 197 | 198 | begin 199 | 200 | host_ip = request.ip 201 | 202 | # Create task id 203 | _part1 = _mk_id_part_hostname 204 | _part2 = _random_str 205 | _time_stamp = Time.now.strftime("%s") 206 | task_id = _part1 + _part2 + _time_stamp 207 | 208 | Logger.info "Task start.\n#{LOG_SYMBOL*20} task_id : #{task_id} #{LOG_SYMBOL*20}" 209 | 210 | if not FileTest::exists?(DEPLOY_LCK) 211 | cmd = "touch %s" %[DEPLOY_LCK] 212 | Logger.info "Touch file #{DEPLOY_LCK}" 213 | system(cmd) 214 | end 215 | 216 | # Make sure only one task runs at a time. 217 | dplck = File.new DEPLOY_LCK 218 | unless dplck.flock File::LOCK_EX | File::LOCK_NB 219 | Logger.fatal "A task was running. Get the LOCK #{DEPLOY_LCK} failed, exit." 220 | return [task_id, "fail", "A task was running"] 221 | end 222 | 223 | # Two flock. 224 | lckself = File.new __FILE__ 225 | unless lckself.flock File::LOCK_EX | File::LOCK_NB 226 | Logger.fatal "A task was running. Get the LOCK #{__FILE__} failed, exit." 227 | return [task_id, "fail", "A task was running"] 228 | end 229 | lckself.flock File::LOCK_UN 230 | 231 | # Write the current task_id to the lock file 232 | File.open(DEPLOY_LCK, "w") do |f| 233 | Logger.info "Write the task_id : #{task_id} to the lock file." 234 | f.puts task_id 235 | end 236 | 237 | # Touch thor log first. 238 | thor_log_path = LOG_PATH + "thor_" + task_id + ".log" 239 | `touch #{thor_log_path}` 240 | 241 | # Do deploy, detach the deploy process. 242 | read, write = IO.pipe 243 | pid = fork { _startDeploy(task_id, port, path, host_ip) } 244 | Process.detach(pid) 245 | write.close 246 | read.close 247 | Logger.info "Detach task #{task_id} succ. Pid : #{pid}" 248 | 249 | rescue => e 250 | Logger.fatal "Deploy #{task_id} exception : #{e}" 251 | return [task_id, "fail", "Exception : #{e}"] 252 | 253 | ensure 254 | dplck.flock File::LOCK_UN 255 | Logger.info "Release lock file #{DEPLOY_LCK}.\n" 256 | end 257 | 258 | return [task_id, "succ", "hahaha"] 259 | 260 | end 261 | 262 | # Parser the log, get the deploy useful info. 263 | def _get_deploy_info_from_log(log_path) 264 | 265 | info_list = [] 266 | re = /\[{1}\@(.*?)\]{1}(.*)/ 267 | 268 | if FileTest::zero?(log_path) 269 | sleep(2) 270 | end 271 | 272 | if FileTest::zero?(log_path) 273 | info_list = ["[@start:init]"] 274 | end 275 | 276 | File.open(log_path).grep(/^\[\@/) do |line| 277 | md = re.match(line) 278 | list_s = [] 279 | # The header, split it by : 280 | header = md[1].strip.chomp.chomp(",").delete(" ") 281 | list_s << header.split(":") 282 | # The content 283 | if md[2].strip.length > 0 284 | list_s << md[2].strip.chomp 285 | end 286 | info_list << list_s 287 | end 288 | 289 | return info_list 290 | 291 | end 292 | 293 | # Get deploy status from info. 294 | def _get_task_deploy_status_from_info(info) 295 | 296 | # If check env was not pass. 297 | for i in info 298 | return [["jobs", -2, "thor deploy check_env failed!"]] if i[0] == ["fail_mod", "check_env"] 299 | end 300 | 301 | mods = [] 302 | # Collect result 303 | result_d = {} 304 | for i in info 305 | mods = i[0][1].split(",") if i[0][0] == "jobs" 306 | if mods 307 | result_d["jobs"] = mods 308 | for mod in mods 309 | result_d[mod] = i[0][0] if i[0][1] == mod 310 | end 311 | end 312 | end 313 | 314 | mods.each {|element| result_d[element] = "waiting" if not result_d.has_key?(element)} 315 | 316 | # -1 means no mod detail presents. 317 | result_l = [["jobs", mods.length == 0 ? -1 : mods.length]] 318 | # result_l finally like : 319 | # [['jobs', 3], ['xbox', 'succ']] 320 | for mod in mods 321 | _status = result_d[mod] 322 | if _status == "start_mod" 323 | result_l << [mod,"start"] 324 | elsif _status == "deploying" 325 | result_l << [mod,"deploying"] 326 | elsif _status == "succ_mod" 327 | result_l << [mod,"succ"] 328 | elsif _status == "fail_mod" 329 | result_l << [mod,"fail"] 330 | else 331 | result_l << [mod,"waiting"] 332 | end 333 | end 334 | return result_l 335 | end 336 | 337 | # Get a mod`s deploy detail log. 338 | def _get_mod_deploy_log(info, mod_name) 339 | result_list = [[mod_name]] 340 | for i in info 341 | result_list << i[1] if i[0][1] == mod_name and i[0][0] == "deploying" 342 | end 343 | return result_list 344 | end 345 | 346 | # Get a task`s deploy status. 347 | def getDeployTaskStatus(task_id) 348 | 349 | Logger.info "\n#{LOG_SYMBOL*29} getDeployTaskStatus #{LOG_SYMBOL*28}" 350 | 351 | thor_log_path = LOG_PATH + "thor_" + task_id + ".log" 352 | log_lines = `wc -l #{thor_log_path}|awk '{print $1}' `.chomp.to_s 353 | 354 | if not FileTest::exists?(thor_log_path) 355 | Logger.fatal "The thor log #{thor_log_path} not exist.\n#{LOG_SYMBOL*27} getDeployTaskStatus END #{LOG_SYMBOL*26}" 356 | return [["jobs", -2, "The thor log not exist."]] 357 | end 358 | 359 | begin 360 | info = _get_deploy_info_from_log(thor_log_path) 361 | result_list = _get_task_deploy_status_from_info(info) 362 | Logger.info "result_list : #{result_list}" 363 | rescue => e 364 | Logger.fatal "Get #{task_id} task status fail. Exception : #{e}" 365 | return [["jobs", -2, "Exception : #{e}"]] 366 | end 367 | 368 | Logger.info "Get #{task_id} task status succ. Log lines : #{log_lines}. Result : #{result_list} \n#{LOG_SYMBOL*27} getDeployLog4Mod END #{LOG_SYMBOL*26}" 369 | return result_list 370 | 371 | end 372 | 373 | # Get a deploy task of mod`s detail log. 374 | def getDeployLog4Mod(task_id, mod_name) 375 | 376 | Logger.info "\n#{LOG_SYMBOL*28} getDeployLog4Mod #{LOG_SYMBOL*28}" 377 | 378 | thor_log_path = LOG_PATH + "thor_" + task_id + ".log" 379 | if not FileTest::exists?(thor_log_path) 380 | Logger.fatal "Get #{task_id} task for mod #{mod_name} fail. The thor log #{thor_log_path} was not exists." 381 | return [["jobs", -2, "The thor log not exist."]] 382 | end 383 | 384 | begin 385 | info = _get_deploy_info_from_log(thor_log_path) 386 | result_list = _get_mod_deploy_log(info, mod_name) 387 | rescue => e 388 | Logger.fatal "Get #{task_id} task for mod #{mod_name} fail. Exception : #{e}" 389 | return [["jobs", -2, "Exception : #{e}"]] 390 | end 391 | 392 | Logger.info "Get #{task_id} task for mod #{mod_name} succ. Result : #{result_list} \n#{LOG_SYMBOL*26} getDeployLog4Mod END #{LOG_SYMBOL*26}" 393 | return result_list 394 | 395 | end 396 | 397 | # Get the global deploy status. 398 | def getDeployStatus() 399 | r_notask = [["jobs", 0, "No task running."]] 400 | 401 | lckself = File.new __FILE__ 402 | if lckself.flock File::LOCK_EX | File::LOCK_NB 403 | lckself.flock File::LOCK_UN 404 | 405 | Logger.info "\n#{LOG_SYMBOL*31} getDeployStatus #{LOG_SYMBOL*30} \nGet global deploy status succ. Get lock and release LOCK #{__FILE__} succ. Result : #{r_notask}" 406 | return r_notask 407 | 408 | else 409 | task_id = File.readlines(DEPLOY_LCK)[0].strip 410 | result_list = getDeployTaskStatus(task_id) 411 | 412 | Logger.info "Get global deploy status succ. Get LOCK #{__FILE__} fail. Result : #{result_list}" 413 | 414 | return result_list 415 | end 416 | Logger.info "\n#{LOG_SYMBOL*29} getDeployStatus END #{LOG_SYMBOL*28}" 417 | end 418 | 419 | # Get a task`s full log. 420 | def getDeployLog( task_id) 421 | 422 | Logger.info "\n#{LOG_SYMBOL*32} getDeployLog #{LOG_SYMBOL*32}" 423 | 424 | thor_log_path = LOG_PATH + "thor_" + task_id + ".log" 425 | if not FileTest::exists?(thor_log_path) 426 | Logger.fatal "Cannot find the log file #{thor_log_path}" 427 | return [["jobs", -2, "The thor log not exist."]] 428 | end 429 | 430 | begin 431 | fulllog = File.readlines(thor_log_path) 432 | rescue => e 433 | Logger.fatal "Read log #{thor_log_path}, exception : #{e}" 434 | return [["jobs", -2, "Exception : #{e}"]] 435 | end 436 | 437 | log_lines = `wc -l #{thor_log_path}|awk '{print $1}' `.chomp.to_s 438 | Logger.info "Get deploy log succ. Log lines : #{log_lines}. Result :\n #{fulllog}" 439 | Logger.info "\n#{LOG_SYMBOL*30} getDeployLog END #{LOG_SYMBOL*30}" 440 | 441 | return fulllog 442 | 443 | end 444 | 445 | end # end of Deploy module 446 | end # end of RPC module 447 | end # end of Frigga module 448 | -------------------------------------------------------------------------------- /static/vendor/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);if(!n.options.delay||!n.options.delay.show)return n.show();clearTimeout(this.timeout),n.hoverState="in",this.timeout=setTimeout(function(){n.hoverState=="in"&&n.show()},n.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); -------------------------------------------------------------------------------- /views/static/vendor/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);if(!n.options.delay||!n.options.delay.show)return n.show();clearTimeout(this.timeout),n.hoverState="in",this.timeout=setTimeout(function(){n.hoverState=="in"&&n.show()},n.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

    '}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); --------------------------------------------------------------------------------