diff --git a/datapoint.go b/datapoint.go index 9e925ad..3672033 100644 --- a/datapoint.go +++ b/datapoint.go @@ -36,7 +36,6 @@ type DatapointValue struct { ValueInt sql.NullInt64 `db:"value_int"` ValueString sql.NullString `db:"value_string"` ValueDateTime sql.NullTime `db:"value_datetime"` - TemplateValue any } func (dp DatapointValue) Value() any { // {{{ @@ -54,13 +53,13 @@ func (dp DatapointValue) Value() any { // {{{ return nil } // }}} -func (dp DatapointValue) FormattedTime() string {// {{{ +func (dp DatapointValue) FormattedTime() string { if dp.ValueDateTime.Valid { return dp.ValueDateTime.Time.Format("2006-01-02 15:04:05") } return "invalid time" -}// }}} -func (dp Datapoint) Update() (err error) {// {{{ +} +func (dp Datapoint) Update() (err error) { name := strings.TrimSpace(dp.Name) if name == "" { err = errors.New("Name can't be empty") @@ -83,7 +82,7 @@ func (dp Datapoint) Update() (err error) {// {{{ } return -}// }}} +} func DatapointAdd[T any](name string, value T) (err error) { // {{{ row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name) diff --git a/main.go b/main.go index 60e1317..87fab04 100644 --- a/main.go +++ b/main.go @@ -95,22 +95,21 @@ func main() { // {{{ 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("/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("/triggers", false, false, pageTriggers) service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit) - service.Register("/trigger/edit/{id}/{sectionID}", 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) @@ -171,45 +170,6 @@ func entryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { / return } - var triggers []Trigger - triggers, err = TriggersRetrieveByDatapoint(dpoint) - if err != nil { - logger.Error("entry", "error", err) - } - for _, trigger := range triggers { - var out any - out, err = trigger.Run() - if err != nil { - err = we.Wrap(err).Log() - logger.Error("entry", "error", err) - - } - logger.Debug("entry", "datapoint", dpoint, "value", value, "trigger", trigger, "result", out) - - switch v := out.(type) { - case bool: - // Trigger returning true - a problem occurred - if v { - err = ProblemStart(trigger) - if err != nil { - err = we.Wrap(err).Log() - logger.Error("entry", "error", err) - } - } else { - err = ProblemClose(trigger) - if err != nil { - err = we.Wrap(err).Log() - logger.Error("entry", "error", err) - } - } - - default: - err := fmt.Errorf(`Expression for trigger %s not returning bool (%T)`, trigger.Name, v) - logger.Info("entry", "error", err) - we.Wrap(err).WithData(v).Log() - } - } - j, _ := json.Marshal(struct{ OK bool }{true}) w.Write(j) } // }}} @@ -244,10 +204,8 @@ func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{ } funcMap := template.FuncMap{ - "format_time": func(t time.Time) template.HTML { - return template.HTML( - t.Local().Format(`2006-01-02 15:04:05`), - ) + "format_time": func(t time.Time) string { + return t.Local().Format("2006-01-02 15:04:05") }, } @@ -275,65 +233,32 @@ func pageIndex(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ } page.Render(w) } // }}} - func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ page := Page{ LAYOUT: "main", PAGE: "problems", } - problems, err := ProblemsRetrieve() - if err != nil { - httpError(w, we.Wrap(err).Log()) - return - } + /* + 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{ - "Problems": problems, + //"Areas": areas, } 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, we.Wrap(err).Log()) - return - } - - err = ProblemAcknowledge(id, true) - if err != nil { - httpError(w, we.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, we.Wrap(err).Log()) - return - } - - err = ProblemAcknowledge(id, false) - if err != nil { - httpError(w, we.Wrap(err).Log()) - return - } - - w.Header().Add("Location", "/problems") - w.WriteHeader(302) - - return -} // }}} - -func pageDatapoints(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ +func pageDatapoints(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ page := Page{ LAYOUT: "main", PAGE: "datapoints", @@ -344,13 +269,7 @@ func pageDatapoints(w http.ResponseWriter, r *http.Request, _ *session.T) { // { httpError(w, we.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 - } + logger.Info("FOO", "dps", datapoints) page.Data = map[string]any{ "Datapoints": datapoints, @@ -413,7 +332,6 @@ func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { w.Header().Add("Location", "/datapoints") w.WriteHeader(302) } // }}} - func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ areas, err := TriggersRetrieve() if err != nil { @@ -443,26 +361,11 @@ func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // 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, we.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, we.Wrap(err).Log()) - return - } - } + trigger, err = TriggerRetrieve(id) + if err != nil { + httpError(w, we.Wrap(err).Log()) + return } datapoints := make(map[string]Datapoint) @@ -472,7 +375,6 @@ func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // httpError(w, we.Wrap(err).Log()) return } - dp.LastDatapointValue.TemplateValue = dp.LastDatapointValue.Value() datapoints[dpname] = dp } @@ -499,23 +401,14 @@ func pageTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { / } var trigger Trigger - if id > 0 { - trigger, err = TriggerRetrieve(id) - if err != nil { - httpError(w, we.Wrap(err).Log()) - return - } - } else { - trigger.SectionID, err = strconv.Atoi(r.FormValue("sectionID")) - if err != nil { - httpError(w, we.Wrap(err).Log()) - return - } + trigger, err = TriggerRetrieve(id) + if err != nil { + httpError(w, we.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, we.Wrap(err).Log()) @@ -543,13 +436,23 @@ func pageTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // { expr, _ := io.ReadAll(r.Body) trigger.Expression = string(expr) + 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 + } + resp := struct { OK bool Output any }{ OK: true, } - resp.Output, err = trigger.Run() + resp.Output, err = trigger.Run(datapoints) if err != nil { we.Wrap(err).Log() httpError(w, err) @@ -560,7 +463,6 @@ func pageTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // { 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 { diff --git a/page.go b/page.go index 2465645..c6e22d4 100644 --- a/page.go +++ b/page.go @@ -52,6 +52,7 @@ func (p *Page) Render(w http.ResponseWriter) { "Data": p.Data, } + logger.Info("foo", "tmpl", tmpl) err = tmpl.Execute(w, data) if err != nil { httpError(w, we.Wrap(err).Log()) diff --git a/problem.go b/problem.go index 5d6f0b5..4c7b00e 100644 --- a/problem.go +++ b/problem.go @@ -2,108 +2,19 @@ package main import ( // External - we "git.gibonuddevalla.se/go/wrappederror" + // we "git.gibonuddevalla.se/go/wrappederror" // Standard - "database/sql" - "encoding/json" - "time" + // "encoding/json" ) +/* type Problem struct { - ID int - Start time.Time - End sql.NullTime - Acknowledged bool - TriggerID int `json:"trigger_id"` - TriggerName string `json:"trigger_name"` - AreaName string `json:"area_name"` - SectionName string `json:"section_name"` + ID int + Name string + SectionID int + Expression string + DatapointNames []string } +*/ -func ProblemsRetrieve() (problems []Problem, err error) { - problems = []Problem{} - row := service.Db.Conn.QueryRow(` - SELECT - jsonb_agg(p.*) - FROM ( - SELECT - p.id, - p.start, - p.end, - p.acknowledged, - t.id AS trigger_id, - t.name AS trigger_name, - a.name AS area_name, - s.name AS section_name - FROM problem p - INNER JOIN "trigger" t ON p.trigger_id = t.id - INNER JOIN section s ON t.section_id = s.id - INNER JOIN area a ON s.area_id = a.id - - WHERE - p.end IS NULL - - ORDER BY p.start DESC - ) p - `) - - var jsonBody []byte - err = row.Scan(&jsonBody) - if err != nil { - err = we.Wrap(err) - return - } - - err = json.Unmarshal(jsonBody, &problems) - if err != nil { - err = we.Wrap(err) - } - return -} - -func ProblemStart(trigger Trigger) (err error) { - row := service.Db.Conn.QueryRow(` - SELECT COUNT(id) - FROM problem - WHERE - trigger_id = $1 AND - "end" IS NULL - GROUP BY trigger_id - `, - trigger.ID, - ) - var openProblems int - err = row.Scan(&openProblems) - if err != nil && err != sql.ErrNoRows { - err = we.Wrap(err).WithData(trigger.ID) - return - } - - // 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) - if err != nil { - err = we.Wrap(err).WithData(trigger) - } - } - 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) - if err != nil { - err = we.Wrap(err).WithData(trigger) - return - } - return -} - -func ProblemAcknowledge(id int, state bool) (err error) { - _, err = service.Db.Conn.Exec(`UPDATE problem SET "acknowledged"=$2 WHERE id=$1`, id, state) - if err != nil { - err = we.Wrap(err).WithData(id) - return - } - return -} diff --git a/sql/00008.sql b/sql/00008.sql deleted file mode 100644 index d7498df..0000000 --- a/sql/00008.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."trigger" ADD CONSTRAINT trigger_sectionname_unique UNIQUE (section_id,"name"); diff --git a/static/css/datapoints.css b/static/css/datapoints.css index 8230ed8..dddb99b 100644 --- a/static/css/datapoints.css +++ b/static/css/datapoints.css @@ -37,7 +37,7 @@ h2 { font-size: 1.25em; } a { - color: #fabd2f; + color: #3f9da1; text-decoration: none; } a:hover { @@ -70,31 +70,13 @@ button { background: #202020; color: #d5c4a1; padding: 8px 32px; - border: 1px solid #535353; + border: 1px solid #3a3a3a; font-size: 1em; height: 3em; } button:focus { background: #333; } -.line { - grid-column: 1 / -1; - border-bottom: 1px solid #4e4e4e; -} -span.date { - color: #d5c4a1; - font-weight: 500; -} -span.time { - font-size: 0.9em; - color: #d5c4a1; -} -span.seconds { - display: none; -} -label { - user-select: none; -} #datapoints { display: grid; grid-template-columns: repeat(4, min-content); @@ -120,10 +102,10 @@ label { width: 100%; } .widgets .datapoints { + font: "Roboto Mono", monospace; display: grid; grid-template-columns: min-content 1fr; gap: 6px 8px; - font-family: "Roboto Mono", monospace; margin-bottom: 8px; } .widgets .action { diff --git a/static/css/main.css b/static/css/main.css index cf2c7e5..f5b869e 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -37,7 +37,7 @@ h2 { font-size: 1.25em; } a { - color: #fabd2f; + color: #3f9da1; text-decoration: none; } a:hover { @@ -70,31 +70,13 @@ button { background: #202020; color: #d5c4a1; padding: 8px 32px; - border: 1px solid #535353; + border: 1px solid #3a3a3a; font-size: 1em; height: 3em; } button:focus { background: #333; } -.line { - grid-column: 1 / -1; - border-bottom: 1px solid #4e4e4e; -} -span.date { - color: #d5c4a1; - font-weight: 500; -} -span.time { - font-size: 0.9em; - color: #d5c4a1; -} -span.seconds { - display: none; -} -label { - user-select: none; -} #layout { display: grid; grid-template-areas: "menu content"; @@ -155,18 +137,13 @@ label { #areas .area .section { margin: 8px 16px; } -#areas .area .section .create { - display: grid; - grid-template-columns: min-content min-content; - grid-gap: 8px; - white-space: nowrap; -} -#areas .area .section .create .new { - font-weight: 500; -} #areas .area .section > .name { font-weight: 500; } +#areas .area .section .triggers a { + color: inherit; + text-decoration: none; +} #areas .area .section .triggers .trigger { display: grid; grid-template-columns: min-content 1fr; @@ -178,11 +155,5 @@ label { height: 16px; } #areas .area .section .triggers .trigger .label { - color: inherit; -} -dialog { - background: #202020; - border: 1px solid #606060; - color: #d5c4a1; - box-shadow: 10px 10px 15px 0px rgba(0, 0, 0, 0.25); + color: #3f9da1; } diff --git a/static/css/problems.css b/static/css/problems.css deleted file mode 100644 index 48d1c30..0000000 --- a/static/css/problems.css +++ /dev/null @@ -1,124 +0,0 @@ -html { - box-sizing: border-box; -} -*, -*:before, -*:after { - box-sizing: inherit; -} -*:focus { - outline: none; -} -[onClick] { - cursor: pointer; -} -html, -body { - margin: 0; - padding: 0; -} -body { - background: #282828; - font-family: "Roboto", sans-serif; - font-weight: 300; - color: #d5c4a1; - font-size: 11pt; -} -h1, -h2 { - margin-top: 0px; - margin-bottom: 4px; -} -h1 { - font-size: 1.5em; - color: #fb4934; -} -h2 { - font-size: 1.25em; -} -a { - color: #fabd2f; - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -b { - font-weight: 500; -} -.roboto-light { - font-family: "Roboto", sans-serif; - font-weight: 300; - font-style: normal; -} -.roboto-medium { - font-family: "Roboto", sans-serif; - font-weight: 500; - font-style: normal; -} -input[type="text"], -textarea, -select { - font-family: "Roboto Mono", monospace; - background: #202020; - color: #d5c4a1; - padding: 4px 8px; - border: none; - font-size: 1em; -} -button { - background: #202020; - color: #d5c4a1; - padding: 8px 32px; - border: 1px solid #535353; - font-size: 1em; - height: 3em; -} -button:focus { - background: #333; -} -.line { - grid-column: 1 / -1; - border-bottom: 1px solid #4e4e4e; -} -span.date { - color: #d5c4a1; - font-weight: 500; -} -span.time { - font-size: 0.9em; - color: #d5c4a1; -} -span.seconds { - display: none; -} -label { - user-select: none; -} -#problems-list, -#acknowledged-list { - display: grid; - grid-template-columns: repeat(5, min-content); - grid-gap: 4px 16px; - margin-bottom: 32px; -} -#problems-list div, -#acknowledged-list div { - white-space: nowrap; -} -#problems-list .header, -#acknowledged-list .header { - font-weight: 500; -} -#problems-list .trigger, -#acknowledged-list .trigger { - color: #fb4934; - font-weight: 500; -} -#problems-list .acknowledge img, -#acknowledged-list .acknowledge img { - height: 16px; -} -#acknowledged-list.hidden { - display: none; -} diff --git a/static/css/theme.css b/static/css/theme.css index bfc6ab6..6191e90 100644 --- a/static/css/theme.css +++ b/static/css/theme.css @@ -37,7 +37,7 @@ h2 { font-size: 1.25em; } a { - color: #fabd2f; + color: #3f9da1; text-decoration: none; } a:hover { @@ -70,28 +70,10 @@ button { background: #202020; color: #d5c4a1; padding: 8px 32px; - border: 1px solid #535353; + border: 1px solid #3a3a3a; font-size: 1em; height: 3em; } button:focus { background: #333; } -.line { - grid-column: 1 / -1; - border-bottom: 1px solid #4e4e4e; -} -span.date { - color: #d5c4a1; - font-weight: 500; -} -span.time { - font-size: 0.9em; - color: #d5c4a1; -} -span.seconds { - display: none; -} -label { - user-select: none; -} diff --git a/static/css/trigger_edit.css b/static/css/trigger_edit.css index 0beff8f..41e14b4 100644 --- a/static/css/trigger_edit.css +++ b/static/css/trigger_edit.css @@ -37,7 +37,7 @@ h2 { font-size: 1.25em; } a { - color: #fabd2f; + color: #3f9da1; text-decoration: none; } a:hover { @@ -70,31 +70,13 @@ button { background: #202020; color: #d5c4a1; padding: 8px 32px; - border: 1px solid #535353; + border: 1px solid #3a3a3a; font-size: 1em; height: 3em; } button:focus { background: #333; } -.line { - grid-column: 1 / -1; - border-bottom: 1px solid #4e4e4e; -} -span.date { - color: #d5c4a1; - font-weight: 500; -} -span.time { - font-size: 0.9em; - color: #d5c4a1; -} -span.seconds { - display: none; -} -label { - user-select: none; -} .widgets { display: grid; grid-template-columns: min-content 1fr; diff --git a/static/images/acknowledge-filled.svg b/static/images/acknowledge-filled.svg deleted file mode 100644 index f2d8a57..0000000 --- a/static/images/acknowledge-filled.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - thumb-up - - - diff --git a/static/images/acknowledge-outline.svg b/static/images/acknowledge-outline.svg deleted file mode 100644 index 7e97bfb..0000000 --- a/static/images/acknowledge-outline.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - thumb-up - thumb-up-outline - - - diff --git a/static/js/problems.mjs b/static/js/problems.mjs deleted file mode 100644 index 54c4e3c..0000000 --- a/static/js/problems.mjs +++ /dev/null @@ -1,23 +0,0 @@ -export class UI { - constructor() { - const showAcked = localStorage.getItem('show_acknowledged') - if (showAcked == 'true') { - document.getElementById('show-acked').checked = true - const list = document.getElementById('acknowledged-list') - list.classList.remove('hidden') - } - - } - - toggleAcknowledged(evt) { - const list = document.getElementById('acknowledged-list') - - if (evt.target.checked) { - list.classList.remove('hidden') - localStorage.setItem('show_acknowledged', true) - } else { - list.classList.add('hidden') - localStorage.setItem('show_acknowledged', false) - } - } -} diff --git a/static/js/trigger_edit.mjs b/static/js/trigger_edit.mjs index e534562..3d18d82 100644 --- a/static/js/trigger_edit.mjs +++ b/static/js/trigger_edit.mjs @@ -1,35 +1,17 @@ export class UI { - constructor() {//{{{ + constructor() { document.getElementById('button-run'). - addEventListener('click', evt => evt.preventDefault()) + addEventListener('click', evt=>evt.preventDefault()) - document.addEventListener('keydown', evt => this.keyHandler(evt)) - - document.querySelector('input[name="name"]').focus() - - this.datapoints = [] - }//}}} - render() {//{{{ - document.querySelectorAll('.datapoints .datapoint').forEach(el => el.remove()); - - const datapoints = document.querySelector('.datapoints') - - let html = Object.keys(this.trigger.datapoints).sort().map(dpName => { - const dp = this.trigger.datapoints[dpName] - return ` -
${dp.Name}
-
${dp.LastDatapointValue.TemplateValue}
- ` - }).join('') - datapoints.innerHTML += html - }//}}} - setTrigger(t) {//{{{ + document.addEventListener('keydown', evt=>this.keyHandler(evt)) + } + setTrigger(t) { this.trigger = t - }//}}} - run() {//{{{ + } + run() { this.trigger.run() - }//}}} - keyHandler(evt) {//{{{ + } + keyHandler(evt) { if (!(evt.altKey && evt.shiftKey)) return @@ -39,75 +21,21 @@ export class UI { switch (evt.key) { case 'T': this.run() - break + break case 'S': - this.update() - break + document.getElementById('form-trigger').submit() + break } - }//}}} - addDatapoint() {//{{{ - const dlg = document.getElementById('dlg-datapoints') - const datalist = document.getElementById('list-datapoints') - dlg.showModal() - - fetch('/datapoints?format=json') - .then(data => data.json()) - .then(json => { - this.datapoints = json - - let html = '' - this.datapoints.forEach(dp => { - html += `