Compare commits

..

No commits in common. "c7ad2aa1b644ba53ee782dd5d2fccd4b8cb53cfc" and "ff402b8fcb1a9a757f4362c2297075142cb75edb" have entirely different histories.

21 changed files with 24 additions and 538 deletions

113
main.go
View File

@ -25,7 +25,6 @@ import (
"slices" "slices"
"sort" "sort"
"strconv" "strconv"
"strings"
"time" "time"
) )
@ -153,8 +152,6 @@ func main() { // {{{
service.Register("/configuration", false, false, pageConfiguration) service.Register("/configuration", false, false, pageConfiguration)
service.Register("/configuration/theme", false, false, actionConfigurationTheme) service.Register("/configuration/theme", false, false, actionConfigurationTheme)
service.Register("/configuration/timezone", false, false, actionConfigurationTimezone) service.Register("/configuration/timezone", false, false, actionConfigurationTimezone)
service.Register("/configuration/notification", false, false, pageConfigurationNotification)
service.Register("/configuration/notification/update/{prio}", false, false, actionConfigurationNotificationUpdate)
service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint) service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint)
go nodataLoop() go nodataLoop()
@ -382,9 +379,9 @@ func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
filenames = append(filenames, componentFilenames...) filenames = append(filenames, componentFilenames...)
logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames) logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
if flagDev { if flagDev {
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...) tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
} else { } else {
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...) tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...)
} }
if err != nil { if err != nil {
err = werr.Wrap(err).Log() err = werr.Wrap(err).Log()
@ -1039,9 +1036,7 @@ func pageConfiguration(w http.ResponseWriter, r *http.Request, _ *session.T) { /
PAGE: "configuration", PAGE: "configuration",
CONFIG: smonConfig.Settings, CONFIG: smonConfig.Settings,
Data: map[string]any{ Data: map[string]any{
"Areas": areas, "Areas": areas,
"NotificationServices": notificationManager.Services(),
"AvailableServices": notification.AvailableServices(),
}, },
} }
@ -1076,105 +1071,3 @@ func actionConfigurationTimezone(w http.ResponseWriter, r *http.Request, _ *sess
w.Header().Add("Location", "/configuration") w.Header().Add("Location", "/configuration")
w.WriteHeader(302) w.WriteHeader(302)
} // }}} } // }}}
func pageConfigurationNotification(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
// This function is either receiving a type when creating a new service,
// or a prio when editing an existing.
notificationType := r.URL.Query().Get("type")
prioStr := r.URL.Query().Get("prio")
var service notification.Service
if notificationType != "" {
// Create a new instance of the selected notification type.
service = notification.GetInstance(notificationType)
} else {
// Find the existing service for editing.
prio, err := strconv.Atoi(prioStr)
if err != nil {
httpError(w, werr.Wrap(err).Log())
return
}
service = *notificationManager.GetService(prio)
}
if service == nil {
data := struct {
typ string
prio string
}{notificationType, prioStr}
err := fmt.Errorf("Invalid service")
pageError(w, "/configuration", werr.Wrap(err).WithData(data).Log())
return
}
// Make it easier for the user by initiating the prio field
// for new notifications to the highest prio + 1.
if notificationType != "" {
prio := 0
for _, svc := range notificationManager.Services() {
prio = max(prio, svc.GetPrio()+1)
}
service.SetPrio(prio)
}
page := Page{
LAYOUT: "notification",
PAGE: "notification/" + strings.ToLower(service.GetType()),
CONFIG: smonConfig.Settings,
Data: map[string]any{
"Service": service,
},
}
page.Render(w, r)
} // }}}
func actionConfigurationNotificationUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
prioStr := r.PathValue("prio")
prio, err := strconv.Atoi(prioStr)
if err != nil {
pageError(w, "/configuration", werr.Wrap(err).Log())
return
}
// prio -1 means a new service, not existing in database.
var svc *notification.Service
if prio == -1 {
emptyService := notification.GetInstance(r.PostFormValue("type"))
svc = &emptyService
} else {
svc = notificationManager.GetService(prio)
if svc == nil {
pageError(w, "/configuration", werr.New("Service with prio %d not found", prio).Log())
return
}
}
// The service is given all data to give it a chance to
// validate and throwing errors.
err = r.ParseForm()
if err != nil {
pageError(w, "/configuration", werr.Wrap(err).Log())
return
}
err = (*svc).Update(r.PostForm)
if err != nil {
pageError(w, "/configuration", werr.Wrap(err).Log())
return
}
var created bool
created, err = UpdateNotificationService(*svc)
if err != nil {
pageError(w, "/configuration", werr.Wrap(err).Log())
return
}
(*svc).Commit()
if created {
notificationManager.AddService(*svc)
}
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
} // }}}

