mirror of https://github.com/statping/statping
parent
b80c61cd04
commit
fd7925aa7e
|
@ -161,66 +161,6 @@ func (u *Message) AfterFind() (err error) {
|
|||
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
|
||||
func (db *DbConfig) InsertCore() (*Core, error) {
|
||||
CoreApp = &Core{Core: &types.Core{
|
||||
|
|
|
@ -31,6 +31,7 @@ type Failure struct {
|
|||
|
||||
const (
|
||||
limitedFailures = 32
|
||||
limitedHits = 32
|
||||
)
|
||||
|
||||
// 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
|
||||
func (s *Service) LimitedHits() ([]*types.Hit, error) {
|
||||
func (s *Service) LimitedHits(amount int64) ([]*types.Hit, error) {
|
||||
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)
|
||||
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
|
||||
func (s *Service) TotalHits() (uint64, error) {
|
||||
var count uint64
|
||||
col := hitsDB().Where("service = ?", s.Id)
|
||||
err := col.Count(&count)
|
||||
return count, err.Error
|
||||
col := hitsDB().Where("service = ?", s.Id).Count(&count)
|
||||
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"))
|
||||
err := rows.Count(&count)
|
||||
return count, err.Error
|
||||
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Count(&count)
|
||||
return count, rows.Error
|
||||
}
|
||||
|
||||
// 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...")
|
||||
|
||||
insertSampleGroups()
|
||||
createdOn := time.Now().Add((-24 * 90) * time.Hour).UTC()
|
||||
createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC()
|
||||
s1 := ReturnService(&types.Service{
|
||||
Name: "Google",
|
||||
Domain: "https://google.com",
|
||||
|
|
|
@ -92,7 +92,7 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
|
|||
checkins := service.AllCheckins()
|
||||
for _, c := range checkins {
|
||||
c.Failures = c.LimitedFailures(limitedFailures)
|
||||
c.Hits = c.LimitedHits(limitedFailures)
|
||||
c.Hits = c.LimitedHits(limitedHits)
|
||||
service.Checkins = append(service.Checkins, c)
|
||||
}
|
||||
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
|
||||
func (s *Service) AvgTime() float64 {
|
||||
func (s *Service) AvgTime() string {
|
||||
total, _ := s.TotalHits()
|
||||
if total == 0 {
|
||||
return float64(0)
|
||||
return "0"
|
||||
}
|
||||
sum := s.Sum()
|
||||
avg := sum / float64(total) * 100
|
||||
amount := fmt.Sprintf("%0.0f", avg*10)
|
||||
val, _ := strconv.ParseFloat(amount, 10)
|
||||
return val
|
||||
return fmt.Sprintf("%0.0f", avg*10)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -185,7 +189,7 @@ func (s *Service) lastFailure() *Failure {
|
|||
// // Online since Monday 3:04:05PM, Jan _2 2006
|
||||
func (s *Service) SmallText() string {
|
||||
last := s.LimitedFailures(1)
|
||||
hits, _ := s.LimitedHits()
|
||||
hits, _ := s.LimitedHits(1)
|
||||
zone := CoreApp.Timezone
|
||||
if s.Online {
|
||||
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
|
||||
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
|
||||
var d []DateScan
|
||||
var amount int64
|
||||
var data []DateScan
|
||||
outgoing := new(DateScanObj)
|
||||
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")
|
||||
rows, err := model.Rows()
|
||||
if err != nil {
|
||||
utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err))
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var gd DateScan
|
||||
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.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
|
||||
|
|
|
@ -156,7 +156,7 @@ func TestServiceHits(t *testing.T) {
|
|||
|
||||
func TestServiceLimitedHits(t *testing.T) {
|
||||
service := SelectService(5)
|
||||
hits, err := service.LimitedHits()
|
||||
hits, err := service.LimitedHits(1024)
|
||||
assert.Nil(t, err)
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
utils.Log(2, fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
|
||||
output := apiResponse{
|
||||
|
|
|
@ -46,9 +46,6 @@ func NewStorage() *Storage {
|
|||
|
||||
//Get a cached content by key
|
||||
func (s Storage) Get(key string) []byte {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
item := s.items[key]
|
||||
if item.Expired() {
|
||||
CacheStorage.Delete(key)
|
||||
|
@ -93,10 +90,10 @@ func cached(duration, contentType string, handler func(w http.ResponseWriter, r
|
|||
w.Write(content)
|
||||
return
|
||||
}
|
||||
if d, err := time.ParseDuration(duration); err == nil {
|
||||
CacheStorage.Set(r.RequestURI, content, d)
|
||||
}
|
||||
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 (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/hunterlong/statping/core"
|
||||
|
@ -27,20 +26,23 @@ import (
|
|||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
cookieKey = "statping_auth"
|
||||
timeout = time.Second * 60
|
||||
timeout = time.Second * 30
|
||||
)
|
||||
|
||||
var (
|
||||
sessionStore *sessions.CookieStore
|
||||
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_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
|
||||
|
@ -91,6 +93,7 @@ func RunHTTPServer(ip string, port int) error {
|
|||
IdleTimeout: timeout,
|
||||
Handler: router,
|
||||
}
|
||||
httpServer.SetKeepAlivesEnabled(false)
|
||||
return httpServer.ListenAndServe()
|
||||
}
|
||||
return nil
|
||||
|
@ -166,112 +169,37 @@ func IsUser(r *http.Request) bool {
|
|||
return session.Values["authenticated"].(bool)
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
func loadTemplate(w http.ResponseWriter, r *http.Request) error {
|
||||
var err error
|
||||
mainTemplate = template.New("main")
|
||||
mainTemplate.Funcs(handlerFuncs(w, r))
|
||||
mainTemplate, err = mainTemplate.Parse(mainTmpl)
|
||||
if err != nil {
|
||||
utils.Log(4, err)
|
||||
return err
|
||||
}
|
||||
// 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
|
||||
func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}, redirect interface{}) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
if usingSSL {
|
||||
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
}
|
||||
|
||||
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"}
|
||||
|
||||
loadTemplate(w, r)
|
||||
render, err := source.TmplBox.String(file)
|
||||
if err != nil {
|
||||
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
|
||||
_, err = t.Parse(render)
|
||||
_, err = mainTemplate.Parse(render)
|
||||
if err != nil {
|
||||
utils.Log(4, err)
|
||||
}
|
||||
|
||||
// execute the template
|
||||
err = t.Execute(w, data)
|
||||
err = mainTemplate.Execute(w, data)
|
||||
if err != nil {
|
||||
utils.Log(4, err)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func Router() *mux.Router {
|
|||
dir := utils.Directory
|
||||
CacheStorage = NewStorage()
|
||||
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) {
|
||||
indexHandler := http.FileServer(http.Dir(dir + "/assets/"))
|
||||
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(dir+"/assets/css"))))
|
||||
|
@ -95,6 +95,7 @@ func Router() *mux.Router {
|
|||
// API Routes
|
||||
r.Handle("/api", http.HandlerFunc(apiIndexHandler))
|
||||
r.Handle("/api/renew", http.HandlerFunc(apiRenewHandler))
|
||||
r.Handle("/api/clear_cache", http.HandlerFunc(apiClearCacheHandler))
|
||||
|
||||
// API SERVICE Routes
|
||||
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/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}/ping", http.HandlerFunc(apiServicePingDataHandler)).Methods("GET")
|
||||
r.Handle("/api/services/{id}/heatmap", http.HandlerFunc(apiServiceHeatmapHandler)).Methods("GET")
|
||||
r.Handle("/api/services/{id}/ping", cached("30s", "application/json", http.HandlerFunc(apiServicePingDataHandler))).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(apiServiceDeleteHandler)).Methods("DELETE")
|
||||
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("/tray", http.HandlerFunc(trayHandler))
|
||||
r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known"))))
|
||||
|
||||
r.NotFoundHandler = http.HandlerFunc(error404Handler)
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package handlers
|
|||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/hunterlong/statping/core"
|
||||
"github.com/hunterlong/statping/types"
|
||||
|
@ -227,7 +228,7 @@ type dataXy struct {
|
|||
}
|
||||
|
||||
type dataXyMonth struct {
|
||||
Date time.Time `json:"date"`
|
||||
Date string `json:"date"`
|
||||
Data []*dataXy `json:"data"`
|
||||
}
|
||||
|
||||
|
@ -242,26 +243,37 @@ func apiServiceHeatmapHandler(w http.ResponseWriter, r *http.Request) {
|
|||
var monthOutput []*dataXyMonth
|
||||
|
||||
start := service.CreatedAt
|
||||
//now := time.Now()
|
||||
|
||||
if start.Year() <= 2 {
|
||||
start = service.CreatedAt.Add(time.Duration((-3 * 24) * time.Hour))
|
||||
}
|
||||
sY, sM, _ := start.Date()
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
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")
|
||||
|
|
|
@ -148,6 +148,12 @@ HTML, BODY {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.service-chart-heatmap {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3e9bff;
|
||||
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 }}
|
||||
|
||||
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);
|
||||
})
|
||||
await RenderChart(chart{{js .Id}}, {{js .Id}}, startOn, endOn);{{end}}
|
||||
}
|
||||
|
||||
$( document ).ready(function() {
|
||||
{{ range .Services }}AjaxChart(chart{{js .Id}}, {{js .Id}}, 0, 9999999999);
|
||||
{{end}}
|
||||
});
|
||||
RenderCharts()
|
||||
});
|
||||
{{end}}
|
||||
|
|
|
@ -111,27 +111,42 @@ $('select#service_type').on('change', function() {
|
|||
}
|
||||
});
|
||||
|
||||
function AjaxChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
|
||||
$.ajax({
|
||||
url: "/api/services/"+service+"/data?start="+start+"&end="+end+"&group="+group,
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
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
|
||||
}]);
|
||||
|
||||
async function RenderChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
|
||||
let chartData = await ChartLatency(service, start, end, group, retry);
|
||||
if (chartData.length === 0) {
|
||||
chartData = await ChartLatency(service, start, end, "minute", retry);
|
||||
}
|
||||
});
|
||||
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) {
|
||||
|
|
|
@ -145,6 +145,12 @@ HTML,BODY {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.service-chart-heatmap {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@mixin dynamic-color-hov($color) {
|
||||
&.dyn-dark {
|
||||
background-color: darken($color, 12%) !important;
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
{{end}}
|
||||
|
||||
<div class="col-12 full-col-12">
|
||||
{{ if not Services }}
|
||||
{{ 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 <b>Dashboard</b> and add a website to begin really using your status page!</p>
|
||||
|
@ -51,7 +51,8 @@
|
|||
</div>
|
||||
{{end}}
|
||||
|
||||
{{ range Services }}
|
||||
{{ range .Services }}
|
||||
{{$avgTime := .AvgTime}}
|
||||
<div class="mb-4" id="service_id_{{.Id}}">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
@ -65,22 +66,22 @@
|
|||
|
||||
<div class="row stats_area mt-5">
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{.AvgTime}}ms</span>
|
||||
<span class="lg_number">{{$avgTime}}ms</span>
|
||||
Average Response
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{.Online24}}%</span>
|
||||
<span class="lg_number">{{.OnlineDaysPercent 1}}%</span>
|
||||
Uptime last 24 Hours
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{.Online7Days}}%</span>
|
||||
<span class="lg_number">{{.OnlineDaysPercent 7}}%</span>
|
||||
Uptime last 7 Days
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ if .AvgUptime24 }}
|
||||
{{ if $avgTime }}
|
||||
<div class="chart-container">
|
||||
<div id="service_{{ .Id }}"></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>
|
||||
{{end}}
|
||||
|
||||
<h4 class="mt-2">{{ $s.Name }}
|
||||
<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 }}
|
||||
|
@ -28,15 +28,13 @@
|
|||
|
||||
<div class="row stats_area mt-5 mb-5">
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{$s.Online24}}%</span>
|
||||
<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
|
||||
|
@ -62,7 +60,7 @@
|
|||
<div id="service"></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="service-chart-heatmap">
|
||||
<div id="service_heatmap"></div>
|
||||
</div>
|
||||
|
||||
|
@ -207,82 +205,137 @@
|
|||
<script src="/js/flatpickr.js"></script>
|
||||
<script src="/js/rangePlugin.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
|
||||
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: "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
|
||||
}
|
||||
]
|
||||
let options = {
|
||||
chart: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
type: "area",
|
||||
animations: {
|
||||
enabled: false,
|
||||
initialAnimation: {
|
||||
enabled: false
|
||||
}
|
||||
],
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: (value) => {
|
||||
return (value * 0.1).toFixed(0) + "ms"
|
||||
},
|
||||
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: {
|
||||
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);
|
||||
await RenderChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
|
||||
}
|
||||
|
||||
AjaxChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
|
||||
|
||||
$(document).ready(async function() {
|
||||
let startDate = $("#service_start").flatpickr({
|
||||
enableTime: false,
|
||||
static: true,
|
||||
|
@ -293,7 +346,7 @@ $(document).ready(function() {
|
|||
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_start").val(one);
|
||||
$("#service_end").val(two);
|
||||
$("#start_date").html(dateStr);
|
||||
},
|
||||
|
@ -304,46 +357,8 @@ $(document).ready(function() {
|
|||
startDate.open()
|
||||
});
|
||||
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
await RenderChartLatency();
|
||||
await RenderHeatmap();
|
||||
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -125,7 +125,8 @@
|
|||
<h3>Additional Settings</h3>
|
||||
|
||||
<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}}
|
||||
<a href="#" class="btn btn-sm btn-secondary float-right ml-1">Authentication QR Code</a>
|
||||
{{end}}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -40,6 +40,15 @@ type CheckinInterface interface {
|
|||
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
|
||||
type CheckinHit struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
func (s *Checkin) Start() {
|
||||
s.Running = make(chan bool)
|
||||
|
|
|
@ -39,6 +39,14 @@ type FailureInterface interface {
|
|||
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
|
||||
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Select() *Service
|
||||
CheckQueue(bool)
|
||||
|
|
|
@ -28,6 +28,14 @@ type Hit struct {
|
|||
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
|
||||
type DbConfig struct {
|
||||
DbConn string `yaml:"connection"`
|
||||
|
|
|
@ -39,3 +39,12 @@ type UserInterface interface {
|
|||
Update() 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