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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,6 +16,7 @@
 | 
			
		|||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,15 +25,16 @@ 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"`
 | 
			
		||||
	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       string        `gorm:"not null;column:post_data" json:"post_data"`
 | 
			
		||||
	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"`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@
 | 
			
		|||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +28,7 @@ type User struct {
 | 
			
		|||
	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"`
 | 
			
		||||
	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