package main import ( // External ws "git.gibonuddevalla.se/go/webservice" "git.gibonuddevalla.se/go/webservice/session" we "git.gibonuddevalla.se/go/wrappederror" // Standard "embed" "encoding/json" "flag" "fmt" "html/template" "io" "io/fs" "log/slog" "net/http" "os" "path" "sort" "strconv" "time" ) const VERSION = "v1" var ( logger *slog.Logger flagConfigFile string flagDev bool service *ws.Service logFile *os.File parsedTemplates map[string]*template.Template componentFilenames []string //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", we.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.Parse() componentFilenames, err = getComponentFilenames(viewFS) if err != nil { logger.Error("application", "error", err) os.Exit(1) } parsedTemplates = make(map[string]*template.Template) } // }}} func main() { // {{{ var err error we.Init() we.SetLogCallback(logHandler) service, err = ws.New(flagConfigFile, VERSION, logger) if err != nil { logger.Error("application", "error", err) os.Exit(1) } smonConf := SmonConfiguration{} 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 } 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 } _, err = service.Db.Conn.Exec(`SET TIMEZONE TO 'Europe/Stockholm'`) if err != nil { logger.Error("application", "error", err) return } service.Register("/", false, false, staticHandler) service.Register("/problems", false, false, pageProblems) service.Register("/datapoints", false, false, pageDatapoints) service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit) service.Register("/datapoint/update/{id}", false, false, pageDatapointUpdate) service.Register("/triggers", false, false, pageTriggers) service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit) service.Register("/trigger/update/{id}", false, false, pageTriggerUpdate) service.Register("/trigger/run/{id}", false, false, pageTriggerRun) service.Register("/configuration", false, false, pageConfiguration) service.Register("/entry/{datapoint}", false, false, entryDatapoint) err = service.Start() if err != nil { logger.Error("webserver", "error", we.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 we.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, we.Wrap(err).Log()) return } var triggers []Trigger triggers, err = TriggersRetrieveByDatapoint(dpoint) for _, trigger := range triggers { trigger.Run() } 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) string { return t.Local().Format("2006-01-02 15:04: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("main.gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...) } else { tmpl, err = template.New("main.gotmpl").ParseFS(viewFS, filenames...) } if err != nil { err = we.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 pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ page := Page{ LAYOUT: "main", PAGE: "problems", } /* areas, err := ProblemsRetrieve() if err != nil { httpError(w, we.Wrap(err).Log()) return } sort.SliceStable(areas, func(i, j int) bool { return areas[i].Name < areas[j].Name }) logger.Info("problems", "areas", areas) */ page.Data = map[string]any{ //"Areas": areas, } page.Render(w) return } // }}} func pageDatapoints(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ page := Page{ LAYOUT: "main", PAGE: "datapoints", } datapoints, err := DatapointsRetrieve() if err != nil { httpError(w, we.Wrap(err).Log()) return } logger.Info("FOO", "dps", datapoints) 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, we.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, we.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, we.Wrap(err).Log()) return } var dp Datapoint dp.ID = id dp.Name = r.FormValue("name") dp.Datatype = DatapointType(r.FormValue("datatype")) err = dp.Update() if err != nil { httpError(w, we.Wrap(err).Log()) return } w.Header().Add("Location", "/datapoints") w.WriteHeader(302) } // }}} func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ areas, err := TriggersRetrieve() if err != nil { httpError(w, we.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 pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { httpError(w, we.Wrap(err).Log()) return } var trigger Trigger trigger, err = TriggerRetrieve(id) if err != nil { httpError(w, we.Wrap(err).Log()) return } datapoints := make(map[string]Datapoint) for _, dpname := range trigger.Datapoints { dp, err := DatapointRetrieve(0, dpname) if err != nil { httpError(w, we.Wrap(err).Log()) return } 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 pageTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { httpError(w, we.Wrap(err).Log()) return } var trigger Trigger trigger, err = TriggerRetrieve(id) if err != nil { httpError(w, we.Wrap(err).Log()) return } trigger.Name = r.FormValue("name") trigger.Expression = r.FormValue("expression") err = trigger.Update() if err != nil { httpError(w, we.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, we.Wrap(err).Log()) return } var trigger Trigger trigger, err = TriggerRetrieve(id) if err != nil { httpError(w, we.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 { we.Wrap(err).Log() httpError(w, err) return } j, _ := json.Marshal(resp) w.Header().Add("Content-Type", "application/json") w.Write(j) } // }}} func pageConfiguration(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ areas, err := AreaRetrieve() if err != nil { httpError(w, we.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) } // }}}