View File

@ -6,12 +6,6 @@ import (
// Standard // Standard
"log/slog" "log/slog"
"slices"
"strings"
)
var (
allServices []Service
) )
func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *slog.Logger) (Service, error) { func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *slog.Logger) (Service, error) {
@ -36,25 +30,3 @@ func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *sl
return nil, werr.New("Unknown notification service, '%s'", t).WithCode("002-0000") return nil, werr.New("Unknown notification service, '%s'", t).WithCode("002-0000")
} }
func GetInstance(typ string) Service {
for _, svc := range allServices {
if strings.ToLower(svc.GetType()) == strings.ToLower(typ) {
return svc
}
}
return nil
}
func AvailableServices() []Service {
slices.SortFunc(allServices, func(a, b Service) int {
if a.GetType() < b.GetType() {
return -1
}
if a.GetType() > b.GetType() {
return 1
}
return 0
})
return allServices
}

View File

@ -11,9 +11,6 @@ import (
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"net/url"
"strconv"
"strings"
) )
type NTFY struct { type NTFY struct {
@ -21,13 +18,6 @@ type NTFY struct {
Prio int Prio int
AcknowledgeURL string AcknowledgeURL string
logger *slog.Logger logger *slog.Logger
exists bool
updated Service
}
func init() {
allServices = append(allServices, &NTFY{})
} }
func NewNTFY(config []byte, prio int, ackURL string) (instance *NTFY, err error) { func NewNTFY(config []byte, prio int, ackURL string) (instance *NTFY, err error) {
@ -54,22 +44,6 @@ func (ntfy *NTFY) GetPrio() int {
return ntfy.Prio return ntfy.Prio
} }
func (ntfy *NTFY) SetPrio(prio int) {
ntfy.Prio = prio
}
func (ntfy *NTFY) SetExists(exists bool) {
ntfy.exists = exists
}
func (ntfy NTFY) Exists() bool {
return ntfy.exists
}
func (ntfy *NTFY) String() string {
return ntfy.URL
}
func (ntfy NTFY) Send(problemID int, msg []byte) (err error) { func (ntfy NTFY) Send(problemID int, msg []byte) (err error) {
var req *http.Request var req *http.Request
var res *http.Response var res *http.Response
@ -107,40 +81,3 @@ func (ntfy NTFY) Send(problemID int, msg []byte) (err error) {
return return
} }
func (ntfy *NTFY) Update(values url.Values) (err error) {
updated := NTFY{}
ntfy.updated = &updated
updated.Prio, err = strconv.Atoi(values.Get("prio"))
if err != nil {
return werr.Wrap(err)
}
givenURL := values.Get("url")
if strings.TrimSpace(givenURL) == "" {
return werr.New("URL cannot be empty")
}
updated.URL = strings.TrimSpace(givenURL)
return
}
func (ntfy *NTFY) Updated() Service {
return ntfy.updated
}
func (ntfy *NTFY) Commit() {
updatedNTFY := ntfy.updated.(*NTFY)
ntfy.Prio = updatedNTFY.Prio
ntfy.URL = updatedNTFY.URL
}
func (ntfy NTFY) JSON() []byte {
data := struct {
URL string `json:"url"`
}{
ntfy.URL,
}
j, _ := json.Marshal(data)
return j
}

View File

@ -6,7 +6,6 @@ import (
// Standard // Standard
"log/slog" "log/slog"
"net/url"
"slices" "slices"
) )
@ -14,15 +13,7 @@ type Service interface {
SetLogger(*slog.Logger) SetLogger(*slog.Logger)
GetPrio() int GetPrio() int
GetType() string GetType() string
SetPrio(int)
SetExists(bool) // Exists in database
Exists() bool // Exists in database
String() string
Send(int, []byte) error Send(int, []byte) error
Update(url.Values) error
Updated() Service
Commit()
JSON() []byte
} }
type Manager struct { type Manager struct {
@ -37,7 +28,6 @@ func NewManager(logger *slog.Logger) (nm Manager) {
} }
func (nm *Manager) AddService(service Service) { func (nm *Manager) AddService(service Service) {
service.SetExists(true)
nm.services = append(nm.services, service) nm.services = append(nm.services, service)
slices.SortFunc(nm.services, func(a, b Service) int { slices.SortFunc(nm.services, func(a, b Service) int {
if a.GetPrio() < b.GetPrio() { if a.GetPrio() < b.GetPrio() {
@ -50,16 +40,6 @@ func (nm *Manager) AddService(service Service) {
}) })
} }
func (nm *Manager) GetService(prio int) *Service {
for _, svc := range nm.services {
if svc.GetPrio() == prio {
return &svc
}
}
return nil
}
func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (err error) { func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (err error) {
for i, service := range nm.services { for i, service := range nm.services {
nm.logger.Info("notification", "service", service.GetType(), "prio", service.GetPrio()) nm.logger.Info("notification", "service", service.GetType(), "prio", service.GetPrio())
@ -69,7 +49,7 @@ func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (er
} else { } else {
data := struct { data := struct {
ProblemID int ProblemID int
Msg []byte Msg []byte
}{ }{
problemID, problemID,
msg, msg,
@ -81,7 +61,3 @@ func (nm *Manager) Send(problemID int, msg []byte, fn func(*Service, error)) (er
return return
} }
func (nm *Manager) Services() (services []Service) {
return nm.services
}

View File

@ -7,7 +7,6 @@ import (
// Standard // Standard
"encoding/json" "encoding/json"
"log/slog" "log/slog"
"net/url"
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
@ -18,13 +17,6 @@ type Script struct {
Prio int Prio int
AcknowledgeURL string AcknowledgeURL string
logger *slog.Logger logger *slog.Logger
exists bool
updated Service
}
func init() {
allServices = append(allServices, &Script{})
} }
func NewScript(config []byte, prio int, ackURL string) (instance *Script, err error) { func NewScript(config []byte, prio int, ackURL string) (instance *Script, err error) {
@ -51,22 +43,6 @@ func (script *Script) GetPrio() int {
return script.Prio return script.Prio
} }
func (script *Script) SetPrio(prio int) {
script.Prio = prio
}
func (script *Script) SetExists(exists bool) {
script.exists = exists
}
func (script Script) Exists() bool {
return script.exists
}
func (script *Script) String() string {
return script.Filename
}
func (script Script) Send(problemID int, msg []byte) (err error) { func (script Script) Send(problemID int, msg []byte) (err error) {
var errbuf strings.Builder var errbuf strings.Builder
cmd := exec.Command(script.Filename, strconv.Itoa(problemID), script.AcknowledgeURL, string(msg)) cmd := exec.Command(script.Filename, strconv.Itoa(problemID), script.AcknowledgeURL, string(msg))
@ -92,40 +68,3 @@ func (script Script) Send(problemID int, msg []byte) (err error) {
return return
} }
func (script *Script) Update(values url.Values) (err error) {
updated := Script{}
updated.Prio, err = strconv.Atoi(values.Get("prio"))
if err != nil {
return werr.Wrap(err)
}
givenFilename := values.Get("filename")
if strings.TrimSpace(givenFilename) == "" {
return werr.New("Filename cannot be empty")
}
updated.Filename = strings.TrimSpace(givenFilename)
script.updated = &updated
return
}
func (script *Script) Updated() Service {
return script.updated
}
func (script *Script) Commit() {
updated := script.updated.(*Script)
script.Prio = updated.Prio
script.Filename = updated.Filename
}
func (script Script) JSON() []byte {
data := struct {
Filename string `json:"filename"`
}{
script.Filename,
}
j, _ := json.Marshal(data)
return j
}

View File

@ -3,7 +3,6 @@ package main
import ( import (
// External // External
werr "git.gibonuddevalla.se/go/wrappederror" werr "git.gibonuddevalla.se/go/wrappederror"
"github.com/lib/pq"
// Internal // Internal
"smon/notification" "smon/notification"
@ -68,61 +67,13 @@ func InitNotificationManager() (nm notification.Manager, err error) { // {{{
return return
} // }}} } // }}}
func UpdateNotificationService(svc notification.Service) (created bool, err error) { // {{{
if svc.Exists() {
_, err = service.Db.Conn.Exec(
`
UPDATE public.notification
SET
prio=$2,
configuration=$3
WHERE
prio=$1
`,
svc.GetPrio(),
svc.Updated().GetPrio(),
svc.Updated().JSON(),
)
} else {
_, err = service.Db.Conn.Exec(
`
INSERT INTO public.notification(prio, configuration, service)
VALUES($1, $2, $3)
`,
svc.Updated().GetPrio(),
svc.Updated().JSON(),
svc.GetType(),
)
created = true
}
if err != nil {
// Check if this is just a duplicated prio, which isn't allowed.
pgErr, isPgErr := err.(*pq.Error)
if isPgErr && pgErr.Code == "23505" {
return false, werr.New("Prio %d is already used by another service", svc.GetPrio())
}
return false, werr.Wrap(err).WithData(
struct {
Prio int
Configuration []byte
}{
svc.GetPrio(),
svc.JSON(),
},
)
}
return
} // }}}
func AcknowledgeNotification(uuid string) (err error) { // {{{ func AcknowledgeNotification(uuid string) (err error) { // {{{
/* /*
_, err = service.Db.Conn.Exec(`UPDATE schedule SET acknowledged=true WHERE schedule_uuid=$1`, uuid) _, err = service.Db.Conn.Exec(`UPDATE schedule SET acknowledged=true WHERE schedule_uuid=$1`, uuid)
if err != nil { if err != nil {
err = werr.Wrap(err).WithData(uuid) err = werr.Wrap(err).WithData(uuid)
} }
*/ */
return return
} // }}} } // }}}

View File

@ -24,20 +24,3 @@
#areas .area .section.configuration img { #areas .area .section.configuration img {
height: 16px; height: 16px;
} }
#services {
display: grid;
grid-template-columns: repeat(3, min-content);
gap: 10px 24px;
background-color: #2979b8;
width: min-content;
padding: 16px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
margin-top: 16px;
}
#services .header {
font-weight: bold;
}
#services div {
white-space: nowrap;
}

View File

@ -43,8 +43,7 @@ button:focus {
#datapoints, #datapoints,
#problems-list, #problems-list,
#acknowledged-list, #acknowledged-list,
#values, #values {
#services {
background-color: #fff !important; background-color: #fff !important;
border: 1px solid #ddd; border: 1px solid #ddd;
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25); box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25);

View File

@ -4,9 +4,3 @@ body {
#areas .area { #areas .area {
box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5); box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5);
} }
#page-error {
border: unset;
color: #fff;
background-color: #a00;
text-align: center;
}

View File

@ -24,20 +24,3 @@
#areas .area .section.configuration img { #areas .area .section.configuration img {
height: 16px; height: 16px;
} }
#services {
display: grid;
grid-template-columns: repeat(3, min-content);
gap: 10px 24px;
background-color: #333;
width: min-content;
padding: 16px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
margin-top: 16px;
}
#services .header {
font-weight: bold;
}
#services div {
white-space: nowrap;
}

