Merge pull request #791 from statping/dev

v0.90.64
pull/805/head
Hunter Long 2020-08-22 23:40:59 -07:00 committed by GitHub
commit 243b6f019f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 4443 additions and 610 deletions

View File

@ -42,7 +42,9 @@ jobs:
env: env:
VERSION: ${{ env.VERSION }} VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }} COMMIT: ${{ github.sha }}
run: make clean compile MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
run: make clean generate compile
- name: Upload Compiled Frontend (rice-box.go) - name: Upload Compiled Frontend (rice-box.go)
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
@ -98,8 +100,22 @@ jobs:
shell: bash shell: bash
- name: Set Linux Build Flags - name: Set Linux Build Flags
if: matrix.platform != 'darwin' if: matrix.platform == 'linux'
run: echo ::set-env name=BUILD_FLAGS::'-extldflags -static' run: |
echo ::set-env name=BUILD_FLAGS::'-extldflags -static'
echo ::set-env name=XGO_TAGS::'netgo,osusergo,linux,sqlite_omit_load_extension'
shell: bash
- name: Set Darwin Build Flags
if: matrix.platform == 'darwin'
run: echo ::set-env name=XGO_TAGS::'netgo,osusergo,darwin,sqlite_omit_load_extension'
shell: bash
- name: Set Windows Build Flags
if: matrix.platform == 'windows'
run: |
echo ::set-env name=BUILD_FLAGS::'-extldflags -static'
echo ::set-env name=XGO_TAGS::'netgo,osusergo,sqlite_omit_load_extension'
shell: bash shell: bash
- name: Build ${{ matrix.platform }}/${{ matrix.arch }} - name: Build ${{ matrix.platform }}/${{ matrix.arch }}
@ -117,6 +133,7 @@ jobs:
x: false x: false
pkg: cmd pkg: cmd
buildmode: pie buildmode: pie
tags: ${{ env.XGO_TAGS }}
ldflags: -s -w -X main.VERSION=${{ env.VERSION }} -X main.COMMIT=${{ env.COMMIT }} ${{ env.BUILD_FLAGS }} ldflags: -s -w -X main.VERSION=${{ env.VERSION }} -X main.COMMIT=${{ env.COMMIT }} ${{ env.BUILD_FLAGS }}
- name: Compress Linux Builds - name: Compress Linux Builds
@ -262,6 +279,10 @@ jobs:
TEST_EMAIL: ${{ secrets.TEST_EMAIL }} TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
GOTIFY_URL: ${{ secrets.GOTIFY_URL }} GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }} GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
SNS_TOKEN: ${{ secrets.SNS_TOKEN }}
SNS_SECRET: ${{ secrets.SNS_SECRET }}
SNS_REGION: ${{ secrets.SNS_REGION }}
SNS_TOPIC: ${{ secrets.SNS_TOPIC }}
- name: Coveralls Testing Coverage - name: Coveralls Testing Coverage
run: | run: |

View File

@ -42,7 +42,9 @@ jobs:
env: env:
VERSION: ${{ env.VERSION }} VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }} COMMIT: ${{ github.sha }}
run: make clean compile MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
run: make clean generate compile
- name: Upload Compiled Frontend (rice-box.go) - name: Upload Compiled Frontend (rice-box.go)
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
@ -98,8 +100,22 @@ jobs:
shell: bash shell: bash
- name: Set Linux Build Flags - name: Set Linux Build Flags
if: matrix.platform != 'darwin' if: matrix.platform == 'linux'
run: echo ::set-env name=BUILD_FLAGS::'-extldflags -static' run: |
echo ::set-env name=BUILD_FLAGS::'-extldflags -static'
echo ::set-env name=XGO_TAGS::'netgo,osusergo,linux,sqlite_omit_load_extension'
shell: bash
- name: Set Darwin Build Flags
if: matrix.platform == 'darwin'
run: echo ::set-env name=XGO_TAGS::'netgo,osusergo,darwin,sqlite_omit_load_extension'
shell: bash
- name: Set Windows Build Flags
if: matrix.platform == 'windows'
run: |
echo ::set-env name=BUILD_FLAGS::'-extldflags -static'
echo ::set-env name=XGO_TAGS::'netgo,osusergo,sqlite_omit_load_extension'
shell: bash shell: bash
- name: Build ${{ matrix.platform }}/${{ matrix.arch }} - name: Build ${{ matrix.platform }}/${{ matrix.arch }}
@ -117,6 +133,7 @@ jobs:
x: false x: false
pkg: cmd pkg: cmd
buildmode: pie buildmode: pie
tags: ${{ env.XGO_TAGS }}
ldflags: -s -w -X main.VERSION=${{ env.VERSION }} -X main.COMMIT=${{ env.COMMIT }} ${{ env.BUILD_FLAGS }} ldflags: -s -w -X main.VERSION=${{ env.VERSION }} -X main.COMMIT=${{ env.COMMIT }} ${{ env.BUILD_FLAGS }}
- name: Compress Linux Builds - name: Compress Linux Builds
@ -262,6 +279,10 @@ jobs:
TEST_EMAIL: ${{ secrets.TEST_EMAIL }} TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
GOTIFY_URL: ${{ secrets.GOTIFY_URL }} GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }} GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
SNS_TOKEN: ${{ secrets.SNS_TOKEN }}
SNS_SECRET: ${{ secrets.SNS_SECRET }}
SNS_REGION: ${{ secrets.SNS_REGION }}
SNS_TOPIC: ${{ secrets.SNS_TOPIC }}
- name: Coveralls Testing Coverage - name: Coveralls Testing Coverage
run: | run: |

1
.gitignore vendored
View File

@ -38,3 +38,4 @@ tmp
/frontend/cypress/screenshots/ /frontend/cypress/screenshots/
/frontend/cypress/videos/ /frontend/cypress/videos/
services.yml services.yml
statping.wiki

View File

@ -1,3 +1,17 @@
# 0.90.64 (08-18-2020)
- Modified max-width for container to 1012px, larger UI
- Added failure sparklines in the Services list view
- Added "Update Available" alert on the top of Settings if new version is available
- Added Version and Github Commit hash to left navigation on Settings page
- Added "reason" for failures (will be used for more custom notification messages) [regex, lookup, timeout, connection, close, status_code]
- Added Help page that is generated from Statping's Wiki repo on build
- Modified Service Group failures on index page to show 90 days of failures
- Modified Service view page, updated Latency and Ping charts, added failures below
- Modified Service chart on index page to show ping data along with latency
- Added AWS SNS Notifier
- Modified dashboard services UI
- Modified service.Failures API to include 32 failures (max)
# 0.90.63 (08-17-2020) # 0.90.63 (08-17-2020)
- Modified build process to use xgo for all arch builds - Modified build process to use xgo for all arch builds
- Modified Statping's Push Notifications server notifier to match with Firebase/gorush params - Modified Statping's Push Notifications server notifier to match with Firebase/gorush params

View File

