Merge pull request #778 from statping/dev

v0.90.62
pull/783/head^2
Hunter Long 2020-08-09 20:22:03 -07:00 committed by GitHub
commit 977013550a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
163 changed files with 3072 additions and 2212 deletions

View File

@ -21,6 +21,12 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '12.18.2'
- name: Configure AWS credentials from account
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- uses: actions/checkout@v2
- name: Add GOBIN to PATH
@ -29,6 +35,13 @@ jobs:
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Font Awesome authentication
env:
FONTAWESOME_TOKEN: ${{ secrets.FONTAWESOME_TOKEN }}
run: |
npm config set "@fortawesome:registry" https://npm.fontawesome.com/
npm config set "//npm.fontawesome.com/:_authToken" $FONTAWESOME_TOKEN
- name: Install Global Dependencies
run: npm install -g yarn sass cross-env
@ -53,6 +66,12 @@ jobs:
name: static-rice-box
path: ./source
- name: Upload Assets to S3
run: |
tar -czvf source.tar.gz source/
aws s3 cp source.tar.gz s3://assets.statping.com/
rm -rf source.tar.gz
test:
needs: compile
runs-on: ubuntu-latest
@ -116,6 +135,7 @@ jobs:
gotestsum --no-summary=skipped --format dots -- -covermode=count -coverprofile=coverage.out -p=1 ./...
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
DB_CONN: sqlite3
STATPING_DIR: ${{ github.workspace }}
API_SECRET: demopassword123
@ -231,6 +251,8 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
run: |
make build
chmod +x statping
@ -287,6 +309,7 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: |
make build
chmod +x statping
@ -359,7 +382,9 @@ jobs:
- name: Build Binaries
env:
VERSION: ${{ env.VERSION }}
COMMIT: $GITHUB_SHA
COMMIT: ${{ github.sha }}
MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
run: make build-folders build-linux build-linux-arm build-darwin build-win compress-folders
docker-release:
@ -392,7 +417,13 @@ jobs:
buildx-docker-master
- name: Docker Build :base
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: make buildx-base
- name: Docker Build :dev
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: make buildx-dev

View File

@ -16,6 +16,12 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '12.18.2'
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- uses: actions/checkout@v2
- name: Add GOBIN to PATH
@ -24,6 +30,13 @@ jobs:
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Font Awesome authentication
env:
FONTAWESOME_TOKEN: ${{ secrets.FONTAWESOME_TOKEN }}
run: |
npm config set "@fortawesome:registry" https://npm.fontawesome.com/
npm config set "//npm.fontawesome.com/:_authToken" $FONTAWESOME_TOKEN
- name: Install Global Dependencies
run: npm install -g yarn sass cross-env
@ -48,6 +61,19 @@ jobs:
name: static-rice-box
path: ./source
- name: Upload Assets to S3
run: |
tar -czvf source.tar.gz source/
cp source.tar.gz source-${VERSION}.tar.gz
aws s3 cp source.tar.gz s3://assets.statping.com/
aws s3 cp source-${VERSION}.tar.gz s3://assets.statping.com/
rm -rf source.tar.gz
rm -rf source-${VERSION}.tar.gz
aws s3 cp source/dist/css/ s3://assets.statping.com/css/ --recursive
aws s3 cp source/dist/js/ s3://assets.statping.com/js/ --recursive
aws s3 cp source/dist/scss/ s3://assets.statping.com/scss/ --recursive
aws s3 cp install.sh s3://assets.statping.com/
test:
needs: compile
runs-on: ubuntu-latest
@ -226,6 +252,8 @@ jobs:
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
run: |
make build
chmod +x statping
@ -355,6 +383,8 @@ jobs:
env:
VERSION: ${{ env.VERSION }}
COMMIT: $GITHUB_SHA
MJML_APP: ${{ secrets.MJML_APP }}
MJML_PRIVATE: ${{ secrets.MJML_PRIVATE }}
run: make build-folders build-linux build-linux-arm build-darwin build-win compress-folders
- name: Upload Builds
@ -434,9 +464,15 @@ jobs:
buildx-docker
- name: Docker Build :base
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: make buildx-base
- name: Docker Build :lastest
env:
VERSION: ${{ env.VERSION }}
COMMIT: ${{ github.sha }}
run: make buildx-latest
sentry-release:

View File

@ -25,6 +25,13 @@ jobs:
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Font Awesome authentication
env:
FONTAWESOME_TOKEN: ${{ secrets.FONTAWESOME_TOKEN }}
run: |
npm config set "@fortawesome:registry" https://npm.fontawesome.com/
npm config set "//npm.fontawesome.com/:_authToken" $FONTAWESOME_TOKEN
- name: Install Global Dependencies
run: npm install -g yarn sass cross-env

View File

@ -1,3 +1,18 @@
# 0.90.62 (08-07-2020)
- Added Notification logs
- Fixed issues with Notifer After (x) failures for notifications
- Modified notifications to not send on initial startup
- Updated Incident UI
- Added additional testing for notifications
- Modified SCSS/SASS files to be generated from 1, main.scss to main.css
- Modified index page to use /assets directory for assets, (main.css, style.css)
- Modified index page to use CDN asset paths
- Fixed New Checkin form
- Modified email notifier template to be rendered from MJML (using go generate)
- Modified database relationships with services using gorm
- Modified "statping env" command to show user/group ID
- Removed "js" folder when exporting assets, js files are always version of release, not static JS files
# 0.90.61 (07-22-2020)
- Modified sass layouts, organized and split up sections
- Modified Checkins to seconds rather than milliseconds (for cronjob)

19
CloudronManifest.json Normal file
View File

@ -0,0 +1,19 @@
{
"id": "com.statping",
"title": "Statping",
"author": "Hunter Long <info@statping.com>",
"description": "Monitor your web services and remote servers",
"tagline": "Server Monitoring",
"version": "0.90.61",
"healthCheckPath": "/health",
"httpPort": 8080,
"addons": {
"localstorage": {}
},
"manifestVersion": 2,
"website": "https://github.com/statping/statping",
"contactEmail": "info@statping.com",
"icon": "https://assets.statping.com/icon.png",
"tags": [ "monitoring", "uptime" ],
"mediaLinks": [ "https://assets.statping.com/cloudron.png" ]
}s

View File

@ -13,6 +13,7 @@ RUN yarn build && yarn cache clean
FROM golang:1.14-alpine AS backend
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
ARG VERSION
ARG COMMIT
ARG BUILDPLATFORM
ARG TARGETARCH
RUN apk add --update --no-cache libstdc++ gcc g++ make git autoconf \
@ -36,8 +37,8 @@ RUN go get github.com/stretchr/testify/assert && \
go get github.com/crazy-max/xgo
COPY . .
COPY --from=frontend /statping/dist/ ./source/dist/
RUN make clean frontend-copy generate embed
RUN GOOS=linux GOARCH=$TARGETARCH go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o statping --tags "netgo linux" ./cmd
RUN make clean generate embed
RUN go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo linux" ./cmd
RUN chmod a+x statping && mv statping /go/bin/statping
# /go/bin/statping - statping binary
# /root/sassc/bin/sassc - sass binary

View File

