├── .dockerignore ├── .gitattributes ├── .gitignore ├── .golangci.yml ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── alert ├── alert.go ├── pushbullet.go └── slack.go ├── config.go ├── config_test.go ├── example.yaml ├── go.mod ├── go.sum ├── main.go ├── manager.go ├── probe ├── dns.go ├── http.go ├── http_test.go ├── minecraft.go ├── port.go ├── probe.go └── smtp.go ├── screenshot.png └── static ├── css ├── font-awesome.min.css └── main.css ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff └── fontawesome-webfont.woff2 ├── index.html └── js ├── angular.min.js └── controller.js /.dockerignore: -------------------------------------------------------------------------------- 1 | README.md 2 | .travis.yaml 3 | .gitignore 4 | .gitattributes 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | config.sample linguist-language=Go 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /board 2 | /board.yaml 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | govet: 3 | check-shadowing: true 4 | 5 | linters: 6 | enable-all: true 7 | disable: 8 | - gomnd 9 | - gochecknoinits 10 | - gochecknoglobals 11 | 12 | issues: 13 | max-per-linter: 0 14 | max-same: 0 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.12.x 5 | 6 | install: true 7 | 8 | git: 9 | depth: 1 10 | 11 | env: 12 | - GO111MODULE=on 13 | 14 | before_script: 15 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 16 | 17 | script: 18 | - golangci-lint run # Run a bunch of code checkers/linters in parallel 19 | - go test -v -race ./... # Run all the tests with the race detector enabled 20 | 21 | notifications: 22 | email: false 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS build-env 2 | 3 | # Dependencies 4 | WORKDIR /build 5 | ENV GO111MODULE=on 6 | RUN go get github.com/GeertJohan/go.rice/rice 7 | COPY go.mod go.sum ./ 8 | RUN go mod download 9 | 10 | # Build 11 | COPY . ./ 12 | RUN CGO_ENABLED=0 go build -ldflags '-w -s' -o /board 13 | RUN rice append --exec /board 14 | 15 | # Build runtime container 16 | FROM alpine 17 | WORKDIR /app 18 | COPY --from=build-env /board /app/board 19 | EXPOSE 8080 20 | CMD ["/app/board"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Board [![Build Status](https://travis-ci.org/Lesterpig/board.svg?branch=master)](https://travis-ci.org/Lesterpig/board) 2 | ======================================================================================================================= 3 | 4 | > One dashboard to check them all. 5 | 6 | This repository contains a small web server used to provide a very accurate status *for all your systems*. It also supports live alerts when a service goes down. 7 | 8 | With just a glance, you'll be able to spot the faulty parts of your infrastructure. 9 | 10 | ![Screenshot](screenshot.png "Screenshot") 11 | 12 | Installation 13 | ------------ 14 | 15 | ``` 16 | go get github.com/Lesterpig/board 17 | cp example.yaml board.yaml 18 | vim board.yaml 19 | ``` 20 | 21 | Docker Build 22 | ------------ 23 | After installation the board can be build as a docker image. 24 | 25 | ```bash 26 | # Building image 27 | docker build -t lesterpig/board . 28 | # Running container with board.yaml from current dir 29 | docker run -p 8080:8080 -v ${PWD}/board.yaml:/app/board.yaml lesterpig/board 30 | ``` 31 | 32 | Build single binary 33 | ------------------- 34 | 35 | You may want to include the `/static/` directory as a ZIP resource in your binary. 36 | 37 | ``` 38 | go build -ldflags "-s -w" . 39 | rice append --exec board 40 | ``` 41 | 42 | Documentation: https://github.com/GeertJohan/go.rice#append 43 | -------------------------------------------------------------------------------- /alert/alert.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import ( 4 | "github.com/Lesterpig/board/probe" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | var log = logrus.StandardLogger() 9 | 10 | // Alerter is the interface all alerters should implement. 11 | type Alerter interface { 12 | Alert(status probe.Status, category, serviceName, message, link, date string) 13 | } 14 | -------------------------------------------------------------------------------- /alert/pushbullet.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/Lesterpig/board/probe" 11 | ) 12 | 13 | // Pushbullet alert container. 14 | type Pushbullet struct { 15 | client *http.Client 16 | token string 17 | } 18 | 19 | // NewPushbullet returns a Pushbullet alerter from the private token 20 | // available in the `account` page. 21 | func NewPushbullet(token string) *Pushbullet { 22 | return &Pushbullet{ 23 | client: &http.Client{}, 24 | token: token, 25 | } 26 | } 27 | 28 | // Alert sends a pushbullet note to the owner of the provided token. 29 | func (p *Pushbullet) Alert(status probe.Status, category, serviceName, message, link, date string) { 30 | u, _ := url.Parse("https://api.pushbullet.com/v2/pushes") 31 | r := strings.NewReader(`{ 32 | "title": "` + strings.Replace(fmt.Sprintf("%s %s", serviceName, status), "\"", "\\\"", -1) + `", 33 | "body": "` + strings.Replace(fmt.Sprintf("%s (%s)", message, date), "\"", "\\\"", -1) + `", 34 | "type": "note" 35 | }`) 36 | 37 | res, err := p.client.Do(&http.Request{ 38 | Method: "POST", 39 | URL: u, 40 | Header: map[string][]string{ 41 | "Access-Token": {p.token}, 42 | "Content-Type": {"application/json"}, 43 | }, 44 | Body: ioutil.NopCloser(r), 45 | }) 46 | 47 | if err == nil { 48 | _ = res.Body.Close() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /alert/slack.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/Lesterpig/board/probe" 12 | ) 13 | 14 | // Slack alert container. 15 | type Slack struct { 16 | client *http.Client 17 | webhookURL string 18 | channel string 19 | httpTimeout time.Duration 20 | } 21 | 22 | // NewSlack returns a Slack alerter from the webhookURL 23 | func NewSlack(webhookURL, channel string) *Slack { 24 | return &Slack{ 25 | client: &http.Client{}, 26 | webhookURL: webhookURL, 27 | channel: channel, 28 | httpTimeout: time.Duration(10) * time.Second, 29 | } 30 | } 31 | 32 | // Alert sends a pushbullet note to the owner of the provided token. 33 | func (p *Slack) Alert(status probe.Status, category, title, message, link, date string) { 34 | color := "#ff0000" 35 | if status == probe.StatusOK { 36 | color = "#00ff00" 37 | } 38 | 39 | slackMessage := SlackPostMessage{ 40 | Channel: p.channel, 41 | Alias: category, 42 | Attachments: []Attachment{ 43 | { 44 | Color: color, 45 | Fields: []AttachmentField{ 46 | { 47 | Short: false, 48 | Title: fmt.Sprintf("%s is %s", title, status), 49 | Value: fmt.Sprintf("[%s](%s) - Response: %s at (%s)", title, link, message, date), 50 | }, 51 | }, 52 | }, 53 | }, 54 | } 55 | 56 | slackBody, _ := json.Marshal(slackMessage) 57 | req, err := http.NewRequest(http.MethodPost, p.webhookURL, bytes.NewBuffer(slackBody)) 58 | 59 | if err != nil { 60 | return 61 | } 62 | 63 | req.Header.Add("Content-Type", "application/json") 64 | 65 | client := &http.Client{Timeout: p.httpTimeout} 66 | 67 | resp, err := client.Do(req) 68 | if err != nil { 69 | log.Errorf("Error sending request to Slack: %s ", err) 70 | return 71 | } 72 | defer resp.Body.Close() 73 | 74 | bytes, err := ioutil.ReadAll(resp.Body) 75 | 76 | if err != nil { 77 | log.Errorf("Error response from Slack: %s ", err) 78 | return 79 | } 80 | 81 | if string(bytes) != "{\"success\":true}" { 82 | log.Errorf("Non-ok response returned from Slack: %s ", string(bytes)) 83 | return 84 | } 85 | 86 | log.Info("Alert sendt") 87 | } 88 | 89 | // All models with inspiration from https://github.com/RocketChat/Rocket.Chat.Go.SDK/blob/master/models/message.go 90 | 91 | // SlackPostMessage is the main model for sending messages 92 | type SlackPostMessage struct { 93 | RoomID string `json:"roomId,omitempty"` 94 | Channel string `json:"channel,omitempty"` 95 | Text string `json:"text,omitempty"` 96 | ParseUrls bool `json:"parseUrls,omitempty"` 97 | Alias string `json:"alias,omitempty"` 98 | Emoji string `json:"emoji,omitempty"` 99 | Avatar string `json:"avatar,omitempty"` 100 | Attachments []Attachment `json:"attachments,omitempty"` 101 | } 102 | 103 | // Attachment Payload for postmessage rest API 104 | // 105 | // https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ 106 | type Attachment struct { 107 | Color string `json:"color,omitempty"` 108 | Fields []AttachmentField `json:"fields,omitempty"` 109 | } 110 | 111 | // AttachmentField Payload for postmessage rest API 112 | // 113 | // https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ 114 | type AttachmentField struct { 115 | Short bool `json:"short"` 116 | Title string `json:"title,omitempty"` 117 | Value string `json:"value,omitempty"` 118 | } 119 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "path/filepath" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Lesterpig/board/alert" 10 | "github.com/Lesterpig/board/probe" 11 | 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | type serviceConfig struct { 16 | probe.Config 17 | Name string 18 | Category string 19 | Probe string 20 | } 21 | 22 | type alertConfig struct { 23 | Type string 24 | Token string 25 | Webhook string 26 | Channel string 27 | } 28 | 29 | var probeConstructors = map[string](func() probe.Prober){ 30 | "dns": func() probe.Prober { return &probe.DNS{} }, 31 | "http": func() probe.Prober { return &probe.HTTP{} }, 32 | "minecraft": func() probe.Prober { return &probe.Minecraft{} }, 33 | "port": func() probe.Prober { return &probe.Port{} }, 34 | "smtp": func() probe.Prober { return &probe.SMTP{} }, 35 | } 36 | 37 | var alertConstructors = map[string](func(c alertConfig) alert.Alerter){ 38 | "pushbullet": func(c alertConfig) alert.Alerter { 39 | return alert.NewPushbullet(c.Token) 40 | }, 41 | "slack": func(c alertConfig) alert.Alerter { 42 | return alert.NewSlack(c.Webhook, c.Channel) 43 | }, 44 | } 45 | 46 | var alerters []alert.Alerter 47 | 48 | func parseConfigString(cnf string) (dir string, name string) { 49 | dir = filepath.Dir(cnf) 50 | basename := filepath.Base(cnf) 51 | name = strings.TrimSuffix(basename, filepath.Ext(basename)) 52 | 53 | return 54 | } 55 | 56 | func loadConfig(configPath, configName string) (*Manager, error) { 57 | viper.SetConfigName(configName) 58 | viper.AddConfigPath(configPath) 59 | 60 | err := viper.ReadInConfig() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | sc := make([]serviceConfig, 0) 66 | 67 | err = viper.UnmarshalKey("services", &sc) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | manager := Manager{} 73 | manager.Services = make(map[string]([]*Service)) 74 | 75 | for _, c := range sc { 76 | constructor := probeConstructors[c.Probe] 77 | if constructor == nil { 78 | return nil, errors.New("unknown probe type: " + c.Probe) 79 | } 80 | 81 | c.Config = setProbeConfigDefaults(c.Config) 82 | 83 | prober := constructor() 84 | 85 | err = prober.Init(c.Config) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | manager.Services[c.Category] = append(manager.Services[c.Category], &Service{ 91 | Prober: prober, 92 | Name: c.Name, 93 | Target: c.Target, 94 | }) 95 | } 96 | 97 | ac := make([]alertConfig, 0) 98 | 99 | err = viper.UnmarshalKey("alerts", &ac) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | alerters = make([]alert.Alerter, 0) 105 | 106 | for _, c := range ac { 107 | constructor := alertConstructors[c.Type] 108 | if constructor == nil { 109 | return nil, errors.New("unknown alert type: " + c.Type) 110 | } 111 | 112 | alerters = append(alerters, constructor(c)) 113 | } 114 | 115 | return &manager, err 116 | } 117 | 118 | func setProbeConfigDefaults(c probe.Config) probe.Config { 119 | if c.Warning == 0 { 120 | c.Warning = 500 * time.Millisecond 121 | } 122 | 123 | if c.Fatal == 0 { 124 | c.Fatal = time.Minute 125 | } 126 | 127 | return c 128 | } 129 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_parseConfigString(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | args string 9 | wantDir string 10 | wantName string 11 | }{ 12 | { 13 | args: "/etc/config/board.yaml", 14 | wantDir: "/etc/config", 15 | wantName: "board", 16 | }, 17 | { 18 | args: "../config/conf.yaml", 19 | wantDir: "../config", 20 | wantName: "conf", 21 | }, 22 | } 23 | for _, tt := range tests { 24 | tt := tt 25 | t.Run(tt.args, func(t *testing.T) { 26 | gotDir, gotName := parseConfigString(tt.args) 27 | if gotDir != tt.wantDir { 28 | t.Errorf("parseConfigString() gotDir = %v, want %v", gotDir, tt.wantDir) 29 | } 30 | if gotName != tt.wantName { 31 | t.Errorf("parseConfigString() gotName = %v, want %v", gotName, tt.wantName) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - name: Lesterpig.com 3 | category: Web 4 | probe: http 5 | config: 6 | target: https://www.lesterpig.com 7 | options: 8 | regex: Loïck Bonniot 9 | - name: SMTP 10 | category: Services 11 | probe: smtp 12 | config: 13 | warning: 10s 14 | fatal: 15s 15 | target: mail.google.com:465 16 | # ... 17 | 18 | alerts: 19 | - type: pushbullet 20 | token: 21 | # ... 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Lesterpig/board 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/GeertJohan/go.rice v1.0.0 7 | github.com/gobuffalo/envy v1.8.1 8 | github.com/miekg/dns v1.1.13 9 | github.com/mitchellh/mapstructure v1.1.2 10 | github.com/sirupsen/logrus v1.2.0 11 | github.com/spf13/viper v1.4.0 12 | github.com/stretchr/testify v1.4.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= 5 | github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= 6 | github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= 7 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 8 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= 9 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 10 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 11 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 13 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 14 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 15 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 16 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 17 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 18 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 19 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 20 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 21 | github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= 22 | github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 27 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 28 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 29 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 30 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 31 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 32 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 33 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 34 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 35 | github.com/gobuffalo/envy v1.8.1 h1:RUr68liRvs0TS1D5qdW3mQv2SjAsu1QWMCx1tG4kDjs= 36 | github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= 37 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 38 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 39 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 40 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 41 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 42 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 45 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 46 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 47 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 48 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 49 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 50 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 51 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 52 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 53 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 54 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 55 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 56 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 57 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 58 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 59 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 60 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 61 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 62 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 63 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 64 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 65 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 66 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 67 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 68 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 69 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 70 | github.com/miekg/dns v1.1.13 h1:x7DQtkU0cedzeS8TD36tT/w1Hm4rDtfCaYYAHE7TTBI= 71 | github.com/miekg/dns v1.1.13/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 72 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 73 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 74 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 75 | github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= 76 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 77 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 78 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 79 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 83 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 84 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 85 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 86 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 87 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 88 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 89 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 90 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 91 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 92 | github.com/rogpeppe/go-internal v1.3.2 h1:XU784Pr0wdahMY2bYcyK6N1KuaRAdLtqD4qd8D18Bfs= 93 | github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 94 | github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= 95 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 96 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 97 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 98 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 99 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 100 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 101 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 102 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 103 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 104 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 105 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 106 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= 107 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 108 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 109 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 110 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 111 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 112 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 113 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 114 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 115 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 116 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 117 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 118 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 119 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 120 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 121 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 122 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 123 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 124 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 125 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 126 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 127 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 128 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 129 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 130 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 131 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 132 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= 133 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 134 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 135 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 138 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 142 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 144 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 146 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 147 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 148 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 149 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 150 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 151 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 152 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 153 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 154 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 155 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 156 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 157 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 158 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 159 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 160 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 161 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 162 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 163 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 164 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 165 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 166 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "time" 11 | 12 | rice "github.com/GeertJohan/go.rice" 13 | "github.com/gobuffalo/envy" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var port = flag.Int("p", 8080, "Port to use") 18 | var intervalCli = flag.Int("i", 0, "Interval in minutes") 19 | var configPath = flag.String("f", "./board.yaml", "Path to config file") 20 | 21 | var log = logrus.StandardLogger() 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | interval := getInterval() 27 | 28 | log.Infof("Probe interval: %d", interval) 29 | 30 | manager, err := loadConfig(parseConfigString(*configPath)) 31 | if err != nil { 32 | fmt.Fprintln(os.Stderr, err) 33 | os.Exit(1) 34 | } 35 | 36 | // Setup static folder 37 | http.Handle("/", http.FileServer(rice.MustFindBox("static").HTTPBox())) 38 | 39 | // Setup logic route 40 | http.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) { 41 | w.Header().Set("Content-Type", "application/json") 42 | data, _ := json.Marshal(manager) 43 | _, _ = w.Write(data) 44 | }) 45 | 46 | go manager.ProbeLoop(time.Duration(int64(interval)) * time.Minute) 47 | log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*port), nil)) 48 | } 49 | 50 | func getInterval() int { 51 | intervalEnv := getInt(envy.Get("INTERVAL", "")) 52 | 53 | if *intervalCli != 0 { 54 | return *intervalCli 55 | } else if intervalEnv != 0 { 56 | return intervalEnv 57 | } 58 | 59 | return 10 60 | } 61 | 62 | func getInt(s string) int { 63 | i, err := strconv.ParseInt(s, 10, 0) 64 | if nil != err { 65 | return 0 66 | } 67 | 68 | return int(i) 69 | } 70 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Lesterpig/board/probe" 7 | ) 8 | 9 | // Service stores several information from a service, especially its last status. 10 | type Service struct { 11 | Prober probe.Prober `json:"-"` 12 | Name string 13 | Status probe.Status 14 | Message string 15 | Target string 16 | } 17 | 18 | // Manager stores several services sorted by categories. 19 | type Manager struct { 20 | LastUpdate time.Time 21 | Services map[string]([]*Service) 22 | } 23 | 24 | // ProbeLoop starts the main loop that will call ProbeAll regularly. 25 | func (manager *Manager) ProbeLoop(interval time.Duration) { 26 | manager.ProbeAll() 27 | 28 | c := time.Tick(interval) 29 | for range c { 30 | manager.ProbeAll() 31 | } 32 | } 33 | 34 | // ProbeAll triggers the Probe function for each registered service in the manager. 35 | // Everything is done asynchronously. 36 | func (manager *Manager) ProbeAll() { 37 | log.Debug("Probing all") 38 | 39 | manager.LastUpdate = time.Now() 40 | 41 | for category, services := range manager.Services { 42 | for _, service := range services { 43 | go func(category string, service *Service) { 44 | prevStatus := service.Status 45 | service.Status, service.Message = service.Prober.Probe() 46 | 47 | if prevStatus != service.Status { 48 | if service.Status == probe.StatusError { 49 | AlertAll(category, service) 50 | } else if prevStatus == probe.StatusError { 51 | AlertAll(category, service) 52 | } 53 | } 54 | }(category, service) 55 | } 56 | } 57 | } 58 | 59 | // AlertAll sends an alert signaling the provided service is DOWN. 60 | // It uses global configuration for list of alert (`A` variable). 61 | func AlertAll(category string, service *Service) { 62 | date := time.Now().Format("15:04:05 MST") 63 | 64 | for _, alert := range alerters { 65 | alert.Alert(service.Status, category, service.Name, service.Message, service.Target, date) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /probe/dns.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "github.com/mitchellh/mapstructure" 6 | ) 7 | 8 | // DNS Probe, used to check whether a DNS server is answering. 9 | // `domain` will be resolved through a lookup for an A record. 10 | // `expected` should be the first returned IPv4 address or empty to accept any IP address. 11 | type DNS struct { 12 | Config 13 | domain, expected string 14 | } 15 | 16 | // Init configures the probe. 17 | func (d *DNS) Init(c Config) error { 18 | err := mapstructure.Decode(c.Options, d) 19 | d.Config = c 20 | 21 | return err 22 | } 23 | 24 | // Probe checks a DNS server. 25 | // If the operation succeeds, the message will be the duration of the dial in ms. 26 | // Otherwise, an error message is returned. 27 | func (d *DNS) Probe() (status Status, message string) { 28 | m := new(dns.Msg) 29 | m.SetQuestion(d.domain, dns.TypeA) 30 | 31 | c := new(dns.Client) 32 | r, rtt, err := c.Exchange(m, d.Target+":53") 33 | 34 | if err != nil { 35 | return StatusError, err.Error() 36 | } 37 | 38 | if r.Rcode != dns.RcodeSuccess { 39 | return StatusError, "Failed to resolve domain." 40 | } 41 | 42 | if answer, ok := r.Answer[0].(*dns.A); ok { 43 | if d.expected != "" && answer.A.String() != d.expected { 44 | return StatusError, "Unexpected DNS answer" 45 | } 46 | } else { 47 | return StatusError, "Failed to resolve domain." 48 | } 49 | 50 | return EvaluateDuration(rtt, d.Warning) 51 | } 52 | -------------------------------------------------------------------------------- /probe/http.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "crypto/tls" 5 | "io/ioutil" 6 | "net/http" 7 | "regexp" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/mitchellh/mapstructure" 12 | ) 13 | 14 | // HTTP Probe, used to check HTTP(S) websites status. 15 | type HTTP struct { 16 | Config 17 | client *http.Client 18 | regex *regexp.Regexp 19 | } 20 | 21 | // HTTPOptions is a structure containing optional parameters. 22 | // The `Regex` is used to check the content of the website, and can be empty. 23 | // Set `VerifyCertificate` to `false` to skip the TLS certificate verification. 24 | type HTTPOptions struct { 25 | Regex string 26 | VerifyCertificate bool 27 | } 28 | 29 | // Init configures the probe. 30 | func (h *HTTP) Init(c Config) error { 31 | h.Config = c 32 | 33 | var opts HTTPOptions 34 | 35 | err := mapstructure.Decode(c.Options, &opts) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | /* #nosec G402 */ 41 | tr := &http.Transport{ 42 | TLSClientConfig: &tls.Config{InsecureSkipVerify: !opts.VerifyCertificate}, 43 | } 44 | 45 | h.client = &http.Client{ 46 | Timeout: c.Fatal, 47 | Transport: tr, 48 | } 49 | h.regex, err = regexp.Compile(opts.Regex) 50 | 51 | return err 52 | } 53 | 54 | // Probe checks a website status. 55 | // If the operation succeeds, the message will be the duration of the HTTP request in ms. 56 | // Otherwise, an error message is returned. 57 | func (h *HTTP) Probe() (status Status, message string) { 58 | start := time.Now() 59 | res, err := h.client.Get(h.Target) 60 | duration := time.Since(start) 61 | 62 | if err != nil { 63 | return StatusError, defaultConnectErrorMsg 64 | } 65 | 66 | defer func() { _ = res.Body.Close() }() 67 | 68 | if res.StatusCode != 200 { 69 | return StatusError, strconv.Itoa(res.StatusCode) 70 | } 71 | 72 | body, _ := ioutil.ReadAll(res.Body) 73 | if h.regex != nil && !h.regex.Match(body) { 74 | return StatusError, "Unexpected result" 75 | } 76 | 77 | return EvaluateDuration(duration, h.Warning) 78 | } 79 | -------------------------------------------------------------------------------- /probe/http_test.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHTTPSuccess(t *testing.T) { 11 | h := &HTTP{} 12 | assert.Nil(t, h.Init(Config{ 13 | Target: "https://www.lesterpig.com", 14 | Warning: 5 * time.Second, 15 | Fatal: 10 * time.Second, 16 | Options: map[string]interface{}{"Regex": "Loïck"}, 17 | })) 18 | 19 | s, m := h.Probe() 20 | 21 | assert.True(t, StatusOK == s) 22 | t.Log(m) 23 | } 24 | 25 | func TestHTTPWarning(t *testing.T) { 26 | h := &HTTP{} 27 | assert.Nil(t, h.Init(Config{ 28 | Target: "https://www.lesterpig.com", 29 | Warning: time.Microsecond, 30 | Fatal: 10 * time.Second, 31 | })) 32 | 33 | s, _ := h.Probe() 34 | 35 | assert.True(t, StatusWarning == s) 36 | } 37 | 38 | func TestHTTP404(t *testing.T) { 39 | h := &HTTP{} 40 | assert.Nil(t, h.Init(Config{ 41 | Target: "https://www.lesterpig.com/404", 42 | Warning: 5 * time.Second, 43 | Fatal: 10 * time.Second, 44 | })) 45 | 46 | s, _ := h.Probe() 47 | 48 | assert.True(t, StatusError == s) 49 | } 50 | 51 | func TestHTTPError(t *testing.T) { 52 | h := &HTTP{} 53 | assert.Nil(t, h.Init(Config{ 54 | Target: "https://www.lesteerpig.com", 55 | Warning: 5 * time.Second, 56 | Fatal: 10 * time.Second, 57 | })) 58 | 59 | s, _ := h.Probe() 60 | 61 | assert.True(t, StatusError == s) 62 | } 63 | 64 | func TestHTTPTimeout(t *testing.T) { 65 | h := &HTTP{} 66 | assert.Nil(t, h.Init(Config{ 67 | Target: "https://www.lesterpig.com/", 68 | Warning: 5 * time.Second, 69 | Fatal: time.Microsecond, 70 | })) 71 | 72 | s, _ := h.Probe() 73 | 74 | assert.True(t, StatusError == s) 75 | } 76 | 77 | func TestHTTPUnexpected(t *testing.T) { 78 | h := &HTTP{} 79 | assert.Nil(t, h.Init(Config{ 80 | Target: "https://www.lesterpig.com", 81 | Warning: 5 * time.Second, 82 | Fatal: 10 * time.Second, 83 | Options: map[string]interface{}{"Regex": "Unexpected"}, 84 | })) 85 | 86 | s, m := h.Probe() 87 | 88 | assert.True(t, StatusError == s) 89 | assert.True(t, m == "Unexpected result") 90 | } 91 | -------------------------------------------------------------------------------- /probe/minecraft.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net" 8 | ) 9 | 10 | // Minecraft Probe, used to check minecraft servers status 11 | type Minecraft struct { 12 | Config 13 | } 14 | 15 | // Init configures the probe. 16 | func (m *Minecraft) Init(c Config) error { 17 | m.Config = c 18 | return nil 19 | } 20 | 21 | // Probe checks a minecraft server status. 22 | // If the operation succeeds, the message will contain the number of connected 23 | // and allowed players and the server version. 24 | // If there is no slot available for a new player, a warning will be triggered. 25 | // Otherwise, an error message is returned. 26 | func (m *Minecraft) Probe() (status Status, message string) { 27 | conn, err := net.DialTimeout("tcp", m.Target, m.Fatal) 28 | if err != nil { 29 | return StatusError, defaultConnectErrorMsg 30 | } 31 | 32 | defer func() { _ = conn.Close() }() 33 | 34 | // Handshake 35 | handshake := []byte{ 36 | 0x06, // Length 37 | 0x00, // PacketID 38 | 0x13, // Protocol version varint (109) 39 | 0x00, // String length of server name, not used 40 | 0x00, // Unsigned-short port used, not used 41 | 0x00, 42 | 0x01, // Ask for status 43 | } 44 | 45 | _, err = conn.Write(handshake) 46 | if err != nil { 47 | return StatusError, "Error during handshake" 48 | } 49 | 50 | // Status 51 | stat := []byte{0x01, 0x00} 52 | 53 | _, err = conn.Write(stat) 54 | if err != nil { 55 | return StatusError, "Error during status" 56 | } 57 | 58 | // Result 59 | _ = readVarInt(conn) // Packet length 60 | _ = readVarInt(conn) // PacketID 61 | data := make([]byte, 10000) 62 | 63 | read, err := conn.Read(data) 64 | if err != nil || read < 2 { 65 | return StatusError, "No stat received" 66 | } 67 | 68 | // Try to parse data 69 | result := new(minecraftServerStats) 70 | 71 | err = json.Unmarshal(data[2:read], result) 72 | if err != nil { 73 | return StatusError, "Invalid stats" 74 | } 75 | 76 | message = fmt.Sprintf("%d / %d - %s", result.Players.Online, result.Players.Max, result.Version.Name) 77 | status = StatusOK 78 | 79 | if result.Players.Online == result.Players.Max { 80 | status = StatusWarning 81 | } 82 | 83 | return status, message 84 | } 85 | 86 | type minecraftServerStats struct { 87 | Version struct { 88 | Name string `json:"name"` 89 | } `json:"version"` 90 | Players struct { 91 | Max int `json:"max"` 92 | Online int `json:"online"` 93 | } `json:"players"` 94 | } 95 | 96 | func readVarInt(c io.Reader) (err error) { 97 | buf := []byte{0x00} 98 | res := 0 99 | 100 | for i := 0; i < 5; i++ { 101 | _, err := c.Read(buf) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | res |= int((buf[0] & 0x7F) << uint(i*7)) 107 | 108 | if buf[0]&0x80 == 0x00 { 109 | break 110 | } 111 | } 112 | 113 | _ = res 114 | 115 | return 116 | } 117 | -------------------------------------------------------------------------------- /probe/port.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | "time" 7 | ) 8 | 9 | // Port Probe, used to check if a port is open or not. 10 | type Port struct { 11 | Config 12 | network, addrport string 13 | } 14 | 15 | // Init configures the probe. 16 | func (p *Port) Init(c Config) error { 17 | p.Config = c 18 | 19 | u, err := url.Parse(p.Target) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | p.network = u.Scheme 25 | p.addrport = u.Host 26 | 27 | return nil 28 | } 29 | 30 | // Probe checks a port status 31 | // If the operation succeeds, the message will be the duration of the dial in ms. 32 | // Otherwise, an error message is returned. 33 | func (p *Port) Probe() (status Status, message string) { 34 | start := time.Now() 35 | 36 | conn, err := net.DialTimeout(p.network, p.addrport, p.Fatal) 37 | if err != nil { 38 | return StatusError, defaultConnectErrorMsg 39 | } 40 | 41 | _ = conn.Close() 42 | 43 | return EvaluateDuration(time.Since(start), p.Warning) 44 | } 45 | -------------------------------------------------------------------------------- /probe/probe.go: -------------------------------------------------------------------------------- 1 | // Package probe stores basic probes that are used to check services health 2 | package probe 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // Config holds probe configuration, submitted through Init methods. 10 | type Config struct { 11 | Target string 12 | Options map[string]interface{} 13 | 14 | Warning time.Duration 15 | Fatal time.Duration 16 | } 17 | 18 | // Status represents the current status of a monitored service. 19 | type Status string 20 | 21 | // These constants represent the different available statuses of a service. 22 | const ( 23 | StatusUnknown Status = "" 24 | StatusWarning Status = "WARNING" 25 | StatusError Status = "ERROR" 26 | StatusOK Status = "OK" 27 | ) 28 | 29 | const defaultConnectErrorMsg = "Unable to connect" 30 | 31 | // Prober is the base interface that each probe must implement. 32 | type Prober interface { 33 | Init(Config) error 34 | Probe() (status Status, message string) 35 | } 36 | 37 | // EvaluateDuration is a shortcut for warning duration checks. 38 | // It returns a message containing the duration, and a OK or a WARNING status 39 | // depending on the provided warning duration. 40 | func EvaluateDuration(duration time.Duration, warning time.Duration) (status Status, message string) { 41 | if duration >= warning { 42 | status = StatusWarning 43 | } else { 44 | status = StatusOK 45 | } 46 | 47 | message = fmt.Sprintf("%d ms", duration.Nanoseconds()/1000000) 48 | 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /probe/smtp.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "time" 8 | ) 9 | 10 | // SMTP Probe, used to check smtp servers status 11 | // BEWARE! Only full TLS servers are working with this probe. 12 | type SMTP struct { 13 | Config 14 | } 15 | 16 | // Init configures the probe. 17 | func (s *SMTP) Init(c Config) error { 18 | s.Config = c 19 | return nil 20 | } 21 | 22 | // Probe checks a mailbox status. 23 | // If the operation succeeds, the message will be the duration of the SMTP handshake in ms. 24 | // Otherwise, an error message is returned. 25 | func (s *SMTP) Probe() (status Status, message string) { 26 | start := time.Now() 27 | 28 | conn, err := net.DialTimeout("tcp", s.Target, s.Fatal) 29 | if err != nil { 30 | return StatusError, defaultConnectErrorMsg 31 | } 32 | 33 | defer func() { _ = conn.Close() }() 34 | 35 | host, _, _ := net.SplitHostPort(s.Target) 36 | secure := tls.Client(conn, &tls.Config{ 37 | ServerName: host, 38 | }) 39 | 40 | data := make([]byte, 4) 41 | 42 | _, err = secure.Read(data) 43 | if err != nil { 44 | return StatusError, "TLS Error" 45 | } 46 | 47 | if fmt.Sprintf("%s", data) != "220 " { 48 | return StatusError, "Unexpected reply" 49 | } 50 | 51 | return EvaluateDuration(time.Since(start), s.Warning) 52 | } 53 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/screenshot.png -------------------------------------------------------------------------------- /static/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.6.1 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.1');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.1') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.1') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.1') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.1') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.1#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} 5 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | /* General */ 2 | 3 | body { 4 | background-color: #192823; 5 | color: white; 6 | font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 7 | } 8 | 9 | h1 { 10 | margin: 15px; 11 | margin-top: 30px; 12 | } 13 | 14 | .service { 15 | margin-left: 20px; 16 | display: inline-block; 17 | position: relative; 18 | text-align: center; 19 | } 20 | 21 | .service p { 22 | color: #D0C6B1; 23 | } 24 | 25 | .service .info { 26 | display: block; 27 | position: absolute; 28 | width: 100%; 29 | bottom: 20px; 30 | z-index: 2; 31 | font-size: 0.8em; 32 | } 33 | 34 | .status { 35 | height: 140px; 36 | width: 150px; 37 | padding-top: 10px; 38 | } 39 | 40 | .status i { 41 | font-size: 100px; 42 | } 43 | 44 | /* Status boxes */ 45 | 46 | .status-OK { 47 | background-color: #218559; 48 | } 49 | 50 | .status-ERROR { 51 | background-color: #DD1E2F; 52 | animation: blink 3s linear infinite; 53 | } 54 | 55 | .status-WARNING { 56 | background-color: #EBB035; 57 | } 58 | 59 | .status-UNKNOWN { 60 | background-color: #D0C6B1; 61 | } 62 | 63 | 64 | .lastupdate { 65 | font-size: 100px; 66 | color: #204237; 67 | 68 | } 69 | /* Animations */ 70 | 71 | @keyframes blink { 72 | 50% { opacity: 0.2; } 73 | } 74 | 75 | /* Links */ 76 | /* unvisited link */ 77 | a:link { 78 | color: white; 79 | } 80 | 81 | /* visited link */ 82 | a:visited { 83 | color: white; 84 | } 85 | 86 | /* mouse over link */ 87 | a:hover { 88 | color: white; 89 | } 90 | 91 | /* selected link */ 92 | a:active { 93 | color: white; 94 | } 95 | -------------------------------------------------------------------------------- /static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Board 4 | 5 | 6 | 7 | 8 |
9 |

{{ category }}

10 | 11 |
12 |

{{ service.Name }}

13 | {{ service.Message }} 14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | Last update: {{ lastUpdate | date: 'yyyy-MM-dd - HH:mm' }} 35 |
36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /static/js/controller.js: -------------------------------------------------------------------------------- 1 | angular.module('boardApp', []) 2 | .controller('boardCtrl', function($scope, $interval, $http) { 3 | 4 | $scope.categories = {} 5 | $scope.lastUpdate = "" 6 | 7 | function pool() { 8 | $http.get('/data').success(function(data) { 9 | $scope.categories = data.Services 10 | $scope.lastUpdate = data.LastUpdate 11 | }) 12 | } 13 | 14 | pool() 15 | $interval(pool, 30000) 16 | 17 | }) 18 | --------------------------------------------------------------------------------