@ -21,14 +21,14 @@ test: clean compile
go test -v -p=1 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -coverprofile=coverage.out ./... go test -v -p=1 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -coverprofile=coverage.out ./...
build: clean build: clean
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo linux" ./cmd CGO_ENABLED=1 go build -a -ldflags "-s -w -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo osusergo" ./cmd
go-build: clean go-build: clean
rm -rf source/dist rm -rf source/dist
rm -rf source/rice-box.go rm -rf source/rice-box.go
wget https://assets.statping.com/source.tar.gz wget https://assets.statping.com/source.tar.gz
tar -xvf source.tar.gz tar -xvf source.tar.gz
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo" ./cmd go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo osusergo" ./cmd
lint: lint:
go fmt ./... go fmt ./...
@ -76,6 +76,7 @@ test-deps:
go get github.com/GeertJohan/go.rice/rice go get github.com/GeertJohan/go.rice/rice
go get github.com/mattn/go-sqlite3 go get github.com/mattn/go-sqlite3
go install github.com/mattn/go-sqlite3 go install github.com/mattn/go-sqlite3
go install github.com/wellington/go-libsass
deps: deps:
go get -d -v -t ./... go get -d -v -t ./...
@ -420,5 +421,13 @@ check:
# sentry-cli releases set-commits --auto $VERSION # sentry-cli releases set-commits --auto $VERSION
# sentry-cli releases files $VERSION upload-sourcemaps dist # sentry-cli releases files $VERSION upload-sourcemaps dist
gen_help:
for file in ./statping.wiki/*.md
do
# convert each file to html and place it in the html directory
# --gfm == use github flavoured markdown
marked -o html/$file.html $file --gfm
done
.PHONY: all check build certs multiarch install-darwin go-build build-all buildx-base buildx-dev buildx-latest build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman .PHONY: all check build certs multiarch install-darwin go-build build-all buildx-base buildx-dev buildx-latest build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
.SILENT: travis_s3_creds .SILENT: travis_s3_creds

View File

@ -29,10 +29,10 @@ var (
func init() { func init() {
stopped = make(chan bool, 1) stopped = make(chan bool, 1)
core.New(VERSION) core.New(VERSION, COMMIT)
utils.InitEnvs() utils.InitEnvs()
configs.Version = VERSION utils.Params.Set("VERSION", VERSION)
configs.Commit = COMMIT utils.Params.Set("COMMIT", COMMIT)
rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
@ -161,7 +161,7 @@ func InitApp() error {
// start routine to delete old records (failures, hits) // start routine to delete old records (failures, hits)
go database.Maintenance() go database.Maintenance()
// init Sentry error monitoring (its useful) // init Sentry error monitoring (its useful)
utils.SentryInit(&VERSION, core.App.AllowReports.Bool) utils.SentryInit(core.App.AllowReports.Bool)
core.App.Setup = true core.App.Setup = true
core.App.Started = utils.Now() core.App.Started = utils.Now()
return nil return nil

View File

@ -71,13 +71,11 @@ func (t *TimeVar) ToValues() ([]*TimeValue, error) {
// GraphData will return all hits or failures // GraphData will return all hits or failures
func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) { func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
dbQuery := g.db.MultipleSelects( g.db = g.db.MultipleSelects(
g.db.SelectByTime(g.Group), g.db.SelectByTime(g.Group),
by.String(), by.String(),
).Group("timeframe").Order("timeframe", true) ).Group("timeframe").Order("timeframe", true)
g.db = dbQuery
caller, err := g.ToTimeValue() caller, err := g.ToTimeValue()
if err != nil { if err != nil {
return nil, err return nil, err
@ -89,6 +87,9 @@ func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
return caller.ToValues() return caller.ToValues()
} }
// ToTimeValue will format the SQL rows into a JSON format for the API.
// [{"timestamp": "2006-01-02T15:04:05Z", "amount": 468293}]
// TODO redo this entire function, use better SQL query to group by time
func (g *GroupQuery) ToTimeValue() (*TimeVar, error) { func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := g.db.Rows() rows, err := g.db.Rows()
if err != nil { if err != nil {
@ -116,27 +117,26 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) { func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {
timeMap := make(map[string]int64) timeMap := make(map[string]int64)
var validSet []*TimeValue var validSet []*TimeValue
dur := t.g.Group
for _, v := range t.data { for _, v := range t.data {
timeMap[v.Timeframe] = v.Amount timeMap[v.Timeframe] = v.Amount
} }
for {
currentStr := types.FixedTime(current, t.g.Group) currentStr := types.FixedTime(current, t.g.Group)
for {
var amount int64 var amount int64
if timeMap[currentStr] != 0 { if timeMap[currentStr] != 0 {
amount = timeMap[currentStr] amount = timeMap[currentStr]
} }
validSet = append(validSet, &TimeValue{ validSet = append(validSet, &TimeValue{
Timeframe: currentStr, Timeframe: currentStr,
Amount: amount, Amount: amount,
}) })
current = current.Add(t.g.Group)
if current.After(end) { if current.After(end) {
break break
} }
current = current.Add(dur)
currentStr = types.FixedTime(current, t.g.Group)
} }
return validSet, nil return validSet, nil
@ -233,10 +233,6 @@ func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
if endField == 0 { if endField == 0 {
query.End = utils.Now() query.End = utils.Now()
} }
if query.End.After(utils.Now()) {
query.End = utils.Now()
}
if query.Limit != 0 { if query.Limit != 0 {
q = q.Limit(query.Limit) q = q.Limit(query.Limit)
} }

View File

@ -19,10 +19,9 @@ func (it *Db) ParseTime(t string) (time.Time, error) {
} }
} }
// FormatTime returns the timestamp in the same format as the DATETIME column in database
func (it *Db) FormatTime(t time.Time) string { func (it *Db) FormatTime(t time.Time) string {
switch it.Type { switch it.Type {
case "mysql":
return t.Format("2006-01-02 15:04:05")
case "postgres": case "postgres":
return t.Format("2006-01-02 15:04:05.999999999") return t.Format("2006-01-02 15:04:05.999999999")
default: default:
@ -30,6 +29,7 @@ func (it *Db) FormatTime(t time.Time) string {
} }
} }
// SelectByTime returns an SQL query that will group "created_at" column by x seconds and returns as "timeframe"
func (it *Db) SelectByTime(increment time.Duration) string { func (it *Db) SelectByTime(increment time.Duration) string {
seconds := int64(increment.Seconds()) seconds := int64(increment.Seconds())
switch it.Type { switch it.Type {
@ -41,33 +41,3 @@ func (it *Db) SelectByTime(increment time.Duration) string {
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %d) * %d, 'unixepoch') as timeframe", seconds, seconds) return fmt.Sprintf("datetime((strftime('%%s', created_at) / %d) * %d, 'unixepoch') as timeframe", seconds, seconds)
} }
} }
func (it *Db) correctTimestamp(increment string) string {
var timestamper string
switch increment {
case "second":
timestamper = "%Y-%m-%d %H:%M:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%M:00"
case "hour":
timestamper = "%Y-%m-%d %H:00:00"
case "day":
timestamper = "%Y-%m-%d 00:00:00"
case "month":
timestamper = "%Y-%m-01 00:00:00"
case "year":
timestamper = "%Y-01-01 00:00:00"
default:
timestamper = "%Y-%m-%d 00:00:00"
}
switch it.Type {
case "mysql":
case "second":
timestamper = "%Y-%m-%d %H:%i:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%i:00"
}
return timestamper
}

105
dev/postman.json vendored
View File

@ -786,6 +786,105 @@
"description": "You can create custom badges with dynamic information by using [Shields.io](https://shields.io/) and parsing JSON fields with [JSONPath](http://jsonpath.com/). \n\n## Examples\n\n#### Service Uptime Percent\n<img src=\"https://img.shields.io/badge/dynamic/json?color=blue&label=%20Statping%20Uptime&query=%24.online_24_hours&url=https%3A%2F%2Fdemo.statping.com%2Fapi%2Fservices%2F2&suffix=%\">\n\n- URL: [https://demo.statping.com/api/services/2](https://demo.statping.com/api/services/2)\n- JSON Path: `$.online_24_hours`\n- Suffix: `%`\n\n```\nhttps://img.shields.io/badge/dynamic/json?color=blue&label=%20Statping%20Uptime&query=%24.online_24_hours&url=https%3A%2F%2Fdemo.statping.com%2Fapi%2Fservices%2F2&suffix=%\n```\n\n#### Count Services\n<img src=\"https://img.shields.io/badge/dynamic/json?color=purple&label=Demo%20Site&query=%24.services&url=https://demo.statping.com/health&suffix=%20services\">\n\n- URL: [https://demo.statping.com/health](https://demo.statping.com/health)\n- JSON Path: `$.services`\n- Suffix: ` services`\n\n```\nhttps://img.shields.io/badge/dynamic/json?color=purple&label=Demo%20Site&query=%24.services&url=https://demo.statping.com/health&suffix=%20services\n```" "description": "You can create custom badges with dynamic information by using [Shields.io](https://shields.io/) and parsing JSON fields with [JSONPath](http://jsonpath.com/). \n\n## Examples\n\n#### Service Uptime Percent\n<img src=\"https://img.shields.io/badge/dynamic/json?color=blue&label=%20Statping%20Uptime&query=%24.online_24_hours&url=https%3A%2F%2Fdemo.statping.com%2Fapi%2Fservices%2F2&suffix=%\">\n\n- URL: [https://demo.statping.com/api/services/2](https://demo.statping.com/api/services/2)\n- JSON Path: `$.online_24_hours`\n- Suffix: `%`\n\n```\nhttps://img.shields.io/badge/dynamic/json?color=blue&label=%20Statping%20Uptime&query=%24.online_24_hours&url=https%3A%2F%2Fdemo.statping.com%2Fapi%2Fservices%2F2&suffix=%\n```\n\n#### Count Services\n<img src=\"https://img.shields.io/badge/dynamic/json?color=purple&label=Demo%20Site&query=%24.services&url=https://demo.statping.com/health&suffix=%20services\">\n\n- URL: [https://demo.statping.com/health](https://demo.statping.com/health)\n- JSON Path: `$.services`\n- Suffix: ` services`\n\n```\nhttps://img.shields.io/badge/dynamic/json?color=purple&label=Demo%20Site&query=%24.services&url=https://demo.statping.com/health&suffix=%20services\n```"
}, },
"response": [] "response": []
},
{
"name": "Send Push Notification",
"event": [
{
"listen": "test",
"script": {
"id": "11fe392f-3636-4d2d-84e9-1119b351d8ee",
"exec": [
""
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"notifications\": [\n {\n \"tokens\": [\"dBLB1WvTJkiWl3ZPjP0-BS:APA91bGXUbKy65CaN1XqExHXZ892jik2k9XORXSiqdUyXhcQ5RDiJ6LfXrckuH3StYJFcma4UCDr_N038YUtxYsRIHYx_8vWZ6D2uq3199LegWXGl5tz-9zk3M4WZGX8WGxIRUJ31QtW\"],\n \"platform\": 2,\n \"message\": \"This notification will go to iOS and Android platform via Firebase!\"\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "https://push.statping.com/api/push",
"protocol": "https",
"host": [
"push",
"statping",
"com"
],
"path": [
"api",
"push"
]
},
"description": "Send a push notification to the Statping mobile app using your Firebase device identifier."
},
"response": [
{
"name": "Send Push Notification",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"notifications\": [\n {\n \"tokens\": [\"dBLB1WvTJkiWl3ZPjP0-BS:APA91bGXUbKy65CaN1XqExHXZ892jik2k9XORXSiqdUyXhcQ5RDiJ6LfXrckuH3StYJFcma4UCDr_N038YUtxYsRIHYx_8vWZ6D2uq3199LegWXGl5tz-9zk3M4WZGX8WGxIRUJ31QtW\"],\n \"platform\": 2,\n \"message\": \"This notification will go to iOS and Android Statping App\"\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "https://push.statping.com/api/push",
"protocol": "https",
"host": [
"push",
"statping",
"com"
],
"path": [
"api",
"push"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Length",
"value": "37"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Date",
"value": "Thu, 13 Aug 2020 02:24:17 GMT"
},
{
"key": "X-Gorush-Version",
"value": "No Version Provided"
}
],
"cookie": [],
"body": "{\n \"counts\": 1,\n \"logs\": [],\n \"success\": \"ok\"\n}"
}
]
} }
], ],
"description": "This is for Statping's miscellaneous API endpoints that aren't a part of another category.", "description": "This is for Statping's miscellaneous API endpoints that aren't a part of another category.",
@ -4099,7 +4198,7 @@
"", "",
"pm.test(\"View All Notifiers\", function () {", "pm.test(\"View All Notifiers\", function () {",
" var jsonData = pm.response.json();", " var jsonData = pm.response.json();",
" pm.expect(jsonData.length).to.eql(12);", " pm.expect(jsonData.length).to.eql(13);",
"});" "});"
], ],
"type": "text/javascript" "type": "text/javascript"
@ -4303,7 +4402,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"method\": \"slack\",\n \"host\": \"https://hooks.slack.com/services/EXAMPLEIDHERE/BV33WKP0C/MtKw3Kc8BFylTv4pohKqHtXX\",\n \"enabled\": true,\n \"limits\": 55\n}", "raw": "{\n \"method\": \"slack\",\n \"host\": \"https://hooks.slack.com/services/TTJ1B90DP/RENU20O9M/9uI823SUnYBuGcxYlpSimD6H\",\n \"enabled\": true,\n \"limits\": 55\n}",
"options": { "options": {
"raw": {} "raw": {}
} }
@ -4415,7 +4514,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"method\": \"success\",\n \"notifier\": {\n \"enabled\": false,\n \"limits\": 60,\n \"method\": \"slack\",\n \"host\": \"https://webhooksurl.slack.com/***\",\n \"success_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"The service {{.Service.Name}} is back online.\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"primary\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\",\n \"failure_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\":warning: The service {{.Service.Name}} is currently offline! :warning:\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"section\\\",\\n \\\"fields\\\": [{\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Service:*\\\\n{{.Service.Name}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*URL:*\\\\n{{.Service.Domain}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Status Code:*\\\\n{{.Service.LastStatusCode}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*When:*\\\\n{{.Failure.CreatedAt}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Downtime:*\\\\n{{.Service.DowntimeAgo}}\\\"\\n }, {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"*Error:*\\\\n{{.Failure.Issue}}\\\"\\n }]\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Offline Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"danger\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\"\n }\n}", "raw": "{\n \"method\": \"success\",\n \"notifier\": {\n \"enabled\": false,\n \"limits\": 60,\n \"method\": \"slack\",\n \"host\": \"https://webhooksurl.slack.com/***\",\n \"success_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"The service {{.Service.Name}} is back online.\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"primary\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\",\n \"failure_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\":warning: The service {{.Service.Name}} is currently offline! :warning:\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"section\\\",\\n \\\"fields\\\": [{\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Service:*\\\\n{{.Service.Name}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*URL:*\\\\n{{.Service.Domain}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Status Code:*\\\\n{{.Service.LastStatusCode}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*When:*\\\\n{{.Failure.CreatedAt}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Downtime:*\\\\n{{.Service.Downtime.Human}}\\\"\\n }, {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"*Error:*\\\\n{{.Failure.Issue}}\\\"\\n }]\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Offline Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"danger\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\"\n }\n}",
"options": { "options": {
"raw": {} "raw": {}
} }

View File

@ -21,7 +21,7 @@
"@fortawesome/vue-fontawesome": "^0.1.9", "@fortawesome/vue-fontawesome": "^0.1.9",
"@sentry/browser": "^5.20.1", "@sentry/browser": "^5.20.1",
"@sentry/integrations": "^5.20.1", "@sentry/integrations": "^5.20.1",
"apexcharts": "^3.15.0", "apexcharts": "^3.6.6",
"axios": "^0.19.1", "axios": "^0.19.1",
"codemirror-colorpicker": "^1.9.66", "codemirror-colorpicker": "^1.9.66",
"core-js": "^3.6.5", "core-js": "^3.6.5",
@ -29,8 +29,9 @@
"js-beautify": "^1.11.0", "js-beautify": "^1.11.0",
"querystring": "^0.2.0", "querystring": "^0.2.0",
"sass": "^1.26.10", "sass": "^1.26.10",
"semver": "^7.3.2",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-apexcharts": "^1.5.2", "vue-apexcharts": "^1.6.0",
"vue-clipboard2": "^0.3.1", "vue-clipboard2": "^0.3.1",
"vue-codemirror": "^4.0.6", "vue-codemirror": "^4.0.6",
"vue-cookies": "^1.7.0", "vue-cookies": "^1.7.0",
@ -75,6 +76,7 @@
"expect": "^25.1.0", "expect": "^25.1.0",
"file-loader": "^5.0.2", "file-loader": "^5.0.2",
"friendly-errors-webpack-plugin": "~1.7", "friendly-errors-webpack-plugin": "~1.7",
"github-wikito-converter": "^1.5.2",
"html-webpack-plugin": "^4.0.0-beta.11", "html-webpack-plugin": "^4.0.0-beta.11",
"jsdom": "^16.2.0", "jsdom": "^16.2.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",

View File

@ -7,7 +7,8 @@ const tokenKey = "statping_auth";
class Api { class Api {
constructor() { constructor() {
this.version = "0.90.64";
this.commit = "130cc3ede7463ec9af8d62abb84992e2a0ef453c";
} }
async oauth() { async oauth() {
@ -51,15 +52,15 @@ class Api {
return axios.post('api/services/' + data.id, data).then(response => (response.data)) return axios.post('api/services/' + data.id, data).then(response => (response.data))
} }
async service_hits(id, start, end, group, fill=true) { async service_hits(id, start, end, group, fill = true) {
return axios.get('api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data)) return axios.get('api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
} }
async service_ping(id, start, end, group, fill=true) { async service_ping(id, start, end, group, fill = true) {
return axios.get('api/services/' + id + '/ping_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data)) return axios.get('api/services/' + id + '/ping_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
} }
async service_failures_data(id, start, end, group, fill=true) { async service_failures_data(id, start, end, group, fill = true) {
return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data)) return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
} }
@ -72,7 +73,7 @@ class Api {
} }
async service_failures(id, start, end, limit = 999, offset = 0) { async service_failures(id, start, end, limit = 999, offset = 0) {
return axios.get('api/services/' + id + '/failures?start=' + start + '&end=' + end + '&limit=' + limit+ '&offset=' + offset).then(response => (response.data)) return axios.get('api/services/' + id + '/failures?start=' + start + '&end=' + end + '&limit=' + limit + '&offset=' + offset).then(response => (response.data))
} }
async service_failures_delete(service) { async service_failures_delete(service) {
@ -129,31 +130,31 @@ class Api {
} }
async incident_updates(incident) { async incident_updates(incident) {
return axios.get('api/incidents/'+incident.id+'/updates').then(response => (response.data)) return axios.get('api/incidents/' + incident.id + '/updates').then(response => (response.data))
} }
async incident_update_create(update) { async incident_update_create(update) {
return axios.post('api/incidents/'+update.incident+'/updates', update).then(response => (response.data)) return axios.post('api/incidents/' + update.incident + '/updates', update).then(response => (response.data))
} }
async incident_update_delete(update) { async incident_update_delete(update) {
return axios.delete('api/incidents/'+update.incident+'/updates/'+update.id).then(response => (response.data)) return axios.delete('api/incidents/' + update.incident + '/updates/' + update.id).then(response => (response.data))
} }
async incidents_service(id) { async incidents_service(id) {
return axios.get('api/services/'+id+'/incidents').then(response => (response.data)) return axios.get('api/services/' + id + '/incidents').then(response => (response.data))
} }
async incident_create(service_id, data) { async incident_create(service_id, data) {
return axios.post('api/services/'+service_id+'/incidents', data).then(response => (response.data)) return axios.post('api/services/' + service_id + '/incidents', data).then(response => (response.data))
} }
async incident_delete(incident) { async incident_delete(incident) {
return axios.delete('api/incidents/'+incident.id).then(response => (response.data)) return axios.delete('api/incidents/' + incident.id).then(response => (response.data))
} }
async checkin(api) { async checkin(api) {
return axios.get('api/checkins/'+api).then(response => (response.data)) return axios.get('api/checkins/' + api).then(response => (response.data))
} }
async checkin_create(data) { async checkin_create(data) {
@ -161,7 +162,7 @@ class Api {
} }
async checkin_delete(checkin) { async checkin_delete(checkin) {
return axios.delete('api/checkins/'+checkin.api_key).then(response => (response.data)) return axios.delete('api/checkins/' + checkin.api_key).then(response => (response.data))
} }
async messages() { async messages() {
@ -270,6 +271,10 @@ class Api {
} }
} }
async github_release() {
return fetch('https://api.github.com/repos/statping/statping/releases/latest').then(response => response.json())
}
async allActions(...all) { async allActions(...all) {
await axios.all([all]) await axios.all([all])
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="app"> <div id="app">
<router-view :loaded="loaded"/> <router-view/>
<Footer v-if="$route.path !== '/setup'"/> <Footer v-if="$route.path !== '/setup'"/>
</div> </div>
</template> </template>

View File

@ -83,13 +83,13 @@
} }
.chartmarker { .chartmarker {
padding: 5px; padding: 0px;
width: 240px; width: 200px;
text-align: right; text-align: right;
} }
.chartmarker SPAN { .chartmarker SPAN {
font-size: 9pt; font-size: 4pt;
display: block; display: block;
color: #8b8b8b; color: #8b8b8b;
} }
@ -103,11 +103,33 @@
background-color: #efefef; background-color: #efefef;
} }
.divided {
display: flex;
align-items: center;
margin-bottom: 0;
}
.divider {
flex-grow: 1;
border-bottom: 1px solid rgba(0,0,0,0.1);
margin: 0 20px 0 20px;
}
.daily-failures {
position: absolute;
padding-top: 3px;
top: 10px;
right: 100px;
width: 300px;
height: 25px;
}
.service_day { .service_day {
height: 20px; height: 20px;
margin-right: 2px; margin-right: 2px;
border-radius: 4px; border-radius: 2px;
max-width: 25px; max-width: 30px;
cursor: pointer;
} }
.service_day SPAN { .service_day SPAN {
@ -154,10 +176,6 @@
padding: 5px 7px; padding: 5px 7px;
} }
.service_li {
min-height: 115px !important;
}
.btn-sm { .btn-sm {
line-height: 1.3; line-height: 1.3;
font-size: 0.75rem; font-size: 0.75rem;
@ -289,10 +307,6 @@
color: #a0a0a0; color: #a0a0a0;
} }
.service_block {
min-height: 340px;
}
.json-field { .json-field {
font-size: 10pt; font-size: 10pt;
} }

View File

@ -92,19 +92,45 @@ A:HOVER {
color: #fff; color: #fff;
} }
.dashboard_card {
background-color: $group-list-background;
box-shadow: rgba(0,0,0,.05) 0px 2px 3px 1px;
}
.dashboard_card:HOVER {
background-color: lighten($group-list-background, 2%) !important;
box-shadow: rgba(0,0,0,.05) 0px 1px 5px 3px;
-webkit-transition-duration: 300ms;
-moz-transition-duration: 300ms;
-o-transition-duration: 300ms;
transition-duration: 300ms;
}
.list-group-item { .list-group-item {
min-height: 85pt; min-height: 85pt;
background-color: $group-list-background; background-color: $group-list-background;
} }
.list-group-item:HOVER { .list-group-item:HOVER {
background-color: lighten($group-list-background, 2%) !important; background-color: lighten($group-list-background, 5%) !important;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px 1px;
-webkit-transition-duration: 300ms;
-moz-transition-duration: 300ms;
-o-transition-duration: 300ms;
transition-duration: 300ms;
margin-top: -1px;
margin-bottom: 1px;
} }
.list-group-item A { .list-group-item A {
color: $group-list-title; color: $group-list-title;
} }
.chart_list_tooltip {
height: 30px;
font-size: 6pt;
}
.container { .container {
padding-top: 20px; padding-top: 20px;
padding-bottom: 25px; padding-bottom: 25px;
@ -113,16 +139,40 @@ A:HOVER {
background-color: $container-color; background-color: $container-color;
} }
.login_container {
border-radius: 5px;
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, 0.15) !important;
background-color: $container-color;
}
.footer { .footer {
text-decoration: none; text-decoration: none;
margin-top: 20px; margin-top: 20px;
} }
.footer A { .footer .links {
color: $footer-text-color; color: $footer-text-color;
text-decoration: none; text-decoration: none;
} }
.footer A:HOVER { .footer .links:HOVER {
color: #6d6d6d; color: #8c8c8c;
}
.footer .statping {
color: lighten($footer-text-color, 10%);
text-decoration: none;
}
.footer .statping:HOVER {
color: lighten($footer-text-color, 0%);
text-decoration: none;
}
.no-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
} }

View File

@ -40,10 +40,6 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
.service_li {
border: 1px solid #f3f3f3 !important;
}
.container { .container {
padding: 0px !important; padding: 0px !important;
padding-top: 0vh !important; padding-top: 0vh !important;

View File

@ -2,20 +2,20 @@
$background-color: #EAEAEA; $background-color: #EAEAEA;
$container-color: #ffffff; $container-color: #ffffff;
$text-color: #2a2a2a; $text-color: #2a2a2a;
$max-width: 860px; $max-width: 1012px;
$title-color: #4e4e4e; $title-color: #4e4e4e;
$description-color: #828282; $description-color: #828282;
$subtitle-color: #747474; $subtitle-color: #747474;
$mobile-card-shadow: 2px 3px 10px #b7b7b7; $mobile-card-shadow: 2px 3px 10px #b7b7b7;
$group-list-background: #fafafa; $group-list-background: #fcfcfc;
$group-list-title: #474747; $group-list-title: #474747;
$navbar-color: #1c1c1c; $navbar-color: #1c1c1c;
$navbar-background: #ffffff; $navbar-background: #ffffff;
$input-background: #fdfdfd; $input-background: #fdfdfd;
$input-color: #4e4e4e; $input-color: #4e4e4e;
$input-border: 1px solid #c9c9c9; $input-border: 1px solid #c9c9c9;
$day-success-background: #20ac13; $day-success-background: #e9e9e9;
$day-error-background: #d50a0a; $day-error-background: #d50a0a;
/* Status Container */ /* Status Container */
@ -35,7 +35,7 @@ $danger-color: #dd3545;
$primary-color: #3e9bff; $primary-color: #3e9bff;
/* Footer Settings */ /* Footer Settings */
$footer-text-color: #8d8d8d; $footer-text-color: #b0b0b0;
$nav-tab-color: #13a00d; $nav-tab-color: #13a00d;
$footer-display: block; $footer-display: block;

View File

