service date range

pull/78/head
Hunter Long 2018-09-18 15:02:27 -07:00
parent baa4ac49ce
commit 4598d0688e
29 changed files with 2491 additions and 187 deletions

View File

@ -1,4 +1,4 @@
VERSION=0.63 VERSION=0.64
BINARY_NAME=statup BINARY_NAME=statup
GOPATH:=$(GOPATH) GOPATH:=$(GOPATH)
GOCMD=go GOCMD=go
@ -61,8 +61,8 @@ run: build
# compile assets using SASS and Rice. compiles scss -> css, and run rice embed-go # compile assets using SASS and Rice. compiles scss -> css, and run rice embed-go
compile: compile:
cd source && $(GOPATH)/bin/rice embed-go
sass source/scss/base.scss source/css/base.css sass source/scss/base.scss source/css/base.css
cd source && $(GOPATH)/bin/rice embed-go
rm -rf .sass-cache rm -rf .sass-cache
# benchmark testing # benchmark testing

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build debug // +build debug
// Statup // Statup

View File

@ -123,7 +123,7 @@ func (s *Service) TotalFailures() (uint64, error) {
// TotalFailuresSince returns the total amount of failures for a service since a specific time/date // TotalFailuresSince returns the total amount of failures for a service since a specific time/date
func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) { func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) {
var count uint64 var count uint64
rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.Format("2006-01-02 15:04:05")) rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05"))
err := rows.Count(&count) err := rows.Count(&count)
return count, err.Error return count, err.Error
} }

View File

@ -78,7 +78,7 @@ func (s *Service) TotalHits() (uint64, 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.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"))
err := rows.Count(&count) err := rows.Count(&count)
return count, err.Error return count, err.Error
} }

View File

@ -18,6 +18,7 @@ package core
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
@ -125,9 +126,17 @@ type DateScan struct {
Value int64 `json:"y"` Value int64 `json:"y"`
} }
// DateScanObj struct is for creating the charts.js graph JSON array
type DateScanObj struct {
Array []DateScan
}
// lastFailure returns the last failure a service had // lastFailure returns the last failure a service had
func (s *Service) lastFailure() *Failure { func (s *Service) lastFailure() *Failure {
limited := s.LimitedFailures() limited := s.LimitedFailures()
if len(limited) == 0 {
return nil
}
last := limited[len(limited)-1] last := limited[len(limited)-1]
return last return last
} }
@ -146,47 +155,61 @@ func (s *Service) SmallText() string {
} }
if len(last) > 0 { if len(last) > 0 {
lastFailure := s.lastFailure() lastFailure := s.lastFailure()
return fmt.Sprintf("%v on %v", lastFailure.ParseError(), utils.Timezoner(last[0].CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006")) got, _ := timeago.TimeAgoWithTime(time.Now().Add(s.Downtime()), time.Now())
return fmt.Sprintf("Reported offline %v, %v", got, lastFailure.ParseError())
} else { } else {
return fmt.Sprintf("%v is currently offline", s.Name) return fmt.Sprintf("%v is currently offline", s.Name)
} }
} }
func (s *Service) DowntimeText() string {
lastFailure := s.lastFailure()
if lastFailure == nil {
return ""
}
got, _ := timeago.TimeAgoWithTime(time.Now().UTC().Add(s.Downtime()), time.Now().UTC())
return fmt.Sprintf("Reported offline %v, %v", got, lastFailure.ParseError())
}
// GroupDataBy returns a SQL query as a string to group a column by a time // GroupDataBy returns a SQL query as a string to group a column by a time
func GroupDataBy(column string, id int64, tm time.Time, increment string) string { func GroupDataBy(column string, id int64, start, end time.Time, increment string) string {
var sql string var sql string
switch CoreApp.DbConnection { switch CoreApp.DbConnection {
case "mysql": case "mysql":
sql = fmt.Sprintf("SELECT CONCAT(date_format(created_at, '%%Y-%%m-%%dT%%H:%%i:00Z')) AS created_at, AVG(latency)*1000 AS value FROM %v WHERE service=%v AND DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%TZ') BETWEEN DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') AND DATE_FORMAT(NOW(), '%%Y-%%m-%%dT%%TZ') GROUP BY 1 ORDER BY created_at ASC;", column, id, tm.Format(time.RFC3339)) sql = fmt.Sprintf("SELECT CONCAT(date_format(created_at, '%%Y-%%m-%%dT%%H:%%i:00Z')) AS created_at, AVG(latency)*1000 AS value FROM %v WHERE service=%v AND DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%TZ') BETWEEN DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') AND DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') GROUP BY 1 ORDER BY created_at ASC;", column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339))
case "sqlite": case "sqlite":
sql = fmt.Sprintf("SELECT strftime('%%Y-%%m-%%dT%%H:%%M:00Z', created_at), AVG(latency)*1000 as value FROM %v WHERE service=%v AND created_at >= '%v' GROUP BY strftime('%%M:00', created_at) ORDER BY created_at ASC;", column, id, tm.Format(time.RFC3339)) sql = fmt.Sprintf("SELECT strftime('%%Y-%%m-%%dT%%H:%%M:00Z', created_at), AVG(latency)*1000 as value FROM %v WHERE service=%v AND created_at >= '%v' AND created_at <= '%v' GROUP BY strftime('%%M:00', created_at) ORDER BY created_at ASC;", column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339))
case "postgres": case "postgres":
sql = fmt.Sprintf("SELECT date_trunc('%v', created_at), AVG(latency)*1000 AS value FROM %v WHERE service=%v AND created_at >= '%v' GROUP BY 1 ORDER BY date_trunc ASC;", increment, column, id, tm.Format(time.RFC3339)) sql = fmt.Sprintf("SELECT date_trunc('%v', created_at), AVG(latency)*1000 AS value FROM %v WHERE service=%v AND created_at >= '%v' AND created_at <= '%v' GROUP BY 1 ORDER BY date_trunc ASC;", increment, column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339))
} }
return sql return sql
} }
// Downtime returns the amount of time of a offline service
func (s *Service) Downtime() time.Duration { func (s *Service) Downtime() time.Duration {
hits, _ := s.Hits() hits, _ := s.Hits()
if len(hits) == 0 {
return time.Duration(0)
}
fails := s.LimitedFailures() fails := s.LimitedFailures()
if len(fails) == 0 { if len(fails) == 0 {
return time.Duration(0) return time.Duration(0)
} }
since := fails[0].CreatedAt.Sub(hits[0].CreatedAt) since := fails[0].CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC())
return since return since
} }
func (s *Service) GraphDataRaw() []*DateScan { func GraphDataRaw(service types.ServiceInterface, start, end time.Time) *DateScanObj {
var d []*DateScan var d []DateScan
since := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0) s := service.Select()
sql := GroupDataBy("hits", s.Id, since, "minute") sql := GroupDataBy("hits", s.Id, start, end, "minute")
rows, err := DbSession.Raw(sql).Rows() rows, err := DbSession.Raw(sql).Rows()
if err != nil { if err != nil {
utils.Log(2, err) utils.Log(2, err)
return nil return nil
} }
for rows.Next() { for rows.Next() {
gd := new(DateScan) var gd DateScan
var tt string var tt string
var ff float64 var ff float64
err := rows.Scan(&tt, &ff) err := rows.Scan(&tt, &ff)
@ -201,12 +224,23 @@ func (s *Service) GraphDataRaw() []*DateScan {
gd.Value = int64(ff) gd.Value = int64(ff)
d = append(d, gd) d = append(d, gd)
} }
return d return &DateScanObj{d}
}
func (d *DateScanObj) ToString() string {
data, err := json.Marshal(d.Array)
if err != nil {
utils.Log(2, err)
return "{}"
}
return string(data)
} }
// GraphData returns the JSON object used by Charts.js to render the chart // GraphData returns the JSON object used by Charts.js to render the chart
func (s *Service) GraphData() string { func (s *Service) GraphData() string {
obj := s.GraphDataRaw() start := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0)
end := time.Now()
obj := GraphDataRaw(s, start, end)
data, err := json.Marshal(obj) data, err := json.Marshal(obj)
if err != nil { if err != nil {
utils.Log(2, err) utils.Log(2, err)

View File

@ -345,19 +345,19 @@ func TestGroupGraphData(t *testing.T) {
service := SelectService(1) service := SelectService(1)
CoreApp.DbConnection = "mysql" CoreApp.DbConnection = "mysql"
lastWeek := time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) lastWeek := time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0)
out := GroupDataBy("services", service.Id, lastWeek, "hour") out := GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour")
t.Log(out) t.Log(out)
assert.Contains(t, out, "SELECT CONCAT(date_format(created_at, '%Y-%m-%dT%H:%i:00Z'))") assert.Contains(t, out, "SELECT CONCAT(date_format(created_at, '%Y-%m-%dT%H:%i:00Z'))")
CoreApp.DbConnection = "postgres" CoreApp.DbConnection = "postgres"
lastWeek = time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) lastWeek = time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0)
out = GroupDataBy("services", service.Id, lastWeek, "hour") out = GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour")
t.Log(out) t.Log(out)
assert.Contains(t, out, "SELECT date_trunc('hour', created_at)") assert.Contains(t, out, "SELECT date_trunc('hour', created_at)")
CoreApp.DbConnection = "sqlite" CoreApp.DbConnection = "sqlite"
lastWeek = time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) lastWeek = time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0)
out = GroupDataBy("services", service.Id, lastWeek, "hour") out = GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour")
t.Log(out) t.Log(out)
assert.Contains(t, out, "SELECT strftime('%Y-%m-%dT%H:%M:00Z'") assert.Contains(t, out, "SELECT strftime('%Y-%m-%dT%H:%M:00Z'")