View File

@ -43,8 +43,7 @@ button:focus {
#datapoints, #datapoints,
#problems-list, #problems-list,
#acknowledged-list, #acknowledged-list,
#values, #values {
#services {
background-color: #fff !important; background-color: #fff !important;
border: 1px solid #ddd; border: 1px solid #ddd;
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25); box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25);

View File

@ -4,9 +4,3 @@ body {
#areas .area { #areas .area {
box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5); box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.5);
} }
#page-error {
border: unset;
color: #fff;
background-color: #a00;
text-align: center;
}

View File

@ -17,7 +17,6 @@ class Graph {
const values = [{ const values = [{
x: this.dataset.xValues(), x: this.dataset.xValues(),
y: this.dataset.yValues(), y: this.dataset.yValues(),
mode: 'scatter',
}] }]
this.layout = { this.layout = {
@ -27,11 +26,7 @@ class Graph {
}, },
} }
this.config = { Plotly.react(this.graphValues, values, this.layout);
displayModeBar: true,
}
Plotly.react(this.graphValues, values, this.layout, this.config);
this.graphValues.on('plotly_relayout', attr => this.relayoutHandler(attr)) this.graphValues.on('plotly_relayout', attr => this.relayoutHandler(attr))
} }
@ -44,7 +39,6 @@ class Graph {
const values = [{ const values = [{
x: this.dataset.xValues(), x: this.dataset.xValues(),
y: this.dataset.yValues(), y: this.dataset.yValues(),
mode: 'scatter',
}] }]
Plotly.react(this.graphValues, values, this.layout) Plotly.react(this.graphValues, values, this.layout)
}) })

