Compare commits
4 Commits
ff402b8fcb
...
c7ad2aa1b6
Author | SHA1 | Date | |
---|---|---|---|
|
c7ad2aa1b6 | ||
|
1ede36b8aa | ||
|
4be53ff357 | ||
|
0494d553d0 |
113
main.go
113
main.go
@ -25,6 +25,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -152,6 +153,8 @@ func main() { // {{{
|
|||||||
service.Register("/configuration", false, false, pageConfiguration)
|
service.Register("/configuration", false, false, pageConfiguration)
|
||||||
service.Register("/configuration/theme", false, false, actionConfigurationTheme)
|
service.Register("/configuration/theme", false, false, actionConfigurationTheme)
|
||||||
service.Register("/configuration/timezone", false, false, actionConfigurationTimezone)
|
service.Register("/configuration/timezone", false, false, actionConfigurationTimezone)
|
||||||
|
service.Register("/configuration/notification", false, false, pageConfigurationNotification)
|
||||||
|
service.Register("/configuration/notification/update/{prio}", false, false, actionConfigurationNotificationUpdate)
|
||||||
service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint)
|
service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint)
|
||||||
|
|
||||||
go nodataLoop()
|
go nodataLoop()
|
||||||
@ -379,9 +382,9 @@ func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
|
|||||||
filenames = append(filenames, componentFilenames...)
|
filenames = append(filenames, componentFilenames...)
|
||||||
logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
|
logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
|
||||||
if flagDev {
|
if flagDev {
|
||||||
tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
|
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
|
||||||
} else {
|
} else {
|
||||||
tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...)
|
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = werr.Wrap(err).Log()
|
err = werr.Wrap(err).Log()
|
||||||
@ -1036,7 +1039,9 @@ func pageConfiguration(w http.ResponseWriter, r *http.Request, _ *session.T) { /
|
|||||||
PAGE: "configuration",
|
PAGE: "configuration",
|
||||||
CONFIG: smonConfig.Settings,
|
CONFIG: smonConfig.Settings,
|
||||||
Data: map[string]any{
|
Data: map[string]any{
|
||||||
"Areas": areas,
|
"Areas": areas,
|
||||||
|
"NotificationServices": notificationManager.Services(),
|
||||||
|
"AvailableServices": notification.AvailableServices(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1071,3 +1076,105 @@ func actionConfigurationTimezone(w http.ResponseWriter, r *http.Request, _ *sess
|
|||||||
w.Header().Add("Location", "/configuration")
|
w.Header().Add("Location", "/configuration")
|
||||||
w.WriteHeader(302)
|
w.WriteHeader(302)
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func pageConfigurationNotification(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
|
// This function is either receiving a type when creating a new service,
|
||||||
|
// or a prio when editing an existing.
|
||||||
|
notificationType := r.URL.Query().Get("type")
|
||||||
|
prioStr := r.URL.Query().Get("prio")
|
||||||
|
|
||||||
|
var service notification.Service
|
||||||
|
if notificationType != "" {
|
||||||
|
// Create a new instance of the selected notification type.
|
||||||
|
service = notification.GetInstance(notificationType)
|
||||||
|
} else {
|
||||||
|
// Find the existing service for editing.
|
||||||
|
prio, err := strconv.Atoi(prioStr)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, werr.Wrap(err).Log())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
service = *notificationManager.GetService(prio)
|
||||||
|
}
|
||||||
|
|
||||||
|
if service == nil {
|
||||||
|
data := struct {
|
||||||
|
typ string
|
||||||
|
prio string
|
||||||
|
}{notificationType, prioStr}
|
||||||
|
err := fmt.Errorf("Invalid service")
|
||||||
|
pageError(w, "/configuration", werr.Wrap(err).WithData(data).Log())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it easier for the user by initiating the prio field
|
||||||
|
// for new notifications to the highest prio + 1.
|
||||||
|
if notificationType != "" {
|
||||||
|
prio := 0
|
||||||
|
for _, svc := range notificationManager.Services() {
|
||||||
|
prio = max(prio, svc.GetPrio()+1)
|
||||||
|
}
|
||||||
|
service.SetPrio(prio)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := Page{
|
||||||
|
LAYOUT: "notification",
|
||||||
|
PAGE: "notification/" + strings.ToLower(service.GetType()),
|
||||||
|
CONFIG: smonConfig.Settings,
|
||||||
|
Data: map[string]any{
|
||||||
|
"Service": service,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
page.Render(w, r)
|
||||||
|
} // }}}
|
||||||
|
func actionConfigurationNotificationUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
|
prioStr := r.PathValue("prio")
|
||||||
|
prio, err := strconv.Atoi(prioStr)
|
||||||
|
if err != nil {
|
||||||
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// prio -1 means a new service, not existing in database.
|
||||||
|
var svc *notification.Service
|
||||||
|
if prio == -1 {
|
||||||
|
emptyService := notification.GetInstance(r.PostFormValue("type"))
|
||||||
|
svc = &emptyService
|
||||||
|
} else {
|
||||||
|
svc = notificationManager.GetService(prio)
|
||||||
|
if svc == nil {
|
||||||
|
pageError(w, "/configuration", werr.New("Service with prio %d not found", prio).Log())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The service is given all data to give it a chance to
|
||||||
|
// validate and throwing errors.
|
||||||
|
err = r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = (*svc).Update(r.PostForm)
|
||||||
|
if err != nil {
|
||||||
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var created bool
|
||||||
|
created, err = UpdateNotificationService(*svc)
|
||||||
|
if err != nil {
|
||||||
|
pageError(w, "/configuration", werr.Wrap(err).Log())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*svc).Commit()
|
||||||
|
|
||||||
|
if created {
|
||||||
|
notificationManager.AddService(*svc)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Location", "/configuration")
|
||||||
|
w.WriteHeader(302)
|
||||||
|
} // }}}
|
||||||
|
@ -6,6 +6,12 @@ import (
|
|||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
allServices []Service
|
||||||
)
|
)
|
||||||
|
|
||||||
func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *slog.Logger) (Service, error) {
|
func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *slog.Logger) (Service, error) {
|
||||||
@ -30,3 +36,25 @@ func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *sl
|
|||||||
|
|
||||||
return nil, werr.New("Unknown notification service, '%s'", t).WithCode("002-0000")
|
return nil, werr.New("Unknown notification service, '%s'", t).WithCode("002-0000")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetInstance(typ string) Service {
|
||||||
|
for _, svc := range allServices {
|
||||||
|
if strings.ToLower(svc.GetType()) == strings.ToLower(typ) {
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AvailableServices() []Service {
|
||||||
|
slices.SortFunc(allServices, func(a, b Service) int {
|
||||||
|
if a.GetType() < b.GetType() {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.GetType() > b.GetType() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
return allServices
|
||||||
|
}
|
||||||
|
@ -11,6 +11,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NTFY struct {
|
type NTFY struct {
|
||||||
@ -18,6 +21,13 @@ type NTFY struct {
|
|||||||
Prio int
|
Prio int
|
||||||
AcknowledgeURL string
|
AcknowledgeURL string
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
||||||
|
exists bool
|
||||||
|
updated Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allServices = append(allServices, &NTFY{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNTFY(config []byte, prio int, ackURL string) (instance *NTFY, err error) {
|
func NewNTFY(config []byte, prio int, ackURL string) (instance *NTFY, err error) {
|
||||||
@ -44,6 +54,22 @@ func (ntfy *NTFY) GetPrio() int {
|
|||||||
return ntfy.Prio
|
return ntfy.Prio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ntfy *NTFY) SetPrio(prio int) {
|
||||||
|
ntfy.Prio = prio
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ntfy *NTFY) SetExists(exists bool) {
|
||||||
|
ntfy.exists = exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ntfy NTFY) Exists() bool {
|
||||||
|
return ntfy.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ntfy *NTFY) String() string {
|
||||||
|
return ntfy.URL
|
||||||
|
}
|
||||||
|
|
||||||
func (ntfy NTFY) Send(problemID int, msg []byte) (err error) {
|
func (ntfy NTFY) Send(problemID int, msg []byte) (err error) {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
var res *http.Response
|
var res *http.Response
|
||||||
@ -81,3 +107,40 @@ func (ntfy NTFY) Send(problemID int, msg []byte) (err error) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ntfy *NTFY) Update(values url.Values) (err error) {
|
||||||
|
updated := NTFY{}
|
||||||
|
ntfy.updated = &updated
|
||||||
|
|
||||||
|
updated.Prio, err = strconv.Atoi(values.Get("prio"))
|
||||||
|
if err != nil {
|
||||||
|
return werr.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
givenURL := values.Get("url")
|
||||||
|
if strings.TrimSpace(givenURL) == "" {
|
||||||
|
return werr.New("URL cannot be empty")
|
||||||
|
}
|
||||||
|
updated.URL = strings.TrimSpace(givenURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ntfy *NTFY) Updated() Service {
|
||||||
|
return ntfy.updated
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ntfy *NTFY) Commit() {
|
||||||
|
updatedNTFY := ntfy.updated.(*NTFY)
|
||||||
|
ntfy.Prio = updatedNTFY.Prio
|
||||||
|
ntfy.URL = updatedNTFY.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ntfy NTFY) JSON() []byte {
|
||||||
|
data := struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}{
|
||||||
|
ntfy.URL,
|
||||||
|
}
|
||||||
|
j, _ := json.Marshal(data)
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,7 +14,15 @@ type Service interface {
|
|||||||
SetLogger(*slog.Logger)
|
SetLogger(*slog.Logger)
|
||||||
GetPrio() int
|
GetPrio() int
|
||||||
GetType() string
|
GetType() string
|
||||||
|
SetPrio(int)
|
||||||
|
SetExists(bool) // Exists in database
|
||||||
|
Exists() bool // Exists in database
|
||||||
|
String() string
|
||||||
Send(int, []byte) error
|
Send(int, []byte) error
|
||||||
|
Update(url.Values) error
|
||||||
|
Updated() Service
|
||||||
|
Commit()
|
||||||
|
JSON() []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@ -28,6 +37,7 @@ func NewManager(logger *slog.Logger) (nm Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (nm *Manager) AddService(service Service) {
|
func (nm *Manager) AddService(service Service) {
|
||||||
|
service.SetExists(true)
|
||||||
nm.services = append(nm.services, service)
|
nm.services = append(nm.services, service)
|
||||||
slices.SortFunc(nm.services, func(a, b Service) int {
|
slices.SortFunc(nm.services, func(a, b Service) int {
|
||||||
if a.GetPrio() < b.GetPrio() {
|
if a.GetPrio() < b.GetPrio() {
|
||||||
@ -40,6 +50,16 @@ func (nm *Manager) AddService(service Service) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (nm *Manager) GetService(prio int) *Service {
|
||||||
|
for _, svc := range nm.services {
|
||||||
|
if svc.GetPrio() == prio {
|
||||||
|
return &svc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (err error) {
|
func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (err error) {
|
||||||
for i, service := range nm.services {
|
for i, service := range nm.services {
|
||||||
nm.logger.Info("notification", "service", service.GetType(), "prio", service.GetPrio())
|
nm.logger.Info("notification", "service", service.GetType(), "prio", service.GetPrio())
|
||||||
@ -49,7 +69,7 @@ func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (er
|
|||||||
} else {
|
} else {
|
||||||
data := struct {
|
data := struct {
|
||||||
ProblemID int
|
ProblemID int
|
||||||
Msg []byte
|
Msg []byte
|
||||||
}{
|
}{
|
||||||
problemID,
|
problemID,
|
||||||
msg,
|
msg,
|
||||||
@ -61,3 +81,7 @@ func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (er
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (nm *Manager) Services() (services []Service) {
|
||||||
|
return nm.services
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
// Standard
|
// Standard
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -17,6 +18,13 @@ type Script struct {
|
|||||||
Prio int
|
Prio int
|
||||||
AcknowledgeURL string
|
AcknowledgeURL string
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
||||||
|
exists bool
|
||||||
|
updated Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allServices = append(allServices, &Script{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScript(config []byte, prio int, ackURL string) (instance *Script, err error) {
|
func NewScript(config []byte, prio int, ackURL string) (instance *Script, err error) {
|
||||||
@ -43,6 +51,22 @@ func (script *Script) GetPrio() int {
|
|||||||
return script.Prio
|
return script.Prio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (script *Script) SetPrio(prio int) {
|
||||||
|
script.Prio = prio
|
||||||
|
}
|
||||||
|
|
||||||
|
func (script *Script) SetExists(exists bool) {
|
||||||
|
script.exists = exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (script Script) Exists() bool {
|
||||||
|
return script.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (script *Script) String() string {
|
||||||
|
return script.Filename
|
||||||
|
}
|
||||||
|
|
||||||
func (script Script) Send(problemID int, msg []byte) (err error) {
|
func (script Script) Send(problemID int, msg []byte) (err error) {
|
||||||
var errbuf strings.Builder
|
var errbuf strings.Builder
|
||||||
cmd := exec.Command(script.Filename, strconv.Itoa(problemID), script.AcknowledgeURL, string(msg))
|
cmd := exec.Command(script.Filename, strconv.Itoa(problemID), script.AcknowledgeURL, string(msg))
|
||||||
@ -68,3 +92,40 @@ func (script Script) Send(problemID int, msg []byte) (err error) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (script *Script) Update(values url.Values) (err error) {
|
||||||
|
updated := Script{}
|
||||||
|
updated.Prio, err = strconv.Atoi(values.Get("prio"))
|
||||||
|
if err != nil {
|
||||||
|
return werr.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
givenFilename := values.Get("filename")
|
||||||
|
if strings.TrimSpace(givenFilename) == "" {
|
||||||
|
return werr.New("Filename cannot be empty")
|
||||||
|
}
|
||||||
|
updated.Filename = strings.TrimSpace(givenFilename)
|
||||||
|
|
||||||
|
script.updated = &updated
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (script *Script) Updated() Service {
|
||||||
|
return script.updated
|
||||||
|
}
|
||||||
|
|
||||||
|
func (script *Script) Commit() {
|
||||||
|
updated := script.updated.(*Script)
|
||||||
|
script.Prio = updated.Prio
|
||||||
|
script.Filename = updated.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
func (script Script) JSON() []byte {
|
||||||
|
data := struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
}{
|
||||||
|
script.Filename,
|
||||||
|
}
|
||||||
|
j, _ := json.Marshal(data)
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
// External
|
// External
|
||||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
|
||||||
// Internal
|
// Internal
|
||||||
"smon/notification"
|
"smon/notification"
|
||||||
@ -67,13 +68,61 @@ func InitNotificationManager() (nm notification.Manager, err error) { // {{{
|
|||||||
|
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func UpdateNotificationService(svc notification.Service) (created bool, err error) { // {{{
|
||||||
|
if svc.Exists() {
|
||||||
|
_, err = service.Db.Conn.Exec(
|
||||||
|
`
|
||||||
|
UPDATE public.notification
|
||||||
|
SET
|
||||||
|
prio=$2,
|
||||||
|
configuration=$3
|
||||||
|
WHERE
|
||||||
|
prio=$1
|
||||||
|
`,
|
||||||
|
svc.GetPrio(),
|
||||||
|
svc.Updated().GetPrio(),
|
||||||
|
svc.Updated().JSON(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
_, err = service.Db.Conn.Exec(
|
||||||
|
`
|
||||||
|
INSERT INTO public.notification(prio, configuration, service)
|
||||||
|
VALUES($1, $2, $3)
|
||||||
|
|
||||||
|
`,
|
||||||
|
svc.Updated().GetPrio(),
|
||||||
|
svc.Updated().JSON(),
|
||||||
|
svc.GetType(),
|
||||||
|
)
|
||||||
|
created = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Check if this is just a duplicated prio, which isn't allowed.
|
||||||
|
pgErr, isPgErr := err.(*pq.Error)
|
||||||
|
if isPgErr && pgErr.Code == "23505" {
|
||||||
|
return false, werr.New("Prio %d is already used by another service", svc.GetPrio())
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, werr.Wrap(err).WithData(
|
||||||
|
struct {
|
||||||
|
Prio int
|
||||||
|
Configuration []byte
|
||||||
|
}{
|
||||||
|
svc.GetPrio(),
|
||||||
|
svc.JSON(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
|
||||||
func AcknowledgeNotification(uuid string) (err error) { // {{{
|
func AcknowledgeNotification(uuid string) (err error) { // {{{
|
||||||
/*
|
/*
|
||||||
_, err = service.Db.Conn.Exec(`UPDATE schedule SET acknowledged=true WHERE schedule_uuid=$1`, uuid)
|
_, err = service.Db.Conn.Exec(`UPDATE schedule SET acknowledged=true WHERE schedule_uuid=$1`, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = werr.Wrap(err).WithData(uuid)
|
err = werr.Wrap(err).WithData(uuid)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
@ -24,3 +24,20 @@
|
|||||||
#areas .area .section.configuration img {
|
#areas .area .section.configuration img {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
#services {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
gap: 10px 24px;
|
||||||
|
background-color: #2979b8;
|
||||||
|
width: min-content;
|
||||||
|
padding: 16px;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
#services .header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#services div {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
@ -43,7 +43,8 @@ button:focus {
|
|||||||
#datapoints,
|
#datapoints,
|
||||||
#problems-list,
|
#problems-list,
|
||||||
#acknowledged-list,
|
#acknowledged-list,
|
||||||
#values {
|
#values,
|
||||||
|
#services {
|
||||||
background-color: #fff !important;
|
background-color: #fff !important;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25);
|
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25);
|
||||||
|
@ -4,3 +4,9 @@ body {
|
|||||||
#areas .area {
|
#areas .area {
|
||||||
box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5);
|
box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
#page-error {
|
||||||
|
border: unset;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #a00;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@ -24,3 +24,20 @@
|
|||||||
#areas .area .section.configuration img {
|
#areas .area .section.configuration img {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
#services {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
gap: 10px 24px;
|
||||||
|
background-color: #333;
|
||||||
|
width: min-content;
|
||||||
|
padding: 16px;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
#services .header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#services div {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
@ -43,7 +43,8 @@ button:focus {
|
|||||||
#datapoints,
|
#datapoints,
|
||||||
#problems-list,
|
#problems-list,
|
||||||
#acknowledged-list,
|
#acknowledged-list,
|
||||||
#values {
|
#values,
|
||||||
|
#services {
|
||||||
background-color: #fff !important;
|
background-color: #fff !important;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25);
|
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25);
|
||||||
|
@ -4,3 +4,9 @@ body {
|
|||||||
#areas .area {
|
#areas .area {
|
||||||
box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5);
|
box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
#page-error {
|
||||||
|
border: unset;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #a00;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@ -17,6 +17,7 @@ class Graph {
|
|||||||
const values = [{
|
const values = [{
|
||||||
x: this.dataset.xValues(),
|
x: this.dataset.xValues(),
|
||||||
y: this.dataset.yValues(),
|
y: this.dataset.yValues(),
|
||||||
|
mode: 'scatter',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
this.layout = {
|
this.layout = {
|
||||||
@ -26,7 +27,11 @@ class Graph {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Plotly.react(this.graphValues, values, this.layout);
|
this.config = {
|
||||||
|
displayModeBar: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
Plotly.react(this.graphValues, values, this.layout, this.config);
|
||||||
this.graphValues.on('plotly_relayout', attr => this.relayoutHandler(attr))
|
this.graphValues.on('plotly_relayout', attr => this.relayoutHandler(attr))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +44,7 @@ class Graph {
|
|||||||
const values = [{
|
const values = [{
|
||||||
x: this.dataset.xValues(),
|
x: this.dataset.xValues(),
|
||||||
y: this.dataset.yValues(),
|
y: this.dataset.yValues(),
|
||||||
|
mode: 'scatter',
|
||||||
}]
|
}]
|
||||||
Plotly.react(this.graphValues, values, this.layout)
|
Plotly.react(this.graphValues, values, this.layout)
|
||||||
})
|
})
|
||||||
|
@ -36,3 +36,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#services {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
gap: 10px 24px;
|
||||||
|
background-color: @bg3;
|
||||||
|
width: min-content;
|
||||||
|
padding: 16px;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -58,7 +58,7 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#datapoints, #problems-list, #acknowledged-list, #values {
|
#datapoints, #problems-list, #acknowledged-list, #values, #services {
|
||||||
background-color: #fff !important;
|
background-color: #fff !important;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.25);
|
box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.25);
|
||||||
|
@ -9,3 +9,10 @@ body {
|
|||||||
box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.5);
|
box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#page-error {
|
||||||
|
border: unset;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #a00;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@ -6,18 +6,6 @@
|
|||||||
{{ template "fonts" }}
|
{{ template "fonts" }}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/main.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/main.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/{{ .CONFIG.THEME }}.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/{{ .CONFIG.THEME }}.css">
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.2.1/dist/chart.umd.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/luxon@3.3.0/build/global/luxon.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
|
|
||||||
|
|
||||||
<script type="importmap">
|
|
||||||
{
|
|
||||||
"imports": {
|
|
||||||
"chart.js": "/js/{{ .VERSION }}/lib/chartjs/index.d.ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}">
|
<div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}">
|
||||||
|
19
views/layouts/notification.gotmpl
Normal file
19
views/layouts/notification.gotmpl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
||||||
|
{{ template "fonts" }}
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/main.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/{{ .CONFIG.THEME }}.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}">
|
||||||
|
<div class="close" onclick="console.log(this.parentElement.classList.remove('show'))">✖</div>
|
||||||
|
{{ .ERROR }}
|
||||||
|
</div>
|
||||||
|
<div id="page">
|
||||||
|
{{ block "page" . }}{{ end }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -62,6 +62,12 @@
|
|||||||
return
|
return
|
||||||
location.href = `/section/delete/${id}`
|
location.href = `/section/delete/${id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newNotification() {
|
||||||
|
const select = document.getElementById('notification-create-type')
|
||||||
|
const nType = select.value
|
||||||
|
location.href = `/configuration/notification?type=${nType}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{ block "page_label" . }}{{end}}
|
{{ block "page_label" . }}{{end}}
|
||||||
@ -108,4 +114,24 @@
|
|||||||
<button style="margin-left: 8px;">Update</button>
|
<button style="margin-left: 8px;">Update</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h1>Notifications</h1>
|
||||||
|
<select id="notification-create-type">
|
||||||
|
{{ range .Data.AvailableServices }}
|
||||||
|
<option>{{ .GetType }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
<button onclick="newNotification()">Create</button>
|
||||||
|
|
||||||
|
<div id="services">
|
||||||
|
<div class="header">Prio</div>
|
||||||
|
<div class="header">Type</div>
|
||||||
|
<div class="header">Target</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
{{ range .Data.NotificationServices }}
|
||||||
|
<div><a href="/configuration/notification?prio={{ .GetPrio }}">{{ .GetPrio }}</a></div>
|
||||||
|
<div><a href="/configuration/notification?prio={{ .GetPrio }}">{{ .GetType }}</a></div>
|
||||||
|
<div><a href="/configuration/notification?prio={{ .GetPrio }}">{{ .String }}</a></div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
34
views/pages/notification/ntfy.gotmpl
Normal file
34
views/pages/notification/ntfy.gotmpl
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{{ define "page" }}
|
||||||
|
<h1>NTFY</h1>
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-gap: 8px 16px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=number] {
|
||||||
|
width: 64px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<form action="/configuration/notification/update/{{ if .Data.Service.Exists }}{{ .Data.Service.GetPrio }}{{ else }}-1{{ end }}" method="post">
|
||||||
|
<input type="hidden" name="type" value="NTFY">
|
||||||
|
<div class="grid">
|
||||||
|
<div>Prio:</div>
|
||||||
|
<input type="number" min=0 name="prio" value="{{ .Data.Service.GetPrio }}">
|
||||||
|
|
||||||
|
<div>URL:</div>
|
||||||
|
<input type="text" name="url" value="{{ .Data.Service.URL }}" style="width: 100%">
|
||||||
|
|
||||||
|
<button style="grid-column: 1 / -1; width: min-content;">OK</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
34
views/pages/notification/script.gotmpl
Normal file
34
views/pages/notification/script.gotmpl
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{{ define "page" }}
|
||||||
|
<h1>Script</h1>
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-gap: 8px 16px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=number] {
|
||||||
|
width: 64px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<form action="/configuration/notification/update/{{ .Data.Service.GetPrio }}" method="post">
|
||||||
|
<input type="hidden" name="type" value="Script">
|
||||||
|
<div class="grid">
|
||||||
|
<div>Prio:</div>
|
||||||
|
<input type="number" min=0 name="prio" value="{{ .Data.Service.GetPrio }}">
|
||||||
|
|
||||||
|
<div>Filename:</div>
|
||||||
|
<input type="text" name="filename" value="{{ .Data.Service.Filename }}" style="width: 100%">
|
||||||
|
|
||||||
|
<button style="grid-column: 1 / -1; width: min-content;">OK</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
Loading…
Reference in New Issue
Block a user