diff --git a/datapoint.go b/datapoint.go index 2d2286e..0690bbd 100644 --- a/datapoint.go +++ b/datapoint.go @@ -162,8 +162,6 @@ func DatapointAdd[T any](name string, value T) (err error) { // {{{ return } - service.Db.Conn.Exec(`UPDATE datapoint SET last_value = NOW(), nodata_is_problem = false WHERE id=$1`, dpID) - return } // }}} @@ -172,30 +170,19 @@ func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{ var rows *sqlx.Rows rows, err = service.Db.Conn.Queryx(` SELECT - dp.id, - dp.name, - dp.datatype, - dp.last_value, - dp.group, - dp.comment, - dp.nodata_problem_seconds, - - dpv.id AS v_id, - dpv.ts, - dpv.value_int, - dpv.value_string, - dpv.value_datetime - - FROM public.datapoint dp - LEFT JOIN ( - SELECT - *, - row_number() OVER (PARTITION BY "datapoint_id" ORDER BY ts DESC) AS rn - FROM datapoint_value - ) dpv ON dpv.datapoint_id = dp.id AND rn = 1 + id, name, datatype, last_value, "group", comment, nodata_problem_seconds, + last_value_id AS v_id, + CASE + WHEN last_value_id IS NULL THEN null + ELSE last_value + END AS ts, + last_value_int AS value_int, + last_value_string AS value_string, + last_value_datetime AS value_datetime + FROM datapoint ORDER BY - dp.group ASC, - dp.name ASC + "group" ASC, + name ASC `) if err != nil { err = werr.Wrap(err) @@ -255,11 +242,11 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{ var query string var param any if id > 0 { - query = `SELECT *, true AS found FROM datapoint WHERE id = $1` + query = `SELECT id, "group", name, "datatype", comment, last_value, nodata_problem_seconds, nodata_is_problem, true AS found FROM public.datapoint WHERE id = $1` param = id dp.ID = id } else { - query = `SELECT *, true AS found FROM datapoint WHERE name = $1` + query = `SELECT id, "group", name, "datatype", comment, last_value, nodata_problem_seconds, nodata_is_problem, true AS found FROM public.datapoint WHERE name = $1` param = name } diff --git a/main.go b/main.go index 9e83055..d771363 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ import ( "time" ) -const VERSION = "v36" +const VERSION = "v41" var ( logger *slog.Logger @@ -663,6 +663,27 @@ func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { / } } + /* 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", @@ -674,6 +695,7 @@ func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { / page.Data = map[string]any{ "Datapoint": datapoint, + "Triggers": triggers, } page.Render(w, r) return @@ -689,8 +711,15 @@ func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) 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.ID = id + 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")) @@ -702,6 +731,29 @@ func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) 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) } // }}} diff --git a/notification/ntfy.go b/notification/ntfy.go index 0e94516..8132d08 100644 --- a/notification/ntfy.go +++ b/notification/ntfy.go @@ -81,7 +81,7 @@ func (ntfy NTFY) Send(problemID int, msg []byte) (err error) { ackURL := fmt.Sprintf("http, OK, %s/notification/ack?problemID=%d", ntfy.AcknowledgeURL, problemID) req.Header.Add("X-Actions", ackURL) - req.Header.Add("X-Priority", "4") // XXX: should be 5 + req.Header.Add("X-Priority", "5") req.Header.Add("X-Tags", "calendar") res, err = http.DefaultClient.Do(req) diff --git a/sql/00026.sql b/sql/00026.sql new file mode 100644 index 0000000..364ccaa --- /dev/null +++ b/sql/00026.sql @@ -0,0 +1,62 @@ +/* Adding last values to the datapoint table since they are a regularly used value. */ +ALTER TABLE public.datapoint ADD COLUMN last_value_id int4 NULL; +ALTER TABLE public.datapoint ADD COLUMN last_value_int int8 NULL; +ALTER TABLE public.datapoint ADD COLUMN last_value_string varchar NULL; +ALTER TABLE public.datapoint ADD COLUMN last_value_datetime timestamptz NULL; + + + +/* Once-run query to update it to the latest, to avoid user having to wait for the next entry. */ +UPDATE public.datapoint AS dp +SET + last_value_id = dpv.id, + last_value_int = dpv.value_int, + last_value_string = dpv.value_string, + last_value_datetime = dpv.value_datetime +FROM ( + SELECT + dp.id AS datapoint_id, + dpv.id, + dpv.value_int, + dpv.value_string, + dpv.value_datetime + FROM public.datapoint dp + LEFT JOIN ( + SELECT + *, + row_number() OVER (PARTITION BY "datapoint_id" ORDER BY ts DESC) AS rn + FROM datapoint_value + ) dpv ON dpv.datapoint_id = dp.id AND rn = 1 +) AS dpv +WHERE + dpv.datapoint_id = dp.id; + + + +/* A trigger keeps the value current without bugs introduced in software missing the entry. */ +CREATE OR REPLACE FUNCTION datapoint_entry() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS +$$ +BEGIN + UPDATE public.datapoint + SET + nodata_is_problem = false, + last_value = NEW.ts, + last_value_id = NEW.id, + last_value_int = NEW.value_int, + last_value_string = NEW.value_string, + last_value_datetime = NEW.value_datetime + WHERE + id = NEW.datapoint_id; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER datapoint_entry + AFTER INSERT + ON public.datapoint_value + FOR EACH ROW + EXECUTE PROCEDURE datapoint_entry(); diff --git a/sql/00027.sql b/sql/00027.sql new file mode 100644 index 0000000..83a5625 --- /dev/null +++ b/sql/00027.sql @@ -0,0 +1,22 @@ +/* Updating a datapoint name also updates the jsonb array entry */ +CREATE OR REPLACE FUNCTION update_triggers_datapoint_name() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS +$$ +BEGIN +UPDATE "trigger" + SET + datapoints = (datapoints - OLD.name) || jsonb_build_array(NEW.name) + WHERE + datapoints ? OLD.name; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER datapoint_renamed + AFTER UPDATE + ON public.datapoint + FOR EACH ROW + EXECUTE PROCEDURE update_triggers_datapoint_name(); diff --git a/static/css/default_light/trigger_edit.css b/static/css/default_light/trigger_edit.css index 5192604..959c7cf 100644 --- a/static/css/default_light/trigger_edit.css +++ b/static/css/default_light/trigger_edit.css @@ -13,17 +13,24 @@ .widgets .datapoints { font: "Roboto Mono", monospace; display: grid; - grid-template-columns: min-content min-content 1fr; + grid-template-columns: repeat(4, min-content); gap: 6px 8px; margin-bottom: 8px; white-space: nowrap; } +.widgets .datapoints div { + white-space: nowrap; +} .widgets .datapoints .invalid { color: #c83737; } .widgets .datapoints .delete img { height: 16px; } +.widgets .datapoints .values img { + height: 16px; + width: 16px; +} .widgets .action { display: grid; grid-template-columns: min-content min-content 1fr; diff --git a/static/css/gruvbox/trigger_edit.css b/static/css/gruvbox/trigger_edit.css index 711dc17..ee5b1fc 100644 --- a/static/css/gruvbox/trigger_edit.css +++ b/static/css/gruvbox/trigger_edit.css @@ -13,17 +13,24 @@ .widgets .datapoints { font: "Roboto Mono", monospace; display: grid; - grid-template-columns: min-content min-content 1fr; + grid-template-columns: repeat(4, min-content); gap: 6px 8px; margin-bottom: 8px; white-space: nowrap; } +.widgets .datapoints div { + white-space: nowrap; +} .widgets .datapoints .invalid { color: #c83737; } .widgets .datapoints .delete img { height: 16px; } +.widgets .datapoints .values img { + height: 16px; + width: 16px; +} .widgets .action { display: grid; grid-template-columns: min-content min-content 1fr; diff --git a/static/js/datapoint_edit.mjs b/static/js/datapoint_edit.mjs index dfe5e9c..e9906e0 100644 --- a/static/js/datapoint_edit.mjs +++ b/static/js/datapoint_edit.mjs @@ -1,5 +1,6 @@ export class UI { - constructor() { + constructor(datapointData) { + this.datapoint = datapointData document.addEventListener('keydown', evt=>this.keyHandler(evt)) document.querySelector('input[name="group"]').focus() } diff --git a/static/js/trigger_edit.mjs b/static/js/trigger_edit.mjs index 826afd4..e74c2e3 100644 --- a/static/js/trigger_edit.mjs +++ b/static/js/trigger_edit.mjs @@ -19,9 +19,10 @@ export class UI { let html = Object.keys(this.trigger.datapoints).sort().map(dpName => { const dp = this.trigger.datapoints[dpName] return ` -