@ -1,11 +1,12 @@
VERSION=$(shell cat version.txt)
COMMIT=$(shell git rev-parse HEAD)
SIGN_KEY=B76D61FAA6DB759466E83D9964B9C6AAE2D55278
BINARY_NAME=statping
GOBUILD=go build -a
GOVERSION=1.14.0
NODE_VERSION=12.18.2
XGO=xgo -go $(GOVERSION) --dest=build
BUILDVERSION=-ldflags "-X main.VERSION=${VERSION}"
BUILDVERSION=-ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}"
TRVIS_SECRET=O3/2KTOV8krv+yZ1EB/7D1RQRe6NdpFUEJNJkMS/ollYqmz3x2mCO7yIgIJKCKguLXZxjM6CxJcjlCrvUwibL+8BBp7xJe4XFIOrjkPvbbVPry4HkFZCf2GfcUK6o4AByQ+RYqsW2F17Fp9KLQ1rL3OT3eLTwCAGKx3tlY8y+an43zkmo5dN64V6sawx26fh6XTfww590ey+ltgQTjf8UPNup2wZmGvMo9Hwvh/bYR/47bR6PlBh6vhlKWyotKf2Fz1Bevbu0zc35pee5YlsrHR+oSF+/nNd/dOij34BhtqQikUR+zQVy9yty8SlmneVwD3yOENvlF+8roeKIXb6P6eZnSMHvelhWpAFTwDXq2N3d/FIgrQtLxsAFTI3nTHvZgs6OoTd6dA0wkhuIGLxaL3FOeztCdxP5J/CQ9GUcTvifh5ArGGwYxRxQU6rTgtebJcNtXFISP9CEUR6rwRtb6ax7h6f1SbjUGAdxt+r2LbEVEk4ZlwHvdJ2DtzJHT5DQtLrqq/CTUgJ8SJFMkrJMp/pPznKhzN4qvd8oQJXygSXX/gz92MvoX0xgpNeLsUdAn+PL9KketfR+QYosBz04d8k05E+aTqGaU7FUCHPTLwlOFvLD8Gbv0zsC/PWgSLXTBlcqLEz5PHwPVHTcVzspKj/IyYimXpCSbvu1YOIjyc=
PUBLISH_BODY='{ "request": { "branch": "master", "message": "Homebrew update version v${VERSION}", "config": { "env": { "VERSION": "${VERSION}", "COMMIT": "$(TRAVIS_COMMIT)" } } } }'
TRAVIS_BUILD_CMD='{ "request": { "branch": "master", "message": "Compile master for Statping v${VERSION}", "config": { "merge_mode": "replace", "language": "go", "go": 1.14, "install": true, "sudo": "required", "services": ["docker"], "env": { "secure": "${TRVIS_SECRET}" }, "before_deploy": ["git config --local user.name \"hunterlong\"", "git config --local user.email \"info@socialeck.com\"", "git tag v$(VERSION) --force"], "deploy": [{ "provider": "releases", "api_key": "$$GITHUB_TOKEN", "file_glob": true, "file": "build/*", "skip_cleanup": true, "on": { "branch": "master" } }], "before_script": ["rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install stable", "nvm install 10.17.0", "nvm use 10.17.0 --default", "npm install -g sass yarn cross-env", "pip install --user awscli"], "script": ["make release"], "after_success": [], "after_deploy": ["make post-release"] } } }'
@ -17,10 +18,23 @@ ARCHS = 386 arm amd64 arm64
all: build-deps compile install test build
test: clean compile
go test -v -p=1 -ldflags="-X main.VERSION=0.99.99" -coverprofile=coverage.out ./...
go test -v -p=1 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -coverprofile=coverage.out ./...
build: clean
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION}" -o statping --tags "netgo linux" ./cmd
go build -a -ldflags "-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" -o statping --tags "netgo linux" ./cmd
go-build: clean
rm -rf source/dist
rm -rf source/rice-box.go
wget https://assets.statping.com/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
lint:
go fmt ./...
golint ./...
impi --local github.com/statping/statping/ --scheme stdLocalThirdParty ./...
goimports ./...
up:
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans
@ -54,7 +68,7 @@ cypress: clean
test-api:
DB_CONN=sqlite DB_HOST=localhost DB_DATABASE=sqlite DB_PASS=none DB_USER=none statping &
sleep 5000 && newman run source/tmpl/postman.json -e dev/postman_environment.json --delay-request 500
sleep 5000 && newman run dev/postman.json -e dev/postman_environment.json --delay-request 500
test-deps:
go get golang.org/x/tools/cmd/cover
@ -129,14 +143,14 @@ frontend-build:
@rm -rf source/dist && rm -rf frontend/dist
@echo "yarn install and build static frontend"
cd frontend && yarn && yarn build
@cp -r frontend/dist source/ && cp -r frontend/src/assets/scss source/dist/
@cp -r source/tmpl/*.* source/dist/
@cp -r frontend/dist source/
@cp -r frontend/src/assets/scss source/dist/
@cp frontend/public/favicon.ico source/dist/
@cp frontend/public/robots.txt source/dist/
@cp frontend/public/banner.png source/dist/
@cp -r frontend/public/favicon source/dist/
@echo "Frontend build complete at ./source/dist"
frontend-copy:
cp -r source/tmpl/*.* source/dist/
yarn:
rm -rf source/dist && rm -rf frontend/dist
cd frontend && yarn
@ -145,6 +159,7 @@ yarn:
compile: frontend-build
rm -f source/rice-box.go
cd source && rice embed-go
make generate
embed:
cd source && rice embed-go
@ -155,8 +170,12 @@ install: build
install-local: build
mv $(BINARY_NAME) /usr/local/bin/$(BINARY_NAME)
install-darwin:
go build -a -ldflags "-X main.VERSION=${VERSION}" -o statping --tags "netgo darwin" ./cmd
mv $(BINARY_NAME) /usr/local/bin/$(BINARY_NAME)
generate:
cd source && go generate
go generate ./...
build-all: clean compile build-folders build-linux build-linux-arm build-darwin build-win compress-folders
@ -291,11 +310,14 @@ post-release: frontend-build upload_to_s3 publish-homebrew dockerhub
publish-homebrew:
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(PUBLISH_BODY) https://api.travis-ci.com/repo/statping%2Fhomebrew-statping/requests
upload_to_s3: travis_s3_creds
aws s3 cp ./source/dist/css $(ASSETS_BKT) --recursive --exclude "*" --include "*.css"
aws s3 cp ./source/dist/js $(ASSETS_BKT) --recursive --exclude "*" --include "*.js"
aws s3 cp ./source/dist/scss $(ASSETS_BKT) --recursive --exclude "*" --include "*.scss"
aws s3 cp ./install.sh $(ASSETS_BKT)
upload_to_s3:
tar -czvf source.tar.gz source/
aws s3 cp source.tar.gz s3://assets.statping.com/
rm -rf source.tar.gz
aws s3 cp source/dist/css/ s3://assets.statping.com/css/ --recursive --exclude "*" --include "*.css"
aws s3 cp source/dist/js/ s3://assets.statping.com/js/ --recursive --exclude "*" --include "*.js"
aws s3 cp source/dist/scss/ s3://assets.statping.com/scss/ --recursive --exclude "*" --include "*.scss"
aws s3 cp install.sh s3://assets.statping.com/
travis_s3_creds:
mkdir -p ~/.aws
@ -357,25 +379,29 @@ certs:
buildx-latest: multiarch
docker buildx create --name statping-latest
docker buildx inspect --builder statping-latest --bootstrap
docker buildx build --builder statping-latest --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile -t statping/statping:latest -t statping/statping:v${VERSION} --build-arg=VERSION=${VERSION} .
docker buildx build --builder statping-latest --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile -t statping/statping:latest -t statping/statping:v${VERSION} --build-arg=VERSION=${VERSION} --build-arg=COMMIT=${COMMIT} .
docker buildx rm statping-latest
buildx-dev: multiarch
docker buildx create --name statping-dev
docker buildx inspect --builder statping-dev --bootstrap
docker buildx build --builder statping-dev --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile -t statping/statping:dev --build-arg=VERSION=${VERSION} .
docker buildx build --builder statping-dev --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile -t statping/statping:dev --build-arg=VERSION=${VERSION} --build-arg=COMMIT=${COMMIT} .
docker buildx rm statping-dev
buildx-base: multiarch
docker buildx create --name statping-base
docker buildx inspect --builder statping-base --bootstrap
docker buildx build --builder statping-base --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile.base -t statping/statping:base --build-arg=VERSION=${VERSION} .
docker buildx build --builder statping-base --cache-from "type=local,src=/tmp/.buildx-cache" --cache-to "type=local,dest=/tmp/.buildx-cache" --pull --push --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -f Dockerfile.base -t statping/statping:base --build-arg=VERSION=${VERSION} --build-arg=COMMIT=${COMMIT} .
docker buildx rm statping-base
multiarch:
mkdir /tmp/.buildx-cache || true
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
delve:
go build -gcflags "all=-N -l" -o statping ./cmd
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./statping
check:
@echo "Checking the programs required for the build are installed..."
@echo "go: $(shell go version) - $(shell which go)" && go version >/dev/null 2>&1 || (echo "ERROR: go 1.14 is required."; exit 1)
@ -383,5 +409,5 @@ check:
@echo "yarn: $(shell yarn --version) - $(shell which yarn)" && yarn --version >/dev/null 2>&1 || (echo "ERROR: yarn is required."; exit 1)
@echo "All required programs are installed!"
.PHONY: all check build certs multiarch 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

View File

@ -2,6 +2,6 @@
"name": "Statping",
"description": "Statping Server Monitoring with Status Page",
"repository": "https://github.com/statping/statping",
"logo": "https://raw.githubusercontent.com/statping/statping/master/source/tmpl/banner.png",
"logo": "https://assets.statping.com/banner.png",
"keywords": ["statping", "server", "monitoring", "status page","golang", "go"]
}

View File

@ -16,6 +16,7 @@ import (
"github.com/statping/statping/utils"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
@ -34,13 +35,77 @@ func assetsCli() error {
return nil
}
func systemctlCli() error {
fmt.Println("still in the works...")
func systemctlCli(dir string, uninstall bool, port int64) error {
location := "/etc/systemd/system/statping.service"
if uninstall {
fmt.Println("systemctl stop statping")
if _, _, err := utils.Command("systemctl", "stop", "statping"); err != nil {
log.Errorln(err)
}
fmt.Println("systemctl disable statping")
if _, _, err := utils.Command("systemctl", "disable", "statping"); err != nil {
log.Errorln(err)
}
fmt.Println("Deleting systemctl: ", location)
if err := utils.DeleteFile(location); err != nil {
log.Errorln(err)
}
return nil
}
if ok := utils.FolderExists(dir); !ok {
return errors.New("directory does not exist: " + dir)
}
binPath, err := os.Executable()
if err != nil {
return err
}
config := []byte(`[Unit]
Description=Statping Server
After=network.target
After=systemd-user-sessions.service
After=network-online.target
[Service]
Type=simple
Restart=always
Environment="STATPING_DIR=` + dir + `"
Environment="ALLOW_REPORTS=true"
ExecStart=` + binPath + ` --port=` + utils.ToString(port) + `
WorkingDirectory=` + dir + `
[Install]
WantedBy=multi-user.target"
`)
fmt.Println("Saving systemctl service to: ", location)
fmt.Printf("Using directory %s for Statping data\n", dir)
fmt.Printf("Running on port %d\n", port)
fmt.Printf("\n\n%s\n\n", string(config))
if err := utils.SaveFile(location, config); err != nil {
return err
}
fmt.Println("systemctl daemon-reload")
if _, _, err := utils.Command("systemctl", "daemon-reload"); err != nil {
return err
}
fmt.Println("systemctl enable statping")
if _, _, err := utils.Command("systemctl", "enable", "statping.service"); err != nil {
return err
}
fmt.Println("systemctl start statping")
if _, _, err := utils.Command("systemctl", "start", "statping"); err != nil {
return err
}
fmt.Println("Statping was will auto start on reboots")
fmt.Println("systemctl service: ", location)
return nil
}
func exportCli(args []string) error {
filename := fmt.Sprintf("%s/statping-%s.json", utils.Directory, time.Now().Format("01-02-2006-1504"))
filename := filepath.Join(utils.Directory, time.Now().Format("01-02-2006-1504")+".json")
if len(args) == 1 {
filename = fmt.Sprintf("%s/%s", utils.Directory, args)
}
@ -78,7 +143,7 @@ func sassCli() error {
if err := source.Assets(); err != nil {
return err
}
if err := source.CompileSASS(source.DefaultScss...); err != nil {
if err := source.CompileSASS(); err != nil {
return err
}
return nil
@ -133,6 +198,10 @@ func resetCli() error {
func envCli() error {
fmt.Println("Statping Configuration")
fmt.Printf("Process ID: %d\n", os.Getpid())
fmt.Printf("Running as user id: %d\n", os.Getuid())
fmt.Printf("Running as group id: %d\n", os.Getgid())
fmt.Printf("Statping Directory: %s\n", utils.Directory)
for k, v := range utils.Params.AllSettings() {
fmt.Printf("%s=%v\n", strings.ToUpper(k), v)
}

View File

@ -2,10 +2,12 @@ package main
import (
"bytes"
"github.com/statping/statping/source"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/ioutil"
"os"
"testing"
)
@ -18,16 +20,15 @@ func init() {
}
func TestStatpingDirectory(t *testing.T) {
dir := utils.Directory
require.NotContains(t, dir, "/cmd")
require.NotEmpty(t, dir)
dir = utils.Params.GetString("STATPING_DIR")
require.NotContains(t, dir, "/cmd")
require.NotEmpty(t, dir)
}
func TestEnvCLI(t *testing.T) {
os.Setenv("API_SECRET", "demoapisecret123")
os.Setenv("SASS", "/usr/local/bin/sass")
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
@ -39,6 +40,12 @@ func TestEnvCLI(t *testing.T) {
assert.Contains(t, string(out), VERSION)
assert.Contains(t, utils.Directory, string(out))
assert.Contains(t, "SAMPLE_DATA=true", string(out))
assert.Contains(t, "API_SECRET=demoapisecret123", string(out))
assert.Contains(t, "STATPING_DIR="+dir, string(out))
assert.Contains(t, "SASS=/usr/local/bin/sass", string(out))
os.Unsetenv("API_SECRET")
os.Unsetenv("SASS")
}
func TestVersionCLI(t *testing.T) {
@ -58,16 +65,26 @@ func TestAssetsCLI(t *testing.T) {
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"assets"})
cmd.Execute()
err := cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
assert.Nil(t, err)
assert.Contains(t, string(out), VERSION)
assert.FileExists(t, utils.Directory+"/assets/css/main.css")
assert.FileExists(t, utils.Directory+"/assets/css/style.css")
assert.FileExists(t, utils.Directory+"/assets/css/vendor.css")
assert.FileExists(t, utils.Directory+"/assets/scss/base.scss")
assert.FileExists(t, utils.Directory+"/assets/scss/mobile.scss")
assert.FileExists(t, utils.Directory+"/assets/scss/variables.scss")
for _, f := range source.RequiredFiles {
assert.FileExists(t, utils.Directory+"/assets/"+f)
}
}
func TestUpdateCLI(t *testing.T) {
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"update"})
err := cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
require.Nil(t, err)
assert.Contains(t, string(out), VERSION)
}
func TestHelpCLI(t *testing.T) {

View File

@ -5,24 +5,28 @@ import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/statping/statping/utils"
"io"
"os"
"os/exec"
)
var rootCmd = &cobra.Command{
Use: "statping",
Short: "A simple Application Status Monitor that is opensource and lightweight.",
Use: "statping",
Version: VERSION,
Short: "A simple Application Status Monitor that is opensource and lightweight.",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
var updateCmd = &cobra.Command{
Use: "update",
Short: "Update to the latest version",
Use: "update",
Example: "statping update",
Short: "Update to the latest version",
RunE: func(cmd *cobra.Command, args []string) error {
log.Infoln("Updating Statping to the latest version...")
log.Infoln("curl -o- -L https://statping.com/install.sh | bash")
curl, err := exec.LookPath("curl")
if err != nil {
return err
@ -58,8 +62,9 @@ var updateCmd = &cobra.Command{
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Statping",
Use: "version",
Example: "statping version",
Short: "Print the version number of Statping",
Run: func(cmd *cobra.Command, args []string) {
if COMMIT != "" {
fmt.Printf("%s (%s)\n", VERSION, COMMIT)
@ -71,26 +76,37 @@ var versionCmd = &cobra.Command{
}
var systemctlCmd = &cobra.Command{
Use: "systemctl [install/uninstall]",
Short: "Install or Uninstall systemctl links",
Use: "systemctl [install/uninstall]",
Example: "statping systemctl install",
Short: "Install or Uninstall systemctl services",
RunE: func(cmd *cobra.Command, args []string) error {
if err := systemctlCli(); err != nil {
if args[1] == "install" {
if len(args) < 3 {
return errors.New("requires 'install <working_path> <port>'")
}
}
port := utils.ToInt(args[2])
if port == 0 {
port = 80
}
if err := systemctlCli(args[1], args[0] == "uninstall", port); err != nil {
return err
}
os.Exit(0)
return nil
},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires 'install' or 'uninstall' as arguments")
if len(args) < 2 {
return errors.New("requires 'install <working_path>' or 'uninstall' as arguments")
}
return nil
},
}
var assetsCmd = &cobra.Command{
Use: "assets",
Short: "Dump all assets used locally to be edited",
Use: "assets",
Example: "statping assets",
Short: "Dump all assets used locally to be edited",
RunE: func(cmd *cobra.Command, args []string) error {
if err := assetsCli(); err != nil {
return err
@ -101,8 +117,9 @@ var assetsCmd = &cobra.Command{
}
var exportCmd = &cobra.Command{
Use: "export",
Short: "Exports your Statping settings to a 'statping-export.json' file.",
Use: "export",
Example: "statping export",
Short: "Exports your Statping settings to a 'statping-export.json' file.",
RunE: func(cmd *cobra.Command, args []string) error {
if err := exportCli(args); err != nil {
return err
@ -113,8 +130,9 @@ var exportCmd = &cobra.Command{
}
var sassCmd = &cobra.Command{
Use: "sass",
Short: "Compile .scss files into the css directory",
Use: "sass",
Example: "statping sass",
Short: "Compile .scss files into the css directory",
RunE: func(cmd *cobra.Command, args []string) error {
if err := sassCli(); err != nil {
return err
@ -125,8 +143,9 @@ var sassCmd = &cobra.Command{
}
var envCmd = &cobra.Command{
Use: "env",
Short: "Return the configs that will be ran",
Use: "env",
Example: "statping env",
Short: "Return the configs that will be ran",
RunE: func(cmd *cobra.Command, args []string) error {
if err := envCli(); err != nil {
return err
@ -137,8 +156,9 @@ var envCmd = &cobra.Command{
}
var resetCmd = &cobra.Command{
Use: "reset",
Short: "Start a fresh copy of Statping",
Use: "reset",
Example: "statping reset",
Short: "Start a fresh copy of Statping",
RunE: func(cmd *cobra.Command, args []string) error {
if err := resetCli(); err != nil {
return err
@ -149,8 +169,9 @@ var resetCmd = &cobra.Command{
}
var onceCmd = &cobra.Command{
Use: "once",
Short: "Check all services 1 time and then quit",
Use: "once",
Example: "statping once",
Short: "Check all services 1 time and then quit",
RunE: func(cmd *cobra.Command, args []string) error {
if err := onceCli(); err != nil {
return err
@ -161,8 +182,9 @@ var onceCmd = &cobra.Command{
}
var importCmd = &cobra.Command{
Use: "import [.json file]",
Short: "Imports settings from a previously saved JSON file.",
Use: "import [.json file]",
Example: "statping import backup.json",
Short: "Imports settings from a previously saved JSON file.",
RunE: func(cmd *cobra.Command, args []string) error {
if err := importCli(args); err != nil {
return err

View File

@ -39,7 +39,7 @@ type GroupQuery struct {
}
func (b GroupQuery) Find(data interface{}) error {
return b.db.Find(data).Error()
return b.db.Order("id DESC").Find(data).Error()
}
func (b GroupQuery) Database() Database {

131
dev/postman.json vendored
View File

@ -3275,6 +3275,8 @@
"pm.test(\"Check Login JWT Token\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property('token');",
" pm.expect(jsonData).to.have.property('admin');",
" pm.globals.set(\"token\", jsonData.token);",
"});"
],
"type": "text/javascript"
@ -3371,24 +3373,141 @@
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Length",
"value": "174"
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Set-Cookie",
"value": "statping_auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInNjb3BlcyI6ImFkbWluIiwiZXhwIjoxNTk2NzQzMDUzfQ.dQQGgUDhFEjCL2Gi-Seg0hBp_sqVsDn3cXB0GpSorJI; Path=/; Expires=Thu, 06 Aug 2020 19:44:13 GMT; Max-Age=259200"
},
{
"key": "Date",
"value": "Mon, 03 Aug 2020 19:44:13 GMT"
},
{
"key": "Content-Length",
"value": "197"
},
{
"key": "Connection",
"value": "close"
}
],
"cookie": [],
"body": "{\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInNjb3BlcyI6ImFkbWluIiwiZXhwIjoxNTk2NzQzMDUzfQ.dQQGgUDhFEjCL2Gi-Seg0hBp_sqVsDn3cXB0GpSorJI\",\n \"admin\": true\n}"
}
]
},
{
"name": "Check User Token",
"event": [
{
"listen": "test",
"script": {
"id": "560e439b-d588-4a2f-a8a6-a0607531d74c",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"View Token Response\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.username).to.eql(\"admin\");",
" pm.expect(jsonData.admin).to.eql(true);",
" pm.expect(jsonData.scopes).to.eql(\"admin\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "token",
"value": "{{token}}",
"type": "text"
}
]
},
"url": {
"raw": "{{endpoint}}/api/users/token",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"users",
"token"
]
},
"description": "Send your JWT token from login to this endpoint to return the JSON values."
},
"response": [
{
"name": "Check User Token",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "token",
"value": "{{token}}",
"type": "text"
}
]
},
"url": {
"raw": "{{endpoint}}/api/users/token",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"users",
"token"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Date",
"value": "Sat, 02 May 2020 00:56:17 GMT"
"value": "Mon, 03 Aug 2020 19:47:23 GMT"
},
{
"key": "Set-Cookie",
"value": "statping_auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImV4cCI6MTU4ODY0MDE3N30.tf399_LfAphSGlKMtgphg6qpPrn-_w92XfCrK5FwbZY; Expires=Tue, 05 May 2020 00:56:17 GMT"
"key": "Content-Length",
"value": "68"
},
{
"key": "Connection",
"value": "close"
}
],
"cookie": [],
"body": "{\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImV4cCI6MTU4ODY0MDE3N30.tf399_LfAphSGlKMtgphg6qpPrn-_w92XfCrK5FwbZY\",\n \"admin\": true\n}"
"body": "{\n \"username\": \"admin\",\n \"admin\": true,\n \"scopes\": \"admin\",\n \"exp\": 1596743053\n}"
}
]
},

View File

@ -60,6 +60,9 @@ const webpackConfig = merge(commonConfig, {
plugins: [
new webpack.EnvironmentPlugin(environment),
new CleanWebpackPlugin(),
// new webpack.optimize.LimitChunkCountPlugin({
// maxChunks: 1
// }),
new MiniCSSExtractPlugin({
filename: 'css/[name].css',
chunkFilename: 'css/[name].css'

View File

@ -19,14 +19,12 @@
"@fortawesome/free-brands-svg-icons": "^5.12.1",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"@sentry/browser": "^5.13.2",
"@sentry/integrations": "^5.13.2",
"@sentry/browser": "^5.20.1",
"@sentry/integrations": "^5.20.1",
"apexcharts": "^3.15.0",
"axios": "^0.19.1",
"bootstrap": "^4.4.1",
"bootstrap-vue": "^2.6.1",
"codemirror-colorpicker": "^1.9.66",
"core-js": "^3.4.4",
"core-js": "^3.6.5",
"date-fns": "^2.9.0",
"js-beautify": "^1.11.0",
"querystring": "^0.2.0",
@ -39,7 +37,6 @@
"vue-flatpickr-component": "^8.1.5",
"vue-github-button": "^1.1.2",
"vue-i18n": "^8.18.1",
"vue-moment": "^4.1.0",
"vue-observe-visibility": "^0.4.6",
"vue-router": "~3.0",
"vuedraggable": "^2.23.2",

View File

@ -43,19 +43,11 @@
<meta property="twitter:image" content="favicon/social.png">
{{if USE_CDN}}
<link rel="stylesheet" href="https://assets.statping.com/vendor.css">
<link rel="stylesheet" href="https://assets.statping.com/style.css">
<link rel="stylesheet" href="https://assets.statping.com/main.css">
{{else}}
{{if USING_ASSETS}}
<link href="css/vendor.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
<link href="css/main.css" rel="stylesheet">
<link rel="stylesheet" href="https://assets.statping.com/css/vendor.css">
{{else}}
<% _.each(htmlWebpackPlugin.tags.headTags, function(headTag) { %>
<%= headTag %> <% }) %>
{{end}}
{{end}}
</head>
<body>
<noscript>
@ -65,11 +57,10 @@
<div id="app" class="statping_container"></div>
{{if USE_CDN}}
<script src="https://assets.statping.com/bundle.js"></script>
<script src="https://assets.statping.com/vendor.chunk.js"></script>
<script src="https://assets.statping.com/polyfill.chunk.js"></script>
<script src="https://assets.statping.com/style.chunk.js"></script>
<script src="https://assets.statping.com/main.chunk.js"></script>
<script src="https://assets.statping.com/js/bundle.js"></script>
<script src="https://assets.statping.com/js/vendor.chunk.js"></script>
<script src="https://assets.statping.com/js/polyfill.chunk.js"></script>
<script src="https://assets.statping.com/js/main.chunk.js"></script>
{{else}}
<% _.each(htmlWebpackPlugin.tags.bodyTags, function(bodyTag) { %>
<%= bodyTag %> <% }) %>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,12 +1,9 @@
import Vue from "vue";
import axios from 'axios'
import * as Sentry from "@sentry/browser";
import * as Integrations from "@sentry/integrations";
const qs = require('querystring');
axios.defaults.withCredentials = true
const tokenKey = "statping_auth";
const errorReporter = "https://bed4d75404924cb3a799e370733a1b64@sentry.statping.com/3"
class Api {
constructor() {
@ -235,6 +232,11 @@ class Api {
return axios.post('api/theme', data).then(response => (response.data))
}
async check_token(token) {
const f = {token: token}
return axios.post('api/users/token', qs.stringify(f)).then(response => (response.data))
}
async login(username, password) {
const f = {username: username, password: password}
return axios.post('api/login', qs.stringify(f)).then(response => (response.data))
@ -272,13 +274,6 @@ class Api {
await axios.all([all])
}
async sentry_init() {
Sentry.init({
dsn: errorReporter,
integrations: [new Integrations.Vue({Vue, attachProps: true})],
});
}
}
const api = new Api()
export default api

View File

@ -1,59 +1,59 @@
<template>
<div id="app">
<router-view :loaded="loaded"/>
<Footer v-if="$route.path !== '/setup'"/>
<Footer v-if="$route.path !== '/setup'"/>
</div>
</template>
<script>
import Footer from "./components/Index/Footer";
const Footer = () => import(/* webpackChunkName: "index" */ "./components/Index/Footer");
export default {
name: 'app',
components: {
Footer
},
data () {
return {
loaded: false,
name: 'app',
components: {
Footer
},
data() {
return {
loaded: false,
version: "",
}
},
computed: {
core() {
return this.$store.getters.core
}
},
async beforeMount() {
await this.$store.dispatch('loadCore')
}
},
computed: {
core() {
return this.$store.getters.core
}
},
async beforeMount() {
await this.$store.dispatch('loadCore')
this.$i18n.locale = this.core.language || "en";
// this.$i18n.locale = "ru";
this.$i18n.locale = this.core.language || "en";
// this.$i18n.locale = "ru";
if (!this.core.setup) {
this.$router.push('/setup')
}
if (this.$route.path !== '/setup') {
if (this.$store.state.admin) {
await this.$store.dispatch('loadAdmin')
} else {
await this.$store.dispatch('loadRequired')
if (this.$route.path !== '/setup') {
if (this.$store.state.admin) {
await this.$store.dispatch('loadAdmin')
} else {
await this.$store.dispatch('loadRequired')
}
this.loaded = true
}
this.loaded = true
}
},
},
async mounted() {
if (this.$route.path !== '/setup') {
if (this.$store.state.admin) {
this.logged_in = true
// await this.$store.dispatch('loadAdmin')
}
}
if (this.$route.path !== '/setup') {
if (this.$store.state.admin) {
this.logged_in = true
// await this.$store.dispatch('loadAdmin')
}
}
}
}
}
</script>
<style lang="scss">
@import "./assets/css/bootstrap.min.css";
@import "./assets/scss/main";
@import "./assets/scss/index";
</style>

