pull/429/head
Hunter Long 2020-01-26 13:01:43 -08:00
parent fefc8a94fe
commit f491cbd019
109 changed files with 1149 additions and 3785 deletions

1
.gitignore vendored
View File

@ -19,6 +19,7 @@ public
.env
logs
tmp
source/dist
/dev/test/node_modules
dev/test/cypress/videos
dev/test/cypress/screenshots

View File

@ -22,6 +22,8 @@ frontend:
frontend-build:
cd frontend && rm -rf dist && yarn build
rm -rf source/dist && cp -r frontend/dist source/
cp -r source/tmpl/*.* source/dist/
# build and push the images to docker hub
docker: docker-build-all docker-publish-all
@ -93,10 +95,8 @@ watch:
-i="Makefile,statping,statup.db,statup.db-journal,handlers/graphql/generated.go"
# compile assets using SASS and Rice. compiles scss -> css, and run rice embed-go
compile: generate
sass source/scss/base.scss source/css/base.css
compile: frontend-build generate
cd source && rice embed-go
rm -rf .sass-cache
# benchmark testing
benchmark:

View File

@ -99,11 +99,11 @@ func (c *Checkin) CreateFailure() (int64, error) {
if len(c.Failures) > limitedFailures {
c.Failures = c.Failures[1:]
}
return fail.Id, row.Error
return fail.Id, row.Error()
}
// LimitedHits will return the last amount of successful hits from a checkin
func (c *Checkin) LimitedHits(amount int64) []*types.CheckinHit {
func (c *Checkin) LimitedHits(amount int) []*types.CheckinHit {
var hits []*types.CheckinHit
checkinHitsDB().Where("checkin = ?", c.Id).Order("id desc").Limit(amount).Find(&hits)
return hits
@ -168,7 +168,7 @@ func (c *Checkin) AllHits() []*types.CheckinHit {
}
// Hits returns all of the CheckinHits for a given Checkin
func (c *Checkin) LimitedFailures(amount int64) []types.FailureInterface {
func (c *Checkin) LimitedFailures(amount int) []types.FailureInterface {
var failures []*Failure
var failInterfaces []types.FailureInterface
col := failuresDB().Where("checkin = ?", c.Id).Where("method = 'checkin'").Limit(amount).Order("id desc")
@ -195,7 +195,7 @@ func (c *Checkin) Delete() error {
slice := service.Checkins
service.Checkins = append(slice[:i], slice[i+1:]...)
row := checkinDB().Delete(&c)
return row.Error
return row.Error()
}
// index returns a checkin index int for updating the *checkin.Service slice
@ -212,25 +212,25 @@ func (c *Checkin) index() int {
func (c *Checkin) Create() (int64, error) {
c.ApiKey = utils.RandomString(7)
row := checkinDB().Create(&c)
if row.Error != nil {
log.Warnln(row.Error)
return 0, row.Error
if row.Error() != nil {
log.Warnln(row.Error())
return 0, row.Error()
}
service := SelectService(c.ServiceId)
service.Checkins = append(service.Checkins, c)
c.Start()
go c.Routine()
return c.Id, row.Error
return c.Id, row.Error()
}
// Update will update a Checkin
func (c *Checkin) Update() (int64, error) {
row := checkinDB().Update(&c)
if row.Error != nil {
log.Warnln(row.Error)
return 0, row.Error
if row.Error() != nil {
log.Warnln(row.Error())
return 0, row.Error()
}
return c.Id, row.Error
return c.Id, row.Error()
}
// Create will create a new successful checkinHit
@ -239,11 +239,11 @@ func (c *CheckinHit) Create() (int64, error) {
c.CreatedAt = utils.Now()
}
row := checkinHitsDB().Create(&c)
if row.Error != nil {
log.Warnln(row.Error)
return 0, row.Error
if row.Error() != nil {
log.Warnln(row.Error())
return 0, row.Error()
}
return c.Id, row.Error
return c.Id, row.Error()
}
// Ago returns the duration of time between now and the last successful checkinHit

View File

@ -89,7 +89,7 @@ func InsertNotifierDB() error {
// UpdateCore will update the CoreApp variable inside of the 'core' table in database
func UpdateCore(c *Core) (*Core, error) {
db := coreDB().Update(&c)
return c, db.Error
return c, db.Error()
}
// CurrentTime will return the current local time
@ -159,12 +159,12 @@ func SelectCore() (*Core, error) {
return nil, errors.New("core database has not been setup yet.")
}
db := coreDB().First(&CoreApp)
if db.Error != nil {
return nil, db.Error
if db.Error() != nil {
return nil, db.Error()
}
CoreApp.Version = VERSION
CoreApp.UseCdn = types.NewNullBool(os.Getenv("USE_CDN") == "true")
return CoreApp, db.Error
return CoreApp, db.Error()
}
// GetLocalIP returns the non loopback local IP of the host

View File

@ -32,7 +32,7 @@ import (
var (
// DbSession stores the Statping database session
DbSession *gorm.DB
DbSession types.Database
DbModels []interface{}
)
@ -48,62 +48,62 @@ func init() {
type DbConfig types.DbConfig
// failuresDB returns the 'failures' database column
func failuresDB() *gorm.DB {
func failuresDB() types.Database {
return DbSession.Model(&types.Failure{})
}
// hitsDB returns the 'hits' database column
func hitsDB() *gorm.DB {
func hitsDB() types.Database {
return DbSession.Model(&types.Hit{})
}
// servicesDB returns the 'services' database column
func servicesDB() *gorm.DB {
func servicesDB() types.Database {
return DbSession.Model(&types.Service{})
}
// coreDB returns the single column 'core'
func coreDB() *gorm.DB {
func coreDB() types.Database {
return DbSession.Table("core").Model(&CoreApp)
}
// usersDB returns the 'users' database column
func usersDB() *gorm.DB {
func usersDB() types.Database {
return DbSession.Model(&types.User{})
}
// checkinDB returns the Checkin records for a service
func checkinDB() *gorm.DB {
func checkinDB() types.Database {
return DbSession.Model(&types.Checkin{})
}
// checkinHitsDB returns the Checkin Hits records for a service
func checkinHitsDB() *gorm.DB {
func checkinHitsDB() types.Database {
return DbSession.Model(&types.CheckinHit{})
}
// messagesDb returns the Checkin records for a service
func messagesDb() *gorm.DB {
func messagesDb() types.Database {
return DbSession.Model(&types.Message{})
}
// messagesDb returns the Checkin records for a service
func groupsDb() *gorm.DB {
func groupsDb() types.Database {
return DbSession.Model(&types.Group{})
}
// incidentsDB returns the 'incidents' database column
func incidentsDB() *gorm.DB {
func incidentsDB() types.Database {
return DbSession.Model(&types.Incident{})
}
// incidentsUpdatesDB returns the 'incidents updates' database column
func incidentsUpdatesDB() *gorm.DB {
func incidentsUpdatesDB() types.Database {
return DbSession.Model(&types.IncidentUpdate{})
}
// HitsBetween returns the gorm database query for a collection of service hits between a time range
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) *gorm.DB {
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) types.Database {
selector := Dbtimestamp(group, column)
if CoreApp.Config.DbConn == "postgres" {
return hitsDB().Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME), t2.UTC().Format(types.TIME))
@ -187,7 +187,7 @@ func (c *Core) InsertCore(db *types.DbConfig) (*Core, error) {
Config: db,
}}
query := coreDB().Create(&CoreApp)
return CoreApp, query.Error
return CoreApp, query.Error()
}
func findDbFile() string {
@ -242,7 +242,7 @@ func (c *Core) Connect(retry bool, location string) error {
conn = fmt.Sprintf("sqlserver://%v:%v@%v?database=%v", CoreApp.Config.DbUser, CoreApp.Config.DbPass, host, CoreApp.Config.DbData)
}
log.WithFields(utils.ToFields(c, conn)).Debugln("attempting to connect to database")
dbSession, err := gorm.Open(dbType, conn)
dbSession, err := types.Openw(dbType, conn)
if err != nil {
log.Debugln(fmt.Sprintf("Database connection error %v", err))
if retry {
@ -261,7 +261,7 @@ func (c *Core) Connect(retry bool, location string) error {
if dbSession.DB().Ping() == nil {
DbSession = dbSession
if utils.VerboseMode >= 4 {
DbSession.LogMode(true).Debug().SetLogger(log)
DbSession.LogMode(true).Debug().SetLogger(gorm.Logger{log})
}
log.Infoln(fmt.Sprintf("Database %v connection was successful.", dbType))
}
@ -289,8 +289,8 @@ func DatabaseMaintence() {
func DeleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, date.Format("2006-01-02"))
db := DbSession.Exec(sql)
if db.Error != nil {
log.Warnln(db.Error)
if db.Error() != nil {
log.Warnln(db.Error())
}
}
@ -346,7 +346,7 @@ func (c *Core) CreateCore() *Core {
MigrationId: time.Now().Unix(),
}
db := coreDB().Create(&newCore)
if db.Error == nil {
if db.Error() == nil {
CoreApp = &Core{Core: newCore}
}
CoreApp, err := SelectCore()
@ -370,7 +370,7 @@ func (c *Core) DropDatabase() error {
err = DbSession.DropTableIfExists("messages")
err = DbSession.DropTableIfExists("incidents")
err = DbSession.DropTableIfExists("incident_updates")
return err.Error
return err.Error()
}
// CreateDatabase will CREATE TABLES for each of the Statping elements
@ -378,12 +378,12 @@ func (c *Core) CreateDatabase() error {
var err error
log.Infoln("Creating Database Tables...")
for _, table := range DbModels {
if err := DbSession.CreateTable(table); err.Error != nil {
return err.Error
if err := DbSession.CreateTable(table); err.Error() != nil {
return err.Error()
}
}
if err := DbSession.Table("core").CreateTable(&types.Core{}); err.Error != nil {
return err.Error
if err := DbSession.Table("core").CreateTable(&types.Core{}); err.Error() != nil {
return err.Error()
}
log.Infoln("Statping Database Created")
return err
@ -401,17 +401,17 @@ func (c *Core) MigrateDatabase() error {
}
}()
if tx.Error != nil {
log.Errorln(tx.Error)
return tx.Error
log.Errorln(tx.Error())
return tx.Error()
}
for _, table := range DbModels {
tx = tx.AutoMigrate(table)
}
if err := tx.Table("core").AutoMigrate(&types.Core{}); err.Error != nil {
if err := tx.Table("core").AutoMigrate(&types.Core{}); err.Error() != nil {
tx.Rollback()
log.Errorln(fmt.Sprintf("Statping Database could not be migrated: %v", tx.Error))
return tx.Error
return tx.Error()
}
log.Infoln("Statping Database Migrated")
return tx.Commit().Error
return tx.Commit().Error()
}

View File

@ -16,33 +16,11 @@
package core
import (
"bytes"
"encoding/json"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"html/template"
)
// ExportChartsJs renders the charts for the index page
func ExportChartsJs() string {
render, err := source.JsBox.String("charts.js")
if err != nil {
log.Errorln(err)
}
t := template.New("charts")
t.Funcs(template.FuncMap{
"safe": func(html string) template.HTML {
return template.HTML(html)
},
})
t.Parse(render)
var tpl bytes.Buffer
if err := t.Execute(&tpl, CoreApp.Services); err != nil {
log.Errorln(err)
}
result := tpl.String()
return result
}
type ExportData struct {
Core *types.Core `json:"core"`

View File

@ -19,6 +19,7 @@ import (
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/types"
"net/http"
"sort"
"strings"
"time"
@ -37,48 +38,51 @@ const (
func (s *Service) CreateFailure(f *types.Failure) (int64, error) {
f.Service = s.Id
row := failuresDB().Create(f)
if row.Error != nil {
log.Errorln(row.Error)
return 0, row.Error
if row.Error() != nil {
log.Errorln(row.Error())
return 0, row.Error()
}
sort.Sort(types.FailSort(s.Failures))
//s.Failures = append(s.Failures, f)
if len(s.Failures) > limitedFailures {
s.Failures = s.Failures[1:]
}
return f.Id, row.Error
return f.Id, row.Error()
}
// AllFailures will return all failures attached to a service
func (s *Service) AllFailures() []*types.Failure {
var fails []*types.Failure
col := failuresDB().Where("service = ?", s.Id).Not("method = 'checkin'").Order("id desc")
err := col.Find(&fails)
if err.Error != nil {
func (s *Service) AllFailures() []types.Failure {
var fails []types.Failure
err := DbSession.Failures(s.Id).Find(&fails)
if err.Error() != nil {
log.Errorln(fmt.Sprintf("Issue getting failures for service %v, %v", s.Name, err))
return nil
}
return fails
}
func (s *Service) FailuresDb(r *http.Request) types.Database {
return failuresDB().Where("service = ?", s.Id).QuerySearch(r).Order("id desc")
}
// DeleteFailures will delete all failures for a service
func (s *Service) DeleteFailures() {
err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, s.Id)
if err.Error != nil {
if err.Error() != nil {
log.Errorln(fmt.Sprintf("failed to delete all failures: %v", err))
}
s.Failures = nil
}
// LimitedFailures will return the last amount of failures from a service
func (s *Service) LimitedFailures(amount int64) []*Failure {
func (s *Service) LimitedFailures(amount int) []*Failure {
var failArr []*Failure
failuresDB().Where("service = ?", s.Id).Not("method = 'checkin'").Order("id desc").Limit(amount).Find(&failArr)
return failArr
}
// LimitedFailures will return the last amount of failures from a service
func (s *Service) LimitedCheckinFailures(amount int64) []*Failure {
func (s *Service) LimitedCheckinFailures(amount int) []*Failure {
var failArr []*Failure
failuresDB().Where("service = ?", s.Id).Where("method = 'checkin'").Order("id desc").Limit(amount).Find(&failArr)
return failArr
@ -98,7 +102,7 @@ func (f *Failure) Select() *types.Failure {
// Delete will remove a Failure record from the database
func (f *Failure) Delete() error {
db := failuresDB().Delete(f)
return db.Error
return db.Error()
}
// Count24HFailures returns the amount of failures for a service within the last 24 hours
@ -116,7 +120,7 @@ func (c *Core) Count24HFailures() uint64 {
func CountFailures() uint64 {
var count uint64
err := failuresDB().Count(&count)
if err.Error != nil {
if err.Error() != nil {
log.Warnln(err.Error)
return 0
}
@ -130,7 +134,7 @@ func (s *Service) TotalFailuresOnDate(ago time.Time) (uint64, error) {
dateend := ago.UTC().Format("2006-01-02") + " 23:59:59"
rows := failuresDB().Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, date, dateend).Not("method = 'checkin'")
err := rows.Count(&count)
return count, err.Error
return count, err.Error()
}
// TotalFailures24 returns the amount of failures for a service within the last 24 hours
@ -144,7 +148,7 @@ func (s *Service) TotalFailures() (uint64, error) {
var count uint64
rows := failuresDB().Where("service = ?", s.Id)
err := rows.Count(&count)
return count, err.Error
return count, err.Error()
}
// FailuresDaysAgo returns the amount of failures since days ago
@ -159,7 +163,7 @@ func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) {
var count uint64
rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Not("method = 'checkin'")
err := rows.Count(&count)
return count, err.Error
return count, err.Error()
}
// ParseError returns a human readable error for a Failure

View File

@ -17,21 +17,21 @@ func (g *Group) Delete() error {
s.Update(false)
}
err := groupsDb().Delete(g)
return err.Error
return err.Error()
}
// Create will create a group and insert it into the database
func (g *Group) Create() (int64, error) {
g.CreatedAt = time.Now().UTC()
db := groupsDb().Create(g)
return g.Id, db.Error
return g.Id, db.Error()
}
// Update will update a group
func (g *Group) Update() (int64, error) {
g.UpdatedAt = time.Now().UTC()
db := groupsDb().Update(g)
return g.Id, db.Error
return g.Id, db.Error()
}
// Services returns all services belonging to a group

View File

@ -17,6 +17,7 @@ package core
import (
"github.com/hunterlong/statping/types"
"net/http"
"time"
)
@ -27,11 +28,11 @@ type Hit struct {
// CreateHit will create a new 'hit' record in the database for a successful/online service
func (s *Service) CreateHit(h *types.Hit) (int64, error) {
db := hitsDB().Create(&h)
if db.Error != nil {
log.Errorln(db.Error)
return 0, db.Error
if db.Error() != nil {
log.Errorln(db.Error())
return 0, db.Error()
}
return h.Id, db.Error
return h.Id, db.Error()
}
// CountHits returns a int64 for all hits for a service
@ -39,7 +40,19 @@ func (s *Service) CountHits() (int64, error) {
var hits int64
col := hitsDB().Where("service = ?", s.Id)
err := col.Count(&hits)
return hits, err.Error
return hits, err.Error()
}
// Hits returns all successful hits for a service
func (s *Service) HitsQuery(r *http.Request) ([]*types.Hit, error) {
var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id).QuerySearch(r).Order("id desc")
err := col.Find(&hits)
return hits, err.Error()
}
func (s *Service) HitsDb(r *http.Request) types.Database {
return hitsDB().Where("service = ?", s.Id).QuerySearch(r).Order("id desc")
}
// Hits returns all successful hits for a service
@ -47,15 +60,15 @@ func (s *Service) Hits() ([]*types.Hit, error) {
var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id).Order("id desc")
err := col.Find(&hits)
return hits, err.Error
return hits, err.Error()
}
// LimitedHits returns the last 1024 successful/online 'hit' records for a service
func (s *Service) LimitedHits(amount int64) ([]*types.Hit, error) {
func (s *Service) LimitedHits(amount int) ([]*types.Hit, error) {
var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id).Order("id desc").Limit(amount)
err := col.Find(&hits)
return reverseHits(hits), err.Error
return reverseHits(hits), err.Error()
}
// reverseHits will reverse the service's hit slice
@ -70,14 +83,14 @@ func reverseHits(input []*types.Hit) []*types.Hit {
func (s *Service) TotalHits() (uint64, error) {
var count uint64
col := hitsDB().Where("service = ?", s.Id).Count(&count)
return count, col.Error
return count, col.Error()
}
// TotalHitsSince returns the total amount of hits based on a specific time/date
func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
var count uint64
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Count(&count)
return count, rows.Error
return count, rows.Error()
}
// Sum returns the added value Latency for all of the services successful hits.

View File

@ -42,32 +42,32 @@ func (i *Incident) AllUpdates() []*IncidentUpdate {
// Delete will remove a incident
func (i *Incident) Delete() error {
err := incidentsDB().Delete(i)
return err.Error
return err.Error()
}
// Create will create a incident and insert it into the database
func (i *Incident) Create() (int64, error) {
i.CreatedAt = time.Now().UTC()
db := incidentsDB().Create(i)
return i.Id, db.Error
return i.Id, db.Error()
}
// Update will update a incident
func (i *Incident) Update() (int64, error) {
i.UpdatedAt = time.Now().UTC()
db := incidentsDB().Update(i)
return i.Id, db.Error
return i.Id, db.Error()
}
// Delete will remove a incident update
func (i *IncidentUpdate) Delete() error {
err := incidentsUpdatesDB().Delete(i)
return err.Error
return err.Error()
}
// Create will create a incident update and insert it into the database
func (i *IncidentUpdate) Create() (int64, error) {
i.CreatedAt = time.Now().UTC()
db := incidentsUpdatesDB().Create(i)
return i.Id, db.Error
return i.Id, db.Error()
}

View File

@ -41,14 +41,14 @@ func ReturnMessage(m *types.Message) *Message {
func SelectMessages() ([]*Message, error) {
var messages []*Message
db := messagesDb().Find(&messages).Order("id desc")
return messages, db.Error
return messages, db.Error()
}
// SelectMessage returns a Message based on the ID passed
func SelectMessage(id int64) (*Message, error) {
var message Message
db := messagesDb().Where("id = ?", id).Find(&message)
return &message, db.Error
return &message, db.Error()
}
func (m *Message) Service() *Service {
@ -62,9 +62,9 @@ func (m *Message) Service() *Service {
func (m *Message) Create() (int64, error) {
m.CreatedAt = time.Now().UTC()
db := messagesDb().Create(m)
if db.Error != nil {
log.Errorln(fmt.Sprintf("Failed to create message %v #%v: %v", m.Title, m.Id, db.Error))
return 0, db.Error
if db.Error() != nil {
log.Errorln(fmt.Sprintf("Failed to create message %v #%v: %v", m.Title, m.Id, db.Error()))
return 0, db.Error()
}
return m.Id, nil
}
@ -72,15 +72,15 @@ func (m *Message) Create() (int64, error) {
// Delete will delete a Message from database
func (m *Message) Delete() error {
db := messagesDb().Delete(m)
return db.Error
return db.Error()
}
// Update will update a Message in the database
func (m *Message) Update() (*Message, error) {
db := messagesDb().Update(m)
if db.Error != nil {
log.Errorln(fmt.Sprintf("Failed to update message %v #%v: %v", m.Title, m.Id, db.Error))
return nil, db.Error
if db.Error() != nil {
log.Errorln(fmt.Sprintf("Failed to update message %v #%v: %v", m.Title, m.Id, db.Error()))
return nil, db.Error()
}
return m, nil
}

View File

@ -21,7 +21,6 @@ import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/jinzhu/gorm"
"reflect"
"strings"
"time"
@ -31,7 +30,7 @@ var (
// AllCommunications holds all the loaded notifiers
AllCommunications []types.AllNotifiers
// db holds the Statping database connection
db *gorm.DB
db types.Database
timezone float32
log = utils.Log.WithField("type", "notifier")
)
@ -112,12 +111,12 @@ func (n *Notification) CanTest() bool {
}
// db will return the notifier database column/record
func modelDb(n *Notification) *gorm.DB {
func modelDb(n *Notification) types.Database {
return db.Model(&Notification{}).Where("method = ?", n.Method).Find(n)
}
// SetDB is called by core to inject the database for a notifier to use
func SetDB(d *gorm.DB, zone float32) {
func SetDB(d types.Database, zone float32) {
db = d
timezone = zone
}
@ -199,7 +198,7 @@ func isInDatabase(n Notifier) bool {
func SelectNotification(n Notifier) (*Notification, error) {
notifier := n.Select()
err := db.Model(&Notification{}).Where("method = ?", notifier.Method).Scan(&notifier)
return notifier, err.Error
return notifier, err.Error()
}
// Update will update the notification into the database
@ -213,7 +212,7 @@ func Update(n Notifier, notif *Notification) (*Notification, error) {
} else {
notif.close()
}
return notif, err.Error
return notif, err.Error()
}
// insertDatabase will create a new record into the database for the notifier
@ -221,10 +220,10 @@ func insertDatabase(n Notifier) (int64, error) {
noti := n.Select()
noti.Limits = 3
query := db.Create(noti)
if query.Error != nil {
return 0, query.Error
if query.Error() != nil {
return 0, query.Error()
}
return noti.Id, query.Error
return noti.Id, query.Error()
}
// SelectNotifier returns the Notification struct from the database

View File

@ -231,7 +231,7 @@ func InsertSampleHits() error {
}()
}
sg.Wait()
err := tx.Commit().Error
err := tx.Commit().Error()
if err != nil {
log.Errorln(err)
}
@ -251,7 +251,7 @@ func insertSampleCore() error {
UseCdn: types.NewNullBool(false),
}
query := coreDB().Create(core)
return query.Error
return query.Error()
}
// insertSampleUsers will create 2 admin users for a seed database
@ -457,13 +457,13 @@ func InsertLargeSampleData() error {
}
// insertFailureRecords will create failures for 15 services from seed
func insertFailureRecords(since time.Time, amount int64) {
func insertFailureRecords(since time.Time, amount int) {
for i := int64(14); i <= 15; i++ {
service := SelectService(i)
log.Infoln(fmt.Sprintf("Adding %v Failure records to service %v", amount, service.Name))
createdAt := since
for fi := int64(1); fi <= amount; fi++ {
for fi := 1; fi <= amount; fi++ {
createdAt = createdAt.Add(2 * time.Minute)
failure := &types.Failure{
@ -478,13 +478,13 @@ func insertFailureRecords(since time.Time, amount int64) {
}
// insertHitRecords will create successful Hit records for 15 services
func insertHitRecords(since time.Time, amount int64) {
func insertHitRecords(since time.Time, amount int) {
for i := int64(1); i <= 15; i++ {
service := SelectService(i)
log.Infoln(fmt.Sprintf("Adding %v hit records to service %v", amount, service.Name))
createdAt := since
p := utils.NewPerlin(2, 2, 5, time.Now().UnixNano())
for hi := int64(1); hi <= amount; hi++ {
for hi := 1; hi <= amount; hi++ {
latency := p.Noise1D(float64(hi / 10))
createdAt = createdAt.Add(1 * time.Minute)
hit := &types.Hit{

View File

@ -100,9 +100,9 @@ func (s *Service) AllCheckins() []*Checkin {
func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
var services []*Service
db := servicesDB().Find(&services).Order("order_id desc")
if db.Error != nil {
log.Errorln(fmt.Sprintf("service error: %v", db.Error))
return nil, db.Error
if db.Error() != nil {
log.Errorln(fmt.Sprintf("service error: %v", db.Error()))
return nil, db.Error()
}
CoreApp.Services = nil
for _, service := range services {
@ -123,7 +123,7 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
CoreApp.Services = append(CoreApp.Services, service)
}
reorderServices()
return services, db.Error
return services, db.Error()
}
// reorderServices will sort the services based on 'order_id'
@ -170,17 +170,6 @@ func (s *Service) OnlineSince(ago time.Time) float32 {
return s.Online24Hours
}
// DateScan struct is for creating the charts.js graph JSON array
type DateScan struct {
CreatedAt string `json:"x,omitempty"`
Value int64 `json:"y"`
}
// DateScanObj struct is for creating the charts.js graph JSON array
type DateScanObj struct {
Array []DateScan `json:"data"`
}
// lastFailure returns the last Failure a service had
func (s *Service) lastFailure() *Failure {
limited := s.LimitedFailures(1)
@ -264,37 +253,20 @@ func (s *Service) Downtime() time.Duration {
return since
}
// DateScanObj struct is for creating the charts.js graph JSON array
type DateScanObj struct {
Array []*types.DateScan `json:"data"`
}
// GraphDataRaw will return all the hits between 2 times for a Service
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
var data []DateScan
outgoing := new(DateScanObj)
model := service.(*Service).HitsBetween(start, end, group, column)
model = model.Order("timeframe asc", false).Group("timeframe")
rows, err := model.Rows()
outgoing, err := model.ToChart()
if err != nil {
log.Errorln(fmt.Errorf("issue fetching service chart data: %v", err))
log.Error(err)
}
for rows.Next() {
var gd DateScan
var createdAt string
var value float64
var createdTime time.Time
var err error
rows.Scan(&createdAt, &value)
if CoreApp.Config.DbConn == "postgres" {
createdTime, err = time.Parse(types.TIME_NANO, createdAt)
if err != nil {
log.Errorln(fmt.Errorf("issue parsing time from database: %v to %v", createdAt, types.TIME_NANO))
}
} else {
createdTime, err = time.Parse(types.TIME, createdAt)
}
gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.CHART_TIME)
gd.Value = int64(value * 1000)
data = append(data, gd)
}
outgoing.Array = data
return outgoing
return &DateScanObj{outgoing}
}
// ToString will convert the DateScanObj into a JSON string for the charts to render
@ -370,24 +342,24 @@ func updateService(s *Service) {
func (s *Service) Delete() error {
i := s.index()
err := servicesDB().Delete(s)
if err.Error != nil {
if err.Error() != nil {
log.Errorln(fmt.Sprintf("Failed to delete service %v. %v", s.Name, err.Error))
return err.Error
return err.Error()
}
s.Close()
slice := CoreApp.Services
CoreApp.Services = append(slice[:i], slice[i+1:]...)
reorderServices()
notifier.OnDeletedService(s.Service)
return err.Error
return err.Error()
}
// Update will update a service in the database, the service's checking routine can be restarted by passing true
func (s *Service) Update(restart bool) error {
err := servicesDB().Update(&s)
if err.Error != nil {
if err.Error() != nil {
log.Errorln(fmt.Sprintf("Failed to update service %v. %v", s.Name, err))
return err.Error
return err.Error()
}
// clear the notification queue for a service
if !s.AllowNotifications.Bool {
@ -405,16 +377,16 @@ func (s *Service) Update(restart bool) error {
reorderServices()
updateService(s)
notifier.OnUpdatedService(s.Service)
return err.Error
return err.Error()
}
// Create will create a service and insert it into the database
func (s *Service) Create(check bool) (int64, error) {
s.CreatedAt = time.Now().UTC()
db := servicesDB().Create(s)
if db.Error != nil {
log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, db.Error))
return 0, db.Error
if db.Error() != nil {
log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, db.Error()))
return 0, db.Error()
}
s.Start()
go s.CheckQueue(check)

View File

@ -43,7 +43,7 @@ func CountUsers() int64 {
func SelectUser(id int64) (*User, error) {
var user User
err := usersDB().Where("id = ?", id).First(&user)
return &user, err.Error
return &user, err.Error()
}
// SelectUsername returns the User based on the User's username
@ -51,19 +51,19 @@ func SelectUsername(username string) (*User, error) {
var user User
res := usersDB().Where("username = ?", username)
err := res.First(&user)
return &user, err.Error
return &user, err.Error()
}
// Delete will remove the User record from the database
func (u *User) Delete() error {
return usersDB().Delete(u).Error
return usersDB().Delete(u).Error()
}
// Update will update the User's record in database
func (u *User) Update() error {
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
return usersDB().Update(u).Error
return usersDB().Update(u).Error()
}
// Create will insert a new User into the database
@ -73,25 +73,25 @@ func (u *User) Create() (int64, error) {
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
db := usersDB().Create(u)
if db.Error != nil {
return 0, db.Error
if db.Error() != nil {
return 0, db.Error()
}
if u.Id == 0 {
log.Errorln(fmt.Sprintf("Failed to create User %v. %v", u.Username, db.Error))
return 0, db.Error
log.Errorln(fmt.Sprintf("Failed to create User %v. %v", u.Username, db.Error()))
return 0, db.Error()
}
return u.Id, db.Error
return u.Id, db.Error()
}
// SelectAllUsers returns all users
func SelectAllUsers() ([]*User, error) {
var users []*User
db := usersDB().Find(&users)
if db.Error != nil {
log.Errorln(fmt.Sprintf("Failed to load all users. %v", db.Error))
return nil, db.Error
if db.Error() != nil {
log.Errorln(fmt.Sprintf("Failed to load all users. %v", db.Error()))
return nil, db.Error()
}
return users, db.Error
return users, db.Error()
}
// AuthUser will return the User and a boolean if authentication was correct.

View File

@ -14,7 +14,7 @@ const webpackConfig = merge(commonConfig, {
path: helpers.root('dist'),
publicPath: '/',
filename: 'js/[name].bundle.js',
chunkFilename: 'js/[id].chunk.js'
chunkFilename: 'js/[name].chunk.js'
},
optimization: {
runtimeChunk: 'single',

View File

@ -16,8 +16,8 @@ const webpackConfig = merge(commonConfig, {
output: {
path: helpers.root('dist'),
publicPath: '/',
filename: 'js/[hash].js',
chunkFilename: 'js/[id].[hash].chunk.js'
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js'
},
optimization: {
runtimeChunk: 'single',
@ -57,8 +57,8 @@ const webpackConfig = merge(commonConfig, {
plugins: [
new webpack.EnvironmentPlugin(environment),
new MiniCSSExtractPlugin({
filename: 'css/[name].[hash].css',
chunkFilename: 'css/[id].[hash].css'
filename: 'css/[name].css',
chunkFilename: 'css/[name].[hash].css'
}),
new CompressionPlugin({
filename: '[path].gz[query]',

View File

@ -4,7 +4,7 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "cross-env NODE_ENV=production webpack",
"build": "rm -rf dist && cross-env NODE_ENV=production webpack",
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress",
"lint": "vue-cli-service lint"
},
@ -56,9 +56,9 @@
"friendly-errors-webpack-plugin": "~1.7",
"html-webpack-plugin": "~3.2",
"mini-css-extract-plugin": "~0.5",
"node-sass": "~4.12",
"node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "~5.0",
"sass-loader": "~7.1",
"sass-loader": "^8.0.2",
"uglifyjs-webpack-plugin": "~1.2",
"vue-loader": "~15.6",
"vue-style-loader": "~4.1",

View File

@ -1,5 +1,5 @@
<template>
<div id="app" v-if="ready">
<div id="app" v-if="loaded">
<router-view/>
<Footer version="DEV" />
</div>
@ -9,32 +9,30 @@
import Api from './components/API';
import Footer from "./components/Footer";
export default {
export default {
name: 'app',
components: {
Footer
},
computed: {
ready () {
return true
}
},
created () {
if (!this.$store.getters.hasPublicData) {
this.setAllObjects()
}
},
mounted () {
this.$store.commit('setHasPublicData', true)
},
data () {
return {
loaded: false
}
},
async created () {
await this.setAllObjects()
this.loaded = true
this.$store.commit('setHasPublicData', true)
},
methods: {
async setAllObjects () {
await this.setCore()
await this.setToken()
await this.setServices()
await this.setGroups()
await this.setMessages()
await this.setToken()
this.$store.commit('setHasPublicData', true)
this.loaded = true
},
async setCore () {
const core = await Api.core()
@ -60,5 +58,9 @@ export default {
}
</script>
<style>
<style lang="scss">
@import "./assets/css/bootstrap.min.css";
@import "./assets/scss/variables";
@import "./assets/scss/base";
@import "./assets/scss/mobile";
</style>

View File

@ -137,7 +137,7 @@ HTML, BODY {
.chart-container {
position: relative;
height: 190px;
height: 200px;
width: 100%;
overflow: hidden;
}

View File

Before

Width:  |  Height:  |  Size: 673 KiB

After

Width:  |  Height:  |  Size: 673 KiB

View File

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

Before

Width:  |  Height:  |  Size: 708 KiB

After

Width:  |  Height:  |  Size: 708 KiB

View File

@ -24,6 +24,14 @@ class Api {
return axios.get('/api/services/'+id).then(response => (response.data))
}
async service_create (data) {
return axios.post('/api/services', data).then(response => (response.data))
}
async service_update (data) {
return axios.post('/api/services/'+data.id, data).then(response => (response.data))
}
async service_hits (id, start, end, group) {
return axios.get('/api/services/'+id+'/data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
}
@ -122,6 +130,10 @@ class Api {
}
}
async allActions (...all) {
await axios.all([all])
}
}
const api = new Api()
export default api

View File

@ -16,7 +16,7 @@
</div>
</div>
<div v-for="(service, index) in $store.getters.servicesInOrder()" v-bind:key="index">
<div v-for="(service, index) in services" v-bind:key="index">
<ServiceInfo :service=service />
</div>
</div>
@ -30,7 +30,12 @@
components: {
ServiceInfo
},
methods: {
data () {
return {
services: this.$store.getters.servicesInOrder()
}
},
methods: {
}
}

View File

@ -1,4 +1,4 @@
<template>
<template v-if="service">
<div class="col-12 card mb-3" style="min-height: 260px">
<div class="card-body">
<h5 class="card-title"><a href="service/7">{{service.name}}</a>
@ -6,16 +6,16 @@
</h5>
<div class="row">
<div class="col-md-3 col-sm-6">
<div id="spark_service_7_1"></div>
<ServiceSparkLine title="here" subtitle="Failures in 7 Days" :series="first"/>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_7_2"></div>
<ServiceSparkLine title="here" subtitle="Failures Last Month" :series="second"/>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_7_3"></div>
<ServiceSparkLine title="here" subtitle="Average Response" :series="third"/>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_7_4"></div>
<ServiceSparkLine title="here" subtitle="Ping Time" :series="fourth"/>
</div>
</div>
</div>
@ -23,12 +23,44 @@
</template>
<script>
export default {
import ServiceSparkLine from "./ServiceSparkLine";
import Api from "../API";
export default {
name: 'ServiceInfo',
components: {
ServiceSparkLine
},
props: {
service: {
type: Object,
required: true
type: Object,
required: true
}
},
data() {
return {
first: [],
second: [],
third: [],
fourth: []
}
},
async created() {
this.first = await this.getFailures(7, "hour")
this.second = await this.getFailures(30, "hour")
this.third = await this.getHits(7, "hour")
this.fourth = await this.getHits(30, "hour")
},
methods: {
async getHits(days, group) {
const start = this.ago(3600 * 24)
const data = await Api.service_hits(this.service.id, start, this.now(), group)
return [data]
},
async getFailures(days, group) {
const start = this.ago(3600 * 24)
const data = await Api.service_failures(this.service.id, start, this.now())
return [data]
}
}
}

View File

@ -0,0 +1,67 @@
<template v-if="series.length">
<apexchart width="100%" height="180" type="area" :options="chartOpts" :series="series"></apexchart>
</template>
<script>
export default {
name: 'ServiceSparkLine',
props: {
series: {
type: Array,
default: []
},
title: {
type: String,
},
subtitle: {
type: String,
}
},
data() {
return {
chartOpts: {
chart: {
type: 'area',
height: 180,
sparkline: {
enabled: true
},
},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.3,
},
yaxis: {
min: 0
},
colors: ['#DCE6EC'],
tooltip: {
enabled: false
},
title: {
text: "title",
offsetX: 0,
style: {
fontSize: '28px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: "subtitle",
offsetX: 0,
style: {
fontSize: '14px',
cssClass: 'apexcharts-yaxis-title'
}
}
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -42,7 +42,8 @@
},
methods: {
async chartHits() {
this.data = await Api.service_hits(this.service.id, 0, 99999999999, "hour")
const start = this.ago(3600 * 24)
this.data = await Api.service_hits(this.service.id, start, this.now(), "hour")
this.series = [{
name: this.service.name,
...this.data
@ -126,7 +127,6 @@
}
},
series: [{
name: this.service.name,
data: []
}]
}

View File

@ -1,57 +0,0 @@
<template>
<form @submit="login">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" v-model="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">Password</label>
<div class="col-sm-10">
<input type="password" v-model="password" name="password" class="form-control" id="password" placeholder="Password">
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click="login" type="submit" class="btn btn-primary btn-block mb-3">Sign in</button>
</div>
</div>
</form>
</template>
<script>
import Api from "../components/API";
export default {
name: 'FormLogin',
props: {
},
data () {
return {
username: "",
password: "",
auth: null
}
},
mounted() {
},
methods: {
async login (e) {
e.preventDefault();
const auth = await Api.login(this.username, this.password)
if (auth.token !== null) {
this.auth = Api.saveToken(this.username, auth.token)
this.$store.commit('setToken', auth)
this.$router.push('/dashboard')
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -63,7 +63,7 @@
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">HTTP Headers</label>
<div class="col-sm-8">
<input v-model="service.headers" class="form-control" autocapitalize="none" spellcheck="false" placeholder='Authorization=1010101,Content-Type=application/json' value="">
<input v-model="service.headers" class="form-control" autocapitalize="none" spellcheck="false" placeholder='Authorization=1010101,Content-Type=application/json'>
<small class="form-text text-muted">Comma delimited list of HTTP Headers (KEY=VALUE,KEY=VALUE)</small>
</div>
</div>
@ -93,28 +93,28 @@
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input v-model="service.check_interval" type="number" class="form-control" value="60" min="1" id="service_interval" required>
<input v-model="service.check_interval" type="number" class="form-control" min="1" id="service_interval" required>
<small id="interval" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8">
<input v-model="service.timeout" type="number" name="timeout" class="form-control" value="15" placeholder="15" min="1">
<input v-model="service.timeout" type="number" name="timeout" class="form-control" placeholder="15" min="1">
<small class="form-text text-muted">If the endpoint does not respond within this time it will be considered to be offline</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Permalink URL</label>
<div class="col-sm-8">
<input v-model="service.permalink" type="text" name="permalink" class="form-control" value="" id="permalink" autocapitalize="none" spellcheck="true" placeholder='awesome_service'>
<input v-model="service.permalink" type="text" name="permalink" class="form-control" id="permalink" autocapitalize="none" spellcheck="true" placeholder='awesome_service'>
<small class="form-text text-muted">Use text for the service URL rather than the service number.</small>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">List Order</label>
<div class="col-sm-8">
<input v-model="service.order" type="number" name="order" class="form-control" min="0" value="0" id="order">
<input v-model="service.order" type="number" name="order" class="form-control" min="0" id="order">
<small class="form-text text-muted">You can also drag and drop services to reorder on the Services tab.</small>
</div>
</div>
@ -122,9 +122,8 @@
<label for="order" class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" checked>
<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">Verify SSL Certificate for this service</label>
<input type="hidden" name="verify_ssl" id="switch-verify-ssl-value" value="true">
</span>
</div>
</div>
@ -132,9 +131,8 @@
<label for="order" class="col-sm-4 col-form-label">Notifications</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" checked>
<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>
<input type="hidden" name="allow_notifications" id="switch-notifications-value" value="true">
</span>
</div>
</div>
@ -142,9 +140,8 @@
<label for="order" class="col-sm-4 col-form-label">Visible</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" checked>
<input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public">
<label for="switch-public">Show service details to the public</label>
<input type="hidden" name="public" id="switch-public-value" value="true">
</span>
</div>
</div>
@ -196,8 +193,8 @@
}
},
async created() {
if (this.in_service) {
this.service = this.in_service
if (this.props.in_service) {
this.service = this.props.in_service
}
if (!this.$store.getters.groups) {
const groups = await Api.groups()
@ -205,8 +202,16 @@
}
},
methods: {
saveService(e) {
async saveService(e) {
e.preventDefault()
let s = this.service
delete s.failures
delete s.created_at
delete s.updated_at
delete s.last_success
delete s.latency
delete s.online_24_hours
await Api.service_save()
}
}
}

View File

@ -3,111 +3,19 @@ import VueRouter from 'vue-router'
import VueApexCharts from 'vue-apexcharts'
import App from '@/App.vue'
import Api from './components/API'
import store from './store'
import {library} from '@fortawesome/fontawesome-svg-core'
import {fas} from '@fortawesome/fontawesome-free-solid';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import DashboardIndex from "./components/Dashboard/DashboardIndex";
import DashboardUsers from "./components/Dashboard/DashboardUsers";
import DashboardServices from "./components/Dashboard/DashboardServices";
import DashboardMessages from "./components/Dashboard/DashboardMessages";
import Settings from "./pages/Settings";
import EditService from "./components/Dashboard/EditService";
import Dashboard from "./pages/Dashboard";
import Index from "./pages/Index";
import Login from "./pages/Login";
import Service from "./pages/Service";
import router from './routes'
import "./mixin"
library.add(fas)
Vue.component('apexchart', VueApexCharts)
Vue.component('font-awesome-icon', FontAwesomeIcon)
require("@/assets/css/bootstrap.min.css")
require("@/assets/css/base.css")
// require("./assets/js/bootstrap.min")
// require("./assets/js/flatpickr")
// require("./assets/js/inputTags.min")
// require("./assets/js/rangePlugin")
// require("./assets/js/sortable.min")
const routes = [
{
path: '/',
name: 'Index',
component: Index
},
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
meta: {
requiresAuth: true
},
children: [{
path: '',
component: DashboardIndex
},{
path: 'users',
component: DashboardUsers
},{
path: 'services',
component: DashboardServices
},{
path: 'create_service',
component: EditService
},{
path: 'edit_service/:id',
component: EditService
},{
path: 'messages',
component: DashboardMessages
},{
path: 'settings',
component: Settings
},{
path: 'logs',
component: DashboardUsers
},{
path: 'help',
component: DashboardUsers
}]
},
{
path: '/login',
name: 'Login',
component: Login
},
{ path: '/logout', redirect: '/' },
{
path: '/service/:id',
name: 'Service',
component: Service,
props: true
}
];
const router = new VueRouter({
mode: 'history',
routes
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (Api.token()) {
next()
return
}
next('/login')
} else {
next()
}
})
Vue.use(VueRouter);
Vue.use(require('vue-moment'));

14
frontend/src/mixin.js Normal file
View File

@ -0,0 +1,14 @@
import Vue from "vue";
export default Vue.mixin({
methods: {
now() {
return Math.round(new Date().getTime() / 1000)
},
ago(seconds) {
return this.now() - seconds
},
hour(){ return 3600 },
day() { return 3600 * 24 }
}
});

View File

@ -8,6 +8,7 @@
<script>
import Login from "./Login";
import TopNav from "../components/Dashboard/TopNav";
import Api from "../components/API";
export default {
name: 'Dashboard',
@ -17,17 +18,19 @@
},
data () {
return {
view: "DashboardIndex",
authenticated: false
}
},
mounted() {
mounted() {
if (this.$store.getters.token !== null) {
this.authenticated = true
}
},
methods: {
async setServices () {
const services = await Api.services()
this.$store.commit('setServices', services)
},
}
}
</script>

View File

@ -2,28 +2,69 @@
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-10 offset-1 col-md-8 offset-md-2 mt-md-2">
<div class="col-12 col-md-8 offset-md-2 mb-4">
<img class="col-12 mt-5 mt-md-0" src="/public/img/banner.png">
<img class="col-12 mt-5 mt-md-0" src="/public/banner.png">
</div>
<FormLogin/>
<form @submit="login">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" v-model="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">Password</label>
<div class="col-sm-10">
<input type="password" v-model="password" name="password" class="form-control" id="password" placeholder="Password">
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click="login" type="submit" class="btn btn-block mb-3" :class="{'btn-primary': !loading, 'btn-default': loading}" v-bind:disabled="loading">
{{loading ? "Loading" : "Sign in"}}
</button>
<div v-if="error" class="alert alert-danger" role="alert">
Incorrect username or password
</div>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
import FormLogin from "../forms/Login";
import Api from "../components/API";
export default {
export default {
name: 'Login',
components: {
FormLogin
},
data () {
return {
}
return {
username: "",
password: "",
auth: {},
loading: false,
error: false
}
},
methods: {
async login (e) {
e.preventDefault();
this.loading = true
this.error = false
const auth = await Api.login(this.username, this.password)
if (auth.error) {
this.error = true
} else if (auth.token) {
this.auth = Api.saveToken(this.username, auth.token)
this.$store.commit('setToken', auth)
this.$router.push('/dashboard')
}
this.loading = false
}
}
}
</script>

View File

@ -0,0 +1,91 @@
import Index from "./pages/Index";
import Dashboard from "./pages/Dashboard";
import DashboardIndex from "./components/Dashboard/DashboardIndex";
import DashboardUsers from "./components/Dashboard/DashboardUsers";
import DashboardServices from "./components/Dashboard/DashboardServices";
import EditService from "./components/Dashboard/EditService";
import DashboardMessages from "./components/Dashboard/DashboardMessages";
import Settings from "./pages/Settings";
import Login from "./pages/Login";
import Service from "./pages/Service";
import VueRouter from "vue-router";
import Api from "./components/API";
const routes = [
{
path: '/',
name: 'Index',
component: Index
},
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
meta: {
requiresAuth: true
},
children: [{
path: '',
component: DashboardIndex,
},{
path: 'users',
component: DashboardUsers
},{
path: 'services',
component: DashboardServices
},{
path: 'create_service',
component: EditService
},{
path: 'edit_service/:id',
component: EditService
},{
path: 'messages',
component: DashboardMessages
},{
path: 'settings',
component: Settings
},{
path: 'logs',
component: DashboardUsers
},{
path: 'help',
component: DashboardUsers
}]
},
{
path: '/login',
name: 'Login',
component: Login
},
{ path: '/logout', redirect: '/' },
{
path: '/service/:id',
name: 'Service',
component: Service,
props: true
}
];
const router = new VueRouter({
mode: 'history',
routes
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
const tk = Api.token()
if (tk === null) {
next()
return
}
if (to.path !== '/login') {
next('/login')
return
}
} else {
next()
}
})
export default router

View File

@ -47,7 +47,7 @@ export default new Vuex.Store({
return state.services.filter(s => s.group_id === id)
},
servicesInOrder: (state) => () => {
return state.services.sort((a, b) => a.order_id - b.order_id)
return state.services
},
onlineServices: (state) => (online) => {
return state.services.filter(s => s.online === online)
@ -79,7 +79,7 @@ export default new Vuex.Store({
state.token = token
},
setServices(state, services) {
state.services = services
state.services = services.sort((a, b) => a.order_id - b.order_id)
},
setGroups(state, groups) {
state.groups = groups

View File

@ -1,5 +1,6 @@
module.exports = {
assetsDir: 'assets',
filenameHashing: false,
devServer: {
proxy: {
'/api': {

View File

@ -2060,15 +2060,14 @@ cliui@^4.0.0:
strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"
clone-deep@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==
clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
dependencies:
for-own "^1.0.0"
is-plain-object "^2.0.4"
kind-of "^6.0.0"
shallow-clone "^1.0.0"
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clone@^1.0.2:
version "1.0.4"
@ -3579,23 +3578,11 @@ follow-redirects@^1.0.0:
dependencies:
debug "^3.0.0"
for-in@^0.1.3:
version "0.1.8"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=
for-in@^1.0.1, for-in@^1.0.2:
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
for-own@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b"
integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=
dependencies:
for-in "^1.0.1"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@ -4798,7 +4785,7 @@ loader-utils@^0.2.16:
json5 "^0.5.0"
object-assign "^4.0.1"
loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
@ -4840,11 +4827,6 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
lodash.tail@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=
lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
@ -5165,14 +5147,6 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mixin-object@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=
dependencies:
for-in "^0.1.3"
is-extendable "^0.1.1"
mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@ -5347,10 +5321,10 @@ node-releases@^1.1.44:
dependencies:
semver "^6.3.0"
node-sass@~4.12:
version "4.12.0"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017"
integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==
node-sass@^4.13.1:
version "4.13.1"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3"
integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==
dependencies:
async-foreach "^0.1.3"
chalk "^1.1.1"
@ -5359,7 +5333,7 @@ node-sass@~4.12:
get-stdin "^4.0.1"
glob "^7.0.3"
in-publish "^2.0.0"
lodash "^4.17.11"
lodash "^4.17.15"
meow "^3.7.0"
mkdirp "^0.5.1"
nan "^2.13.2"
@ -6806,17 +6780,16 @@ sass-graph@^2.2.4:
scss-tokenizer "^0.2.3"
yargs "^7.0.0"
sass-loader@~7.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d"
integrity sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==
sass-loader@^8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d"
integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==
dependencies:
clone-deep "^2.0.1"
loader-utils "^1.0.1"
lodash.tail "^4.1.1"
neo-async "^2.5.0"
pify "^3.0.0"
semver "^5.5.0"
clone-deep "^4.0.1"
loader-utils "^1.2.3"
neo-async "^2.6.1"
schema-utils "^2.6.1"
semver "^6.3.0"
sax@~1.2.4:
version "1.2.4"
@ -6840,7 +6813,7 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
schema-utils@^2.0.0, schema-utils@^2.5.0:
schema-utils@^2.0.0, schema-utils@^2.5.0, schema-utils@^2.6.1:
version "2.6.4"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.4.tgz#a27efbf6e4e78689d91872ee3ccfa57d7bdd0f53"
integrity sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==
@ -6978,14 +6951,12 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallow-clone@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571"
integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
dependencies:
is-extendable "^0.1.1"
kind-of "^5.0.0"
mixin-object "^2.0.1"
kind-of "^6.0.2"
shebang-command@^1.2.0:
version "1.2.0"

View File

@ -31,7 +31,6 @@ import (
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
)
@ -41,12 +40,11 @@ const (
)
var (
jwtKey string
httpServer *http.Server
usingSSL bool
mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
templates = []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_integration.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
javascripts = []string{"charts.js", "chart_index.js"}
jwtKey string
httpServer *http.Server
usingSSL bool
mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
templates = []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_integration.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
)
// RunHTTPServer will start a HTTP server on a specific IP and port
@ -152,7 +150,6 @@ func IsFullAuthenticated(r *http.Request) bool {
func getJwtToken(r *http.Request) (JwtClaim, error) {
c, err := r.Cookie(cookieKey)
if err != nil {
utils.Log.Errorln(err)
if err == http.ErrNoCookie {
return JwtClaim{}, err
}
@ -164,14 +161,12 @@ func getJwtToken(r *http.Request) (JwtClaim, error) {
return []byte(jwtKey), nil
})
if err != nil {
utils.Log.Errorln("error getting jwt token: ", err)
if err == jwt.ErrSignatureInvalid {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
if !tkn.Valid {
utils.Log.Errorln("token is not valid")
return claims, errors.New("token is not valid")
}
return claims, err
@ -237,15 +232,6 @@ func loadTemplate(w http.ResponseWriter, r *http.Request) (*template.Template, e
return nil, err
}
}
// render all javascript files
for _, temp := range javascripts {
tmp, _ := source.JsBox.String(temp)
mainTemplate, err = mainTemplate.Parse(tmp)
if err != nil {
log.Errorln(err)
return nil, err
}
}
return mainTemplate, err
}
@ -278,28 +264,28 @@ func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data i
// executeJSResponse will render a Javascript response
func executeJSResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}) {
render, err := source.JsBox.String(file)
if err != nil {
log.Errorln(err)
}
if usingSSL {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}
t := template.New("charts")
t.Funcs(template.FuncMap{
"safe": func(html string) template.HTML {
return template.HTML(html)
},
"Services": func() []types.ServiceInterface {
return core.CoreApp.Services
},
})
if _, err := t.Parse(render); err != nil {
log.Errorln(err)
}
if err := t.Execute(w, data); err != nil {
log.Errorln(err)
}
//render, err := source.JsBox.String(file)
//if err != nil {
// log.Errorln(err)
//}
//if usingSSL {
// w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
//}
//t := template.New("charts")
//t.Funcs(template.FuncMap{
// "safe": func(html string) template.HTML {
// return template.HTML(html)
// },
// "Services": func() []types.ServiceInterface {
// return core.CoreApp.Services
// },
//})
//if _, err := t.Parse(render); err != nil {
// log.Errorln(err)
//}
//if err := t.Execute(w, data); err != nil {
// log.Errorln(err)
//}
}
func returnJson(d interface{}, w http.ResponseWriter, r *http.Request) {
@ -313,5 +299,5 @@ func error404Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}
w.WriteHeader(http.StatusNotFound)
ExecuteResponse(w, r, "error_404.gohtml", nil, nil)
ExecuteResponse(w, r, "index.html", nil, nil)
}

View File

@ -25,7 +25,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return
}
ExecuteResponse(w, r, "index.gohtml", core.CoreApp, nil)
ExecuteResponse(w, r, "index.html", core.CoreApp, nil)
}
func healthCheckHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -1,13 +1,16 @@
package handlers
import (
"compress/gzip"
"crypto/subtle"
"encoding/json"
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/utils"
"io"
"net/http"
"net/http/httptest"
"strings"
"time"
)
@ -16,6 +19,30 @@ var (
authPass string
)
// Gzip Compression
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func Gzip(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
handler.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
handler.ServeHTTP(gzw, r)
})
}
// basicAuthHandler is a middleware to implement HTTP basic authentication using
// AUTH_USERNAME and AUTH_PASSWORD environment variables
func basicAuthHandler(next http.Handler) http.Handler {

View File

@ -17,10 +17,8 @@ package handlers
import (
"fmt"
"github.com/99designs/gqlgen/handler"
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/handlers/graphql"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"net/http"
@ -56,65 +54,26 @@ func Router() *mux.Router {
r.Use(sendLog)
if source.UsingAssets(dir) {
indexHandler := http.FileServer(http.Dir(dir + "/assets/"))
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath+"css/", http.FileServer(http.Dir(dir+"/assets/css"))))
r.PathPrefix("/css/").Handler(Gzip(http.StripPrefix(basePath+"css/", http.FileServer(http.Dir(dir+"/assets/css")))))
r.PathPrefix("/font/").Handler(http.StripPrefix(basePath+"font/", http.FileServer(http.Dir(dir+"/assets/font"))))
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath+"js/", http.FileServer(http.Dir(dir+"/assets/js"))))
r.PathPrefix("/js/").Handler(Gzip(http.StripPrefix(basePath+"js/", http.FileServer(http.Dir(dir+"/assets/js")))))
r.PathPrefix("/robots.txt").Handler(http.StripPrefix(basePath, indexHandler))
r.PathPrefix("/favicon.ico").Handler(http.StripPrefix(basePath, indexHandler))
r.PathPrefix("/banner.png").Handler(http.StripPrefix(basePath, indexHandler))
} else {
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath+"css/", http.FileServer(source.CssBox.HTTPBox())))
r.PathPrefix("/font/").Handler(http.StripPrefix(basePath+"font/", http.FileServer(source.FontBox.HTTPBox())))
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath+"js/", http.FileServer(source.JsBox.HTTPBox())))
//r.PathPrefix("/").Handler(http.StripPrefix(basePath+"/", http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/css/").Handler(Gzip(http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/font/").Handler(http.FileServer(source.TmplBox.HTTPBox()))
r.PathPrefix("/js/").Handler(Gzip(http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/robots.txt").Handler(http.StripPrefix(basePath, http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/favicon.ico").Handler(http.StripPrefix(basePath, http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/banner.png").Handler(http.StripPrefix(basePath, http.FileServer(source.TmplBox.HTTPBox())))
}
r.Handle("/charts.js", http.HandlerFunc(renderServiceChartsHandler))
r.Handle("/setup", http.HandlerFunc(setupHandler)).Methods("GET")
r.Handle("/setup", http.HandlerFunc(processSetupHandler)).Methods("POST")
r.Handle("/dashboard", http.HandlerFunc(dashboardHandler)).Methods("GET")
r.Handle("/dashboard", http.HandlerFunc(loginHandler)).Methods("POST")
r.Handle("/logout", http.HandlerFunc(logoutHandler))
r.Handle("/plugins/download/{name}", authenticated(pluginsDownloadHandler, true))
r.Handle("/plugins/{name}/save", authenticated(pluginSavedHandler, true)).Methods("POST")
r.Handle("/help", authenticated(helpHandler, true))
r.Handle("/logs", authenticated(logsHandler, true))
r.Handle("/logs/line", readOnly(logsLineHandler, true))
// GRAPHQL Route
r.Handle("/graphql", authenticated(handler.GraphQL(graphql.NewExecutableSchema(graphql.Config{Resolvers: &graphql.Resolver{}})), true))
// USER Routes
r.Handle("/users", readOnly(usersHandler, true)).Methods("GET")
r.Handle("/user/{id}", authenticated(usersEditHandler, true)).Methods("GET")
// MESSAGES Routes
r.Handle("/messages", authenticated(messagesHandler, true)).Methods("GET")
r.Handle("/message/{id}", authenticated(viewMessageHandler, true)).Methods("GET")
// SETTINGS Routes
r.Handle("/settings", authenticated(settingsHandler, true)).Methods("GET")
r.Handle("/settings", authenticated(saveSettingsHandler, true)).Methods("POST")
r.Handle("/settings/css", authenticated(saveSASSHandler, true)).Methods("POST")
r.Handle("/settings/build", authenticated(saveAssetsHandler, true)).Methods("GET")
r.Handle("/settings/delete_assets", authenticated(deleteAssetsHandler, true)).Methods("GET")
r.Handle("/settings/export", authenticated(exportHandler, true)).Methods("GET")
r.Handle("/settings/bulk_import", authenticated(bulkImportHandler, true)).Methods("POST")
r.Handle("/settings/integrator/{name}", authenticated(integratorHandler, true)).Methods("POST")
// SERVICE Routes
r.Handle("/services", authenticated(servicesHandler, true)).Methods("GET")
r.Handle("/service/create", authenticated(createServiceHandler, true)).Methods("GET")
r.Handle("/service/{id}", readOnly(servicesViewHandler, true)).Methods("GET")
r.Handle("/service/{id}/edit", authenticated(servicesViewHandler, true)).Methods("GET")
r.Handle("/service/{id}/delete_failures", authenticated(servicesDeleteFailuresHandler, true)).Methods("GET")
r.Handle("/group/{id}", http.HandlerFunc(groupViewHandler)).Methods("GET")
// API Routes
r.Handle("/api", scoped(apiIndexHandler))
r.Handle("/api/login", http.HandlerFunc(apiLoginHandler)).Methods("POST")
r.Handle("/api/setup", http.HandlerFunc(processSetupHandler)).Methods("POST")
r.Handle("/api/logout", http.HandlerFunc(logoutHandler))
r.Handle("/api/renew", authenticated(apiRenewHandler, false))
r.Handle("/api/clear_cache", authenticated(apiClearCacheHandler, false))
@ -177,6 +136,8 @@ func Router() *mux.Router {
r.Handle("/api/checkin/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE")
r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler))
//r.PathPrefix("/").Handler(http.HandlerFunc(indexHandler))
// Static Files Routes
r.PathPrefix("/files/postman.json").Handler(http.StripPrefix("/files/", http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/files/swagger.json").Handler(http.StripPrefix("/files/", http.FileServer(source.TmplBox.HTTPBox())))

View File

@ -322,7 +322,11 @@ func apiServiceFailuresHandler(r *http.Request) interface{} {
if servicer == nil {
return errors.New("service not found")
}
return servicer.AllFailures()
fails, err := servicer.FailuresDb(r).Fails()
if err != nil {
return err
}
return fails
}
func apiServiceHitsHandler(r *http.Request) interface{} {
@ -331,8 +335,7 @@ func apiServiceHitsHandler(r *http.Request) interface{} {
if servicer == nil {
return errors.New("service not found")
}
hits, err := servicer.Hits()
hits, err := servicer.HitsDb(r).Hits()
if err != nil {
return err
}

View File

@ -97,7 +97,7 @@ func saveAssetsHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := source.CompileSASS(dir); err != nil {
source.CopyToPublic(source.CssBox, dir+"/assets/css", "base.css")
source.CopyToPublic(source.TmplBox, dir+"/assets/css", "base.css")
log.Errorln("Default 'base.css' was inserted because SASS did not work.")
}
resetRouter()

File diff suppressed because one or more lines are too long

View File

@ -1,137 +0,0 @@
{{define "chartIndex"}}
var ctx_{{js .Id}} = document.getElementById("service_{{js .Id}}").getContext('2d');
var chartdata_{{js .Id}} = new Chart(ctx_{{js .Id}}, {
type: 'line',
data: {
datasets: [{
label: 'Response Time (Milliseconds)',
data: [],
backgroundColor: ['{{if .Online}}rgba(47, 206, 30, 0.92){{else}}rgb(221, 53, 69){{end}}'],
borderColor: ['{{if .Online}}rgb(47, 171, 34){{else}}rgb(183, 32, 47){{end}}'],
borderWidth: 1
}]
},
options: {
maintainAspectRatio: !1,
scaleShowValues: !0,
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: -10
}
},
hover: {
animationDuration: 0,
},
responsiveAnimationDuration: 0,
animation: {
duration: 3500,
onComplete: function() {
var chartInstance = this.chart,
ctx = chartInstance.ctx;
var controller = this.chart.controller;
var xAxis = controller.scales['x-axis-0'];
var yAxis = controller.scales['y-axis-0'];
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
var numTicks = xAxis.ticks.length;
var yOffsetStart = xAxis.width / numTicks;
var halfBarWidth = (xAxis.width / (numTicks * 2));
xAxis.ticks.forEach(function(value, index) {
var xOffset = 20;
var yOffset = (yOffsetStart * index) + halfBarWidth;
ctx.fillStyle = '#e2e2e2';
ctx.fillText(value, yOffset, xOffset)
});
this.data.datasets.forEach(function(dataset, i) {
var meta = chartInstance.controller.getDatasetMeta(i);
var hxH = 0;
var hyH = 0;
var hxL = 0;
var hyL = 0;
var highestNum = 0;
var lowestnum = 999999999999;
meta.data.forEach(function(bar, index) {
var data = dataset.data[index];
if (lowestnum > data.y) {
lowestnum = data.y;
hxL = bar._model.x;
hyL = bar._model.y
}
if (data.y > highestNum) {
highestNum = data.y;
hxH = bar._model.x;
hyH = bar._model.y
}
});
if (hxH >= 820) {
hxH = 820
} else if (50 >= hxH) {
hxH = 50
}
if (hxL >= 820) {
hxL = 820
} else if (70 >= hxL) {
hxL = 70
}
ctx.fillStyle = '#ffa7a2';
ctx.fillText(highestNum + "ms", hxH - 40, hyH + 15);
ctx.fillStyle = '#45d642';
ctx.fillText(lowestnum + "ms", hxL, hyL + 10);
})
}
},
legend: {
display: !1
},
tooltips: {
enabled: !1
},
scales: {
yAxes: [{
display: !1,
ticks: {
fontSize: 20,
display: !1,
beginAtZero: !1
},
gridLines: {
display: !1
}
}],
xAxes: [{
type: 'time',
distribution: 'series',
autoSkip: !1,
time: {
displayFormats: {
'hour': 'MMM DD hA'
},
source: 'auto'
},
gridLines: {
display: !1
},
ticks: {
source: 'auto',
stepSize: 1,
min: 0,
fontColor: "white",
fontSize: 20,
display: !1
}
}]
},
elements: {
point: {
radius: 0
}
}
}
});
AjaxChart(chartdata_{{js .Id}},{{js .Id}},0,99999999999,"hour");
{{end}}

View File

@ -1,141 +0,0 @@
{{define "charts"}}
{{$start := .Start}}
{{$end := .End}}
const axisOptions = {
labels: {
show: false
},
crosshairs: {
show: false
},
lines: {
show: false
},
tooltip: {
enabled: false
},
axisTicks: {
show: false
},
grid: {
show: false
},
marker: {
show: false
}
};
const annotationColor = {
strokeDashArray: 0,
borderColor: "#d0222d",
label: {
show: false,
}
};
let annotation = {
annotations: {
xaxis: [
{
// in a datetime series, the x value should be a timestamp, just like it is generated below
x: new Date("01/29/2019").getTime(),
...annotationColor
}]
}
};
let options = {
chart: {
height: 210,
width: "100%",
type: "area",
animations: {
enabled: false,
initialAnimation: {
enabled: false
}
},
selection: {
enabled: false
},
zoom: {
enabled: false
},
toolbar: {
show: false
}
},
grid: {
show: false,
padding: {
top: 0,
right: 0,
bottom: 0,
left: -10,
},
},
tooltip: {
enabled: false,
marker: {
show: false,
},
x: {
show: false,
}
},
legend: {
show: false,
},
dataLabels: {
enabled: false
},
floating: true,
axisTicks: {
show: false
},
axisBorder: {
show: false
},
fill: {
colors: ["#48d338"],
opacity: 1,
type: 'solid'
},
stroke: {
show: true,
curve: 'smooth',
lineCap: 'butt',
colors: ["#3aa82d"],
},
series: [
{
name: "Response Time",
data: [],
}
],
xaxis: {
type: "datetime",
...axisOptions
},
yaxis: {
...axisOptions
},
};
const startOn = UTCTime() - (86400 * 14);
async function RenderCharts() {
{{ range .Services }}
options.fill.colors = {{if .Online}}["#48d338"]{{else}}["#dd3545"]{{end}};
options.stroke.colors = {{if .Online}}["#3aa82d"]{{else}}["#c23342"]{{end}};
let chart{{.Id}} = new ApexCharts(document.querySelector("#service_{{js .Id}}"), options);
await RenderChart(chart{{js .Id}}, {{js .Id}}, startOn);{{end}}
}
$( document ).ready(function() {
RenderCharts()
});
{{end}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,492 +0,0 @@
/*
* Statping
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statping
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
$('.service_li').on('click', function() {
var id = $(this).attr('data-id');
var position = $('#service_id_' + id).offset();
window.scroll(0, position.top - 23);
return false;
});
$('.test_notifier').on('click', function(e) {
var btn = $(this);
var form = $(this).parents('form:first');
var values = form.serialize();
var notifier = form.find('input[name=method]').val();
var success = $('#'+notifier+'-success');
var error = $('#'+notifier+'-error');
Spinner(btn);
$.ajax({
url: form.attr("action")+"/test",
type: 'POST',
data: values,
success: function(data) {
if (data === 'ok') {
success.removeClass('d-none');
setTimeout(function() {
success.addClass('d-none');
}, 5000)
} else {
error.removeClass('d-none');
error.html(data);
setTimeout(function() {
error.addClass('d-none');
}, 8000)
}
Spinner(btn, true);
}
});
e.preventDefault();
});
$('.spin_form').on('submit', function() {
Spinner($(this).find('button[type=submit]'));
});
function Spinner(btn, off = false) {
btn.prop('disabled', !off);
if (off) {
let pastVal = btn.attr("data-past");
btn.text(pastVal);
btn.removeAttr("data-past");
} else {
let pastVal = btn.text();
btn.attr("data-past", pastVal);
btn.html('<i class="fa fa-spinner fa-spin"></i>');
}
}
function SaveNotifier(data) {
let button = data.element.find('button[type=submit]');
button.text('Saved!')
button.removeClass('btn-primary')
button.addClass('btn-success')
}
$('.scrollclick').on('click',function(e) {
let element = $(this).attr("data-id");
$('html, body').animate({
scrollTop: $("#"+element).offset().top - 15
}, 500);
e.preventDefault();
});
$('.toggle-service').on('click',function(e) {
let obj = $(this);
let serviceId = obj.attr("data-id");
let online = obj.attr("data-online");
let d = confirm("Do you want to "+(eval(online) ? "stop" : "start")+" checking this service?");
if (d) {
$.ajax({
url: "api/services/" + serviceId + "/running",
type: 'POST',
success: function (data) {
if (online === "true") {
obj.removeClass("fa-toggle-on text-success");
obj.addClass("fa-toggle-off text-black-50");
} else {
obj.removeClass("fa-toggle-off text-black-50");
obj.addClass("fa-toggle-on text-success");
}
obj.attr("data-online", online !== "true");
}
});
}
});
$('select#service_type').on('change', function() {
var selected = $('#service_type option:selected').val();
var typeLabel = $('#service_type_label');
if (selected === 'tcp' || selected === 'udp') {
if (selected === 'tcp') {
typeLabel.html('TCP Port')
} else {
typeLabel.html('UDP Port')
}
$('#service_port').parent().parent().removeClass('d-none');
$('#service_check_type').parent().parent().addClass('d-none');
$('#service_url').attr('placeholder', '192.168.1.1');
$('#post_data').parent().parent().addClass('d-none');
$('#service_response').parent().parent().addClass('d-none');
$('#service_response_code').parent().parent().addClass('d-none');
$('#headers').parent().parent().addClass('d-none');
} else if (selected === 'icmp') {
$('#service_port').parent().parent().removeClass('d-none');
$('#headers').parent().parent().addClass('d-none');
$('#service_check_type').parent().parent().addClass('d-none');
$('#service_url').attr('placeholder', '192.168.1.1');
$('#post_data').parent().parent().addClass('d-none');
$('#service_response').parent().parent().addClass('d-none');
$('#service_response_code').parent().parent().addClass('d-none');
} else {
$('#post_data').parent().parent().removeClass('d-none');
$('#service_response').parent().parent().removeClass('d-none');
$('#service_response_code').parent().parent().removeClass('d-none');
$('#service_check_type').parent().parent().removeClass('d-none');
$('#service_url').attr('placeholder', 'https://google.com');
$('#service_port').parent().parent().addClass('d-none');
}
});
async function RenderChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
if (!chart.el) {
return
}
let chartData = await ChartLatency(service, start, end, group, retry);
if (!chartData) {
chartData = await ChartLatency(service, start, end, "minute", retry);
}
chart.render();
chart.updateSeries([{
data: chartData || []
}]);
}
function UTCTime() {
var now = new Date();
now = new Date(now.toUTCString());
return Math.floor(now.getTime() / 1000);
}
function ChartLatency(service, start=0, end=9999999999, group="hour", retry=true) {
let url = "api/services/" + service + "/data?start=" + start + "&end=" + end + "&group=" + group;
return new Promise(resolve => {
$.ajax({
url: url,
type: 'GET',
success: function (data) {
resolve(data.data);
}
});
});
}
function ChartHeatmap(service) {
return new Promise(resolve => {
$.ajax({
url: "api/services/" + service + "/heatmap",
type: 'GET',
success: function (data) {
resolve(data);
}
});
});
}
function FailureAnnotations(chart, service, start=0, end=9999999999, group="hour", retry=true) {
const annotationColor = {
strokeDashArray: 0,
borderColor: "#d0222d",
label: {
show: false,
}
};
var dataArr = [];
$.ajax({
url: "api/services/"+service+"/failures?start="+start+"&end="+end+"&group="+group,
type: 'GET',
success: function(data) {
data.forEach(function (d) {
dataArr.push({x: d.created_at, ...annotationColor})
});
chart.addXaxisAnnotation(dataArr);
}
});
}
$('input[id=service_name]').on('keyup', function() {
var url = $(this).val();
url = url.replace(/[^\w\s]/gi, '').replace(/\s+/g, '-').toLowerCase();
$('#permalink').val(url);
});
$('input[type=checkbox]').on('change', function() {
var element = $(this).attr('id');
$("#"+element+"-value").val(this.checked ? "true" : "false")
});
function PingAjaxChart(chart, service, start=0, end=9999999999, group="hour") {
$.ajax({
url: "api/services/"+service+"/ping?start="+start+"&end="+end+"&group="+group,
type: 'GET',
success: function(data) {
chart.data.labels.pop();
chart.data.datasets.push({
label: "Ping Time",
backgroundColor: "#bababa"
});
chart.update();
data.data.forEach(function(d) {
chart.data.datasets[1].data.push(d);
});
chart.update();
}
});
}
$('.confirm_btn').on('click', function() {
let msg = $(this).attr('data-msg');
var r = confirm(msg);
if (r !== true) {
return false;
}
return true;
});
$('.ajax_delete').on('click', function() {
var r = confirm('Are you sure you want to delete?');
if (r !== true) {
return false;
}
let obj = $(this);
let id = obj.attr('data-id');
let element = obj.attr('data-obj');
let url = obj.attr('href');
let method = obj.attr('data-method');
$.ajax({
url: url,
type: method,
data: JSON.stringify({id: id}),
success: function (data) {
if (data.status === 'error') {
alert(data.error)
} else {
console.log(data);
$('#' + element).remove();
}
}
});
return false
});
$('form.ajax_form').on('submit', function() {
const form = $(this);
let values = form.serializeArray();
let method = form.attr('method');
let action = form.attr('action');
let func = form.attr('data-func');
let redirect = form.attr('data-redirect');
let button = form.find('button[type=submit]');
let alerter = form.find('#alerter');
var arrayData = [];
let newArr = {};
Spinner(button);
values.forEach(function(k, v) {
if (k.name === "password_confirm" || k.value === "" || k.name === "enabled-option") {
return
}
if (k.value === "on") {
k.value = (k.value === "on")
}
if (k.value === "false" || k.value === "true") {
k.value = (k.value === "true")
}
if($.isNumeric(k.value)){
if (k.name !== "password") {
k.value = parseInt(k.value)
}
}
if (k.name === "var1" || k.name === "var2" || k.name === "host" || k.name === "username" || k.name === "password" || k.name === "api_key" || k.name === "api_secret") {
k.value = k.value.toString()
}
newArr[k.name] = k.value;
arrayData.push(newArr)
});
let sendData = JSON.stringify(newArr);
$.ajax({
url: action,
type: method,
data: sendData,
success: function (data) {
setTimeout(function () {
if (data.status === 'error') {
let alerter = form.find('#alerter');
alerter.html(data.error);
alerter.removeClass("d-none");
Spinner(button, true);
} else {
Spinner(button, true);
if (func) {
let fn = window[func];
if (typeof fn === "function") fn({element: form, form: newArr, data: data});
}
if (redirect) {
window.location.href = redirect;
}
}
}, 1000);
}
});
return false;
});
function CreateService(output) {
let form = output.form;
let data = output.data.output;
let objTbl = `<tr id="service_${data.id}">
<td><span class="drag_icon d-none d-md-inline"><i class="fas fa-bars"></i></span> ${form.name}</td>
<td class="d-none d-md-table-cell">${data.online}<span class="badge badge-success">ONLINE</span></td>
<td class="text-right">
<div class="btn-group">
<a href="service/${data.id}" class="btn btn-outline-secondary"><i class="fas fa-chart-area"></i> View</a>
<a href="api/services/${data.id}" class="ajax_delete btn btn-danger confirm-btn" data-method="DELETE" data-obj="service_${data.id}" data-id="${data.id}"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>`;
$('#services_table').append(objTbl);
}
function CreateUser(output) {
console.log('creating user', output)
let form = output.form;
let data = output.data.output;
let objTbl = `<tr id="user_${data.id}">
<td>${form.username}</td>
<td class="text-right">
<div class="btn-group">
<a href="user/${data.id}" class="btn btn-outline-secondary"><i class="fas fa-user-edit"></i> Edit</a>
<a href="api/users/${data.id}" class="ajax_delete btn btn-danger confirm-btn" data-method="DELETE" data-obj="user_${data.id}" data-id="${data.id}"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>`;
$('#users_table').append(objTbl);
}
$('select#service_check_type').on('change', function() {
var selected = $('#service_check_type option:selected').val();
if (selected === 'POST') {
$('#post_data').parent().parent().removeClass('d-none');
} else {
$('#post_data').parent().parent().addClass('d-none');
}
});
$(function() {
var pathname = window.location.pathname;
if (pathname === '/logs') {
var lastline;
var logArea = $('#live_logs');
setInterval(function() {
$.get('/logs/line', function(data, status) {
if (lastline !== data) {
var curr = $.trim(logArea.text());
var line = data.replace(/(\r\n|\n|\r)/gm, ' ');
line = line + '\n';
logArea.text(line + curr);
lastline = data;
}
});
}, 200);
}
});
$('.confirm-btn').on('click', function() {
var r = confirm('Are you sure you want to delete?');
let obj = $(this);
let redirect = obj.attr('data-redirect');
let href = obj.attr('href');
let method = obj.attr('data-method');
let data = obj.attr('data-object');
if (r === true) {
$.ajax({
url: href,
type: method,
data: data ? data : null,
success: function (data) {
console.log("send to url: ", href);
if (redirect) {
window.location.href = redirect;
}
return false;
}
});
} else {
return false;
}
});
$('.select-input').on('click', function() {
$(this).select();
});
// $('input[name=password], input[name=password_confirm]').on('change keyup input paste', function() {
// var password = $('input[name=password]'),
// repassword = $('input[name=password_confirm]'),
// both = password.add(repassword).removeClass('is-valid is-invalid');
//
// var btn = $(this).parents('form:first').find('button[type=submit]');
// password.addClass(
// password.val().length > 0 ? 'is-valid' : 'is-invalid'
// );
// repassword.addClass(
// password.val().length > 0 ? 'is-valid' : 'is-invalid'
// );
//
// if (password.val() !== repassword.val()) {
// both.addClass('is-invalid');
// btn.prop('disabled', true);
// } else {
// btn.prop('disabled', false);
// }
// });
var ranVar = false;
var ranTheme = false;
var ranMobile = false;
$('a[data-toggle=pill]').on('shown.bs.tab', function(e) {
var target = $(e.target).attr('href');
if (target === '#v-pills-style' && !ranVar) {
var sass_vars = CodeMirror.fromTextArea(document.getElementById('sass_vars'), {
lineNumbers: true,
matchBrackets: true,
mode: 'text/x-scss',
colorpicker: true
});
sass_vars.setSize(null, 900);
ranVar = true;
} else if (target === '#pills-theme' && !ranTheme) {
var theme_css = CodeMirror.fromTextArea(document.getElementById('theme_css'), {
lineNumbers: true,
matchBrackets: true,
mode: 'text/x-scss',
colorpicker: true
});
theme_css.setSize(null, 900);
ranTheme = true;
} else if (target === '#pills-mobile' && !ranMobile) {
var mobile_css = CodeMirror.fromTextArea(document.getElementById('mobile_css'), {
lineNumbers: true,
matchBrackets: true,
mode: 'text/x-scss',
colorpicker: true
});
mobile_css.setSize(null, 900);
ranMobile = true;
}
});

View File

@ -1,57 +0,0 @@
/*
* Statping
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statping
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var currentLocation = window.location;
var domain = $("#domain_input");
if (domain.val() === "") {
domain.val(currentLocation.origin);
}
$('select#database_type').on('change', function(){
var selected = $('#database_type option:selected').val();
if (selected=="sqlite") {
$("#db_host").hide();
$("#db_password").hide();
$("#db_port").hide();
$("#db_user").hide();
$("#db_database").hide();
} else {
$("#db_host").show();
$("#db_password").show();
$("#db_port").show();
$("#db_user").show();
$("#db_database").show();
}
if (selected=="mysql") {
$("#db_port_in").val('3306');
} else if (selected=="postgres") {
$("#db_port_in").val('5432');
}
});
$("#setup_form").submit(function() {
$("#setup_button").prop("disabled", true);
$("#setup_button").text("Creating Statping...");
return true;
});
$('form').submit(function() {
$(this).find("button[type='submit']").prop('disabled',true);
$(this).find("button[type='submit']").text('Loading...');
});

File diff suppressed because one or more lines are too long

View File

@ -29,20 +29,12 @@ import (
var (
log = utils.Log.WithField("type", "source")
CssBox *rice.Box // CSS files from the 'source/css' directory, this will be loaded into '/assets/css'
ScssBox *rice.Box // SCSS files from the 'source/scss' directory, this will be loaded into '/assets/scss'
JsBox *rice.Box // JS files from the 'source/js' directory, this will be loaded into '/assets/js'
TmplBox *rice.Box // HTML and other small files from the 'source/tmpl' directory, this will be loaded into '/assets'
FontBox *rice.Box // HTML and other small files from the 'source/tmpl' directory, this will be loaded into '/assets'
)
// Assets will load the Rice boxes containing the CSS, SCSS, JS, and HTML files.
func Assets() error {
CssBox = rice.MustFindBox("css")
ScssBox = rice.MustFindBox("scss")
JsBox = rice.MustFindBox("js")
TmplBox = rice.MustFindBox("tmpl")
FontBox = rice.MustFindBox("font")
TmplBox = rice.MustFindBox("dist")
return nil
}
@ -93,7 +85,7 @@ func UsingAssets(folder string) bool {
CreateAllAssets(folder)
err := CompileSASS(folder)
if err != nil {
CopyToPublic(CssBox, folder+"/css", "base.css")
//CopyToPublic(CssBox, folder+"/css", "base.css")
log.Warnln("Default 'base.css' was insert because SASS did not work.")
return true
}
@ -135,11 +127,7 @@ func CreateAllAssets(folder string) error {
MakePublicFolder(folder + "/assets/font")
MakePublicFolder(folder + "/assets/files")
log.Infoln("Inserting scss, css, and javascript files into assets folder")
CopyAllToPublic(ScssBox, "scss")
CopyAllToPublic(FontBox, "font")
CopyAllToPublic(CssBox, "css")
CopyAllToPublic(JsBox, "js")
CopyToPublic(FontBox, folder+"/assets/font", "all.css")
CopyAllToPublic(TmplBox, folder+"/assets")
CopyToPublic(TmplBox, folder+"/assets", "robots.txt")
CopyToPublic(TmplBox, folder+"/assets", "banner.png")
CopyToPublic(TmplBox, folder+"/assets", "favicon.ico")

View File

@ -1,11 +0,0 @@
name,domain,expected,expected_status,interval,type,method,post_data,port,timeout,order,allow_notifications,public,group_id,headers,permalink,verify_ssl
Bulk Upload,http://google.com,,200,60s,http,get,,,60s,1,TRUE,TRUE,,Authorization=example,bulk_example,FALSE
JSON Post,https://jsonplaceholder.typicode.com/posts,,200,1m,http,post,"{""id"": 1, ""title"": 'foo', ""body"": 'bar', ""userId"": 1}",,15s,2,TRUE,TRUE,,Content-Type=application/json,json_post_example,FALSE
Google DNS,8.8.8.8,,,60s,tcp,,,53,10s,3,TRUE,TRUE,,,google_dns_example,FALSE
Google DNS UDP,8.8.8.8,,,60s,udp,,,53,10s,4,TRUE,TRUE,,,google_dns_udp_example,FALSE
Statping Demo Page,https://demo.statping.com/health,"(\""online\"": true)",200,30s,http,get,,,10s,5,TRUE,TRUE,,,demo_link,FALSE
Statping MySQL Page,https://mysql.statping.com/health,"(\""online\"": true)",200,30s,http,get,,,10s,6,TRUE,TRUE,,,mysql_demo_link,FALSE
Statping SQLite Page,https://sqlite.statping.com/health,"(\""online\"": true)",200,30s,http,get,,,10s,7,TRUE,TRUE,,,sqlite_demo_link,FALSE
Token Balance,https://status.tokenbalance.com/health,"(\""online\"": true)",200,30s,http,get,,,10s,8,TRUE,TRUE,,,token_balance,FALSE
CloudFlare DNS,1.1.1.1,,,60s,tcp,,,53,10s,9,TRUE,TRUE,,,cloudflare_dns_example,FALSE
Verisign DNS,64.6.64.4,,,60s,tcp,,,53,10s,10,TRUE,TRUE,,,verisign_dns_example,FALSE
1 name domain expected expected_status interval type method post_data port timeout order allow_notifications public group_id headers permalink verify_ssl
2 Bulk Upload http://google.com 200 60s http get 60s 1 TRUE TRUE Authorization=example bulk_example FALSE
3 JSON Post https://jsonplaceholder.typicode.com/posts 200 1m http post {"id": 1, "title": 'foo', "body": 'bar', "userId": 1} 15s 2 TRUE TRUE Content-Type=application/json json_post_example FALSE
4 Google DNS 8.8.8.8 60s tcp 53 10s 3 TRUE TRUE google_dns_example FALSE
5 Google DNS UDP 8.8.8.8 60s udp 53 10s 4 TRUE TRUE google_dns_udp_example FALSE
6 Statping Demo Page https://demo.statping.com/health (\"online\": true) 200 30s http get 10s 5 TRUE TRUE demo_link FALSE
7 Statping MySQL Page https://mysql.statping.com/health (\"online\": true) 200 30s http get 10s 6 TRUE TRUE mysql_demo_link FALSE
8 Statping SQLite Page https://sqlite.statping.com/health (\"online\": true) 200 30s http get 10s 7 TRUE TRUE sqlite_demo_link FALSE
9 Token Balance https://status.tokenbalance.com/health (\"online\": true) 200 30s http get 10s 8 TRUE TRUE token_balance FALSE
10 CloudFlare DNS 1.1.1.1 60s tcp 53 10s 9 TRUE TRUE cloudflare_dns_example FALSE
11 Verisign DNS 64.6.64.4 60s tcp 53 10s 10 TRUE TRUE verisign_dns_example FALSE

View File

@ -1,208 +0,0 @@
{{define "title"}}Statping | Dashboard{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{template "nav" }}
<div class="col-12 mt-3">
<div class="row stats_area mb-5">
<div class="col-4">
<span class="lg_number">{{ len Services }}</span>
Total Services
</div>
<div class="col-4">
<span class="lg_number">{{ CoreApp.Count24HFailures }}</span>
Failures last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{ CoreApp.CountOnline }}</span>
Online Services
</div>
</div>
{{ range Services }}
<div class="col-12 card mb-3" style="min-height: 260px">
<div class="card-body">
<h5 class="card-title"><a href="service/{{.Id}}">{{.Name}}</a> {{if .Online}}<span class="badge float-right badge-success">ONLINE</span>{{else}}<span class="badge float-right badge-danger">OFFLINE</span>{{end}}</h5>
<div class="row">
<div class="col-md-3 col-sm-6">
<div id="spark_service_{{.Id}}_1"></div>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_{{.Id}}_2"></div>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_{{.Id}}_3"></div>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_{{.Id}}_4"></div>
</div>
</div>
</div>
</div>
{{end}}
<div class="row mt-4">
<div class="col-12">
{{if eq (len CoreApp.Services) 0}}
<div class="jumbotron jumbotron-fluid">
<div class="text-center">
<h1 class="display-4">No Services!</h1>
<a class="lead">You don't have any websites or applications being monitored by your Statping server. <p><a href="service/create" class="btn btn-secondary mt-3">Add Service</a></p></p>
</div>
</div>
{{end}}
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_scripts"}}
<script>
function AddTotal(arr) {
var total = 0;
arr.forEach(function(d) {
total += d
});
return total.toString();
}
function Average(arr) {
var total = AddTotal(arr);
if (arr.length === 0) {
return total+"ms"
}
return (total / arr.length).toFixed(0)+"ms";
}
const sparkOption = {
chart: {
type: 'area',
height: 180,
sparkline: {
enabled: true
},
},
stroke: {
curve: 'straight'
},
fill: {
opacity: 0.3,
},
yaxis: {
min: 0
},
colors: ['#DCE6EC'],
tooltip: {
enabled: false
}
}
{{ range Services }}
var sparklineData_{{js .Id}}_1 = {{js (.SparklineDayFailures 7)}};
var sparklineData_{{js .Id}}_2 = {{js (.SparklineDayFailures 30)}};
var sparklineData_{{js .Id}}_3 = {{js (.SparklineHourResponse 30 "latency")}};
var sparklineData_{{js .Id}}_4 = {{js (.SparklineHourResponse 30 "ping_time")}};
var options_{{js .Id}}_1 = {
...sparkOption,
title: {
text: AddTotal(sparklineData_{{js .Id}}_1),
offsetX: 0,
style: {
fontSize: '28px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: 'Failures Last 7 Days',
offsetX: 0,
style: {
fontSize: '14px',
cssClass: 'apexcharts-yaxis-title'
}
},
series: [{
data: sparklineData_{{js .Id}}_1
}],
};
var options_{{js .Id}}_2 = {
...sparkOption,
title: {
text: AddTotal(sparklineData_{{js .Id}}_2),
offsetX: 0,
style: {
fontSize: '28px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: 'Failures Last Month',
offsetX: 0,
style: {
fontSize: '14px',
cssClass: 'apexcharts-yaxis-title'
}
},
series: [{
data: sparklineData_{{js .Id}}_2
}],
};
var options_{{js .Id}}_3 = {
...sparkOption,
title: {
text: Average(sparklineData_{{js .Id}}_3),
offsetX: 0,
style: {
fontSize: '28px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: 'Average Response',
offsetX: 0,
style: {
fontSize: '14px',
cssClass: 'apexcharts-yaxis-title'
}
},
series: [{
data: sparklineData_{{js .Id}}_3
}],
};
var options_{{js .Id}}_4 = {
...sparkOption,
title: {
text: Average(sparklineData_{{js .Id}}_4),
offsetX: 0,
style: {
fontSize: '28px',
cssClass: 'apexcharts-yaxis-title'
}
},
subtitle: {
text: 'Ping Time',
offsetX: 0,
style: {
fontSize: '14px',
cssClass: 'apexcharts-yaxis-title'
}
},
series: [{
data: sparklineData_{{js .Id}}_4
}],
};
{{end}}
{{ range Services }}
var spark_{{js .Id}}_1 = new ApexCharts(document.querySelector("#spark_service_{{.Id}}_1"), options_{{js .Id}}_1);
var spark_{{js .Id}}_2 = new ApexCharts(document.querySelector("#spark_service_{{.Id}}_2"), options_{{js .Id}}_2);
var spark_{{js .Id}}_3 = new ApexCharts(document.querySelector("#spark_service_{{.Id}}_3"), options_{{js .Id}}_3);
var spark_{{js .Id}}_4 = new ApexCharts(document.querySelector("#spark_service_{{.Id}}_4"), options_{{js .Id}}_4);
spark_{{js .Id}}_1.render();
spark_{{js .Id}}_2.render();
spark_{{js .Id}}_3.render();
spark_{{js .Id}}_4.render();
{{end}}
</script>
{{end}}

View File

@ -1,10 +0,0 @@
{{define "title"}}Statping Page Not Found{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-12 mt-3">
<div class="alert alert-danger" role="alert">
Sorry, this page doesn't seem to exist.
</div>
</div>
</div>
{{end}}

View File

@ -1,9 +0,0 @@
{{ define "footer"}}
<div class="footer text-center mb-4 p-2">
{{ if CoreApp.Footer.String }}
{{ safe CoreApp.Footer.String }}
{{ else }}
<a href="https://github.com/hunterlong/statping" target="_blank">Statping {{VERSION}} made with <i class="text-danger fas fa-heart"></i></a> | <a href="dashboard">Dashboard</a>
{{ end }}
</div>
{{ end }}

View File

@ -1,27 +0,0 @@
{{define "form_checkin"}}
<div class="card">
<div class="card-body">
<form class="ajax_form" action="api/checkin" data-redirect="/service/{{.Id}}" method="POST">
<div class="form-group row">
<div class="col-md-3">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-3">
<label for="checkin_interval" class="col-form-label">Interval (seconds)</label>
<input type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
</div>
<div class="col-3">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div>
<div class="col-3">
<label for="submit" class="col-form-label"></label>
<input type="hidden" name="service_id" class="form-control" id="service_id" value="{{.Id}}">
<button type="submit" id="submit" class="btn btn-success d-block" style="margin-top: 14px;">Save Checkin</button>
</div>
</div>
</form>
</div>
</div>
{{end}}

View File

@ -1,31 +0,0 @@
{{define "form_group"}}
<div class="card">
<div class="card-body">
{{$message := .}}
<form class="ajax_form" action="api/groups{{if ne .Id 0}}/{{.Id}}{{end}}" data-redirect="services" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Group Name</label>
<div class="col-sm-8">
<input type="text" name="name" class="form-control" value="{{.Name}}" id="title" placeholder="Group Name" required>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Public Group</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="public" class="switch" id="switch-group-public" {{if .Public.Bool}}checked{{end}}>
<label for="switch-group-public">Show group services to the public</label>
<input type="hidden" name="public" id="switch-group-public-value" value="{{if .Public.Bool}}true{{else}}false{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update Group{{else}}Create Group{{end}}</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
{{end}}

View File

@ -1,84 +0,0 @@
{{define "form_incident"}}
<div class="card">
<div class="card-body">
{{$message := .}}
<form class="ajax_form" action="api/messages{{if ne .Id 0}}/{{.Id}}{{end}}" data-redirect="/messages" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input type="text" name="title" class="form-control" value="{{.Title}}" id="title" placeholder="Message Title" required>
</div>
</div>
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Description</label>
<div class="col-sm-8">
<textarea rows="5" name="description" class="form-control" id="description" required>{{.Description}}</textarea>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Message Date Range</label>
<div class="col-sm-4">
<input type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="{{ParseTime .StartOn "2006-01-02T15:04:05Z"}}" required>
</div>
<div class="col-sm-4">
<input type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="{{ParseTime .EndOn "2006-01-02T15:04:05Z"}}" required>
</div>
</div>
<div class="form-group row">
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
<div class="col-sm-8">
<select class="form-control" name="service" id="service_id">
<option value="0" {{if eq (ToString .ServiceId) "0"}}selected{{end}}>Global Message</option>
{{range Services}}
{{$s := .Select}}
<option value="{{$s.Id}}" {{if eq $message.ServiceId $s.Id}}selected{{end}}>{{$s.Name}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notification Method</label>
<div class="col-sm-8">
<input type="text" name="notify_method" class="form-control" id="notify_method" value="{{.NotifyMethod}}" placeholder="email">
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notify Users</label>
<div class="col-sm-8">
<span class="switch">
<input type="checkbox" name="notify_users-value" class="switch" id="switch-normal"{{if .NotifyUsers.Bool}} checked{{end}}>
<label for="switch-normal">Notify Users Before Scheduled Time</label>
<input type="hidden" name="notify_users" id="switch-normal-value" value="{{if .NotifyUsers.Bool}}true{{else}}false{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label>
<div class="col-sm-8">
<div class="form-inline">
<input type="number" name="notify_before" class="col-4 form-control" id="notify_before" value="{{.NotifyBefore.Int64}}">
<select class="ml-2 col-7 form-control" name="notify_before_scale" id="notify_before_scale">
<option value="minute"{{if ne .Id 0}} selected{{else}}{{if eq .NotifyBeforeScale "minute"}}selected{{end}}{{end}}>Minutes</option>
<option value="hour"{{if eq .NotifyBeforeScale "hour"}} selected{{end}}>Hours</option>
<option value="day"{{if eq .NotifyBeforeScale "day"}} selected{{end}}>Days</option>
</select>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update Message{{else}}Create Message{{end}}</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
{{end}}

View File

@ -1,32 +0,0 @@
{{define "form_integration"}}
{{$i := .Get}}
<form class="integration_{{underscore $i.ShortName }}" action="settings/integrator/{{ $i.ShortName }}" method="POST">
<input type="hidden" name="integrator" class="form-control" value="{{ $i.ShortName }}">
{{if $i.ShortName}}<h4 class="text-capitalize">{{$i.ShortName}}</h4>{{end}}
{{if $i.Description}}<p class="small text-muted">{{safe $i.Description}}</p>{{end}}
{{range $i.Fields}}
<div class="form-group">
<label class="text-capitalize" for="{{underscore .Name}}">{{.Name}}</label>
{{if eq .Type "textarea"}}
<textarea rows="3" class="form-control" name="{{underscore .Name}}" id="{{underscore .Name}}">{{ .Value }}</textarea>
{{else if eq .Type "text"}}
<input type="text" name="{{underscore .Name}}" class="form-control" value="{{ .Value }}" id="{{underscore .Name}}">
{{else if eq .Type "password"}}
<input type="password" name="{{underscore .Name}}" class="form-control" value="{{ .Value }}" id="{{underscore .Name}}">
{{else if eq .Type "integer"}}
<input type="number" name="{{underscore .Name}}" class="form-control" value="{{ .Value }}" id="{{underscore .Name}}">
{{else if eq .Type "file"}}
<input type="file" name="{{underscore .Name}}" class="form-control" value="{{ .Value }}" id="{{underscore .Name}}">
{{end}}
{{if .Description}}
<small class="form-text text-muted">{{safe .Description}}</small>
{{end}}
</div>
{{end}}
<button type="submit" class="btn btn-block btn-info fetch_integrator">Fetch Services</button>
<div class="alert alert-danger d-none" id="integration_alerter" role="alert"></div>
</form>
{{end}}

View File

@ -1,84 +0,0 @@
{{define "form_message"}}
<div class="card">
<div class="card-body">
{{$message := .}}
<form class="ajax_form" action="api/messages{{if ne .Id 0}}/{{.Id}}{{end}}" data-redirect="messages" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input type="text" name="title" class="form-control" value="{{.Title}}" id="title" placeholder="Message Title" required>
</div>
</div>
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Description</label>
<div class="col-sm-8">
<textarea rows="5" name="description" class="form-control" id="description" required>{{.Description}}</textarea>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Message Date Range</label>
<div class="col-sm-4">
<input type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="{{ParseTime .StartOn "2006-01-02T15:04:05Z"}}" required>
</div>
<div class="col-sm-4">
<input type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="{{ParseTime .EndOn "2006-01-02T15:04:05Z"}}" required>
</div>
</div>
<div class="form-group row">
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
<div class="col-sm-8">
<select class="form-control" name="service" id="service_id">
<option value="0" {{if eq (ToString .ServiceId) "0"}}selected{{end}}>Global Message</option>
{{range Services}}
{{$s := .Select}}
<option value="{{$s.Id}}" {{if eq $message.ServiceId $s.Id}}selected{{end}}>{{$s.Name}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notification Method</label>
<div class="col-sm-8">
<input type="text" name="notify_method" class="form-control" id="notify_method" value="{{.NotifyMethod}}" placeholder="email">
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notify Users</label>
<div class="col-sm-8">
<span class="switch">
<input type="checkbox" name="notify_users-value" class="switch" id="switch-normal"{{if .NotifyUsers.Bool}} checked{{end}}>
<label for="switch-normal">Notify Users Before Scheduled Time</label>
<input type="hidden" name="notify_users" id="switch-normal-value" value="{{if .NotifyUsers.Bool}}true{{else}}false{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label>
<div class="col-sm-8">
<div class="form-inline">
<input type="number" name="notify_before" class="col-4 form-control" id="notify_before" value="{{.NotifyBefore.Int64}}">
<select class="ml-2 col-7 form-control" name="notify_before_scale" id="notify_before_scale">
<option value="minute"{{if ne .Id 0}} selected{{else}}{{if eq .NotifyBeforeScale "minute"}}selected{{end}}{{end}}>Minutes</option>
<option value="hour"{{if eq .NotifyBeforeScale "hour"}} selected{{end}}>Hours</option>
<option value="day"{{if eq .NotifyBeforeScale "day"}} selected{{end}}>Days</option>
</select>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update Message{{else}}Create Message{{end}}</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
{{end}}

View File

@ -1,73 +0,0 @@
{{define "form_notifier"}}
{{$n := .Select}}
<form class="ajax_form {{underscore $n.Method }}" data-func="SaveNotifier" action="api/notifier/{{ $n.Method }}" method="POST">
{{if $n.Title}}<h4 class="text-capitalize">{{$n.Title}}</h4>{{end}}
{{if $n.Description}}<p class="small text-muted">{{safe $n.Description}}</p>{{end}}
{{range $n.Form}}
<div class="form-group">
<label class="text-capitalize{{if .IsHidden}} d-none{{end}}" for="{{underscore .Title}}">{{.Title}}</label>
{{if eq .Type "textarea"}}
<textarea rows="3" class="form-control{{if .IsHidden}} d-none{{end}}" name="{{underscore .DbField}}" id="{{underscore .Title}}">{{ $n.GetValue .DbField }}</textarea>
{{else}}
<input type="{{.Type}}" name="{{underscore .DbField}}" class="form-control{{if .IsHidden}} d-none{{end}}" value="{{ $n.GetValue .DbField }}" id="{{underscore .Title}}" placeholder="{{.Placeholder}}" {{if .Required}}required{{end}}>
{{end}}
{{if .SmallText}}
<small class="form-text text-muted{{if .IsHidden}} d-none{{end}}">{{safe .SmallText}}</small>
{{end}}
</div>
{{end}}
<div class="row">
<div class="col-9 col-sm-6">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text">Limit</div>
</div>
<input type="number" class="form-control" name="limits" min="1" max="60" id="limits_per_hour_{{underscore $n.Method }}" value="{{$n.Limits}}" placeholder="7">
<div class="input-group-append">
<div class="input-group-text">Per Minute</div>
</div>
</div>
</div>
<div class="col-3 col-sm-2 mt-1">
<span class="switch">
<input type="checkbox" name="enabled-option" class="switch" id="switch-{{ $n.Method }}" {{if $n.Enabled.Bool}}checked{{end}}>
<label for="switch-{{ $n.Method }}"></label>
<input type="hidden" name="enabled" id="switch-{{ $n.Method }}-value" value="{{if $n.Enabled.Bool}}true{{else}}false{{end}}">
</span>
</div>
<input type="hidden" name="method" value="{{underscore $n.Method }}">
<div class="col-12 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button type="submit" class="btn btn-primary btn-block text-capitalize"><i class="fa fa-check-circle"></i> Save</button>
</div>
{{if $n.CanTest}}
<div class="col-12 col-sm-12">
<button class="test_notifier btn btn-secondary btn-block text-capitalize col-12 float-right"><i class="fa fa-vial"></i> Test Notifier</button>
</div>
<div class="col-12 col-sm-12 mt-2">
<div class="alert alert-danger d-none" id="{{underscore $n.Method}}-error" role="alert">
<i class="fa fa-exclamation-triangle"></i> {{$n.Method}} has an error!
</div>
<div class="alert alert-success d-none" id="{{underscore $n.Method}}-success" role="alert">
<i class="fa fa-smile-beam"></i> The {{$n.Method}} notifier is working correctly!
</div>
</div>
{{end}}
</div>
{{if $n.Author}}
<span class="d-block small text-center mt-3 mb-5">
<span class="text-capitalize">{{$n.Title}}</span> Notifier created by <a href="{{$n.AuthorUrl}}" target="_blank">{{$n.Author}}</a>
</span>
{{ end }}
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
{{end}}

View File

@ -1,170 +0,0 @@
{{define "form_service"}}
<div class="card">
<div class="card-body">
{{$s := .}}
<form class="ajax_form" action="api/services{{if ne .Id 0}}/{{.Id}}{{end}}" data-redirect="services" method="POST">
<h4 class="mb-5 text-muted">Basic Information</h4>
<div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8">
<input type="text" name="name" class="form-control" id="service_name" value="{{.Name}}" placeholder="Name" required spellcheck="false" autocorrect="off">
<small class="form-text text-muted">Give your service a name you can recognize</small>
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Type</label>
<div class="col-sm-8">
<select name="type" class="form-control" id="service_type" value="{{.Type}}" {{if ne .Type ""}}readonly{{end}}>
<option value="http" {{if eq .Type "http"}}selected{{end}}>HTTP Service</option>
<option value="tcp" {{if eq .Type "tcp"}}selected{{end}}>TCP Service</option>
<option value="udp" {{if eq .Type "udp"}}selected{{end}}>UDP Service</option>
<option value="icmp" {{if eq .Type "icmp"}}selected{{end}}>ICMP Ping</option>
</select>
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
</div>
</div>
<div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">{{if (eq .Type "tcp") or (eq .Type "udp")}}Host/IP Address{{else}}Application Endpoint (URL){{end}}</label>
<div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="service_url" value="{{.Domain}}" placeholder="https://google.com" required autocapitalize="none" spellcheck="false">
<small class="form-text text-muted">Statping will attempt to connect to this URL</small>
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Group</label>
<div class="col-sm-8">
<select name="group_id" class="form-control" id="group_id">
<option value="0" {{if eq $s.GroupId 0}}selected{{end}}>None</option>
{{range Groups false}}
<option value="{{.Id}}" {{if eq $s.GroupId .Id}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
<small class="form-text text-muted">Attach this service to a group</small>
</div>
</div>
<h4 class="mt-5 mb-5 text-muted">Request Details</h4>
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
<label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8">
<select name="method" class="form-control" id="service_check_type" value="{{.Method}}">
<option value="GET" {{if eq .Method "GET"}}selected{{end}}>GET</option>
<option value="POST" {{if eq .Method "POST"}}selected{{end}}>POST</option>
<option value="DELETE" {{if eq .Method "DELETE"}}selected{{end}}>DELETE</option>
<option value="PATCH" {{if eq .Method "PATCH"}}selected{{end}}>PATCH</option>
<option value="PUT" {{if eq .Method "PUT"}}selected{{end}}>PUT</option>
</select>
<small class="form-text text-muted">A GET request will simply request the endpoint, you can also send data with POST.</small>
</div>
</div>
<div class="form-group row{{if ne .Method "POST"}} d-none{{end}}">
<label for="post_data" class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
<div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="none" spellcheck="false" placeholder='{"data": { "method": "success", "id": 148923 } }'>{{.PostData.String}}</textarea>
<small class="form-text text-muted">Insert a JSON string to send data to the endpoint.</small>
</div>
</div>
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
<label for="headers" class="col-sm-4 col-form-label">HTTP Headers</label>
<div class="col-sm-8">
<input name="headers" class="form-control" id="headers" autocapitalize="none" spellcheck="false" placeholder='Authorization=1010101,Content-Type=application/json' value="{{.Headers.String}}">
<small class="form-text text-muted">Comma delimited list of HTTP Headers (KEY=VALUE,KEY=VALUE)</small>
</div>
</div>
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8">
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="none" spellcheck="false" placeholder='(method)": "((\\"|[success])*)"'>{{.Expected.String}}</textarea>
<small class="form-text text-muted">You can use plain text or insert <a target="_blank" href="https://regex101.com/r/I5bbj9/1">Regex</a> to validate the response</small>
</div>
</div>
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{if ne .ExpectedStatus 0}}{{.ExpectedStatus}}{{else}}200{{end}}" placeholder="200" id="service_response_code">
<small class="form-text text-muted">A status code of 200 is success, or view all the <a target="_blank" href="https://www.restapitutorial.com/httpstatuscodes.html">HTTP Status Codes</a></small>
</div>
</div>
<div class="form-group row{{if (ne .Type "tcp") and (ne .Type "udp") and (ne .Type "icmp")}} d-none{{end}}">
<label for="port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{if ne .Port 0}}{{.Port}}{{end}}" id="service_port" placeholder="8080">
</div>
</div>
<h4 class="mt-5 mb-5 text-muted">Additional Options</h4>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input type="number" name="check_interval" class="form-control" value="{{if ne .Interval 0}}{{.Interval}}{{else}}60{{end}}" min="1" id="service_interval" required>
<small id="interval" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
</div>
</div>
<div class="form-group row">
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8">
<input type="number" name="timeout" class="form-control" value="{{if ne .Timeout 0}}{{.Timeout}}{{else}}15{{end}}" placeholder="15" id="service_timeout" min="1">
<small class="form-text text-muted">If the endpoint does not respond within this time it will be considered to be offline</small>
</div>
</div>
<div class="form-group row">
<label for="post_data" class="col-sm-4 col-form-label">Permalink URL</label>
<div class="col-sm-8">
<input type="text" name="permalink" class="form-control" value="{{.Permalink.String}}" id="permalink" autocapitalize="none" spellcheck="true" placeholder='awesome_service'>
<small class="form-text text-muted">Use text for the service URL rather than the service number.</small>
</div>
</div>
<div class="form-group row d-none">
<label for="order" class="col-sm-4 col-form-label">List Order</label>
<div class="col-sm-8">
<input type="number" name="order" class="form-control" min="0" value="{{.Order}}" id="order">
<small class="form-text text-muted">You can also drag and drop services to reorder on the Services tab.</small>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" {{if eq .Id 0}}checked{{end}}{{if .VerifySSL.Bool}}checked{{end}}>
<label for="switch-verify-ssl">Verify SSL Certificate for this service</label>
<input type="hidden" name="verify_ssl" id="switch-verify-ssl-value" value="{{if eq .Id 0}}true{{else}}{{if .VerifySSL.Bool}}true{{else}}false{{end}}{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Notifications</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" {{if eq .Id 0}}checked{{end}}{{if .AllowNotifications.Bool}}checked{{end}}>
<label for="switch-notifications">Allow notifications to be sent for this service</label>
<input type="hidden" name="allow_notifications" id="switch-notifications-value" value="{{if eq .Id 0}}true{{else}}{{if .AllowNotifications.Bool}}true{{else}}false{{end}}{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Visible</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="public-option" class="switch" id="switch-public" {{if eq .Id 0}}checked{{else}}{{if .Public.Bool}}checked{{end}}{{end}}>
<label for="switch-public">Show service details to the public</label>
<input type="hidden" name="public" id="switch-public-value" value="{{if eq .Id 0}}true{{else}}{{if .Public.Bool}}true{{else}}false{{end}}{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<div class="{{if ne .Id 0}}col-6{{else}}col-12{{end}}">
<button type="submit" class="btn btn-success btn-block">{{if ne .Id 0}}Update Service{{else}}Create Service{{end}}</button>
</div>
{{if ne .Id 0}}
<div class="col-6">
<a href="service/{{ .Id }}/delete_failures" data-method="GET" data-redirect="/service/{{ .Id }}" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
</div>
{{end}}
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
{{end}}

View File

@ -1,45 +0,0 @@
{{define "form_user"}}
<div class="card">
<div class="card-body">
<form class="ajax_form" action="api/users{{if ne .Id 0}}/{{.Id}}{{end}}" data-redirect="users" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Username</label>
<div class="col-6 col-md-4">
<input type="text" name="username" class="form-control" value="{{.Username}}" id="username" placeholder="Username" required autocorrect="off" autocapitalize="none">
</div>
<div class="col-6 col-md-4">
<span class="switch">
<input type="checkbox" name="admin" class="switch" id="switch-normal"{{if .Admin.Bool}} checked{{end}}>
<label for="switch-normal">Administrator</label>
<input type="hidden" name="admin" id="switch-normal-value" value="{{if .Admin.Bool}}true{{else}}false{{end}}">
</span>
</div>
</div>
<div class="form-group row">
<label for="email" class="col-sm-4 col-form-label">Email Address</label>
<div class="col-sm-8">
<input type="email" name="email" class="form-control" id="email" value="{{.Email}}" placeholder="user@domain.com" required autocapitalize="none" spellcheck="false">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-4 col-form-label">Password</label>
<div class="col-sm-8">
<input type="password" name="password" class="form-control" id="password" {{if ne .Id 0}}value=""{{end}} placeholder="Password" required>
</div>
</div>
<div class="form-group row">
<label for="password_confirm" class="col-sm-4 col-form-label">Confirm Password</label>
<div class="col-sm-8">
<input type="password" name="password_confirm" class="form-control" id="password_confirm" {{if ne .Id 0}}value=""{{end}} placeholder="Confirm Password" required>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update User{{else}}Create User{{end}}</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
{{end}}

View File

@ -1,17 +0,0 @@
{{define "title"}}{{.Name}} Status{{end}}
{{define "description"}}Group {{.Name}}{{end}}
{{ define "content" }}
{{$isAdmin := Auth}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{if IsUser}}
{{template "nav"}}
{{end}}
<div class="col-12 mb-4">
{{template "form_group" .Group}}
</div>
</div>
{{end}}

View File

@ -1,21 +0,0 @@
{{ define "head"}}
<head>
<meta charset="utf-8">
<title>{{block "title" .}} {{end}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1.0, user-scalable=0">
<meta name="description" content="{{block "description" .}}{{end}}">
<base href="{{BasePath}}">
{{if USE_CDN}}
<link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
<link rel="stylesheet" href="https://assets.statping.com/base.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.1/css/all.css" integrity="sha384-5sAR7xN1Nv6T6+dT2mhtzEpVJvfS3NScPQTrOxhwjIuvcA67KV2R5Jz6kr4abQsz" crossorigin="anonymous">
{{ else }}
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="font/all.css">
{{end}}
{{block "extra_css" .}} {{end}}
</head>
{{end}}

View File

@ -1,28 +0,0 @@
{{define "title"}}Statping | Help{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{if IsUser}}
{{template "nav"}}
{{end}}
<div class="col-12 bg-white p-4">
<div class="col-12 col-md-8 offset-md-2 mb-4 mt-3">
<img class="col-12 mt-5 mt-md-0" src="banner.png">
</div>
<h3>Index</h3>
{{ safe . }}
</div>
</div>
{{end}}
{{define "extra_css"}}
<style>
pre {
background-color: white;
padding: 10px 15px;
border: 1px solid #a2a2a233;
border-radius: 7px;
}
code {
color: #d87e1a;
}
</style>
{{end}}

View File

@ -1,108 +0,0 @@
{{define "title"}}{{CoreApp.Name}} Status{{end}}
{{define "description"}}{{CoreApp.Name}} is currently monitoring {{len CoreApp.Services}} services with 0 of them offline. {{CoreApp.Name}} is using Statping to monitor applications.{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-2 sm-container">
<h1 class="col-12 text-center mb-4 mt-sm-3 header-title">{{.Name}}</h1>
{{ if .Description }}
<h5 class="col-12 text-center mb-5 header-desc">{{ .Description }}</h5>
{{ end }}
{{ range Groups true }}
{{if ne (len .Services) 0}}
<div class="col-12 full-col-12">
<h4 class="group_header">{{.Name}}</h4>
<div class="list-group online_list mb-3">
{{ range VisibleGroupServices . }}
<a href="#" class="service_li list-group-item list-group-item-action {{if not .Online}}bg-danger text-white{{ end }}" data-id="{{.Id}}">
{{ .Name }}
{{if .Online}}
<span class="badge bg-success float-right pulse-glow">ONLINE</span>
{{ else }}
<span class="badge bg-white text-black-50 float-right pulse">OFFLINE</span>
{{end}}
</a>
{{ end }}
</div>
</div>
{{ end }}
{{end}}
{{ if .Messages }}
<div class="col-12">
{{range .Messages}}
<div class="alert alert-primary" role="alert">
<h3>{{.Title}}</h3>
<span class="mb-3">{{safe .Description}}</span>
<div class="d-block mt-2 mb-4">
<span class="float-left small">Starts on {{ToString .StartOn}}</span>
<span class="float-right small">Ends on {{ToString .EndOn}}</span>
</div>
</div>
{{end}}
</div>
{{end}}
<div class="col-12 full-col-12">
{{ if not .Services }}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">No Services to Monitor!</h4>
<p>Your Statping Status Page is working correctly, but you don't have any services to monitor. Go to the <a href="dashboard">Dashboard</a> and add a website to begin really using your status page!</p>
<hr>
<p class="mb-0">If this is a bug, please make an issue in the Statping Github Repo. <a href="https://github.com/hunterlong/statping" class="btn btn-sm btn-outline-danger float-right">Statping Github Repo</a></p>
</div>
{{end}}
{{ range VisibleServices }}
{{$avgTime := .AvgTime}}
<div class="mb-4" id="service_id_{{.Id}}">
<div class="card">
<div class="card-body">
<div class="col-12">
<h4 class="mt-3"><a href="service/{{ServiceLink .}}"{{if not .Online}} class="text-danger"{{end}}>{{ .Name }}</a>
{{if .Online}}
<span class="badge bg-success float-right">ONLINE</span>
{{ else }}
<span class="badge bg-danger float-right pulse">OFFLINE</span>
{{end}}</h4>
<div class="row stats_area mt-5">
<div class="col-4">
<span class="lg_number">{{$avgTime}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{.OnlineDaysPercent 1}}%</span>
Uptime last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{.OnlineDaysPercent 7}}%</span>
Uptime last 7 Days
</div>
</div>
</div>
</div>
{{ if $avgTime }}
<div class="chart-container">
<div id="service_{{ .Id }}"></div>
</div>
{{ end }}
<div class="row lower_canvas full-col-12 text-white{{if not .Online}} bg-danger{{end}}">
<div class="col-10 text-truncate">
<span class="d-none d-md-inline">{{.SmallText}}</span>
</div>
<div class="col-sm-12 col-md-2">
<a href="service/{{ServiceLink .}}" class="btn {{if .Online}}btn-success{{else}}btn-danger{{end}} btn-sm float-right dyn-dark btn-block">View Service</a>
</div>
</div>
</div>
</div>
{{ end }}
</div>
</div>
{{end}}
{{define "extra_scripts"}}
<script src="charts.js"></script>
{{end}}

View File

@ -1,90 +0,0 @@
{{define "title"}}Statping | {{.Integrator.Name}} Integration{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{if Auth}}
{{template "nav"}}
{{end}}
{{$i := .Integrator}}
<div class="col-12">
<h3 class="mb-2 text-muted">{{$i.Name}} Integration</h3>
<p>{{safe $i.Description}}</p>
{{if .Error}}
<div class="alert alert-danger" role="alert">
{{.Error}}
</div>
{{else}}
<table id="integrator_table" class="table table-striped">
<thead>
<tr>
<th scope="col"><input name="all" type="checkbox" checked></th></th>
<th scope="col">Service Name</th>
<th scope="col">Endpoint</th>
<th scope="col">Port</th>
<th scope="col">Type</th>
<th scope="col">Interval</th>
<th scope="col">Timeout</th>
</tr>
</thead>
<tbody id="integrator_services">
{{range .Services}}
<tr id="{{underscore .Name}}">
<th scope="row"><input name="add" type="checkbox" checked></th>
<th><div class="input-group-sm"><input type="text" class="form-control" name="name" value="{{.Name}}"></div></th>
<th><div style="width: 80pt;" class="input-group-sm"><input type="text" class="form-control" name="domain" value="{{.Domain}}"></div></th>
<th><div style="width: 55pt;" class="input-group-sm"><input type="number" class="form-control" name="port" value="{{.Port}}"></div></th>
<th><div style="width: 32pt;" class="input-group-sm"><input type="text" class="form-control" name="type" value="{{.Type}}"></div></th>
<th><div style="width: 40pt;" class="input-group-sm"><input type="text" class="form-control" name="check_interval" value="{{.Interval}}"></div></th>
<th><div style="width: 40pt;" class="input-group-sm"><input type="text" class="form-control" name="timeout" value="{{.Timeout}}"></div></th>
</tr>
{{end}}
</tbody>
</table>
<div id="imported_area"></div>
<button class="btn btn-block btn-primary add_integration_services mb-5" data-id="{{.Integrator.ShortName}}">Add Services</button>
{{end}}
</div>
</div>
{{end}}
{{define "extra_scripts"}}
<script>
$('.add_integration_services').on('click', function(e) {
var table = $(`#integrator_services`);
table.find('tr').each(function() {
var t = $(this).find('input');
var eachService = t.serializeArray();
let newArr = {};
var add = false;
eachService.forEach(function(k, v) {
if (k.value === "on" && k.name === "add") {
add = true
}
if($.isNumeric(k.value)){
k.value = parseInt(k.value)
}
if (add && k.name !== "add") {
newArr[k.name] = k.value;
}
});
let sendData = JSON.stringify(newArr);
$.ajax({
url: "/api/services",
type: "POST",
data: sendData,
success: function (data) {
var box = `<div class="alert alert-success" role="alert">Service '${data.output.name}' Added <a href="service/${data.output.id}" class="badge badge-secondary mt-1 float-right">View</a></div>`;
$("#imported_area").append(box);
}
});
});
});
</script>
{{end}}

View File

@ -1,34 +0,0 @@
{{define "title"}}Statping Login{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-10 offset-1 col-md-8 offset-md-2 mt-md-2">
<div class="col-12 col-md-8 offset-md-2 mb-4">
<img class="col-12 mt-5 mt-md-0" src="banner.png">
</div>
{{ if .Error }}
<div class="alert alert-danger" role="alert">
Incorrect login information submitted, try again.
</div>
{{ end }}
<form action="dashboard" class="spin_form" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" 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">Password</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control" id="password" placeholder="Password">
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block mb-3">Sign in</button>
</div>
</div>
</form>
</div>
</div>
{{end}}

View File

@ -1,20 +0,0 @@
{{define "title"}}Statping | Logs{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{if Auth}}
{{template "nav"}}
{{end}}
<div class="col-12">
<textarea id="live_logs" class="form-control" rows="40" readonly>{{range .}}{{.}}{{end}}</textarea>
</div>
</div>
{{end}}
{{define "extra_css"}}
<style>
@media (max-width: 767px) {
#live_logs {
font-size: 6pt;
}
}
</style>
{{end}}

View File

@ -1,27 +0,0 @@
{{define "title"}}Statping | {{.Title}}{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{template "nav"}}
<div class="col-12">
<h3>{{.Title}}</h3>
{{template "form_message" .}}
</div>
</div>
{{end}}
{{define "extra_css"}}
<link rel="stylesheet" href="css/flatpickr.min.css">
{{end}}
{{define "extra_scripts"}}
<script src="js/flatpickr.js"></script>
<script src="js/rangePlugin.js"></script>
<script>
$(document).ready(function() {
$("#start_on").flatpickr({
enableTime: true,
dateFormat: "Z",
minDate: "today",
"plugins": [new rangePlugin({ input: "#end_on"})]
});
});
</script>
{{end}}

View File

@ -1,59 +0,0 @@
{{define "title"}}Statping Messages{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{template "nav"}}
{{if .}}
<div class="col-12">
<h1 class="text-black-50">Messages</h1>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col" class="d-none d-md-table-cell">Service</th>
<th scope="col" class="d-none d-md-table-cell">Begins</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{{range .}}
<tr id="message_{{.Id}}">
<td>{{.Title}}</td>
<td class="d-none d-md-table-cell">{{if .Service}}<a href="service/{{.Service.Id}}">{{.Service.Name}}</a>{{end}}</td>
<td class="d-none d-md-table-cell">{{ToString .StartOn}}</td>
<td class="text-right">
{{if Auth}}<div class="btn-group">
<a href="message/{{.Id}}" class="btn btn-outline-secondary"><i class="fas fa-exclamation-triangle"></i> Edit</a>
<a href="api/messages/{{.Id}}" class="ajax_delete btn btn-danger" data-method="DELETE" data-obj="message_{{.Id}}" data-id="{{.Id}}"><i class="fas fa-times"></i></a>
</div>{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{if Auth}}
<div class="col-12">
<h1 class="text-black-50 mt-5">Create Message</h1>
{{template "form_message" NewMessage}}
</div>
{{end}}
</div>
{{end}}
{{define "extra_css"}}
<link rel="stylesheet" href="css/flatpickr.min.css">
{{end}}
{{define "extra_scripts"}}
<script src="js/flatpickr.js"></script>
<script src="js/rangePlugin.js"></script>
<script>
$(document).ready(function() {
$("#start_on").flatpickr({
enableTime: true,
dateFormat: "Z",
minDate: "today",
"plugins": [new rangePlugin({ input: "#end_on"})]
});
});
</script>
{{end}}

View File

@ -1,40 +0,0 @@
{{define "nav"}}
{{$isAdmin := Auth}}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="">Statping</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item{{ if eq URL "dashboard" }} active{{ end }}">
<a class="nav-link" href="dashboard">Dashboard</a>
</li>
<li class="nav-item{{ if eq URL "services" }} active{{ end }}">
<a class="nav-link" href="services">Services</a>
</li>
<li class="nav-item{{ if eq URL "users" }} active{{ end }}">
<a class="nav-link" href="users">Users</a>
</li>
<li class="nav-item{{ if eq URL "messages" }} active{{ end }}">
<a class="nav-link" href="messages">Messages</a>
</li>
{{ if $isAdmin }}
<li class="nav-item{{ if eq URL "settings" }} active{{ end }}">
<a class="nav-link" href="settings">Settings</a>
</li>
<li class="nav-item{{ if eq URL "logs" }} active{{ end }}">
<a class="nav-link" href="logs">Logs</a>
</li>
{{end}}
<li class="nav-item{{ if eq URL "help" }} active{{ end }}">
<a class="nav-link" href="help">Help</a>
</li>
</ul>
<span class="navbar-text">
<a class="nav-link" href="logout">Logout</a>
</span>
</div>
</nav>
{{end}}

View File

@ -1,14 +0,0 @@
{{define "scripts"}}
{{if USE_CDN}}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script src="https://assets.statping.com/main.js"></script>
{{ else }}
<script src="js/jquery-3.3.1.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/apexcharts.min.js"></script>
<script src="js/main.js"></script>
{{end}}
{{block "extra_scripts" .}} {{end}}
{{end}}

View File

@ -1,395 +0,0 @@
{{define "title"}}{{.Service.Name}} Status{{end}}
{{define "description"}}{{$s := .Service}}{{if $s.Online }}{{.Service.Name}} is currently online and responding within {{$s.AvgTime}} milliseconds with {{$s.TotalUptime}}% total uptime on {{$s.Domain}}.{{else}}{{.Service.Name}} is currently offline on {{$s.Domain}}. Notify the admin to let them know their service is offline.{{end}}{{end}}
{{ define "content" }}
{{$s := .Service}}
{{$failures := $s.LimitedFailures 16}}
{{$incidents := $s.Incidents}}
{{$checkinFailures := $s.LimitedCheckinFailures 16}}
{{$isAdmin := Auth}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{if IsUser}}
{{template "nav"}}
{{end}}
<div class="col-12 mb-4">
{{if $s.Online }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
{{ else }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
{{end}}
<h4 class="mt-2"><a href="">{{CoreApp.Name}}</a> - {{ $s.Name }}
{{if $s.Online }}
<span class="badge bg-success float-right d-none d-md-block">ONLINE</span>
{{ else }}
<span class="badge bg-danger float-right d-none d-md-block">OFFLINE</span>
{{end}}</h4>
<div class="row stats_area mt-5 mb-5">
<div class="col-4">
<span class="lg_number">{{$s.OnlineDaysPercent 1}}%</span>
Online last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{$s.AvgTime}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{$s.TotalUptime}}%</span>
Total Uptime
</div>
</div>
{{if $s.ActiveMessages}}
<div class="col-12 mb-5">
{{range $s.ActiveMessages}}
<div class="alert alert-warning" role="alert">
<h3>{{.Title}}</h3>
<span class="mb-3">{{safe .Description}}</span>
<div class="d-block mt-2 mb-4">
<span class="float-left small">Starts at {{.StartOn}}</span>
<span class="float-right small">Ends on {{.EndOn}}</span>
</div>
</div>
{{end}}
</div>
{{end}}
<div class="service-chart-container">
<div id="service"></div>
<div id="service-bar"></div>
</div>
<div class="service-chart-heatmap">
<div id="service_heatmap"></div>
</div>
<form id="service_date_form" class="col-12 mt-2 mb-3">
<input type="text" class="d-none" name="start" id="service_start" data-input>
<span data-toggle title="toggle" id="start_date" class="text-muted small float-left pointer mt-2">{{.Start}} to {{.End}}</span>
<button type="submit" class="btn btn-light btn-sm mt-2">Set Timeframe</button>
<input type="text" class="d-none" name="end" id="service_end" data-input>
<div id="start_container"></div>
<div id="end_container"></div>
</form>
{{if not $s.Online}}
<div class="col-12 small text-center mt-3 text-muted">{{$s.DowntimeText}}</div>
{{end}}
{{if IsUser}}
<nav class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs" role="serviceLists">
{{if $isAdmin}}<a class="flex-sm-fill text-sm-center nav-link active" id="edit-tab" data-toggle="tab" href="#edit" role="tab" aria-controls="edit" aria-selected="false">Edit Service</a>{{end}}
<a class="flex-sm-fill text-sm-center nav-link{{ if not $failures }} disabled{{end}}" id="failures-tab" data-toggle="tab" href="#failures" role="tab" aria-controls="failures" aria-selected="true">Failures</a>
<a class="flex-sm-fill text-sm-center nav-link{{ if not $incidents }} disabled{{end}}" id="incidents-tab" data-toggle="tab" href="#incidents" role="tab" aria-controls="incidents" aria-selected="true">Incidents</a>
{{if $isAdmin}}<a class="flex-sm-fill text-sm-center nav-link" id="checkins-tab" data-toggle="tab" href="#checkins" role="tab" aria-controls="checkins" aria-selected="false">Checkins</a>{{end}}
<a class="flex-sm-fill text-sm-center nav-link{{if not $isAdmin}} active{{end}}" id="response-tab" data-toggle="tab" href="#response" role="tab" aria-controls="response" aria-selected="false">Response</a>
</nav>
<div class="tab-content" id="myTabContent">
{{if $isAdmin}}
<div class="tab-pane fade" id="failures" role="serviceLists" aria-labelledby="failures-tab">
{{ if $failures }}
<div class="list-group mt-3 mb-4">
{{ range $failures }}
<a href="#" class="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">{{.ParseError}}</h5>
<small>{{.Ago}}</small>
</div>
<p class="mb-1">{{.Issue}}</p>
</a>
{{ end }}
</div>
{{ end }}
</div>
{{end}}
<div class="tab-pane fade" id="incidents" role="serviceLists" aria-labelledby="incidents-tab">
{{ if $incidents }}
<div class="list-group mt-3 mb-4">
{{ range $incidents }}
<div class="list-group-item flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{.Title}}</h5>
<small>{{.CreatedAt}}</small>
</div>
<p class="mb-1">{{.Description}}</p>
<ul class="list-group mt-3">
{{ range .AllUpdates }}
<li class="list-group-item">
<p>
<span class="badge badge-primary">{{.Type}}</span>
<span class="float-right">
{{.Message}}
<p class="text-muted text-right small">{{.CreatedAt}}</p>
</span>
</p>
</li>
{{end}}
</ul>
</div>
{{ end }}
</div>
{{ end }}
</div>
{{if $isAdmin}}
<div class="tab-pane fade" id="checkins" role="serviceLists" aria-labelledby="checkins-tab">
{{if $s.AllCheckins}}
<table class="table">
<thead>
<tr>
<th scope="col">Checkin</th>
<th scope="col">Report Period<br>Grace Period</th>
<th scope="col">Last Seen</th>
<th scope="col">Expected</th>
<th scope="col"></th>
</tr>
</thead>
<tbody style="font-size: 10pt;">
{{range $s.AllCheckins}}
{{ $ch := . }}
<tr id="checkin_{{$ch.Id}}" class="{{ if lt $ch.Expected 0}}bg-warning text-black{{else}}bg-light{{end}}">
<td>{{$ch.Name}}<br><a href="{{$ch.Link}}" target="_blank">{{$ch.Link}}</a></td>
<td>every {{Duration $ch.Period}}<br>after {{Duration $ch.Grace}}</td>
<td>{{ if $ch.Last.CreatedAt.IsZero}}
Never
{{else}}
{{Ago $ch.Last.CreatedAt}}
{{end}}
</td>
<td>
{{ if $ch.Last.CreatedAt.IsZero}}
-
{{else}}
{{ if lt $ch.Expected 0}}{{Duration $ch.Expected}} ago{{else}}in {{Duration $ch.Expected}}{{end}}
{{end}}
</td>
<td><a href="api/checkin/{{$ch.ApiKey}}" data-method="DELETE" data-obj="checkin_{{$ch.Id}}" data-id="{{$ch.Id}}" class="ajax_delete btn btn-sm btn-danger">Delete</a></td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
{{if $isAdmin}}
{{template "form_checkin" $s}}
{{end}}
{{ if $checkinFailures }}
<div class="list-group mt-3 mb-4">
{{ range $checkinFailures }}
<a href="#" class="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">{{.ParseError}}</h5>
<small>{{.Ago}}</small>
</div>
<p class="mb-1">{{.Issue}}</p>
</a>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
<div class="tab-pane fade{{if not $isAdmin}} show active{{end}}" id="response" role="serviceLists" aria-labelledby="response-tab">
<div class="col-12 mt-4{{if ne $s.Type "http"}} d-none{{end}}">
<h3>Last Response</h3>
<textarea rows="8" class="form-control" readonly>{{ $s.LastResponse }}</textarea>
<div class="form-group row mt-2">
<label for="last_status_code" class="col-sm-3 col-form-label">HTTP Status Code</label>
<div class="col-sm-2">
<input type="text" id="last_status_code" class="form-control" value="{{ $s.LastStatusCode }}" readonly>
</div>
</div>
</div>
</div>
{{if $isAdmin}}
<div class="tab-pane fade show active" id="edit" role="serviceLists" aria-labelledby="edit-tab">
{{template "form_service" $s}}
</div>
{{end}}
</div>
{{else}}
{{if $s.Public.Bool }}
{{ if $failures }}
<div class="list-group mt-3 mb-4">
{{ range $failures }}
<a href="#" class="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">{{.ParseError}}</h5>
<small>{{.Ago}}</small>
</div>
<p class="mb-1">{{.Issue}}</p>
</a>
{{ end }}
</div>
{{ end }}
{{end}}
{{end}}
</div>
</div>
{{end}}
{{define "extra_css"}}
<link rel="stylesheet" href="css/flatpickr.min.css">
{{end}}
{{define "extra_scripts"}}
{{$s := .Service}}
<script src="js/flatpickr.js"></script>
<script src="js/rangePlugin.js"></script>
<script>
let options = {
chart: {
height: "100%",
width: "100%",
type: "area",
animations: {
enabled: false,
initialAnimation: {
enabled: false
}
},
},
fill: {
colors: ["#48d338"],
opacity: 1,
type: 'solid'
},
stroke: {
show: true,
curve: 'smooth',
lineCap: 'butt',
colors: ["#3aa82d"],
},
series: [{name: "Latency", data: [{}]}],
tooltip: {
enabled: true,
x: {show: true, format: 'MMM dd hh:mm:ss tt'},
},
xaxis: {
type: "datetime",
tickAmount: 8,
},
yaxis: {
labels: {
formatter: (value) => {
return (value).toFixed(0) + "ms"
},
},
},
dataLabels: {
enabled: false
},
};
var heat_options = {
chart: {
height: "100%",
width: "100%",
type: 'heatmap',
toolbar: {
show: false
}
},
dataLabels: {
enabled: false,
},
enableShades: true,
shadeIntensity: 0.5,
colors: ["#d53a3b"],
series: [{data: [{}]}],
yaxis: {
labels: {
formatter: (value) => {
return value
},
},
},
tooltip: {
enabled: true,
x: {
show: false,
},
y: {
formatter: function(val, opts) { return val+" Failures" },
title: {
formatter: (seriesName) => seriesName,
},
},
}
};
async function zoomedEvent(chart, { xaxis, yaxis }) {
let start = Math.round(xaxis.min / 1000),
end = Math.round(xaxis.max / 1000);
let chartData = await ChartLatency({{$s.Id}}, start, end);
if (!chartData) {
chartData = await ChartLatency({{$s.Id}}, start, end, "minute");
}
if(!chartData || !chartData.length) {
return false
}
chart.updateSeries([{ name: "Latency", data: chartData }]);
}
async function RenderHeatmap() {
let heatChart = new ApexCharts(
document.querySelector("#service_heatmap"),
heat_options
);
let dataArr = [];
let heatmapData = await ChartHeatmap({{$s.Id}});
heatmapData.forEach(function(d) {
var date = new Date(d.date);
dataArr.push({name: date.toLocaleString('en-us', { month: 'long' }), data: d.data});
});
heatChart.render();
heatChart.updateSeries(dataArr);
}
async function RenderChartLatency() {
options.chart.events = {
zoomed: zoomedEvent,
}
options.fill.colors = {{if $s.Online}}["#48d338"]{{else}}["#dd3545"]{{end}};
options.stroke.colors = {{if $s.Online}}["#3aa82d"]{{else}}["#c23342"]{{end}};
let chart = new ApexCharts(document.querySelector("#service"), options);
await RenderChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
}
$(document).ready(async function() {
let startDate = $("#service_start").flatpickr({
enableTime: false,
static: true,
altInput: true,
altFormat: "U",
maxDate: "today",
dateFormat: "F j, Y",
onChange: function(selectedDates, dateStr, instance) {
var one = Math.round((new Date(selectedDates[0])).getTime() / 1000);
var two = Math.round((new Date(selectedDates[1])).getTime() / 1000);
$("#service_start").val(one);
$("#service_end").val(two);
$("#start_date").html(dateStr);
},
"plugins": [new rangePlugin({ input: "#service_end"})]
});
$("#start_date").click(function(e) {
startDate.open()
});
await RenderChartLatency();
await RenderHeatmap();
});
</script>
{{end}}

View File

@ -1,9 +0,0 @@
{{define "title"}}Statping | Create Service{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{template "nav"}}
<div class="col-12">
{{template "form_service" NewService}}
</div>
</div>
{{end}}

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