mirror of https://github.com/statping/statping
parent
b80c61cd04
commit
fd7925aa7e
|
@ -161,66 +161,6 @@ func (u *Message) AfterFind() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate for Hit will set CreatedAt to UTC
|
|
||||||
func (h *Hit) BeforeCreate() (err error) {
|
|
||||||
if h.CreatedAt.IsZero() {
|
|
||||||
h.CreatedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeCreate for Failure will set CreatedAt to UTC
|
|
||||||
func (f *Failure) BeforeCreate() (err error) {
|
|
||||||
if f.CreatedAt.IsZero() {
|
|
||||||
f.CreatedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeCreate for User will set CreatedAt to UTC
|
|
||||||
func (u *User) BeforeCreate() (err error) {
|
|
||||||
if u.CreatedAt.IsZero() {
|
|
||||||
u.CreatedAt = time.Now().UTC()
|
|
||||||
u.UpdatedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeCreate for Message will set CreatedAt to UTC
|
|
||||||
func (u *Message) BeforeCreate() (err error) {
|
|
||||||
if u.CreatedAt.IsZero() {
|
|
||||||
u.CreatedAt = time.Now().UTC()
|
|
||||||
u.UpdatedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeCreate for Service will set CreatedAt to UTC
|
|
||||||
func (s *Service) BeforeCreate() (err error) {
|
|
||||||
if s.CreatedAt.IsZero() {
|
|
||||||
s.CreatedAt = time.Now().UTC()
|
|
||||||
s.UpdatedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeCreate for Checkin will set CreatedAt to UTC
|
|
||||||
func (c *Checkin) BeforeCreate() (err error) {
|
|
||||||
if c.CreatedAt.IsZero() {
|
|
||||||
c.CreatedAt = time.Now().UTC()
|
|
||||||
c.UpdatedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeCreate for checkinHit will set CreatedAt to UTC
|
|
||||||
func (c *CheckinHit) BeforeCreate() (err error) {
|
|
||||||
if c.CreatedAt.IsZero() {
|
|
||||||
c.CreatedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertCore create the single row for the Core settings in Statping
|
// InsertCore create the single row for the Core settings in Statping
|
||||||
func (db *DbConfig) InsertCore() (*Core, error) {
|
func (db *DbConfig) InsertCore() (*Core, error) {
|
||||||
CoreApp = &Core{Core: &types.Core{
|
CoreApp = &Core{Core: &types.Core{
|
||||||
|
|
|
@ -31,6 +31,7 @@ type Failure struct {
|
||||||
|
|
||||||
const (
|
const (
|
||||||
limitedFailures = 32
|
limitedFailures = 32
|
||||||
|
limitedHits = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateFailure will create a new Failure record for a service
|
// CreateFailure will create a new Failure record for a service
|
||||||
|
|
14
core/hits.go
14
core/hits.go
|
@ -52,9 +52,9 @@ func (s *Service) Hits() ([]*types.Hit, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LimitedHits returns the last 1024 successful/online 'hit' records for a service
|
// LimitedHits returns the last 1024 successful/online 'hit' records for a service
|
||||||
func (s *Service) LimitedHits() ([]*types.Hit, error) {
|
func (s *Service) LimitedHits(amount int64) ([]*types.Hit, error) {
|
||||||
var hits []*types.Hit
|
var hits []*types.Hit
|
||||||
col := hitsDB().Where("service = ?", s.Id).Order("id desc").Limit(1024)
|
col := hitsDB().Where("service = ?", s.Id).Order("id desc").Limit(amount)
|
||||||
err := col.Find(&hits)
|
err := col.Find(&hits)
|
||||||
return reverseHits(hits), err.Error
|
return reverseHits(hits), err.Error
|
||||||
}
|
}
|
||||||
|
@ -70,17 +70,15 @@ func reverseHits(input []*types.Hit) []*types.Hit {
|
||||||
// TotalHits returns the total amount of successful hits a service has
|
// TotalHits returns the total amount of successful hits a service has
|
||||||
func (s *Service) TotalHits() (uint64, error) {
|
func (s *Service) TotalHits() (uint64, error) {
|
||||||
var count uint64
|
var count uint64
|
||||||
col := hitsDB().Where("service = ?", s.Id)
|
col := hitsDB().Where("service = ?", s.Id).Count(&count)
|
||||||
err := col.Count(&count)
|
return count, col.Error
|
||||||
return count, err.Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TotalHitsSince returns the total amount of hits based on a specific time/date
|
// TotalHitsSince returns the total amount of hits based on a specific time/date
|
||||||
func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
|
func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
|
||||||
var count uint64
|
var count uint64
|
||||||
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05"))
|
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Count(&count)
|
||||||
err := rows.Count(&count)
|
return count, rows.Error
|
||||||
return count, err.Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum returns the added value Latency for all of the services successful hits.
|
// Sum returns the added value Latency for all of the services successful hits.
|
||||||
|
|
|
@ -28,7 +28,7 @@ func InsertSampleData() error {
|
||||||
utils.Log(1, "Inserting Sample Data...")
|
utils.Log(1, "Inserting Sample Data...")
|
||||||
|
|
||||||
insertSampleGroups()
|
insertSampleGroups()
|
||||||
createdOn := time.Now().Add((-24 * 90) * time.Hour).UTC()
|
createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC()
|
||||||
s1 := ReturnService(&types.Service{
|
s1 := ReturnService(&types.Service{
|
||||||
Name: "Google",
|
Name: "Google",
|
||||||
Domain: "https://google.com",
|
Domain: "https://google.com",
|
||||||
|
|
|
@ -92,7 +92,7 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
|
||||||
checkins := service.AllCheckins()
|
checkins := service.AllCheckins()
|
||||||
for _, c := range checkins {
|
for _, c := range checkins {
|
||||||
c.Failures = c.LimitedFailures(limitedFailures)
|
c.Failures = c.LimitedFailures(limitedFailures)
|
||||||
c.Hits = c.LimitedHits(limitedFailures)
|
c.Hits = c.LimitedHits(limitedHits)
|
||||||
service.Checkins = append(service.Checkins, c)
|
service.Checkins = append(service.Checkins, c)
|
||||||
}
|
}
|
||||||
CoreApp.Services = append(CoreApp.Services, service)
|
CoreApp.Services = append(CoreApp.Services, service)
|
||||||
|
@ -113,16 +113,20 @@ func (s *Service) ToJSON() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AvgTime will return the average amount of time for a service to response back successfully
|
// AvgTime will return the average amount of time for a service to response back successfully
|
||||||
func (s *Service) AvgTime() float64 {
|
func (s *Service) AvgTime() string {
|
||||||
total, _ := s.TotalHits()
|
total, _ := s.TotalHits()
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return float64(0)
|
return "0"
|
||||||
}
|
}
|
||||||
sum := s.Sum()
|
sum := s.Sum()
|
||||||
avg := sum / float64(total) * 100
|
avg := sum / float64(total) * 100
|
||||||
amount := fmt.Sprintf("%0.0f", avg*10)
|
return fmt.Sprintf("%0.0f", avg*10)
|
||||||
val, _ := strconv.ParseFloat(amount, 10)
|
}
|
||||||
return val
|
|
||||||
|
// OnlineDaysPercent returns the service's uptime percent within last 24 hours
|
||||||
|
func (s *Service) OnlineDaysPercent(days int) float32 {
|
||||||
|
ago := time.Now().Add((-24 * time.Duration(days)) * time.Hour)
|
||||||
|
return s.OnlineSince(ago)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Online24 returns the service's uptime percent within last 24 hours
|
// Online24 returns the service's uptime percent within last 24 hours
|
||||||
|
@ -185,7 +189,7 @@ func (s *Service) lastFailure() *Failure {
|
||||||
// // Online since Monday 3:04:05PM, Jan _2 2006
|
// // Online since Monday 3:04:05PM, Jan _2 2006
|
||||||
func (s *Service) SmallText() string {
|
func (s *Service) SmallText() string {
|
||||||
last := s.LimitedFailures(1)
|
last := s.LimitedFailures(1)
|
||||||
hits, _ := s.LimitedHits()
|
hits, _ := s.LimitedHits(1)
|
||||||
zone := CoreApp.Timezone
|
zone := CoreApp.Timezone
|
||||||
if s.Online {
|
if s.Online {
|
||||||
if len(last) == 0 {
|
if len(last) == 0 {
|
||||||
|
@ -255,19 +259,14 @@ func (s *Service) Downtime() time.Duration {
|
||||||
|
|
||||||
// GraphDataRaw will return all the hits between 2 times for a Service
|
// 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 {
|
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
|
||||||
var d []DateScan
|
var data []DateScan
|
||||||
var amount int64
|
outgoing := new(DateScanObj)
|
||||||
model := service.(*Service).HitsBetween(start, end, group, column)
|
model := service.(*Service).HitsBetween(start, end, group, column)
|
||||||
model.Count(&amount)
|
|
||||||
if amount == 0 {
|
|
||||||
return &DateScanObj{[]DateScan{}}
|
|
||||||
}
|
|
||||||
model = model.Order("timeframe asc", false).Group("timeframe")
|
model = model.Order("timeframe asc", false).Group("timeframe")
|
||||||
rows, err := model.Rows()
|
rows, err := model.Rows()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err))
|
utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var gd DateScan
|
var gd DateScan
|
||||||
var createdAt string
|
var createdAt string
|
||||||
|
@ -285,9 +284,10 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st
|
||||||
}
|
}
|
||||||
gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.CHART_TIME)
|
gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.CHART_TIME)
|
||||||
gd.Value = int64(value * 1000)
|
gd.Value = int64(value * 1000)
|
||||||
d = append(d, gd)
|
data = append(data, gd)
|
||||||
}
|
}
|
||||||
return &DateScanObj{d}
|
outgoing.Array = data
|
||||||
|
return outgoing
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToString will convert the DateScanObj into a JSON string for the charts to render
|
// ToString will convert the DateScanObj into a JSON string for the charts to render
|
||||||
|
|
|
@ -156,7 +156,7 @@ func TestServiceHits(t *testing.T) {
|
||||||
|
|
||||||
func TestServiceLimitedHits(t *testing.T) {
|
func TestServiceLimitedHits(t *testing.T) {
|
||||||
service := SelectService(5)
|
service := SelectService(5)
|
||||||
hits, err := service.LimitedHits()
|
hits, err := service.LimitedHits(1024)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, int(1024), len(hits))
|
assert.Equal(t, int(1024), len(hits))
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,15 @@ func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/settings", http.StatusSeeOther)
|
http.Redirect(w, r, "/settings", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !IsFullAuthenticated(r) {
|
||||||
|
sendUnauthorizedJson(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
CacheStorage = NewStorage()
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) {
|
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) {
|
||||||
utils.Log(2, fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
|
utils.Log(2, fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
|
||||||
output := apiResponse{
|
output := apiResponse{
|
||||||
|
|
|
@ -46,9 +46,6 @@ func NewStorage() *Storage {
|
||||||
|
|
||||||
//Get a cached content by key
|
//Get a cached content by key
|
||||||
func (s Storage) Get(key string) []byte {
|
func (s Storage) Get(key string) []byte {
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
item := s.items[key]
|
item := s.items[key]
|
||||||
if item.Expired() {
|
if item.Expired() {
|
||||||
CacheStorage.Delete(key)
|
CacheStorage.Delete(key)
|
||||||
|
@ -93,10 +90,10 @@ func cached(duration, contentType string, handler func(w http.ResponseWriter, r
|
||||||
w.Write(content)
|
w.Write(content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if d, err := time.ParseDuration(duration); err == nil {
|
|
||||||
CacheStorage.Set(r.RequestURI, content, d)
|
|
||||||
}
|
|
||||||
w.Write(content)
|
w.Write(content)
|
||||||
|
if d, err := time.ParseDuration(duration); err == nil {
|
||||||
|
go CacheStorage.Set(r.RequestURI, content, d)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/hunterlong/statping/core"
|
||||||
|
"github.com/hunterlong/statping/types"
|
||||||
|
"github.com/hunterlong/statping/utils"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap {
|
||||||
|
return template.FuncMap{
|
||||||
|
"js": func(html interface{}) template.JS {
|
||||||
|
return template.JS(utils.ToString(html))
|
||||||
|
},
|
||||||
|
"safe": func(html string) template.HTML {
|
||||||
|
return template.HTML(html)
|
||||||
|
},
|
||||||
|
"safeURL": func(u string) template.URL {
|
||||||
|
return template.URL(u)
|
||||||
|
},
|
||||||
|
"Auth": func() bool {
|
||||||
|
return IsFullAuthenticated(r)
|
||||||
|
},
|
||||||
|
"IsUser": func() bool {
|
||||||
|
return IsUser(r)
|
||||||
|
},
|
||||||
|
"VERSION": func() string {
|
||||||
|
return core.VERSION
|
||||||
|
},
|
||||||
|
"CoreApp": func() *core.Core {
|
||||||
|
return core.CoreApp
|
||||||
|
},
|
||||||
|
"Services": func() []types.ServiceInterface {
|
||||||
|
return core.CoreApp.Services
|
||||||
|
},
|
||||||
|
"Groups": func(includeAll bool) []*core.Group {
|
||||||
|
auth := IsUser(r)
|
||||||
|
return core.SelectGroups(includeAll, auth)
|
||||||
|
},
|
||||||
|
"len": func(g interface{}) int {
|
||||||
|
val := reflect.ValueOf(g)
|
||||||
|
return val.Len()
|
||||||
|
},
|
||||||
|
"IsNil": func(g interface{}) bool {
|
||||||
|
return g == nil
|
||||||
|
},
|
||||||
|
"USE_CDN": func() bool {
|
||||||
|
return core.CoreApp.UseCdn.Bool
|
||||||
|
},
|
||||||
|
"QrAuth": func() string {
|
||||||
|
return fmt.Sprintf("statping://setup?domain=%v&api=%v", core.CoreApp.Domain, core.CoreApp.ApiSecret)
|
||||||
|
},
|
||||||
|
"Type": func(g interface{}) []string {
|
||||||
|
fooType := reflect.TypeOf(g)
|
||||||
|
var methods []string
|
||||||
|
methods = append(methods, fooType.String())
|
||||||
|
for i := 0; i < fooType.NumMethod(); i++ {
|
||||||
|
method := fooType.Method(i)
|
||||||
|
fmt.Println(method.Name)
|
||||||
|
methods = append(methods, method.Name)
|
||||||
|
}
|
||||||
|
return methods
|
||||||
|
},
|
||||||
|
"ToJSON": func(g interface{}) template.HTML {
|
||||||
|
data, _ := json.Marshal(g)
|
||||||
|
return template.HTML(string(data))
|
||||||
|
},
|
||||||
|
"underscore": func(html string) string {
|
||||||
|
return utils.UnderScoreString(html)
|
||||||
|
},
|
||||||
|
"URL": func() string {
|
||||||
|
return r.URL.String()
|
||||||
|
},
|
||||||
|
"CHART_DATA": func() string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
"Error": func() string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
"ToString": func(v interface{}) string {
|
||||||
|
return utils.ToString(v)
|
||||||
|
},
|
||||||
|
"Ago": func(t time.Time) string {
|
||||||
|
return utils.Timestamp(t).Ago()
|
||||||
|
},
|
||||||
|
"Duration": func(t time.Duration) string {
|
||||||
|
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", t.Seconds()))
|
||||||
|
return utils.FormatDuration(duration)
|
||||||
|
},
|
||||||
|
"ToUnix": func(t time.Time) int64 {
|
||||||
|
return t.UTC().Unix()
|
||||||
|
},
|
||||||
|
"FromUnix": func(t int64) string {
|
||||||
|
return utils.Timezoner(time.Unix(t, 0), core.CoreApp.Timezone).Format("Monday, January 02")
|
||||||
|
},
|
||||||
|
"NewService": func() *types.Service {
|
||||||
|
return new(types.Service)
|
||||||
|
},
|
||||||
|
"NewUser": func() *types.User {
|
||||||
|
return new(types.User)
|
||||||
|
},
|
||||||
|
"NewCheckin": func() *types.Checkin {
|
||||||
|
return new(types.Checkin)
|
||||||
|
},
|
||||||
|
"NewMessage": func() *types.Message {
|
||||||
|
return new(types.Message)
|
||||||
|
},
|
||||||
|
"NewGroup": func() *types.Group {
|
||||||
|
return new(types.Group)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,6 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/hunterlong/statping/core"
|
"github.com/hunterlong/statping/core"
|
||||||
|
@ -27,20 +26,23 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cookieKey = "statping_auth"
|
cookieKey = "statping_auth"
|
||||||
timeout = time.Second * 60
|
timeout = time.Second * 30
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sessionStore *sessions.CookieStore
|
sessionStore *sessions.CookieStore
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
usingSSL bool
|
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_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
|
||||||
|
javascripts = []string{"charts.js", "chart_index.js"}
|
||||||
|
mainTemplate *template.Template
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunHTTPServer will start a HTTP server on a specific IP and port
|
// RunHTTPServer will start a HTTP server on a specific IP and port
|
||||||
|
@ -91,6 +93,7 @@ func RunHTTPServer(ip string, port int) error {
|
||||||
IdleTimeout: timeout,
|
IdleTimeout: timeout,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
}
|
}
|
||||||
|
httpServer.SetKeepAlivesEnabled(false)
|
||||||
return httpServer.ListenAndServe()
|
return httpServer.ListenAndServe()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -166,112 +169,37 @@ func IsUser(r *http.Request) bool {
|
||||||
return session.Values["authenticated"].(bool)
|
return session.Values["authenticated"].(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap {
|
func loadTemplate(w http.ResponseWriter, r *http.Request) error {
|
||||||
return template.FuncMap{
|
var err error
|
||||||
"js": func(html interface{}) template.JS {
|
mainTemplate = template.New("main")
|
||||||
return template.JS(utils.ToString(html))
|
mainTemplate.Funcs(handlerFuncs(w, r))
|
||||||
},
|
mainTemplate, err = mainTemplate.Parse(mainTmpl)
|
||||||
"safe": func(html string) template.HTML {
|
if err != nil {
|
||||||
return template.HTML(html)
|
utils.Log(4, err)
|
||||||
},
|
return err
|
||||||
"safeURL": func(u string) template.URL {
|
|
||||||
return template.URL(u)
|
|
||||||
},
|
|
||||||
"Auth": func() bool {
|
|
||||||
return IsFullAuthenticated(r)
|
|
||||||
},
|
|
||||||
"IsUser": func() bool {
|
|
||||||
return IsUser(r)
|
|
||||||
},
|
|
||||||
"VERSION": func() string {
|
|
||||||
return core.VERSION
|
|
||||||
},
|
|
||||||
"CoreApp": func() *core.Core {
|
|
||||||
return core.CoreApp
|
|
||||||
},
|
|
||||||
"Services": func() []types.ServiceInterface {
|
|
||||||
return core.CoreApp.Services
|
|
||||||
},
|
|
||||||
"Groups": func(includeAll bool) []*core.Group {
|
|
||||||
auth := IsUser(r)
|
|
||||||
return core.SelectGroups(includeAll, auth)
|
|
||||||
},
|
|
||||||
"len": func(g interface{}) int {
|
|
||||||
val := reflect.ValueOf(g)
|
|
||||||
return val.Len()
|
|
||||||
},
|
|
||||||
"IsNil": func(g interface{}) bool {
|
|
||||||
return g == nil
|
|
||||||
},
|
|
||||||
"USE_CDN": func() bool {
|
|
||||||
return core.CoreApp.UseCdn.Bool
|
|
||||||
},
|
|
||||||
"QrAuth": func() string {
|
|
||||||
return fmt.Sprintf("statping://setup?domain=%v&api=%v", core.CoreApp.Domain, core.CoreApp.ApiSecret)
|
|
||||||
},
|
|
||||||
"Type": func(g interface{}) []string {
|
|
||||||
fooType := reflect.TypeOf(g)
|
|
||||||
var methods []string
|
|
||||||
methods = append(methods, fooType.String())
|
|
||||||
for i := 0; i < fooType.NumMethod(); i++ {
|
|
||||||
method := fooType.Method(i)
|
|
||||||
fmt.Println(method.Name)
|
|
||||||
methods = append(methods, method.Name)
|
|
||||||
}
|
|
||||||
return methods
|
|
||||||
},
|
|
||||||
"ToJSON": func(g interface{}) template.HTML {
|
|
||||||
data, _ := json.Marshal(g)
|
|
||||||
return template.HTML(string(data))
|
|
||||||
},
|
|
||||||
"underscore": func(html string) string {
|
|
||||||
return utils.UnderScoreString(html)
|
|
||||||
},
|
|
||||||
"URL": func() string {
|
|
||||||
return r.URL.String()
|
|
||||||
},
|
|
||||||
"CHART_DATA": func() string {
|
|
||||||
return ""
|
|
||||||
},
|
|
||||||
"Error": func() string {
|
|
||||||
return ""
|
|
||||||
},
|
|
||||||
"ToString": func(v interface{}) string {
|
|
||||||
return utils.ToString(v)
|
|
||||||
},
|
|
||||||
"Ago": func(t time.Time) string {
|
|
||||||
return utils.Timestamp(t).Ago()
|
|
||||||
},
|
|
||||||
"Duration": func(t time.Duration) string {
|
|
||||||
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", t.Seconds()))
|
|
||||||
return utils.FormatDuration(duration)
|
|
||||||
},
|
|
||||||
"ToUnix": func(t time.Time) int64 {
|
|
||||||
return t.UTC().Unix()
|
|
||||||
},
|
|
||||||
"FromUnix": func(t int64) string {
|
|
||||||
return utils.Timezoner(time.Unix(t, 0), core.CoreApp.Timezone).Format("Monday, January 02")
|
|
||||||
},
|
|
||||||
"NewService": func() *types.Service {
|
|
||||||
return new(types.Service)
|
|
||||||
},
|
|
||||||
"NewUser": func() *types.User {
|
|
||||||
return new(types.User)
|
|
||||||
},
|
|
||||||
"NewCheckin": func() *types.Checkin {
|
|
||||||
return new(types.Checkin)
|
|
||||||
},
|
|
||||||
"NewMessage": func() *types.Message {
|
|
||||||
return new(types.Message)
|
|
||||||
},
|
|
||||||
"NewGroup": func() *types.Group {
|
|
||||||
return new(types.Group)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
// render all templates
|
||||||
|
mainTemplate.Funcs(handlerFuncs(w, r))
|
||||||
|
for _, temp := range templates {
|
||||||
|
tmp, _ := source.TmplBox.String(temp)
|
||||||
|
mainTemplate, err = mainTemplate.Parse(tmp)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log(4, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// render all javascript files
|
||||||
|
for _, temp := range javascripts {
|
||||||
|
tmp, _ := source.JsBox.String(temp)
|
||||||
|
mainTemplate, err = mainTemplate.Parse(tmp)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log(4, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
|
|
||||||
|
|
||||||
// ExecuteResponse will render a HTTP response for the front end user
|
// ExecuteResponse will render a HTTP response for the front end user
|
||||||
func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}, redirect interface{}) {
|
func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}, redirect interface{}) {
|
||||||
utils.Http(r)
|
utils.Http(r)
|
||||||
|
@ -279,53 +207,21 @@ func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data i
|
||||||
http.Redirect(w, r, url, http.StatusSeeOther)
|
http.Redirect(w, r, url, http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if usingSSL {
|
if usingSSL {
|
||||||
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||||
}
|
}
|
||||||
|
loadTemplate(w, r)
|
||||||
templates := []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
|
|
||||||
javascripts := []string{"charts.js", "chart_index.js"}
|
|
||||||
|
|
||||||
render, err := source.TmplBox.String(file)
|
render, err := source.TmplBox.String(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log(4, err)
|
utils.Log(4, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup the main template and handler funcs
|
|
||||||
t := template.New("main")
|
|
||||||
t.Funcs(handlerFuncs(w, r))
|
|
||||||
t, err = t.Parse(mainTmpl)
|
|
||||||
if err != nil {
|
|
||||||
utils.Log(4, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// render all templates
|
|
||||||
for _, temp := range templates {
|
|
||||||
tmp, _ := source.TmplBox.String(temp)
|
|
||||||
t, err = t.Parse(tmp)
|
|
||||||
if err != nil {
|
|
||||||
utils.Log(4, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render all javascript files
|
|
||||||
for _, temp := range javascripts {
|
|
||||||
tmp, _ := source.JsBox.String(temp)
|
|
||||||
t, err = t.Parse(tmp)
|
|
||||||
if err != nil {
|
|
||||||
utils.Log(4, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render the page requested
|
// render the page requested
|
||||||
_, err = t.Parse(render)
|
_, err = mainTemplate.Parse(render)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log(4, err)
|
utils.Log(4, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// execute the template
|
// execute the template
|
||||||
err = t.Execute(w, data)
|
err = mainTemplate.Execute(w, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log(4, err)
|
utils.Log(4, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ func Router() *mux.Router {
|
||||||
dir := utils.Directory
|
dir := utils.Directory
|
||||||
CacheStorage = NewStorage()
|
CacheStorage = NewStorage()
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.Handle("/", cached("120s", "text/html", http.HandlerFunc(indexHandler)))
|
r.Handle("/", cached("60s", "text/html", http.HandlerFunc(indexHandler)))
|
||||||
if source.UsingAssets(dir) {
|
if source.UsingAssets(dir) {
|
||||||
indexHandler := http.FileServer(http.Dir(dir + "/assets/"))
|
indexHandler := http.FileServer(http.Dir(dir + "/assets/"))
|
||||||
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(dir+"/assets/css"))))
|
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(dir+"/assets/css"))))
|
||||||
|
@ -95,6 +95,7 @@ func Router() *mux.Router {
|
||||||
// API Routes
|
// API Routes
|
||||||
r.Handle("/api", http.HandlerFunc(apiIndexHandler))
|
r.Handle("/api", http.HandlerFunc(apiIndexHandler))
|
||||||
r.Handle("/api/renew", http.HandlerFunc(apiRenewHandler))
|
r.Handle("/api/renew", http.HandlerFunc(apiRenewHandler))
|
||||||
|
r.Handle("/api/clear_cache", http.HandlerFunc(apiClearCacheHandler))
|
||||||
|
|
||||||
// API SERVICE Routes
|
// API SERVICE Routes
|
||||||
r.Handle("/api/services", http.HandlerFunc(apiAllServicesHandler)).Methods("GET")
|
r.Handle("/api/services", http.HandlerFunc(apiAllServicesHandler)).Methods("GET")
|
||||||
|
@ -102,8 +103,8 @@ func Router() *mux.Router {
|
||||||
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceHandler)).Methods("GET")
|
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceHandler)).Methods("GET")
|
||||||
r.Handle("/api/reorder", http.HandlerFunc(reorderServiceHandler)).Methods("POST")
|
r.Handle("/api/reorder", http.HandlerFunc(reorderServiceHandler)).Methods("POST")
|
||||||
r.Handle("/api/services/{id}/data", cached("30s", "application/json", http.HandlerFunc(apiServiceDataHandler))).Methods("GET")
|
r.Handle("/api/services/{id}/data", cached("30s", "application/json", http.HandlerFunc(apiServiceDataHandler))).Methods("GET")
|
||||||
r.Handle("/api/services/{id}/ping", http.HandlerFunc(apiServicePingDataHandler)).Methods("GET")
|
r.Handle("/api/services/{id}/ping", cached("30s", "application/json", http.HandlerFunc(apiServicePingDataHandler))).Methods("GET")
|
||||||
r.Handle("/api/services/{id}/heatmap", http.HandlerFunc(apiServiceHeatmapHandler)).Methods("GET")
|
r.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", http.HandlerFunc(apiServiceHeatmapHandler))).Methods("GET")
|
||||||
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceUpdateHandler)).Methods("POST")
|
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceUpdateHandler)).Methods("POST")
|
||||||
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceDeleteHandler)).Methods("DELETE")
|
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceDeleteHandler)).Methods("DELETE")
|
||||||
r.Handle("/api/services/{id}/failures", http.HandlerFunc(apiServiceFailuresHandler)).Methods("GET")
|
r.Handle("/api/services/{id}/failures", http.HandlerFunc(apiServiceFailuresHandler)).Methods("GET")
|
||||||
|
@ -146,6 +147,7 @@ func Router() *mux.Router {
|
||||||
r.Handle("/health", http.HandlerFunc(healthCheckHandler))
|
r.Handle("/health", http.HandlerFunc(healthCheckHandler))
|
||||||
r.Handle("/tray", http.HandlerFunc(trayHandler))
|
r.Handle("/tray", http.HandlerFunc(trayHandler))
|
||||||
r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known"))))
|
r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known"))))
|
||||||
|
|
||||||
r.NotFoundHandler = http.HandlerFunc(error404Handler)
|
r.NotFoundHandler = http.HandlerFunc(error404Handler)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/hunterlong/statping/core"
|
"github.com/hunterlong/statping/core"
|
||||||
"github.com/hunterlong/statping/types"
|
"github.com/hunterlong/statping/types"
|
||||||
|
@ -227,7 +228,7 @@ type dataXy struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type dataXyMonth struct {
|
type dataXyMonth struct {
|
||||||
Date time.Time `json:"date"`
|
Date string `json:"date"`
|
||||||
Data []*dataXy `json:"data"`
|
Data []*dataXy `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,26 +243,37 @@ func apiServiceHeatmapHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var monthOutput []*dataXyMonth
|
var monthOutput []*dataXyMonth
|
||||||
|
|
||||||
start := service.CreatedAt
|
start := service.CreatedAt
|
||||||
|
//now := time.Now()
|
||||||
|
|
||||||
if start.Year() <= 2 {
|
sY, sM, _ := start.Date()
|
||||||
start = service.CreatedAt.Add(time.Duration((-3 * 24) * time.Hour))
|
|
||||||
}
|
|
||||||
|
|
||||||
for y := start; y.Year() == start.Year(); y = y.AddDate(1, 0, 0) {
|
var date time.Time
|
||||||
|
|
||||||
for m := y; m.Month() == y.Month(); m = m.AddDate(0, 1, 0) {
|
month := int(sM)
|
||||||
|
maxMonth := 12
|
||||||
|
|
||||||
|
for year := int(sY); year <= time.Now().Year(); year++ {
|
||||||
|
|
||||||
|
if year == time.Now().Year() {
|
||||||
|
maxMonth = int(time.Now().Month())
|
||||||
|
}
|
||||||
|
|
||||||
|
for m := month; m <= maxMonth; m++ {
|
||||||
|
|
||||||
var output []*dataXy
|
var output []*dataXy
|
||||||
|
|
||||||
for day := 1; day <= 31; day++ {
|
for day := 1; day <= 31; day++ {
|
||||||
date := time.Date(y.Year(), y.Month(), day, 0, 0, 0, 0, time.UTC)
|
date = time.Date(year, time.Month(m), day, 0, 0, 0, 0, time.UTC)
|
||||||
failures, _ := service.TotalFailuresOnDate(date)
|
failures, _ := service.TotalFailuresOnDate(date)
|
||||||
output = append(output, &dataXy{day, int(failures)})
|
output = append(output, &dataXy{day, int(failures)})
|
||||||
}
|
}
|
||||||
|
|
||||||
monthOutput = append(monthOutput, &dataXyMonth{m, output})
|
thisDate := fmt.Sprintf("%v-%v-01 00:00:00", year, m)
|
||||||
|
monthOutput = append(monthOutput, &dataXyMonth{thisDate, output})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
month = 1
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
|
@ -148,6 +148,12 @@ HTML, BODY {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-chart-heatmap {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #3e9bff;
|
background-color: #3e9bff;
|
||||||
border-color: #006fe6;
|
border-color: #006fe6;
|
||||||
|
|
|
@ -148,71 +148,20 @@ let options = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startOn = Math.floor(Date.now() / 1000) - (86400 * 14);
|
||||||
|
const endOn = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
|
||||||
|
async function RenderCharts() {
|
||||||
|
{{ range .Services }}
|
||||||
|
let chart{{.Id}} = new ApexCharts(document.querySelector("#service_{{js .Id}}"), options);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{ range .Services }}
|
{{ range .Services }}
|
||||||
|
await RenderChart(chart{{js .Id}}, {{js .Id}}, startOn, endOn);{{end}}
|
||||||
let chart{{.Id}} = new ApexCharts(document.querySelector("#service_{{js .Id}}"), options);
|
|
||||||
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
function onChartComplete(chart) {
|
|
||||||
var chartInstance = chart.chart,
|
|
||||||
ctx = chartInstance.ctx;
|
|
||||||
var controller = chart.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);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$( document ).ready(function() {
|
$( document ).ready(function() {
|
||||||
{{ range .Services }}AjaxChart(chart{{js .Id}}, {{js .Id}}, 0, 9999999999);
|
RenderCharts()
|
||||||
{{end}}
|
});
|
||||||
});
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -111,27 +111,42 @@ $('select#service_type').on('change', function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function AjaxChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
|
|
||||||
$.ajax({
|
async function RenderChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
|
||||||
url: "/api/services/"+service+"/data?start="+start+"&end="+end+"&group="+group,
|
let chartData = await ChartLatency(service, start, end, group, retry);
|
||||||
type: 'GET',
|
if (chartData.length === 0) {
|
||||||
success: function(data) {
|
chartData = await ChartLatency(service, start, end, "minute", retry);
|
||||||
if (data.data.length < 12) {
|
|
||||||
if (retry) {
|
|
||||||
AjaxChart(chart, service, 0, 9999999999, "second", false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (data.data.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chart.render();
|
|
||||||
chart.updateSeries([{
|
|
||||||
data: data.data
|
|
||||||
}]);
|
|
||||||
}
|
}
|
||||||
});
|
chart.render();
|
||||||
|
chart.updateSeries([{
|
||||||
|
data: chartData
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ChartLatency(service, start=0, end=9999999999, group="hour", retry=true) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/services/" + service + "/data?start=" + start + "&end=" + end + "&group=" + group,
|
||||||
|
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) {
|
function FailureAnnotations(chart, service, start=0, end=9999999999, group="hour", retry=true) {
|
||||||
|
|
|
@ -145,6 +145,12 @@ HTML,BODY {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-chart-heatmap {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@mixin dynamic-color-hov($color) {
|
@mixin dynamic-color-hov($color) {
|
||||||
&.dyn-dark {
|
&.dyn-dark {
|
||||||
background-color: darken($color, 12%) !important;
|
background-color: darken($color, 12%) !important;
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="col-12 full-col-12">
|
<div class="col-12 full-col-12">
|
||||||
{{ if not Services }}
|
{{ if not .Services }}
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
<h4 class="alert-heading">No Services to Monitor!</h4>
|
<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 <b>Dashboard</b> and add a website to begin really using your status page!</p>
|
<p>Your Statping Status Page is working correctly, but you don't have any services to monitor. Go to the <b>Dashboard</b> and add a website to begin really using your status page!</p>
|
||||||
|
@ -51,7 +51,8 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{ range Services }}
|
{{ range .Services }}
|
||||||
|
{{$avgTime := .AvgTime}}
|
||||||
<div class="mb-4" id="service_id_{{.Id}}">
|
<div class="mb-4" id="service_id_{{.Id}}">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
@ -65,22 +66,22 @@
|
||||||
|
|
||||||
<div class="row stats_area mt-5">
|
<div class="row stats_area mt-5">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="lg_number">{{.AvgTime}}ms</span>
|
<span class="lg_number">{{$avgTime}}ms</span>
|
||||||
Average Response
|
Average Response
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="lg_number">{{.Online24}}%</span>
|
<span class="lg_number">{{.OnlineDaysPercent 1}}%</span>
|
||||||
Uptime last 24 Hours
|
Uptime last 24 Hours
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="lg_number">{{.Online7Days}}%</span>
|
<span class="lg_number">{{.OnlineDaysPercent 7}}%</span>
|
||||||
Uptime last 7 Days
|
Uptime last 7 Days
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ if .AvgUptime24 }}
|
{{ if $avgTime }}
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="service_{{ .Id }}"></div>
|
<div id="service_{{ .Id }}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
|
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<h4 class="mt-2">{{ $s.Name }}
|
<h4 class="mt-2"><a href="/">{{CoreApp.Name}}</a> - {{ $s.Name }}
|
||||||
{{if $s.Online }}
|
{{if $s.Online }}
|
||||||
<span class="badge bg-success float-right d-none d-md-block">ONLINE</span>
|
<span class="badge bg-success float-right d-none d-md-block">ONLINE</span>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
@ -28,15 +28,13 @@
|
||||||
|
|
||||||
<div class="row stats_area mt-5 mb-5">
|
<div class="row stats_area mt-5 mb-5">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="lg_number">{{$s.Online24}}%</span>
|
<span class="lg_number">{{$s.OnlineDaysPercent 1}}%</span>
|
||||||
Online last 24 Hours
|
Online last 24 Hours
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="lg_number">{{$s.AvgTime}}ms</span>
|
<span class="lg_number">{{$s.AvgTime}}ms</span>
|
||||||
Average Response
|
Average Response
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span class="lg_number">{{$s.TotalUptime}}%</span>
|
<span class="lg_number">{{$s.TotalUptime}}%</span>
|
||||||
Total Uptime
|
Total Uptime
|
||||||
|
@ -62,7 +60,7 @@
|
||||||
<div id="service"></div>
|
<div id="service"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="service-chart-heatmap">
|
||||||
<div id="service_heatmap"></div>
|
<div id="service_heatmap"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -207,82 +205,137 @@
|
||||||
<script src="/js/flatpickr.js"></script>
|
<script src="/js/flatpickr.js"></script>
|
||||||
<script src="/js/rangePlugin.js"></script>
|
<script src="/js/rangePlugin.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
|
||||||
|
|
||||||
|
let options = {
|
||||||
let options = {
|
chart: {
|
||||||
chart: {
|
height: "100%",
|
||||||
height: "100%",
|
width: "100%",
|
||||||
width: "100%",
|
type: "area",
|
||||||
type: "area",
|
animations: {
|
||||||
animations: {
|
enabled: false,
|
||||||
enabled: false,
|
initialAnimation: {
|
||||||
initialAnimation: {
|
enabled: false
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
colors: ["#48d338"],
|
|
||||||
opacity: 1,
|
|
||||||
type: 'solid'
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
show: true,
|
|
||||||
curve: 'smooth',
|
|
||||||
lineCap: 'butt',
|
|
||||||
colors: ["#3aa82d"],
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: "Response Time",
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
x: "02-10-2017 GMT",
|
|
||||||
y: 34
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: "02-11-2017 GMT",
|
|
||||||
y: 43
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: "02-12-2017 GMT",
|
|
||||||
y: 31
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: "02-13-2017 GMT",
|
|
||||||
y: 43
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: "02-14-2017 GMT",
|
|
||||||
y: 33
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: "02-15-2017 GMT",
|
|
||||||
y: 52
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
xaxis: {
|
|
||||||
type: "datetime",
|
|
||||||
},
|
},
|
||||||
yaxis: {
|
},
|
||||||
labels: {
|
fill: {
|
||||||
formatter: (value) => {
|
colors: ["#48d338"],
|
||||||
return (value * 0.1).toFixed(0) + "ms"
|
opacity: 1,
|
||||||
|
type: 'solid'
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
show: true,
|
||||||
|
curve: 'smooth',
|
||||||
|
lineCap: 'butt',
|
||||||
|
colors: ["#3aa82d"],
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Response Time",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: "02-10-2017 GMT",
|
||||||
|
y: 34
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
x: "02-11-2017 GMT",
|
||||||
|
y: 43
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: "02-12-2017 GMT",
|
||||||
|
y: 31
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: "02-13-2017 GMT",
|
||||||
|
y: 43
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: "02-14-2017 GMT",
|
||||||
|
y: 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: "02-15-2017 GMT",
|
||||||
|
y: 52
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xaxis: {
|
||||||
|
type: "datetime",
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: (value) => {
|
||||||
|
return (value).toFixed(0) + "ms"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dataLabels: {
|
},
|
||||||
enabled: false
|
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: [{}],
|
||||||
|
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 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() {
|
||||||
let chart = new ApexCharts(document.querySelector("#service"), options);
|
let chart = new ApexCharts(document.querySelector("#service"), options);
|
||||||
|
await RenderChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
|
||||||
|
}
|
||||||
|
|
||||||
AjaxChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
|
$(document).ready(async function() {
|
||||||
|
|
||||||
let startDate = $("#service_start").flatpickr({
|
let startDate = $("#service_start").flatpickr({
|
||||||
enableTime: false,
|
enableTime: false,
|
||||||
static: true,
|
static: true,
|
||||||
|
@ -293,7 +346,7 @@ $(document).ready(function() {
|
||||||
onChange: function(selectedDates, dateStr, instance) {
|
onChange: function(selectedDates, dateStr, instance) {
|
||||||
var one = Math.round((new Date(selectedDates[0])).getTime() / 1000);
|
var one = Math.round((new Date(selectedDates[0])).getTime() / 1000);
|
||||||
var two = Math.round((new Date(selectedDates[1])).getTime() / 1000);
|
var two = Math.round((new Date(selectedDates[1])).getTime() / 1000);
|
||||||
$("#service_start").val(one);
|
$("#service_start").val(one);
|
||||||
$("#service_end").val(two);
|
$("#service_end").val(two);
|
||||||
$("#start_date").html(dateStr);
|
$("#start_date").html(dateStr);
|
||||||
},
|
},
|
||||||
|
@ -304,46 +357,8 @@ $(document).ready(function() {
|
||||||
startDate.open()
|
startDate.open()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await RenderChartLatency();
|
||||||
|
await RenderHeatmap();
|
||||||
var heat_options = {
|
|
||||||
chart: {
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
type: 'heatmap',
|
|
||||||
toolbar: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
enableShades: true,
|
|
||||||
shadeIntensity: 0.5,
|
|
||||||
colors: ["#d53a3b"],
|
|
||||||
series: [{}],
|
|
||||||
};
|
|
||||||
|
|
||||||
var heatChart = new ApexCharts(
|
|
||||||
document.querySelector("#service_heatmap"),
|
|
||||||
heat_options
|
|
||||||
);
|
|
||||||
|
|
||||||
var heatmapData = [];
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: "/api/services/{{$s.Id}}/heatmap",
|
|
||||||
type: 'GET',
|
|
||||||
success: function(data) {
|
|
||||||
data.forEach(function(d) {
|
|
||||||
var date = new Date(d.date);
|
|
||||||
heatmapData.push({name: date.toLocaleString('en-us', { month: 'long' }), data: d.data});
|
|
||||||
});
|
|
||||||
heatChart.render();
|
|
||||||
heatChart.updateSeries(heatmapData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -125,7 +125,8 @@
|
||||||
<h3>Additional Settings</h3>
|
<h3>Additional Settings</h3>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a href="/settings/export" class="btn btn-sm btn-secondary float-right">Export Settings</a>
|
<a href="/api/clear_cache" class="btn btn-sm btn-secondary float-right">Clear Cache</a>
|
||||||
|
<a href="/settings/export" class="btn btn-sm btn-secondary float-right">Export Settings</a>
|
||||||
{{if .Domain}}
|
{{if .Domain}}
|
||||||
<a href="#" class="btn btn-sm btn-secondary float-right ml-1">Authentication QR Code</a>
|
<a href="#" class="btn btn-sm btn-secondary float-right ml-1">Authentication QR Code</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -40,6 +40,15 @@ type CheckinInterface interface {
|
||||||
Select() *Checkin
|
Select() *Checkin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate for Checkin will set CreatedAt to UTC
|
||||||
|
func (c *Checkin) BeforeCreate() (err error) {
|
||||||
|
if c.CreatedAt.IsZero() {
|
||||||
|
c.CreatedAt = time.Now().UTC()
|
||||||
|
c.UpdatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// CheckinHit is a successful response from a Checkin
|
// CheckinHit is a successful response from a Checkin
|
||||||
type CheckinHit struct {
|
type CheckinHit struct {
|
||||||
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
||||||
|
@ -48,6 +57,14 @@ type CheckinHit struct {
|
||||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate for checkinHit will set CreatedAt to UTC
|
||||||
|
func (c *CheckinHit) BeforeCreate() (err error) {
|
||||||
|
if c.CreatedAt.IsZero() {
|
||||||
|
c.CreatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Start will create a channel for the checkin checking go routine
|
// Start will create a channel for the checkin checking go routine
|
||||||
func (s *Checkin) Start() {
|
func (s *Checkin) Start() {
|
||||||
s.Running = make(chan bool)
|
s.Running = make(chan bool)
|
||||||
|
|
|
@ -39,6 +39,14 @@ type FailureInterface interface {
|
||||||
ParseError() string // ParseError returns a human readable error for a service failure
|
ParseError() string // ParseError returns a human readable error for a service failure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate for Failure will set CreatedAt to UTC
|
||||||
|
func (f *Failure) BeforeCreate() (err error) {
|
||||||
|
if f.CreatedAt.IsZero() {
|
||||||
|
f.CreatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type FailSort []FailureInterface
|
type FailSort []FailureInterface
|
||||||
|
|
||||||
func (s FailSort) Len() int {
|
func (s FailSort) Len() int {
|
||||||
|
|
|
@ -34,3 +34,12 @@ type Message struct {
|
||||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate for Message will set CreatedAt to UTC
|
||||||
|
func (u *Message) BeforeCreate() (err error) {
|
||||||
|
if u.CreatedAt.IsZero() {
|
||||||
|
u.CreatedAt = time.Now().UTC()
|
||||||
|
u.UpdatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,15 @@ type Service struct {
|
||||||
Checkins []CheckinInterface `gorm:"-" json:"checkins,omitempty"`
|
Checkins []CheckinInterface `gorm:"-" json:"checkins,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate for Service will set CreatedAt to UTC
|
||||||
|
func (s *Service) BeforeCreate() (err error) {
|
||||||
|
if s.CreatedAt.IsZero() {
|
||||||
|
s.CreatedAt = time.Now().UTC()
|
||||||
|
s.UpdatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type ServiceInterface interface {
|
type ServiceInterface interface {
|
||||||
Select() *Service
|
Select() *Service
|
||||||
CheckQueue(bool)
|
CheckQueue(bool)
|
||||||
|
|
|
@ -28,6 +28,14 @@ type Hit struct {
|
||||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate for Hit will set CreatedAt to UTC
|
||||||
|
func (h *Hit) BeforeCreate() (err error) {
|
||||||
|
if h.CreatedAt.IsZero() {
|
||||||
|
h.CreatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// DbConfig struct is used for the database connection and creates the 'config.yml' file
|
// DbConfig struct is used for the database connection and creates the 'config.yml' file
|
||||||
type DbConfig struct {
|
type DbConfig struct {
|
||||||
DbConn string `yaml:"connection"`
|
DbConn string `yaml:"connection"`
|
||||||
|
|
|
@ -39,3 +39,12 @@ type UserInterface interface {
|
||||||
Update() error
|
Update() error
|
||||||
Delete() error
|
Delete() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate for User will set CreatedAt to UTC
|
||||||
|
func (u *User) BeforeCreate() (err error) {
|
||||||
|
if u.CreatedAt.IsZero() {
|
||||||
|
u.CreatedAt = time.Now().UTC()
|
||||||
|
u.UpdatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.80.40
|
0.80.41
|
||||||
|
|
Loading…
Reference in New Issue