diff --git a/main.go b/main.go index 55dd765..c2e20ff 100644 --- a/main.go +++ b/main.go @@ -150,6 +150,8 @@ func main() { // {{{ 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) @@ -1194,3 +1196,61 @@ func actionConfigurationNotificationDelete(w http.ResponseWriter, r *http.Reques w.Header().Add("Location", "/configuration") w.WriteHeader(302) } // }}} + +func pageNotifications(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ + var err error + + // GET parameters. + var timeFrom, timeTo time.Time + lastWeek := time.Now().Add(time.Duration(-7 * 24 * time.Hour)) + + timeFrom, err = parseHTMLDateTime(r.URL.Query().Get("f"), lastWeek) + if err != nil { + httpError(w, werr.Wrap(err).Log()) + return + } + + timeTo, err = parseHTMLDateTime(r.URL.Query().Get("t"), time.Now()) + if err != nil { + httpError(w, werr.Wrap(err).Log()) + return + } + + var presetTime int + now := time.Now() + presetStr := r.URL.Query().Get("preset") + if presetStr != "" { + presetTime, err = strconv.Atoi(presetStr) + if err != nil { + pageError(w, "/notifications", werr.Wrap(err).WithData(presetStr).Log()) + return + } + timeFrom = now.Add(time.Hour * -1 * time.Duration(presetTime)) + timeTo = now + } + + // Apply an optionally set offset (in seconds). + var offsetTime int + offsetTimeStr := r.URL.Query().Get("offset-time") + offsetTime, err = strconv.Atoi(offsetTimeStr) + timeFrom = timeFrom.Add(time.Second * time.Duration(offsetTime)) + timeTo = timeTo.Add(time.Second * time.Duration(offsetTime)) + + 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, + "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), + "TimeTo": timeTo.Format("2006-01-02T15:04:05"), + }, + } + + page.Render(w, r) +} // }}} diff --git a/notification_log.go b/notification_log.go index 7b70d35..c6d0a71 100644 --- a/notification_log.go +++ b/notification_log.go @@ -1,11 +1,32 @@ package main import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + "github.com/jmoiron/sqlx" + // Internal "smon/notification" + // Standard + "database/sql" + "encoding/json" + "time" ) +type NotificationSend struct { + Prio int + Service string + ID int + UUID string + Sent time.Time `db:"send"` + OK bool + Error sql.NullString + ErrorIndented string + Acknowledged bool + TriggerName string `db:"trigger_name"` +} + 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) @@ -18,3 +39,61 @@ func notificationLog(notificationService *notification.Service, problemID int, e logger.Error("notification", "service", (*notificationService).GetType(), "problemID", problemID, "prio", (*notificationService).GetPrio(), "ok", false, "error", err) } } + +func notificationsSent(from, to time.Time) (nss []NotificationSend, err error) { + var rows *sqlx.Rows + rows, err = service.Db.Conn.Queryx( + ` + SELECT + n.prio, + n.service, + + t.name AS trigger_name, + + ns.id, + ns.uuid, + ns.send, + ns.ok, + ns.error::varchar, + ns.acknowledged + + FROM public.notification_send ns + INNER JOIN notification n ON ns.notification_id = n.id + INNER JOIN problem p ON ns.problem_id = p.id + INNER JOIN "trigger" t ON p.trigger_id = t.id + WHERE + ns.send >= $1 AND + ns.send <= $2 + + ORDER BY + send DESC + `, + from, + to, + ) + if err != nil { + err = werr.Wrap(err) + return + } + defer rows.Close() + + for rows.Next() { + ns := NotificationSend{} + err = rows.StructScan(&ns) + if err != nil { + err = werr.Wrap(err) + return + } + + // Error contains json (can be NULL), + // and can at least be presented indented. + foo := make(map[string]any) + json.Unmarshal([]byte(ns.Error.String), &foo) + var j []byte + j, err = json.MarshalIndent(foo, "", " ") + ns.ErrorIndented = string(j) + + nss = append(nss, ns) + } + return +} diff --git a/static/css/default_light/default_light.css b/static/css/default_light/default_light.css index 57e3f8c..638f1a5 100644 --- a/static/css/default_light/default_light.css +++ b/static/css/default_light/default_light.css @@ -40,11 +40,16 @@ button:focus { #areas .area .section .name { font-weight: normal; } +dialog { + border-radius: 8px; +} +dialog, #datapoints, #problems-list, #acknowledged-list, #values, -#services { +#services, +#notifications { background-color: #fff !important; border: 1px solid #ddd; box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25); diff --git a/static/css/default_light/gruvbox.css b/static/css/default_light/gruvbox.css index 54b45b4..d0860ec 100644 --- a/static/css/default_light/gruvbox.css +++ b/static/css/default_light/gruvbox.css @@ -1,6 +1,9 @@ body { background-image: url(/images/v0/gruvbox/background.svg); } +#menu { + box-shadow: 2px 0px 5px 3px rgba(0, 0, 0, 0.25); +} #areas .area { box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5); } diff --git a/static/css/default_light/main.css b/static/css/default_light/main.css index 30f8dae..10baf58 100644 --- a/static/css/default_light/main.css +++ b/static/css/default_light/main.css @@ -39,7 +39,7 @@ html { #layout { display: grid; grid-template-areas: "menu content"; - grid-template-columns: 104px 1fr; + grid-template-columns: 128px 1fr; height: 100vh; } #menu { diff --git a/static/css/default_light/notifications.css b/static/css/default_light/notifications.css new file mode 100644 index 0000000..a39d982 --- /dev/null +++ b/static/css/default_light/notifications.css @@ -0,0 +1,65 @@ +#time-select { + display: grid; + grid-template-columns: min-content min-content; + grid-gap: 6px 16px; + padding: 16px; + border: 1px solid #7bb8eb; + width: min-content; + border-radius: 6px; +} +#time-select button { + width: 100px; + margin-top: 12px; + justify-self: end; +} +#time-select #time-offsets { + display: grid; + grid-template-columns: min-content repeat(3, min-content); + grid-gap: 16px; + margin-top: 16px; + align-items: center; + justify-items: center; +} +#time-select #time-offsets .header-1 { + font-weight: bold; + justify-self: start; +} +#time-select #time-offsets .header-2 { + font-weight: bold; + justify-self: start; + grid-column: 2 / -1; +} +#time-select #time-offsets .preset { + white-space: nowrap; + justify-self: start; + padding-right: 32px; +} +input[type="datetime-local"] { + padding: 6px; +} +#notifications { + display: grid; + grid-template-columns: repeat(5, min-content); + grid-gap: 4px 16px; + margin-top: 32px; + margin-bottom: 32px; + background-color: #2979b8; + padding: 16px 24px; + width: min-content; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} +#notifications div { + white-space: nowrap; + line-height: 24px; +} +#notifications .header { + font-weight: 800; + color: #7bb8eb; +} +#notifications .ok { + color: #0a0; +} +#notifications .error { + color: #a00; +} diff --git a/static/css/gruvbox/default_light.css b/static/css/gruvbox/default_light.css index 3361608..95d95f2 100644 --- a/static/css/gruvbox/default_light.css +++ b/static/css/gruvbox/default_light.css @@ -40,11 +40,16 @@ button:focus { #areas .area .section .name { font-weight: normal; } +dialog { + border-radius: 8px; +} +dialog, #datapoints, #problems-list, #acknowledged-list, #values, -#services { +#services, +#notifications { background-color: #fff !important; border: 1px solid #ddd; box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25); diff --git a/static/css/gruvbox/main.css b/static/css/gruvbox/main.css index b69f507..98f785f 100644 --- a/static/css/gruvbox/main.css +++ b/static/css/gruvbox/main.css @@ -39,7 +39,7 @@ html { #layout { display: grid; grid-template-areas: "menu content"; - grid-template-columns: 104px 1fr; + grid-template-columns: 128px 1fr; height: 100vh; } #menu { diff --git a/static/css/gruvbox/notifications.css b/static/css/gruvbox/notifications.css new file mode 100644 index 0000000..aaeab41 --- /dev/null +++ b/static/css/gruvbox/notifications.css @@ -0,0 +1,65 @@ +#time-select { + display: grid; + grid-template-columns: min-content min-content; + grid-gap: 6px 16px; + padding: 16px; + border: 1px solid #777; + width: min-content; + border-radius: 6px; +} +#time-select button { + width: 100px; + margin-top: 12px; + justify-self: end; +} +#time-select #time-offsets { + display: grid; + grid-template-columns: min-content repeat(3, min-content); + grid-gap: 16px; + margin-top: 16px; + align-items: center; + justify-items: center; +} +#time-select #time-offsets .header-1 { + font-weight: bold; + justify-self: start; +} +#time-select #time-offsets .header-2 { + font-weight: bold; + justify-self: start; + grid-column: 2 / -1; +} +#time-select #time-offsets .preset { + white-space: nowrap; + justify-self: start; + padding-right: 32px; +} +input[type="datetime-local"] { + padding: 6px; +} +#notifications { + display: grid; + grid-template-columns: repeat(5, min-content); + grid-gap: 4px 16px; + margin-top: 32px; + margin-bottom: 32px; + background-color: #333; + padding: 16px 24px; + width: min-content; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} +#notifications div { + white-space: nowrap; + line-height: 24px; +} +#notifications .header { + font-weight: 800; + color: #777; +} +#notifications .ok { + color: #0a0; +} +#notifications .error { + color: #a00; +} diff --git a/static/images/default_light/notifications.svg b/static/images/default_light/notifications.svg new file mode 100644 index 0000000..1e709b5 --- /dev/null +++ b/static/images/default_light/notifications.svg @@ -0,0 +1,51 @@ + + + + + + + + email-fast + email-fast-outline + + + diff --git a/static/images/default_light/notifications_selected.svg b/static/images/default_light/notifications_selected.svg new file mode 100644 index 0000000..46616f0 --- /dev/null +++ b/static/images/default_light/notifications_selected.svg @@ -0,0 +1,49 @@ + + + + + + + + email-fast + + + diff --git a/static/images/gruvbox/notifications.svg b/static/images/gruvbox/notifications.svg new file mode 100644 index 0000000..db6dace --- /dev/null +++ b/static/images/gruvbox/notifications.svg @@ -0,0 +1,51 @@ + + + + + + + + email-fast + email-fast-outline + + + diff --git a/static/images/gruvbox/notifications_selected.svg b/static/images/gruvbox/notifications_selected.svg new file mode 100644 index 0000000..00469dd --- /dev/null +++ b/static/images/gruvbox/notifications_selected.svg @@ -0,0 +1,51 @@ + + + + + + + + email-fast + email-fast-outline + + + diff --git a/static/less/default_light.less b/static/less/default_light.less index 542a083..ba51838 100644 --- a/static/less/default_light.less +++ b/static/less/default_light.less @@ -58,7 +58,11 @@ button { } } -#datapoints, #problems-list, #acknowledged-list, #values, #services { +dialog { + border-radius: 8px; +} + +dialog, #datapoints, #problems-list, #acknowledged-list, #values, #services, #notifications { background-color: #fff !important; border: 1px solid #ddd; box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.25); diff --git a/static/less/main.less b/static/less/main.less index 81d35a5..aa11833 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -52,7 +52,7 @@ html { #layout { display: grid; grid-template-areas: "menu content"; - grid-template-columns: 104px 1fr; + grid-template-columns: 128px 1fr; height: 100vh; } @@ -248,7 +248,7 @@ button { } .line { - grid-column: 1 / -1; + grid-column: ~"1 / -1"; border-bottom: 1px solid .lighterOrDarker(@bg1, 15%)[@result]; } diff --git a/static/less/notifications.less b/static/less/notifications.less new file mode 100644 index 0000000..62037d6 --- /dev/null +++ b/static/less/notifications.less @@ -0,0 +1,81 @@ +@import "theme-@{THEME}.less"; + +#time-select { + display: grid; + grid-template-columns: min-content min-content; + grid-gap: 6px 16px; + + padding: 16px; + border: 1px solid @text3; + width: min-content; + border-radius: 6px; + + button { + //grid-column: ~"1 / -1"; + width: 100px; + margin-top: 12px; + justify-self: end; + } + + #time-offsets { + display: grid; + grid-template-columns: min-content repeat(3, min-content); + grid-gap: 16px; + margin-top: 16px; + + align-items: center; + justify-items: center; + + .header-1 { + font-weight: bold; + justify-self: start; + } + + .header-2 { + font-weight: bold; + justify-self: start; + grid-column: ~"2 / -1"; + } + + .preset { + white-space: nowrap; + justify-self: start; + padding-right: 32px; + } + } +} + +input[type="datetime-local"] { + padding: 6px; +} + +#notifications { + display: grid; + grid-template-columns: repeat(5, min-content); + grid-gap: 4px 16px; + margin-top: 32px; + margin-bottom: 32px; + background-color: @bg3; + padding: 16px 24px; + width: min-content; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + + div { + white-space: nowrap; + line-height: 24px; + } + + .header { + font-weight: @bold; + color: @text3; + } + + .ok { + color: #0a0; + } + + .error { + color: #a00; + } +} diff --git a/static/less/problems.less b/static/less/problems.less index e68a06e..601a9b4 100644 --- a/static/less/problems.less +++ b/static/less/problems.less @@ -52,7 +52,7 @@ .name { color: @text2; - grid-column: 1 / -1; + grid-column: ~"1 / -1"; font-weight: bold !important; line-height: 24px; } diff --git a/views/components/menu.gotmpl b/views/components/menu.gotmpl index c0c1ddf..5a45fe0 100644 --- a/views/components/menu.gotmpl +++ b/views/components/menu.gotmpl @@ -28,6 +28,13 @@ +
+ + +
Notifications
+
+
+
diff --git a/views/pages/notifications.gotmpl b/views/pages/notifications.gotmpl new file mode 100644 index 0000000..afe39b3 --- /dev/null +++ b/views/pages/notifications.gotmpl @@ -0,0 +1,98 @@ +{{ define "page" }} + + {{ block "page_label" . }}{{end}} + {{ $version := .VERSION }} + {{ $theme := .CONFIG.THEME }} + + + +
+ + + +
+
From
+
To
+ + + +
+
Presets
+
Offsets
+ +
+ +
+
Hour
+
+ + + +
+
Day
+
+ + + +
+
Week
+
+ + + +
+
Month
+
+
+ +
+
+ +
+
Sent
+
OK
+
Trigger name
+
Service
+
Error
+ {{ range .Data.Notifications }} +
{{ format_time .Sent }}
+
{{ if .OK }}{{ else }}{{ end }}
+
{{ .TriggerName }}
+
{{ .Prio }}:{{ .Service }}
+
+ {{ if .Error.Valid }} + + +
+
{{ .ErrorIndented }}
+
+ +
+
+
+ {{ else }} + + {{ end }} +
+ {{ end }} +
+ +{{ end }}