View File

@ -154,6 +154,10 @@
padding: 5px 7px;
}
.service_li {
min-height: 115px !important;
}
.btn-sm {
line-height: 1.3;
font-size: 0.75rem;

View File

@ -31,7 +31,7 @@
.form-control[readonly] {
background-color: lighten($background-color, 12%) !important;
color: darken($background-color, 5%) !important;
color: lighten($input-color, 30%) !important;
}
/* The slider itself */
@ -268,6 +268,10 @@ input.inputTags-field:focus {
color: $text-color;
}
.nav-link.active A:HOVER {
color: white !important;
}
.nav-pills I {
margin-right: 10px;
}

View File

@ -1,4 +1,6 @@
@import 'base';
@import 'variables';
@import 'mixin';
@import 'layout';
@import 'base';
@import 'forms';
@import 'mobile';

View File

@ -15,7 +15,7 @@ A:HOVER {
}
.text-muted {
color: darken($text-color, 30%) !important;
color: lighten($text-color, 30%) !important;
}
.day-success {

View File

@ -74,10 +74,19 @@
position: absolute;
}
.btn-sm {
line-height: 1.4rem;
font-size: 0.83rem;
}
.btn-sm {
line-height: 1.3;
font-size: 0.75rem;
}
.badge {
padding-top: 2px;
padding-bottom: 3px;
}
.switch LABEL {
font-size: 10pt;
}
.full-col-12 {
padding-left: 0px;

View File

@ -1,7 +1,7 @@
/* Index Page */
$background-color: #f5f5f5;
$background-color: #EAEAEA;
$container-color: #ffffff;
$text-color: #1d1d1d;
$text-color: #2a2a2a;
$max-width: 860px;
$title-color: #4e4e4e;
$description-color: #828282;
@ -15,7 +15,7 @@ $navbar-background: #ffffff;
$input-background: #fdfdfd;
$input-color: #4e4e4e;
$input-border: 1px solid #c9c9c9;
$day-success-background: #18ce08;
$day-success-background: #20ac13;
$day-error-background: #d50a0a;
/* Status Container */

View File

@ -6,7 +6,7 @@ CodeMirror.defineMode('mymode', () => {
if (stream.match(".Service") || (stream.match(".Core")) || (stream.match(".Failure"))) {
return "var-highlight"
} else if (stream.match(".Id") || stream.match(".Domain") || stream.match(".CreatedAt") ||
stream.match(".Name") || stream.match(".DowntimeAgo") || stream.match(".Issue") || stream.match(".LastStatusCode") ||
stream.match(".Name") || stream.match(".Downtime.Human") || stream.match(".Issue") || stream.match(".LastStatusCode") ||
stream.match(".Port") || stream.match(".FailuresLast24Hours") || stream.match(".PingTime")) {
return "var-sub-highlight"
} else if (stream.match("{{") || stream.match("}}")) {

View File

@ -6,10 +6,9 @@
<div v-for="(checkin, i) in checkins" class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
{{checkin.name}}
<button @click="deleteCheckin(checkin)" class="btn btn-sm btn-danger float-right text-uppercase">Delete</button>
<button @click="deleteCheckin(checkin)" class="btn btn-sm small btn-danger float-right text-uppercase">Delete</button>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<div class="input-group-append copy-btn">
@ -18,8 +17,8 @@
</div>
<span class="small">Send a GET request to this URL every {{checkin.interval}} minutes</span>
<span class="small float-right mt-1">Requested {{ago(checkin.last_hit)}} ago</span>
<span class="small float-right mt-1 mr-3">Request expected every {{checkin.interval}} minutes</span>
<span class="small float-right mt-1 mr-3 d-none d-md-block">Requested {{ago(checkin.last_hit)}} ago</span>
<span class="small float-right mt-1 mr-3 d-none d-md-block">Request expected every {{checkin.interval}} minutes</span>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">
@ -62,15 +61,15 @@
<div class="card-body">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<div class="col-7 col-md-5">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-3">
<div class="col-5 col-md-3">
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-3">
<div class="col-12 col-md-4">
<label class="col-form-label"></label>
<button :disabled="btn_disabled" @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
</div>
@ -137,6 +136,9 @@ export default {
},
last_record(checkin) {
const r = this.records(checkin)
if (r.length === 0) {
return {success: false}
}
return r[0]
},
fixInts() {
@ -161,10 +163,3 @@ export default {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -32,7 +32,7 @@
</template>
<script>
const ServiceInfo = () => import('@/components/Dashboard/ServiceInfo')
const ServiceInfo = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServiceInfo')
export default {
name: 'DashboardIndex',

View File

@ -47,7 +47,7 @@
<script>
import Api from "../../API"
import FormMessage from "../../forms/Message";
const FormMessage = () => import(/* webpackChunkName: "dashboard" */ "../../forms/Message");
export default {
name: 'DashboardMessages',

View File

@ -25,7 +25,7 @@
<thead>
<tr>
<th scope="col">{{ $t('dashboard.name') }}</th>
<th scope="col">{{ $tc('dashboard.service', 2) }}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $tc('dashboard.service', 2) }}</th>
<th scope="col">{{ $t('dashboard.visibility') }}</th>
<th scope="col"></th>
</tr>
@ -36,7 +36,7 @@
<td><span class="drag_icon d-none d-md-inline">
<font-awesome-icon icon="bars" class="mr-3" /></span> {{group.name}}
</td>
<td>{{$store.getters.servicesInGroup(group.id).length}}</td>
<td class="d-none d-md-table-cell">{{$store.getters.servicesInGroup(group.id).length}}</td>
<td>
<span class="badge text-uppercase" :class="{'badge-primary': group.public, 'badge-secondary': !group.public}">
{{group.public ? $t('public') : $t('private')}}
@ -67,11 +67,11 @@
</template>
<script>
const FormGroup = () => import('@/forms/Group')
const ToggleSwitch = () => import('@/forms/ToggleSwitch')
const ServicesList = () => import('@/components/Dashboard/ServicesList')
const FormGroup = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Group')
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '@/forms/ToggleSwitch')
const ServicesList = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServicesList')
import Api from "../../API";
import draggable from 'vuedraggable'
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
export default {
name: 'DashboardServices',

View File

@ -45,7 +45,7 @@
<script>
import Api from "../../API"
const FormUser = () => import('@/forms/User')
const FormUser = () => import(/* webpackChunkName: "dashboard" */ '@/forms/User')
export default {
name: 'DashboardUsers',

View File

@ -13,11 +13,11 @@
</template>
<script>
import FormGroup from "../../forms/Group";
const FormGroup = () => import(/* webpackChunkName: "dashboard" */ "../../forms/Group");
import Api from "../../API";
import ToggleSwitch from "../../forms/ToggleSwitch";
import draggable from 'vuedraggable'
import FormService from "../../forms/Service";
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ "../../forms/ToggleSwitch");
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
const FormService = () => import(/* webpackChunkName: "dashboard" */ "../../forms/Service");
export default {
name: 'EditService',

View File

@ -1,22 +1,77 @@
<template>
<div class="col-12">
<h2>{{service.name}} Failures
<button v-if="failures.length>0" @click="deleteFailures" class="btn btn-outline-danger float-right">Delete All</button></h2>
<div class="list-group mt-3 mb-4">
<div v-if="service" class="col-12">
<h3>{{service.name}} Failures
<button v-if="failures.length>0" @click="deleteFailures" class="btn btn-danger float-right">Delete All</button>
</h3>
<div class="alert alert-info" v-if="failures.length===0">
You don't have any failures for {{service.name}}. Way to go!
</div>
<div v-for="(failure, index) in failures" :key="index" class="mb-2 list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{failure.issue}}</h5>
<small>{{niceDate(failure.created_at)}}</small>
<div class="card mt-4 mb-4">
<div class="card-header">
Search and Filter
<span class="float-right">
<font-awesome-icon v-if="loading" icon="circle-notch" spin/>
</span>
</div>
<div class="card-body">
<form>
<div class="form-row">
<div class="col">
<label for="fromdate">From Date</label>
<flatPickr id="fromdate" :disabled="loading" @on-change="load" 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 d-block" required />
</div>
<div class="col">
<label for="todate">To Date</label>
<flatPickr id="todate" :disabled="loading" @on-change="load" 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 d-block" required />
</div>
<div class="col">
<label for="search">Search Terms</label>
<input id="search" type="text" v-model="search" class="form-control">
</div>
</div>
<div class="form-row mt-3">
<div class="col">
<span @click="show_checkins = !!show_checkins" class="switch float-left">
<input v-model="show_checkins" type="checkbox" class="switch" id="showcheckins" v-bind:checked="show_checkins">
<label v-if="show_checkins" for="showcheckins">Showing Checkin Failures</label>
<label v-else for="showcheckins">View Checkin Failures</label>
</span>
</div>
</div>
</form>
</div>
<p class="mb-1">{{failure.issue}}</p>
</div>
<nav v-if="total > 4" class="mt-3">
<div v-if="failures.length === 0" class="alert alert-info">
<span v-if="search">
Could not find any failures with issue: "{{search}}"
</span>
<span v-else>
You don't have any failures for {{service.name}}. Way to go!
</span>
</div>
<table v-else class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Issue</th>
<th scope="col">Status Code</th>
<th scope="col">Ping</th>
<th scope="col">Created</th>
</tr>
</thead>
<tbody>
<tr v-for="(failure, index) in failures" :key="index">
<th class="font-1" scope="row">{{failure.id}}</th>
<td class="font-1">{{failure.issue}}</td>
<td class="font-1">{{failure.error_code}}</td>
<td class="font-1">{{humanTime(failure.ping)}}</td>
<td class="font-1">{{ago(failure.created_at)}}</td>
</tr>
</tbody>
</table>
<nav v-if="total > 4 && failures.length !== 0" class="mt-3">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{'disabled': page===1}">
<a @click.prevent="gotoPage(page-1)" :disabled="page===1" class="page-link" href="#" aria-label="Previous">
@ -35,42 +90,59 @@
</li>
</ul>
<div class="text-center">
<span>{{total}} Failures</span>
<span>{{total}} Failures</span>
</div>
</nav>
</div>
</div>
</template>
<script>
import Api from "../../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
export default {
name: 'Failures',
components: {
flatPickr
},
data() {
return {
service: {},
loading: true,
search: "",
show_checkins: false,
service: null,
fails: [],
limit: 10,
limit: 64,
offset: 0,
total: 0,
page: 1
page: 1,
start_time: this.nowSubtract(216000).toISOString(),
end_time: this.nowSubtract(0).toISOString(),
}
},
watch: {
'$route': 'reloadTimes',
},
computed: {
failures() {
return this.fails.sort(function(a,b) {return b.id - a.id;});
let sorted = this.fails
if (this.show_checkins) {
sorted = sorted.filter(f => f.method === "checkin");
} else {
sorted = sorted.filter(f => f.method !== "checkin");
}
if (this.search !== "") {
sorted = sorted.filter(f => f.issue.toLowerCase().includes(this.search));
}
return sorted
},
pages() {
return Math.floor(this.total / this.limit)
},
maxPages() {
const p = Math.floor(this.total / this.limit)
if (p > 16) {
return 16
} else {
return p
}
return Math.floor(this.total / this.limit)
}
},
async created() {
@ -94,15 +166,10 @@ export default {
await this.load()
},
async load() {
this.fails = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
this.loading = true
this.fails = await Api.service_failures(this.service.id, this.toUnix(this.parseISO(this.start_time)), this.toUnix(this.parseISO(this.end_time)), this.limit, this.offset)
this.loading = false
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -4,7 +4,7 @@
<div v-for="incident in incidents" :key="incident.id" class="card contain-card mb-4">
<div class="card-header">Incident: {{incident.title}}
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<font-awesome-icon icon="times" /> Delete
<font-awesome-icon icon="times" />
</button>
</div>
@ -50,8 +50,9 @@
</template>
<script>
import Api from "../../API";
const FormIncidentUpdates = () => import('@/forms/IncidentUpdates')
import Api from "../../API";
const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/forms/IncidentUpdates')
export default {
name: 'Incidents',
@ -112,10 +113,3 @@
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div>
<Loading :loading="!loaded"/>
<div v-if="loaded && !service.online" class="bg-white shadow-sm mt-3 p-3 pr-4 pl-4 col-12">
<font-awesome-icon icon="exclamation" class="mr-3" size="1x"/>
Last failure was {{ago(service.last_error)}} ago.
<code v-if="failure" class="d-block bg-light p-3 mt-3">
{{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 messages" 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>
</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"/>
{{incident.title}} - {{incident.description}}
<div v-for="update in incident.updates" class="d-block small">
<span class="font-weight-bold text-capitalize">{{update.type}}</span> - {{update.message}}
</div>
</div>
</div>
</template>
<script>
import Api from "../../API";
const Loading = () => import(/* webpackChunkName: "index" */ "@/components/Elements/Loading");
export default {
name: "ServiceEvents",
components: {
Loading
},
props: {
service: {
type: Object,
required: true
}
},
data() {
return {
messages: null,
incidents: null,
failure: null,
loaded: false,
}
},
mounted() {
this.load()
},
methods: {
async load() {
this.loaded = false
if (!this.service.online) {
await this.getFailure()
}
await this.getMessages()
await this.getIncidents()
this.loaded = true
},
async getMessages() {
this.messages = await Api.messages()
},
async getFailure() {
const f = await Api.service_failures(this.service.id, null, null, 1)
this.failure = f[0]
},
async getIncidents() {
this.incidents = await Api.incidents_service(this.service.id)
},
}
}
</script>
<style scoped>
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="card mb-4" :class="{'offline-card': !service.online}">
<div class="card-title px-4 pt-3">
<div class="card-header pb-1">
<h4 v-observe-visibility="setVisible">
<router-link :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
@ -9,61 +9,31 @@
</h4>
</div>
<div class="card-body p-3 p-md-1 pt-md-1 pb-md-1">
<div class="card-body">
<transition name="fade">
<div v-if="loaded" class="col-12 pb-2">
<div v-if="false" class="row mb-4 align-content-center">
<div v-if="!service.online" class="col-3 text-left">
<span css="text-danger font-5 font-weight-bold"></span>
<span class="font-2 d-block">Current Downtime</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
{{service.online_24_hours.toString()}} %
</span>
<span class="font-2 d-block">Total Uptime</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
0
</span>
<span class="font-2 d-block">Downtime Today</span>
</div>
<div v-if="service.online" class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">
{{(uptime.uptime / 10000).toFixed(0).toString()}}
</span>
<span class="font-2 d-block">Uptime Duration</span>
</div>
<div class="col-3 text-left">
<span class="text-danger font-5 font-weight-bold">
{{service.failures_24_hours}}
</span>
<span class="font-2 d-block">Failures last 24 hours</span>
</div>
</div>
<div class="row">
<div v-if="loaded" class="row pl-2 pr-2">
<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"/>
</div>
<div class="col-md-6 col-sm-12 mt-4 mt-md-0 mb-3">
<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"/>
</div>
</div>
<div v-else class="row mt-5 mb-5 pt-5 pb-5">
<div class="col-6 text-center text-muted">
<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>
</transition>
</div>
<div class="card-footer">
<div class="row">
@ -100,19 +70,20 @@
</template>
<script>
import Checkin from '../../forms/Checkin';
import FormIncident from '../../forms/Incident';
import FormMessage from '../../forms/Message';
import ServiceFailures from '../Service/ServiceFailures';
import ServiceSparkLine from "./ServiceSparkLine";
const Checkin = () => import(/* webpackChunkName: "dashboard" */ '../../forms/Checkin');
const FormMessage = () => import(/* webpackChunkName: "dashboard" */ '../../forms/Message');
const ServiceFailures = () => import(/* webpackChunkName: "dashboard" */ '../Service/ServiceFailures');
const ServiceSparkLine = () => import(/* webpackChunkName: "dashboard" */ "./ServiceSparkLine");
import Api from "../../API";
const ServiceEvents = () => import(/* webpackChunkName: "dashboard" */ "@/components/Dashboard/ServiceEvents");
export default {
name: 'ServiceInfo',
components: {
ServiceEvents,
Checkin,
ServiceFailures,
FormIncident,
FormMessage,
ServiceSparkLine
},
@ -197,16 +168,3 @@
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.offline-card {
background-color: #fff5f5;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .75s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>

View File

@ -5,7 +5,6 @@
<script>
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
export default {
name: 'ServiceSparkLine',
props: {

View File

@ -33,13 +33,13 @@
</td>
<td class="text-right">
<div class="btn-group">
<button 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">
<font-awesome-icon icon="edit" />
</button>
<button @click.prevent="goto({path: serviceLink(service), params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<button :disabled="loading" @click.prevent="goto({path: serviceLink(service), params: {service: service} })" class="btn btn-sm btn-outline-secondary">
<font-awesome-icon icon="chart-area" />
</button>
<button v-if="$store.state.admin" @click.prevent="deleteService(service)" href="#" class="btn btn-sm btn-danger">
<button :disabled="loading" v-if="$store.state.admin" @click.prevent="deleteService(service)" class="btn btn-sm btn-danger">
<font-awesome-icon v-if="!loading" icon="times" />
<font-awesome-icon v-if="loading" icon="circle-notch" spin/>
</button>
@ -53,8 +53,8 @@
<script>
import Api from "../../API";
import draggable from 'vuedraggable'
import ToggleSwitch from '../../forms/ToggleSwitch';
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '../../forms/ToggleSwitch');
export default {
name: 'ServicesList',

View File

@ -57,15 +57,16 @@
</template>
<script>
import Api from "../../API";
import Api from "../../API";
// require component
import { codemirror } from 'vue-codemirror'
import 'codemirror/mode/css/css.js'
// require component
import {codemirror} from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror-colorpicker/dist/codemirror-colorpicker.css'
import 'codemirror-colorpicker'
import('codemirror/mode/css/css.js')
import('codemirror/lib/codemirror.css')
import('codemirror-colorpicker/dist/codemirror-colorpicker.css')
import('codemirror-colorpicker')
export default {
name: 'ThemeEditor',
@ -191,9 +192,3 @@
}
}
</script>
<style scoped>
.CodeMirror {
border: 1px solid #eee;
height: 550px;
}
</style>

View File

@ -37,7 +37,6 @@
<script>
import Api from "../../API"
import Vue from "vue";
export default {
name: 'TopNav',

View File

@ -0,0 +1,42 @@
<template>
<div class="col-12 mb-3 pb-2 border-bottom" role="alert">
<span class="font-weight-bold text-capitalize" :class="{'text-success': update.type.toLowerCase()==='resolved', 'text-danger': update.type.toLowerCase()==='investigating', 'text-warning': update.type.toLowerCase()==='update'}">{{update.type}}</span>
<span class="text-muted">- {{update.message}}
<button v-if="admin" @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>
</button>
</span>
<span class="d-block small">{{ago(update.created_at)}} ago</span>
</div>
</template>
<script>
import Api from "@/API";
export default {
name: "IncidentUpdate",
props: {
update: {
required: true
},
admin: {
required: true
},
onUpdate: {
required: false
}
},
methods: {
async delete_update(update) {
this.res = await Api.incident_update_delete(update)
if (this.res.status === "success") {
this.onUpdate()
}
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<div v-if="loading" class="row mt-3 mb-3">
<div class="col-12 text-center text-muted">
<font-awesome-icon icon="circle-notch" size="3x" spin/>
</div>
</div>
</template>
<script>
export default {
name: "Loading",
props: {
loading: {
type: Boolean,
required: true,
}
}
}
</script>
<style scoped>
</style>

View File

@ -3,7 +3,7 @@
<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">
<a 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 $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action">
<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 }">
{{service.online ? $t('online') : $t('offline')}}
@ -12,16 +12,16 @@
<GroupServiceFailures :service="service"/>
<IncidentsBlock :service="service"/>
</a>
</div>
</div>
</div>
</template>
<script>
import Api from '../../API';
import GroupServiceFailures from './GroupServiceFailures';
import IncidentsBlock from './IncidentsBlock';
const GroupServiceFailures = () => import(/* webpackChunkName: "index" */ './GroupServiceFailures');
const IncidentsBlock = () => import(/* webpackChunkName: "index" */ './IncidentsBlock');
export default {
name: 'Group',

View File

@ -2,7 +2,7 @@
<div>
<div class="d-flex mt-3 mb-2">
<div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'day-error': d.amount > 0, 'day-success': d.amount === 0}">
<span v-if="d.amount != 0" class="small">{{d.amount}}</span>
<span v-if="d.amount !== 0" class="d-none d-md-block text-center small">{{d.amount}}</span>
</div>
</div>
<div class="row mt-2">

View File

@ -1,25 +1,25 @@
<template>
<div class="row">
<div v-for="(incident, i) in incidents" class="col-12 mt-4 mb-3">
<div v-for="(incident, i) in incidents" class="col-12">
<span class="braker mt-1 mb-3"></span>
<h6>Incident: {{incident.title}}
<h6>{{incident.title}}
<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span>
</h6>
<span class="font-2" v-html="incident.description"></span>
<UpdatesBlock :incident="incident"/>
<div class="font-2 mb-3" v-html="incident.description"></div>
<IncidentUpdate v-for="(update, i) in incident.updates" v-bind:key="i" :update="update" :admin="false"/>
</div>
</div>
</template>
<script>
import Api from '../../API';
import UpdatesBlock from "@/components/Index/UpdatesBlock";
import IncidentUpdate from "@/components/Elements/IncidentUpdate";
export default {
name: 'IncidentsBlock',
components: {UpdatesBlock},
components: {
IncidentUpdate
},
props: {
service: {
type: Object,
@ -49,8 +49,7 @@ export default {
this.incidents = await Api.incidents_service(this.service.id)
},
async incident_updates(incident) {
await Api.incident_updates(incident).then((d) => {return d})
return o
return await Api.incident_updates(incident)
}
}
}

View File

@ -1,12 +1,12 @@
<template>
<div class="alert alert-warning pb-4 pt-3 mt-5 mb-5" role="alert">
<div class="alert alert-secondary pb-3 pt-3 mt-5 mb-3" role="alert">
<h3 class="mb-3">{{message.title}}</h3>
<span class="mb-3">{{message.description}}</span>
<div class="row d-block mt-3">
<span class="col-12 col-md-6 text-left small">
<span class="col-12 col-md-6 text-left small text-muted">
Started {{niceDate(message.start_on)}} ({{ago(message.start_on)}} ago)
</span>
<span class="col-12 col-md-6 text-right float-right small">
<span class="col-12 col-md-6 text-right float-right small text-muted">
Ends on {{niceDate(message.end_on)}} (in {{ago(message.end_on)}})</span>
</div>
</div>
@ -17,7 +17,8 @@ export default {
name: 'MessageBlock',
props: {
message: {
type: Object
type: Object,
required: true,
}
}
}

View File

@ -1,52 +0,0 @@
<template>
<div class="row">
<div v-for="(update, i) in updates" v-bind:key="i" class="col-12 mt-3">
<div class="col-md-2 col-12">
<span class="badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
</div>
<div class="col-md-12 col-12 mt-2 font-3">{{update.message}}</div>
<div class="col-12 font-1 float-right mt-2">{{ago(update.created_at)}} ago</div>
</div>
</div>
</template>
<script>
import Api from '../../API';
export default {
name: 'UpdatesBlock',
props: {
incident: {
type: Object,
required: true
}
},
data() {
return {
updates: null,
}
},
mounted () {
this.getIncidentUpdates()
},
methods: {
badgeClass(val) {
switch (val.toLowerCase()) {
case "resolved":
return "badge-success"
case "update":
return "badge-info"
case "investigating":
return "badge-danger"
}
},
async getIncidentUpdates() {
this.updates = await Api.incident_updates(this.incident)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,11 +1,6 @@
<template>
<div class="card text-black-50 bg-white mt-3 mb-3">
<div class="card-header text-capitalize">Service Latency</div>
<div class="card-body">
<div class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div>
</div>
<div class="service-chart-container">
<apexchart width="100%" type="area" :options="main_chart_options" :series="main_chart"></apexchart>
</div>
</template>

View File

@ -1,21 +0,0 @@
<template>
<div class="row stats_area mt-5 mb-4">
okok
</div>
</template>
<script>
export default {
name: 'Incidents',
props: {
service: {
type: Object,
required: true
},
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -58,11 +58,10 @@
</template>
<script>
import Api from '../../API';
import Analytics from './Analytics';
import ServiceChart from "./ServiceChart";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
import Graphing from '../../graphing'
const Analytics = () => import(/* webpackChunkName: "service" */ './Analytics');
const ServiceChart = () => import(/* webpackChunkName: "service" */ "./ServiceChart");
const ServiceTopStats = () => import(/* webpackChunkName: "service" */ "@/components/Service/ServiceTopStats");
const Graphing = () => import(/* webpackChunkName: "service" */ '../../graphing');
export default {
name: 'ServiceBlock',
@ -227,8 +226,4 @@ export default {
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
</script>

View File

@ -178,6 +178,7 @@
},
methods: {
async chartHits(val) {
this.ready = false
const start = val.start_time
const end = this.toUnix(new Date())
this.data = await Api.service_hits(this.service.id, start, end, val.interval, false)

View File

@ -32,7 +32,7 @@
</template>
<script>
import ServiceChart from "./ServiceChart";
const ServiceChart = () => import(/* webpackChunkName: "service" */ "./ServiceChart");
import Api from "../../API";
export default {

View File

@ -16,19 +16,15 @@
<div class="col-12 alert alert-light">
<form @submit.prevent="saveCheckin">
<div class="form-group row">
<div class="col-5">
<div class="col-12 col-md-5">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<div class="col-12 col-md-5">
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div>
<div class="col-3">
<div class="col-12 col-md-5">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-success d-block mt-2">Save Checkin</button>
</div>
@ -54,7 +50,6 @@
checkin: {
name: "",
interval: 60,
grace: 60,
service_id: this.service.id
}
}

View File

@ -1,44 +0,0 @@
<template>
<div>
</div>
</template>
<script>
import Api from "../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
import FormIncidentUpdates from './IncidentUpdates';
export default {
name: 'FormIncident',
components: {
FormIncidentUpdates
},
props: {
service: {
type: Object
}
},
data () {
return {
incident: {
title: "",
description: "",
service: this.service.id,
},
incidents: [],
}
},
async mounted () {
await this.loadIncidents()
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -5,20 +5,12 @@
No updates found, create a new Incident Update below.
</div>
<div v-for="update in updates" :key="update.id">
<div class="alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span>
<span class="float-right font-2">{{ago(update.created_at)}} ago</span>
<span class="d-block mt-2">{{update.message}}
<button @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>
</button>
</span>
</div>
<div v-for="update in updates.reverse()" :key="update.id">
<IncidentUpdate :update="update" :onUpdate="loadUpdates" :admin="true"/>
</div>
<form class="row" @submit.prevent="createIncidentUpdate">
<div class="col-3">
<div class="col-12 col-md-3 mb-3 mb-md-0">
<select v-model="incident_update.type" class="form-control">
<option value="Investigating">Investigating</option>
<option value="Update">Update</option>
@ -26,11 +18,11 @@
<option value="Resolved">Resolved</option>
</select>
</div>
<div class="col-7">
<input v-model="incident_update.message" rows="5" name="description" class="form-control" id="message" required>
<div class="col-12 col-md-7 mb-3 mb-md-0">
<input v-model="incident_update.message" name="description" class="form-control" id="message" required>
</div>
<div class="col-2">
<div class="col-12 col-md-2">
<button @click.prevent="createIncidentUpdate"
:disabled="!incident_update.message"
type="submit" class="btn btn-block btn-primary">
@ -44,12 +36,11 @@
<script>
import Api from "../API";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
const IncidentUpdate = () => import(/* webpackChunkName: "index" */ "@/components/Elements/IncidentUpdate");
export default {
name: 'FormIncidentUpdates',
components: {},
components: {IncidentUpdate},
props: {
incident: {
type: Object,
@ -58,7 +49,7 @@
},
data () {
return {
updates: [],
updates: null,
incident_update: {
incident: this.incident.id,
message: "",
@ -72,15 +63,6 @@
},
methods: {
async delete_update(update) {
this.res = await Api.incident_update_delete(update)
if (this.res.status === "success") {
this.updates = this.updates.filter(obj => obj.id !== update.id); // this is better in terms of not having to querry the db to get a fresh copy of all updates
//await this.loadUpdates()
}
},
async createIncidentUpdate() {
this.res = await Api.incident_update_create(this.incident_update)
if (this.res.status === "success") {

View File

@ -4,13 +4,13 @@
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">{{$t('username')}}</label>
<div class="col-sm-10">
<input @keyup="checkForm" type="text" v-model="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="Username" autocorrect="off" autocapitalize="none">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">{{$t('password')}}</label>
<div class="col-sm-10">
<input @keyup="checkForm" type="password" v-model="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="Password">
</div>
</div>
<div class="form-group row">
@ -59,17 +59,20 @@
},
data() {
return {
username: "",
password: "",
auth: {},
loading: false,
error: false,
disabled: true,
username: "",
password: "",
auth: {},
loading: false,
error: false,
disabled: true,
google_scope: "https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email",
slack_scope: "identity.email,identity.basic"
}
},
methods: {
mounted() {
this.$cookies.remove("statping_auth")
},
methods: {
checkForm() {
if (!this.username || !this.password) {
this.disabled = true
@ -84,9 +87,10 @@
if (auth.error) {
this.error = true
} else if (auth.token) {
// this.$cookies.set("statping_auth", auth.token)
this.$cookies.set("statping_auth", auth.token)
await this.$store.dispatch('loadAdmin')
this.$store.commit('setAdmin', auth.admin)
this.$store.commit('setLoggedIn', true)
this.$router.push('/dashboard')
}
this.loading = false

View File

@ -28,7 +28,7 @@
<label class="col-sm-4 col-form-label">Service</label>
<div class="col-sm-8">
<select v-model="message.service" name="service_id" class="form-control">
<option :value="0">Global Announcement</option>
<option v-bind:value="0">Global Announcement</option>
<option v-for="service in $store.getters.services" :value="service.id" v-bind:key="service.id" >{{service.name}}</option>
</select>
</div>
@ -39,7 +39,7 @@
<div class="col-sm-4">
<flatPickr v-model="message.start_on" @on-change="startChange" :config="config" type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required />
</div>
<div class="col-sm-4">
<div class="col-sm-4 mt-3 mt-md-0">
<flatPickr v-model="message.end_on" @on-change="endChange" :config="config" type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="0001-01-01T00:00:00Z" required />
</div>
</div>
@ -48,7 +48,7 @@
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
<div class="col-sm-8">
<select v-model="message.service" class="form-control" name="service" id="service_id">
<option :value="0">Global Message</option>
<option v-bind:value="0">Global Message</option>
<option v-for="service in $store.getters.services" :value="service.id" v-bind:key="service.id">{{service.name}}</option>
</select>
</div>
@ -130,6 +130,7 @@
start_on: new Date(),
end_on: new Date(),
service_id: 0,
service: 0,
notify_method: "",
notify: false,
notify_before: 0,

View File

@ -14,10 +14,10 @@
<div v-if="notifier.method==='mobile'">
<div class="form-group row mt-3">
<label for="domain" class="col-sm-4 col-form-label">Statping Domain</label>
<label for="statping_domain" class="col-sm-4 col-form-label">Statping Domain</label>
<div class="col-sm-8">
<div class="input-group">
<input v-bind:value="$store.getters.core.domain" type="text" class="form-control" id="domain" readonly>
<input v-bind:value="$store.getters.core.domain" type="text" class="form-control" id="statping_domain" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy($store.getters.core.domain)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
@ -69,7 +69,7 @@
</div>
</div>
<div v-if="notifier.data_type" class="card mb-3">
<div v-if="notifier.data_type" class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded = !expanded" :icon="expanded ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{notifier.title}} Outgoing Request
@ -136,6 +136,31 @@
<font-awesome-icon v-if="loadingTest" icon="circle-notch" class="mr-2" spin/>{{loadingTest ? "Loading..." : "Test Failure"}}</button>
</div>
</div>
</div>
</div>
<div v-if="notifier.logs" class="card mb-3">
<div class="card-header text-capitalize">
<font-awesome-icon @click="expanded_logs = !expanded_logs" :icon="expanded_logs ? 'minus' : 'plus'" class="mr-2 pointer"/>
{{notifier.title}} Logs
<span class="badge badge-info float-right text-uppercase mt-1">{{notifier.logs.length}}</span>
</div>
<div class="card-body" :class="{'d-none': !expanded_logs}">
<div v-for="(log, i) in notifier.logs.reverse()" class="alert" :class="{'alert-danger': log.error, 'alert-dark': !log.success && !log.error, 'alert-success': log.success && !log.error}">
<span class="d-block">
Service '{{$store.getters.serviceById(log.service).name}}'
{{log.success ? "Success Triggered" : "Failure Triggered"}}
</span>
<div class="bg-white p-3 small mt-2">
<code>{{log.message}}</code>
</div>
<div class="row mt-2">
<span class="col-6 small">{{niceDate(log.created_at)}}</span>
</div>
</div>
</div>
</div>
@ -148,16 +173,19 @@
</template>
<script>
import Api from "../API";
import Api from "../API";
/* webpackChunkName: "codemirror" */
import {codemirror} from 'vue-codemirror'
/* webpackChunkName: "codemirror" */
import 'codemirror/mode/javascript/javascript.js'
/* webpackChunkName: "codemirror" */
import 'codemirror/lib/codemirror.css'
/* webpackChunkName: "codemirror" */
import 'codemirror/theme/neat.css'
/* webpackChunkName: "codemirror" */
import '../codemirror_json'
const beautify = require('js-beautify').js
// require component
import { codemirror } from 'vue-codemirror'
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/neat.css'
import '../codemirror_json'
const beautify = require('js-beautify').js
export default {
name: 'Notifier',
@ -183,6 +211,7 @@ export default {
success: false,
saved: false,
expanded: false,
expanded_logs: false,
success_data: null,
failure_data: null,
form: {},
@ -300,10 +329,3 @@ export default {
}
}
</script>
<style scoped>
.CodeMirror {
border: 1px solid #eee;
height: 550px;
font-size: 9pt;
}
</style>

View File

@ -50,12 +50,6 @@
<small>Optional comma delimited list of Github Organizations</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Enable Github Login</label>
<div class="col-md-8 col-xs-12 mt-1">
</div>
</div>
<div class="form-group row">
<label for="gh_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8">

View File

@ -44,7 +44,7 @@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Public Service</label>
<div class="col-8 mt-1">
<div class="col-12 col-md-8 mt-1 mb-2">
<span @click="service.public = !!service.public" class="switch float-left">
<input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public">
<label v-if="service.public" for="switch-public">This service will be visible for everyone</label>
@ -145,8 +145,8 @@
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Follow HTTP Redirects</label>
<div class="col-8 mt-1">
<label class="col-12 col-md-4 col-form-label">Follow HTTP Redirects</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.redirect = !!service.redirect" class="switch float-left">
<input v-model="service.redirect" type="checkbox" name="redirect-option" class="switch" id="switch-redirect" v-bind:checked="service.redirect">
<label for="switch-redirect">Follow HTTP Redirects if server attempts</label>
@ -155,8 +155,8 @@
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
<label class="col-12 col-md-4 col-form-label">Verify SSL</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left">
<input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" v-bind:checked="service.verify_ssl">
<label for="switch-verify-ssl" v-if="service.verify_ssl">Verify SSL Certificate for this service</label>
@ -166,8 +166,8 @@
</div>
<div v-if="service.type.match(/^(tcp|http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Use TLS Certificate</label>
<div class="col-8 mt-1">
<label class="col-12 col-md-4 col-form-label">Use TLS Certificate</label>
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="use_tls = !!use_tls" class="switch float-left">
<input v-model="use_tls" type="checkbox" name="verify_ssl-option" class="switch" id="switch-use-tls" v-bind:checked="use_tls">
<label for="switch-use-tls" v-if="use_tls">Custom TLS Certificates for mTLS services</label>
@ -209,7 +209,7 @@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Enable Notifications</label>
<div class="col-8 mt-1">
<div class="col-12 col-md-8 mt-1 mb-2 mb-md-0">
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
<label for="switch-notifications">Allow notifications to be sent for this service</label>
@ -226,7 +226,7 @@
</div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify All Changes</label>
<div class="col-8 mt-1">
<div class="col-12 col-md-8 mt-1">
<span @click="service.notify_all_changes = !!service.notify_all_changes" class="switch float-left">
<input v-model="service.notify_all_changes" type="checkbox" name="notify_all-option" class="switch" id="notify_all" v-bind:checked="service.notify_all_changes">
<label v-if="service.notify_all_changes" for="notify_all">Continuously send notifications when service is failing.</label>

View File

@ -7,7 +7,7 @@
<div class="col-12">
<form @submit.prevent="saveSetup">
<div class="row">
<div class="col-6">
<div class="col-12 col-md-6">
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.language') }}</label>
<select @change="changeLanguages" v-model="setup.language" id="language" class="form-control">
@ -27,13 +27,13 @@
</select>
</div>
<div class="row">
<div class="col-6">
<div class="col-7 col-md-6">
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('setup.host') }}</label>
<input @keyup="canSubmit" v-model="setup.db_host" id="db_host" type="text" class="form-control" placeholder="localhost">
</div>
</div>
<div class="col-6">
<div class="col-5 col-md-6">
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('port') }}</label>
<input @keyup="canSubmit" v-model="setup.db_port" id="db_port" type="number" class="form-control" placeholder="5432">
@ -65,12 +65,11 @@
</span>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="col-12 col-md-6">
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.project_name') }}</label>
@ -125,9 +124,11 @@
{{error}}
</div>
<button @click.prevent="saveSetup" v-bind:disabled="disabled || loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? "Loading..." : "Save Settings"}}
</button>
<div class="col-12">
<button @click.prevent="saveSetup" v-bind:disabled="disabled || loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{loading ? "Loading..." : "Save Settings"}}
</button>
</div>
</div>
</form>
@ -137,7 +138,6 @@
<script>
import Api from "../API";
import Index from "../pages/Index";
export default {
name: 'Setup',

View File

@ -66,7 +66,7 @@
<script>
import Api from "../API";
import LoadButton from "@/components/Elements/LoadButton";
const LoadButton = () => import(/* webpackChunkName: "index" */ "@/components/Elements/LoadButton");
export default {
name: 'FormUser',

View File

@ -40,8 +40,8 @@ const english = {
wrong_login: 'Incorrect username or password'
},
settings: {
name: "Project Name",
description: "Project Name",
name: "Site Name",
description: "Site Description",
footer: "Custom Footer",
footer_notes: "HTML is allowed inside the footer",
error_reporting: "Enable Error Reporting",

View File

@ -5,13 +5,18 @@ import VueObserveVisibility from 'vue-observe-visibility'
import VueClipboard from 'vue-clipboard2'
import VueCookies from 'vue-cookies'
import VueI18n from 'vue-i18n'
import * as Sentry from "@sentry/browser";
import * as Integrations from "@sentry/integrations";
import router from './routes'
import "./mixin"
import "./icons"
const App = () => import('@/App.vue')
import store from './store'
import language from './languages'
const errorReporter = "https://bed4d75404924cb3a799e370733a1b64@sentry.statping.com/3"
const App = () => import(/* webpackChunkName: "index" */ '@/App.vue')
Vue.component('apexchart', VueApexCharts)
Vue.use(VueClipboard);
@ -27,6 +32,11 @@ const i18n = new VueI18n({
Vue.$cookies.config('3d')
Sentry.init({
dsn: errorReporter,
integrations: [new Integrations.Vue({Vue, attachProps: true, logErrors: true})],
});
Vue.config.productionTip = false
new Vue({
router,

View File

@ -106,10 +106,6 @@ export default Vue.mixin({
isAdmin() {
return this.$store.state.admin
},
loggedIn() {
const core = this.$store.getters.core
return core.logged_in === true
},
iconName(name) {
switch (name) {
case "fas fa-terminal":

View File

@ -6,7 +6,7 @@
</template>
<script>
const TopNav = () => import('@/components/Dashboard/TopNav')
const TopNav = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/TopNav')
export default {
name: 'Dashboard',
@ -35,6 +35,3 @@
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -6,4 +6,4 @@
export default {
name: 'Help',
}
</script>
</script>

View File

@ -5,12 +5,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">
<a class="service_li list-group-item list-group-item-action">
<div class="service_li list-group-item list-group-item-action">
<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>
<GroupServiceFailures :service="service"/>
<IncidentsBlock :service="service"/>
</a>
</div>
</div>
</div>
@ -32,22 +32,23 @@
</template>
<script>
const Group = () => import('@/components/Index/Group')
const Header = () => import('@/components/Index/Header')
const MessageBlock = () => import('@/components/Index/MessageBlock')
const ServiceBlock = () => import('@/components/Service/ServiceBlock')
const GroupServiceFailures = () => import('@/components/Index/GroupServiceFailures')
const IncidentsBlock = () => import('@/components/Index/IncidentsBlock')
import Api from "@/API";
const Group = () => import(/* webpackChunkName: "index" */ '@/components/Index/Group')
const Header = () => import(/* webpackChunkName: "index" */ '@/components/Index/Header')
const MessageBlock = () => import(/* webpackChunkName: "index" */ '@/components/Index/MessageBlock')
const ServiceBlock = () => import(/* webpackChunkName: "index" */ '@/components/Service/ServiceBlock')
const GroupServiceFailures = () => import(/* webpackChunkName: "index" */ '@/components/Index/GroupServiceFailures')
const IncidentsBlock = () => import(/* webpackChunkName: "index" */ '@/components/Index/IncidentsBlock')
export default {
name: 'Index',
components: {
IncidentsBlock,
GroupServiceFailures,
ServiceBlock,
MessageBlock,
Group,
Header
ServiceBlock,
MessageBlock,
Group,
Header
},
data() {
return {
@ -67,27 +68,27 @@ export default {
services_no_group() {
return this.$store.getters.servicesNoGroup
}
},
async created() {
this.logged_in = this.loggedIn()
},
async mounted() {
},
methods: {
async checkLogin() {
const token = this.$cookies.get('statping_auth')
if (!token) {
this.$store.commit('setLoggedIn', false)
return
}
try {
const jwt = await Api.check_token(token)
this.$store.commit('setAdmin', jwt.admin)
if (jwt.username) {
this.$store.commit('setLoggedIn', true)
}
} catch (e) {
console.error(e)
}
},
inRange(message) {
return this.isBetween(this.now(), message.start_on, message.start_on === message.end_on ? this.maxDate().toISOString() : message.end_on)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>

View File

@ -10,7 +10,7 @@
</template>
<script>
const FormLogin = () => import('@/forms/Login')
const FormLogin = () => import(/* webpackChunkName: "index" */ '@/forms/Login')
export default {
name: 'Login',
@ -22,7 +22,10 @@
}
},
methods: {
mounted() {
},
methods: {
}
}

View File

@ -87,10 +87,3 @@ export default {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -1,37 +1,35 @@
<template>
<div class="container col-md-7 col-sm-12 mt-md-5">
<div 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}">
{{service.online ? $t('online') : $t('offline')}}
</span>
<h4 class="mt-2">
<span class="mt-2 font-3">
<router-link to="/" class="text-black-50 text-decoration-none">{{core.name}}</router-link> - <span class="text-muted">{{service.name}}</span>
<span class="badge float-right d-none d-md-block text-uppercase" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
{{service.online ? $t('online') : $t('offline')}}
</span>
</h4>
</span>
<ServiceTopStats :service="service"/>
<ServiceTopStats v-if="loaded" :service="service"/>
<MessageBlock v-for="message in messagesInRange" v-bind:key="message.id" :message="message"/>
<MessageBlock v-if="loaded" v-for="message in messagesInRange" v-bind:key="message.id" :message="message"/>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header text-capitalize">Timeframe</div>
<div class="card-body">
<div class="card-body pb-4">
<div class="row">
<div class="col-12 col-md-4 font-2">
<flatPickr :disabled="loading" @on-change="onnn" v-model="start_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date() }" type="text" class="btn btn-white text-left" required />
<div class="col-12 col-md-4 font-2 mb-3 mb-md-0">
<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 />
<small class="d-block">From {{this.format(new Date(start_time))}}</small>
</div>
<div class="col-12 col-md-4 font-2">
<flatPickr :disabled="loading" @on-change="onnn" v-model="end_time" :config="{ enableTime: true, altInput: true, altFormat: 'Y-m-d h:i K', maxDate: new Date()}" type="text" class="btn btn-white text-left" required />
<div class="col-12 col-md-4 font-2 mb-3 mb-md-0">
<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 />
<small class="d-block">To {{this.format(new Date(end_time))}}</small>
</div>
<div class="col-12 col-md-4">
<select :disabled="loading" @change="chartHits" v-model="group" class="form-control">
<div class="col-12 col-md-4 mb-1 mb-md-0">
<select :disabled="!loaded" @change="chartHits" v-model="group" class="form-control">
<option value="1m">1 Minute</option>
<option value="5m">5 Minutes</option>
<option value="15m">15 Minute</option>
@ -50,10 +48,22 @@
</div>
</div>
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
<div class="card text-black-50 bg-white mt-3 mb-3">
<div class="card-header text-capitalize">Service Latency</div>
<div v-if="loaded" class="card-body">
<div class="row mb-5">
<AdvancedChart :group="group" :updated="updated_chart" :start="start_time.toString()" :end="end_time.toString()" :service="service"/>
</div>
<div class="row mt-5">
<apexchart height="220" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart>
</div>
</div>
<div v-else class="row mt-3 mb-3">
<div class="col-12 text-center">
<font-awesome-icon icon="circle-notch" size="3x" spin/>
</div>
</div>
<div v-if="!loading" class="row">
<apexchart width="100%" height="120" type="rangeBar" :options="timeRangeOptions" :series="uptime_data"></apexchart>
</div>
<div class="card text-black-50 bg-white mb-3">
@ -72,12 +82,12 @@
<script>
import Api from "../API"
const MessageBlock = () => import('@/components/Index/MessageBlock')
const ServiceFailures = () => import('@/components/Service/ServiceFailures')
const Checkin = () => import('@/forms/Checkin')
const ServiceHeatmap = () => import('@/components/Service/ServiceHeatmap')
const ServiceTopStats = () => import('@/components/Service/ServiceTopStats')
const AdvancedChart = () => import('@/components/Service/AdvancedChart')
const MessageBlock = () => import(/* webpackChunkName: "index" */ '@/components/Index/MessageBlock')
const ServiceFailures = () => import(/* webpackChunkName: "service" */ '@/components/Service/ServiceFailures')
const Checkin = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Checkin')
const ServiceHeatmap = () => import(/* webpackChunkName: "service" */ '@/components/Service/ServiceHeatmap')
const ServiceTopStats = () => import(/* webpackChunkName: "service" */ '@/components/Service/ServiceTopStats')
const AdvancedChart = () => import(/* webpackChunkName: "service" */ '@/components/Service/AdvancedChart')
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
@ -120,13 +130,14 @@ export default {
},
data() {
return {
service: null,
tab: "failures",
authenticated: false,
ready: true,
group: "1h",
data: null,
uptime_data: null,
loading: true,
loaded: false,
messages: [],
failures: [],
start_time: this.nowSubtract(84600 * 30),
@ -144,6 +155,7 @@ export default {
chart: {
id: 'uptime',
height: 120,
width: "100%",
type: 'rangeBar',
toolbar: {
show: false
@ -336,9 +348,6 @@ export default {
}
},
computed: {
service () {
return this.$store.getters.serviceByAll(this.id)
},
core () {
return this.$store.getters.core
},
@ -362,30 +371,36 @@ export default {
},
},
watch: {
service: function(n, o) {
this.onnn()
},
load_timedata: function(n, o) {
this.onnn()
'$route': 'reload',
},
created() {
this.reload()
},
async mounted() {
if (!this.$store.getters.service) {
// const s = await Api.service(this.id)
// this.$store.commit('setService', s)
}
},
async mounted() {
if (!this.$store.getters.service) {
const s = await Api.service(this.id)
this.$store.commit('setService', s)
}
},
methods: {
async updated_chart(start, end) {
this.loaded = false
this.start_time = start
this.end_time = end
this.loading = false
this.loaded = true
},
async onnn() {
this.loading = true
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.fetchUptime()
this.loading = false
this.loaded = true
},
async fetchUptime() {
const uptime = await Api.service_uptime(this.service.id, this.params.start, this.params.end)
@ -395,7 +410,6 @@ export default {
const data = timedata.series.filter((g) => g.online) || []
const offData = timedata.series.filter((g) => !g.online) || []
let arr = [];
window.console.log(data)
if (data) {
data.forEach((d) => {
arr.push({

View File

@ -119,12 +119,12 @@
import GithubButton from 'vue-github-button'
import Variables from "@/components/Dashboard/Variables";
const CoreSettings = () => import('@/forms/CoreSettings')
const FormIntegration = () => import('@/forms/Integration')
const Notifier = () => import('@/forms/Notifier')
const OAuth = () => import('@/forms/OAuth')
const ThemeEditor = () => import('@/components/Dashboard/ThemeEditor')
const Cache = () => import('@/components/Dashboard/Cache')
const CoreSettings = () => import(/* webpackChunkName: "dashboard" */ '@/forms/CoreSettings')
const FormIntegration = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Integration')
const Notifier = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Notifier')
const OAuth = () => import(/* webpackChunkName: "dashboard" */ '@/forms/OAuth')
const ThemeEditor = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ThemeEditor')
const Cache = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/Cache')
export default {
name: 'Settings',
@ -152,10 +152,10 @@
}
},
mounted() {
this.update()
},
created() {
this.update()
this.update()
},
methods: {
async update() {

View File

@ -1,28 +1,36 @@
const Index = () => import('@/pages/Index')
const Dashboard = () => import('@/pages/Dashboard')
const DashboardIndex = () => import('@/components/Dashboard/DashboardIndex')
const DashboardUsers = () => import('@/components/Dashboard/DashboardUsers')
const DashboardServices = () => import('@/components/Dashboard/DashboardServices')
const DashboardMessages = () => import('@/components/Dashboard/DashboardMessages')
const EditService = () => import('@/components/Dashboard/EditService')
const Logs = () => import('@/pages/Logs')
const Settings = () => import('@/pages/Settings')
const Login = () => import('@/pages/Login')
const Service = () => import('@/pages/Service')
const Setup = () => import('@/forms/Setup')
const Incidents = () => import('@/components/Dashboard/Incidents')
const Checkins = () => import('@/components/Dashboard/Checkins')
const Failures = () => import('@/components/Dashboard/Failures')
const NotFound = () => import('@/pages/NotFound')
const Index = () => import(/* webpackChunkName: "index" */ '@/pages/Index')
const Dashboard = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Dashboard')
const DashboardIndex = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/DashboardIndex')
const DashboardUsers = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/DashboardUsers')
const DashboardServices = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/DashboardServices')
const DashboardMessages = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/DashboardMessages')
const EditService = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/EditService')
const Logs = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Logs')
const Settings = () => import(/* webpackChunkName: "dashboard" */ '@/pages/Settings')
const Login = () => import(/* webpackChunkName: "index" */ '@/pages/Login')
const Service = () => import(/* webpackChunkName: "index" */ '@/pages/Service')
const Setup = () => import(/* webpackChunkName: "index" */ '@/forms/Setup')
const Incidents = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/Incidents')
const Checkins = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/Checkins')
const Failures = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/Failures')
const NotFound = () => import(/* webpackChunkName: "index" */ '@/pages/NotFound')
import VueRouter from "vue-router";
import Api from "./API";
import store from "./store"
const Loading = {
template: '<div class="jumbotron">LOADING</div>'
}
const routes = [
{
path: '/setup',
name: 'Setup',
component: Setup
component: Setup,
meta: {
title: 'Statping Setup',
}
},
{
path: '/',
@ -33,14 +41,37 @@ const routes = [
path: '/dashboard',
component: Dashboard,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Dashboard',
},
beforeEnter: async (to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
let tk = await Api.token()
if (to.path !== '/login' && !tk) {
next('/login')
return
if (to.path !== '/login') {
if(store.getters.loggedIn) {
next()
return
}
const token = $cookies.get('statping_auth')
if (!token) {
next('/login')
return
}
try {
const jwt = await Api.check_token(token)
store.commit('setAdmin', jwt.admin)
if (jwt.admin) {
store.commit('setLoggedIn', true)
store.commit('setUser', true)
} else {
store.commit('setLoggedIn', false)
next('/login')
return
}
} catch (e) {
console.error(e)
next('/login')
return
}
}
next()
} else {
@ -51,80 +82,96 @@ const routes = [
path: '',
component: DashboardIndex,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Dashboard',
}
},{
path: 'users',
component: DashboardUsers,
loading: Loading,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Users',
}
},{
path: 'services',
component: DashboardServices,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Services',
}
},{
path: 'create_service',
component: EditService,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Create Service',
}
},{
path: 'edit_service/:id',
component: EditService,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Edit Service',
}
},{
path: 'service/:id/incidents',
component: Incidents,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Incidents',
}
},{
path: 'service/:id/checkins',
component: Checkins,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Checkins',
}
},{
path: 'service/:id/failures',
component: Failures,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Service Failures',
}
},{
path: 'messages',
component: DashboardMessages,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Messages',
}
},{
path: 'settings',
component: Settings,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Settings',
}
},{
path: 'logs',
component: Logs,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Logs',
}
},{
path: 'help',
component: Logs,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Help',
}
}]
},
{
path: '/login',
name: 'Login',
component: Login
component: Login,
meta: {
title: 'Statping - Login',
}
},
{ path: '/logout', redirect: '/' },
{
@ -152,23 +199,23 @@ const router = new VueRouter({
routes
})
let CheckAuth = (to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
let item = this.$cookies.get("statping_auth")
window.console.log(item)
if (to.path !== '/login' && !item) {
next('/login')
return
}
const auth = JSON.parse(item)
if (!auth.token) {
next('/login')
return
}
next()
} else {
next()
}
}
router.beforeEach((to, from, next) => {
const nearestWithTitle = to.matched.slice().reverse().find(r => r.meta && r.meta.title);
const nearestWithMeta = to.matched.slice().reverse().find(r => r.meta && r.meta.metaTags);
const previousNearestWithMeta = from.matched.slice().reverse().find(r => r.meta && r.meta.metaTags);
if(nearestWithTitle) document.title = nearestWithTitle.meta.title;
Array.from(document.querySelectorAll('[data-vue-router-controlled]')).map(el => el.parentNode.removeChild(el));
if(!nearestWithMeta) return next();
nearestWithMeta.meta.metaTags.map(tagDef => {
const tag = document.createElement('meta');
Object.keys(tagDef).forEach(key => {
tag.setAttribute(key, tagDef[key]);
});
tag.setAttribute('data-vue-router-controlled', '');
return tag;
})
.forEach(tag => document.head.appendChild(tag));
next();
});
export default router

View File

@ -30,7 +30,8 @@ export default new Vuex.Store({
notifiers: [],
checkins: [],
admin: false,
user: false
user: false,
loggedIn: false
},
getters: {
hasAllData: state => state.hasAllData,
@ -46,6 +47,7 @@ export default new Vuex.Store({
users: state => state.users,
notifiers: state => state.notifiers,
checkins: state => state.checkins,
loggedIn: state => state.loggedIn,
isAdmin: state => state.admin,
isUser: state => state.user,
@ -61,13 +63,13 @@ export default new Vuex.Store({
},
serviceByAll: (state) => (element) => {
if (element % 1 === 0) {
return state.services.find(s => s.id == element)
return state.services.find(s => s.id === element)
} else {
return state.services.find(s => s.permalink === element)
}
},
serviceById: (state) => (id) => {
return state.services.find(s => s.id == id)
return state.services.find(s => s.id === id)
},
serviceByPermalink: (state) => (permalink) => {
return state.services.find(s => s.permalink === permalink)
@ -131,6 +133,9 @@ export default new Vuex.Store({
setAdmin (state, admin) {
state.admin = admin
},
setLoggedIn (state, loggedIn) {
state.loggedIn = loggedIn
},
setUser (state, user) {
state.user = user
},

File diff suppressed because it is too large Load Diff

8
go.mod
View File

@ -9,9 +9,9 @@ require (
github.com/fatih/structs v1.1.0
github.com/foomo/simplecert v1.7.5
github.com/foomo/tlsconfig v0.0.0-20180418120404-b67861b076c9
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/getsentry/sentry-go v0.5.1
github.com/go-mail/mail v2.3.1+incompatible
github.com/golang/protobuf v1.4.0
github.com/gorilla/mux v1.7.4
github.com/hako/durafmt v0.0.0-20200605151348-3a43fc422dd9
github.com/jinzhu/gorm v1.9.12
@ -24,12 +24,14 @@ require (
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.0.0
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.3
github.com/stretchr/testify v1.5.1
github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1
golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a
github.com/tdewolff/minify/v2 v2.8.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
google.golang.org/grpc v1.28.1
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect

19
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.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -357,6 +358,7 @@ github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6i
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d h1:V5Rs9ztEWdp58oayPq/ulmlqJJZeJP6pP79uP3qjcao=
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.0 h1:GhthINjveNZAdFUD8QoQYfjxnOONZgztK/Yr6M23UTY=
@ -396,6 +398,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.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
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-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@ -573,6 +576,12 @@ 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/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/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 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
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/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -641,6 +650,8 @@ golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 h1:DOmugCavvUtnUD114C1Wh+
golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a h1:y6sBfNd1b9Wy08a6K1Z1DZc4aXABUN5TKjkYhz7UKmo=
golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -662,6 +673,7 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@ -704,6 +716,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -719,6 +733,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -759,6 +775,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -808,6 +826,7 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4 h1:kDtqNkeBrZb8B+atrj50B5XLHpzXXqcCdZPP/ApQ5NY=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -112,6 +112,9 @@ func TestSetupRoutes(t *testing.T) {
if !core.App.Setup {
return errors.New("core has not been setup")
}
if core.App.ApiSecret == "" {
return errors.New("API Key has not been set")
}
if len(services.AllInOrder()) == 0 {
return errors.New("no services where found")
}

View File

@ -84,19 +84,35 @@ func apiThemeSaveHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
defer r.Body.Close()
fmt.Println(themes.Variables)
if err := source.SaveAsset([]byte(themes.Base), "scss/base.scss"); err != nil {
sendErrorJson(err, w, r)
return
}
if err := source.SaveAsset([]byte(themes.Layout), "scss/layout.scss"); err != nil {
sendErrorJson(err, w, r)
return
}
if err := source.SaveAsset([]byte(themes.Variables), "scss/variables.scss"); err != nil {
sendErrorJson(err, w, r)
return
}
if err := source.SaveAsset([]byte(themes.Forms), "scss/forms.scss"); err != nil {
sendErrorJson(err, w, r)
return
}
if err := source.SaveAsset([]byte(themes.Mixins), "scss/mixin.scss"); err != nil {
sendErrorJson(err, w, r)
return
}
if err := source.SaveAsset([]byte(themes.Mobile), "scss/mobile.scss"); err != nil {
sendErrorJson(err, w, r)
return
}
if err := source.CompileSASS(source.DefaultScss...); err != nil {
if err := source.CompileSASS(); err != nil {
sendErrorJson(err, w, r)
return
}
@ -114,14 +130,14 @@ func apiThemeCreateHandler(w http.ResponseWriter, r *http.Request) {
}
utils.Log.Infof("creating assets in folder: %s/%s", dir, "assets")
if err := source.CreateAllAssets(dir); err != nil {
log.Errorln(err)
sendErrorJson(err, w, r)
return
}
if err := source.CompileSASS(source.DefaultScss...); err != nil {
source.CopyToPublic(source.TmplBox, "css", "main.css")
source.CopyToPublic(source.TmplBox, "css", "base.css")
log.Errorln("Default 'base.css' was inserted because SASS did not work.")
if err := source.CopyToPublic(source.TmplBox, "css", "style.css"); err != nil {
log.Errorln(err)
sendErrorJson(err, w, r)
return
} else {
log.Errorln(err)
sendErrorJson(err, w, r)
}
}
resetRouter()
sendJsonAction(dir+"/assets", "created", w, r)

View File

@ -141,7 +141,7 @@ func TestGroupAPIRoutes(t *testing.T) {
},
{
Name: "Statping View Unknown Group",
URL: "/api/groups/8383883838",
URL: "/api/groups/38383",
Method: "GET",
BeforeTest: SetTestENV,
ExpectedStatus: 404,

View File

@ -45,7 +45,7 @@ func apiIncidentUpdatesHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
returnJson(incid.Updates(), w, r)
returnJson(incid.Updates, w, r)
}
func apiCreateIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
@ -100,8 +100,7 @@ func apiIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
return
}
updates := incident.Updates()
sendJsonAction(updates, "update", w, r)
sendJsonAction(incident.Updates, "update", w, r)
}
func apiDeleteIncidentHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -22,3 +22,8 @@ func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
}
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

@ -51,17 +51,9 @@ func setJwtToken(user *users.User, w http.ResponseWriter) (JwtClaim, string) {
return jwtClaim, tokenString
}
func getJwtToken(r *http.Request) (JwtClaim, error) {
c, err := r.Cookie(cookieName)
if err != nil {
if err == http.ErrNoCookie {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
func parseToken(token string) (JwtClaim, error) {
var claims JwtClaim
tkn, err := jwt.ParseWithClaims(c.Value, &claims, func(token *jwt.Token) (interface{}, error) {
tkn, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
@ -74,5 +66,16 @@ func getJwtToken(r *http.Request) (JwtClaim, error) {
if !tkn.Valid {
return claims, errors.New("token is not valid")
}
return claims, err
return claims, nil
}
func getJwtToken(r *http.Request) (JwtClaim, error) {
c, err := r.Cookie(cookieName)
if err != nil {
if err == http.ErrNoCookie {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
return parseToken(c.Value)
}

View File

@ -2,6 +2,7 @@ package handlers
import (
"github.com/gorilla/mux"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/services"
@ -11,12 +12,10 @@ import (
func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
var notifs []notifications.Notification
notifiers := services.AllNotifiers()
for _, n := range notifiers {
notif := n.Select()
notifer, _ := notifications.Find(notif.Method)
notif.UpdateFields(notifer)
notifs = append(notifs, *notif)
for _, n := range services.AllNotifiers() {
no := n.Select()
notif, _ := notifications.Find(no.Method)
notifs = append(notifs, *no.UpdateFields(notif))
}
sort.Sort(notifications.NotificationOrder(notifs))
returnJson(notifs, w, r)
@ -24,10 +23,9 @@ func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
notif := services.FindNotifier(vars["notifier"])
notifer, err := notifications.Find(notif.Method)
if err != nil {
sendErrorJson(err, w, r)
notifer := services.FindNotifier(vars["notifier"])
if notifer == nil {
sendErrorJson(errors.New("could not find notifier"), w, r)
return
}
returnJson(notifer, w, r)
@ -48,13 +46,17 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
log.Infof("Updating %s Notifier", notifer.Title)
err = notifer.Update()
if err != nil {
if err := notifer.Update(); err != nil {
sendErrorJson(err, w, r)
return
}
notif := services.ReturnNotifier(notifer.Method)
if err := notif.Valid(notifer.Values()); err != nil {
sendErrorJson(err, w, r)
return
}
if _, err := notif.OnSave(); err != nil {
sendErrorJson(err, w, r)
return
@ -70,9 +72,9 @@ type testNotificationReq struct {
func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
n, err := notifications.Find(vars["notifier"])
if err != nil {
sendErrorJson(err, w, r)
n := services.FindNotifier(vars["notifier"])
if n == nil {
sendErrorJson(errors.New("unknown notifier"), w, r)
return
}
@ -85,6 +87,7 @@ func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
notif := services.ReturnNotifier(n.Method)
var out string
var err error
if req.Method == "success" {
out, err = notif.OnSuccess(services.Example(true))
} else {

View File

@ -5,6 +5,7 @@ import (
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
@ -12,7 +13,7 @@ func TestAttachment(t *testing.T) {
notifiers.InitNotifiers()
}
func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
func TestAuthenticatedNotifierRoutes(t *testing.T) {
slackWebhookUrl := utils.Params.GetString("SLACK_URL")
tests := []HTTPTest{
@ -42,7 +43,12 @@ func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
URL: "/api/notifier/slack",
Method: "GET",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
BeforeTest: func(t *testing.T) error {
notif := services.FindNotifier("slack")
require.NotNil(t, notif)
assert.Equal(t, "slack", notif.Method)
return SetTestENV(t)
},
},
{
Name: "No Authentication - Update Notifier",
@ -68,7 +74,20 @@ func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
"limits": 55
}`,
ExpectedStatus: 200,
BeforeTest: SetTestENV,
BeforeTest: func(t *testing.T) error {
notif := services.FindNotifier("slack")
require.NotNil(t, notif)
assert.Equal(t, "slack", notif.Method)
assert.False(t, notif.Enabled.Bool)
return SetTestENV(t)
},
AfterTest: func(t *testing.T) error {
notif := services.FindNotifier("slack")
require.NotNil(t, notif)
assert.Equal(t, "slack", notif.Method)
assert.True(t, notif.Enabled.Bool)
return UnsetTestENV(t)
},
},
{
Name: "Test Notifier (OnSuccess)",
@ -82,7 +101,7 @@ func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
"method": "slack",
"host": "` + slackWebhookUrl + `",
"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}",
"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}"
"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}"
}
}`,
ExpectedStatus: 200,
@ -101,7 +120,7 @@ func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
"method": "slack",
"host": "` + slackWebhookUrl + `",
"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}",
"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}"
"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}"
}
}`,
ExpectedStatus: 200,
@ -154,7 +173,7 @@ func TestApiNotifiersRoutes(t *testing.T) {
URL: "/api/notifier/slack",
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{`"method":"slack"`},
ExpectedContains: []string{`"method":"slack"`, `"host":"https://slack.api/example/12345"`},
BeforeTest: SetTestENV,
SecureRoute: true,
},

View File

@ -69,24 +69,22 @@ func Router() *mux.Router {
if source.UsingAssets(dir) {
indexHandler := http.FileServer(http.Dir(dir + "/assets/"))
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath, Gzip(staticAssets("css"))))
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath, Gzip(staticAssets("js"))))
r.PathPrefix("/scss/").Handler(http.StripPrefix(basePath, Gzip(staticAssets("scss"))))
r.PathPrefix("/favicon/").Handler(http.StripPrefix(basePath, Gzip(staticAssets("favicon"))))
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath, staticAssets("css")))
r.PathPrefix("/favicon/").Handler(http.StripPrefix(basePath, staticAssets("favicon")))
r.PathPrefix("/robots.txt").Handler(http.StripPrefix(basePath, indexHandler))
r.PathPrefix("/banner.png").Handler(http.StripPrefix(basePath, indexHandler))
} else {
tmplFileSrv := http.FileServer(source.TmplBox.HTTPBox())
tmplBoxHandler := http.StripPrefix(basePath, tmplFileSrv)
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath, Gzip(tmplFileSrv)))
r.PathPrefix("/scss/").Handler(http.StripPrefix(basePath, Gzip(tmplFileSrv)))
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath, Gzip(tmplFileSrv)))
r.PathPrefix("/favicon/").Handler(http.StripPrefix(basePath, Gzip(tmplFileSrv)))
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath, tmplFileSrv))
r.PathPrefix("/favicon/").Handler(http.StripPrefix(basePath, tmplFileSrv))
r.PathPrefix("/robots.txt").Handler(tmplBoxHandler)
r.PathPrefix("/banner.png").Handler(tmplBoxHandler)
}
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath, http.FileServer(source.TmplBox.HTTPBox())))
api := r.NewRoute().Subrouter()
api.Use(apiMiddleware)
api.Use(prometheusMiddleware)
@ -154,6 +152,7 @@ func Router() *mux.Router {
// API USER Routes
api.Handle("/api/users", authenticated(apiAllUsersHandler, false)).Methods("GET")
api.Handle("/api/users", authenticated(apiCreateUsersHandler, false)).Methods("POST")
api.Handle("/api/users/token", http.HandlerFunc(apiCheckUserTokenHandler)).Methods("POST")
api.Handle("/api/users/{id}", authenticated(apiUserHandler, false)).Methods("GET")
api.Handle("/api/users/{id}", authenticated(apiUserUpdateHandler, false)).Methods("POST")
api.Handle("/api/users/{id}", authenticated(apiUserDeleteHandler, false)).Methods("DELETE")
@ -181,11 +180,12 @@ func Router() *mux.Router {
// API Generic Routes
r.Handle("/metrics", readOnly(promhttp.Handler(), false))
r.Handle("/health", http.HandlerFunc(healthCheckHandler))
r.NotFoundHandler = http.HandlerFunc(error404Handler)
r.NotFoundHandler = http.HandlerFunc(notFoundHandler)
return r
}
func resetRouter() {
log.Infoln("Restarting HTTP Router")
router = Router()
httpServer.Handler = router
}

View File

@ -269,8 +269,7 @@ func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
err = service.DeleteFailures()
if err != nil {
if err := service.AllFailures().DeleteAll(); err != nil {
sendErrorJson(err, w, r)
return
}

View File

@ -11,7 +11,6 @@ import (
"net/http"
"net/url"
"strconv"
"time"
)
func processSetupHandler(w http.ResponseWriter, r *http.Request) {
@ -123,7 +122,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
CacheStorage.Delete("/")
resetCookies()
time.Sleep(2 * time.Second)
out := struct {
Message string `json:"message"`
Config *configs.DbConfig `json:"config"`

View File

@ -80,6 +80,23 @@ func apiAllUsersHandler(w http.ResponseWriter, r *http.Request) {
returnJson(allUsers, w, r)
}
func apiCheckUserTokenHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
token := r.PostForm.Get("token")
if token == "" {
sendErrorJson(errors.New("missing token parameter"), w, r)
return
}
claim, err := parseToken(token)
if err != nil {
sendErrorJson(err, w, r)
return
}
returnJson(claim, w, r)
}
func apiCreateUsersHandler(w http.ResponseWriter, r *http.Request) {
var user *users.User
err := DecodeJSON(r, &user)

View File

@ -22,6 +22,10 @@ func (c *commandLine) Select() *notifications.Notification {
return c.Notification
}
func (c *commandLine) Valid(values notifications.Values) error {
return nil
}
var Command = &commandLine{&notifications.Notification{
Method: "command",
Title: "Command",

View File

@ -51,6 +51,10 @@ func (d *discord) Select() *notifications.Notification {
return d.Notification
}
func (d *discord) Valid(values notifications.Values) error {
return nil
}
// OnFailure will trigger failing service
func (d *discord) OnFailure(s services.Service, f failures.Failure) (string, error) {
out, err := d.sendRequest(ReplaceVars(d.FailureData.String, s, f))

View File

@ -28,6 +28,10 @@ func (e *emailer) Select() *notifications.Notification {
return e.Notification
}
func (e *emailer) Valid(values notifications.Values) error {
return nil
}
var email = &emailer{&notifications.Notification{
Method: "email",
Title: "SMTP Mail",
@ -88,7 +92,7 @@ type emailOutgoing struct {
// OnFailure will trigger failing service
func (e *emailer) OnFailure(s services.Service, f failures.Failure) (string, error) {
subject := fmt.Sprintf("Service %s is Offline", s.Name)
tmpl := renderEmail(s, f)
tmpl := renderEmail(s, f, emailFailure)
email := &emailOutgoing{
To: e.Var2.String,
Subject: subject,
@ -101,7 +105,7 @@ func (e *emailer) OnFailure(s services.Service, f failures.Failure) (string, err
// OnSuccess will trigger successful service
func (e *emailer) OnSuccess(s services.Service) (string, error) {
subject := fmt.Sprintf("Service %s is Back Online", s.Name)
tmpl := renderEmail(s, failures.Failure{})
tmpl := renderEmail(s, failures.Failure{}, emailSuccess)
email := &emailOutgoing{
To: e.Var2.String,
Subject: subject,
@ -111,13 +115,13 @@ func (e *emailer) OnSuccess(s services.Service) (string, error) {
return tmpl, e.dialSend(email)
}
func renderEmail(s services.Service, f failures.Failure) string {
func renderEmail(s services.Service, f failures.Failure, emailData string) string {
wr := bytes.NewBuffer(nil)
tmpl := template.New("email")
tmpl, err := tmpl.Parse(emailBase)
tmpl, err := tmpl.Parse(emailData)
if err != nil {
log.Errorln(err)
return emailBase
return emailData
}
data := replacer{
@ -129,7 +133,7 @@ func renderEmail(s services.Service, f failures.Failure) string {
if err = tmpl.ExecuteTemplate(wr, "email", data); err != nil {
log.Errorln(err)
return emailBase
return emailData
}
return wr.String()
@ -142,7 +146,7 @@ func (e *emailer) OnTest() (string, error) {
email := &emailOutgoing{
To: e.Var2.String,
Subject: subject,
Template: renderEmail(service, failures.Example()),
Template: renderEmail(service, failures.Example(), emailFailure),
From: e.Var1.String,
}
return subject, e.dialSend(email)

52
notifiers/email_rendered.go Executable file

File diff suppressed because one or more lines are too long

View File

@ -1,628 +0,0 @@
package notifiers
const emailBase = `
{{$banner := "https://assets.statping.com/greenbackground.png"}}
{{$color := "#4caf50"}}
{{if not .Service.Online}}
{{$banner = "https://assets.statping.com/offlinebanner.png"}}
{{$color = "#c30c0c"}}
{{end}}
<!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">
<head>
<title> Statping Service Notification </title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E7E7E7;">
<div style="background-color:#E7E7E7;">
<!-- Top Bar -->
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<v:rect style="width:600px;" xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false">
<v:fill origin="0.5, 0" position="0.5, 0" src="{{$banner}}" color="#FF3FB4" type="tile" />
<v:textbox style="mso-fit-shape-to-text:true" inset="0,0,0,0">
<![endif]-->
<div style="background:#FF3FB4 url({{$banner}}) top center / auto repeat;margin:0px auto;max-width:600px;">
<div style="line-height:0;font-size:0;">
<table align="center" background="{{$banner}}" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FF3FB4 url({{$banner}}) top center / auto repeat;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:45px;"> <a href="https://statping.com" target="_blank">
<img
alt="Statping" height="auto" src="https://assets.statping.com/iconlight.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="45"
/>
</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!--[if mso | IE]>
</v:textbox>
</v:rect>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:15px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:22px;line-height:30px;text-align:left;color:#000000;">
{{if .Service.Online}}
{{.Service.Name}} is back online.
{{else}}
{{.Service.Name}} is currently offline, you might want to check it.
{{end}}
</div>
</td>
</tr>
<tr>
<td style="font-size:0px;padding:20px 0;padding-top:10px;padding-right:0px;padding-bottom:10px;padding-left:0px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:10px;padding-left:0px;padding-right:0px;padding-top:10px;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:center;color:#626262;">
{{if .Service.Online}}
Online for {{.Service.Uptime.Human}}
{{else}}
Offline for {{.Service.Downtime.Human}}
{{end}}
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="{{$color}}" role="presentation" style="border:none;border-radius:4px;cursor:auto;mso-padding-alt:10px 25px;background:{{$color}};" valign="middle">
<a href="{{.Core.Domain}}/service/{{.Service.Id}}" style="display:inline-block;background:{{$color}};color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:4px;"
target="_blank">
View Dashboard
</a> </td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<!-- Bottom Graphic -->
<tr>
<td style="font-size:0px;padding:0px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#fafafa;background-color:#fafafa;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#fafafa;background-color:#fafafa;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:left;color:#626262;">Service Domain</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#626262;">{{.Service.Domain}}</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td style="font-size:0px;padding:0px;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
{{if .Failure}}
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:left;color:#626262;">Current Issue</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:14px;line-height:1;text-align:left;color:#626262;">{{.Failure.Issue}}</div>
</td>
</tr>
</table>
</div>
{{end}}
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td style="font-size:0px;word-break:break-word;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td height="30" style="vertical-align:top;height:30px;">
<![endif]-->
<div style="height:30px;"> &nbsp; </div>
<!--[if mso | IE]>
</td></tr></table>
<![endif]-->
</td>
</tr>
<tr>
<td style="font-size:0px;padding:0;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<v:rect style="width:600px;" xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false">
<v:fill origin="0.5, 0" position="0.5, 0" src="{{$banner}}" color="#F15822" type="tile" />
<v:textbox style="mso-fit-shape-to-text:true" inset="0,0,0,0">
<![endif]-->
<div style="background:#F15822 url({{$banner}}) top center / auto repeat;margin:0px auto;max-width:600px;">
<div style="line-height:0;font-size:0;">
<table align="center" background="{{$banner}}" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#F15822 url({{$banner}}) top center / auto repeat;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:250px;"> <a href="https://www.sphero.com" target="_blank">
<img
height="auto" src="https://assets.statping.com/statpingcom.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="250"
/>
</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!--[if mso | IE]>
</v:textbox>
</v:rect>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td style="font-size:0px;padding:20px 0;padding-top:10px;padding-bottom:0;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0;padding-top:10px;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:16px;text-align:center;color:#445566;">You are receiving this email because one of your services has changed on your Statping instance. You can modify this email on the Email Notifier page in Settings.</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;line-height:16px;text-align:center;color:#445566;">&copy; Statping</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td style="font-size:0px;padding:20px 0;padding-top:0;padding-bottom:0;word-break:break-word;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0;padding-top:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;">
<!--[if mso | IE]>
<table
border="0" cellpadding="0" cellspacing="0" role="presentation"
>
<tr>
<td
style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding-right:0;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:11px;font-weight:bold;line-height:16px;text-align:center;color:#445566;"><a class="footer-link" href="https://statping.com">Statping.com</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0; <a class="footer-link" href="https://github.com/statping/statping">Github</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;
<a class="footer-link" href="https://statping.com/privacy">Privacy</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0; <a class="footer-link" href="https://www.google.com">Unsubscribe</a></div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div>
</body>
</html>
`

270
notifiers/generate.go Normal file
View File

@ -0,0 +1,270 @@
// +build ignore
package main
import (
"bytes"
"encoding/json"
"fmt"
"github.com/statping/statping/utils"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/html"
"os"
"time"
)
var (
mjmlApplication string
mjmlPrivate string
)
func main() {
utils.InitEnvs()
mjmlApplication = os.Getenv("MJML_APP")
mjmlPrivate = os.Getenv("MJML_PRIVATE")
if mjmlApplication == "" || mjmlPrivate == "" {
fmt.Println("skipping email MJML template render, missing MJML_APP and MJML_PRIVATE")
return
}
fmt.Println("Generating success/failure email templates from MJML to a HTML golang constant")
success := convertMJML(emailSuccessMJML)
fail := convertMJML(emailFailureMJML)
htmlOut := `// DO NOT EDIT ** This file was generated with go generate on ` + utils.Now().String() + ` ** DO NOT EDIT //
package notifiers
const emailSuccess = ` + minimize(success) + `
const emailFailure = ` + minimize(fail) + `
`
utils.SaveFile("email_rendered.go", []byte(htmlOut))
fmt.Println("Email MJML to HTML const saved: notifiers/email_rendered.go")
}
type mjmlInput struct {
Mjml string `json:"mjml"`
}
func minimize(val string) string {
m := minify.New()
m.Add("text/html", &html.Minifier{
KeepDefaultAttrVals: true,
})
s, err := m.String("text/html", val)
if err != nil {
panic(err)
}
return fmt.Sprintf("`%s`", s)
}
func convertMJML(mjml string) string {
input, _ := json.Marshal(mjmlInput{mjml})
auth := fmt.Sprintf("%s:%s", mjmlApplication, mjmlPrivate)
resp, _, err := utils.HttpRequest("https://"+auth+"@api.mjml.io/v1/render", "POST", "application/json", nil, bytes.NewBuffer(input), 15*time.Minute, false, nil)
if err != nil {
panic(err)
}
var respData mjmlApi
if err := json.Unmarshal(resp, &respData); err != nil {
panic(err)
}
return respData.Html
}
type mjmlApi struct {
Html string `json:"html"`
Mjml string `json:"mjml"`
Version string `json:"mjml_version"`
}
const emailFailureMJML = `<mjml>
<mj-head>
<mj-title>Statping Service Notification</mj-title>
</mj-head>
<mj-body background-color="#E7E7E7">
<mj-raw>
<!-- Top Bar -->
</mj-raw>
<mj-section background-color="#a30911" background-url="https://assets.statping.com/offlinebanner.png" padding="0px">
<mj-column>
<mj-image width="45px" href="https://statping.com" src="https://assets.statping.com/iconlight.png" align="center" alt="Sphero"></mj-image>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff">
<mj-column width="100%">
<mj-text font-family="Ubuntu, Helvetica, Arial, sans-serif" font-size="22px" padding="15px" line-height="30px">
{{.Service.Name}} is currently offline, you might want to check it.
</mj-text>
<mj-section padding-left="0px" padding-right="0px" padding-top="10px" padding-bottom="10px">
<mj-column>
<mj-text font-color="#d50d0d" align="center" font-size="20px" color="#626262">Offline for {{.Service.Downtime.Human}}</mj-text>
<mj-button border-radius="4px" background-color="#cb121c" href="{{.Core.Domain}}/service/{{.Service.Id}}">View Dashboard</mj-button>
</mj-column>
</mj-section>
<mj-raw>
<!-- Bottom Graphic -->
</mj-raw>
<mj-section padding="0px" background-color="#fafafa">
<mj-column>
<mj-text font-size="20px" color="#626262">Service Domain</mj-text>
<mj-text padding-top="0px" font-size="14px" color="#626262">{{.Service.Domain}}</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0px" background-color="#ffffff">
<mj-column>
<mj-text font-size="20px" color="#626262">Current Issue</mj-text>
<mj-text padding-top="0px" font-size="14px" color="#626262">{{.Failure.Issue}}</mj-text>
</mj-column>
</mj-section>
<mj-spacer height="30px" />
<mj-section padding="0" background-url="https://assets.statping.com/offlinebanner.png" background-color="#a30911">
<mj-column>
<mj-image width="250px" href="https://statping.com" src="https://assets.statping.com/statpingcom.png" align="center"></mj-image>
</mj-column>
</mj-section>
<mj-section padding-bottom="0" padding-top="10px">
<mj-column>
<mj-text color="#445566" font-size="11px" align="center" line-height="16px">
You are receiving this email because one of your services has changed on your Statping instance. You can modify this email on the Email Notifier page in Settings.
</mj-text>
<mj-text color="#445566" font-size="11px" align="center" line-height="16px">
&copy; Statping
</mj-text>
</mj-column>
</mj-section>
<mj-section padding-top="0" padding-bottom="0">
<mj-group>
<mj-column width="100%" padding-right="0">
<mj-text color="#445566" font-size="11px" align="center" line-height="16px" font-weight="bold">
<a class="footer-link" href="https://statping.com">Statping.com</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;
<a class="footer-link" href="https://github.com/statping/statping">Github</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;
<a class="footer-link" href="https://statping.com/privacy">Privacy</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;
</mj-text>
</mj-column>
</mj-group>
</mj-section>
</mj-column>
</mj-section>
</mj-body>
</mjml>`
const emailSuccessMJML = `<mjml>
<mj-head>
<mj-title>Statping Service Notification</mj-title>
</mj-head>
<mj-body background-color="#E7E7E7">
<mj-raw>
<!-- Top Bar -->
</mj-raw>
<mj-section background-color="#12ab0c" background-url="https://assets.statping.com/greenbackground.png" padding="0px">
<mj-column>
<mj-image width="45px" href="https://statping.com" src="https://assets.statping.com/iconlight.png" align="center" alt="Sphero"></mj-image>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff">
<mj-column width="100%">
<mj-text font-family="Ubuntu, Helvetica, Arial, sans-serif" font-size="22px" padding="15px" line-height="30px">
{{.Service.Name}} is currently offline, you might want to check it.
</mj-text>
<mj-section padding-left="0px" padding-right="0px" padding-top="10px" padding-bottom="10px">
<mj-column>
<mj-text font-color="#d50d0d" align="center" font-size="20px" color="#626262">Offline for {{.Service.Downtime.Human}}</mj-text>
<mj-button border-radius="4px" background-color="#4caf50" href="{{.Core.Domain}}/service/{{.Service.Id}}">View Dashboard</mj-button>
</mj-column>
</mj-section>
<mj-raw>
<!-- Bottom Graphic -->
</mj-raw>
<mj-section padding="0px" background-color="#fafafa">
<mj-column>
<mj-text font-size="20px" color="#626262">Service Domain</mj-text>
<mj-text padding-top="0px" font-size="14px" color="#626262">{{.Service.Domain}}</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0px" background-color="#ffffff">
<mj-column>
<mj-text font-size="20px" color="#626262">Current Issue</mj-text>
<mj-text padding-top="0px" font-size="14px" color="#626262">{{.Failure.Issue}}</mj-text>
</mj-column>
</mj-section>
<mj-spacer height="30px" />
<mj-section padding="0" background-url="https://assets.statping.com/greenbackground.png" background-color="#12ab0c">
<mj-column>
<mj-image width="250px" href="https://statping.com" src="https://assets.statping.com/statpingcom.png" align="center"></mj-image>
</mj-column>
</mj-section>
<mj-section padding-bottom="0" padding-top="10px">
<mj-column>
<mj-text color="#445566" font-size="11px" align="center" line-height="16px">
You are receiving this email because one of your services has changed on your Statping instance. You can modify this email on the Email Notifier page in Settings.
</mj-text>
<mj-text color="#445566" font-size="11px" align="center" line-height="16px">
&copy; Statping
</mj-text>
</mj-column>
</mj-section>
<mj-section padding-top="0" padding-bottom="0">
<mj-group>
<mj-column width="100%" padding-right="0">
<mj-text color="#445566" font-size="11px" align="center" line-height="16px" font-weight="bold">
<a class="footer-link" href="https://statping.com">Statping.com</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;
<a class="footer-link" href="https://github.com/statping/statping">Github</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;
<a class="footer-link" href="https://statping.com/privacy">Privacy</a>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;
</mj-text>
</mj-column>
</mj-group>
</mj-section>
</mj-column>
</mj-section>
</mj-body>
</mjml>`

View File

@ -22,6 +22,10 @@ func (g *gotify) Select() *notifications.Notification {
return g.Notification
}
func (g *gotify) Valid(values notifications.Values) error {
return nil
}
var Gotify = &gotify{&notifications.Notification{
Method: "gotify",
Title: "Gotify",

View File

@ -21,9 +21,10 @@ var (
)
func TestGotifyNotifier(t *testing.T) {
t.Parallel()
err := utils.InitLogs()
require.Nil(t, err)
t.Parallel()
GOTIFY_URL = utils.Params.GetString("GOTIFY_URL")
GOTIFY_TOKEN = utils.Params.GetString("GOTIFY_TOKEN")

View File

@ -26,6 +26,10 @@ func (l *lineNotifier) Select() *notifications.Notification {
return l.Notification
}
func (l *lineNotifier) Valid(values notifications.Values) error {
return nil
}
var LineNotify = &lineNotifier{&notifications.Notification{
Method: lineNotifyMethod,
Title: "LINE Notify",

View File

@ -22,6 +22,10 @@ func (m *mobilePush) Select() *notifications.Notification {
return m.Notification
}
func (m *mobilePush) Valid(values notifications.Values) error {
return nil
}
var Mobile = &mobilePush{&notifications.Notification{
Method: "mobile",
Title: "Mobile",

Some files were not shown because too many files have changed in this diff Show More