943 lines
22 KiB
Go
943 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
// External
|
|
ws "git.gibonuddevalla.se/go/webservice"
|
|
"git.gibonuddevalla.se/go/webservice/session"
|
|
werr "git.gibonuddevalla.se/go/wrappederror"
|
|
|
|
// Internal
|
|
"smon/notification"
|
|
|
|
// Standard
|
|
"embed"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const VERSION = "v17"
|
|
|
|
var (
|
|
logger *slog.Logger
|
|
flagConfigFile string
|
|
flagDev bool
|
|
flagVersion bool
|
|
service *ws.Service
|
|
logFile *os.File
|
|
parsedTemplates map[string]*template.Template
|
|
componentFilenames []string
|
|
notificationManager notification.Manager
|
|
smonConf SmonConfiguration
|
|
|
|
//go:embed sql
|
|
sqlFS embed.FS
|
|
|
|
//go:embed views
|
|
viewFS embed.FS
|
|
|
|
//go:embed static
|
|
staticFS embed.FS
|
|
)
|
|
|
|
func init() { // {{{
|
|
opts := slog.HandlerOptions{}
|
|
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opts))
|
|
|
|
confDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
logger.Error("application", "error", werr.Wrap(err))
|
|
}
|
|
cfgPath := path.Join(confDir, "smon.yaml")
|
|
flag.StringVar(&flagConfigFile, "config", cfgPath, "Path and filename of the YAML configuration file")
|
|
flag.BoolVar(&flagDev, "dev", false, "reload templates on each request")
|
|
flag.BoolVar(&flagVersion, "version", false, "Display version and exit")
|
|
flag.Parse()
|
|
|
|
componentFilenames, err = getComponentFilenames(viewFS)
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
parsedTemplates = make(map[string]*template.Template)
|
|
} // }}}
|
|
|
|
func main() { // {{{
|
|
if flagVersion {
|
|
fmt.Println(VERSION)
|
|
os.Exit(0)
|
|
}
|
|
|
|
var err error
|
|
|
|
werr.Init()
|
|
werr.SetLogCallback(logHandler)
|
|
|
|
service, err = ws.New(flagConfigFile, VERSION, logger)
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
j, _ := json.Marshal(service.Config.Application)
|
|
json.Unmarshal(j, &smonConf)
|
|
logFile, err = os.OpenFile(smonConf.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
return
|
|
}
|
|
if smonConf.NodataInterval < 10 {
|
|
logger.Error("application → nodata_interval has to be larger or equal to 10.")
|
|
return
|
|
}
|
|
|
|
service.SetDatabase(sqlProvider)
|
|
service.SetStaticFS(staticFS, "static")
|
|
service.SetStaticDirectory("static", flagDev)
|
|
service.InitDatabaseConnection()
|
|
err = service.Db.Connect()
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
return
|
|
}
|
|
|
|
notificationManager, err = InitNotificationManager()
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("notification", "error", err)
|
|
}
|
|
|
|
service.Register("/", false, false, staticHandler)
|
|
|
|
service.Register("/area/new/{name}", false, false, areaNew)
|
|
service.Register("/area/rename/{id}/{name}", false, false, areaRename)
|
|
service.Register("/area/delete/{id}", false, false, areaDelete)
|
|
|
|
service.Register("/section/new/{areaID}/{name}", false, false, sectionNew)
|
|
service.Register("/section/rename/{id}/{name}", false, false, sectionRename)
|
|
service.Register("/section/delete/{id}", false, false, sectionDelete)
|
|
|
|
service.Register("/problems", false, false, pageProblems)
|
|
service.Register("/problem/acknowledge/{id}", false, false, pageProblemAcknowledge)
|
|
service.Register("/problem/unacknowledge/{id}", false, false, pageProblemUnacknowledge)
|
|
|
|
service.Register("/datapoints", false, false, pageDatapoints)
|
|
service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit)
|
|
service.Register("/datapoint/update/{id}", false, false, pageDatapointUpdate)
|
|
service.Register("/datapoint/delete/{id}", false, false, pageDatapointDelete)
|
|
service.Register("/datapoint/values/{id}", false, false, pageDatapointValues)
|
|
|
|
service.Register("/triggers", false, false, pageTriggers)
|
|
service.Register("/trigger/create/{sectionID}/{name}", false, false, triggerCreate)
|
|
service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit)
|
|
service.Register("/trigger/edit/{id}/{sectionID}", false, false, pageTriggerEdit)
|
|
service.Register("/trigger/addDatapoint/{id}/{datapointName}", false, false, pageTriggerDatapointAdd)
|
|
service.Register("/trigger/update/{id}", false, false, pageTriggerUpdate)
|
|
service.Register("/trigger/run/{id}", false, false, pageTriggerRun)
|
|
service.Register("/trigger/delete/{id}", false, false, actionTriggerDelete)
|
|
|
|
service.Register("/configuration", false, false, pageConfiguration)
|
|
service.Register("/entry/{datapoint}", false, false, entryDatapoint)
|
|
|
|
go nodataLoop()
|
|
|
|
err = service.Start()
|
|
if err != nil {
|
|
logger.Error("webserver", "error", werr.Wrap(err))
|
|
os.Exit(1)
|
|
}
|
|
|
|
} // }}}
|
|
|
|
func sqlProvider(dbname string, version int) (sql []byte, found bool) { // {{{
|
|
var err error
|
|
sql, err = sqlFS.ReadFile(fmt.Sprintf("sql/%05d.sql", version))
|
|
if err != nil {
|
|
return
|
|
}
|
|
found = true
|
|
return
|
|
} // }}}
|
|
func logHandler(err werr.Error) { // {{{
|
|
j, _ := json.Marshal(err)
|
|
logFile.Write(j)
|
|
logFile.Write([]byte("\n"))
|
|
} // }}}
|
|
|
|
func httpError(w http.ResponseWriter, err error) { // {{{
|
|
resp := struct {
|
|
OK bool
|
|
Error string
|
|
}{
|
|
false,
|
|
err.Error(),
|
|
}
|
|
j, _ := json.Marshal(resp)
|
|
w.Write(j)
|
|
} // }}}
|
|
|
|
func staticHandler(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
if flagDev && !reloadTemplates(w) {
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == "/" {
|
|
pageIndex(w, r, sess)
|
|
return
|
|
}
|
|
|
|
service.StaticHandler(w, r, sess)
|
|
} // }}}
|
|
func entryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
dpoint := r.PathValue("datapoint")
|
|
value, _ := io.ReadAll(r.Body)
|
|
|
|
err := DatapointAdd(dpoint, value)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var triggers []Trigger
|
|
triggers, err = TriggersRetrieveByDatapoint(dpoint)
|
|
if err != nil {
|
|
logger.Error("entry", "error", err)
|
|
}
|
|
|
|
// Multiple triggers can use the same datapoint.
|
|
for _, trigger := range triggers {
|
|
var out any
|
|
out, err = trigger.Run()
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("entry", "error", err)
|
|
|
|
}
|
|
logger.Debug("entry", "datapoint", dpoint, "value", value, "trigger", trigger, "result", out)
|
|
|
|
var problemID int
|
|
var problemState string
|
|
switch v := out.(type) {
|
|
case bool:
|
|
// Trigger returning true - a problem occurred
|
|
if v {
|
|
problemID, err = ProblemStart(trigger)
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("entry", "error", err)
|
|
}
|
|
problemState = "PROBLEM"
|
|
} else {
|
|
// A problem didn't occur.
|
|
problemID, err = ProblemClose(trigger)
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("entry", "error", err)
|
|
}
|
|
problemState = "OK"
|
|
}
|
|
|
|
// Has a change in problem state happened?
|
|
if problemID == 0 && err == nil {
|
|
logger.Debug("notification", "trigger", trigger.ID, "state", "no change")
|
|
continue
|
|
} else {
|
|
logger.Debug("notification", "trigger", trigger.ID, "state", "change")
|
|
}
|
|
|
|
err = notificationManager.Send(problemID, []byte(problemState+": "+trigger.Name), func(notificationService *notification.Service, err error) {
|
|
logger.Info(
|
|
"notification",
|
|
"service", (*notificationService).GetType(),
|
|
"problemID", problemID,
|
|
"prio", (*notificationService).GetPrio(),
|
|
"ok", true,
|
|
)
|
|
|
|
var errBody any
|
|
if err != nil {
|
|
errBody, _ = json.Marshal(err)
|
|
} else {
|
|
errBody = nil
|
|
}
|
|
_, err = service.Db.Conn.Exec(
|
|
`
|
|
INSERT INTO notification_send(notification_id, problem_id, uuid, ok, error)
|
|
SELECT
|
|
id, $3, '', $4, $5
|
|
FROM notification
|
|
WHERE
|
|
service=$1 AND
|
|
prio=$2
|
|
`,
|
|
(*notificationService).GetType(),
|
|
(*notificationService).GetPrio(),
|
|
problemID,
|
|
err == nil,
|
|
errBody,
|
|
)
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("entry", "error", err)
|
|
}
|
|
})
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
logger.Error("notification", "error", err)
|
|
}
|
|
|
|
default:
|
|
err := fmt.Errorf(`Expression for trigger %s not returning bool (%T)`, trigger.Name, v)
|
|
logger.Info("entry", "error", err)
|
|
werr.Wrap(err).WithData(v).Log()
|
|
}
|
|
}
|
|
|
|
j, _ := json.Marshal(struct{ OK bool }{true})
|
|
w.Write(j)
|
|
} // }}}
|
|
|
|
func reloadTemplates(w http.ResponseWriter) bool { // {{{
|
|
parsedTemplates = make(map[string]*template.Template)
|
|
return true
|
|
} // }}}
|
|
func getComponentFilenames(efs fs.FS) (files []string, err error) { // {{{
|
|
files = []string{}
|
|
if err := fs.WalkDir(efs, "views/components", func(path string, d fs.DirEntry, err error) error {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
files = append(files, path)
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return files, nil
|
|
} // }}}
|
|
func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
|
|
layoutFilename := fmt.Sprintf("views/layouts/%s.gotmpl", layout)
|
|
pageFilename := fmt.Sprintf("views/pages/%s.gotmpl", page)
|
|
|
|
if tmpl, found := parsedTemplates[page]; found {
|
|
return tmpl, nil
|
|
}
|
|
|
|
funcMap := template.FuncMap{
|
|
"format_time": func(t time.Time) template.HTML {
|
|
return template.HTML(
|
|
t.Local().Format(`<span class="date">2006-01-02</span> <span class="time">15:04<span class="seconds">:05</span></span>`),
|
|
)
|
|
},
|
|
}
|
|
|
|
filenames := []string{layoutFilename, pageFilename}
|
|
filenames = append(filenames, componentFilenames...)
|
|
logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
|
|
if flagDev {
|
|
tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
|
|
} else {
|
|
tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...)
|
|
}
|
|
if err != nil {
|
|
err = werr.Wrap(err).Log()
|
|
return
|
|
}
|
|
|
|
parsedTemplates[page] = tmpl
|
|
return
|
|
} // }}}
|
|
|
|
func pageIndex(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "index",
|
|
}
|
|
page.Render(w)
|
|
} // }}}
|
|
|
|
func areaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
name := r.PathValue("name")
|
|
err := AreaCreate(name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
func areaRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
name := r.PathValue("name")
|
|
err = AreaRename(id, name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
func areaDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(idStr).Log())
|
|
return
|
|
}
|
|
|
|
err = AreaDelete(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(id).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
|
|
func sectionNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("areaID")
|
|
areaID, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
name := r.PathValue("name")
|
|
err = SectionCreate(areaID, name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
func sectionRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
name := r.PathValue("name")
|
|
err = SectionRename(id, name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
func sectionDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(idStr).Log())
|
|
return
|
|
}
|
|
|
|
err = SectionDelete(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(id).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/configuration")
|
|
w.WriteHeader(302)
|
|
return
|
|
} // }}}
|
|
|
|
func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "problems",
|
|
}
|
|
|
|
problems, err := ProblemsRetrieve()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var found bool
|
|
problemsGrouped := make(map[string]map[string][]Problem)
|
|
for _, problem := range problems {
|
|
if _, found = problemsGrouped[problem.AreaName]; !found {
|
|
problemsGrouped[problem.AreaName] = make(map[string][]Problem)
|
|
}
|
|
if _, found = problemsGrouped[problem.AreaName][problem.SectionName]; !found {
|
|
problemsGrouped[problem.AreaName][problem.SectionName] = []Problem{}
|
|
}
|
|
|
|
problemsGrouped[problem.AreaName][problem.SectionName] = append(
|
|
problemsGrouped[problem.AreaName][problem.SectionName],
|
|
problem,
|
|
)
|
|
}
|
|
|
|
page.Data = map[string]any{
|
|
"Problems": problems,
|
|
"ProblemsGrouped": problemsGrouped,
|
|
}
|
|
page.Render(w)
|
|
return
|
|
} // }}}
|
|
func pageProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = ProblemAcknowledge(id, true)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/problems")
|
|
w.WriteHeader(302)
|
|
|
|
return
|
|
} // }}}
|
|
func pageProblemUnacknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = ProblemAcknowledge(id, false)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/problems")
|
|
w.WriteHeader(302)
|
|
|
|
return
|
|
} // }}}
|
|
|
|
func pageDatapoints(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "datapoints",
|
|
}
|
|
|
|
datapoints, err := DatapointsRetrieve()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
// The datapoint selector in trigger edit wants the raw data in JSON.
|
|
if r.URL.Query().Get("format") == "json" {
|
|
j, _ := json.Marshal(datapoints)
|
|
w.Write(j)
|
|
return
|
|
}
|
|
|
|
page.Data = map[string]any{
|
|
"Datapoints": datapoints,
|
|
}
|
|
page.Render(w)
|
|
return
|
|
} // }}}
|
|
func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var datapoint Datapoint
|
|
if id == 0 {
|
|
datapoint.Name = "new_datapoint"
|
|
datapoint.Datatype = "INT"
|
|
} else {
|
|
datapoint, err = DatapointRetrieve(id, "")
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
}
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "datapoint_edit",
|
|
MENU: "datapoints",
|
|
Icon: "datapoints",
|
|
Label: "Datapoint",
|
|
}
|
|
|
|
page.Data = map[string]any{
|
|
"Datapoint": datapoint,
|
|
}
|
|
page.Render(w)
|
|
return
|
|
} // }}}
|
|
func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var nodataSeconds int
|
|
nodataSeconds, _ = strconv.Atoi(r.FormValue("nodata_seconds"))
|
|
|
|
var dp Datapoint
|
|
dp.ID = id
|
|
dp.Group = r.FormValue("group")
|
|
dp.Name = r.FormValue("name")
|
|
dp.Datatype = DatapointType(r.FormValue("datatype"))
|
|
dp.NodataProblemSeconds = nodataSeconds
|
|
err = dp.Update()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/datapoints")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func pageDatapointDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = DatapointDelete(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/datapoints")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var datapoint Datapoint
|
|
datapoint, err = DatapointRetrieve(id, "")
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var values []DatapointValue
|
|
values, err = DatapointValues(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "datapoint_values",
|
|
MENU: "datapoints",
|
|
Icon: "datapoints",
|
|
Label: "Values for " + datapoint.Name,
|
|
}
|
|
|
|
page.Data = map[string]any{
|
|
"Datapoint": datapoint,
|
|
"Values": values,
|
|
}
|
|
page.Render(w)
|
|
return
|
|
} // }}}
|
|
|
|
func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
|
|
areas, err := TriggersRetrieve()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
sort.SliceStable(areas, func(i, j int) bool {
|
|
return areas[i].Name < areas[j].Name
|
|
})
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "triggers",
|
|
Data: map[string]any{
|
|
"Areas": areas,
|
|
},
|
|
}
|
|
|
|
page.Render(w)
|
|
} // }}}
|
|
func triggerCreate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
name := r.PathValue("name")
|
|
sectionIDStr := r.PathValue("sectionID")
|
|
sectionID, err := strconv.Atoi(sectionIDStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(sectionIDStr).Log())
|
|
return
|
|
}
|
|
|
|
t, err := TriggerCreate(sectionID, name)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).WithData(struct {
|
|
SectionID int
|
|
Name string
|
|
}{
|
|
sectionID,
|
|
name,
|
|
}).Log())
|
|
return
|
|
}
|
|
|
|
resp := struct {
|
|
OK bool
|
|
Trigger Trigger
|
|
}{
|
|
true,
|
|
t,
|
|
}
|
|
j, _ := json.Marshal(resp)
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(j)
|
|
} // }}}
|
|
func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
// Creating a new trigger uses the edit function.
|
|
// ID == 0 - create a new trigger.
|
|
// ID > 0 - edit existing trigger.
|
|
var trigger Trigger
|
|
if id > 0 {
|
|
trigger, err = TriggerRetrieve(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
} else {
|
|
// A new trigger needs to know which section it belongs to.
|
|
sectionIDStr := r.PathValue("sectionID")
|
|
if sectionIDStr != "" {
|
|
trigger.SectionID, err = strconv.Atoi(sectionIDStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
datapoints := make(map[string]Datapoint)
|
|
for _, dpname := range trigger.Datapoints {
|
|
dp, err := DatapointRetrieve(0, dpname)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
dp.LastDatapointValue.TemplateValue = dp.LastDatapointValue.Value()
|
|
datapoints[dpname] = dp
|
|
}
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "trigger_edit",
|
|
MENU: "triggers",
|
|
Label: "Trigger",
|
|
Icon: "triggers",
|
|
Data: map[string]any{
|
|
"Trigger": trigger,
|
|
"Datapoints": datapoints,
|
|
},
|
|
}
|
|
|
|
page.Render(w)
|
|
} // }}}
|
|
func pageTriggerDatapointAdd(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
triggerID := r.PathValue("id")
|
|
dpName := r.PathValue("datapointName")
|
|
|
|
id, err := strconv.Atoi(triggerID)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var trigger Trigger
|
|
if id > 0 {
|
|
trigger, err = TriggerRetrieve(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
}
|
|
|
|
if !slices.Contains(trigger.Datapoints, dpName) {
|
|
trigger.Datapoints = append(trigger.Datapoints, dpName)
|
|
}
|
|
err = trigger.Update()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
j, _ := json.Marshal(struct{ OK bool }{OK: true})
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(j)
|
|
} // }}}
|
|
func pageTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var trigger Trigger
|
|
if id > 0 {
|
|
trigger, err = TriggerRetrieve(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
} else {
|
|
trigger.SectionID, err = strconv.Atoi(r.FormValue("sectionID"))
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
}
|
|
|
|
trigger.Name = r.FormValue("name")
|
|
trigger.Expression = r.FormValue("expression")
|
|
trigger.Datapoints = r.Form["datapoints[]"]
|
|
err = trigger.Update()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/triggers")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
func pageTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
var trigger Trigger
|
|
trigger, err = TriggerRetrieve(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
expr, _ := io.ReadAll(r.Body)
|
|
trigger.Expression = string(expr)
|
|
|
|
resp := struct {
|
|
OK bool
|
|
Output any
|
|
}{
|
|
OK: true,
|
|
}
|
|
resp.Output, err = trigger.Run()
|
|
if err != nil {
|
|
werr.Wrap(err).Log()
|
|
httpError(w, err)
|
|
return
|
|
}
|
|
|
|
j, _ := json.Marshal(resp)
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(j)
|
|
} // }}}
|
|
func actionTriggerDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
err = TriggerDelete(id)
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Location", "/triggers")
|
|
w.WriteHeader(302)
|
|
} // }}}
|
|
|
|
func pageConfiguration(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
|
|
areas, err := AreaRetrieve()
|
|
if err != nil {
|
|
httpError(w, werr.Wrap(err).Log())
|
|
return
|
|
}
|
|
|
|
sort.SliceStable(areas, func(i, j int) bool {
|
|
return areas[i].Name < areas[j].Name
|
|
})
|
|
|
|
page := Page{
|
|
LAYOUT: "main",
|
|
PAGE: "configuration",
|
|
Data: map[string]any{
|
|
"Areas": areas,
|
|
},
|
|
}
|
|
|
|
page.Render(w)
|
|
} // }}}
|