View File

@ -23,6 +23,7 @@ import (
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http" "net/http"
"os" "os"
"time"
) )
type ApiResponse struct { type ApiResponse struct {
@ -99,6 +100,22 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
fields := parseGet(r)
startField := utils.StringInt(fields.Get("start"))
endField := utils.StringInt(fields.Get("end"))
var start time.Time
var end time.Time
if startField == 0 {
start = time.Now().Add(-24 * time.Hour).UTC()
} else {
start = time.Unix(startField, 0)
}
if endField == 0 {
end = time.Now().UTC()
} else {
end = time.Unix(endField, 0)
}
service := core.SelectService(utils.StringInt(vars["id"])) service := core.SelectService(utils.StringInt(vars["id"]))
if service == nil { if service == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
@ -106,7 +123,7 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(service.GraphDataRaw()) json.NewEncoder(w).Encode(core.GraphDataRaw(service, start, end).Array)
} }
func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) { func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -147,6 +147,12 @@ func executeResponse(w http.ResponseWriter, r *http.Request, file string, data i
"ToString": func(v interface{}) string { "ToString": func(v interface{}) string {
return utils.ToString(v) return utils.ToString(v)
}, },
"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")
},
}) })
t, err = t.Parse(nav) t, err = t.Parse(nav)
if err != nil { if err != nil {

View File

@ -24,6 +24,7 @@ import (
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http" "net/http"
"strconv" "strconv"
"time"
) )
type Service struct { type Service struct {
@ -36,17 +37,56 @@ func renderServiceChartHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
fields := parseGet(r)
startField := fields.Get("start")
endField := fields.Get("end")
var start time.Time
var end time.Time
if startField == "" {
start = time.Now().Add(-24 * time.Hour)
} else {
start = time.Unix(utils.StringInt(startField), 0)
}
if endField == "" {
end = time.Now()
} else {
end = time.Unix(utils.StringInt(endField), 0)
}
service := core.SelectService(utils.StringInt(vars["id"])) service := core.SelectService(utils.StringInt(vars["id"]))
w.Header().Set("Content-Type", "text/javascript") w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Cache-Control", "max-age=60") w.Header().Set("Cache-Control", "max-age=60")
executeJSResponse(w, r, "charts.js", []*core.Service{service})
data := core.GraphDataRaw(service, start, end).ToString()
out := struct {
Services []*core.Service
Data []string
}{[]*core.Service{service}, []string{data}}
executeJSResponse(w, r, "charts.js", out)
} }
func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) { func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
services := core.CoreApp.Services services := core.CoreApp.Services
w.Header().Set("Content-Type", "text/javascript") w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Cache-Control", "max-age=60") w.Header().Set("Cache-Control", "max-age=60")
executeJSResponse(w, r, "charts.js", services)
var data []string
end := time.Now()
start := end.Add(-24 * time.Hour)
for _, s := range services {
d := core.GraphDataRaw(s, start, end).ToString()
data = append(data, d)
}
out := struct {
Services []types.ServiceInterface
Data []string
}{services, data}
executeJSResponse(w, r, "charts.js", out)
} }
func servicesHandler(w http.ResponseWriter, r *http.Request) { func servicesHandler(w http.ResponseWriter, r *http.Request) {
@ -139,12 +179,27 @@ func servicesDeleteHandler(w http.ResponseWriter, r *http.Request) {
func servicesViewHandler(w http.ResponseWriter, r *http.Request) { func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
fields := parseGet(r)
startField := utils.StringInt(fields.Get("start"))
endField := utils.StringInt(fields.Get("end"))
serv := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
if serv == nil { if serv == nil {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
executeResponse(w, r, "service.html", serv, nil)
if startField == 0 || endField == 0 {
startField = time.Now().Add(-24 * time.Hour).UTC().Unix()
endField = time.Now().UTC().Unix()
}
out := struct {
Service *core.Service
Start int64
End int64
}{serv, startField, endField}
executeResponse(w, r, "service.html", out, nil)
} }
func servicesUpdateHandler(w http.ResponseWriter, r *http.Request) { func servicesUpdateHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -129,6 +129,11 @@ func parseForm(r *http.Request) url.Values {
return r.PostForm return r.PostForm
} }
func parseGet(r *http.Request) url.Values {
r.ParseForm()
return r.Form
}
func saveNotificationHandler(w http.ResponseWriter, r *http.Request) { func saveNotificationHandler(w http.ResponseWriter, r *http.Request) {
var err error var err error
if !IsAuthenticated(r) { if !IsAuthenticated(r) {

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifiers package notifiers
import ( import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifiers package notifiers
import ( import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifiers package notifiers
import ( import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifiers package notifiers
import ( import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package notifiers package notifiers
import ( import (

View File

@ -1,3 +1,36 @@
@charset "UTF-8";
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* Index Page */ /* Index Page */
/* Status Container */ /* Status Container */
/* Button Colors */ /* Button Colors */
@ -6,53 +39,65 @@
/* Mobile Settings */ /* Mobile Settings */
/* Mobile Service Container */ /* Mobile Service Container */
HTML, BODY { HTML, BODY {
background-color: #fcfcfc; } background-color: #fcfcfc;
}
.container { .container {
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;
max-width: 860px; } max-width: 860px;
}
.header-title { .header-title {
color: #464646; } color: #464646;
}
.header-desc { .header-desc {
color: #939393; } color: #939393;
}
.btn { .btn {
border-radius: 0.2rem; } border-radius: 0.2rem;
}
.online_list .badge { .online_list .badge {
margin-top: 0.2rem; } margin-top: 0.2rem;
}
.navbar { .navbar {
margin-bottom: 30px; } margin-bottom: 30px;
}
.btn-sm { .btn-sm {
line-height: 1.3; line-height: 1.3;
font-size: 0.75rem; } font-size: 0.75rem;
}
.view_service_btn { .view_service_btn {
position: absolute; position: absolute;
bottom: -40px; bottom: -40px;
right: 40px; } right: 40px;
}
.service_lower_info { .service_lower_info {
position: absolute; position: absolute;
bottom: -40px; bottom: -40px;
left: 40px; left: 40px;
color: #d1ffca; color: #d1ffca;
font-size: 0.85rem; } font-size: 0.85rem;
}
.lg_number { .lg_number {
font-size: 2.3rem; font-size: 2.3rem;
font-weight: bold; font-weight: bold;
display: block; display: block;
color: #4f4f4f; } color: #4f4f4f;
}
.stats_area { .stats_area {
text-align: center; text-align: center;
color: #a5a5a5; } color: #a5a5a5;
}
.lower_canvas { .lower_canvas {
height: 3.4rem; height: 3.4rem;
@ -60,104 +105,134 @@ HTML, BODY {
background-color: #48d338; background-color: #48d338;
padding: 15px 10px; padding: 15px 10px;
margin-left: 0px !important; margin-left: 0px !important;
margin-right: 0px !important; } margin-right: 0px !important;
}
.lower_canvas SPAN { .lower_canvas SPAN {
font-size: 1rem; font-size: 1rem;
color: #fff; } color: #fff;
}
.footer { .footer {
text-decoration: none; text-decoration: none;
margin-top: 20px; } margin-top: 20px;
}
.footer A { .footer A {
color: #8d8d8d; color: #8d8d8d;
text-decoration: none; } text-decoration: none;
}
.footer A:HOVER { .footer A:HOVER {
color: #6d6d6d; } color: #6d6d6d;
}
.badge { .badge {
color: white; color: white;
border-radius: 0.2rem; } border-radius: 0.2rem;
}
.btn-group { .btn-group {
height: 25px; } height: 25px;
.btn-group A { }
padding: 0.1rem .75rem; .btn-group A {
font-size: 0.8rem; } padding: 0.1rem 0.75rem;
font-size: 0.8rem;
}
.card-body .badge { .card-body .badge {
color: #fff; } color: #fff;
}
.nav-pills .nav-link { .nav-pills .nav-link {
border-radius: 0.2rem; } border-radius: 0.2rem;
}
.form-control { .form-control {
border-radius: 0.2rem; } border-radius: 0.2rem;
}
.card { .card {
background-color: #ffffff; background-color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.125); } border: 1px solid rgba(0, 0, 0, 0.125);
}
.card-body { .card-body {
overflow: hidden; } overflow: hidden;
}
.card-body H4 A { .card-body H4 A {
color: #444444; color: #444444;
text-decoration: none; } text-decoration: none;
}
.chart-container { .chart-container {
position: relative; position: relative;
height: 170px; height: 170px;
width: 100%; } width: 100%;
}
.btn-primary { .btn-primary {
background-color: #3e9bff; background-color: #3e9bff;
border-color: #006fe6; border-color: #006fe6;
color: white; } color: white;
.btn-primary.dyn-dark { }
background-color: #32a825 !important; .btn-primary.dyn-dark {
border-color: #2c9320 !important; } background-color: #32a825 !important;
.btn-primary.dyn-light { border-color: #2c9320 !important;
background-color: #75de69 !important; }
border-color: #88e37e !important; } .btn-primary.dyn-light {
background-color: #75de69 !important;
border-color: #88e37e !important;
}
.btn-success { .btn-success {
background-color: #47d337; } background-color: #47d337;
.btn-success.dyn-dark { }
background-color: #32a825 !important; .btn-success.dyn-dark {
border-color: #2c9320 !important; } background-color: #32a825 !important;
.btn-success.dyn-light { border-color: #2c9320 !important;
background-color: #75de69 !important; }
border-color: #88e37e !important; } .btn-success.dyn-light {
background-color: #75de69 !important;
border-color: #88e37e !important;
}
.btn-danger { .btn-danger {
background-color: #dd3545; } background-color: #dd3545;
.btn-danger.dyn-dark { }
background-color: #b61f2d !important; .btn-danger.dyn-dark {
border-color: #a01b28 !important; } background-color: #b61f2d !important;
.btn-danger.dyn-light { border-color: #a01b28 !important;
background-color: #e66975 !important; }
border-color: #e97f89 !important; } .btn-danger.dyn-light {
background-color: #e66975 !important;
border-color: #e97f89 !important;
}
.bg-success { .bg-success {
background-color: #47d337 !important; } background-color: #47d337 !important;
}
.bg-danger { .bg-danger {
background-color: #dd3545 !important; } background-color: #dd3545 !important;
}
.bg-success .dyn-dark { .bg-success .dyn-dark {
background-color: #35b027 !important; } background-color: #35b027 !important;
}
.bg-danger .dyn-dark { .bg-danger .dyn-dark {
background-color: #bf202f !important; } background-color: #bf202f !important;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link { .nav-pills .nav-link.active, .nav-pills .show > .nav-link {
background-color: #13a00d; } background-color: #13a00d;
}
.nav-pills A { .nav-pills A {
color: #424242; } color: #424242;
}
.CodeMirror { .CodeMirror {
/* Bootstrap Settings */ /* Bootstrap Settings */
@ -177,23 +252,26 @@ HTML, BODY {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
/* Code Mirror Settings */ /* Code Mirror Settings */
font-family: monospace; font-family: monospace;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
height: 80vh; } height: 80vh;
}
.CodeMirror-focused { .CodeMirror-focused {
/* Bootstrap Settings */ /* Bootstrap Settings */
border-color: #66afe9; border-color: #66afe9;
outline: 0; outline: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
.switch { .switch {
font-size: 1rem; font-size: 1rem;
position: relative; } position: relative;
}
.switch input { .switch input {
position: absolute; position: absolute;
@ -204,7 +282,8 @@ HTML, BODY {
clip: rect(0 0 0 0); clip: rect(0 0 0 0);
clip-path: inset(50%); clip-path: inset(50%);
overflow: hidden; overflow: hidden;
padding: 0; } padding: 0;
}
.switch input + label { .switch input + label {
position: relative; position: relative;
@ -217,23 +296,26 @@ HTML, BODY {
outline: none; outline: none;
user-select: none; user-select: none;
vertical-align: middle; vertical-align: middle;
text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem); } text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem);
}
.switch input + label::before, .switch input + label::before,
.switch input + label::after { .switch input + label::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: calc(calc(2.375rem * .8) * 2); width: calc(calc(2.375rem * .8) * 2);
bottom: 0; bottom: 0;
display: block; } display: block;
}
.switch input + label::before { .switch input + label::before {
right: 0; right: 0;
background-color: #dee2e6; background-color: #dee2e6;
border-radius: calc(2.375rem * .8); border-radius: calc(2.375rem * .8);
transition: 0.2s all; } transition: 0.2s all;
}
.switch input + label::after { .switch input + label::after {
top: 2px; top: 2px;
@ -242,120 +324,154 @@ HTML, BODY {
height: calc(calc(2.375rem * .8) - calc(2px * 2)); height: calc(calc(2.375rem * .8) - calc(2px * 2));
border-radius: 50%; border-radius: 50%;
background-color: white; background-color: white;
transition: 0.2s all; } transition: 0.2s all;
}
.switch input:checked + label::before { .switch input:checked + label::before {
background-color: #08d; } background-color: #08d;
}
.switch input:checked + label::after { .switch input:checked + label::after {
margin-left: calc(2.375rem * .8); } margin-left: calc(2.375rem * .8);
}
.switch input:focus + label::before { .switch input:focus + label::before {
outline: none; outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25); } box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25);
}
.switch input:disabled + label { .switch input:disabled + label {
color: #868e96; color: #868e96;
cursor: not-allowed; } cursor: not-allowed;
}
.switch input:disabled + label::before { .switch input:disabled + label::before {
background-color: #e9ecef; } background-color: #e9ecef;
}
.switch.switch-sm { .switch.switch-sm {
font-size: 0.875rem; } font-size: 0.875rem;
}
.switch.switch-sm input + label { .switch.switch-sm input + label {
min-width: calc(calc(1.9375rem * .8) * 2); min-width: calc(calc(1.9375rem * .8) * 2);
height: calc(1.9375rem * .8); height: calc(1.9375rem * .8);
line-height: calc(1.9375rem * .8); line-height: calc(1.9375rem * .8);
text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem); } text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem);
}
.switch.switch-sm input + label::before { .switch.switch-sm input + label::before {
width: calc(calc(1.9375rem * .8) * 2); } width: calc(calc(1.9375rem * .8) * 2);
}
.switch.switch-sm input + label::after { .switch.switch-sm input + label::after {
width: calc(calc(1.9375rem * .8) - calc(2px * 2)); width: calc(calc(1.9375rem * .8) - calc(2px * 2));
height: calc(calc(1.9375rem * .8) - calc(2px * 2)); } height: calc(calc(1.9375rem * .8) - calc(2px * 2));
}
.switch.switch-sm input:checked + label::after { .switch.switch-sm input:checked + label::after {
margin-left: calc(1.9375rem * .8); } margin-left: calc(1.9375rem * .8);
}
.switch.switch-lg { .switch.switch-lg {
font-size: 1.25rem; } font-size: 1.25rem;
}
.switch.switch-lg input + label { .switch.switch-lg input + label {
min-width: calc(calc(3rem * .8) * 2); min-width: calc(calc(3rem * .8) * 2);
height: calc(3rem * .8); height: calc(3rem * .8);
line-height: calc(3rem * .8); line-height: calc(3rem * .8);
text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem); } text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem);
}
.switch.switch-lg input + label::before { .switch.switch-lg input + label::before {
width: calc(calc(3rem * .8) * 2); } width: calc(calc(3rem * .8) * 2);
}
.switch.switch-lg input + label::after { .switch.switch-lg input + label::after {
width: calc(calc(3rem * .8) - calc(2px * 2)); width: calc(calc(3rem * .8) - calc(2px * 2));
height: calc(calc(3rem * .8) - calc(2px * 2)); } height: calc(calc(3rem * .8) - calc(2px * 2));
}
.switch.switch-lg input:checked + label::after { .switch.switch-lg input:checked + label::after {
margin-left: calc(3rem * .8); } margin-left: calc(3rem * .8);
}
.switch + .switch { .switch + .switch {
margin-left: 1rem; } margin-left: 1rem;
}
@keyframes pulse_animation { @keyframes pulse_animation {
0% { 0% {
transform: scale(1); } transform: scale(1);
}
30% { 30% {
transform: scale(1); } transform: scale(1);
}
40% { 40% {
transform: scale(1.02); } transform: scale(1.02);
}
50% { 50% {
transform: scale(1); } transform: scale(1);
}
60% { 60% {
transform: scale(1); } transform: scale(1);
}
70% { 70% {
transform: scale(1.05); } transform: scale(1.05);
}
80% { 80% {
transform: scale(1); } transform: scale(1);
}
100% { 100% {
transform: scale(1); } } transform: scale(1);
}
}
.pulse { .pulse {
animation-name: pulse_animation; animation-name: pulse_animation;
animation-duration: 1500ms; animation-duration: 1500ms;
transform-origin: 70% 70%; transform-origin: 70% 70%;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; } animation-timing-function: linear;
}
@keyframes glow-grow { @keyframes glow-grow {
0% { 0% {
opacity: 0; opacity: 0;
transform: scale(1); } transform: scale(1);
}
80% { 80% {
opacity: 1; } opacity: 1;
}
100% { 100% {
transform: scale(2); transform: scale(2);
opacity: 0; } } opacity: 0;
}
}
.pulse-glow { .pulse-glow {
animation-name: glow-grown; animation-name: glow-grown;
animation-duration: 100ms; animation-duration: 100ms;
transform-origin: 70% 30%; transform-origin: 70% 30%;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; } animation-timing-function: linear;
}
.pulse-glow:before, .pulse-glow:before,
.pulse-glow:after { .pulse-glow:after {
position: absolute; position: absolute;
content: ''; content: "";
height: 0.5rem; height: 0.5rem;
width: 1.75rem; width: 1.75rem;
top: 1.2rem; top: 1.2rem;
right: 2.15rem; right: 2.15rem;
border-radius: 0; border-radius: 0;
box-shadow: 0 0 7px #47d337; box-shadow: 0 0 7px #47d337;
animation: glow-grow 2s ease-out infinite; } animation: glow-grow 2s ease-out infinite;
}
.sortable_drag { .sortable_drag {
background-color: #0000000f; } background-color: #0000000f;
}
.drag_icon { .drag_icon {
cursor: move; cursor: move;
@ -369,82 +485,346 @@ HTML, BODY {
margin-right: 5px; margin-right: 5px;
margin-left: -10px; margin-left: -10px;
text-align: center; text-align: center;
color: #b1b1b1; } color: #b1b1b1;
}
/* (Optional) Apply a "closed-hand" cursor during drag operation. */ /* (Optional) Apply a "closed-hand" cursor during drag operation. */
.drag_icon:active { .drag_icon:active {
cursor: grabbing; cursor: grabbing;
cursor: -moz-grabbing; cursor: -moz-grabbing;
cursor: -webkit-grabbing; } cursor: -webkit-grabbing;
}
.switch_btn { .switch_btn {
float: right; float: right;
margin: -1px 0px 0px 0px; margin: -1px 0px 0px 0px;
display: block; } display: block;
}
#start_container {
position: absolute;
z-index: 99999;
margin-top: 20px;
}
#end_container {
position: absolute;
z-index: 99999;
margin-top: 20px;
right: 0;
}
.pointer {
cursor: pointer;
}
/*!
* Pikaday
* Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/
*/
.pika-single {
z-index: 9999;
display: block;
position: relative;
color: #333;
background: #fff;
border: 1px solid #ccc;
border-bottom-color: #bbb;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.pika-single.is-hidden {
display: none;
}
.pika-single.is-bound {
position: absolute;
box-shadow: 0 5px 15px -5px rgba(0, 0, 0, 0.5);
}
.pika-single {
*zoom: 1;
}
.pika-single:before, .pika-single:after {
content: " ";
display: table;
}
.pika-single:after {
clear: both;
}
.pika-lendar {
float: left;
width: 240px;
margin: 8px;
}
.pika-title {
position: relative;
text-align: center;
}
.pika-title select {
cursor: pointer;
position: absolute;
z-index: 9998;
margin: 0;
left: 0;
top: 5px;
filter: alpha(opacity=0);
opacity: 0;
}
.pika-label {
display: inline-block;
*display: inline;
position: relative;
z-index: 9999;
overflow: hidden;
margin: 0;
padding: 5px 3px;
font-size: 14px;
line-height: 20px;
font-weight: bold;
color: #333;
background-color: #fff;
}
.pika-prev,
.pika-next {
display: block;
cursor: pointer;
position: relative;
outline: none;
border: 0;
padding: 0;
width: 20px;
height: 30px;
text-indent: 20px;
white-space: nowrap;
overflow: hidden;
background-color: transparent;
background-position: center center;
background-repeat: no-repeat;
background-size: 75% 75%;
opacity: 0.5;
*position: absolute;
*top: 0;
}
.pika-prev:hover,
.pika-next:hover {
opacity: 1;
}
.pika-prev.is-disabled,
.pika-next.is-disabled {
cursor: default;
opacity: 0.2;
}
.pika-prev,
.is-rtl .pika-next {
float: left;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==");
*left: 0;
}
.pika-next,
.is-rtl .pika-prev {
float: right;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=");
*right: 0;
}
.pika-select {
display: inline-block;
*display: inline;
}
.pika-table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
border: 0;
}
.pika-table th,
.pika-table td {
width: 14.2857142857%;
padding: 0;
}
.pika-table th {
color: #999;
font-size: 12px;
line-height: 25px;
font-weight: bold;
text-align: center;
}
.pika-table abbr {
border-bottom: none;
cursor: help;
}
.pika-button {
cursor: pointer;
display: block;
-moz-box-sizing: border-box;
box-sizing: border-box;
outline: none;
border: 0;
margin: 0;
width: 100%;
padding: 5px;
color: #666;
font-size: 12px;
line-height: 15px;
text-align: right;
background: #f5f5f5;
}
.is-today .pika-button {
color: #33aaff;
font-weight: bold;
}
.is-selected .pika-button {
color: #fff;
font-weight: bold;
background: #33aaff;
box-shadow: inset 0 1px 3px #178fe5;
border-radius: 3px;
}
.is-disabled .pika-button, .is-outside-current-month .pika-button {
color: #999;
opacity: 0.3;
}
.is-disabled .pika-button {
pointer-events: none;
cursor: default;
}
.pika-button:hover {
color: #fff;
background: #ff8000;
box-shadow: none;
border-radius: 3px;
}
.pika-button .is-selection-disabled {
pointer-events: none;
cursor: default;
}
.pika-week {
font-size: 11px;
color: #999;
}
.is-inrange .pika-button {
background: #D5E9F7;
}
.is-startrange .pika-button {
color: #fff;
background: #6CB31D;
box-shadow: none;
border-radius: 3px;
}
.is-endrange .pika-button {
color: #fff;
background: #33aaff;
box-shadow: none;
border-radius: 3px;
}
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@media (max-width: 767px) { @media (max-width: 767px) {
HTML, BODY { HTML, BODY {
background-color: #fcfcfc; } background-color: #fcfcfc;
}
.sm-container { .sm-container {
margin-top: 40px !important; margin-top: 40px !important;
padding: 0 !important; } padding: 0 !important;
}
.list-group-item H5 { .list-group-item H5 {
font-size: 0.9rem; } font-size: 0.9rem;
}
.container { .container {
padding: 0 !important; } padding: 0 !important;
}
.navbar { .navbar {
margin-left: 0px; margin-left: 0px;
margin-top: 0px; margin-top: 0px;
width: 100%; width: 100%;
margin-bottom: 0; } margin-bottom: 0;
}
.btn-sm { .btn-sm {
line-height: 0.9rem; line-height: 0.9rem;
font-size: 0.65rem; } font-size: 0.65rem;
}
.full-col-12 { .full-col-12 {
padding-left: 0px; padding-left: 0px;
padding-right: 0px; } padding-right: 0px;
}
.card { .card {
border: 0; border: 0;
border-radius: 0rem; border-radius: 0rem;
padding: 0; padding: 0;
background-color: #ffffff; } background-color: #ffffff;
}
.card-body { .card-body {
font-size: 6pt; font-size: 6pt;
padding: 5px 5px; } padding: 5px 5px;
}
.lg_number { .lg_number {
font-size: 7.8vw; } font-size: 7.8vw;
}
.stats_area { .stats_area {
margin-top: 1.5rem !important; margin-top: 1.5rem !important;
margin-bottom: 1.5rem !important; } margin-bottom: 1.5rem !important;
}
.stats_area .col-4 { .stats_area .col-4 {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
font-size: 0.6rem; } font-size: 0.6rem;
}
.list-group-item { .list-group-item {
border-top: 1px solid #e4e4e4; border-top: 1px solid #e4e4e4;
border: 0px; } border: 0px;
}
.list-group-item:first-child { .list-group-item:first-child {
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; } border-top-right-radius: 0;
}
.list-group-item:last-child { .list-group-item:last-child {
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-bottom-left-radius: 0; } border-bottom-left-radius: 0;
}
.list-group-item P { .list-group-item P {
font-size: 0.7rem; } } font-size: 0.7rem;
}
}
/*# sourceMappingURL=base.css.map */ /*# sourceMappingURL=base.css.map */

View File

@ -1,4 +1,21 @@
{{ range . }}{{ if .AvgTime }}var ctx_{{.Id}}=document.getElementById("service_{{.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe .GraphData}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y} /*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
{{$d := .Data}}{{ range $i, $s := .Services }}{{ if $s.AvgTime }}var ctx_{{$s.Id}}=document.getElementById("service_{{$s.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{$s.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe (index $d $i)}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y}
if(data.y>highestNum){highestNum=data.y;hxH=bar._model.x;hyH=bar._model.y}});if(hxH>=820){hxH=820}else if(50>=hxH){hxH=50} if(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} 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);console.log("done service_id_{{.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}}) ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}})

1257
source/js/pikaday.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,20 @@
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import 'variables'; @import 'variables';
@ -441,4 +458,23 @@ HTML,BODY {
display: block; display: block;
} }
@import 'mobile'; #start_container {
position: absolute;
z-index: 99999;
margin-top: 20px;
}
#end_container {
position: absolute;
z-index: 99999;
margin-top: 20px;
right: 0;
}
.pointer {
cursor: pointer;
}
@import './pikaday';
@import './mobile';

View File

@ -1,3 +1,19 @@
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@media (max-width: 767px) { @media (max-width: 767px) {

255
source/scss/pikaday.scss Normal file
View File

@ -0,0 +1,255 @@
/*!
* Pikaday
* Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/
*/
// Variables
// Declare any of these variables before importing this SCSS file to easily override defaults
// Variables are namespaced with the pd (pikaday) prefix
// Colours
$pd-text-color: #333 !default;
$pd-title-color: #333 !default;
$pd-title-bg: #fff !default;
$pd-picker-bg: #fff !default;
$pd-picker-border: #ccc !default;
$pd-picker-border-bottom: #bbb !default;
$pd-picker-shadow: rgba(0,0,0,.5) !default;
$pd-th-color: #999 !default;
$pd-day-color: #666 !default;
$pd-day-bg: #f5f5f5 !default;
$pd-day-hover-color: #fff !default;
$pd-day-hover-bg: #ff8000 !default;
$pd-day-today-color: #33aaff !default;
$pd-day-selected-color: #fff !default;
$pd-day-selected-bg: #33aaff !default;
$pd-day-selected-shadow: #178fe5 !default;
$pd-day-disabled-color: #999 !default;
$pd-week-color: #999 !default;
// Font
$pd-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !default;
.pika-single {
z-index: 9999;
display: block;
position: relative;
color: $pd-text-color;
background: $pd-picker-bg;
border: 1px solid $pd-picker-border;
border-bottom-color: $pd-picker-border-bottom;
font-family: $pd-font-family;
&.is-hidden {
display: none;
}
&.is-bound {
position: absolute;
box-shadow: 0 5px 15px -5px $pd-picker-shadow;
}
}
// clear child float (pika-lendar), using the famous micro clearfix hack
// http://nicolasgallagher.com/micro-clearfix-hack/
.pika-single {
*zoom: 1;
&:before,
&:after {
content: " ";
display: table;
}
&:after { clear: both }
}
.pika-lendar {
float: left;
width: 240px;
margin: 8px;
}
.pika-title {
position: relative;
text-align: center;
select {
cursor: pointer;
position: absolute;
z-index: 9998;
margin: 0;
left: 0;
top: 5px;
filter: alpha(opacity=0);
opacity: 0;
}
}
.pika-label {
display: inline-block;
*display: inline;
position: relative;
z-index: 9999;
overflow: hidden;
margin: 0;
padding: 5px 3px;
font-size: 14px;
line-height: 20px;
font-weight: bold;
color: $pd-title-color;
background-color: $pd-title-bg;
}
.pika-prev,
.pika-next {
display: block;
cursor: pointer;
position: relative;
outline: none;
border: 0;
padding: 0;
width: 20px;
height: 30px;
text-indent: 20px; // hide text using text-indent trick, using width value (it's enough)
white-space: nowrap;
overflow: hidden;
background-color: transparent;
background-position: center center;
background-repeat: no-repeat;
background-size: 75% 75%;
opacity: .5;
*position: absolute;
*top: 0;
&:hover {
opacity: 1;
}
&.is-disabled {
cursor: default;
opacity: .2;
}
}
.pika-prev,
.is-rtl .pika-next {
float: left;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==');
*left: 0;
}
.pika-next,
.is-rtl .pika-prev {
float: right;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=');
*right: 0;
}
.pika-select {
display: inline-block;
*display: inline;
}
.pika-table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
border: 0;
th,
td {
width: 14.285714285714286%;
padding: 0;
}
th {
color: $pd-th-color;
font-size: 12px;
line-height: 25px;
font-weight: bold;
text-align: center;
}
abbr {
border-bottom: none;
cursor: help;
}
}
.pika-button {
cursor: pointer;
display: block;
-moz-box-sizing: border-box;
box-sizing: border-box;
outline: none;
border: 0;
margin: 0;
width: 100%;
padding: 5px;
color: $pd-day-color;
font-size: 12px;
line-height: 15px;
text-align: right;
background: $pd-day-bg;
.is-today & {
color: $pd-day-today-color;
font-weight: bold;
}
.is-selected & {
color: $pd-day-selected-color;
font-weight: bold;
background: $pd-day-selected-bg;
box-shadow: inset 0 1px 3px $pd-day-selected-shadow;
border-radius: 3px;
}
.is-disabled &,
.is-outside-current-month & {
color: $pd-day-disabled-color;
opacity: .3;
}
.is-disabled & {
pointer-events: none;
cursor: default;
}
&:hover {
color: $pd-day-hover-color;
background: $pd-day-hover-bg;
box-shadow: none;
border-radius: 3px;
}
.is-selection-disabled {
pointer-events: none;
cursor: default;
}
}
.pika-week {
font-size: 11px;
color: $pd-week-color;
}
.is-inrange .pika-button {
background: #D5E9F7;
}
.is-startrange .pika-button {
color: #fff;
background: #6CB31D;
box-shadow: none;
border-radius: 3px;
}
.is-endrange .pika-button {
color: #fff;
background: #33aaff;
box-shadow: none;
border-radius: 3px;
}

View File

@ -1,3 +1,20 @@
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* Index Page */ /* Index Page */
$background-color: #fcfcfc; $background-color: #fcfcfc;
$max-width: 860px; $max-width: 860px;

View File

@ -158,6 +158,7 @@ func CreateAllAssets(folder string) error {
CopyToPublic(ScssBox, folder+"/assets/scss", "base.scss") CopyToPublic(ScssBox, folder+"/assets/scss", "base.scss")
CopyToPublic(ScssBox, folder+"/assets/scss", "variables.scss") CopyToPublic(ScssBox, folder+"/assets/scss", "variables.scss")
CopyToPublic(ScssBox, folder+"/assets/scss", "mobile.scss") CopyToPublic(ScssBox, folder+"/assets/scss", "mobile.scss")
CopyToPublic(ScssBox, folder+"/assets/scss", "pikaday.scss")
CopyToPublic(CssBox, folder+"/assets/css", "bootstrap.min.css") CopyToPublic(CssBox, folder+"/assets/css", "bootstrap.min.css")
CopyToPublic(CssBox, folder+"/assets/css", "base.css") CopyToPublic(CssBox, folder+"/assets/css", "base.css")
//CopyToPublic(JsBox, folder+"/assets/js", "bootstrap.min.js") //CopyToPublic(JsBox, folder+"/assets/js", "bootstrap.min.js")

View File

@ -1,3 +1,4 @@
{{$s := .Service}}
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@ -13,7 +14,7 @@
<script src="/js/Chart.bundle.min.js"></script> <script src="/js/Chart.bundle.min.js"></script>
{{end}} {{end}}
<title>Statup | {{.Name}} Service</title> <title>Statup | {{$s.Name}} Service</title>
</head> </head>
<body> <body>
@ -25,14 +26,14 @@
<div class="col-12 mb-4"> <div class="col-12 mb-4">
{{if .Online }} {{if $s.Online }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span> <span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
{{ else }} {{ else }}
<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">{{ .Name }} <h4 class="mt-2">{{ $s.Name }}
{{if .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 }}
<span class="badge bg-danger float-right d-none d-md-block">OFFLINE</span> <span class="badge bg-danger float-right d-none d-md-block">OFFLINE</span>
@ -41,28 +42,42 @@
<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">{{.Online24}}%</span> <span class="lg_number">{{$s.Online24}}%</span>
Online last 24 Hours Online last 24 Hours
</div> </div>
<div class="col-4"> <div class="col-4">
<span class="lg_number">{{.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">{{.TotalUptime}}%</span> <span class="lg_number">{{$s.TotalUptime}}%</span>
Total Uptime Total Uptime
</div> </div>
</div> </div>
<div class="chart-container"> <div class="chart-container" style="height: 250px">
<canvas id="service_{{ .Id }}"></canvas> <canvas id="service_{{ $s.Id }}"></canvas>
</div> </div>
{{ if .LimitedFailures }} <form id="service_date_form" class="row mt-2 mb-3">
<span id="start_date" class="text-muted small float-left pointer">{{FromUnix .Start}}</span>
<span id="end_date" class="text-muted small float-right pointer" style="position: absolute;right: 0;">{{FromUnix .End}}</span>
<input type="hidden" name="start" class="form-control" id="service_start" spellcheck="false">
<input type="hidden" name="end" class="form-control" id="service_end" spellcheck="false">
<button type="submit" class="btn btn-light btn-block btn-sm mt-2">Set Timeframe</button>
<div id="start_container"></div>
<div id="end_container"></div>
</form>
{{if not $s.Online}}
<div class="col-12 small text-center mt-3 text-muted">{{$s.DowntimeText}}</div>
{{end}}
{{ if $s.LimitedFailures }}
<div class="list-group mt-3 mb-4"> <div class="list-group mt-3 mb-4">
{{ range .LimitedFailures }} {{ range $s.LimitedFailures }}
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start"> <a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{.ParseError}}</h5> <h5 class="mb-1">{{.ParseError}}</h5>
@ -83,82 +98,82 @@
<h3>Edit Service</h3> <h3>Edit Service</h3>
<form action="/service/{{.Id}}" method="POST"> <form action="/service/{{$s.Id}}" method="POST">
<div class="form-group row"> <div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label> <label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="name" class="form-control" id="service_name" value="{{.Name}}" placeholder="Name" required spellcheck="false"> <input type="text" name="name" class="form-control" id="service_name" value="{{$s.Name}}" placeholder="Name" required spellcheck="false">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label> <label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select name="check_type" class="form-control" id="service_type" value="{{.Type}}"> <select name="check_type" class="form-control" id="service_type" value="{{$s.Type}}">
<option value="http" {{if eq .Type "http"}}selected{{end}}>HTTP Service</option> <option value="http" {{if eq $s.Type "http"}}selected{{end}}>HTTP Service</option>
<option value="tcp" {{if eq .Type "tcp"}}selected{{end}}>TCP Service</option> <option value="tcp" {{if eq $s.Type "tcp"}}selected{{end}}>TCP Service</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label> <label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="service_url" value="{{.Domain}}" placeholder="https://google.com" required autocapitalize="false" spellcheck="false"> <input type="text" name="domain" class="form-control" id="service_url" value="{{$s.Domain}}" placeholder="https://google.com" required autocapitalize="false" spellcheck="false">
</div> </div>
</div> </div>
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}"> <div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label> <label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select name="method" class="form-control" id="service_check_type" value="{{.Method}}"> <select name="method" class="form-control" id="service_check_type" value="{{$s.Method}}">
<option value="GET" {{if eq .Method "GET"}}selected{{end}}>GET</option> <option value="GET" {{if eq $s.Method "GET"}}selected{{end}}>GET</option>
<option value="POST" {{if eq .Method "POST"}}selected{{end}}>POST</option> <option value="POST" {{if eq $s.Method "POST"}}selected{{end}}>POST</option>
<option value="DELETE" {{if eq .Method "DELETE"}}selected{{end}}>DELETE</option> <option value="DELETE" {{if eq $s.Method "DELETE"}}selected{{end}}>DELETE</option>
<option value="PATCH" {{if eq .Method "PATCH"}}selected{{end}}>PATCH</option> <option value="PATCH" {{if eq $s.Method "PATCH"}}selected{{end}}>PATCH</option>
<option value="PUT" {{if eq .Method "PUT"}}selected{{end}}>PUT</option> <option value="PUT" {{if eq $s.Method "PUT"}}selected{{end}}>PUT</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row{{if ne .Method "POST"}} d-none{{end}}"> <div class="form-group row{{if ne $s.Method "POST"}} d-none{{end}}">
<label for="post_data" class="col-sm-4 col-form-label">Optional Post Data (JSON)</label> <label for="post_data" class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="false" spellcheck="false">{{.PostData}}</textarea> <textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="false" spellcheck="false">{{$s.PostData}}</textarea>
<small id="emailHelp" class="form-text text-muted">You can insert <a target="_blank" href="https://regex101.com/r/I5bbj9/1">Regex</a> to validate the response</small> <small id="emailHelp" class="form-text text-muted">You can insert <a target="_blank" href="https://regex101.com/r/I5bbj9/1">Regex</a> to validate the response</small>
</div> </div>
</div> </div>
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}"> <div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label> <label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="false" spellcheck="false">{{.Expected}}</textarea> <textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="false" spellcheck="false">{{$s.Expected}}</textarea>
</div> </div>
</div> </div>
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}"> <div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label> <label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="service_response_code"> <input type="number" name="expected_status" class="form-control" value="{{$s.ExpectedStatus}}" id="service_response_code">
</div> </div>
</div> </div>
<div class="form-group row{{if eq .Type "http"}} d-none{{end}}"> <div class="form-group row{{if eq $s.Type "http"}} d-none{{end}}">
<label for="service_port" class="col-sm-4 col-form-label">TCP Port</label> <label for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{.Port}}" id="service_port" placeholder="8080"> <input type="number" name="port" class="form-control" value="{{$s.Port}}" id="service_port" placeholder="8080">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label> <label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{.Interval}}" min="1" id="service_interval" required> <input type="number" name="interval" class="form-control" value="{{$s.Interval}}" min="1" id="service_interval" required>
<small id="emailHelp" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small> <small id="emailHelp" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label> <label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="timeout" class="form-control" value="{{.Timeout}}" id="service_timeout" min="1"> <input type="number" name="timeout" class="form-control" value="{{$s.Timeout}}" id="service_timeout" min="1">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">List Order</label> <label for="order" class="col-sm-4 col-form-label">List Order</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="order" class="form-control" min="0" value="{{.Order}}" id="order"> <input type="number" name="order" class="form-control" min="0" value="{{$s.Order}}" id="order">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -166,34 +181,33 @@
<button type="submit" class="btn btn-success btn-block">Update Service</button> <button type="submit" class="btn btn-success btn-block">Update Service</button>
</div> </div>
<div class="col-6"> <div class="col-6">
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a> <a href="/service/{{ $s.Id }}/delete_failures" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<div class="col-12 mt-4{{if eq .Type "tcp"}} d-none{{end}}"> <div class="col-12 mt-4{{if eq $s.Type "tcp"}} d-none{{end}}">
<h3>Last Response</h3> <h3>Last Response</h3>
<textarea rows="8" class="form-control" readonly>{{ .LastResponse }}</textarea> <textarea rows="8" class="form-control" readonly>{{ $s.LastResponse }}</textarea>
<div class="form-group row mt-2"> <div class="form-group row mt-2">
<label for="last_status_code" class="col-sm-3 col-form-label">HTTP Status Code</label> <label for="last_status_code" class="col-sm-3 col-form-label">HTTP Status Code</label>
<div class="col-sm-2"> <div class="col-sm-2">
<input type="text" id="last_status_code" class="form-control" value="{{ .LastStatusCode }}" readonly> <input type="text" id="last_status_code" class="form-control" value="{{ $s.LastStatusCode }}" readonly>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 mt-4{{if eq .Type "tcp"}} d-none{{end}}"> <div class="col-12 mt-4{{if eq $s.Type "tcp"}} d-none{{end}}">
<h3>Service Checkins</h3> <h3>Service Checkins</h3>
{{ range .Checkins }} {{ range $s.Checkins }}
<h5>Check #{{.Id}} <span class="badge online_badge float-right">Checked in {{.Ago}}</span></h5> <h5>Check #{{.Id}} <span class="badge online_badge float-right">Checked in {{.Ago}}</span></h5>
<input type="text" class="form-control" value="https://domainhere.com/api/checkin/{{.Api}}"> <input type="text" class="form-control" value="https://domainhere.com/api/checkin/{{.Api}}">
{{ end }} {{ end }}
<form action="/service/{{.Id}}/checkin" method="POST"> <form action="/service/{{$s.Id}}/checkin" method="POST">
<div class="form-group row"> <div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Check Interval (in seconds)</label> <label for="service_name" class="col-sm-4 col-form-label">Check Interval (in seconds)</label>
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
@ -222,15 +236,58 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js"></script>
<script src="https://assets.statup.io/pikaday.js"></script>
<script src="https://assets.statup.io/main.js"></script> <script src="https://assets.statup.io/main.js"></script>
{{ else }} {{ else }}
<script src="/js/jquery-3.3.1.min.js"></script> <script src="/js/jquery-3.3.1.min.js"></script>
<script src="/js/bootstrap.min.js"></script> <script src="/js/bootstrap.min.js"></script>
<script src="/js/Chart.bundle.min.js"></script> <script src="/js/Chart.bundle.min.js"></script>
<script src="/js/pikaday.js"></script>
<script src="/js/main.js"></script> <script src="/js/main.js"></script>
{{end}} {{end}}
<script src="/charts/{{.Id}}.js"></script> <script>
var startPick = new Pikaday({
field: $('#service_start')[0],
bound: false,
trigger: $("#start_date"),
container: $("#start_container")[0],
maxDate: new Date(),
onSelect: function(date) {
$('#service_start')[0].value = Math.round(date.getTime() / 1000);
this.hide();
}
});
var endPick = new Pikaday({
field: $('#service_end')[0],
bound: false,
trigger: $("#end_date"),
container: $("#end_container")[0],
maxDate: new Date(),
onSelect: function(date) {
$('#service_end')[0].value = Math.round(date.getTime() / 1000);
this.hide();
}
});
startPick.setDate(new Date({{.Start}}*1000));
endPick.setDate(new Date({{.End}}*1000));
startPick.hide()
endPick.hide()
$("#start_date").click(function(e) {
startPick.show()
})
$("#end_date").click(function(e) {
endPick.show()
})
</script>
<script src="/charts/{{$s.Id}}.js?start={{.Start}}&end={{.End}}"></script>
</body> </body>
</html> </html>

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package types package types
import ( import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package types package types
import ( import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package types package types
import ( import (

View File

@ -47,6 +47,7 @@ type Service struct {
DnsLookup float64 `gorm:"-" json:"dns_lookup_time"` DnsLookup float64 `gorm:"-" json:"dns_lookup_time"`
Failures []interface{} `gorm:"-" json:"failures,omitempty"` Failures []interface{} `gorm:"-" json:"failures,omitempty"`
Checkins []*Checkin `gorm:"-" json:"checkins,omitempty"` Checkins []*Checkin `gorm:"-" json:"checkins,omitempty"`
Range [2]time.Time `gorm:"-" json:"-"`
} }
type ServiceInterface interface { type ServiceInterface interface {

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package types package types
import ( import (