From d72694a8b4751710398018fa125dbad6e5b66a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sun, 5 May 2024 20:16:28 +0200 Subject: [PATCH] View datapoint values --- datapoint.go | 35 ++++++++--- main.go | 98 ++++++++++++++++++++++++++++- notification/ntfy.go | 6 +- notification/pkg.go | 14 +++-- notification_log.go | 20 ++++++ problem.go | 16 +++-- sql/00012.sql | 13 ++++ static/css/datapoints.css | 11 ++++ static/css/main.css | 1 - static/images/values.svg | 67 ++++++++++++++++++++ static/less/datapoints.less | 13 ++++ static/less/main.less | 2 - views/pages/datapoint_values.gotmpl | 13 ++++ views/pages/datapoints.gotmpl | 7 ++- 14 files changed, 288 insertions(+), 28 deletions(-) create mode 100644 notification_log.go create mode 100644 sql/00012.sql create mode 100644 static/images/values.svg create mode 100644 views/pages/datapoint_values.gotmpl diff --git a/datapoint.go b/datapoint.go index 7d9bc8f..055c7a3 100644 --- a/datapoint.go +++ b/datapoint.go @@ -2,7 +2,7 @@ package main import ( // External - we "git.gibonuddevalla.se/go/wrappederror" + werr "git.gibonuddevalla.se/go/wrappederror" "github.com/jmoiron/sqlx" // Standard @@ -94,7 +94,7 @@ func DatapointAdd[T any](name string, value T) (err error) { // {{{ err = row.Scan(&dpID, &dpType) if err != nil { - err = we.Wrap(err).WithData(struct { + err = werr.Wrap(err).WithData(struct { Name string Value any }{name, value}) @@ -110,7 +110,7 @@ func DatapointAdd[T any](name string, value T) (err error) { // {{{ _, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_datetime) VALUES($1, $2)`, dpID, value) } if err != nil { - err = we.Wrap(err).WithData(struct { + err = werr.Wrap(err).WithData(struct { ID int value any }{dpID, value}) @@ -147,7 +147,7 @@ func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{ dp.name ASC `) if err != nil { - err = we.Wrap(err) + err = werr.Wrap(err) } defer rows.Close() @@ -170,7 +170,7 @@ func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{ res := DbRes{} err = rows.StructScan(&res) if err != nil { - err = we.Wrap(err) + err = werr.Wrap(err) return } @@ -218,7 +218,7 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{ } if err != nil { - err = we.Wrap(err).WithData(name) + err = werr.Wrap(err).WithData(name) return } @@ -238,7 +238,7 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{ } if err != nil { - err = we.Wrap(err).WithData(dp.ID) + err = werr.Wrap(err).WithData(dp.ID) return } @@ -247,7 +247,26 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{ func DatapointDelete(id int) (err error) {// {{{ _, err = service.Db.Conn.Exec(`DELETE FROM datapoint WHERE id=$1`, id) if err != nil { - err = we.Wrap(err).WithData(id) + err = werr.Wrap(err).WithData(id) + } + return +}// }}} +func DatapointValues(id int) (values []DatapointValue, err error) {// {{{ + rows, err := service.Db.Conn.Queryx(`SELECT * FROM datapoint_value WHERE datapoint_id=$1 ORDER BY ts DESC LIMIT 500`, id) + if err != nil { + err = werr.Wrap(err).WithData(id) + return + } + defer rows.Close() + + for rows.Next() { + dpv := DatapointValue{} + err = rows.StructScan(&dpv) + if err != nil { + err = werr.Wrap(err).WithData(id) + return + } + values = append(values, dpv) } return }// }}} diff --git a/main.go b/main.go index e8c5f2b..e999bdd 100644 --- a/main.go +++ b/main.go @@ -122,6 +122,7 @@ func main() { // {{{ 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/edit/{id}", false, false, pageTriggerEdit) @@ -194,6 +195,8 @@ func entryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { / 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() @@ -204,23 +207,77 @@ func entryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { / } logger.Debug("entry", "datapoint", dpoint, "value", value, "trigger", trigger, "result", out) + var problemID int switch v := out.(type) { case bool: // Trigger returning true - a problem occurred if v { - err = ProblemStart(trigger) + problemID, err = ProblemStart(trigger) + logger.Info("FOO", "problemID", problemID, "err==nil", err == nil) if err != nil { err = werr.Wrap(err).Log() logger.Error("entry", "error", err) } + } else { - err = ProblemClose(trigger) + // A problem didn't occur. + problemID, err = ProblemClose(trigger) + logger.Info("FOO", "problemID", problemID, "err==nil", err == nil) if err != nil { err = werr.Wrap(err).Log() logger.Error("entry", "error", err) } } + // 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(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) @@ -519,6 +576,43 @@ func pageDatapointDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { 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() diff --git a/notification/ntfy.go b/notification/ntfy.go index 4cae808..51b5896 100644 --- a/notification/ntfy.go +++ b/notification/ntfy.go @@ -44,7 +44,7 @@ func (ntfy *NTFY) GetPrio() int { return ntfy.Prio } -func (ntfy NTFY) Send(uuid string, msg []byte) (err error) { +func (ntfy NTFY) Send(problemID int, msg []byte) (err error) { var req *http.Request var res *http.Response req, err = http.NewRequest("POST", ntfy.URL, bytes.NewReader(msg)) @@ -53,9 +53,9 @@ func (ntfy NTFY) Send(uuid string, msg []byte) (err error) { return } - ackURL := fmt.Sprintf("http, OK, %s/notification/ack?uuid=%s", ntfy.AcknowledgeURL, uuid) + ackURL := fmt.Sprintf("http, OK, %s/notification/ack?problemID=%d", ntfy.AcknowledgeURL, problemID) req.Header.Add("X-Actions", ackURL) - req.Header.Add("X-Priority", "3") // XXX: should be 5 + req.Header.Add("X-Priority", "4") // XXX: should be 5 req.Header.Add("X-Tags", "calendar") res, err = http.DefaultClient.Do(req) diff --git a/notification/pkg.go b/notification/pkg.go index 2e01519..cc8bbcc 100644 --- a/notification/pkg.go +++ b/notification/pkg.go @@ -13,7 +13,7 @@ type Service interface { SetLogger(*slog.Logger) GetPrio() int GetType() string - Send(string, []byte) error + Send(int, []byte) error } type Manager struct { @@ -40,20 +40,22 @@ func (nm *Manager) AddService(service Service) { }) } -func (nm *Manager) Send(uuid string, msg []byte) (err error) { - for _, service := range nm.services { +func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (err error) { + for i, service := range nm.services { nm.logger.Info("notification", "service", service.GetType(), "prio", service.GetPrio()) - if err = service.Send(uuid, msg); err == nil { + if err = service.Send(problemID, msg); err == nil { + fn(&nm.services[i], nil) break } else { data := struct { - UUID string + ProblemID int Msg []byte }{ - uuid, + problemID, msg, } werr.Wrap(err).WithData(data).Log() + fn(&nm.services[i], err) } } diff --git a/notification_log.go b/notification_log.go new file mode 100644 index 0000000..7b70d35 --- /dev/null +++ b/notification_log.go @@ -0,0 +1,20 @@ +package main + +import ( + // Internal + "smon/notification" + // Standard +) + +func notificationLog(notificationService *notification.Service, problemID int, err error) { + if err == nil { + logger.Info("notification", "service", (*notificationService).GetType(), "problemID", problemID, "prio", (*notificationService).GetPrio(), "ok", true) + service.Db.Conn.Query( + ` + INSERT INTO notification_send() + `, + ) + } else { + logger.Error("notification", "service", (*notificationService).GetType(), "problemID", problemID, "prio", (*notificationService).GetPrio(), "ok", false, "error", err) + } +} diff --git a/problem.go b/problem.go index 67717dd..1286828 100644 --- a/problem.go +++ b/problem.go @@ -66,7 +66,7 @@ func ProblemsRetrieve() (problems []Problem, err error) { return } -func ProblemStart(trigger Trigger) (err error) { +func ProblemStart(trigger Trigger) (problemID int, err error) { row := service.Db.Conn.QueryRow(` SELECT COUNT(id) FROM problem @@ -86,7 +86,8 @@ func ProblemStart(trigger Trigger) (err error) { // Open up a new problem if no open exists. if openProblems == 0 { - _, err = service.Db.Conn.Exec(`INSERT INTO problem(trigger_id) VALUES($1)`, trigger.ID) + row = service.Db.Conn.QueryRow(`INSERT INTO problem(trigger_id) VALUES($1) RETURNING id`, trigger.ID) + err = row.Scan(&problemID) if err != nil { err = we.Wrap(err).WithData(trigger) } @@ -94,8 +95,15 @@ func ProblemStart(trigger Trigger) (err error) { return } -func ProblemClose(trigger Trigger) (err error) { - _, err = service.Db.Conn.Exec(`UPDATE problem SET "end"=NOW() WHERE trigger_id=$1 AND "end" IS NULL`, trigger.ID) +func ProblemClose(trigger Trigger) (problemID int, err error) { + row := service.Db.Conn.QueryRow(`UPDATE problem SET "end"=NOW() WHERE trigger_id=$1 AND "end" IS NULL RETURNING id`, trigger.ID) + err = row.Scan(&problemID) + + if err == sql.ErrNoRows { + err = nil + return + } + if err != nil { err = we.Wrap(err).WithData(trigger) return diff --git a/sql/00012.sql b/sql/00012.sql new file mode 100644 index 0000000..8ccfff4 --- /dev/null +++ b/sql/00012.sql @@ -0,0 +1,13 @@ +CREATE TABLE public.notification_send ( + id serial NOT NULL, + notification_id int4 NOT NULL, + "uuid" char(36) NOT NULL, + send timestamptz DEFAULT now() NOT NULL, + ok bool NOT NULL, + error jsonb NULL, + acknowledged bool DEFAULT false NOT NULL, + problem_id int8 NOT NULL, + CONSTRAINT notification_send_pk PRIMARY KEY (id), + CONSTRAINT notification_send_notification_fk FOREIGN KEY (notification_id) REFERENCES public.notification(id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT notification_send_problem_fk FOREIGN KEY (problem_id) REFERENCES public.problem(id) ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/static/css/datapoints.css b/static/css/datapoints.css index 0713253..82b89a2 100644 --- a/static/css/datapoints.css +++ b/static/css/datapoints.css @@ -109,6 +109,17 @@ label { #datapoints div { white-space: nowrap; } +#datapoints .icons { + display: flex; + gap: 12px; + align-items: center; +} +#values { + display: grid; + grid-template-columns: repeat(2, min-content); + gap: 16px; + white-space: nowrap; +} .widgets { display: grid; grid-template-columns: min-content 1fr; diff --git a/static/css/main.css b/static/css/main.css index f3d5d59..039c547 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -101,7 +101,6 @@ label { display: grid; grid-template-areas: "menu content"; grid-template-columns: 64px 1fr; - grid-template-rows: 100% 100%; height: 100vh; } #menu { diff --git a/static/images/values.svg b/static/images/values.svg new file mode 100644 index 0000000..78b7b92 --- /dev/null +++ b/static/images/values.svg @@ -0,0 +1,67 @@ + + + + + + + + + + image/svg+xml + + + + + + format-list-bulleted + + + diff --git a/static/less/datapoints.less b/static/less/datapoints.less index 7483cc9..db152b9 100644 --- a/static/less/datapoints.less +++ b/static/less/datapoints.less @@ -13,6 +13,19 @@ div { white-space: nowrap; } + + .icons { + display: flex; + gap: 12px; + align-items: center; + } +} + +#values { + display: grid; + grid-template-columns: repeat(2, min-content); + gap: 16px; + white-space: nowrap; } .widgets { diff --git a/static/less/main.less b/static/less/main.less index 54e54ae..979e0f8 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -4,8 +4,6 @@ display: grid; grid-template-areas: "menu content"; grid-template-columns: 64px 1fr; - grid-template-rows: - 100% 100%; height: 100vh; } diff --git a/views/pages/datapoint_values.gotmpl b/views/pages/datapoint_values.gotmpl new file mode 100644 index 0000000..fe4651c --- /dev/null +++ b/views/pages/datapoint_values.gotmpl @@ -0,0 +1,13 @@ +{{ define "page" }} + {{ $version := .VERSION }} + + + {{ block "page_label" . }}{{end}} + +
+ {{ range .Data.Values }} +
{{ format_time .Ts }}
+
{{ .Value }}
+ {{ end }} +
+{{ end }} diff --git a/views/pages/datapoints.gotmpl b/views/pages/datapoints.gotmpl index 50e3727..a8dbf1c 100644 --- a/views/pages/datapoints.gotmpl +++ b/views/pages/datapoints.gotmpl @@ -23,7 +23,10 @@ {{ else }}
{{ .LastDatapointValue.Value }}
{{ end }} -
- {{ end }} +
+
+
+
+ {{ end }} {{ end }}