View File

@ -36,23 +36,3 @@
} }
} }
} }
#services {
display: grid;
grid-template-columns: repeat(3, min-content);
gap: 10px 24px;
background-color: @bg3;
width: min-content;
padding: 16px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
margin-top: 16px;
.header {
font-weight: bold;
}
div {
white-space: nowrap;
}
}

View File

@ -58,7 +58,7 @@ button {
} }
} }
#datapoints, #problems-list, #acknowledged-list, #values, #services { #datapoints, #problems-list, #acknowledged-list, #values {
background-color: #fff !important; background-color: #fff !important;
border: 1px solid #ddd; border: 1px solid #ddd;
box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.25); box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.25);

View File

@ -9,10 +9,3 @@ body {
box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.5); box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.5);
} }
} }
#page-error {
border: unset;
color: #fff;
background-color: #a00;
text-align: center;
}

View File

@ -6,6 +6,18 @@
{{ template "fonts" }} {{ template "fonts" }}
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/main.css"> <link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/main.css">
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/{{ .CONFIG.THEME }}.css"> <link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/{{ .CONFIG.THEME }}.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.2.1/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@3.3.0/build/global/luxon.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
<script type="importmap">
{
"imports": {
"chart.js": "/js/{{ .VERSION }}/lib/chartjs/index.d.ts"
}
}
</script>
</head> </head>
<body> <body>
<div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}"> <div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}">

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
{{ template "fonts" }}
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/main.css">
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/{{ .CONFIG.THEME }}.css">
</head>
<body>
<div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}">
<div class="close" onclick="console.log(this.parentElement.classList.remove('show'))">✖</div>
{{ .ERROR }}
</div>
<div id="page">
{{ block "page" . }}{{ end }}
</div>
</body>
</html>

