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" "net/url" "os" "path" "slices" "sort" "strconv" "strings" "time" ) const VERSION = "v41" 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 fileConf FileConfiguration //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, &fileConf) logFile, err = os.OpenFile(fileConf.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { logger.Error("application", "error", err) return } if fileConf.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, actionAreaNew) service.Register("/area/rename/{id}/{name}", false, false, actionAreaRename) service.Register("/area/delete/{id}", false, false, actionAreaDelete) service.Register("/section/new/{areaID}/{name}", false, false, actionSectionNew) service.Register("/section/rename/{id}/{name}", false, false, actionSectionRename) service.Register("/section/delete/{id}", false, false, actionSectionDelete) service.Register("/problems", false, false, pageProblems) service.Register("/problem/acknowledge/{id}", false, false, actionProblemAcknowledge) service.Register("/problem/unacknowledge/{id}", false, false, actionProblemUnacknowledge) service.Register("/datapoints", false, false, pageDatapoints) service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit) service.Register("/datapoint/update/{id}", false, false, actionDatapointUpdate) service.Register("/datapoint/delete/{id}", false, false, actionDatapointDelete) service.Register("/datapoint/values/{id}", false, false, pageDatapointValues) service.Register("/datapoint/json/{id}", false, false, actionDatapointJson) service.Register("/triggers", false, false, pageTriggers) service.Register("/trigger/create/{sectionID}/{name}", false, false, actionTriggerCreate) 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, actionTriggerDatapointAdd) service.Register("/trigger/update/{id}", false, false, actionTriggerUpdate) service.Register("/trigger/run/{id}", false, false, actionTriggerRun) service.Register("/trigger/delete/{id}", false, false, actionTriggerDelete) service.Register("/notifications", false, false, pageNotifications) service.Register("/configuration", false, false, pageConfiguration) service.Register("/configuration/theme", false, false, actionConfigurationTheme) 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("/configuration/notification/delete/{prio}", false, false, actionConfigurationNotificationDelete) service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint) go nodataLoop() smonConfig, err = SmonConfigInit() if err != nil { logger.Error("configuration", "error", werr.Wrap(err)) os.Exit(1) } err = smonConfig.Validate() if err != nil { logger.Error("configuration", "error", werr.Wrap(err)) os.Exit(1) } 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 pageError(w http.ResponseWriter, redirectURL string, pageErr error) { // {{{ u, err := url.Parse(redirectURL) if err != nil { httpError(w, err) return } values := u.Query() values.Add("_err", pageErr.Error()) u.RawQuery = values.Encode() w.Header().Add("Location", u.String()) w.WriteHeader(302) } // }}} 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 actionEntryDatapoint(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.In(smonConfig.Timezone()).Format(`2006-01-02 15:04:05:05`), ) }, } 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(layout+".gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...) } else { tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...) } if err != nil { err = werr.Wrap(err).Log() return } parsedTemplates[page] = tmpl return } // }}} func pageIndex(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ page := Page{ LAYOUT: "main", PAGE: "index", CONFIG: smonConfig.Settings, } page.Render(w, r) } // }}} func actionAreaNew(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 actionAreaRename(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 actionAreaDelete(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 actionSectionNew(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 actionSectionRename(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 actionSectionDelete(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, r *http.Request, _ *session.T) { // {{{ page := Page{ LAYOUT: "main", PAGE: "problems", CONFIG: smonConfig.Settings, } // Manage the values from the timefilter component var err error var timeFrom, timeTo time.Time timeFrom, timeTo, err = timefilterParse( r.URL.Query().Get("time-f"), r.URL.Query().Get("time-t"), r.URL.Query().Get("time-offset"), r.URL.Query().Get("time-preset"), ) if err != nil { httpError(w, err) return } // GET parameters for this page var selection string if r.URL.Query().Get("selection") == "all" { selection = "ALL" } else { selection = "CURRENT" } problems, err := ProblemsRetrieve(selection == "ALL", timeFrom, timeTo) 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, "Selection": selection, "TimeHidden": selection == "CURRENT", "TimeSubmit": "/problems", "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), "TimeTo": timeTo.Format("2006-01-02T15:04:05"), } page.Render(w, r) return } // }}} func actionProblemAcknowledge(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 actionProblemUnacknowledge(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", CONFIG: smonConfig.Settings, } 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, r) 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 } } /* Triggers using this datapoint is provided as a list to update * if changing the datapoint name. Parsing expr and automatically * changing it to renamed datapoints would be nice in the future. */ var triggers []Trigger triggers, err = TriggersRetrieveByDatapoint(datapoint.Name) if err != nil { httpError(w, werr.Wrap(err).Log()) return } slices.SortFunc(triggers, func(a, b Trigger) int { an := strings.ToUpper(a.Name) bn := strings.ToUpper(b.Name) if an < bn { return -1 } if an > bn { return 1 } return 0 }) page := Page{ LAYOUT: "main", PAGE: "datapoint_edit", CONFIG: smonConfig.Settings, MENU: "datapoints", Icon: "datapoints", Label: "Datapoint", } page.Data = map[string]any{ "Datapoint": datapoint, "Triggers": triggers, } page.Render(w, r) return } // }}} func actionDatapointUpdate(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")) // Datapoint needs to be retrieved from database for the name. // If name has changed, trigger expressions needs to be updated. var dp Datapoint dp, err = DatapointRetrieve(id, "") if err != nil { httpError(w, werr.Wrap(err).WithData(id).Log()) return } prevDatapointName := dp.Name dp.Group = r.FormValue("group") dp.Name = r.FormValue("name") dp.Datatype = DatapointType(r.FormValue("datatype")) dp.Comment = r.FormValue("comment") dp.NodataProblemSeconds = nodataSeconds err = dp.Update() if err != nil { httpError(w, werr.Wrap(err).Log()) return } // Update the trigger expressions using this // datapoint name if changed. if prevDatapointName != dp.Name { var triggers []Trigger triggers, err = TriggersRetrieveByDatapoint(dp.Name) if err != nil { httpError(w, werr.Wrap(err).WithData(dp.Name)) return } for _, trigger := range triggers { err = trigger.RenameDatapoint(prevDatapointName, dp.Name) if err != nil { httpError(w, werr.Wrap(err).WithData([]string{prevDatapointName, dp.Name})) return } err = trigger.Update() if err != nil { httpError(w, werr.Wrap(err).WithData([]string{prevDatapointName, dp.Name, trigger.Name})) return } } } w.Header().Add("Location", "/datapoints") w.WriteHeader(302) } // }}} func actionDatapointDelete(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 } // Manage the values from the timefilter component var timeFrom, timeTo time.Time timeFrom, timeTo, err = timefilterParse( r.URL.Query().Get("time-f"), r.URL.Query().Get("time-t"), r.URL.Query().Get("time-offset"), r.URL.Query().Get("time-preset"), ) // Fetch data point values according to the times. var values []DatapointValue values, err = DatapointValues(id, timeFrom, timeTo) if err != nil { httpError(w, werr.Wrap(err).Log()) return } // GET parameters. display := r.URL.Query().Get("display") if display == "" && datapoint.Datatype == INT { display = "graph" } page := Page{ LAYOUT: "main", PAGE: "datapoint_values", CONFIG: smonConfig.Settings, MENU: "datapoints", Icon: "datapoints", Label: "Values for " + datapoint.Name, } page.Data = map[string]any{ "Datapoint": datapoint, "Values": values, "Display": display, "TimeSubmit": "/datapoint/values/" + strconv.Itoa(datapoint.ID), "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), "TimeTo": timeTo.Format("2006-01-02T15:04:05"), } page.Render(w, r) return } // }}} func actionDatapointJson(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 } fromStr := r.URL.Query().Get("time-f") from, err := time.ParseInLocation("2006-01-02 15:04:05", fromStr[0:min(19, len(fromStr))], smonConfig.Timezone()) if err != nil { httpError(w, werr.Wrap(err).Log()) return } toStr := r.URL.Query().Get("time-t") to, err := time.ParseInLocation("2006-01-02 15:04:05", toStr[0:min(19, len(toStr))], smonConfig.Timezone()) if err != nil { httpError(w, werr.Wrap(err).Log()) return } values, err := DatapointValues(id, from, to) if err != nil { httpError(w, werr.Wrap(err).Log()) return } j, _ := json.Marshal(values) w.Write(j) } // }}} func pageTriggers(w http.ResponseWriter, r *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", CONFIG: smonConfig.Settings, Data: map[string]any{ "Areas": areas, }, } page.Render(w, r) } // }}} func actionTriggerCreate(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", CONFIG: smonConfig.Settings, Label: "Trigger", Icon: "triggers", Data: map[string]any{ "Trigger": trigger, "Datapoints": datapoints, }, } page.Render(w, r) } // }}} func actionTriggerDatapointAdd(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 } // Also retrieve the datapoint to get the latest value // for immediate presentation when added. dp, err := DatapointRetrieve(0, dpName) if err != nil { httpError(w, werr.Wrap(err).WithData(dpName).Log()) return } dp.LastDatapointValue.TemplateValue = dp.LastDatapointValue.Value() j, _ := json.Marshal( struct { OK bool Datapoint Datapoint }{ true, dp, }) w.Header().Add("Content-Type", "application/json") w.Write(j) } // }}} func actionTriggerUpdate(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 actionTriggerRun(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, r *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", CONFIG: smonConfig.Settings, Data: map[string]any{ "Areas": areas, "NotificationServices": notificationManager.Services(), "AvailableServices": notification.AvailableServices(), }, } page.Render(w, r) } // }}} func actionConfigurationTheme(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ theme := r.FormValue("theme") err := smonConfig.SetTheme(theme) if err != nil { httpError(w, werr.Wrap(err).Log()) return } w.Header().Add("Location", "/configuration") w.WriteHeader(302) } // }}} func actionConfigurationTimezone(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ timezone := r.FormValue("timezone") _, err := time.LoadLocation(timezone) if err != nil { pageError(w, "/configuration", werr.Wrap(err).Log()) return } err = smonConfig.SetTimezone(timezone) if err != nil { pageError(w, "/configuration", werr.Wrap(err).Log()) return } w.Header().Add("Location", "/configuration") 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.NewInstance(notificationType) logger.Info("FOO", "service", fmt.Sprintf("%p %#v\n", service, service)) } 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.NewInstance(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) } notificationManager.Reprioritize() w.Header().Add("Location", "/configuration") w.WriteHeader(302) } // }}} func actionConfigurationNotificationDelete(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 } DeleteNotificationService(prio) notificationManager.RemoveService(prio) w.Header().Add("Location", "/configuration") w.WriteHeader(302) } // }}} func pageNotifications(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ var err error // Manage the values from the timefilter component var timeFrom, timeTo time.Time timeFrom, timeTo, err = timefilterParse( r.URL.Query().Get("time-f"), r.URL.Query().Get("time-t"), r.URL.Query().Get("time-offset"), r.URL.Query().Get("time-preset"), ) nss, err := notificationsSent(timeFrom, timeTo) if err != nil { pageError(w, "/", werr.Wrap(err).Log()) } page := Page{ LAYOUT: "main", PAGE: "notifications", CONFIG: smonConfig.Settings, Data: map[string]any{ "Notifications": nss, "TimeSubmit": "/notifications", "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), "TimeTo": timeTo.Format("2006-01-02T15:04:05"), }, } page.Render(w, r) } // }}}