@ -1,6 +1,5 @@
<template> <template>
<div class="col-12 mt-4 mt-md-3"> <div class="col-12 mt-4 mt-md-3">
<div class="row stats_area mb-5"> <div class="row stats_area mb-5">
<div class="col-4"> <div class="col-4">
<span class="font-6 font-weight-bold d-block">{{$store.getters.services.length}}</span> <span class="font-6 font-weight-bold d-block">{{$store.getters.services.length}}</span>
@ -33,23 +32,27 @@
</span> </span>
</div> </div>
<div class="row">
<div v-for="(service, index) in services" class="service_block" v-bind:key="index"> <div v-for="(service, index) in services_no_group" class="col-12 col-md-4">
<ServiceInfo :service=service /> <ServiceInfo :service="service" />
</div> </div>
</div> </div>
<div v-for="group in groups">
<GroupedServices :group="group"/>
</div>
</div>
</template> </template>
<script> <script>
import isAfter from "date-fns/isAfter"; import GroupedServices from "@/components/Dashboard/GroupedServices";
import parseISO from "date-fns/parseISO";
import isBefore from "date-fns/isBefore";
const ServiceInfo = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServiceInfo') const ServiceInfo = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServiceInfo')
export default { export default {
name: 'DashboardIndex', name: 'DashboardIndex',
components: { components: {
GroupedServices,
ServiceInfo ServiceInfo
}, },
data() { data() {
@ -63,10 +66,15 @@
}, },
services() { services() {
return this.$store.getters.services return this.$store.getters.services
} },
services_no_group() {
return this.$store.getters.servicesNoGroup
},
groups() {
return this.$store.getters.groupsInOrder
},
}, },
methods: { methods: {
failuresLast24Hours() { failuresLast24Hours() {
let total = 0; let total = 0;
this.services.map((s) => { this.services.map((s) => {

View File

@ -9,6 +9,7 @@
<th scope="col">{{$t('username')}}</th> <th scope="col">{{$t('username')}}</th>
<th scope="col">{{$t('type')}}</th> <th scope="col">{{$t('type')}}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('last_login') }}</th> <th scope="col" class="d-none d-md-table-cell">{{ $t('last_login') }}</th>
<th scope="col" class="d-none d-md-table-cell">Scopes</th>
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
@ -22,6 +23,7 @@
</span> </span>
</td> </td>
<td class="d-none d-md-table-cell">{{niceDate(user.updated_at)}}</td> <td class="d-none d-md-table-cell">{{niceDate(user.updated_at)}}</td>
<td class="d-none d-md-table-cell">{{user.scopes}}</td>
<td class="text-right"> <td class="text-right">
<div class="btn-group"> <div class="btn-group">
<a @click.prevent="editUser(user, edit)" href="#" class="btn btn-outline-secondary edit-user"> <a @click.prevent="editUser(user, edit)" href="#" class="btn btn-outline-secondary edit-user">

View File

@ -0,0 +1,59 @@
<template>
<div class="row">
<h5 v-if="group.name" class="h5 col-12 mb-3 mt-2 text-dim">
<font-awesome-icon @click="toggle" :icon="expanded ? 'minus' : 'plus'" class="pointer mr-3"/> {{group.name}}
<span class="badge badge-success text-uppercase float-right ml-2">{{services_online.length}} online</span>
<span v-if="services_online.services_offline > 0" class="badge badge-danger text-uppercase float-right">
{{services_offline.length}} offline
</span>
</h5>
<div class="col-12 col-md-4" v-if="expanded" v-for="service in group_services">
<ServiceInfo :service="service" />
</div>
</div>
</template>
<script>
const ServiceInfo = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServiceInfo')
export default {
name: "GroupedServices",
components: {
ServiceInfo
},
data() {
return {
expanded: true
}
},
props: {
group: {
required: true,
type: Object,
}
},
computed: {
services_online() {
return this.$store.getters.servicesInGroup(this.group.id).filter((s) => s.online)
},
services_offline() {
return this.$store.getters.servicesInGroup(this.group.id).filter((s) => !s.online)
},
group_services() {
return this.$store.getters.servicesInGroup(this.group.id)
},
},
methods: {
toggle() {
this.expanded = !this.expanded
},
dashboard_cookies() {
const data = [{group: 5, show: false}]
if (!this.$cookies.isKey("statping_layout")) {
this.$cookies.set("statping_layout", JSON.stringify(data))
}
}
}
}
</script>

View File

@ -1,28 +1,33 @@
<template> <template>
<div> <div class="row p-2">
<Loading :loading="!loaded"/>
<div v-if="loaded && !service.online" class="bg-white shadow-sm mt-3 p-3 pr-4 pl-4 col-12"> <div v-if="loaded && last_failure && failureBefore" class="col-12 text-danger font-2 m-0 mb-2">
<font-awesome-icon icon="exclamation" class="mr-3" size="1x"/> <font-awesome-icon icon="exclamation" class="mr-1 text-danger font-weight-bold" size="1x"/> Recent Failure<br>
Last failure was {{ago(service.last_error)}} ago. <span class="font-italic font-weight-light text-dim mt-1" style="max-width: 270px">
<code v-if="failure" class="d-block bg-light p-3 mt-3"> Last failure was {{ago(last_failure.created_at)}} ago. {{last_failure.issue}}
{{failure.issue}}
<span class="d-block text-dim float-right small mt-3 mb-1">Failure #{{failure.id}}</span>
</code>
</div>
<div v-if="loaded" v-for="message in $store.getters.serviceMessages(service.id)" class="bg-light shadow-sm p-3 pr-4 pl-4 col-12 mt-3">
<font-awesome-icon icon="calendar" class="mr-3" size="1x"/> {{message.description}}
<span class="d-block small text-muted mt-3">
Starts at <strong>{{niceDate(message.start_on)}}</strong> till <strong>{{niceDate(message.end_on)}}</strong>
({{dur(parseISO(message.start_on), parseISO(message.end_on))}})
</span> </span>
</div> </div>
<div v-if="loaded" v-for="incident in incidents" class="bg-light shadow-sm p-3 pr-4 pl-4 col-12 mt-3">
<font-awesome-icon icon="calendar" class="mr-3" size="1x"/> <div v-if="loaded" v-for="message in messages" class="col-12 font-2 m-0 mb-2">
{{incident.title}} - {{incident.description}} <font-awesome-icon icon="calendar" class="mr-1" size="1x"/> Upcoming Announcement<br>
<div v-for="update in incident.updates" class="d-block small"> <span class="font-italic font-weight-light text-dim mt-1">{{message.description}}</span>
<span class="font-weight-bold text-capitalize">{{update.type}}</span> - {{update.message}} <span class="font-0 text-dim float-right font-weight-light mt-1">@ <strong>{{niceDate(message.start_on)}}</strong>
</span>
</div> </div>
<div v-if="loaded" v-for="incident in incidents" class="col-12 font-2 m-0 mb-2">
<font-awesome-icon icon="bullhorn" class="mr-1" size="1x"/>Recent Incident<br>
<span class="font-italic font-weight-light text-dim mt-1" style="max-width: 270px">{{incident.title}} - {{incident.description}}</span>
<span class="font-0 text-dim float-right font-weight-light mt-1">@ <strong>{{niceDate(incident.created_at)}}</strong></span>
</div> </div>
<div v-if="success_event && !failureBefore" class="col-12 font-2 m-0 mb-2">
<span class="text-success"><font-awesome-icon icon="check" class="mr-1" size="1x"/>No New Events</span>
<span class="font-italic d-inline-block text-truncate text-dim mt-1" style="max-width: 270px">
Last failure was {{ago(service.last_error)}} ago.
</span>
</div>
</div> </div>
</template> </template>
@ -34,7 +39,6 @@ export default {
name: "ServiceEvents", name: "ServiceEvents",
components: { components: {
Loading Loading
}, },
props: { props: {
service: { service: {
@ -45,7 +49,6 @@ name: "ServiceEvents",
data() { data() {
return { return {
incidents: null, incidents: null,
failure: null,
loaded: false, loaded: false,
} }
}, },
@ -53,16 +56,28 @@ name: "ServiceEvents",
this.load() this.load()
}, },
computed: { computed: {
last_failure() {
if (!this.service.failures) {
return null
}
return this.service.failures[0]
},
failureBefore() {
return this.isAfter(this.parseISO(this.service.last_error), this.nowSubtract(43200).toISOString())
},
messages() { messages() {
return this.$store.getters.serviceMessages(this.service.id) return this.$store.getters.serviceMessages(this.service.id)
},
success_event() {
if (this.service.online && this.service.messages.length === 0 && this.service.incidents.length === 0) {
return true
}
return false
} }
}, },
methods: { methods: {
async load() { async load() {
this.loaded = false this.loaded = false
if (!this.service.online) {
await this.getFailure()
}
await this.getMessages() await this.getMessages()
await this.getIncidents() await this.getIncidents()
this.loaded = true this.loaded = true
@ -70,10 +85,6 @@ name: "ServiceEvents",
async getMessages() { async getMessages() {
// this.messages = this.$store.getters.serviceMessages(this.service.id) // this.messages = this.$store.getters.serviceMessages(this.service.id)
}, },
async getFailure() {
const f = await Api.service_failures(this.service.id, null, null, 1)
this.failure = f[0]
},
async getIncidents() { async getIncidents() {
this.incidents = await Api.incidents_service(this.service.id) this.incidents = await Api.incidents_service(this.service.id)
}, },

View File

@ -1,64 +1,56 @@
<template> <template>
<div class="card mb-4" :class="{'offline-card': !service.online}"> <div class="dashboard_card card mb-4" :class="{'offline-card': !service.online}">
<div class="card-header pb-1"> <div class="card-header pb-1">
<h4 v-observe-visibility="setVisible"> <h6 v-observe-visibility="setVisible">
<router-link :to="serviceLink(service)">{{service.name}}</router-link> <router-link :to="serviceLink(service)" class="no-decoration">{{service.name}}</router-link>
<span class="badge float-right text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}"> <span class="badge float-right text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? $t('online') : $t('offline')}} {{service.online ? $t('online') : $t('offline')}}
</span> </span>
</h4> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<transition name="fade"> <div v-if="loaded" class="row pl-2">
<div v-if="loaded" class="row pl-2 pr-2"> <div class="col-md-6 col-sm-12 pl-2 mt-2 mt-md-0 mb-3">
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3">
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/> <ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
</div> </div>
<div class="col-md-6 col-sm-12 mt-4 mt-md-0 mb-3"> <div class="col-md-6 col-sm-12 pl-0 mt-4 mt-md-0 mb-3">
<ServiceSparkLine :title="set1_name" subtitle="Latency Last 7 Days" :series="set1"/> <ServiceSparkLine :title="set1_name" subtitle="Latency Last 7 Days" :series="set1"/>
</div> </div>
<div class="col-12 mt-2 mt-md-0 mb-3">
<ServiceEvents :service="service"/> <ServiceEvents :service="service"/>
</div> </div>
<div v-else class="row mb-5">
<div class="col-12 col-md-6 text-center">
<font-awesome-icon icon="circle-notch" class="text-dim" size="2x" spin/>
</div> </div>
<div v-else class="row mt-5 mb-5 pt-5 pb-5"> <div class="col-12 col-md-6 text-center text-dim">
<div class="col-6 text-center text-muted"> <font-awesome-icon icon="circle-notch" class="text-dim" size="2x" spin/>
<font-awesome-icon icon="circle-notch" size="3x" spin/>
</div>
<div class="col-6 text-center text-muted">
<font-awesome-icon icon="circle-notch" size="3x" spin/>
</div> </div>
</div> </div>
</transition>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<div class="row"> <div class="row">
<div class="col-5 pr-0">
<span class="small text-dim"> {{ hoverbtn }}</span>
</div>
<div class="col-12 col-md-3 mb-2 mb-md-0"> <div class="col-7 pr-2 pl-0">
<router-link :to="{path: `/dashboard/service/${service.id}/incidents`, params: {id: service.id} }" class="btn btn-block btn-white text-capitalize incident"> <div class="btn-group float-right">
{{$tc('incident', 2)}} <button @click="$router.push({path: `/dashboard/service/${service.id}/incidents`, params: {id: service.id}})" @mouseleave="unsetHover" @mouseover="setHover('Incidents')" class="btn btn-sm btn-white incident">
</router-link> <font-awesome-icon icon="bullhorn"/>
</button>
<button @click="$router.push({path: `/dashboard/service/${service.id}/checkins`, params: {id: service.id}})" @mouseleave="unsetHover" @mouseover="setHover('Checkins')" class="btn btn-sm btn-white checkins">
<font-awesome-icon icon="calendar-check"/>
</button>
<button @click="$router.push({path: `/dashboard/service/${service.id}/failures`, params: {id: service.id}})" @mouseleave="unsetHover" @mouseover="setHover('Failures')" class="btn btn-sm btn-white failures">
<font-awesome-icon icon="exclamation-triangle"/> <span v-if="service.stats.failures !== 0" class="badge badge-danger ml-1">{{service.stats.failures}}</span>
</button>
</div> </div>
<div class="col-12 col-md-3 mb-2 mb-md-0">
<router-link :to="{path: `/dashboard/service/${service.id}/checkins`, params: {id: service.id} }" class="btn btn-block btn-white text-capitalize checkins">
{{$tc('checkin', 2)}}
</router-link>
</div>
<div class="col-12 col-md-3 mb-2 mb-md-0">
<router-link :to="{path: `/dashboard/service/${service.id}/failures`, params: {id: service.id} }" class="btn btn-block btn-white text-capitalize failures">
{{$tc('failure', 2)}} <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span>
</router-link>
</div>
<div class="col-12 col-md-3 mb-2 mb-md-0 mt-0 mt-md-1">
<span class="float-md-right">
{{$t('uptime', [service.online_7_days])}}
</span>
</div> </div>
</div> </div>
</div> </div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light"> <span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
@ -96,6 +88,8 @@
data() { data() {
return { return {
uptime: null, uptime: null,
hovered: false,
hoverbtn: "",
openTab: "", openTab: "",
set1: [], set1: [],
set2: [], set2: [],
@ -108,8 +102,17 @@
}, },
watch: { watch: {
},
mounted() {
this.unsetHover()
}, },
methods: { methods: {
setHover(name) {
this.hoverbtn = name
},
unsetHover() {
this.hoverbtn = this.$t('uptime', [this.service.online_7_days])
},
async setVisible(isVisible, entry) { async setVisible(isVisible, entry) {
if (isVisible && !this.visible) { if (isVisible && !this.visible) {
await this.loadInfo() await this.loadInfo()

View File

@ -1,5 +1,5 @@
<template v-if="series.length"> <template v-if="series.length">
<apexchart width="100%" height="180" type="bar" :options="chartOpts" :series="series"></apexchart> <apexchart width="100%" height="100" type="bar" :options="chartOpts" :series="series"></apexchart>
</template> </template>
<script> <script>
@ -37,6 +37,9 @@
enabled: true enabled: true
}, },
}, },
showPoint: false,
fullWidth:true,
chartPadding: {top: 0,right: 0,bottom: 0,left: 0},
stroke: { stroke: {
curve: 'straight' curve: 'straight'
}, },
@ -50,12 +53,11 @@
tooltip: { tooltip: {
theme: false, theme: false,
enabled: true, enabled: true,
custom: function({series, seriesIndex, dataPointIndex, w}) { custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex]; let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions) const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex]; let val = series[seriesIndex][dataPointIndex];
val = val + " ms" return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${this.humanTime(val)}</span><span>${dt}</span></div>`
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
}, },
fixed: { fixed: {
enabled: true, enabled: true,
@ -74,15 +76,16 @@
text: this.title, text: this.title,
offsetX: 0, offsetX: 0,
style: { style: {
fontSize: '28px', fontSize: '18px',
cssClass: 'apexcharts-yaxis-title' cssClass: 'apexcharts-yaxis-title'
} }
}, },
subtitle: { subtitle: {
text: this.subtitle, text: this.subtitle,
offsetX: 0, offsetX: 0,
offsetY: 20,
style: { style: {
fontSize: '14px', fontSize: '9px',
cssClass: 'apexcharts-yaxis-title' cssClass: 'apexcharts-yaxis-title'
} }
} }

View File

@ -9,8 +9,18 @@
<thead> <thead>
<tr> <tr>
<th scope="col">Name</th> <th scope="col">Name</th>
<th scope="col" class="d-none d-md-table-cell">Status</th>
<th scope="col" class="d-none d-md-table-cell">Visibility</th> <th scope="col" class="d-none d-md-table-cell">Visibility</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('group') }}</th> <th scope="col" class="d-none d-md-table-cell">{{ $t('group') }}</th>
<th scope="col" class="d-none d-md-table-cell" style="width: 130px">
Failures
<div class="btn-group float-right" role="group">
<a @click="list_timeframe='3h'" type="button" class="small" :class="{'text-success': list_timeframe==='3h', 'text-muted': list_timeframe!=='3h'}">3h</a>
<a @click="list_timeframe='12h'" type="button" class="small" :class="{'text-success': list_timeframe==='12h', 'text-muted': list_timeframe!=='12h'}">12h</a>
<a @click="list_timeframe='24h'" type="button" class="small" :class="{'text-success': list_timeframe==='24h', 'text-muted': list_timeframe!=='24h'}">24h</a>
<a @click="list_timeframe='7d'" type="button" class="small" :class="{'text-success': list_timeframe==='7d', 'text-muted': list_timeframe!=='7d'}">7d</a>
</div>
</th>
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
@ -21,6 +31,11 @@
<font-awesome-icon icon="bars" class="mr-3"/> <font-awesome-icon icon="bars" class="mr-3"/>
</span> {{service.name}} </span> {{service.name}}
</td> </td>
<td class="d-none d-md-table-cell">
<span class="badge text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? $t('online') : $t('offline')}}
</span>
</td>
<td class="d-none d-md-table-cell"> <td class="d-none d-md-table-cell">
<span class="badge text-uppercase" :class="{'badge-primary': service.public, 'badge-secondary': !service.public}"> <span class="badge text-uppercase" :class="{'badge-primary': service.public, 'badge-secondary': !service.public}">
{{service.public ? $t('public') : $t('private')}} {{service.public ? $t('public') : $t('private')}}
@ -31,6 +46,9 @@
<span class="badge badge-secondary">{{serviceGroup(service)}}</span> <span class="badge badge-secondary">{{serviceGroup(service)}}</span>
</div> </div>
</td> </td>
<td class="d-none d-md-table-cell">
<ServiceSparkList :service="service" :timeframe="list_timeframe"/>
</td>
<td class="text-right"> <td class="text-right">
<div class="btn-group"> <div class="btn-group">
<button :disabled="loading" v-if="$store.state.admin" @click.prevent="goto({path: `/dashboard/edit_service/${service.id}`, params: {service: service} })" class="btn btn-sm btn-outline-secondary"> <button :disabled="loading" v-if="$store.state.admin" @click.prevent="goto({path: `/dashboard/edit_service/${service.id}`, params: {service: service} })" class="btn btn-sm btn-outline-secondary">
@ -53,18 +71,70 @@
<script> <script>
import Api from "../../API"; import Api from "../../API";
import ServiceSparkList from "@/components/Service/ServiceSparkList";
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable') const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '../../forms/ToggleSwitch'); const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '../../forms/ToggleSwitch');
export default { export default {
name: 'ServicesList', name: 'ServicesList',
components: { components: {
ServiceSparkList,
ToggleSwitch, ToggleSwitch,
draggable draggable
}, },
data() { data() {
return { return {
loading: false, loading: false,
list_timeframe: "12h",
chartOpts: {
chart: {
type: 'bar',
height: 50,
sparkline: {
enabled: true
},
},
xaxis: {
type: 'numeric',
},
showPoint: false,
fullWidth:true,
chartPadding: {top: 0,right: 0,bottom: 0,left: 0},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.8,
},
yaxis: {
min: 0
},
plotOptions: {
bar: {
colors: {
ranges: [{
from: 0,
to: 1,
color: '#39c10a'
}, {
from: 2,
to: 90,
color: '#e01a1a'
}]
},
},
},
tooltip: {
theme: false,
enabled: false,
},
title: {
enabled: false,
},
subtitle: {
enabled: false,
}
}
} }
}, },
computed: { computed: {

View File

@ -14,18 +14,21 @@
<li @click="navopen = !navopen" class="nav-item navbar-item"> <li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/services" class="nav-link">{{ $t('top_nav.services') }}</router-link> <router-link to="/dashboard/services" class="nav-link">{{ $t('top_nav.services') }}</router-link>
</li> </li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item"> <li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/users" class="nav-link">{{ $t('top_nav.users') }}</router-link> <router-link to="/dashboard/users" class="nav-link">{{ $t('top_nav.users') }}</router-link>
</li> </li>
<li @click="navopen = !navopen" class="nav-item navbar-item"> <li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/messages" class="nav-link">{{ $t('top_nav.announcements') }}</router-link> <router-link to="/dashboard/messages" class="nav-link">{{ $t('top_nav.announcements') }}</router-link>
</li> </li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item"> <li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/settings" class="nav-link">{{ $t('top_nav.settings') }}</router-link> <router-link to="/dashboard/settings" class="nav-link">{{ $t('top_nav.settings') }}</router-link>
</li> </li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item"> <li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/logs" class="nav-link">{{ $t('top_nav.logs') }}</router-link> <router-link to="/dashboard/logs" class="nav-link">{{ $t('top_nav.logs') }}</router-link>
</li> </li>
<li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/help" class="nav-link">{{ $t('top_nav.help') }}</router-link>
</li>
</ul> </ul>
<span class="navbar-text"> <span class="navbar-text">
<a href="#" class="nav-link" @click.prevent="logout">{{ $t('top_nav.logout') }}</a> <a href="#" class="nav-link" @click.prevent="logout">{{ $t('top_nav.logout') }}</a>
@ -45,6 +48,11 @@
navopen: false navopen: false
} }
}, },
computed: {
admin() {
return this.$store.state.admin
}
},
methods: { methods: {
async logout () { async logout () {
await Api.logout() await Api.logout()
@ -57,7 +65,3 @@
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,46 @@
const serviceSparkLine = {
chart: {
type: 'bar',
height: 50,
sparkline: {
enabled: true
},
},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.3,
},
yaxis: {
min: 0
},
colors: ['#b3bdc3'],
tooltip: {
theme: false,
enabled: false,
},
title: {
text: this.title,
offsetX: 0,
style: {
fontSize: '28px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: this.subtitle,
offsetX: 0,
style: {
fontSize: '14px',
cssClass: 'apexcharts-yaxis-title'
}
}
}
export default {
ServiceList: serviceSparkLine
}

View File

@ -1,10 +1,15 @@
<template> <template>
<footer> <footer>
<div v-if="!core.footer" class="footer text-center mb-4 p-2"> <div v-if="!core.footer" class="footer text-center mb-4 p-2">
<a href="https://github.com/statping/statping" target="_blank"> <div class="d-block text-dim">
Statping {{core.version}} made with <font-awesome-icon icon="heart" class="text-danger"/> <div class="mb-3">
</a> | <router-link class="links" :to="admin ? '/dashboard' : '/login'">{{$t('top_nav.dashboard')}}</router-link>
<router-link :to="$store.state.admin ? '/dashboard' : '/login'">{{$t('top_nav.dashboard')}}</router-link> </div>
<span class="font-1 mt-3">
<a href="https://github.com/statping/statping" class="statping" target="_blank">
Statping v{{core.version}} made with <font-awesome-icon icon="heart" class="hlight font-1"/></a>
</span>
</div>
</div> </div>
<div v-else class="footer text-center mb-4 p-2" v-html="core.footer"></div> <div v-else class="footer text-center mb-4 p-2" v-html="core.footer"></div>
</footer> </footer>
@ -21,11 +26,19 @@
computed: { computed: {
core() { core() {
return this.$store.getters.core return this.$store.getters.core
} },
commit() {
return this.$store.getters.core.commit.slice(0,8)
},
admin() {
return this.$store.getters.admin
},
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped> <style scoped>
.hlight {
color: #f6cbcb;
}
</style> </style>

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="col-12 full-col-12"> <div v-if="services.length > 0" class="col-12 full-col-12">
<h4 v-if="group.name !== 'Empty Group'" class="group_header mb-3 mt-4">{{group.name}}</h4> <h4 v-if="group.name !== 'Empty Group'" class="group_header mb-3 mt-4">{{group.name}}</h4>
<div class="list-group online_list mb-4"> <div class="list-group online_list mb-4">
<div v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action"> <div v-for="(service, index) in services" v-bind:key="index" class="list-group-item list-group-item-action">
<router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link> <router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge text-uppercase float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }"> <span class="badge text-uppercase float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">
{{service.online ? $t('online') : $t('offline')}} {{service.online ? $t('online') : $t('offline')}}
@ -30,11 +30,15 @@ export default {
GroupServiceFailures GroupServiceFailures
}, },
props: { props: {
group: Object group: {
type: Object,
required: true,
}
}, },
computed: {
services() {
return this.$store.getters.servicesInGroup(this.group.id)
}
}
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,17 +1,31 @@
<template> <template>
<div> <div>
<div class="d-flex mt-3 mb-2"> <div v-observe-visibility="{callback: visibleChart, once: true}" v-if="!loaded" class="row">
<div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'day-error': d.amount > 0, 'day-success': d.amount === 0}"> <div class="col-12 text-center mt-3">
<span v-if="d.amount !== 0" class="d-none d-md-block text-center small">{{d.amount}}</span> <font-awesome-icon icon="circle-notch" class="text-dim" size="2x" spin/>
</div>
</div>
<transition name="fade">
<div v-if="loaded">
<div class="d-flex mt-3">
<div class="flex-fill service_day" v-for="(d, index) in failureData" @mouseover="mouseover(d)" @mouseout="mouseout" :class="{'day-error': d.amount > 0, 'day-success': d.amount === 0}">
<span v-if="d.amount !== 0" class="d-none d-md-block text-center small"></span>
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-3 text-left font-2 text-muted">30 Days Ago</div> <div class="col-12 no-select">
<div class="col-6 text-center font-2" :class="{'text-muted': service.online, 'text-danger': !service.online}"> <p class="divided">
{{service_txt}} <span class="font-2 text-muted">90 Days Ago</span>
<span class="divider"></span>
<span class="text-center font-2" :class="{'text-muted': service.online, 'text-danger': !service.online}">{{service_txt}}</span>
<span class="divider"></span>
<span class="font-2 text-muted">Today</span>
</p>
</div> </div>
<div class="col-3 text-right font-2 text-muted">Today</div>
</div> </div>
<div class="daily-failures small text-right text-dim">{{hover_text}}</div>
</div>
</transition>
</div> </div>
</template> </template>
@ -26,6 +40,9 @@ export default {
data() { data() {
return { return {
failureData: [], failureData: [],
hover_text: "",
loaded: false,
visible: false,
} }
}, },
props: { props: {
@ -40,21 +57,34 @@ export default {
} }
}, },
mounted () { mounted () {
this.lastDaysFailures()
}, },
methods: { methods: {
visibleChart(isVisible, entry) {
if (isVisible && !this.visible) {
this.visible = true
this.lastDaysFailures().then(() => this.loaded = true)
}
},
mouseout() {
this.hover_text = ""
},
mouseover(e) {
let txt = `${e.amount} Failures`
if (e.amount === 0) {
txt = `No Issues`
}
this.hover_text = `${e.date.toLocaleDateString()} - ${txt}`
},
async lastDaysFailures() { async lastDaysFailures() {
const start = this.nowSubtract(86400 * 30) const start = this.beginningOf('day', this.nowSubtract(86400 * 90))
const data = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(this.startToday()), "24h") const end = this.endOf('tomorrow')
const data = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(end), "24h", true)
data.forEach((d) => { data.forEach((d) => {
let date = this.parseISO(d.timeframe) let date = this.parseISO(d.timeframe)
this.failureData.push({month: 1, day: date.getDate(), amount: d.amount}) this.failureData.push({month: date.getMonth(), day: date.getDate(), date: date, amount: d.amount})
}) })
} }
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -15,7 +15,3 @@ export default {
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="row"> <div class="row">
<div v-for="(incident, i) in incidents" class="col-12"> <div v-for="(incident, i) in incidents" class="col-12 mt-2">
<span class="braker mt-1 mb-3"></span> <span class="braker mt-1 mb-3"></span>
<h6>{{incident.title}} <h6>{{incident.title}}
<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span> <span class="font-2 float-right">{{niceDate(incident.created_at)}}</span>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="service-chart-container"> <div class="service-chart-container">
<apexchart width="100%" type="area" :options="main_chart_options" :series="main_chart"></apexchart> <apexchart width="100%" height="350" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div> </div>
</template> </template>
@ -36,6 +36,7 @@
return { return {
loading: true, loading: true,
main_data: null, main_data: null,
ping_data: null,
expanded_data: null, expanded_data: null,
main_chart_options: { main_chart_options: {
noData: { noData: {
@ -51,6 +52,7 @@
}, },
chart: { chart: {
id: 'mainchart', id: 'mainchart',
stacked: true,
events: { events: {
dataPointSelection: (event, chartContext, config) => { dataPointSelection: (event, chartContext, config) => {
window.console.log('slect') window.console.log('slect')
@ -110,7 +112,9 @@
}, },
yaxis: { yaxis: {
labels: { labels: {
show: true formatter: (value) => {
return this.humanTime(value)
}
}, },
}, },
markers: { markers: {
@ -165,15 +169,15 @@
show: false show: false
}, },
fill: { fill: {
colors: ["#48d338"], colors: ["#f1771f", "#48d338"],
opacity: 1, opacity: 1,
type: 'solid' type: 'solid'
}, },
stroke: { stroke: {
show: true, show: true,
curve: 'smooth', curve: 'stepline',
lineCap: 'butt', lineCap: 'butt',
colors: ["#3aa82d"], colors: ["#f1771f", "#48d338"],
width: 2, width: 2,
} }
}, },
@ -227,8 +231,11 @@
computed: { computed: {
main_chart () { main_chart () {
return [{ return [{
name: this.service.name, name: "latency",
...this.convertToChartData(this.main_data) ...this.convertToChartData(this.main_data)
},{
name: "ping",
...this.convertToChartData(this.ping_data)
}] }]
}, },
expanded_chart () { expanded_chart () {
@ -261,9 +268,13 @@
}, },
async chartHits() { async chartHits() {
this.main_data = await this.load_hits() this.main_data = await this.load_hits()
this.ping_data = await this.load_ping()
}, },
async load_hits(start=this.params.start, end=this.params.end, group=this.group) { async load_hits(start=this.params.start, end=this.params.end, group=this.group) {
return await Api.service_hits(this.service.id, start, end, group, false) return await Api.service_hits(this.service.id, start, end, group, false)
},
async load_ping(start=this.params.start, end=this.params.end, group=this.group) {
return await Api.service_ping(this.service.id, start, end, group, false)
} }
} }
} }

View File

@ -0,0 +1,166 @@
<template>
<div class="col-12">
<div class="text-center" style="width:210px" v-if="!loaded">
<font-awesome-icon icon="circle-notch" class="h-25 text-dim" spin/>
</div>
<apexchart v-else width="100%" height="50" type="bar" :options="chartOpts" :series="data"></apexchart>
</div>
</template>
<script>
import Api from "@/API";
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
export default {
name: "FailuresBarChart",
props: {
service: {
required: true,
type: Object,
},
group: {
required: true,
type: String,
},
start: {
required: true,
type: String,
},
end: {
required: true,
type: String,
},
},
data() {
return {
data: null,
loaded: false,
chartOpts: {
chart: {
type: 'bar',
height: 150,
sparkline: {
enabled: true
},
animations: {
enabled: false,
},
},
xaxis: {
type: 'datetime',
},
showPoint: false,
fullWidth:true,
chartPadding: {top: 0,right: 0,bottom: 0,left: 80},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.4,
},
yaxis: {
min: 0,
max: 1,
},
plotOptions: {
bar: {
colors: {
ranges: [{
from: 0,
to: 1,
color: '#cfcfcf'
}, {
from: 2,
to: 3,
color: '#f58e49'
}, {
from: 3,
to: 20,
color: '#e01a1a'
}, {
from: 21,
to: Infinity,
color: '#9b0909'
}]
},
},
},
tooltip: {
theme: false,
enabled: true,
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let val = series[seriesIndex][dataPointIndex];
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let ago = `${(dataPointIndex-12) * -1} hours ago`
if ((dataPointIndex-12) * -1 === 0) {
ago = `Current hour`
}
return `<div class="chart_list_tooltip font-2 mb-4">${val-1} Failures<br>${dt}</div>`
},
fixed: {
enabled: true,
position: 'topLeft',
offsetX: 0,
offsetY: 0,
},
x: {
formatter: (value) => { return value },
},
y: {
show: false
},
},
title: {
enabled: false,
},
subtitle: {
enabled: false,
}
}
}
},
async mounted() {
await this.loadFailures()
},
watch: {
group(o, n) {
this.loaded = false
this.loadFailures()
this.loaded = true
},
start(o, n) {
this.loaded = false
this.loadFailures()
this.loaded = true
},
end(o, n) {
this.loaded = false
this.loadFailures()
this.loaded = true
},
},
methods: {
convertChartData(data) {
if (!data) {
return []
}
let arr = []
data.forEach((d, k) => {
arr.push({
x: d.timeframe,
y: d.amount+1,
})
})
return arr
},
async loadFailures() {
this.loaded = false
const data = await Api.service_failures_data(this.service.id, this.toUnix(this.parseISO(this.start)), this.toUnix(this.parseISO(this.end)), this.group, true)
this.loaded = true
this.data = [{data: this.convertChartData(data)}]
}
},
}
</script>

View File

@ -13,7 +13,7 @@
</div> </div>
</div> </div>
<div v-show="!expanded" v-observe-visibility="visibleChart" class="chart-container"> <div v-show="!expanded" v-observe-visibility="{callback: visibleChart, throttle: 200}" class="chart-container">
<ServiceChart :service="service" :visible="visible" :chart_timeframe="chartTimeframe"/> <ServiceChart :service="service" :visible="visible" :chart_timeframe="chartTimeframe"/>
</div> </div>
@ -67,18 +67,12 @@ export default {
name: 'ServiceBlock', name: 'ServiceBlock',
components: { Analytics, ServiceTopStats, ServiceChart}, components: { Analytics, ServiceTopStats, ServiceChart},
props: { props: {
in_service: { service: {
type: Object, type: Object,
required: true required: true
}, },
}, },
watch: {
},
computed: { computed: {
service() {
return this.track_service
},
timeframepick() { timeframepick() {
return this.timeframes.find(s => s.value === this.timeframe_val) return this.timeframes.find(s => s.value === this.timeframe_val)
}, },
@ -151,14 +145,13 @@ export default {
value: 0, value: 0,
} }
}, },
track_service: null,
} }
}, },
beforeDestroy() { beforeDestroy() {
// clearInterval(this.timer_func) // clearInterval(this.timer_func)
}, },
async created() { created() {
this.track_service = this.in_service
}, },
methods: { methods: {
disabled_interval(interval) { disabled_interval(interval) {
@ -189,29 +182,7 @@ export default {
}, },
async setService() { async setService() {
await this.$store.commit('setService', this.service) await this.$store.commit('setService', this.service)
this.$router.push('/service/'+this.service.id, {props: {in_service: this.service}}) this.$router.push('/service/'+this.service.id, {props: {service: this.service}})
},
async showMoreStats() {
this.expanded = !this.expanded;
const failData = await Graphing.failures(this.service, 7)
this.stats.total_failures.chart = failData.data;
this.stats.total_failures.value = failData.total;
const hitsData = await Graphing.hits(this.service, 7)
this.stats.high_latency.chart = hitsData.chart;
this.stats.high_latency.value = this.humanTime(hitsData.high);
this.stats.lowest_latency.chart = hitsData.chart;
this.stats.lowest_latency.value = this.humanTime(hitsData.low);
const pingData = await Graphing.pings(this.service, 7)
this.stats.high_ping.chart = pingData.chart;
this.stats.high_ping.value = this.humanTime(pingData.high);
this.stats.low_ping.chart = pingData.chart;
this.stats.low_ping.value = this.humanTime(pingData.low);
}, },
visibleChart(isVisible, entry) { visibleChart(isVisible, entry) {
if (isVisible && !this.visible) { if (isVisible && !this.visible) {

View File

@ -47,8 +47,14 @@
return { return {
ready: false, ready: false,
showing: false, showing: false,
data: [], data: null,
chartOptions: { ping_data: null,
series: null,
}
},
computed: {
chartOptions() {
return {
noData: { noData: {
text: 'Loading...' text: 'Loading...'
}, },
@ -58,9 +64,20 @@
type: "area", type: "area",
animations: { animations: {
enabled: true, enabled: true,
initialAnimation: { easing: 'easeinout',
enabled: true speed: 800,
} animateGradually: {
enabled: false,
delay: 400,
},
dynamicAnimation: {
enabled: true,
speed: 500
},
hover: {
animationDuration: 0, // duration of animations when hovering an item
},
responsiveAnimationDuration: 0,
}, },
selection: { selection: {
enabled: false enabled: false
@ -112,13 +129,13 @@
custom: ({series, seriesIndex, dataPointIndex, w}) => { custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex]; let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions) const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex]; let val = series[0][dataPointIndex];
if (val >= 10000) { let pingVal = series[1][dataPointIndex];
val = Math.round(val / 1000) + " ms" return `<div class="chartmarker">
} else { <span>Average Response Time: ${this.humanTime(val)}/${this.chart_timeframe.interval}</span>
val = val + " μs" <span>Average Ping: ${this.humanTime(pingVal)}/${this.chart_timeframe.interval}</span>
} <span>${dt}</span>
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>` </div>`
}, },
fixed: { fixed: {
enabled: true, enabled: true,
@ -130,7 +147,9 @@
show: false, show: false,
}, },
y: { y: {
formatter: (value) => { return value + " %" }, formatter: (value) => {
return value + " %"
},
}, },
}, },
legend: { legend: {
@ -147,20 +166,17 @@
show: false show: false
}, },
fill: { fill: {
colors: [this.service.online ? "#48d338" : "#dd3545"], colors: this.service.online ? ["#3dc82f", "#48d338"] : ["#c60f20", "#dd3545"],
opacity: 1, opacity: 1,
type: 'solid' type: 'solid',
}, },
stroke: { stroke: {
show: false, show: false,
curve: 'smooth', curve: 'smooth',
lineCap: 'butt', lineCap: 'butt',
colors: [this.service.online ? "#3aa82d" : "#dd3545"], colors: this.service.online ? ["#38bc2a", "#48d338"] : ["#c60f20", "#dd3545"],
}
} }
},
series: [{
data: []
}]
} }
}, },
watch: { watch: {
@ -185,10 +201,12 @@
if (this.data === null && val.interval !== "5m") { if (this.data === null && val.interval !== "5m") {
await this.chartHits({start_time: val.start_time, interval: "5m"}) await this.chartHits({start_time: val.start_time, interval: "5m"})
} }
this.series = [{ this.ping_data = await Api.service_ping(this.service.id, start, end, val.interval, false)
name: this.service.name,
...this.convertToChartData(this.data) this.series = [
}] {name: "Latency", ...this.convertToChartData(this.data)},
{name: "Ping", ...this.convertToChartData(this.ping_data)},
]
this.ready = true this.ready = true
} }
} }

View File

@ -0,0 +1,162 @@
<template>
<div class="text-center" style="width:210px" v-if="!loaded">
<font-awesome-icon icon="circle-notch" class="h-25 text-dim" spin/>
</div>
<apexchart v-else width="240" height="30" type="bar" :options="chartOpts" :series="data"></apexchart>
</template>
<script>
import Api from "@/API";
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
export default {
name: "ServiceSparkList",
props: {
service: {
required: true,
type: Object,
},
timeframe: {
required: true,
type: String,
}
},
data() {
return {
data: null,
loaded: false,
chartOpts: {
chart: {
type: 'bar',
height: 50,
sparkline: {
enabled: true
},
animations: {
enabled: false,
},
},
xaxis: {
type: 'datetime',
},
showPoint: false,
fullWidth:true,
chartPadding: {top: 0,right: 0,bottom: 0,left: 0},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.4,
},
yaxis: {
min: 0,
max: 5,
},
plotOptions: {
bar: {
colors: {
ranges: [{
from: 0,
to: 1,
color: '#cfcfcf'
}, {
from: 2,
to: 3,
color: '#f58e49'
}, {
from: 3,
to: 20,
color: '#e01a1a'
}, {
from: 21,
to: Infinity,
color: '#9b0909'
}]
},
},
},
tooltip: {
theme: false,
enabled: true,
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let val = series[seriesIndex][dataPointIndex];
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let ago = `${(dataPointIndex-12) * -1} hours ago`
if ((dataPointIndex-12) * -1 === 0) {
ago = `Current hour`
}
return `<div class="chart_list_tooltip">${val-1} Failures<br>${dt}</div>`
},
fixed: {
enabled: true,
position: 'topLeft',
offsetX: 0,
offsetY: 0,
},
x: {
formatter: (value) => { return value },
},
y: {
show: false
},
},
title: {
enabled: false,
},
subtitle: {
enabled: false,
}
}
}
},
mounted() {
this.loadFailures()
},
watch: {
timeframe(o, n) {
this.loaded = false
this.loadFailures()
this.loaded = true
}
},
methods: {
convertChartData(data) {
if (!data) {
return []
}
let arr = []
data.forEach((d, k) => {
arr.push({
x: d.timeframe,
y: d.amount+1,
})
})
return arr
},
async loadFailures() {
this.loaded = false
let start = 43200
let group = "12h"
if (this.timeframe === "3h") {
start = 10800
group = "5m"
} else if (this.timeframe === "12h") {
start = 43200
group = "1h"
} else if (this.timeframe === "24h") {
start = 86400
group = "2h"
} else if (this.timeframe === "7d") {
start = 86400 * 7
group = "24h"
}
const data = await Api.service_failures_data(this.service.id, this.toUnix(this.nowSubtract(start)), this.toUnix(this.now()), group, true)
this.loaded = true
this.data = [{data: this.convertChartData(data)}]
}
},
}
</script>

View File

@ -2,15 +2,15 @@
<div> <div>
<form @submit.prevent="login" autocomplete="on"> <form @submit.prevent="login" autocomplete="on">
<div class="form-group row"> <div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">{{$t('username')}}</label> <label for="username" class="col-4 col-form-label">{{$t('username')}}</label>
<div class="col-sm-10"> <div class="col-8">
<input @keyup="checkForm" type="text" v-model="username" autocomplete="username" name="username" class="form-control" id="username" placeholder="Username" autocorrect="off" autocapitalize="none"> <input @keyup="checkForm" type="text" v-model="username" autocomplete="username" name="username" class="form-control" id="username" placeholder="admin" autocorrect="off" autocapitalize="none">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">{{$t('password')}}</label> <label for="password" class="col-4 col-form-label">{{$t('password')}}</label>
<div class="col-sm-10"> <div class="col-8">
<input @keyup="checkForm" type="password" v-model="password" autocomplete="current-password" name="password" class="form-control" id="password" placeholder="Password"> <input @keyup="checkForm" type="password" v-model="password" autocomplete="current-password" name="password" class="form-control" id="password" placeholder="password123">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -18,14 +18,14 @@
<div v-if="error" class="alert alert-danger" role="alert"> <div v-if="error" class="alert alert-danger" role="alert">
{{$t('dashboard.wrong_login')}} {{$t('dashboard.wrong_login')}}
</div> </div>
<button @click.prevent="login" type="submit" class="btn btn-block mb-3 btn-primary" :disabled="disabled || loading"> <button @click.prevent="login" type="submit" class="btn btn-block btn-primary" :disabled="disabled || loading">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? $t('dashboard.loading') : $t('dashboard.sign_in')}} <font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? $t('dashboard.loading') : $t('dashboard.sign_in')}}
</button> </button>
</div> </div>
</div> </div>
</form> </form>
<a v-if="oauth && oauth.gh_client_id" @click.prevent="GHlogin" href="#" class="btn btn-block btn-outline-dark"> <a v-if="oauth && oauth.gh_client_id" @click.prevent="GHlogin" href="#" class="mt-4 btn btn-block btn-outline-dark">
<font-awesome-icon :icon="['fab', 'github']" /> Login with Github <font-awesome-icon :icon="['fab', 'github']" /> Login with Github
</a> </a>

