Compare commits

..

5 Commits

Author SHA1 Message Date
32523270a8 Merge branch 'notifications' 2024-06-29 19:32:05 +02:00
17a22caa5d Time filtering of notifications 2024-06-29 19:30:26 +02:00
Magnus Åhall
b0ffce05f0 Start of error reporting 2024-06-29 18:09:56 +02:00
e728a302ee Added static resources 2024-06-29 15:21:52 +02:00
ca9a6c3e1d Initial add of notifications 2024-06-29 15:20:31 +02:00
19 changed files with 682 additions and 8 deletions

60
main.go
View File

@ -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)
} // }}}

View File

@ -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
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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);

View File

@ -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 {

View File

@ -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;
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="15.63086mm"
height="8.46667mm"
viewBox="0 0 15.63086 8.46667"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="notification_selected.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="11.313708"
inkscape:cx="11.623068"
inkscape:cy="6.3197669"
inkscape:window-width="1916"
inkscape:window-height="1161"
inkscape:window-x="0"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-101.86454,-146.84375)">
<title
id="title1">email-fast</title>
<title
id="title1-8">email-fast-outline</title>
<path
d="m 116.19281,146.84375 h -8.46666 c -0.7164,0 -1.30257,0.58614 -1.30257,1.30254 v 5.86156 c 0,0.72292 0.58617,1.30257 1.30257,1.30257 h 8.46666 c 0.72294,0 1.30259,-0.57965 1.30259,-1.30257 v -5.86156 c 0,-0.7164 -0.57965,-1.30254 -1.30259,-1.30254 m 0,7.1641 h -8.46666 v -4.77389 l 4.23333,2.16877 4.23333,-2.16877 v 4.77389 m -4.23333,-3.70579 -4.23333,-2.15577 h 8.46666 l -4.23333,2.15577 m -6.83846,3.70579 c 0,0.11076 0.0197,0.21487 0.0325,0.32565 h -2.63771 c -0.35951,0 -0.65127,-0.29308 -0.65127,-0.65129 0,-0.35821 0.29176,-0.65127 0.65127,-0.65127 h 2.60515 v 0.97691 m -1.30256,-6.18718 h 1.33512 c -0.0123,0.11075 -0.0325,0.21486 -0.0325,0.32562 v 0.97694 h -1.30256 c -0.35821,0 -0.65129,-0.29306 -0.65129,-0.65127 0,-0.35821 0.29308,-0.65129 0.65129,-0.65129 m -1.30259,3.25641 c 0,-0.35821 0.29311,-0.65127 0.6513,-0.65127 h 1.95385 v 1.30254 h -1.95385 c -0.35819,0 -0.6513,-0.29306 -0.6513,-0.65127 z"
id="path1"
style="stroke-width:0.431972;font-variation-settings:normal;opacity:1;vector-effect:none;fill:#7bb8eb;fill-opacity:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="15.63086mm"
height="8.46667mm"
viewBox="0 0 15.63086 8.46667"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="notification_selected.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="11.313708"
inkscape:cx="11.623068"
inkscape:cy="6.850097"
inkscape:window-width="1916"
inkscape:window-height="1161"
inkscape:window-x="0"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-101.86456,-146.71146)">
<title
id="title1">email-fast</title>
<path
d="m 116.19286,146.71146 h -8.46669 c -0.7164,0 -1.30254,0.58614 -1.30254,1.30254 v 5.86156 c 0,0.72292 0.58614,1.30257 1.30254,1.30257 h 8.46669 c 0.72291,0 1.30256,-0.57965 1.30256,-1.30257 V 148.014 c 0,-0.7164 -0.57965,-1.30254 -1.30256,-1.30254 m 0,2.39021 -4.23334,2.16877 -4.23335,-2.16877 V 148.014 l 4.23335,2.15577 4.23334,-2.15577 v 1.08767 m -11.07182,4.77389 c 0,0.11076 0.0197,0.21487 0.0325,0.32565 h -2.63769 c -0.35951,0 -0.65129,-0.29308 -0.65129,-0.65129 0,-0.35821 0.29178,-0.65127 0.65129,-0.65127 h 2.60513 v 0.97691 m -1.30256,-6.18718 h 1.33512 c -0.0123,0.11075 -0.0325,0.21486 -0.0325,0.32562 v 0.97694 h -1.30256 c -0.35821,0 -0.65129,-0.29306 -0.65129,-0.65127 0,-0.35821 0.29308,-0.65129 0.65129,-0.65129 m -1.30256,3.25641 c 0,-0.35821 0.29308,-0.65127 0.65127,-0.65127 h 1.95385 v 1.30254 h -1.95385 c -0.35819,0 -0.65127,-0.29306 -0.65127,-0.65127 z"
id="path1"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#9bff07;fill-opacity:1;stroke-width:0.32964801;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="73.846642"
height="39.999989"
viewBox="0 0 19.53859 10.58333"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="notifications.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="11.313708"
inkscape:cx="11.623068"
inkscape:cy="6.3197671"
inkscape:window-width="1916"
inkscape:window-height="1161"
inkscape:window-x="0"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-101.86452,-146.84375)">
<title
id="title1">email-fast</title>
<title
id="title1-8">email-fast-outline</title>
<path
d="m 119.77487,146.84375 h -10.58332 c -0.8955,0 -1.62821,0.73267 -1.62821,1.62817 v 7.32695 c 0,0.90365 0.73271,1.62821 1.62821,1.62821 h 10.58332 c 0.90368,0 1.62824,-0.72456 1.62824,-1.62821 v -7.32695 c 0,-0.8955 -0.72456,-1.62817 -1.62824,-1.62817 m 0,8.95512 h -10.58332 v -5.96736 l 5.29166,2.71096 5.29166,-2.71096 v 5.96736 m -5.29166,-4.63223 -5.29166,-2.69472 h 10.58332 l -5.29166,2.69472 m -8.54807,4.63223 c 0,0.13845 0.0246,0.26859 0.0406,0.40706 h -3.29713 c -0.44939,0 -0.81409,-0.36635 -0.81409,-0.81411 0,-0.44776 0.3647,-0.81409 0.81409,-0.81409 h 3.25643 v 1.22114 m -1.6282,-7.73397 h 1.6689 c -0.0154,0.13844 -0.0406,0.26857 -0.0406,0.40702 v 1.22118 h -1.6282 c -0.44776,0 -0.81411,-0.36633 -0.81411,-0.81409 0,-0.44776 0.36635,-0.81411 0.81411,-0.81411 m -1.62824,4.07051 c 0,-0.44776 0.36639,-0.81409 0.81413,-0.81409 h 2.44231 v 1.62818 h -2.44231 c -0.44774,0 -0.81413,-0.36633 -0.81413,-0.81409 z"
id="path1"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#777777;fill-opacity:1;stroke-width:0.41206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="73.846642"
height="39.999989"
viewBox="0 0 19.53859 10.58333"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="notification_selected.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="1"
inkscape:cx="11.5"
inkscape:cy="6.5"
inkscape:window-width="1916"
inkscape:window-height="1161"
inkscape:window-x="0"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-101.86452,-146.84375)">
<title
id="title1">email-fast</title>
<title
id="title1-8">email-fast-outline</title>
<path
d="m 119.77487,146.84375 h -10.58332 c -0.8955,0 -1.62821,0.73267 -1.62821,1.62817 v 7.32695 c 0,0.90365 0.73271,1.62821 1.62821,1.62821 h 10.58332 c 0.90368,0 1.62824,-0.72456 1.62824,-1.62821 v -7.32695 c 0,-0.8955 -0.72456,-1.62817 -1.62824,-1.62817 m 0,8.95512 h -10.58332 v -5.96736 l 5.29166,2.71096 5.29166,-2.71096 v 5.96736 m -5.29166,-4.63223 -5.29166,-2.69472 h 10.58332 l -5.29166,2.69472 m -8.54807,4.63223 c 0,0.13845 0.0246,0.26859 0.0406,0.40706 h -3.29713 c -0.44939,0 -0.81409,-0.36635 -0.81409,-0.81411 0,-0.44776 0.3647,-0.81409 0.81409,-0.81409 h 3.25643 v 1.22114 m -1.6282,-7.73397 h 1.6689 c -0.0154,0.13844 -0.0406,0.26857 -0.0406,0.40702 v 1.22118 h -1.6282 c -0.44776,0 -0.81411,-0.36633 -0.81411,-0.81409 0,-0.44776 0.36635,-0.81411 0.81411,-0.81411 m -1.62824,4.07051 c 0,-0.44776 0.36639,-0.81409 0.81413,-0.81409 h 2.44231 v 1.62818 h -2.44231 c -0.44774,0 -0.81413,-0.36633 -0.81413,-0.81409 z"
id="path1"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#fb4934;fill-opacity:1;stroke-width:0.41206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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);