View File

@ -62,12 +62,6 @@
return return
location.href = `/section/delete/${id}` location.href = `/section/delete/${id}`
} }
function newNotification() {
const select = document.getElementById('notification-create-type')
const nType = select.value
location.href = `/configuration/notification?type=${nType}`
}
</script> </script>
{{ block "page_label" . }}{{end}} {{ block "page_label" . }}{{end}}
@ -114,24 +108,4 @@
<button style="margin-left: 8px;">Update</button> <button style="margin-left: 8px;">Update</button>
</form> </form>
<h1>Notifications</h1>
<select id="notification-create-type">
{{ range .Data.AvailableServices }}
<option>{{ .GetType }}</option>
{{ end }}
</select>
<button onclick="newNotification()">Create</button>
<div id="services">
<div class="header">Prio</div>
<div class="header">Type</div>
<div class="header">Target</div>
<div class="line"></div>
{{ range .Data.NotificationServices }}
<div><a href="/configuration/notification?prio={{ .GetPrio }}">{{ .GetPrio }}</a></div>
<div><a href="/configuration/notification?prio={{ .GetPrio }}">{{ .GetType }}</a></div>
<div><a href="/configuration/notification?prio={{ .GetPrio }}">{{ .String }}</a></div>
{{ end }}
</div>
{{ end }} {{ end }}

View File

@ -1,34 +0,0 @@
{{ define "page" }}
<h1>NTFY</h1>
<style>
.grid {
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 8px 16px;
align-items: center;
margin-top: 32px;
}
input[type=number] {
width: 64px;
padding: 4px;
}
button {
margin-top: 16px;
}
</style>
<form action="/configuration/notification/update/{{ if .Data.Service.Exists }}{{ .Data.Service.GetPrio }}{{ else }}-1{{ end }}" method="post">
<input type="hidden" name="type" value="NTFY">
<div class="grid">
<div>Prio:</div>
<input type="number" min=0 name="prio" value="{{ .Data.Service.GetPrio }}">
<div>URL:</div>
<input type="text" name="url" value="{{ .Data.Service.URL }}" style="width: 100%">
<button style="grid-column: 1 / -1; width: min-content;">OK</button>
</div>
</form>
{{ end }}

View File

@ -1,34 +0,0 @@
{{ define "page" }}
<h1>Script</h1>
<style>
.grid {
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 8px 16px;
align-items: center;
margin-top: 32px;
}
input[type=number] {
width: 64px;
padding: 4px;
}
button {
margin-top: 16px;
}
</style>
<form action="/configuration/notification/update/{{ .Data.Service.GetPrio }}" method="post">
<input type="hidden" name="type" value="Script">
<div class="grid">
<div>Prio:</div>
<input type="number" min=0 name="prio" value="{{ .Data.Service.GetPrio }}">
<div>Filename:</div>
<input type="text" name="filename" value="{{ .Data.Service.Filename }}" style="width: 100%">
<button style="grid-column: 1 / -1; width: min-content;">OK</button>
</div>
</form>
{{ end }}