mirror of https://github.com/statping/statping
password fix - Messages for service - fixed null bool/string - better date lib
parent
52756ee359
commit
6c28635763
|
@ -165,7 +165,7 @@ func (s *Service) checkHttp(record bool) *Service {
|
|||
|
||||
var response *http.Response
|
||||
if s.Method == "POST" {
|
||||
response, err = client.Post(s.Domain, "application/json", bytes.NewBuffer([]byte(s.PostData)))
|
||||
response, err = client.Post(s.Domain, "application/json", bytes.NewBuffer([]byte(s.PostData.String)))
|
||||
} else {
|
||||
response, err = client.Get(s.Domain)
|
||||
}
|
||||
|
@ -190,11 +190,11 @@ func (s *Service) checkHttp(record bool) *Service {
|
|||
s.LastResponse = string(contents)
|
||||
s.LastStatusCode = response.StatusCode
|
||||
|
||||
if s.Expected != "" {
|
||||
if s.Expected.String != "" {
|
||||
if err != nil {
|
||||
utils.Log(2, err)
|
||||
}
|
||||
match, err := regexp.MatchString(s.Expected, string(contents))
|
||||
match, err := regexp.MatchString(s.Expected.String, string(contents))
|
||||
if err != nil {
|
||||
utils.Log(2, err)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-yaml/yaml"
|
||||
|
@ -71,9 +72,8 @@ func LoadUsingEnv() (*DbConfig, error) {
|
|||
CoreApp.Name = os.Getenv("NAME")
|
||||
CoreApp.Domain = os.Getenv("DOMAIN")
|
||||
CoreApp.DbConnection = Configs.DbConn
|
||||
if os.Getenv("USE_CDN") == "true" {
|
||||
CoreApp.UseCdn = true
|
||||
}
|
||||
CoreApp.UseCdn = sql.NullBool{os.Getenv("USE_CDN") == "true", true}
|
||||
|
||||
err := Configs.Connect(true, utils.Directory)
|
||||
if err != nil {
|
||||
utils.Log(4, err)
|
||||
|
@ -94,7 +94,7 @@ func LoadUsingEnv() (*DbConfig, error) {
|
|||
Username: "admin",
|
||||
Password: "admin",
|
||||
Email: "info@admin.com",
|
||||
Admin: true,
|
||||
Admin: sql.NullBool{true, true},
|
||||
})
|
||||
_, err := admin.Create()
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/hunterlong/statup/core/notifier"
|
||||
"github.com/hunterlong/statup/source"
|
||||
|
@ -147,9 +148,7 @@ func SelectCore() (*Core, error) {
|
|||
}
|
||||
CoreApp.DbConnection = Configs.DbConn
|
||||
CoreApp.Version = VERSION
|
||||
if os.Getenv("USE_CDN") == "true" {
|
||||
CoreApp.UseCdn = true
|
||||
}
|
||||
CoreApp.UseCdn = sql.NullBool{os.Getenv("USE_CDN") == "true", true}
|
||||
return CoreApp, db.Error
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,11 @@ func checkinDB() *gorm.DB {
|
|||
return DbSession.Model(&types.Checkin{})
|
||||
}
|
||||
|
||||
// messagesDb returns the Checkin records for a service
|
||||
func messagesDb() *gorm.DB {
|
||||
return DbSession.Model(&types.Message{})
|
||||
}
|
||||
|
||||
// checkinHitsDB returns the 'hits' from the Checkin record
|
||||
func checkinHitsDB() *gorm.DB {
|
||||
return DbSession.Model(&types.CheckinHit{})
|
||||
|
@ -329,6 +334,7 @@ func (db *DbConfig) DropDatabase() error {
|
|||
err = DbSession.DropTableIfExists("hits")
|
||||
err = DbSession.DropTableIfExists("services")
|
||||
err = DbSession.DropTableIfExists("users")
|
||||
err = DbSession.DropTableIfExists("messages")
|
||||
return err.Error
|
||||
}
|
||||
|
||||
|
@ -343,6 +349,7 @@ func (db *DbConfig) CreateDatabase() error {
|
|||
err = DbSession.CreateTable(&types.Hit{})
|
||||
err = DbSession.CreateTable(&types.Service{})
|
||||
err = DbSession.CreateTable(&types.User{})
|
||||
err = DbSession.CreateTable(&types.Message{})
|
||||
utils.Log(1, "Statup Database Created")
|
||||
return err.Error
|
||||
}
|
||||
|
@ -361,7 +368,7 @@ func (db *DbConfig) MigrateDatabase() error {
|
|||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Checkin{}, &types.CheckinHit{}, ¬ifier.Notification{}).Table("core").AutoMigrate(&types.Core{})
|
||||
tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Checkin{}, &types.CheckinHit{}, ¬ifier.Notification{}).Table("core").AutoMigrate(&types.Core{})
|
||||
if tx.Error != nil {
|
||||
tx.Rollback()
|
||||
utils.Log(3, fmt.Sprintf("Statup Database could not be migrated: %v", tx.Error))
|
||||
|
|
|
@ -17,6 +17,7 @@ package core
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/hunterlong/statup/source"
|
||||
"github.com/hunterlong/statup/utils"
|
||||
|
@ -32,7 +33,7 @@ func ExportIndexHTML() string {
|
|||
source.Assets()
|
||||
injectDatabase()
|
||||
CoreApp.SelectAllServices(false)
|
||||
CoreApp.UseCdn = true
|
||||
CoreApp.UseCdn = sql.NullBool{true, true}
|
||||
for _, srv := range CoreApp.Services {
|
||||
service := srv.(*Service)
|
||||
service.Check(true)
|
||||
|
@ -60,7 +61,7 @@ func ExportIndexHTML() string {
|
|||
return CoreApp
|
||||
},
|
||||
"USE_CDN": func() bool {
|
||||
return CoreApp.UseCdn
|
||||
return CoreApp.UseCdn.Bool
|
||||
},
|
||||
"underscore": func(html string) string {
|
||||
return utils.UnderScoreString(html)
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
// 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 core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hunterlong/statup/types"
|
||||
"github.com/hunterlong/statup/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
*types.Message
|
||||
}
|
||||
|
||||
// SelectServiceMessages returns all messages for a service
|
||||
func SelectServiceMessages(id int64) []*Message {
|
||||
var message []*Message
|
||||
messagesDb().Where("service = ?", id).Limit(10).Find(&message)
|
||||
return message
|
||||
}
|
||||
|
||||
// ReturnMessage will convert *types.Message to *core.Message
|
||||
func ReturnMessage(m *types.Message) *Message {
|
||||
return &Message{m}
|
||||
}
|
||||
|
||||
// SelectMessages returns all messages
|
||||
func SelectMessages() ([]*Message, error) {
|
||||
var messages []*Message
|
||||
db := messagesDb().Find(&messages).Order("id desc")
|
||||
return messages, db.Error
|
||||
}
|
||||
|
||||
// SelectMessage returns a Message based on the ID passed
|
||||
func SelectMessage(id int64) (*Message, error) {
|
||||
var message Message
|
||||
db := messagesDb().Where("id = ?", id).Find(&message)
|
||||
return &message, db.Error
|
||||
}
|
||||
|
||||
// Create will create a Message and insert it into the database
|
||||
func (m *Message) Create() (int64, error) {
|
||||
m.CreatedAt = time.Now().UTC()
|
||||
db := messagesDb().Create(m)
|
||||
if db.Error != nil {
|
||||
utils.Log(3, fmt.Sprintf("Failed to create message %v #%v: %v", m.Title, m.Id, db.Error))
|
||||
return 0, db.Error
|
||||
}
|
||||
return m.Id, nil
|
||||
}
|
||||
|
||||
// Delete will delete a Message from database
|
||||
func (m *Message) Delete() error {
|
||||
db := messagesDb().Delete(m)
|
||||
return db.Error
|
||||
}
|
||||
|
||||
// Update will update a Message in the database
|
||||
func (m *Message) Update() (*Message, error) {
|
||||
db := messagesDb().Update(m)
|
||||
if db.Error != nil {
|
||||
utils.Log(3, fmt.Sprintf("Failed to update message %v #%v: %v", m.Title, m.Id, db.Error))
|
||||
return nil, db.Error
|
||||
}
|
||||
return m, nil
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/hunterlong/statup/types"
|
||||
"github.com/hunterlong/statup/utils"
|
||||
|
@ -60,11 +61,11 @@ func InsertSampleData() error {
|
|||
Name: "JSON API Tester",
|
||||
Domain: "https://jsonplaceholder.typicode.com/posts",
|
||||
ExpectedStatus: 201,
|
||||
Expected: `(title)": "((\\"|[statup])*)"`,
|
||||
Expected: utils.NullString(`(title)": "((\\"|[statup])*)"`),
|
||||
Interval: 30,
|
||||
Type: "http",
|
||||
Method: "POST",
|
||||
PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`,
|
||||
PostData: utils.NullString(`{ "title": "statup", "body": "bar", "userId": 19999 }`),
|
||||
Timeout: 30,
|
||||
Order: 4,
|
||||
})
|
||||
|
@ -155,7 +156,7 @@ func insertSampleCore() error {
|
|||
Domain: "http://localhost:8080",
|
||||
Version: "test",
|
||||
CreatedAt: time.Now(),
|
||||
UseCdn: false,
|
||||
UseCdn: sql.NullBool{false, true},
|
||||
}
|
||||
query := coreDB().Create(core)
|
||||
return query.Error
|
||||
|
@ -167,14 +168,14 @@ func insertSampleUsers() {
|
|||
Username: "testadmin",
|
||||
Password: "password123",
|
||||
Email: "info@betatude.com",
|
||||
Admin: true,
|
||||
Admin: sql.NullBool{true, true},
|
||||
})
|
||||
|
||||
u3 := ReturnUser(&types.User{
|
||||
Username: "testadmin2",
|
||||
Password: "password123",
|
||||
Email: "info@adminhere.com",
|
||||
Admin: true,
|
||||
Admin: sql.NullBool{true, true},
|
||||
})
|
||||
|
||||
u2.Create()
|
||||
|
|
|
@ -394,6 +394,12 @@ func (s *Service) Create(check bool) (int64, error) {
|
|||
return s.Id, nil
|
||||
}
|
||||
|
||||
// Messages returns all Messages for a Service
|
||||
func (s *Service) Messages() []*Message {
|
||||
messages := SelectServiceMessages(s.Id)
|
||||
return messages
|
||||
}
|
||||
|
||||
// ServicesCount returns the amount of services inside the []*core.Services slice
|
||||
func (c *Core) ServicesCount() int {
|
||||
return len(c.Services)
|
||||
|
|
|
@ -35,7 +35,7 @@ func ReturnUser(u *types.User) *user {
|
|||
// SelectUser returns the user based on the user's ID.
|
||||
func SelectUser(id int64) (*user, error) {
|
||||
var user user
|
||||
err := usersDB().First(&user, id)
|
||||
err := usersDB().Where("id = ?", id).First(&user)
|
||||
return &user, err.Error
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,6 @@ func (u *user) Delete() error {
|
|||
|
||||
// Update will update the user's record in database
|
||||
func (u *user) Update() error {
|
||||
u.Password = utils.HashPassword(u.Password)
|
||||
u.ApiKey = utils.NewSHA1Hash(5)
|
||||
u.ApiSecret = utils.NewSHA1Hash(10)
|
||||
return usersDB().Update(u).Error
|
||||
|
|
|
@ -108,8 +108,11 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
|
|||
"len": func(g []types.ServiceInterface) int {
|
||||
return len(g)
|
||||
},
|
||||
"IsNil": func(g interface{}) bool {
|
||||
return g == nil
|
||||
},
|
||||
"USE_CDN": func() bool {
|
||||
return core.CoreApp.UseCdn
|
||||
return core.CoreApp.UseCdn.Bool
|
||||
},
|
||||
"Type": func(g interface{}) []string {
|
||||
fooType := reflect.TypeOf(g)
|
||||
|
@ -163,6 +166,9 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
|
|||
"NewCheckin": func() *types.Checkin {
|
||||
return new(types.Checkin)
|
||||
},
|
||||
"NewMessage": func() *types.Message {
|
||||
return new(types.Message)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,7 +182,7 @@ func executeResponse(w http.ResponseWriter, r *http.Request, file string, data i
|
|||
return
|
||||
}
|
||||
|
||||
templates := []string{"base.html", "head.html", "nav.html", "footer.html", "scripts.html", "form_service.html", "form_notifier.html", "form_user.html", "form_checkin.html"}
|
||||
templates := []string{"base.html", "head.html", "nav.html", "footer.html", "scripts.html", "form_service.html", "form_notifier.html", "form_user.html", "form_checkin.html", "form_message.html"}
|
||||
|
||||
javascripts := []string{"charts.js", "chart_index.js"}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"github.com/hunterlong/statup/core"
|
||||
"github.com/hunterlong/statup/types"
|
||||
|
@ -108,7 +109,7 @@ func DesktopInit(ip string, port int) {
|
|||
Username: config.Username,
|
||||
Password: config.Password,
|
||||
Email: config.Email,
|
||||
Admin: true,
|
||||
Admin: sql.NullBool{true, true},
|
||||
})
|
||||
admin.Create()
|
||||
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
// 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 handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/hunterlong/statup/core"
|
||||
"github.com/hunterlong/statup/types"
|
||||
"github.com/hunterlong/statup/utils"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func messagesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !IsAuthenticated(r) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
messages, _ := core.SelectMessages()
|
||||
executeResponse(w, r, "messages.html", messages, nil)
|
||||
}
|
||||
|
||||
func deleteMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !IsAuthenticated(r) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
id := utils.StringInt(vars["id"])
|
||||
message, err := core.SelectMessage(id)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/messages", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
message.Delete()
|
||||
http.Redirect(w, r, "/messages", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func viewMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !IsAuthenticated(r) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
id := utils.StringInt(vars["id"])
|
||||
message, err := core.SelectMessage(id)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
executeResponse(w, r, "message.html", message, nil)
|
||||
}
|
||||
|
||||
func updateMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !IsAuthenticated(r) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
id := utils.StringInt(vars["id"])
|
||||
message, err := core.SelectMessage(id)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
r.ParseForm()
|
||||
|
||||
title := r.PostForm.Get("title")
|
||||
description := r.PostForm.Get("description")
|
||||
notifyMethod := r.PostForm.Get("notify_method")
|
||||
notifyUsers := r.PostForm.Get("notify_users")
|
||||
startOn := r.PostForm.Get("start_on")
|
||||
endOn := r.PostForm.Get("end_on")
|
||||
notifyBefore := r.PostForm.Get("notify_before")
|
||||
serviceId := utils.StringInt(r.PostForm.Get("service_id"))
|
||||
|
||||
start, err := time.Parse(utils.FlatpickrTime, startOn)
|
||||
if err != nil {
|
||||
utils.Log(3, err)
|
||||
}
|
||||
end, _ := time.Parse(utils.FlatpickrTime, endOn)
|
||||
before, _ := time.ParseDuration(notifyBefore)
|
||||
|
||||
message.Title = title
|
||||
message.Description = description
|
||||
message.NotifyUsers = utils.NullBool(notifyUsers == "on")
|
||||
message.NotifyMethod = notifyMethod
|
||||
message.StartOn = start.UTC()
|
||||
message.EndOn = end.UTC()
|
||||
message.NotifyBefore = before
|
||||
message.ServiceId = serviceId
|
||||
|
||||
message.Update()
|
||||
executeResponse(w, r, "messages.html", message, "/messages")
|
||||
}
|
||||
|
||||
func createMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !IsAuthenticated(r) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
r.ParseForm()
|
||||
|
||||
title := r.PostForm.Get("title")
|
||||
description := r.PostForm.Get("description")
|
||||
notifyMethod := r.PostForm.Get("notify_method")
|
||||
notifyUsers := r.PostForm.Get("notify_users")
|
||||
startOn := r.PostForm.Get("start_on")
|
||||
endOn := r.PostForm.Get("end_on")
|
||||
notifyBefore := r.PostForm.Get("notify_before")
|
||||
serviceId := utils.StringInt(r.PostForm.Get("service_id"))
|
||||
|
||||
start, _ := time.Parse(utils.FlatpickrTime, startOn)
|
||||
end, _ := time.Parse(utils.FlatpickrTime, endOn)
|
||||
before, _ := time.ParseDuration(notifyBefore)
|
||||
|
||||
message := core.ReturnMessage(&types.Message{
|
||||
Title: title,
|
||||
Description: description,
|
||||
StartOn: start.UTC(),
|
||||
EndOn: end.UTC(),
|
||||
ServiceId: serviceId,
|
||||
NotifyUsers: utils.NullBool(notifyUsers == "on"),
|
||||
NotifyMethod: notifyMethod,
|
||||
NotifyBefore: before,
|
||||
})
|
||||
_, err := message.Create()
|
||||
if err != nil {
|
||||
utils.Log(3, fmt.Sprintf("Error creating message %v", err))
|
||||
}
|
||||
messages, _ := core.SelectMessages()
|
||||
executeResponse(w, r, "messages.html", messages, "/messages")
|
||||
}
|
|
@ -70,6 +70,13 @@ func Router() *mux.Router {
|
|||
r.Handle("/user/{id}", http.HandlerFunc(updateUserHandler)).Methods("POST")
|
||||
r.Handle("/user/{id}/delete", http.HandlerFunc(usersDeleteHandler)).Methods("GET")
|
||||
|
||||
// MESSAGES Routes
|
||||
r.Handle("/messages", http.HandlerFunc(messagesHandler)).Methods("GET")
|
||||
r.Handle("/messages", http.HandlerFunc(createMessageHandler)).Methods("POST")
|
||||
r.Handle("/message/{id}", http.HandlerFunc(viewMessageHandler)).Methods("GET")
|
||||
r.Handle("/message/{id}", http.HandlerFunc(updateMessageHandler)).Methods("POST")
|
||||
r.Handle("/message/{id}/delete", http.HandlerFunc(deleteMessageHandler)).Methods("GET")
|
||||
|
||||
// SETTINGS Routes
|
||||
r.Handle("/settings", http.HandlerFunc(settingsHandler)).Methods("GET")
|
||||
r.Handle("/settings", http.HandlerFunc(saveSettingsHandler)).Methods("POST")
|
||||
|
|
|
@ -104,12 +104,12 @@ func createServiceHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Name: name,
|
||||
Domain: domain,
|
||||
Method: method,
|
||||
Expected: expected,
|
||||
Expected: utils.NullString(expected),
|
||||
ExpectedStatus: status,
|
||||
Interval: interval,
|
||||
Type: checkType,
|
||||
Port: port,
|
||||
PostData: postData,
|
||||
PostData: utils.NullString(postData),
|
||||
Timeout: timeout,
|
||||
Order: order,
|
||||
})
|
||||
|
@ -139,8 +139,11 @@ func servicesDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|||
func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
fields := parseGet(r)
|
||||
r.ParseForm()
|
||||
|
||||
startField := utils.StringInt(fields.Get("start"))
|
||||
endField := utils.StringInt(fields.Get("end"))
|
||||
group := r.Form.Get("group")
|
||||
serv := core.SelectService(utils.StringInt(vars["id"]))
|
||||
if serv == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
@ -156,15 +159,18 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
|
|||
if endField != 0 {
|
||||
end = time.Unix(endField, 0)
|
||||
}
|
||||
if group == "" {
|
||||
group = "hour"
|
||||
}
|
||||
|
||||
data := core.GraphDataRaw(serv, start, end, "hour", "latency")
|
||||
data := core.GraphDataRaw(serv, start, end, group, "latency")
|
||||
|
||||
out := struct {
|
||||
Service *core.Service
|
||||
Start int64
|
||||
End int64
|
||||
Start string
|
||||
End string
|
||||
Data string
|
||||
}{serv, start.Unix(), end.Unix(), data.ToString()}
|
||||
}{serv, start.Format(utils.FlatpickrReadable), end.Format(utils.FlatpickrReadable), data.ToString()}
|
||||
|
||||
executeResponse(w, r, "service.html", out, nil)
|
||||
}
|
||||
|
@ -193,11 +199,11 @@ func servicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||
service.Domain = domain
|
||||
service.Method = method
|
||||
service.ExpectedStatus = status
|
||||
service.Expected = expected
|
||||
service.Expected = utils.NullString(expected)
|
||||
service.Interval = interval
|
||||
service.Type = checkType
|
||||
service.Port = port
|
||||
service.PostData = postData
|
||||
service.PostData = utils.NullString(postData)
|
||||
service.Timeout = timeout
|
||||
service.Order = order
|
||||
|
||||
|
|
|
@ -56,8 +56,8 @@ func saveSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||
app.Style = style
|
||||
}
|
||||
footer := r.PostForm.Get("footer")
|
||||
if footer != app.Footer {
|
||||
app.Footer = footer
|
||||
if footer != app.Footer.String {
|
||||
app.Footer = utils.NullString(footer)
|
||||
}
|
||||
domain := r.PostForm.Get("domain")
|
||||
if domain != app.Domain {
|
||||
|
@ -67,7 +67,7 @@ func saveSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||
timeFloat, _ := strconv.ParseFloat(timezone, 10)
|
||||
app.Timezone = float32(timeFloat)
|
||||
|
||||
app.UseCdn = (r.PostForm.Get("enable_cdn") == "on")
|
||||
app.UseCdn = utils.NullBool(r.PostForm.Get("enable_cdn") == "on")
|
||||
core.CoreApp, _ = core.UpdateCore(app)
|
||||
//notifiers.OnSettingsSaved(core.CoreApp.ToCore())
|
||||
executeResponse(w, r, "settings.html", core.CoreApp, "/settings")
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/hunterlong/statup/core"
|
||||
"github.com/hunterlong/statup/types"
|
||||
"github.com/hunterlong/statup/utils"
|
||||
|
@ -113,7 +114,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Username: config.Username,
|
||||
Password: config.Password,
|
||||
Email: config.Email,
|
||||
Admin: true,
|
||||
Admin: sql.NullBool{true, true},
|
||||
})
|
||||
admin.Create()
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/hunterlong/statup/core"
|
||||
|
@ -52,8 +53,8 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
r.ParseForm()
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.Atoi(vars["id"])
|
||||
user, err := core.SelectUser(int64(id))
|
||||
id := utils.StringInt(vars["id"])
|
||||
user, err := core.SelectUser(id)
|
||||
if err != nil {
|
||||
utils.Log(3, fmt.Sprintf("user error: %v", err))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
@ -62,12 +63,21 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
user.Username = r.PostForm.Get("username")
|
||||
user.Email = r.PostForm.Get("email")
|
||||
user.Admin = (r.PostForm.Get("admin") == "on")
|
||||
isAdmin := r.PostForm.Get("admin") == "on"
|
||||
user.Admin = sql.NullBool{isAdmin, true}
|
||||
password := r.PostForm.Get("password")
|
||||
if password != "##########" {
|
||||
user.Password = utils.HashPassword(password)
|
||||
}
|
||||
user.Update()
|
||||
err = user.Update()
|
||||
if err != nil {
|
||||
utils.Log(3, err)
|
||||
}
|
||||
|
||||
fmt.Println(user.Id)
|
||||
fmt.Println(user.Password)
|
||||
fmt.Println(user.Admin.Bool)
|
||||
|
||||
users, _ := core.SelectAllUsers()
|
||||
executeResponse(w, r, "users.html", users, "/users")
|
||||
}
|
||||
|
@ -87,7 +97,7 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Username: username,
|
||||
Password: password,
|
||||
Email: email,
|
||||
Admin: (admin == "on"),
|
||||
Admin: sql.NullBool{admin == "on", true},
|
||||
})
|
||||
_, err := user.Create()
|
||||
if err != nil {
|
||||
|
|
|
@ -246,7 +246,7 @@ func (u *email) OnTest() error {
|
|||
Method: "GET",
|
||||
Timeout: 20,
|
||||
LastStatusCode: 200,
|
||||
Expected: "test example",
|
||||
Expected: utils.NullString("test example"),
|
||||
LastResponse: "<html>this is an example response</html>",
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ func (w *webhooker) OnTest() error {
|
|||
Method: "GET",
|
||||
Timeout: 20,
|
||||
LastStatusCode: 404,
|
||||
Expected: "test example",
|
||||
Expected: utils.NullString("test example"),
|
||||
LastResponse: "<html>this is an example response</html>",
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
@charset "UTF-8";
|
||||
/* Index Page */
|
||||
/* Status Container */
|
||||
/* Button Colors */
|
||||
|
@ -406,186 +405,6 @@ HTML, BODY {
|
|||
.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: .5;
|
||||
*position: absolute;
|
||||
*top: 0; }
|
||||
.pika-prev:hover,
|
||||
.pika-next:hover {
|
||||
opacity: 1; }
|
||||
.pika-prev.is-disabled,
|
||||
.pika-next.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; }
|
||||
.pika-table th,
|
||||
.pika-table td {
|
||||
width: 14.285714285714286%;
|
||||
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: .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; }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
HTML, BODY {
|
||||
background-color: #fcfcfc; }
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1257
source/js/pikaday.js
1257
source/js/pikaday.js
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,147 @@
|
|||
/* flatpickr v4.5.2, @license MIT */
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.rangePlugin = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
function rangePlugin(config) {
|
||||
if (config === void 0) {
|
||||
config = {};
|
||||
}
|
||||
|
||||
return function (fp) {
|
||||
var dateFormat = "",
|
||||
secondInput,
|
||||
_secondInputFocused,
|
||||
_prevDates;
|
||||
|
||||
var createSecondInput = function createSecondInput() {
|
||||
if (config.input) {
|
||||
secondInput = config.input instanceof Element ? config.input : window.document.querySelector(config.input);
|
||||
} else {
|
||||
secondInput = fp._input.cloneNode();
|
||||
secondInput.removeAttribute("id");
|
||||
secondInput._flatpickr = undefined;
|
||||
}
|
||||
|
||||
if (secondInput.value) {
|
||||
var parsedDate = fp.parseDate(secondInput.value);
|
||||
if (parsedDate) fp.selectedDates.push(parsedDate);
|
||||
}
|
||||
|
||||
secondInput.setAttribute("data-fp-omit", "");
|
||||
|
||||
fp._bind(secondInput, ["focus", "click"], function () {
|
||||
if (fp.selectedDates[1]) {
|
||||
fp.latestSelectedDateObj = fp.selectedDates[1];
|
||||
|
||||
fp._setHoursFromDate(fp.selectedDates[1]);
|
||||
|
||||
fp.jumpToDate(fp.selectedDates[1]);
|
||||
}
|
||||
_secondInputFocused = true;
|
||||
fp.isOpen = false;
|
||||
fp.open(undefined, secondInput);
|
||||
});
|
||||
|
||||
fp._bind(fp._input, ["focus", "click"], function (e) {
|
||||
e.preventDefault();
|
||||
fp.isOpen = false;
|
||||
fp.open();
|
||||
});
|
||||
|
||||
if (fp.config.allowInput) fp._bind(secondInput, "keydown", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
fp.setDate([fp.selectedDates[0], secondInput.value], true, dateFormat);
|
||||
secondInput.click();
|
||||
}
|
||||
});
|
||||
if (!config.input) fp._input.parentNode && fp._input.parentNode.insertBefore(secondInput, fp._input.nextSibling);
|
||||
};
|
||||
|
||||
var plugin = {
|
||||
onParseConfig: function onParseConfig() {
|
||||
fp.config.mode = "range";
|
||||
dateFormat = fp.config.altInput ? fp.config.altFormat : fp.config.dateFormat;
|
||||
},
|
||||
onReady: function onReady() {
|
||||
createSecondInput();
|
||||
fp.config.ignoredFocusElements.push(secondInput);
|
||||
|
||||
if (fp.config.allowInput) {
|
||||
fp._input.removeAttribute("readonly");
|
||||
|
||||
secondInput.removeAttribute("readonly");
|
||||
} else {
|
||||
secondInput.setAttribute("readonly", "readonly");
|
||||
}
|
||||
|
||||
fp._bind(fp._input, "focus", function () {
|
||||
fp.latestSelectedDateObj = fp.selectedDates[0];
|
||||
|
||||
fp._setHoursFromDate(fp.selectedDates[0]);
|
||||
_secondInputFocused = false;
|
||||
fp.jumpToDate(fp.selectedDates[0]);
|
||||
});
|
||||
|
||||
if (fp.config.allowInput) fp._bind(fp._input, "keydown", function (e) {
|
||||
if (e.key === "Enter") fp.setDate([fp._input.value, fp.selectedDates[1]], true, dateFormat);
|
||||
});
|
||||
fp.setDate(fp.selectedDates, false);
|
||||
plugin.onValueUpdate(fp.selectedDates);
|
||||
},
|
||||
onPreCalendarPosition: function onPreCalendarPosition() {
|
||||
if (_secondInputFocused) {
|
||||
fp._positionElement = secondInput;
|
||||
setTimeout(function () {
|
||||
fp._positionElement = fp._input;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
onChange: function onChange() {
|
||||
if (!fp.selectedDates.length) {
|
||||
setTimeout(function () {
|
||||
if (fp.selectedDates.length) return;
|
||||
secondInput.value = "";
|
||||
_prevDates = [];
|
||||
}, 10);
|
||||
}
|
||||
|
||||
if (_secondInputFocused) {
|
||||
setTimeout(function () {
|
||||
secondInput.focus();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
onDestroy: function onDestroy() {
|
||||
if (!config.input) secondInput.parentNode && secondInput.parentNode.removeChild(secondInput);
|
||||
},
|
||||
onValueUpdate: function onValueUpdate(selDates) {
|
||||
if (!secondInput) return;
|
||||
_prevDates = !_prevDates || selDates.length >= _prevDates.length ? selDates.concat() : _prevDates;
|
||||
|
||||
if (_prevDates.length > selDates.length) {
|
||||
var newSelectedDate = selDates[0];
|
||||
var newDates = _secondInputFocused ? [_prevDates[0], newSelectedDate] : [newSelectedDate, _prevDates[1]];
|
||||
fp.setDate(newDates, false);
|
||||
_prevDates = newDates.concat();
|
||||
}
|
||||
|
||||
var _fp$selectedDates$map = fp.selectedDates.map(function (d) {
|
||||
return fp.formatDate(d, dateFormat);
|
||||
});
|
||||
|
||||
var _fp$selectedDates$map2 = _fp$selectedDates$map[0];
|
||||
fp._input.value = _fp$selectedDates$map2 === void 0 ? "" : _fp$selectedDates$map2;
|
||||
var _fp$selectedDates$map3 = _fp$selectedDates$map[1];
|
||||
secondInput.value = _fp$selectedDates$map3 === void 0 ? "" : _fp$selectedDates$map3;
|
||||
}
|
||||
};
|
||||
return plugin;
|
||||
};
|
||||
}
|
||||
|
||||
return rangePlugin;
|
||||
|
||||
})));
|
|
@ -468,6 +468,4 @@ HTML,BODY {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
@import './pikaday';
|
||||
|
||||
@import './mobile';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{ define "footer"}}
|
||||
<div class="footer text-center mb-4">
|
||||
{{ if CoreApp.Footer}}
|
||||
{{ CoreApp.Footer }}
|
||||
{{ if CoreApp.Footer.String }}
|
||||
{{ CoreApp.Footer.String }}
|
||||
{{ else }}
|
||||
<a href="https://github.com/hunterlong/statup" target="_blank">Statup {{VERSION}} made with <i class="text-danger fas fa-heart"></i></a> | <a href="/dashboard">Dashboard</a>
|
||||
{{ end }}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
{{define "form_message"}}
|
||||
{{$message := .}}
|
||||
<form action="{{if ne .Id 0}}/message/{{.Id}}{{else}}/messages{{end}}" method="POST">
|
||||
<div class="form-group row">
|
||||
<label for="username" class="col-sm-4 col-form-label">Title</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" name="title" class="form-control" value="{{.Title}}" id="title" placeholder="Message Title" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="username" class="col-sm-4 col-form-label">Description</label>
|
||||
<div class="col-sm-8">
|
||||
<textarea rows="5" name="description" class="form-control" id="description" required>{{.Description}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Message Date Range</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="{{.StartOn}}" required>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="{{.EndOn}}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="form-control" name="service_id" id="service_id">
|
||||
<option value="0" {{if eq (ToString .ServiceId) "0"}}selected{{end}}>Global Message</option>
|
||||
{{range Services}}
|
||||
{{$s := .Select}}
|
||||
<option value="{{$s.Id}}" {{if eq $message.ServiceId $s.Id}}selected{{end}}>{{$s.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="notify_method" class="col-sm-4 col-form-label">Notification Method</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" name="notify_method" class="form-control" id="notify_method" value="{{.NotifyUsers.Bool}}" placeholder="email">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" name="notify_before" class="form-control" id="notify_before" value="{{.NotifyBefore}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update Message{{else}}Create Message{{end}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
|
@ -41,14 +41,14 @@
|
|||
<div class="form-group row{{if ne .Method "POST"}} d-none{{end}}">
|
||||
<label for="post_data" class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
|
||||
<div class="col-sm-8">
|
||||
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="false" spellcheck="false" placeholder='{"data": { "method": "success", "id": 148923 } }'>{{.PostData}}</textarea>
|
||||
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="false" spellcheck="false" placeholder='{"data": { "method": "success", "id": 148923 } }'>{{.PostData.String}}</textarea>
|
||||
<small class="form-text text-muted">Insert a JSON string to send data to the endpoint.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
|
||||
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
|
||||
<div class="col-sm-8">
|
||||
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="false" spellcheck="false" placeholder='(method)": "((\\"|[success])*)"'>{{.Expected}}</textarea>
|
||||
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="false" spellcheck="false" placeholder='(method)": "((\\"|[success])*)"'>{{.Expected.String}}</textarea>
|
||||
<small class="form-text text-muted">You can use plain text or insert <a target="_blank" href="https://regex101.com/r/I5bbj9/1">Regex</a> to validate the response</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -60,16 +60,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="form-group row{{if (ne .Type "tcp") and (ne .Type "udp")}} d-none{{end}}">
|
||||
<label id="service_type_label" for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
|
||||
<label for="port" class="col-sm-4 col-form-label">TCP Port</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="number" name="port" class="form-control" value="{{if ne .Port 0}}{{.Port}}{{end}}" id="service_port" placeholder="8080">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
|
||||
<label for="interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="number" name="interval" class="form-control" value="{{if ne .Interval 0}}{{.Interval}}{{else}}60{{end}}" 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="interval" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<span class="switch">
|
||||
<input type="checkbox" name="admin" class="switch" id="switch-normal"{{if .Admin}} checked{{end}}>
|
||||
<input type="checkbox" name="admin" class="switch" id="switch-normal"{{if .Admin.Bool}} checked{{end}}>
|
||||
<label for="switch-normal">Administrator</label>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{{define "title"}}Statup | {{.Title}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
|
||||
{{template "nav"}}
|
||||
<div class="col-12">
|
||||
<h3>Message {{.Title}}</h3>
|
||||
{{template "form_message" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "extra_css"}}
|
||||
<link rel="stylesheet" href="/css/flatpickr.min.css">
|
||||
{{end}}
|
||||
{{define "extra_scripts"}}
|
||||
<script src="/js/flatpickr.js"></script>
|
||||
<script src="/js/rangePlugin.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$("#start_on").flatpickr({
|
||||
enableTime: true,
|
||||
dateFormat: "Y-m-d H:i",
|
||||
minDate: "today",
|
||||
"plugins": [new rangePlugin({ input: "#end_on"})]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
|
@ -0,0 +1,52 @@
|
|||
{{define "title"}}Statup Messages{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
|
||||
{{template "nav"}}
|
||||
<div class="col-12">
|
||||
<h3>Messages</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.Title}}</td>
|
||||
<td class="text-right" id="message_{{.Id}}">
|
||||
<div class="btn-group">
|
||||
<a href="/message/{{.Id}}" class="btn btn-outline-secondary"><i class="fas fa-exclamation-triangle"></i> Edit</a>
|
||||
<a href="/message/{{.Id}}/delete" class="btn btn-danger confirm-btn" data-id="message_{{.Id}}"><i class="fas fa-times"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Create Message</h3>
|
||||
|
||||
{{template "form_message" NewMessage}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "extra_css"}}
|
||||
<link rel="stylesheet" href="/css/flatpickr.min.css">
|
||||
{{end}}
|
||||
{{define "extra_scripts"}}
|
||||
<script src="/js/flatpickr.js"></script>
|
||||
<script src="/js/rangePlugin.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$("#start_on").flatpickr({
|
||||
enableTime: true,
|
||||
dateFormat: "Y-m-d H:i",
|
||||
minDate: "today",
|
||||
"plugins": [new rangePlugin({ input: "#end_on"})]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
|
@ -16,6 +16,9 @@
|
|||
<li class="nav-item{{ if eq URL "/users" }} active{{ end }}">
|
||||
<a class="nav-link" href="/users">Users</a>
|
||||
</li>
|
||||
<li class="nav-item{{ if eq URL "/messages" }} active{{ end }}">
|
||||
<a class="nav-link" href="/messages">Messages</a>
|
||||
</li>
|
||||
<li class="nav-item{{ if eq URL "/settings" }} active{{ end }}">
|
||||
<a class="nav-link" href="/settings">Settings</a>
|
||||
</li>
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
<div class="col-12 mb-4">
|
||||
|
||||
{{if $s.Online }}
|
||||
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
|
||||
{{ else }}
|
||||
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
|
||||
{{end}}
|
||||
{{if $s.Online }}
|
||||
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
|
||||
{{ else }}
|
||||
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
|
||||
{{end}}
|
||||
|
||||
<h4 class="mt-2">{{ $s.Name }}
|
||||
{{if $s.Online }}
|
||||
|
@ -46,11 +46,11 @@
|
|||
</div>
|
||||
|
||||
<form id="service_date_form" class="col-12 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>
|
||||
<input type="text" class="d-none" name="start" id="service_start" data-input>
|
||||
<span data-toggle title="toggle" id="start_date" class="text-muted small float-left pointer mt-2">{{.Start}} to {{.End}}</span>
|
||||
<button type="submit" class="btn btn-light btn-sm mt-2">Set Timeframe</button>
|
||||
<input type="text" class="d-none" name="end" id="service_end" data-input>
|
||||
|
||||
<div id="start_container"></div>
|
||||
<div id="end_container"></div>
|
||||
</form>
|
||||
|
@ -59,6 +59,15 @@
|
|||
<div class="col-12 small text-center mt-3 text-muted">{{$s.DowntimeText}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if $s.Messages}}
|
||||
{{range $s.Messages}}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h3>{{.Title}}</h3>
|
||||
<span>{{safe .Description}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{ if $s.LimitedFailures }}
|
||||
<div class="list-group mt-3 mb-4">
|
||||
{{ range $s.LimitedFailures }}
|
||||
|
@ -137,14 +146,15 @@
|
|||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "extra_scripts"}}
|
||||
{{if USE_CDN}}
|
||||
<script src="https://assets.statup.io/pikaday.js"></script>
|
||||
{{ else }}
|
||||
<script src="/js/pikaday.js"></script>
|
||||
{{define "extra_css"}}
|
||||
<link rel="stylesheet" href="/css/flatpickr.min.css">
|
||||
{{end}}
|
||||
{{define "extra_scripts"}}
|
||||
{{$s := .Service}}
|
||||
<script src="/js/flatpickr.js"></script>
|
||||
<script src="/js/rangePlugin.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
var ctx = document.getElementById("service").getContext('2d');
|
||||
|
||||
|
@ -208,45 +218,29 @@
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
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()
|
||||
});
|
||||
|
||||
AjaxChart(chartdata,{{$s.Id}},{{.Start}},{{.End}},"hour");
|
||||
|
||||
let startDate = $("#service_start").flatpickr({
|
||||
enableTime: false,
|
||||
static: true,
|
||||
altInput: true,
|
||||
altFormat: "U",
|
||||
maxDate: "today",
|
||||
dateFormat: "F j, Y",
|
||||
onChange: function(selectedDates, dateStr, instance) {
|
||||
var one = Math.round((new Date(selectedDates[0])).getTime() / 1000);
|
||||
var two = Math.round((new Date(selectedDates[1])).getTime() / 1000);
|
||||
$("#service_start").val(one);
|
||||
$("#service_end").val(two);
|
||||
$("#start_date").html(dateStr);
|
||||
},
|
||||
"plugins": [new rangePlugin({ input: "#service_end"})]
|
||||
});
|
||||
|
||||
$("#start_date").click(function(e) {
|
||||
startDate.open()
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label for="footer">Custom Footer</label>
|
||||
<textarea rows="4" name="footer" class="form-control" id="footer">{{ .Footer }}</textarea>
|
||||
<textarea rows="4" name="footer" class="form-control" id="footer">{{ .Footer.String }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -32,11 +33,11 @@ type Core struct {
|
|||
ApiKey string `gorm:"column:api_key" json:"-"`
|
||||
ApiSecret string `gorm:"column:api_secret" json:"-"`
|
||||
Style string `gorm:"not null;column:style" json:"style,omitempty"`
|
||||
Footer string `gorm:"not null;column:footer" json:"footer,omitempty"`
|
||||
Domain string `gorm:"not null;column:domain" json:"domain,omitempty"`
|
||||
Footer sql.NullString `gorm:"not null;column:footer" json:"footer"`
|
||||
Domain string `gorm:"not null;column:domain" json:"domain"`
|
||||
Version string `gorm:"column:version" json:"version"`
|
||||
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
|
||||
UseCdn bool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
|
||||
UseCdn sql.NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
|
||||
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message is for creating Announcements, Alerts and other messages for the end users
|
||||
type Message struct {
|
||||
Id int64 `gorm:"primary_key;column:id"`
|
||||
Title string `gorm:"column:title"`
|
||||
Description string `gorm:"column:description"`
|
||||
StartOn time.Time `gorm:"column:start_on"`
|
||||
EndOn time.Time `gorm:"column:end_on"`
|
||||
ServiceId int64 `gorm:"index;column:service"`
|
||||
NotifyUsers sql.NullBool `gorm:"column:notify_users"`
|
||||
NotifyMethod string `gorm:"column:notify_method"`
|
||||
NotifyBefore time.Duration `gorm:"column:notify_before"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
|
@ -16,37 +16,39 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Service is the main struct for Services
|
||||
type Service struct {
|
||||
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
Domain string `gorm:"column:domain" json:"domain"`
|
||||
Expected string `gorm:"not null;column:expected" json:"expected"`
|
||||
ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status"`
|
||||
Interval int `gorm:"default:30;column:check_interval" json:"check_interval"`
|
||||
Type string `gorm:"column:check_type" json:"type"`
|
||||
Method string `gorm:"column:method" json:"method"`
|
||||
PostData string `gorm:"not null;column:post_data" json:"post_data"`
|
||||
Port int `gorm:"not null;column:port" json:"port"`
|
||||
Timeout int `gorm:"default:30;column:timeout" json:"timeout"`
|
||||
Order int `gorm:"default:0;column:order_id" json:"order_id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
Online bool `gorm:"-" json:"online"`
|
||||
Latency float64 `gorm:"-" json:"latency"`
|
||||
PingTime float64 `gorm:"-" json:"ping_time"`
|
||||
Online24Hours float32 `gorm:"-" json:"online_24_hours"`
|
||||
AvgResponse string `gorm:"-" json:"avg_response"`
|
||||
Running chan bool `gorm:"-" json:"-"`
|
||||
Checkpoint time.Time `gorm:"-" json:"-"`
|
||||
SleepDuration time.Duration `gorm:"-" json:"-"`
|
||||
LastResponse string `gorm:"-" json:"-"`
|
||||
LastStatusCode int `gorm:"-" json:"status_code"`
|
||||
LastOnline time.Time `gorm:"-" json:"last_online"`
|
||||
Failures []interface{} `gorm:"-" json:"failures,omitempty"`
|
||||
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
Domain string `gorm:"column:domain" json:"domain"`
|
||||
Expected sql.NullString `gorm:"not null;column:expected" json:"expected"`
|
||||
ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status"`
|
||||
Interval int `gorm:"default:30;column:check_interval" json:"check_interval"`
|
||||
Type string `gorm:"column:check_type" json:"type"`
|
||||
Method string `gorm:"column:method" json:"method"`
|
||||
PostData sql.NullString `gorm:"not null;column:post_data" json:"post_data"`
|
||||
Port int `gorm:"not null;column:port" json:"port"`
|
||||
Timeout int `gorm:"default:30;column:timeout" json:"timeout"`
|
||||
Order int `gorm:"default:0;column:order_id" json:"order_id"`
|
||||
AllowNotifications sql.NullBool `gorm:"default:false;column:allow_notifications" json:"allow_notifications"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
Online bool `gorm:"-" json:"online"`
|
||||
Latency float64 `gorm:"-" json:"latency"`
|
||||
PingTime float64 `gorm:"-" json:"ping_time"`
|
||||
Online24Hours float32 `gorm:"-" json:"online_24_hours"`
|
||||
AvgResponse string `gorm:"-" json:"avg_response"`
|
||||
Running chan bool `gorm:"-" json:"-"`
|
||||
Checkpoint time.Time `gorm:"-" json:"-"`
|
||||
SleepDuration time.Duration `gorm:"-" json:"-"`
|
||||
LastResponse string `gorm:"-" json:"-"`
|
||||
LastStatusCode int `gorm:"-" json:"status_code"`
|
||||
LastOnline time.Time `gorm:"-" json:"last_online"`
|
||||
Failures []interface{} `gorm:"-" json:"failures,omitempty"`
|
||||
}
|
||||
|
||||
type ServiceInterface interface {
|
||||
|
|
|
@ -16,20 +16,21 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// User is the main struct for Users
|
||||
type User struct {
|
||||
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
||||
Username string `gorm:"type:varchar(100);unique;column:username;" json:"username"`
|
||||
Password string `gorm:"column:password" json:"-"`
|
||||
Email string `gorm:"type:varchar(100);unique;column:email" json:"-"`
|
||||
ApiKey string `gorm:"column:api_key" json:"api_key"`
|
||||
ApiSecret string `gorm:"column:api_secret" json:"-"`
|
||||
Admin bool `gorm:"column:administrator" json:"admin"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
||||
Username string `gorm:"type:varchar(100);unique;column:username;" json:"username"`
|
||||
Password string `gorm:"column:password" json:"-"`
|
||||
Email string `gorm:"type:varchar(100);unique;column:email" json:"-"`
|
||||
ApiKey string `gorm:"column:api_key" json:"api_key"`
|
||||
ApiSecret string `gorm:"column:api_secret" json:"-"`
|
||||
Admin sql.NullBool `gorm:"column:administrator" json:"admin"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
UserInterface `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,12 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
FlatpickrTime = "2006-01-02 15:04"
|
||||
FlatpickrDay = "2006-01-02"
|
||||
FlatpickrReadable = "Mon, 02 Jan 2006"
|
||||
)
|
||||
|
||||
// FormatDuration converts a time.Duration into a string
|
||||
func FormatDuration(d time.Duration) string {
|
||||
var out string
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ararog/timeago"
|
||||
|
@ -43,6 +44,20 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
func NullString(s string) sql.NullString {
|
||||
return sql.NullString{s, true}
|
||||
}
|
||||
|
||||
func NullBool(s bool) sql.NullBool {
|
||||
return sql.NullBool{s, true}
|
||||
}
|
||||
|
||||
func StringPoint(s string) *string {
|
||||
val := new(string)
|
||||
*val = s
|
||||
return val
|
||||
}
|
||||
|
||||
// StringInt converts a string to an int64
|
||||
func StringInt(s string) int64 {
|
||||
num, _ := strconv.Atoi(s)
|
||||
|
|
Loading…
Reference in New Issue