View File

@ -61,7 +61,7 @@
<small id="interval" class="form-text text-muted">Interval to check your service state</small> <small id="interval" class="form-text text-muted">Interval to check your service state</small>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">
<input v-model="service.check_interval" type="text" name="check_interval" class="form-control"> <input v-model="service.check_interval" type="number" name="check_interval" class="form-control">
</div> </div>
</div> </div>
@ -73,9 +73,11 @@
<div class="card-body"> <div class="card-body">
<div class="form-group row"> <div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Service Endpoint {{service.type === 'http' ? "(URL)" : "(Domain)"}}</label> <label for="service_url" class="col-sm-4 col-form-label">
Service Endpoint {{service.type === 'http' ? "(URL)" : "(Domain)"}}
</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input v-model="service.domain" type="text" class="form-control" id="service_url" :placeholder="service.type === 'http' ? 'https://google.com' : '192.168.1.1'" required autocapitalize="none" spellcheck="false"> <input v-model="service.domain" type="url" class="form-control" id="service_url" :placeholder="service.type === 'http' ? 'https://google.com' : '192.168.1.1'" required autocapitalize="none" spellcheck="false">
<small class="form-text text-muted">Statping will attempt to connect to this address</small> <small class="form-text text-muted">Statping will attempt to connect to this address</small>
</div> </div>
</div> </div>
@ -110,7 +112,7 @@
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">
<input v-model="service.timeout" type="text" name="service_timeout" class="form-control"> <input v-model="service.timeout" type="number" name="service_timeout" class="form-control">
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="container col-md-7 col-sm-12 mt-2 sm-container"> <div class="container col-md-7 col-sm-12 mt-2 sm-container">
<div class="col-12 col-md-8 offset-md-2 mb-4"> <div class="col-12 col-md-6 offset-md-3 mb-4">
<img alt="Statping Setup" class="col-12 mt-5 mt-md-0" style="max-width:680px" src="banner.png"> <img alt="Statping Setup" class="img-fluid mt-5 mt-md-0" src="banner.png">
</div> </div>
<div class="col-12"> <div class="col-12">

View File