View File

@ -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];
}

View File

@ -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;
}
}

View File

@ -52,7 +52,7 @@
.name {
color: @text2;
grid-column: 1 / -1;
grid-column: ~"1 / -1";
font-weight: bold !important;
line-height: 24px;
}

View File

@ -28,6 +28,13 @@
</a>
</div>
<div class="entry {{ if eq .MENU "notifications" }}selected{{ end }}">
<a href="/notifications">
<img src="/images/{{ .VERSION }}/{{ .CONFIG.THEME }}/notifications{{ if eq .MENU "notifications" }}_selected{{ end }}.svg" style="width: 36px">
<div class="label">Notifications</div>
</a>
</div>
<div class="entry {{ if eq .MENU "configuration" }}selected{{ end }}">
<a href="/configuration">
<img src="/images/{{ .VERSION }}/{{ .CONFIG.THEME }}/configuration{{ if eq .MENU "configuration" }}_selected{{ end }}.svg">

View File

@ -0,0 +1,98 @@
{{ define "page" }}
{{ block "page_label" . }}{{end}}
{{ $version := .VERSION }}
{{ $theme := .CONFIG.THEME }}
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/notifications.css">
<script type="text/javascript">
function dialogClick(evt) {
if (evt.target.tagName.toUpperCase() == 'DIALOG') {
evt.target.close()
}
}
function preset(hours) {
const inputPreset = document.querySelector('input[name="preset"]')
inputPreset.value = hours
inputPreset.form.submit()
}
function offsetTime(seconds) {
const el = document.querySelector('input[name="offset-time"]')
el.value = seconds
el.form.submit()
}
</script>
<form action="/notifications" method="get">
<input type="hidden" name="preset" value="">
<input type="hidden" name="offset-time" value="">
<div id="time-select">
<div>From</div>
<div>To</div>
<input name="f" value="{{ .Data.TimeFrom }}" type="datetime-local">
<input name="t" value="{{ .Data.TimeTo }}" type="datetime-local">
<div id="time-offsets">
<div class="header-1">Presets</div>
<div class="header-2">Offsets</div>
<div class="preset"><a href="#" onclick="preset(1)">Last hour</a></div>
<div><a href="#" onclick="offsetTime(-3600)">◀</a></div>
<div>Hour</div>
<div><a href="#" onclick="offsetTime(3600)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24)">Last 24 hours</a></div>
<div><a href="#" onclick="offsetTime(-86400)">◀</a></div>
<div>Day</div>
<div><a href="#" onclick="offsetTime(86400)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24 * 7)">Last 7 days</a></div>
<div><a href="#" onclick="offsetTime(-7 * 86400)">◀</a></div>
<div>Week</div>
<div><a href="#" onclick="offsetTime(7 * 86400)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24 * 7 * 31)">Last 31 days</a></div>
<div><a href="#" onclick="offsetTime(-31 * 86400)">◀</a></div>
<div>Month</div>
<div><a href="#" onclick="offsetTime(31 * 86400)">▶</a></div>
</div>
<button>OK</button>
</div>
</form>
<div id="notifications">
<div class="header">Sent</div>
<div class="header">OK</div>
<div class="header">Trigger name</div>
<div class="header">Service</div>
<div class="header">Error</div>
{{ range .Data.Notifications }}
<div>{{ format_time .Sent }}</div>
<div>{{ if .OK }}<span class="ok">✔</span>{{ else }}<span class="error">✗</span>{{ end }}</div>
<div>{{ .TriggerName }}</div>
<div>{{ .Prio }}:{{ .Service }}</div>
<div>
{{ if .Error.Valid }}
<img src="/images/{{ $version }}/{{ $theme }}/info-filled.svg" onclick="document.getElementById('error-{{ .ID }}').showModal()">
<dialog id="error-{{ .ID }}" onclick="dialogClick(event)">
<div style="padding: 16px 32px">
<pre>{{ .ErrorIndented }}</pre>
<div style="text-align: center">
<button onclick="document.getElementById('error-{{ .ID }}').close()">Close</button>
</div>
</div>
</dialog>
{{ else }}
<img src="/images/{{ $version }}/{{ $theme }}/info-outline.svg">
{{ end }}
</div>
{{ end }}
</div>
{{ end }}