@ -7,6 +7,7 @@ const english = {
announcements: "Announcements", announcements: "Announcements",
settings: "Settings", settings: "Settings",
logs: "Logs", logs: "Logs",
help: "Help",
logout: 'Logout', logout: 'Logout',
}, },
setup: { setup: {

View File

@ -7,6 +7,7 @@ const french = {
announcements: "Announcements", announcements: "Announcements",
settings: "Settings", settings: "Settings",
logs: "Logs", logs: "Logs",
help: "Help",
logout: 'Logout', logout: 'Logout',
}, },
setup: { setup: {

View File

@ -7,6 +7,7 @@ const german = {
announcements: "Announcements", announcements: "Announcements",
settings: "Settings", settings: "Settings",
logs: "Logs", logs: "Logs",
help: "Help",
logout: 'Logout', logout: 'Logout',
}, },
setup: { setup: {

View File

@ -7,6 +7,7 @@ const russian = {
announcements: "Announcements", announcements: "Announcements",
settings: "Settings", settings: "Settings",
logs: "Logs", logs: "Logs",
help: "Help",
logout: 'Logout', logout: 'Logout',
}, },
setup: { setup: {

View File

@ -7,6 +7,7 @@ const spanish = {
announcements: "Announcements", announcements: "Announcements",
settings: "Settings", settings: "Settings",
logs: "Logs", logs: "Logs",
help: "Help",
logout: 'Logout', logout: 'Logout',
}, },
setup: { setup: {

View File

@ -37,7 +37,10 @@ Sentry.init({
integrations: [new Integrations.Vue({Vue, attachProps: true, logErrors: true})], integrations: [new Integrations.Vue({Vue, attachProps: true, logErrors: true})],
}); });
Vue.config.productionTip = false Vue.config.productionTip = process.env.NODE_ENV !== 'production'
Vue.config.devtools = process.env.NODE_ENV !== 'production'
Vue.config.performance = process.env.NODE_ENV !== 'production'
new Vue({ new Vue({
router, router,
store, store,

View File

@ -1,10 +1,11 @@
import Vue from "vue"; import Vue from "vue";
const { startOfToday, startOfMonth, lastDayOfMonth, subSeconds, getUnixTime, fromUnixTime, differenceInSeconds, formatDistance, addMonths, addSeconds, isWithinInterval } = require('date-fns') const { startOfDay, startOfWeek, endOfMonth, startOfToday, startOfTomorrow, startOfYesterday, endOfYesterday, endOfTomorrow, endOfToday, endOfDay, startOfMonth, lastDayOfMonth, subSeconds, getUnixTime, fromUnixTime, differenceInSeconds, formatDistance, addMonths, addSeconds, isWithinInterval } = require('date-fns')
import formatDistanceToNow from 'date-fns/formatDistanceToNow' import formatDistanceToNow from 'date-fns/formatDistanceToNow'
import format from 'date-fns/format' import format from 'date-fns/format'
import parseISO from 'date-fns/parseISO' import parseISO from 'date-fns/parseISO'
import isBefore from 'date-fns/isBefore' import isBefore from 'date-fns/isBefore'
import isAfter from 'date-fns/isAfter' import isAfter from 'date-fns/isAfter'
import { roundToNearestMinutes } from 'date-fns'
export default Vue.mixin({ export default Vue.mixin({
methods: { methods: {
@ -33,7 +34,7 @@ export default Vue.mixin({
return lastDayOfMonth(t1) return lastDayOfMonth(t1)
}, },
nowSubtract(seconds) { nowSubtract(seconds) {
return subSeconds(new Date(), seconds) return subSeconds(this.now(), seconds)
}, },
isAfter(date, compare) { isAfter(date, compare) {
return isAfter(date, parseISO(compare)) return isAfter(date, parseISO(compare))
@ -53,11 +54,45 @@ export default Vue.mixin({
parseISO(v) { parseISO(v) {
return parseISO(v) return parseISO(v)
}, },
round(minutes) {
return roundToNearestMinutes(minutes)
},
endOf(method, val) {
switch (method) {
case "day":
return endOfDay(val)
case "today":
return endOfToday()
case "tomorrow":
return endOfTomorrow()
case "yesterday":
return endOfYesterday()
case "month":
return endOfMonth(val)
}
return roundToNearestMinutes(val)
},
beginningOf(method, val) {
switch (method) {
case "day":
return startOfDay(val)
case "today":
return startOfToday()
case "tomorrow":
return startOfTomorrow()
case "yesterday":
return startOfYesterday()
case "week":
return startOfWeek()
case "month":
return startOfMonth(val)
}
return roundToNearestMinutes(val)
},
isZero(val) { isZero(val) {
return getUnixTime(parseISO(val)) <= 0 return getUnixTime(parseISO(val)) <= 0
}, },
smallText(s) { smallText(s) {
const incidents = s.incidents
if (s.online) { if (s.online) {
return `Online, checked ${this.ago(s.last_success)} ago` return `Online, checked ${this.ago(s.last_success)} ago`
} else { } else {
@ -71,6 +106,24 @@ export default Vue.mixin({
return `Service has been offline for ${this.ago(s.last_success)}` return `Service has been offline for ${this.ago(s.last_success)}`
} }
}, },
round_time(frame, val) {
switch(frame) {
case "15m":
return roundToNearestMinutes(val, {nearestTo: 60 * 15})
case "30m":
return roundToNearestMinutes(val, {nearestTo: 60 * 30})
case "1h":
return roundToNearestMinutes(val, {nearestTo: 3600})
case "3h":
return roundToNearestMinutes(val, {nearestTo: 3600 * 3})
case "6h":
return roundToNearestMinutes(val, {nearestTo: 3600 * 6})
case "12h":
return roundToNearestMinutes(val, {nearestTo: 3600 * 12})
case "24h":
return roundToNearestMinutes(val, {nearestTo: 3600 * 24})
}
},
toUnix(val) { toUnix(val) {
return getUnixTime(val) return getUnixTime(val)
}, },
@ -96,7 +149,7 @@ export default Vue.mixin({
}, },
serviceLink(service) { serviceLink(service) {
if (service.permalink) { if (service.permalink) {
service = this.$store.getters.serviceByPermalink(service.permalink) service = this.$store.getters.serviceById(service.permalink)
} }
if (service === undefined || this.isEmptyObject(service)) { if (service === undefined || this.isEmptyObject(service)) {
return `/service/0` return `/service/0`
@ -177,6 +230,12 @@ export default Vue.mixin({
} }
return val + " μs" return val + " μs"
}, },
humanTimeNum(val) {
if (val >= 1000) {
return Math.round(val / 1000)
}
return val
},
firstDayOfMonth(date) { firstDayOfMonth(date) {
return startOfMonth(date) return startOfMonth(date)
}, },

2294
frontend/src/pages/Help.vue Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,18 @@
<Header/> <Header/>
<div v-if="!loaded" class="row mt-5 mb-5">
<div class="col-12 mt-5 mb-2 text-center">
<font-awesome-icon icon="circle-notch" class="text-dim" size="2x" spin/>
</div>
<div class="col-12 text-center mt-3 mb-3">
<span class="text-dim">{{loading_text}}</span>
</div>
</div>
<div class="col-12 full-col-12"> <div class="col-12 full-col-12">
<div v-for="service in services_no_group" v-bind:key="service.id" class="list-group online_list mb-4"> <div v-for="service in services_no_group" v-bind:key="service.id" class="list-group online_list mb-4">
<div class="service_li list-group-item list-group-item-action"> <div class="list-group-item list-group-item-action">
<router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link> <router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">{{service.online ? "ONLINE" : "OFFLINE"}}</span> <span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
<GroupServiceFailures :service="service"/> <GroupServiceFailures :service="service"/>
@ -14,7 +23,7 @@
</div> </div>
</div> </div>
<div> <div v-if="loaded">
<Group v-for="group in groups" v-bind:key="group.id" :group=group /> <Group v-for="group in groups" v-bind:key="group.id" :group=group />
</div> </div>
@ -24,7 +33,7 @@
<div class="col-12 full-col-12"> <div class="col-12 full-col-12">
<div v-for="service in services" :ref="service.id" v-bind:key="service.id"> <div v-for="service in services" :ref="service.id" v-bind:key="service.id">
<ServiceBlock :in_service=service /> <ServiceBlock :service="service" />
</div> </div>
</div> </div>
@ -52,10 +61,29 @@ export default {
}, },
data() { data() {
return { return {
logged_in: false logged_in: false,
} }
}, },
computed: { computed: {
loading_text() {
if (this.core == null) {
return "Loading Core"
} else if (this.groups == null) {
return "Loading Groups"
} else if (this.services == null) {
return "Loading Services"
} else if (this.messages == null) {
return "Loading Announcements"
} else {
return "Completed"
}
},
loaded() {
return this.core !== null && this.groups !== null && this.services !== null
},
core() {
return this.$store.getters.core
},
messages() { messages() {
return this.$store.getters.messages.filter(m => this.inRange(m) && m.service === 0) return this.$store.getters.messages.filter(m => this.inRange(m) && m.service === 0)
}, },

View File

@ -1,9 +1,11 @@
<template> <template>
<div class="container col-md-7 col-sm-12 mt-md-5"> <div class="offset-md-3 offset-lg-4 offset-0 col-lg-4 col-md-6 mt-5">
<div class="col-10 offset-1 col-md-8 offset-md-2 mt-md-2">
<div class="col-12 col-md-8 offset-md-2 mb-4"> <div class="offset-1 offset-lg-2 col-lg-8 col-10 mb-4 mb-md-3">
<img alt="Statping Login" class="col-12 mt-5 mt-md-0" style="max-width:650px" src="banner.png"> <img alt="Statping Login" class="embed-responsive" src="http://0.0.0.0:8585/banner.png">
</div> </div>
<div class="login_container col-12 p-4">
<FormLogin/> <FormLogin/>
</div> </div>
</div> </div>

View File

@ -1,6 +1,16 @@
<template> <template>
<div class="container col-md-7 col-sm-12 mt-md-5"> <div class="container col-md-7 col-sm-12 mt-md-5">
<div class="col-12 mb-4">
<div v-if="!ready" class="row mt-5">
<div class="col-12 text-center">
<font-awesome-icon icon="circle-notch" size="3x" spin/>
</div>
<div class="col-12 text-center mt-3 mb-3">
<span class="text-muted">Loading Service</span>
</div>
</div>
<div v-if="ready" class="col-12 mb-4">
<span class="mt-3 mb-3 text-white d-md-none btn d-block d-md-none text-uppercase" :class="{'bg-success': service.online, 'bg-danger': !service.online}"> <span class="mt-3 mb-3 text-white d-md-none btn d-block d-md-none text-uppercase" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
{{service.online ? $t('online') : $t('offline')}} {{service.online ? $t('online') : $t('offline')}}
</span> </span>
@ -20,16 +30,16 @@
<div class="card-header text-capitalize">Timeframe</div> <div class="card-header text-capitalize">Timeframe</div>
<div class="card-body pb-4"> <div class="card-body pb-4">
<div class="row"> <div class="row">
<div class="col-12 col-md-4 font-2 mb-3 mb-md-0"> <div class="col">
<flatPickr :disabled="!loaded" @on-change="reload" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="form-control text-left d-block" required /> <flatPickr :disabled="!loaded" @on-change="reload" v-model="start_time" :config="{ wrap: true, allowInput: true, enableTime: true, dateFormat: 'Z', altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="form-control text-left" required />
<small class="d-block">From {{this.format(new Date(start_time))}}</small> <small class="d-block">From {{this.format(new Date(start_time))}}</small>
</div> </div>
<div class="col-12 col-md-4 font-2 mb-3 mb-md-0"> <div class="col">
<flatPickr :disabled="!loaded" @on-change="reload" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="form-control text-left" required /> <flatPickr :disabled="!loaded" @on-change="reload" v-model="end_time" :config="{ wrap: true, allowInput: true, enableTime: true, dateFormat: 'Z', altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="form-control text-left" required />
<small class="d-block">To {{this.format(new Date(end_time))}}</small> <small class="d-block">To {{this.format(new Date(end_time))}}</small>
</div> </div>
<div class="col-12 col-md-4 mb-1 mb-md-0"> <div class="col">
<select :disabled="!loaded" @change="chartHits" v-model="group" class="form-control"> <select :disabled="!loaded" @change="chartHits(service)" v-model="group" class="form-control">
<option value="1m">1 Minute</option> <option value="1m">1 Minute</option>
<option value="5m">5 Minutes</option> <option value="5m">5 Minutes</option>
<option value="15m">15 Minute</option> <option value="15m">15 Minute</option>
@ -51,12 +61,13 @@
<div class="card text-black-50 bg-white mt-3 mb-3"> <div class="card text-black-50 bg-white mt-3 mb-3">
<div class="card-header text-capitalize">Service Latency</div> <div class="card-header text-capitalize">Service Latency</div>
<div v-if="loaded" class="card-body"> <div v-if="loaded" class="card-body">
<div class="row mb-5"> <div class="row">
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/> <AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
</div> </div>
<div class="row mt-5"> <div>
<apexchart height="220" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart> <FailuresBarChart :service="service" :start="start_time.toString()" :end="end_time.toString()" :group="group"/>
</div> </div>
</div> </div>
<div v-else class="row mt-3 mb-3"> <div v-else class="row mt-3 mb-3">
<div class="col-12 text-center"> <div class="col-12 text-center">
@ -91,6 +102,7 @@
import flatPickr from 'vue-flatpickr-component'; import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css'; import 'flatpickr/dist/flatpickr.css';
import FailuresBarChart from "@/components/Service/FailuresBarChart";
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }; const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
const axisOptions = { const axisOptions = {
@ -120,6 +132,7 @@
export default { export default {
name: 'Service', name: 'Service',
components: { components: {
FailuresBarChart,
AdvancedChart, AdvancedChart,
ServiceTopStats, ServiceTopStats,
ServiceHeatmap, ServiceHeatmap,
@ -130,12 +143,13 @@ export default {
}, },
data() { data() {
return { return {
service: null, id: null,
tab: "failures", tab: "failures",
authenticated: false, authenticated: false,
ready: true, ready: false,
group: "1h", group: "15m",
data: null, data: null,
service: null,
uptime_data: null, uptime_data: null,
loaded: false, loaded: false,
messages: [], messages: [],
@ -154,8 +168,8 @@ export default {
timeRangeOptions: { timeRangeOptions: {
chart: { chart: {
id: 'uptime', id: 'uptime',
height: 120, height: 220,
width: "100%", width: '100%',
type: 'rangeBar', type: 'rangeBar',
toolbar: { toolbar: {
show: false show: false
@ -189,7 +203,7 @@ export default {
type: 'datetime' type: 'datetime'
}, },
yaxis: { yaxis: {
show: false show: true,
}, },
grid: { grid: {
row: { row: {
@ -256,14 +270,14 @@ export default {
}, },
stroke: { stroke: {
show: false, show: false,
curve: 'smooth', curve: 'stepline',
lineCap: 'butt', lineCap: 'butt',
}, },
}, },
xaxis: { xaxis: {
type: "datetime", type: "datetime",
labels: { labels: {
show: true format: 'MM yyyy'
}, },
tooltip: { tooltip: {
enabled: false enabled: false
@ -347,6 +361,9 @@ export default {
}, },
} }
}, },
watch: {
'$route': 'fetchData'
},
computed: { computed: {
core () { core () {
return this.$store.getters.core return this.$store.getters.core
@ -354,9 +371,6 @@ export default {
params () { params () {
return {start: this.toUnix(new Date(this.start_time)), end: this.toUnix(new Date(this.end_time))} return {start: this.toUnix(new Date(this.start_time)), end: this.toUnix(new Date(this.end_time))}
}, },
id () {
return this.$route.params.id;
},
uptimeSeries () { uptimeSeries () {
return this.timedata.series return this.timedata.series
}, },
@ -370,19 +384,25 @@ export default {
return this.$store.getters.serviceMessages(this.service.id).filter(m => this.inRange(m)) return this.$store.getters.serviceMessages(this.service.id).filter(m => this.inRange(m))
}, },
}, },
watch: {
'$route': 'reload',
},
created() { created() {
this.reload() this.fetchData()
}, },
async mounted() { mounted() {
if (!this.$store.getters.service) {
// const s = await Api.service(this.id)
// this.$store.commit('setService', s)
}
}, },
methods: { methods: {
async fetchData () {
if (!this.$route.params.id) {
this.ready = false
return
}
this.services = await Api.services()
await this.$store.commit("setServices", this.services)
this.service = await Api.service(this.$route.params.id)
await this.reload()
this.ready = true
},
async updated_chart(start, end) { async updated_chart(start, end) {
this.loaded = false this.loaded = false
this.start_time = start this.start_time = start
@ -390,19 +410,15 @@ export default {
this.loaded = true this.loaded = true
}, },
async reload() { async reload() {
this.loaded = false
const services = await Api.services()
this.$store.commit("setServices", services)
if (this.isNumeric(this.$route.params.id)) {
this.service = this.$store.getters.serviceById(this.$route.params.id)
} else {
this.service = this.$store.getters.serviceByPermalink(this.$route.params.id)
}
await this.chartHits() await this.chartHits()
await this.chartFailures()
await this.fetchUptime() await this.fetchUptime()
this.loaded = true this.loaded = true
}, },
async fetchUptime() { async fetchUptime(service) {
if (service) {
return
}
const uptime = await Api.service_uptime(this.service.id, this.params.start, this.params.end) const uptime = await Api.service_uptime(this.service.id, this.params.start, this.params.end)
this.uptime_data = this.parse_uptime(uptime) this.uptime_data = this.parse_uptime(uptime)
}, },
@ -438,20 +454,19 @@ export default {
}, },
inRange(message) { inRange(message) {
return this.isBetween(this.now(), message.start_on, message.start_on === message.end_on ? this.maxDate().toISOString() : message.end_on) return this.isBetween(this.now(), message.start_on, message.start_on === message.end_on ? this.maxDate().toISOString() : message.end_on)
},
async getService() {
await this.chartHits()
await this.serviceFailures()
},
async serviceFailures() {
this.failures = await Api.service_failures(this.service.id, this.params.start, this.params.end)
}, },
async chartHits(start=0, end=99999999999) { async chartHits(start=0, end=99999999999) {
this.data = await Api.service_hits(this.service.id, this.params.start, this.params.end, this.group, false) if (!this.service) {
if (this.data.length === 0 && this.group !== "1h") { return
this.group = "1h"
await this.chartHits("1h")
} }
this.data = await Api.service_hits(this.service.id, this.params.start, this.params.end, this.group, false)
this.ready = true
},
async chartFailures(start=0, end=99999999999) {
if (!this.service) {
return
}
this.failures_data = await Api.service_failures_data(this.service.id, this.params.start, this.params.end, this.group, true)
this.ready = true this.ready = true
} }
} }

View File

@ -3,6 +3,13 @@
<div class="row"> <div class="row">
<div class="col-md-3 col-sm-12 mb-4 mb-md-0"> <div class="col-md-3 col-sm-12 mb-4 mb-md-0">
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical"> <div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<div v-if="version_below" class="alert small text-center mt-0 pt-0 pb-0">
Update {{github.tag_name}} Available
<a href="https://github.com/statping/statping/releases/latest" class="btn btn-sm text-success mt-2">Download</a>
<a href="https://github.com/statping/statping/blob/master/CHANGELOG.md" class="btn btn-sm text-dim mt-2">Changelog</a>
</div>
<h6 class="text-muted">{{ $t('settings.main') }}</h6> <h6 class="text-muted">{{ $t('settings.main') }}</h6>
<a @click.prevent="changeTab" class="nav-link" v-bind:class="{active: liClass('v-pills-home-tab')}" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true"> <a @click.prevent="changeTab" class="nav-link" v-bind:class="{active: liClass('v-pills-home-tab')}" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true">
@ -48,9 +55,10 @@
<font-awesome-icon icon="code-branch" class="mr-3"/> {{$t('settings.repo')}} <font-awesome-icon icon="code-branch" class="mr-3"/> {{$t('settings.repo')}}
</a> </a>
<div class="row justify-content-center mt-2"> <span class="small text-dim text-center mt-5">Statping v{{core.version}}<br>
<github-button href="https://github.com/statping/statping" data-icon="octicon-star" data-show-count="true" aria-label="Star Statping on GitHub">Star</github-button> <a class="small text-muted no-decoration" v-if="core.commit" v-bind:href="`https://github.com/statping/statping/commit/${core.commit}`">{{core.commit.slice(0,8)}}</a>
</div> </span>
</div> </div>
@ -116,8 +124,8 @@
<script> <script>
import Api from '../API'; import Api from '../API';
import GithubButton from 'vue-github-button'
import Variables from "@/components/Dashboard/Variables"; import Variables from "@/components/Dashboard/Variables";
const semver = require('semver')
const CoreSettings = () => import(/* webpackChunkName: "dashboard" */ '@/forms/CoreSettings') const CoreSettings = () => import(/* webpackChunkName: "dashboard" */ '@/forms/CoreSettings')
const FormIntegration = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Integration') const FormIntegration = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Integration')
@ -130,7 +138,6 @@
name: 'Settings', name: 'Settings',
components: { components: {
Variables, Variables,
GithubButton,
OAuth, OAuth,
Cache, Cache,
ThemeEditor, ThemeEditor,
@ -141,6 +148,7 @@
data() { data() {
return { return {
tab: "v-pills-home-tab", tab: "v-pills-home-tab",
github: null,
} }
}, },
computed: { computed: {
@ -149,6 +157,12 @@
}, },
notifiers() { notifiers() {
return this.$store.getters.notifiers return this.$store.getters.notifiers
},
version_below() {
if (!this.github || !this.core.version) {
return false
}
return semver.gt(semver.coerce(this.github.tag_name), semver.coerce(this.core.version))
} }
}, },
mounted() { mounted() {
@ -159,11 +173,15 @@
}, },
methods: { methods: {
async update() { async update() {
const c = await Api.core()
this.$store.commit('setCore', c)
const n = await Api.notifiers()
this.$store.commit('setNotifiers', n)
this.cache = await Api.cache() this.cache = await Api.cache()
await this.getGithub()
},
async getGithub() {
try {
this.github = await Api.github_release()
} catch(e) {
console.error(e)
}
}, },
changeTab(e) { changeTab(e) {
this.tab = e.target.id this.tab = e.target.id
@ -172,19 +190,24 @@
return this.tab === id return this.tab === id
}, },
async renewApiKeys() { async renewApiKeys() {
let r = confirm("Are you sure you want to reset the API keys?"); let r = confirm("Are you sure you want to reset the API keys? You will be logged out.");
if (r === true) { if (r === true) {
await Api.renewApiKeys() await Api.renewApiKeys()
const core = await Api.core() const core = await Api.core()
this.$store.commit('setCore', core) this.$store.commit('setCore', core)
this.core = core this.core = core
await this.logout()
} }
}, },
async logout () {
await Api.logout()
this.$store.commit('setHasAllData', false)
this.$store.commit('setToken', null)
this.$store.commit('setAdmin', false)
this.$store.commit('setUser', false)
// this.$cookies.remove("statping_auth")
await this.$router.push('/logout')
}
} }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -6,6 +6,7 @@ const DashboardServices = () => import(/* webpackChunkName: "dashboard" */ '@/co
const DashboardMessages = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/DashboardMessages') const DashboardMessages = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/DashboardMessages')
const EditService = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/EditService') const EditService = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/EditService')
const Logs = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Logs') const Logs = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Logs')
const Help = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Help')
const Settings = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Settings') const Settings = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Settings')
const Login = () => import(/* webpackChunkName: "index" */ '@/pages/Login') const Login = () => import(/* webpackChunkName: "index" */ '@/pages/Login')
const Service = () => import(/* webpackChunkName: "index" */ '@/pages/Service') const Service = () => import(/* webpackChunkName: "index" */ '@/pages/Service')
@ -158,7 +159,7 @@ const routes = [
} }
},{ },{
path: 'help', path: 'help',
component: Logs, component: Help,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Statping - Help', title: 'Statping - Help',

View File

@ -36,6 +36,7 @@ export default new Vuex.Store({
getters: { getters: {
hasAllData: state => state.hasAllData, hasAllData: state => state.hasAllData,
hasPublicData: state => state.hasPublicData, hasPublicData: state => state.hasPublicData,
admin: state => state.admin,
core: state => state.core, core: state => state.core,
oauth: state => state.oauth, oauth: state => state.oauth,
token: state => state.token, token: state => state.token,
@ -69,10 +70,7 @@ export default new Vuex.Store({
} }
}, },
serviceById: (state) => (id) => { serviceById: (state) => (id) => {
return state.services.find(s => s.id === id) return state.services.find(s => s.permalink === id || s.id === id)
},
serviceByPermalink: (state) => (permalink) => {
return state.services.find(s => s.permalink === permalink)
}, },
servicesInGroup: (state) => (id) => { servicesInGroup: (state) => (id) => {
return state.services.filter(s => s.group_id === id).sort((a, b) => a.order_id - b.order_id) return state.services.filter(s => s.group_id === id).sort((a, b) => a.order_id - b.order_id)

View File

@ -2,6 +2,9 @@ module.exports = {
baseUrl: '/', baseUrl: '/',
assetsDir: 'assets', assetsDir: 'assets',
filenameHashing: false, filenameHashing: false,
productionTip: process.env.NODE_ENV !== 'production',
devtools: process.env.NODE_ENV !== 'production',
performance: process.env.NODE_ENV !== 'production',
devServer: { devServer: {
disableHostCheck: true, disableHostCheck: true,
proxyTable: { proxyTable: {

View File

@ -2034,7 +2034,7 @@ anymatch@~3.1.1:
normalize-path "^3.0.0" normalize-path "^3.0.0"
picomatch "^2.0.4" picomatch "^2.0.4"
apexcharts@^3.15.0: apexcharts@^3.6.6:
version "3.20.0" version "3.20.0"
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.20.0.tgz#768bb963d2bd87abe3a37a6ee35c7fc7d43bbfb7" resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.20.0.tgz#768bb963d2bd87abe3a37a6ee35c7fc7d43bbfb7"
integrity sha512-DuQ9SlFPJBJwamYudzwf/Z6KMaIRUhnVBQk/TBa3mXzN2SwS3GgGz7V39OS1GfcPlPUTTy8vXv91M8pYmfFkCg== integrity sha512-DuQ9SlFPJBJwamYudzwf/Z6KMaIRUhnVBQk/TBa3mXzN2SwS3GgGz7V39OS1GfcPlPUTTy8vXv91M8pYmfFkCg==
@ -2176,6 +2176,11 @@ async@^2.6.2:
dependencies: dependencies:
lodash "^4.17.14" lodash "^4.17.14"
async@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9"
integrity sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=
asynckit@^0.4.0: asynckit@^0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -2338,6 +2343,11 @@ bluebird@3.7.2, bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bluebird@^2.9.34:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
version "4.11.9" version "4.11.9"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
@ -2381,6 +2391,11 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bootstrap@^3.3.7:
version "3.4.1"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.4.1.tgz#c3a347d419e289ad11f4033e3c4132b87c081d72"
integrity sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -3104,6 +3119,11 @@ colorette@^1.2.1:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
colors@1.0.x:
version "1.0.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
combined-stream@^1.0.6, combined-stream@~1.0.6: combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -3116,7 +3136,7 @@ commander@2.17.x:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
commander@^2.18.0, commander@^2.19.0, commander@^2.20.0: commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.8.1:
version "2.20.3" version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -3622,6 +3642,11 @@ cssstyle@^2.2.0:
dependencies: dependencies:
cssom "~0.3.6" cssom "~0.3.6"
cycle@1.0.x:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI=
cyclist@^1.0.1: cyclist@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@ -3643,6 +3668,15 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0" whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0" whatwg-url "^8.0.0"
datauri@^1.0.5:
version "1.1.0"
resolved "https://registry.yarnpkg.com/datauri/-/datauri-1.1.0.tgz#c6184ff6b928ede4e41ccc23ab954c7839c4fb39"
integrity sha512-0q+cTTKx7q8eDteZRIQLTFJuiIsVing17UbWTPssY4JLSMaYsk/VKpNulBDo9NSgQWcvlPrkEHW8kUO67T/7mQ==
dependencies:
image-size "^0.6.2"
mimer "^0.3.2"
semver "^5.5.0"
date-fns@^2.9.0: date-fns@^2.9.0:
version "2.15.0" version "2.15.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f"
@ -3756,7 +3790,7 @@ default-gateway@^5.0.5:
dependencies: dependencies:
execa "^3.3.0" execa "^3.3.0"
defaults@^1.0.3: defaults@^1.0.2, defaults@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
@ -4635,6 +4669,11 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
eyes@0.1.x:
version "0.1.8"
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=
fast-deep-equal@^3.1.1: fast-deep-equal@^3.1.1:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -4950,6 +4989,16 @@ from@~0:
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
fs-extra@^0.23.1:
version "0.23.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.23.1.tgz#6611dba6adf2ab8dc9c69fab37cddf8818157e3d"
integrity sha1-ZhHbpq3yq43Jxp+rN83fiBgVfj0=
dependencies:
graceful-fs "^4.1.2"
jsonfile "^2.1.0"
path-is-absolute "^1.0.0"
rimraf "^2.2.8"
fs-extra@^7.0.1: fs-extra@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
@ -5060,6 +5109,25 @@ github-buttons@^2.8.0:
resolved "https://registry.yarnpkg.com/github-buttons/-/github-buttons-2.14.0.tgz#32ce381651091accda09217cd7a6e4c77f91e222" resolved "https://registry.yarnpkg.com/github-buttons/-/github-buttons-2.14.0.tgz#32ce381651091accda09217cd7a6e4c77f91e222"
integrity sha512-rAwKwFOiWoyhb3g5ZyXjI3XXprAa36jCd0tm467aEUYtiDZkqEXkepuzNg9LryLbnuLRQmcifIPTxLUBnuYpXQ== integrity sha512-rAwKwFOiWoyhb3g5ZyXjI3XXprAa36jCd0tm467aEUYtiDZkqEXkepuzNg9LryLbnuLRQmcifIPTxLUBnuYpXQ==
github-wikito-converter@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/github-wikito-converter/-/github-wikito-converter-1.5.2.tgz#ae78bfdcdee86dc0137064d585543c59dbe47cc1"
integrity sha512-m7CcUUovtUCH5kwxmClxDnC0Q/QgepGr1SCZpyqIjshxTBdx1o7mKOmKboMQzdbXk/8DRLZ1Bbc1h3cKWx2IRw==
dependencies:
bluebird "^2.9.34"
bootstrap "^3.3.7"
commander "^2.8.1"
datauri "^1.0.5"
defaults "^1.0.2"
fs-extra "^0.23.1"
highlight.js "^9.12.0"
jquery "^2.1.4"
marked "^0.3.6"
node-dir "^0.1.9"
open "0.0.5"
winston "^1.0.1"
wkhtmltopdf "^0.1.5"
glob-parent@5.1.0: glob-parent@5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2"
@ -5372,7 +5440,7 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
highlight.js@^9.6.0: highlight.js@^9.12.0, highlight.js@^9.6.0:
version "9.18.3" version "9.18.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634"
integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ== integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==
@ -5650,6 +5718,11 @@ ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
image-size@^0.6.2:
version "0.6.3"
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.6.3.tgz#e7e5c65bb534bd7cdcedd6cb5166272a85f75fb2"
integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==
import-cwd@^2.0.0: import-cwd@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@ -6188,7 +6261,7 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isstream@~0.1.2: isstream@0.1.x, isstream@~0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
@ -6250,6 +6323,11 @@ jest-worker@^25.4.0:
merge-stream "^2.0.0" merge-stream "^2.0.0"
supports-color "^7.0.0" supports-color "^7.0.0"
jquery@^2.1.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02"
integrity sha1-LInWiJterFIqfuoywUUhVZxsvwI=
js-beautify@^1.11.0, js-beautify@^1.6.12: js-beautify@^1.11.0, js-beautify@^1.6.12:
version "1.11.0" version "1.11.0"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.11.0.tgz#afb873dc47d58986360093dcb69951e8bcd5ded2" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.11.0.tgz#afb873dc47d58986360093dcb69951e8bcd5ded2"
@ -6395,6 +6473,13 @@ json5@^2.1.1, json5@^2.1.2:
dependencies: dependencies:
minimist "^1.2.5" minimist "^1.2.5"
jsonfile@^2.1.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug=
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^4.0.0: jsonfile@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
@ -6749,6 +6834,11 @@ markdown-table@^2.0.0:
dependencies: dependencies:
repeat-string "^1.0.0" repeat-string "^1.0.0"
marked@^0.3.6:
version "0.3.19"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==
mathml-tag-names@^2.1.3: mathml-tag-names@^2.1.3:
version "2.1.3" version "2.1.3"
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
@ -6913,6 +7003,11 @@ mime@^2.3.1, mime@^2.4.4:
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
mimer@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/mimer/-/mimer-0.3.2.tgz#0b83aabdf48eaacfd2e093ed4c0ed3d38eda8073"
integrity sha512-N6NcgDQAevhP/02DQ/epK6daLy4NKrIHyTlJcO6qBiYn98q+Y4a/knNsAATCe1xLS2F0nEmJp+QYli2s8vKwyQ==
mimic-fn@^1.0.0: mimic-fn@^1.0.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
@ -7222,6 +7317,13 @@ no-case@^3.0.3:
lower-case "^2.0.1" lower-case "^2.0.1"
tslib "^1.10.0" tslib "^1.10.0"
node-dir@^0.1.9:
version "0.1.17"
resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5"
integrity sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU=
dependencies:
minimatch "^3.0.2"
node-environment-flags@1.0.6: node-environment-flags@1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088"
@ -7496,6 +7598,11 @@ onetime@^5.1.0:
dependencies: dependencies:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
open@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc"
integrity sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=
open@^6.3.0: open@^6.3.0:
version "6.4.0" version "6.4.0"
resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9"
@ -7954,6 +8061,11 @@ pkg-dir@^4.1.0:
dependencies: dependencies:
find-up "^4.0.0" find-up "^4.0.0"
pkginfo@0.3.x:
version "0.3.1"
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=
pnp-webpack-plugin@^1.6.4: pnp-webpack-plugin@^1.6.4:
version "1.6.4" version "1.6.4"
resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
@ -9185,6 +9297,11 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.2:
version "7.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
send@0.17.1: send@0.17.1:
version "0.17.1" version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@ -9335,6 +9452,11 @@ simple-swizzle@^0.2.2:
dependencies: dependencies:
is-arrayish "^0.3.1" is-arrayish "^0.3.1"
slang@>=0.2:
version "0.3.0"
resolved "https://registry.yarnpkg.com/slang/-/slang-0.3.0.tgz#13af75b4f0c018c6a8193d704f65b23be4fbabdc"
integrity sha1-E691tPDAGMaoGT1wT2WyO+T7q9w=
slash@^1.0.0: slash@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@ -9601,6 +9723,11 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
stack-trace@0.0.x:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
stack-utils@^1.0.1: stack-utils@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
@ -10691,7 +10818,7 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vue-apexcharts@^1.5.2: vue-apexcharts@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-apexcharts/-/vue-apexcharts-1.6.0.tgz#c7d3afd93f712433d404e5ceeb4e3aa65f422af2" resolved "https://registry.yarnpkg.com/vue-apexcharts/-/vue-apexcharts-1.6.0.tgz#c7d3afd93f712433d404e5ceeb4e3aa65f422af2"
integrity sha512-sT6tuVTLBwfH3TA7azecDNS/W70bmz14ZJI7aE7QIqcG9I6OywyH7x3hcOeY1v1DxttI8Svc5RuYj4Dd+A5F4g== integrity sha512-sT6tuVTLBwfH3TA7azecDNS/W70bmz14ZJI7aE7QIqcG9I6OywyH7x3hcOeY1v1DxttI8Svc5RuYj4Dd+A5F4g==
@ -11201,6 +11328,26 @@ wide-align@1.1.3:
dependencies: dependencies:
string-width "^1.0.2 || 2" string-width "^1.0.2 || 2"
winston@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/winston/-/winston-1.1.2.tgz#68edd769ff79d4f9528cf0e5d80021aade67480c"
integrity sha1-aO3Xaf951PlSjPDl2AAhqt5nSAw=
dependencies:
async "~1.0.0"
colors "1.0.x"
cycle "1.0.x"
eyes "0.1.x"
isstream "0.1.x"
pkginfo "0.3.x"
stack-trace "0.0.x"
wkhtmltopdf@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/wkhtmltopdf/-/wkhtmltopdf-0.1.6.tgz#e9db84eb10d4ee50a40f4c3a7f58ca3d5d365ec4"
integrity sha1-6duE6xDU7lCkD0w6f1jKPV02XsQ=
dependencies:
slang ">=0.2"
word-wrap@~1.2.3: word-wrap@~1.2.3:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"

7
go.mod
View File

@ -5,6 +5,7 @@ go 1.14
require ( require (
github.com/GeertJohan/go.rice v1.0.0 github.com/GeertJohan/go.rice v1.0.0
github.com/aws/aws-sdk-go v1.30.20
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fatih/structs v1.1.0 github.com/fatih/structs v1.1.0
github.com/foomo/simplecert v1.7.5 github.com/foomo/simplecert v1.7.5
@ -12,6 +13,8 @@ require (
github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/getsentry/sentry-go v0.5.1 github.com/getsentry/sentry-go v0.5.1
github.com/go-mail/mail v2.3.1+incompatible github.com/go-mail/mail v2.3.1+incompatible
github.com/golang/protobuf v1.4.0
github.com/gomarkdown/markdown v0.0.0-20200820230800-3724143f5294 // indirect
github.com/gorilla/mux v1.7.4 github.com/gorilla/mux v1.7.4
github.com/hako/durafmt v0.0.0-20200605151348-3a43fc422dd9 github.com/hako/durafmt v0.0.0-20200605151348-3a43fc422dd9
github.com/jinzhu/gorm v1.9.12 github.com/jinzhu/gorm v1.9.12
@ -19,6 +22,7 @@ require (
github.com/pelletier/go-toml v1.7.0 // indirect github.com/pelletier/go-toml v1.7.0 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.1.0 github.com/prometheus/client_golang v1.1.0
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.5.0 github.com/sirupsen/logrus v1.5.0
github.com/spf13/afero v1.2.2 // indirect github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect
@ -28,6 +32,9 @@ require (
github.com/spf13/viper v1.6.3 github.com/spf13/viper v1.6.3
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1 github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1
github.com/tdewolff/minify/v2 v2.8.0 // indirect
github.com/wellington/go-libsass v0.9.2
github.com/wellington/sass v0.0.0-20160911051022-cab90b3986d6
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d

18
go.sum
View File

@ -114,6 +114,7 @@ github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -253,6 +254,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20200820230800-3724143f5294 h1:rSb2ZQZ3B1rlWBWamxobyn0jTuGZHbPO5Rmjw48uWRM=
github.com/gomarkdown/markdown v0.0.0-20200820230800-3724143f5294/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -397,6 +400,7 @@ github.com/liquidweb/liquidweb-go v1.6.1/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVL
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@ -523,6 +527,8 @@ github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3
github.com/sacloud/libsacloud v1.36.1 h1:tCpFjWsvu/2Im8/SDmRZ49SttVXy7nHerobRc1LU9pI= github.com/sacloud/libsacloud v1.36.1 h1:tCpFjWsvu/2Im8/SDmRZ49SttVXy7nHerobRc1LU9pI=
github.com/sacloud/libsacloud v1.36.1/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg= github.com/sacloud/libsacloud v1.36.1/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@ -574,6 +580,13 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1 h1:HGVkRrwDCbmSP6h1CoBDj6l/mhnvsP5JbYaQ4ss0R6o= github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1 h1:HGVkRrwDCbmSP6h1CoBDj6l/mhnvsP5JbYaQ4ss0R6o=
github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1/go.mod h1:I3xbaE9ud9/TEXzehwkHx86SyJwqeSNsX2X5oV61jIg= github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1/go.mod h1:I3xbaE9ud9/TEXzehwkHx86SyJwqeSNsX2X5oV61jIg=
github.com/tdewolff/minify v1.1.0 h1:nxHQi1ML+g3ZbZHffiZ6eC7vMqNvSRfX3KB5Y5y/kfw=
github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
github.com/tdewolff/minify/v2 v2.8.0 h1:t3tOPWkTpKhsgxm3IM9Sy8hE2eIt30Oaa+2havJGGIE=
github.com/tdewolff/minify/v2 v2.8.0/go.mod h1:6zN8VLhMfFxNrwHROcboYNo2+huPNu4SV8DPh3PUQ8E=
github.com/tdewolff/parse/v2 v2.4.4 h1:uMdbQRtYbKR/msP9CbI7li9wK6pionYiH6s7ipltyGY=
github.com/tdewolff/parse/v2 v2.4.4/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7 h1:CpHxIaZzVy26GqJn8ptRyto8fuoYOd1v0fXm9bG3wQ8= github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7 h1:CpHxIaZzVy26GqJn8ptRyto8fuoYOd1v0fXm9bG3wQ8=
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY= github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -597,6 +610,10 @@ github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA= github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
github.com/vultr/govultr v0.3.3 h1:fVaF4h9u3VzTXxFsxvgBUCiM52EiahLqAPkizamLzYM= github.com/vultr/govultr v0.3.3 h1:fVaF4h9u3VzTXxFsxvgBUCiM52EiahLqAPkizamLzYM=
github.com/vultr/govultr v0.3.3/go.mod h1:TUuUizMOFc7z+PNMssb6iGjKjQfpw5arIaOLfocVudQ= github.com/vultr/govultr v0.3.3/go.mod h1:TUuUizMOFc7z+PNMssb6iGjKjQfpw5arIaOLfocVudQ=
github.com/wellington/go-libsass v0.9.2 h1:6Ims04UDdBs6/CGSVK5JC8FNikR5ssrsMMKE/uaO5Q8=
github.com/wellington/go-libsass v0.9.2/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs=
github.com/wellington/sass v0.0.0-20160911051022-cab90b3986d6 h1:qPS12y9iMXyKr2flmOG7RgiyUGkQxQibp1hx7uug9IQ=
github.com/wellington/sass v0.0.0-20160911051022-cab90b3986d6/go.mod h1:ncYBwTYUjmb7N+sZbf8WJYynLivoqFL+U2f8uOX2Yzk=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
@ -623,6 +640,7 @@ go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZ
go.uber.org/ratelimit v0.1.0 h1:U2AruXqeTb4Eh9sYQSTrMhH8Cb7M0Ian2ibBOnBcnAw= go.uber.org/ratelimit v0.1.0 h1:U2AruXqeTb4Eh9sYQSTrMhH8Cb7M0Ian2ibBOnBcnAw=
go.uber.org/ratelimit v0.1.0/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= go.uber.org/ratelimit v0.1.0/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

View File

@ -90,7 +90,7 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
} }
app.UseCdn = null.NewNullBool(c.UseCdn.Bool) app.UseCdn = null.NewNullBool(c.UseCdn.Bool)
app.AllowReports = null.NewNullBool(c.AllowReports.Bool) app.AllowReports = null.NewNullBool(c.AllowReports.Bool)
utils.SentryInit(nil, app.AllowReports.Bool) utils.SentryInit(app.AllowReports.Bool)
err = app.Update() err = app.Update()
returnJson(core.App, w, r) returnJson(core.App, w, r)
} }

View File

@ -32,7 +32,7 @@ func init() {
utils.InitLogs() utils.InitLogs()
source.Assets() source.Assets()
dir = utils.Directory dir = utils.Directory
core.New("test") core.New("test", "testcommithere")
} }
func TestFailedHTTPServer(t *testing.T) { func TestFailedHTTPServer(t *testing.T) {
@ -205,12 +205,6 @@ func TestMainApiRoutes(t *testing.T) {
return nil return nil
}, },
}, },
{
Name: "404 Error Page",
URL: "/api/missing_404_page",
Method: "GET",
ExpectedStatus: 404,
},
{ {
Name: "Health Check endpoint", Name: "Health Check endpoint",
URL: "/health", URL: "/health",

View File

@ -14,6 +14,10 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
ExecuteResponse(w, r, "base.gohtml", core.App, nil) ExecuteResponse(w, r, "base.gohtml", core.App, nil)
} }
func baseHandler(w http.ResponseWriter, r *http.Request) {
ExecuteResponse(w, r, "base.gohtml", core.App, nil)
}
func healthCheckHandler(w http.ResponseWriter, r *http.Request) { func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
health := map[string]interface{}{ health := map[string]interface{}{
"services": len(services.All()), "services": len(services.All()),
@ -22,8 +26,3 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
} }
returnJson(health, w, r) returnJson(health, w, r)
} }
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
ExecuteResponse(w, r, "base.gohtml", core.App, nil)
}

View File

@ -161,7 +161,7 @@ func TestApiNotifiersRoutes(t *testing.T) {
Method: "POST", Method: "POST",
Body: `{ Body: `{
"method": "slack", "method": "slack",
"host": "https://slack.api/example/12345", "host": "https://hooks.slack.com/services/TTJ1B49DP/XBNU09O9M/9uI2123SUnYBuGcxLopZomz9H",
"enabled": true, "enabled": true,
"limits": 55 "limits": 55
}`, }`,
@ -173,7 +173,7 @@ func TestApiNotifiersRoutes(t *testing.T) {
URL: "/api/notifier/slack", URL: "/api/notifier/slack",
Method: "GET", Method: "GET",
ExpectedStatus: 200, ExpectedStatus: 200,
ExpectedContains: []string{`"method":"slack"`, `"host":"https://slack.api/example/12345"`}, ExpectedContains: []string{`"method":"slack"`, `"host":"https://hooks.slack.com/services/TTJ1B49DP/XBNU09O9M/9uI2123SUnYBuGcxLopZomz9H"`},
BeforeTest: SetTestENV, BeforeTest: SetTestENV,
SecureRoute: true, SecureRoute: true,
}, },

View File

@ -9,6 +9,7 @@ import (
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
"time"
_ "github.com/statping/statping/types/metrics" _ "github.com/statping/statping/types/metrics"
) )
@ -38,7 +39,7 @@ func Router() *mux.Router {
} }
bPath := utils.Params.GetString("BASE_PATH") bPath := utils.Params.GetString("BASE_PATH")
sentryHandler := sentryhttp.New(sentryhttp.Options{}) sentryHandler := sentryhttp.New(sentryhttp.Options{Timeout: 5 * time.Second})
if bPath != "" { if bPath != "" {
basePath = "/" + bPath + "/" basePath = "/" + bPath + "/"
@ -180,7 +181,7 @@ func Router() *mux.Router {
// API Generic Routes // API Generic Routes
r.Handle("/metrics", readOnly(promhttp.Handler(), false)) r.Handle("/metrics", readOnly(promhttp.Handler(), false))
r.Handle("/health", http.HandlerFunc(healthCheckHandler)) r.Handle("/health", http.HandlerFunc(healthCheckHandler))
r.NotFoundHandler = http.HandlerFunc(notFoundHandler) r.NotFoundHandler = http.HandlerFunc(baseHandler)
return r return r
} }

View File

@ -94,9 +94,9 @@ func startSSLServer(ip string) {
PreferServerCipherSuites: true, PreferServerCipherSuites: true,
CipherSuites: []uint16{ CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
}, },
} }
srv := &http.Server{ srv := &http.Server{

View File

@ -101,7 +101,7 @@ func apiServicePatchHandler(w http.ResponseWriter, r *http.Request) {
} }
if !req.Online { if !req.Online {
services.RecordFailure(service, issueDefault) services.RecordFailure(service, issueDefault, "trigger")
} else { } else {
services.RecordSuccess(service) services.RecordSuccess(service)
} }

150
notifiers/amazon_sns.go Normal file
View File

@ -0,0 +1,150 @@
package notifiers
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sns"
"github.com/statping/statping/types/null"
"time"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/notifier"
"github.com/statping/statping/types/services"
)
var _ notifier.Notifier = (*amazonSNS)(nil)
type amazonSNS struct {
*notifications.Notification
}
func (g *amazonSNS) Select() *notifications.Notification {
return g.Notification
}
func (g *amazonSNS) Valid(values notifications.Values) error {
return nil
}
var AmazonSNS = &amazonSNS{&notifications.Notification{
Method: "amazon_sns",
Title: "Amazon SNS",
Description: "Use amazonSNS to receive push notifications. Add your amazonSNS URL and App Token to receive notifications.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Icon: "amazon",
Delay: 5 * time.Second,
Limits: 60,
SuccessData: null.NewNullString(`{{.Service.Name}} is back online and was down for {{.Service.Downtime.Human}}`),
FailureData: null.NewNullString(`{{.Service.Name}} is offline and has been down for {{.Service.Downtime.Human}}`),
DataType: "html",
Form: []notifications.NotificationForm{{
Type: "text",
Title: "AWS Access Token",
DbField: "api_key",
Placeholder: "AKPMED5XUXSEU3O5AB6M",
Required: true,
}, {
Type: "text",
Title: "AWS Secret Key",
DbField: "api_secret",
Placeholder: "39eAZODxEosHRgzLx173ttX9sCtJVOE8rzElRE9B",
Required: true,
}, {
Type: "text",
Title: "Region",
SmallText: "Amazon Region for SNS",
DbField: "var1",
Placeholder: "us-west-2",
Required: true,
}, {
Type: "text",
Title: "SNS Topic ARN",
SmallText: "The ARN of the Topic",
DbField: "host",
Placeholder: "arn:aws:sns:us-west-2:123456789012:YourTopic",
Required: true,
}}},
}
func valToAttr(val interface{}) *sns.MessageAttributeValue {
dataType := "String"
switch val.(type) {
case string, bool:
dataType = "String"
case int, int64, uint, uint64, uint32:
dataType = "Number"
}
return &sns.MessageAttributeValue{
DataType: aws.String(dataType),
StringValue: aws.String(fmt.Sprintf("%v", val)),
}
}
func messageAttributesSNS(s services.Service, f failures.Failure) map[string]*sns.MessageAttributeValue {
attr := make(map[string]*sns.MessageAttributeValue)
attr["service_id"] = valToAttr(s.Id)
attr["online"] = valToAttr(s.Online)
attr["downtime_milliseconds"] = valToAttr(s.Downtime().Milliseconds())
if f.Id != 0 {
attr["failure_issue"] = valToAttr(f.Issue)
attr["failure_reason"] = valToAttr(f.Reason)
attr["failure_status_code"] = valToAttr(f.ErrorCode)
attr["failure_ping"] = valToAttr(f.PingTime)
}
return attr
}
// Send will send a HTTP Post to the amazonSNS API. It accepts type: string
func (g *amazonSNS) sendMessage(msg string, s services.Service, f failures.Failure) (string, error) {
creds := credentials.NewStaticCredentials(g.ApiKey.String, g.ApiSecret.String, "")
c := aws.NewConfig()
c.Credentials = creds
c.Region = aws.String(g.Var1.String)
sess, err := session.NewSession(c)
if err != nil {
return "", err
}
client := sns.New(sess)
input := &sns.PublishInput{
Message: aws.String(msg),
TopicArn: aws.String(g.Host.String),
MessageAttributes: messageAttributesSNS(s, f),
}
result, err := client.Publish(input)
if err != nil {
return "", err
}
return result.String(), nil
}
// OnFailure will trigger failing service
func (g *amazonSNS) OnFailure(s services.Service, f failures.Failure) (string, error) {
msg := ReplaceVars(g.FailureData.String, s, f)
return g.sendMessage(msg, s, f)
}
// OnSuccess will trigger successful service
func (g *amazonSNS) OnSuccess(s services.Service) (string, error) {
msg := ReplaceVars(g.SuccessData.String, s, failures.Failure{})
return g.sendMessage(msg, s, failures.Failure{})
}
// OnTest will test the amazonSNS notifier
func (g *amazonSNS) OnTest() (string, error) {
s := services.Example(true)
f := failures.Example()
msg := ReplaceVars(`This is a test SNS notification from Statping. Service: {{.Service.Name}} - Downtime: {{.Service.Downtime.Human}}`, s, f)
return g.sendMessage(msg, s, f)
}
// OnSave will trigger when this notifier is saved
func (g *amazonSNS) OnSave() (string, error) {
return "", nil
}

View File

@ -0,0 +1,76 @@
package notifiers
import (
"testing"
"time"
"github.com/statping/statping/database"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/null"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAmazonSNSNotifier(t *testing.T) {
err := utils.InitLogs()
require.Nil(t, err)
snsToken := utils.Params.GetString("SNS_TOKEN")
snsSecret := utils.Params.GetString("SNS_SECRET")
snsRegion := utils.Params.GetString("SNS_REGION")
snsTopic := utils.Params.GetString("SNS_TOPIC")
db, err := database.OpenTester()
require.Nil(t, err)
db.AutoMigrate(&notifications.Notification{})
notifications.SetDB(db)
core.Example()
if snsToken == "" || snsSecret == "" || snsRegion == "" || snsTopic == "" {
t.Log("SNS notifier testing skipped, missing SNS_TOKEN, SNS_SECRET, SNS_REGION, SNS_TOPIC environment variables")
t.SkipNow()
}
t.Run("Load SNS", func(t *testing.T) {
AmazonSNS.ApiKey = null.NewNullString(snsToken)
AmazonSNS.ApiSecret = null.NewNullString(snsSecret)
AmazonSNS.Var1 = null.NewNullString(snsRegion)
AmazonSNS.Host = null.NewNullString(snsTopic)
AmazonSNS.Delay = 15 * time.Second
AmazonSNS.Enabled = null.NewNullBool(true)
Add(AmazonSNS)
assert.Equal(t, "Hunter Long", AmazonSNS.Author)
assert.Equal(t, snsToken, AmazonSNS.ApiKey.String)
assert.Equal(t, snsSecret, AmazonSNS.ApiSecret.String)
})
t.Run("SNS Notifier Tester", func(t *testing.T) {
assert.True(t, AmazonSNS.CanSend())
})
t.Run("SNS Notifier Tester OnSave", func(t *testing.T) {
_, err := AmazonSNS.OnSave()
assert.Nil(t, err)
})
t.Run("SNS OnFailure", func(t *testing.T) {
_, err := AmazonSNS.OnFailure(services.Example(false), failures.Example())
assert.Nil(t, err)
})
t.Run("SNS OnSuccess", func(t *testing.T) {
_, err := AmazonSNS.OnSuccess(services.Example(true))
assert.Nil(t, err)
})
t.Run("SNS Test", func(t *testing.T) {
_, err := AmazonSNS.OnTest()
assert.Nil(t, err)
})
}

View File

@ -1,4 +1,4 @@
// DO NOT EDIT ** This file was generated with go generate on 2020-08-06 20:20:14.476432 +0000 UTC ** DO NOT EDIT // // DO NOT EDIT ** This file was generated with go generate on 2020-08-21 21:37:06.638898 +0000 UTC ** DO NOT EDIT //
package notifiers package notifiers
const emailSuccess = `<!doctype html><html xmlns=http://www.w3.org/1999/xhtml xmlns:v=urn:schemas-microsoft-com:vml xmlns:o=urn:schemas-microsoft-com:office:office><title>Statping Service Notification</title><meta http-equiv=x-ua-compatible content="IE=edge"><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><style type=text/css> const emailSuccess = `<!doctype html><html xmlns=http://www.w3.org/1999/xhtml xmlns:v=urn:schemas-microsoft-com:vml xmlns:o=urn:schemas-microsoft-com:office:office><title>Statping Service Notification</title><meta http-equiv=x-ua-compatible content="IE=edge"><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><style type=text/css>

View File

@ -79,7 +79,7 @@ func (m *mobilePush) OnFailure(s services.Service, f failures.Failure) (string,
func (m *mobilePush) OnSuccess(s services.Service) (string, error) { func (m *mobilePush) OnSuccess(s services.Service) (string, error) {
data := dataJson(s, failures.Failure{}) data := dataJson(s, failures.Failure{})
msg := &pushArray{ msg := &pushArray{
Message: fmt.Sprintf("%s is currently online!", s.Name), Message: fmt.Sprintf("%s is back online and was down for %s", s.Name, s.Downtime().Human()),
Title: "Service Online", Title: "Service Online",
Data: data, Data: data,
Platform: 2, Platform: 2,

View File

@ -19,7 +19,6 @@ var (
) )
func TestMobileNotifier(t *testing.T) { func TestMobileNotifier(t *testing.T) {
t.SkipNow()
err := utils.InitLogs() err := utils.InitLogs()
require.Nil(t, err) require.Nil(t, err)
@ -48,7 +47,7 @@ func TestMobileNotifier(t *testing.T) {
Add(Mobile) Add(Mobile)
assert.Equal(t, "Hunter Long", Mobile.Author) assert.Equal(t, "Hunter Long", Mobile.Author)
assert.Equal(t, mobileToken, Mobile.Var1) assert.Equal(t, mobileToken, Mobile.Var1.String)
}) })
t.Run("Mobile Notifier Tester", func(t *testing.T) { t.Run("Mobile Notifier Tester", func(t *testing.T) {

View File

@ -35,6 +35,7 @@ func InitNotifiers() {
Pushover, Pushover,
statpingMailer, statpingMailer,
Gotify, Gotify,
AmazonSNS,
) )
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/statping/statping/types/null" "github.com/statping/statping/types/null"
"github.com/statping/statping/types/services" "github.com/statping/statping/types/services"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"regexp"
"strings" "strings"
"time" "time"
) )
@ -93,5 +94,10 @@ func (s *slack) OnSave() (string, error) {
} }
func (s *slack) Valid(values notifications.Values) error { func (s *slack) Valid(values notifications.Values) error {
regex := `https\:\/\/hooks\.slack\.com/services/[A-Z0-9]{7,11}/[A-Z0-9]{7,11}/[a-zA-Z0-9]{20,22}`
r := regexp.MustCompile(regex)
if !r.MatchString(values.Host) {
return errors.New("slack webhook does not match with expected regex " + regex)
}
return nil return nil
} }

View File

@ -110,7 +110,7 @@ func (w *webhooker) sendHttpWebhook(body string) (*http.Response, error) {
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
} }
req.Header.Set("User-Agent", "Statping") req.Header.Set("User-Agent", "Statping")
req.Header.Set("Statping-Version", utils.Version) req.Header.Set("Statping-Version", utils.Params.GetString("VERSION"))
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err

183
source/generate_help.go Normal file
View File

@ -0,0 +1,183 @@
// +build ignore
package main
import (
"bufio"
"bytes"
"fmt"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"html/template"
"io/ioutil"
"os"
"os/exec"
"strings"
"time"
)
const wikiUrl = "https://github.com/statping/statping.wiki"
var vue = `<template>
<div class="col-12">
<div class="row mb-4">
{{ range .Categories }}
<div class="col">
<h4 class="h4 mb-2">{{ .String }}</h4>
{{ range .Pages }}
<a @click.prevent='tab="{{.String}}"' class="d-block mb-1 text-link" href="#">{{.String}}</a>
{{end}}
</div>
{{end}}
</div>
<div class="col-12" v-if='tab === "Home"'>
<div v-pre>
{{html .Home.Data}}
</div>
</div>
{{ range .Pages }}
<div class="col-12" v-if='tab === "{{.String}}"'>
<h1 class="h1 mt-5 mb-5 text-muted">{{ .String }}</h1>
<span class="spacer"></span>
<div v-pre>
{{html .Data}}
</div>
</div>
{{end}}
<div class="col-12 shadow-md mt-5">
<div class="text-dim" v-pre>
{{html .Footer.Data}}
</div>
</div>
<div class="text-center small text-dim" v-pre>
Automatically generated from Statping's Wiki on {{.CreatedAt}}
</div>
</div>
</template>
<script>
export default {
name: 'Help',
data () {
return {
tab: "Home",
}
}
}
</script>
<style scoped>
IMG {
max-width: 80%;
alignment: center;
display: block;
}
</style>
`
var temp *template.Template
type Category struct {
String string
Pages []*Page
}
type Page struct {
String string
Data string
}
type Render struct {
Categories []*Category
Pages []*Page
Home *Page
Footer *Page
CreatedAt time.Time
}
func main() {
fmt.Println("RUNNING: ./source/generate_help.go")
fmt.Println("\n\nGenerating Help.vue from Statping's Wiki")
fmt.Println("Cloning ", wikiUrl)
cmd := exec.Command("git", "clone", wikiUrl)
cmd.Start()
cmd.Wait()
fmt.Println("Generating Help view from Wiki")
d, _ := ioutil.ReadFile("statping.wiki/_Sidebar.md")
var cats []*Category
var pages []*Page
scanner := bufio.NewScanner(strings.NewReader(string(d)))
var thisCategory *Category
for scanner.Scan() {
txt := scanner.Text()
if txt == "" {
continue
}
if txt[0:1] == "#" {
newCate := &Category{
String: txt[2:len(txt)],
}
if txt[2:len(txt)] == "Contact" || txt[2:len(txt)] == "Badges" {
continue
}
thisCategory = newCate
cats = append(cats, newCate)
}
if txt[0:2] == "[[" {
file := "statping.wiki/" + txt[2:len(txt)-2] + ".md"
file = strings.ReplaceAll(file, " ", "-")
page := &Page{
String: txt[2 : len(txt)-2],
Data: open(file),
}
pages = append(pages, page)
thisCategory.Pages = append(thisCategory.Pages, page)
}
}
home := &Page{
String: "Home",
Data: open("statping.wiki/Home.md"),
}
footer := &Page{
String: "Footer",
Data: open("statping.wiki/_Footer.md"),
}
w := bytes.NewBufferString("")
temp = template.New("wiki")
temp.Funcs(template.FuncMap{
"html": func(val string) template.HTML {
return template.HTML(val)
},
"fake": func(val string) template.HTML {
return template.HTML(`{{` + val + `}}`)
},
})
temp, _ = temp.Parse(vue)
temp.ExecuteTemplate(w, "wiki", Render{Categories: cats, Pages: pages, Home: home, Footer: footer, CreatedAt: time.Now().UTC()})
fmt.Println("Saving wiki page to: ./frontend/src/pages/Home.vue")
ioutil.WriteFile("../frontend/src/pages/Help.vue", w.Bytes(), os.FileMode(0755))
fmt.Println("Deleting statping wiki repo")
os.RemoveAll("statping.wiki")
}
func open(filename string) string {
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
d, _ := ioutil.ReadFile(filename)
output := markdown.ToHTML(d, nil, renderer)
return string(output)
}

View File

@ -0,0 +1,41 @@
// +build ignore
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"regexp"
"strings"
)
const replace = `this\.version = "[0-9]\.[0-9]{2}\.[0-9]{2}";`
const replaceCommit = `this\.commit = \"[a-z0-9]{40}\"\;`
func main() {
fmt.Println("RUNNING: ./source/generate_version.go")
version, _ := ioutil.ReadFile("../version.txt")
apiJsFile, _ := ioutil.ReadFile("../frontend/src/API.js")
w := bytes.NewBuffer(nil)
cmd := exec.Command("git", "rev-parse", "HEAD")
cmd.Stdout = w
cmd.Run()
gitCommit := strings.TrimSpace(w.String())
fmt.Println("git commit: ", gitCommit)
replaceWith := `this.version = "` + strings.TrimSpace(string(version)) + `";`
replaceCommitWith := `this.commit = "` + gitCommit + `";`
vRex := regexp.MustCompile(replace)
newApiFile := vRex.ReplaceAllString(string(apiJsFile), replaceWith)
cRex := regexp.MustCompile(replaceCommit)
newApiFile = cRex.ReplaceAllString(newApiFile, replaceCommitWith)
fmt.Printf("Setting version %s to frontend/src/API.js\n", string(version))
ioutil.WriteFile("../frontend/src/API.js", []byte(newApiFile), os.FileMode(0755))
}

View File

@ -1,5 +1,8 @@
package source package source
//go:generate go run generate_help.go
//go:generate go run generate_version.go
import ( import (
"fmt" "fmt"
"github.com/GeertJohan/go.rice" "github.com/GeertJohan/go.rice"

View File

@ -21,11 +21,6 @@ import (
"github.com/statping/statping/types/users" "github.com/statping/statping/types/users"
) )
var (
Version string
Commit string
)
func (d *DbConfig) ResetCore() error { func (d *DbConfig) ResetCore() error {
if d.Db.HasTable("core") { if d.Db.HasTable("core") {
return nil return nil
@ -126,7 +121,7 @@ func (d *DbConfig) MigrateDatabase() error {
} }
} }
log.Infof("Migrating App to version: %s (%s)", Version, Commit) log.Infof("Migrating App to version: %s (%s)", utils.Params.GetString("VERSION"), utils.Params.GetString("COMMIT"))
if err := tx.Table("core").AutoMigrate(&core.Core{}); err.Error() != nil { if err := tx.Table("core").AutoMigrate(&core.Core{}); err.Error() != nil {
tx.Rollback() tx.Rollback()
log.Errorln(fmt.Sprintf("Statping Database could not be migrated: %v", tx.Error())) log.Errorln(fmt.Sprintf("Statping Database could not be migrated: %v", tx.Error()))
@ -137,7 +132,7 @@ func (d *DbConfig) MigrateDatabase() error {
return err return err
} }
d.Db.Table("core").Model(&core.Core{}).Update("version", Version) d.Db.Table("core").Model(&core.Core{}).Update("version", utils.Params.GetString("VERSION"))
log.Infoln("Statping Database Tables Migrated") log.Infoln("Statping Database Tables Migrated")

View File

@ -57,6 +57,8 @@ func Select() (*Core, error) {
if utils.Params.GetString("API_SECRET") != "" { if utils.Params.GetString("API_SECRET") != "" {
App.ApiSecret = utils.Params.GetString("API_SECRET") App.ApiSecret = utils.Params.GetString("API_SECRET")
} }
App.Version = utils.Params.GetString("VERSION")
App.Commit = utils.Params.GetString("COMMIT")
return App, q.Error() return App, q.Error()
} }

View File

@ -41,7 +41,8 @@ func Samples() error {
MigrationId: utils.Now().Unix(), MigrationId: utils.Now().Unix(),
Language: utils.Params.GetString("LANGUAGE"), Language: utils.Params.GetString("LANGUAGE"),
OAuth: oauth, OAuth: oauth,
Version: utils.Version, Version: utils.Params.GetString("VERSION"),
Commit: utils.Params.GetString("COMMIT"),
} }
return core.Create() return core.Create()

View File

@ -10,9 +10,10 @@ var (
App *Core App *Core
) )
func New(version string) { func New(version, commit string) {
App = new(Core) App = new(Core)
App.Version = version App.Version = version
App.Commit = commit
App.Started = utils.Now() App.Started = utils.Now()
} }
@ -28,6 +29,7 @@ type Core struct {
Footer null.NullString `gorm:"column:footer" json:"footer"` Footer null.NullString `gorm:"column:footer" json:"footer"`
Domain string `gorm:"not null;column:domain" json:"domain"` Domain string `gorm:"not null;column:domain" json:"domain"`
Version string `gorm:"column:version" json:"version"` Version string `gorm:"column:version" json:"version"`
Commit string `gorm:"-" json:"commit"`
Language string `gorm:"column:language" json:"language"` Language string `gorm:"column:language" json:"language"`
Setup bool `gorm:"-" json:"setup"` Setup bool `gorm:"-" json:"setup"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"` MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`

View File

@ -22,6 +22,7 @@ func Example() Failure {
Service: 1, Service: 1,
Checkin: 0, Checkin: 0,
PingTime: 48309, PingTime: 48309,
Reason: "status_code",
CreatedAt: utils.Now(), CreatedAt: utils.Now(),
} }
} }
@ -34,6 +35,7 @@ func Samples() error {
f1 := &Failure{ f1 := &Failure{
Service: i, Service: i,
Issue: "Server failure", Issue: "Server failure",
Reason: "lookup",
CreatedAt: utils.Now().Add(-time.Duration(3*i) * 86400), CreatedAt: utils.Now().Add(-time.Duration(3*i) * 86400),
} }
if err := f1.Create(); err != nil { if err := f1.Create(); err != nil {
@ -42,7 +44,8 @@ func Samples() error {
f2 := &Failure{ f2 := &Failure{
Service: i, Service: i,
Issue: "Server failure", Issue: "Regex failed to match the response",
Reason: "regex",
CreatedAt: utils.Now().Add(-time.Duration(5*i) * 12400), CreatedAt: utils.Now().Add(-time.Duration(5*i) * 12400),
} }
if err := f2.Create(); err != nil { if err := f2.Create(); err != nil {

View File

@ -2,11 +2,6 @@ package failures
import "time" import "time"
const (
limitedFailures = 32
limitedHits = 32
)
// Failure is a failed attempt to check a service. Any a service does not meet the expected requirements, // Failure is a failed attempt to check a service. Any a service does not meet the expected requirements,
// a new Failure will be inserted into Db. // a new Failure will be inserted into Db.
type Failure struct { type Failure struct {
@ -18,6 +13,7 @@ type Failure struct {
Service int64 `gorm:"index;column:service" json:"-"` Service int64 `gorm:"index;column:service" json:"-"`
Checkin int64 `gorm:"index;column:checkin" json:"-"` Checkin int64 `gorm:"index;column:checkin" json:"-"`
PingTime int64 `gorm:"column:ping_time" json:"ping"` PingTime int64 `gorm:"column:ping_time" json:"ping"`
Reason string `gorm:"column:reason" json:"reason,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
} }

View File

@ -7,6 +7,8 @@ import (
"time" "time"
) )
const limitFailures = 32
func (s *Service) FailuresColumnID() (string, int64) { func (s *Service) FailuresColumnID() (string, int64) {
return "service", s.Id return "service", s.Id
} }

View File

@ -6,6 +6,11 @@ import (
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
) )
func AddNotifier(n ServiceNotifier) {
notif := n.Select()
allNotifiers[notif.Method] = n
}
func sendSuccess(s *Service) { func sendSuccess(s *Service) {
if !s.AllowNotifications.Bool { if !s.AllowNotifications.Bool {
return return

View File

@ -94,7 +94,7 @@ func CheckIcmp(s *Service, record bool) (*Service, error) {
dur, err := utils.Ping(s.Domain, s.Timeout) dur, err := utils.Ping(s.Domain, s.Timeout)
if err != nil { if err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err)) RecordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err), "lookup")
} }
return s, err return s, err
} }
@ -118,7 +118,7 @@ func CheckGrpc(s *Service, record bool) (*Service, error) {
dnsLookup, err := dnsCheck(s) dnsLookup, err := dnsCheck(s)
if err != nil { if err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("Could not get IP address for GRPC service %v, %v", s.Domain, err)) RecordFailure(s, fmt.Sprintf("Could not get IP address for GRPC service %v, %v", s.Domain, err), "lookup")
} }
return s, err return s, err
} }
@ -137,13 +137,13 @@ func CheckGrpc(s *Service, record bool) (*Service, error) {
} }
if err != nil { if err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("Dial Error %v", err)) RecordFailure(s, fmt.Sprintf("Dial Error %v", err), "connection")
} }
return s, err return s, err
} }
if err := conn.Close(); err != nil { if err := conn.Close(); err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err)) RecordFailure(s, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err), "close")
} }
return s, err return s, err
} }
@ -165,7 +165,7 @@ func CheckTcp(s *Service, record bool) (*Service, error) {
dnsLookup, err := dnsCheck(s) dnsLookup, err := dnsCheck(s)
if err != nil { if err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err)) RecordFailure(s, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err), "lookup")
} }
return s, err return s, err
} }
@ -189,7 +189,7 @@ func CheckTcp(s *Service, record bool) (*Service, error) {
conn, err := net.DialTimeout(s.Type, domain, time.Duration(s.Timeout)*time.Second) conn, err := net.DialTimeout(s.Type, domain, time.Duration(s.Timeout)*time.Second)
if err != nil { if err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("Dial Error: %v", err)) RecordFailure(s, fmt.Sprintf("Dial Error: %v", err), "tls")
} }
return s, err return s, err
} }
@ -203,7 +203,7 @@ func CheckTcp(s *Service, record bool) (*Service, error) {
conn, err := tls.DialWithDialer(dialer, s.Type, domain, tlsConfig) conn, err := tls.DialWithDialer(dialer, s.Type, domain, tlsConfig)
if err != nil { if err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("Dial Error: %v", err)) RecordFailure(s, fmt.Sprintf("Dial Error: %v", err), "tls")
} }
return s, err return s, err
} }
@ -232,7 +232,7 @@ func CheckHttp(s *Service, record bool) (*Service, error) {
dnsLookup, err := dnsCheck(s) dnsLookup, err := dnsCheck(s)
if err != nil { if err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err)) RecordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err), "lookup")
} }
return s, err return s, err
} }
@ -284,7 +284,7 @@ func CheckHttp(s *Service, record bool) (*Service, error) {
content, res, err = utils.HttpRequest(s.Domain, s.Method, contentType, headers, data, timeout, s.VerifySSL.Bool, customTLS) content, res, err = utils.HttpRequest(s.Domain, s.Method, contentType, headers, data, timeout, s.VerifySSL.Bool, customTLS)
if err != nil { if err != nil {
if record { if record {
RecordFailure(s, fmt.Sprintf("HTTP Error %v", err)) RecordFailure(s, fmt.Sprintf("HTTP Error %v", err), "request")
} }
return s, err return s, err
} }
@ -301,14 +301,14 @@ func CheckHttp(s *Service, record bool) (*Service, error) {
} }
if !match { if !match {
if record { if record {
RecordFailure(s, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected)) RecordFailure(s, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected), "regex")
} }
return s, err return s, err
} }
} }
if s.ExpectedStatus != res.StatusCode { if s.ExpectedStatus != res.StatusCode {
if record { if record {
RecordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus)) RecordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus), "status_code")
} }
return s, err return s, err
} }
@ -341,13 +341,8 @@ func RecordSuccess(s *Service) {
sendSuccess(s) sendSuccess(s)
} }
func AddNotifier(n ServiceNotifier) {
notif := n.Select()
allNotifiers[notif.Method] = n
}
// RecordFailure will create a new 'Failure' record in the database for a offline service // RecordFailure will create a new 'Failure' record in the database for a offline service
func RecordFailure(s *Service, issue string) { func RecordFailure(s *Service, issue, reason string) {
s.LastOffline = utils.Now() s.LastOffline = utils.Now()
fail := &failures.Failure{ fail := &failures.Failure{
@ -356,6 +351,7 @@ func RecordFailure(s *Service, issue string) {
PingTime: s.PingTime, PingTime: s.PingTime,
CreatedAt: utils.Now(), CreatedAt: utils.Now(),
ErrorCode: s.LastStatusCode, ErrorCode: s.LastStatusCode,
Reason: reason,
} }
log.WithFields(utils.ToFields(fail, s)). log.WithFields(utils.ToFields(fail, s)).
Warnln(fmt.Sprintf("Service %v Failing: %v | Lookup in: %v", s.Name, issue, humanMicro(fail.PingTime))) Warnln(fmt.Sprintf("Service %v Failing: %v | Lookup in: %v", s.Name, issue, humanMicro(fail.PingTime)))
@ -365,6 +361,14 @@ func RecordFailure(s *Service, issue string) {
} }
s.Online = false s.Online = false
s.DownText = s.DowntimeText() s.DownText = s.DowntimeText()
limitOffset := len(s.Failures)
if len(s.Failures) >= limitFailures {
limitOffset = limitFailures - 1
}
s.Failures = append([]*failures.Failure{fail}, s.Failures[:limitOffset]...)
metrics.Gauge("online", 0., s.Name, s.Type) metrics.Gauge("online", 0., s.Name, s.Type)
metrics.Inc("failure", s.Name) metrics.Inc("failure", s.Name)
sendFailure(s, fail) sendFailure(s, fail)

View File

@ -3,7 +3,6 @@ package services
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/statping/statping/database" "github.com/statping/statping/database"
"github.com/statping/statping/types/checkins" "github.com/statping/statping/types/checkins"
@ -470,7 +469,7 @@ func TestServices(t *testing.T) {
item, err := Find(1) item, err := Find(1)
require.Nil(t, err) require.Nil(t, err)
amount := item.Downtime().Seconds() amount := item.Downtime().Seconds()
assert.Equal(t, "75", fmt.Sprintf("%0.f", amount)) assert.GreaterOrEqual(t, int64(75), int64(amount))
}) })
t.Run("Test Failures Since", func(t *testing.T) { t.Run("Test Failures Since", func(t *testing.T) {

View File

@ -269,7 +269,7 @@ func runNotifyTests(t *testing.T, notif *exampleNotifier, tests ...notifyTest) {
if test.OnSuccess { if test.OnSuccess {
RecordSuccess(test.Service) RecordSuccess(test.Service)
} else { } else {
RecordFailure(test.Service, "test issue") RecordFailure(test.Service, "test issue", "lookup")
} }
assert.Equal(t, test.ExpectedSuccess, notif.success) assert.Equal(t, test.ExpectedSuccess, notif.success)

View File

@ -24,11 +24,11 @@ func InitEnvs() {
Log.Errorln(err) Log.Errorln(err)
defaultDir = "." defaultDir = "."
} }
Params.Set("VERSION", Version)
Params.SetDefault("DISABLE_HTTP", false) Params.SetDefault("DISABLE_HTTP", false)
Params.SetDefault("STATPING_DIR", defaultDir) Params.SetDefault("STATPING_DIR", defaultDir)
Params.SetDefault("GO_ENV", "production") Params.SetDefault("GO_ENV", "production")
Params.SetDefault("DEBUG", false) Params.SetDefault("DEBUG", false)
Params.SetDefault("DEMO_MODE", false)
Params.SetDefault("DB_CONN", "") Params.SetDefault("DB_CONN", "")
Params.SetDefault("DISABLE_LOGS", false) Params.SetDefault("DISABLE_LOGS", false)
Params.SetDefault("USE_ASSETS", false) Params.SetDefault("USE_ASSETS", false)

View File

@ -21,7 +21,6 @@ var (
LastLines []*logRow LastLines []*logRow
LockLines sync.Mutex LockLines sync.Mutex
VerboseMode int VerboseMode int
Version string
allowReports bool allowReports bool
) )
@ -30,21 +29,15 @@ const (
errorReporter = "https://ddf2784201134d51a20c3440e222cebe@sentry.statping.com/4" errorReporter = "https://ddf2784201134d51a20c3440e222cebe@sentry.statping.com/4"
) )
func SentryInit(v *string, allow bool) { func SentryInit(allow bool) {
allowReports = allow allowReports = allow
if v != nil {
if *v == "" {
*v = "development"
}
Version = *v
}
goEnv := Params.GetString("GO_ENV") goEnv := Params.GetString("GO_ENV")
allowReports := Params.GetBool("ALLOW_REPORTS") allowReports := Params.GetBool("ALLOW_REPORTS")
if allow || goEnv == "test" || allowReports { if allow || goEnv == "test" || allowReports {
if err := sentry.Init(sentry.ClientOptions{ if err := sentry.Init(sentry.ClientOptions{
Dsn: errorReporter, Dsn: errorReporter,
Environment: goEnv, Environment: goEnv,
Release: Version, Release: Params.GetString("VERSION"),
AttachStacktrace: true, AttachStacktrace: true,
}); err != nil { }); err != nil {
Log.Errorln(err) Log.Errorln(err)
@ -70,7 +63,7 @@ func SentryLogEntry(entry *Logger.Entry) {
e := sentry.NewEvent() e := sentry.NewEvent()
e.Message = entry.Message e.Message = entry.Message
e.Tags = sentryTags() e.Tags = sentryTags()
e.Release = Version e.Release = Params.GetString("VERSION")
e.Contexts = entry.Data e.Contexts = entry.Data
sentry.CaptureEvent(e) sentry.CaptureEvent(e)
} }

View File

@ -174,7 +174,7 @@ func HttpRequest(endpoint, method string, contentType interface{}, headers []str
} }
// set default headers so end user can overwrite them if needed // set default headers so end user can overwrite them if needed
req.Header.Set("User-Agent", "Statping") req.Header.Set("User-Agent", "Statping")
req.Header.Set("Statping-Version", Version) req.Header.Set("Statping-Version", Params.GetString("VERSION"))
if contentType != nil { if contentType != nil {
req.Header.Set("Content-Type", contentType.(string)) req.Header.Set("Content-Type", contentType.(string))
} }

View File

@ -1 +1 @@
0.90.63 0.90.64