Initial commit

This commit is contained in:
Magnus Åhall 2024-04-29 08:36:13 +02:00
commit 89f483171a
43 changed files with 2245 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
smon
smon.exe

62
area.go Normal file
View File

@ -0,0 +1,62 @@
package main
import (
// External
re "git.gibonuddevalla.se/go/wrappederror"
// Standard
"encoding/json"
"sort"
)
type Area struct {
ID int
Name string
Sections []Section
}
func AreaRetrieve() (areas []Area, err error) {// {{{
areas = []Area{}
row := service.Db.Conn.QueryRow(`
SELECT
jsonb_agg(jsonsections)
FROM (
SELECT
a.id,
a.name,
jsonb_agg(
jsonb_build_object(
'id', s.id,
'name', s.name
)
) AS sections
FROM area a
INNER JOIN section s ON s.area_id = a.id
GROUP BY
a.id, a.name
) jsonsections`,
)
var jsonData []byte
err = row.Scan(&jsonData)
if err != nil {
err = re.Wrap(err)
return
}
err = json.Unmarshal(jsonData, &areas)
if err != nil {
err = re.Wrap(err)
return
}
return
}// }}}
func (a Area) SortedSections() []Section {// {{{
sort.SliceStable(a.Sections, func (i, j int) bool {
return a.Sections[i].Name < a.Sections[j].Name
})
return a.Sections
}// }}}

5
config.go Normal file
View File

@ -0,0 +1,5 @@
package main
type SmonConfiguration struct {
LogFile string
}

112
datapoint.go Normal file
View File

@ -0,0 +1,112 @@
package main
import (
// External
we "git.gibonuddevalla.se/go/wrappederror"
// Standard
"database/sql"
"time"
)
type DatapointType string
const (
INT DatapointType = "INT"
STRING = "STRING"
DATETIME = "DATETIME"
)
type Datapoint struct {
ID int
Name string
SectionID int `db:"section_id"`
Datatype DatapointType
LastValue time.Time `db:"last_value"`
DatapointValueJSON []byte `db:"datapoint_value_json"`
LastDatapointValue DatapointValue
}
type DatapointValue struct {
ID int
DatapointID int `db:"datapoint_id"`
Ts time.Time
ValueInt sql.NullInt64 `db:"value_int"`
ValueString sql.NullString `db:"value_string"`
ValueDateTime sql.NullTime `db:"value_datetime"`
}
func (dp DatapointValue) Value() any {
if dp.ValueInt.Valid {
return dp.ValueInt.Int64
}
if dp.ValueString.Valid {
return dp.ValueString.String
}
if dp.ValueDateTime.Valid {
return dp.ValueDateTime.Time
}
return nil
}
func DatapointAdd[T any](name string, value T) (err error) {
row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name)
var dpID int
var dpType DatapointType
err = row.Scan(&dpID, &dpType)
if err != nil {
err = we.Wrap(err).WithData(struct {
Name string
Value any
}{name, value})
return
}
switch dpType {
case INT:
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_int) VALUES($1, $2)`, dpID, value)
case STRING:
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_string) VALUES($1, $2)`, dpID, value)
case DATETIME:
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_datetime) VALUES($1, $2)`, dpID, value)
}
if err != nil {
err = we.Wrap(err).WithData(struct {
ID int
value any
}{dpID, value})
return
}
return
}
func DatapointRetrieve(name string) (dp Datapoint, err error) {
row := service.Db.Conn.QueryRowx(
`SELECT * FROM datapoint dp WHERE dp.name = $1`,
name,
)
err = row.StructScan(&dp)
if err != nil {
err = we.Wrap(err).WithData(name)
return
}
row = service.Db.Conn.QueryRowx(`
SELECT *
FROM datapoint_value
WHERE datapoint_id = $1
ORDER BY ts DESC
LIMIT 1
`,
dp.ID,
)
err = row.StructScan(&dp.LastDatapointValue)
if err != nil {
err = we.Wrap(err).WithData(dp.ID)
return
}
return
}

19
go.mod Normal file
View File

@ -0,0 +1,19 @@
module smon
go 1.22.0
require (
git.gibonuddevalla.se/go/webservice v0.2.12
git.gibonuddevalla.se/go/wrappederror v0.3.4
github.com/expr-lang/expr v1.16.5
)
require (
git.gibonuddevalla.se/go/dbschema v1.3.0 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/lib/pq v1.10.9 // indirect
golang.org/x/net v0.17.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

33
go.sum Normal file
View File

@ -0,0 +1,33 @@
git.gibonuddevalla.se/go/dbschema v1.3.0 h1:HzFMR29tWfy/ibIjltTbIMI4inVktj/rh8bESALibgM=
git.gibonuddevalla.se/go/dbschema v1.3.0/go.mod h1:BNw3q/574nXbGoeWyK+tLhRfggVkw2j2aXZzrBKC3ig=
git.gibonuddevalla.se/go/webservice v0.2.12 h1:IcaIycmF7eO88RmFQkslHaKRWYxXdciVQXUAvJ36b4g=
git.gibonuddevalla.se/go/webservice v0.2.12/go.mod h1:3uBS6nLbK9qbuGzDls8MZD5Xr9ORY1Srbj6v06BIhws=
git.gibonuddevalla.se/go/wrappederror v0.3.4 h1:dcKp9/+QrZSO3S4fVnq7yG2p7DUZVmlztBAb/OzoZNY=
git.gibonuddevalla.se/go/wrappederror v0.3.4/go.mod h1:j4w320Hk1wvhOPjUaK4GgLvmtnjUUM5yVu6JFO1OCSc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

391
main.go Normal file
View File

@ -0,0 +1,391 @@
package main
import (
// External
ws "git.gibonuddevalla.se/go/webservice"
"git.gibonuddevalla.se/go/webservice/session"
we "git.gibonuddevalla.se/go/wrappederror"
// Standard
"embed"
"encoding/json"
"flag"
"fmt"
"html/template"
"io"
"io/fs"
"log/slog"
"net/http"
"os"
"path"
"sort"
"strconv"
)
const VERSION = "v1"
var (
logger *slog.Logger
flagConfigFile string
flagDev bool
service *ws.Service
logFile *os.File
parsedTemplates map[string]*template.Template
componentFilenames []string
//go:embed sql
sqlFS embed.FS
//go:embed views
viewFS embed.FS
//go:embed static
staticFS embed.FS
)
func init() { // {{{
opts := slog.HandlerOptions{}
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opts))
confDir, err := os.UserConfigDir()
if err != nil {
logger.Error("application", "error", we.Wrap(err))
}
cfgPath := path.Join(confDir, "smon.yaml")
flag.StringVar(&flagConfigFile, "config", cfgPath, "Path and filename of the YAML configuration file")
flag.BoolVar(&flagDev, "dev", false, "reload templates on each request")
flag.Parse()
componentFilenames, err = getComponentFilenames(viewFS)
if err != nil {
logger.Error("application", "error", err)
os.Exit(1)
}
parsedTemplates = make(map[string]*template.Template)
} // }}}
func main() { // {{{
var err error
we.Init()
we.SetLogCallback(logHandler)
service, err = ws.New(flagConfigFile, VERSION, logger)
if err != nil {
logger.Error("application", "error", err)
os.Exit(1)
}
smonConf := SmonConfiguration{}
j, _ := json.Marshal(service.Config.Application)
json.Unmarshal(j, &smonConf)
logFile, err = os.OpenFile(smonConf.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
logger.Error("application", "error", err)
return
}
service.SetDatabase(sqlProvider)
service.SetStaticFS(staticFS, "static")
service.SetStaticDirectory("static", flagDev)
service.Register("/", false, false, staticHandler)
service.Register("/problems", false, false, pageProblems)
service.Register("/triggers", false, false, pageTriggers)
service.Register("/trigger/edit/{id}", 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)
err = service.Start()
if err != nil {
logger.Error("webserver", "error", we.Wrap(err))
os.Exit(1)
}
} // }}}
func sqlProvider(dbname string, version int) (sql []byte, found bool) { // {{{
var err error
sql, err = sqlFS.ReadFile(fmt.Sprintf("sql/%05d.sql", version))
if err != nil {
return
}
found = true
return
} // }}}
func logHandler(err we.Error) { // {{{
j, _ := json.Marshal(err)
logFile.Write(j)
logFile.Write([]byte("\n"))
} // }}}
func httpError(w http.ResponseWriter, err error) { // {{{
resp := struct {
OK bool
Error string
}{
false,
err.Error(),
}
j, _ := json.Marshal(resp)
w.Write(j)
} // }}}
func staticHandler(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
if flagDev && !reloadTemplates(w) {
return
}
if r.URL.Path == "/" {
pageIndex(w, r, sess)
return
}
service.StaticHandler(w, r, sess)
} // }}}
func entryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
dpoint := r.PathValue("datapoint")
value, _ := io.ReadAll(r.Body)
err := DatapointAdd(dpoint, value)
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
j, _ := json.Marshal(struct{ OK bool }{true})
w.Write(j)
} // }}}
func reloadTemplates(w http.ResponseWriter) bool { // {{{
parsedTemplates = make(map[string]*template.Template)
return true
} // }}}
func getComponentFilenames(efs fs.FS) (files []string, err error) { // {{{
files = []string{}
if err := fs.WalkDir(efs, "views/components", func(path string, d fs.DirEntry, err error) error {
if d == nil {
return nil
}
if d.IsDir() {
return nil
}
files = append(files, path)
return nil
}); err != nil {
return nil, err
}
return files, nil
} // }}}
func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
layoutFilename := fmt.Sprintf("views/layouts/%s.gotmpl", layout)
pageFilename := fmt.Sprintf("views/pages/%s.gotmpl", page)
if tmpl, found := parsedTemplates[page]; found {
return tmpl, nil
}
filenames := []string{layoutFilename, pageFilename}
filenames = append(filenames, componentFilenames...)
logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
if flagDev {
parsedTemplates[page], err = template.ParseFS(os.DirFS("."), filenames...)
} else {
parsedTemplates[page], err = template.ParseFS(viewFS, filenames...)
}
tmpl = parsedTemplates[page]
if err != nil {
err = we.Wrap(err).Log()
return
}
return
} // }}}
func pageIndex(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page := Page{
LAYOUT: "main",
PAGE: "index",
}
page.Render(w)
} // }}}
func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page := Page{
LAYOUT: "main",
PAGE: "problems",
}
/*
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{
//"Areas": areas,
}
page.Render(w)
return
} // }}}
func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
areas, err := TriggersRetrieve()
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
})
page := Page{
LAYOUT: "main",
PAGE: "triggers",
Data: map[string]any{
"Areas": areas,
},
}
page.Render(w)
} // }}}
func pageTriggerEdit(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
}
var trigger Trigger
trigger, err = TriggerRetrieve(id)
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
datapoints := make(map[string]Datapoint)
for _, dpname := range trigger.Datapoints {
dp, err := DatapointRetrieve(dpname)
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
datapoints[dpname] = dp
}
page := Page{
LAYOUT: "main",
PAGE: "trigger_edit",
MENU: "triggers",
Label: "Trigger",
Icon: "triggers",
Data: map[string]any{
"Trigger": trigger,
"Datapoints": datapoints,
},
}
page.Render(w)
} // }}}
func pageTriggerUpdate(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
}
var trigger Trigger
trigger, err = TriggerRetrieve(id)
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
trigger.Name = r.FormValue("name")
trigger.Expression = r.FormValue("expression")
err = trigger.Update()
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
w.Header().Add("Location", fmt.Sprintf("/trigger/edit/%d", id))
w.WriteHeader(302)
} // }}}
func pageTriggerRun(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
}
var trigger Trigger
trigger, err = TriggerRetrieve(id)
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
expr, _ := io.ReadAll(r.Body)
trigger.Expression = string(expr)
datapoints := make(map[string]Datapoint)
for _, dpname := range trigger.Datapoints {
dp, err := DatapointRetrieve(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(datapoints)
if err != nil {
we.Wrap(err).Log()
httpError(w, err)
return
}
j, _ := json.Marshal(resp)
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 {
httpError(w, we.Wrap(err).Log())
return
}
sort.SliceStable(areas, func(i, j int) bool {
return areas[i].Name < areas[j].Name
})
page := Page{
LAYOUT: "main",
PAGE: "configuration",
Data: map[string]any{
"Areas": areas,
},
}
page.Render(w)
} // }}}

59
page.go Normal file
View File

@ -0,0 +1,59 @@
package main
import (
// External
we "git.gibonuddevalla.se/go/wrappederror"
// Standard
"fmt"
"net/http"
"unicode"
"unicode/utf8"
)
type Page struct {
LAYOUT string
PAGE string
MENU string
Label string
Icon string
Data any
}
func (p *Page) Render(w http.ResponseWriter) {
tmpl, err := getPage(p.LAYOUT, p.PAGE)
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
if p.Icon == "" {
p.Icon = p.PAGE
}
if p.Label == "" {
r, _ := utf8.DecodeRuneInString(p.PAGE)
titled := unicode.ToTitle(r)
p.Label = fmt.Sprintf("%s%s", string(titled), p.PAGE[1:len(p.PAGE)])
}
if p.MENU == "" {
p.MENU = p.PAGE
}
data := map[string]any{
"VERSION": VERSION,
"LAYOUT": p.LAYOUT,
"PAGE": p.PAGE,
"MENU": p.MENU,
"Label": p.Label,
"Icon": p.Icon,
"Data": p.Data,
}
err = tmpl.Execute(w, data)
if err != nil {
httpError(w, we.Wrap(err).Log())
}
}

20
problem.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
// External
// we "git.gibonuddevalla.se/go/wrappederror"
// Standard
// "encoding/json"
)
/*
type Problem struct {
ID int
Name string
SectionID int
Expression string
DatapointNames []string
}
*/

23
section.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
// Standard
"sort"
)
type Section struct {
ID int
Name string
AreaID int `db:"area_id"`
Area Area
Triggers []Trigger
}
func (s *Section) SortedTriggers() []Trigger {
sort.SliceStable(s.Triggers, func(i, j int) bool {
return s.Triggers[i].Name < s.Triggers[j].Name
})
return s.Triggers
}

10
sql/00001.sql Normal file
View File

@ -0,0 +1,10 @@
CREATE TYPE datapoint_type AS ENUM('INT', 'STRING', 'DATETIME');
CREATE TABLE public.datapoint (
"id" serial NOT NULL,
"name" varchar NOT NULL,
"datatype" datapoint_type NOT NULL,
"last_value" timestamptz DEFAULT '1970-01-01 00:00:00+00' NOT NULL,
CONSTRAINT datapoint_pk PRIMARY KEY (id),
CONSTRAINT datapoint_unique UNIQUE ("name")
);

10
sql/00002.sql Normal file
View File

@ -0,0 +1,10 @@
CREATE TABLE public.datapoint_value (
id serial8 NOT NULL,
datapoint_id int4 NOT NULL,
ts timestamptz DEFAULT NOW() NOT NULL,
value_int int8 NULL,
value_string varchar NULL,
value_datetime timestamptz NULL,
CONSTRAINT datapoint_value_pk PRIMARY KEY (id),
CONSTRAINT datapoint_value_datapoint_fk FOREIGN KEY (datapoint_id) REFERENCES public.datapoint(id) ON DELETE CASCADE ON UPDATE CASCADE
);

13
sql/00003.sql Normal file
View File

@ -0,0 +1,13 @@
CREATE TABLE public.area (
id serial NOT NULL,
name varchar NOT NULL,
CONSTRAINT area_pk PRIMARY KEY (id)
);
CREATE TABLE public.section (
id serial NOT NULL,
name varchar NOT NULL,
area_id int4 NOT NULL,
CONSTRAINT section_pk PRIMARY KEY (id),
CONSTRAINT section_area_fk FOREIGN KEY (area_id) REFERENCES public.area(id) ON DELETE RESTRICT ON UPDATE RESTRICT
);

2
sql/00004.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE public.datapoint ADD section_id int4 NULL;
ALTER TABLE public.datapoint ADD CONSTRAINT datapoint_section_fk FOREIGN KEY (section_id) REFERENCES public."section"(id) ON DELETE SET NULL ON UPDATE SET NULL;

11
sql/00005.sql Normal file
View File

@ -0,0 +1,11 @@
CREATE TABLE public.trigger (
id serial NOT NULL,
name varchar NOT NULL,
section_id int4 NOT NULL,
expression varchar NOT NULL,
datapoints jsonb NOT NULL DEFAULT '[]',
CONSTRAINT trigger_pk PRIMARY KEY (id),
CONSTRAINT trigger_section_fk FOREIGN KEY (section_id) REFERENCES public.section(id) ON DELETE RESTRICT ON UPDATE RESTRICT
);

143
static/css/main.css Normal file
View File

@ -0,0 +1,143 @@
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;
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;
}
.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 {
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 #3a3a3a;
font-size: 1em;
height: 3em;
}
#layout {
display: grid;
grid-template-areas: "menu content";
grid-template-columns: 64px 1fr;
grid-template-rows: 100% 100%;
height: 100vh;
}
#menu {
display: flex;
flex-flow: column wrap;
justify-content: flex-start;
gap: 24px;
grid-area: menu;
background: #202020;
padding: 16px;
}
#menu img {
display: block;
width: 32px;
}
#page {
grid-area: content;
padding: 32px;
}
#page .page-label {
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 12px;
align-items: center;
margin-bottom: 32px;
}
#page .page-label div {
font-size: 1.5em;
color: #fb4934;
}
#page .page-label img {
display: block;
}
#areas {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
#areas .area {
background: #333;
border-radius: 4px;
}
#areas .area > .name {
background: #fb4934;
color: #fff;
font-weight: bold;
padding: 4px 16px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
#areas .area .section {
margin: 8px 16px;
}
#areas .area .section > .name {
font-weight: bold;
}
#areas .area .section .triggers a {
color: inherit;
text-decoration: none;
}
#areas .area .section .triggers .trigger {
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 8px;
align-items: center;
margin-top: 8px;
}
#areas .area .section .triggers .trigger img {
height: 16px;
}
#areas .area .section .triggers .trigger .label {
color: #f7edd7;
}

64
static/css/theme.css Normal file
View File

@ -0,0 +1,64 @@
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;
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;
}
.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 {
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 #3a3a3a;
font-size: 1em;
height: 3em;
}

101
static/css/trigger_edit.css Normal file
View File

@ -0,0 +1,101 @@
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;
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;
}
.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 {
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 #3a3a3a;
font-size: 1em;
height: 3em;
}
.widgets {
display: grid;
grid-template-columns: min-content 1fr;
gap: 8px 16px;
}
.widgets .label {
margin-top: 4px;
}
.widgets input[type="text"],
.widgets textarea {
width: 100%;
}
.widgets .datapoints {
font: "Roboto Mono", monospace;
display: grid;
grid-template-columns: min-content 1fr;
gap: 6px 8px;
margin-bottom: 8px;
}
.widgets .action {
display: grid;
grid-template-columns: min-content min-content 1fr;
grid-gap: 8px;
}
.widgets .action #run-result {
font-family: 'Roboto Mono', monospace;
margin-left: 16px;
padding: 16px;
background: #202020;
min-height: 8em;
}
.widgets .action #run-result.ok {
color: #d5c4a1;
}
.widgets .action #run-result.error {
color: #fb4934;
}

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32.000023"
height="32.890053"
viewBox="0 0 8.4666725 8.70216"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="settings.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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="10.5"
inkscape:cy="10"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.101544,-148.43131)">
<title
id="title1">cog</title>
<title
id="title1-9">cog-outline</title>
<path
d="m 94.33488,151.04195 a 1.7404377,1.7404377 0 0 1 1.740437,1.74043 1.7404377,1.7404377 0 0 1 -1.740437,1.74043 1.7404377,1.7404377 0 0 1 -1.740438,-1.74043 1.7404377,1.7404377 0 0 1 1.740438,-1.74043 m 0,0.8702 a 0.87021889,0.87021889 0 0 0 -0.87022,0.87023 0.87021889,0.87021889 0 0 0 0.87022,0.87022 0.87021889,0.87021889 0 0 0 0.870219,-0.87022 0.87021889,0.87021889 0 0 0 -0.870219,-0.87023 m -0.87022,5.22132 c -0.108784,0 -0.20015,-0.0783 -0.217553,-0.18274 l -0.160998,-1.15305 c -0.274119,-0.1087 -0.509079,-0.2567 -0.735335,-0.43076 l -1.083423,0.43946 c -0.09573,0.0349 -0.213203,0 -0.265416,-0.0957 l -0.870219,-1.50548 c -0.05657,-0.0957 -0.03046,-0.21321 0.05221,-0.27848 l 0.918081,-0.72227 -0.03046,-0.42206 0.03046,-0.43511 -0.918081,-0.70922 c -0.08267,-0.0653 -0.108784,-0.18276 -0.05221,-0.27848 l 0.870219,-1.50548 c 0.05221,-0.0957 0.169692,-0.13485 0.265416,-0.0957 l 1.083423,0.4351 c 0.226256,-0.1697 0.461216,-0.31764 0.735335,-0.42641 l 0.160998,-1.15304 c 0.0174,-0.10443 0.108767,-0.18274 0.217553,-0.18274 h 1.740439 c 0.108768,0 0.20015,0.0783 0.217554,0.18274 l 0.160997,1.15304 c 0.27412,0.10871 0.509079,0.25671 0.735335,0.42641 l 1.083422,-0.4351 c 0.09573,-0.0391 0.213204,0 0.265417,0.0957 l 0.87022,1.50548 c 0.05657,0.0957 0.03046,0.21321 -0.05221,0.27848 l -0.918081,0.70922 0.03046,0.43511 -0.03046,0.4351 0.918081,0.70923 c 0.08267,0.0653 0.108785,0.18275 0.05221,0.27848 l -0.87022,1.50548 c -0.05221,0.0957 -0.169692,0.13485 -0.265417,0.0957 l -1.083422,-0.4351 c -0.226256,0.1697 -0.461215,0.31764 -0.735335,0.4264 l -0.160997,1.15305 c -0.0174,0.10443 -0.108784,0.18274 -0.217554,0.18274 H 93.46466 m 0.543887,-7.83197 -0.160998,1.13565 c -0.522131,0.1087 -0.983348,0.38723 -1.318382,0.77448 l -1.048614,-0.45251 -0.326331,0.56565 0.918081,0.67441 c -0.174045,0.50908 -0.174045,1.06166 0,1.5664 l -0.922433,0.67877 0.326332,0.56564 1.057316,-0.45252 c 0.335034,0.38289 0.7919,0.66137 1.30968,0.7658 l 0.160997,1.13999 h 0.661366 l 0.160997,-1.13565 c 0.51778,-0.1087 0.974646,-0.38725 1.30968,-0.77014 l 1.057315,0.45252 0.326333,-0.56564 -0.922433,-0.67442 c 0.174045,-0.50909 0.174045,-1.06167 0,-1.57075 l 0.918081,-0.67441 -0.326331,-0.56565 -1.048614,0.45251 c -0.335034,-0.38725 -0.796251,-0.66572 -1.318382,-0.77014 l -0.160998,-1.13999 z"
id="path1-1"
style="fill:#777777;fill-opacity:1;stroke-width:0.435109" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32.000031"
height="32.897346"
viewBox="0 0 8.4666745 8.7040898"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="settings_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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="9.5"
inkscape:cy="10"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.294521,-148.43129)">
<title
id="title1">cog</title>
<path
d="m 94.528826,154.30658 a 1.5232321,1.5232321 0 0 1 -1.523231,-1.52325 1.5232321,1.5232321 0 0 1 1.523231,-1.52323 1.5232321,1.5232321 0 0 1 1.523232,1.52323 1.5232321,1.5232321 0 0 1 -1.523232,1.52325 m 3.233603,-1.10109 c 0.0174,-0.13932 0.03046,-0.27853 0.03046,-0.42216 0,-0.1436 -0.013,-0.28723 -0.03046,-0.4352 l 0.918292,-0.70939 c 0.08269,-0.0653 0.10445,-0.1828 0.05222,-0.27853 l -0.870417,-1.50583 c -0.05222,-0.0957 -0.169732,-0.13488 -0.265478,-0.0957 l -1.083672,0.4352 c -0.226308,-0.16972 -0.461321,-0.31769 -0.735503,-0.4265 l -0.161018,-1.15329 c -0.0174,-0.10445 -0.108809,-0.1828 -0.217605,-0.1828 h -1.740836 c -0.108809,0 -0.200198,0.0783 -0.217605,0.1828 l -0.161035,1.15329 c -0.274181,0.10889 -0.509194,0.25678 -0.735503,0.4265 l -1.08367,-0.4352 c -0.09575,-0.0391 -0.213253,0 -0.265478,0.0957 l -0.870419,1.50583 c -0.05658,0.0957 -0.03046,0.21324 0.05222,0.27853 l 0.918292,0.70939 c -0.0174,0.14804 -0.03046,0.29158 -0.03046,0.4352 0,0.1436 0.013,0.28289 0.03046,0.42216 l -0.918292,0.72245 c -0.08269,0.0653 -0.108809,0.18278 -0.05222,0.27853 l 0.870419,1.50582 c 0.05222,0.0957 0.16973,0.13061 0.265478,0.0957 l 1.08367,-0.43957 c 0.226309,0.17408 0.461322,0.32206 0.735503,0.43087 l 0.161035,1.15329 c 0.0174,0.10445 0.108809,0.1828 0.217605,0.1828 h 1.740836 c 0.108809,0 0.200195,-0.0783 0.217605,-0.1828 l 0.161018,-1.15329 c 0.274182,-0.11317 0.509195,-0.25679 0.735503,-0.43087 l 1.083672,0.43957 c 0.09575,0.0349 0.213253,0 0.265478,-0.0957 l 0.870417,-1.50582 c 0.05223,-0.0957 0.03046,-0.21325 -0.05222,-0.27853 z"
id="path1"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#fb4934;fill-opacity:1;stroke-width:0.710544;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: 3.6 KiB

73
static/images/logo.svg Normal file
View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="31.999998"
height="28.799999"
viewBox="0 0 8.466666 7.62"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="logo.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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="9.03125"
inkscape:cy="17.3125"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.275834,-148.48417)">
<title
id="title1">alert-octagram</title>
<title
id="title1-7">alert-octagram-outline</title>
<title
id="title1-3">database-alert</title>
<title
id="title1-6">database-alert-outline</title>
<path
d="m 93.6625,148.48417 c -1.871133,0 -3.386666,0.75776 -3.386666,1.69332 v 4.23335 c 0,0.93557 1.519766,1.69333 3.386666,1.69333 1.866901,0 3.386667,-0.75776 3.386667,-1.69333 v -4.23335 c 0,-0.93556 -1.515533,-1.69332 -3.386667,-1.69332 m 2.54,5.92667 c 0,0.21166 -0.901699,0.84666 -2.54,0.84666 -1.6383,0 -2.540001,-0.635 -2.540001,-0.84666 v -0.94403 c 0.681568,0.33019 1.574801,0.52069 2.540001,0.52069 0.9652,0 1.858434,-0.1905 2.54,-0.52069 v 0.94403 m 0,-1.92618 c -0.550333,0.40218 -1.515533,0.65618 -2.54,0.65618 -1.024466,0 -1.989666,-0.254 -2.540001,-0.65618 v -1.18956 c 0.622301,0.35137 1.528234,0.57574 2.540001,0.57574 1.011768,0 1.917701,-0.22437 2.54,-0.57574 v 1.18956 m -2.54,-1.46049 c -1.6383,0 -2.540001,-0.635 -2.540001,-0.84668 0,-0.21166 0.901701,-0.84665 2.540001,-0.84665 1.638301,0 2.54,0.63499 2.54,0.84665 0,0.21168 -0.901699,0.84668 -2.54,0.84668 m 5.08,-0.84668 v 2.54001 h -0.846665 v -2.54001 H 98.7425 m -0.846665,3.38668 H 98.7425 v 0.84667 h -0.846665 z"
id="path1"
style="fill:#777777;fill-opacity:1;stroke-width:0.423333" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="28.800037"
viewBox="0 0 8.4666665 7.6200101"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="logo_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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="9.03125"
inkscape:cy="17.3125"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.275834,-148.48417)">
<title
id="title1">alert-octagram</title>
<title
id="title1-7">alert-octagram-outline</title>
<title
id="title1-3">database-alert</title>
<path
d="m 97.049172,150.1775 c 0,0.93557 -1.515534,1.69334 -3.38667,1.69334 -1.871134,0 -3.386668,-0.75777 -3.386668,-1.69334 0,-0.93557 1.515534,-1.69333 3.386668,-1.69333 1.871136,0 3.38667,0.75776 3.38667,1.69333 m -3.38667,4.65667 c -1.871134,0 -3.386668,-0.75776 -3.386668,-1.69333 v 1.27001 c 0,0.93557 1.515534,1.69333 3.386668,1.69333 1.871136,0 3.38667,-0.75776 3.38667,-1.69333 v -1.27001 c 0,0.93557 -1.515534,1.69333 -3.38667,1.69333 m 0,-2.11667 c -1.871134,0 -3.386668,-0.75776 -3.386668,-1.69332 v 1.26999 c 0,0.93557 1.515534,1.69333 3.386668,1.69333 1.871136,0 3.38667,-0.75776 3.38667,-1.69333 v -1.26999 c 0,0.93556 -1.515534,1.69332 -3.38667,1.69332 m 4.233338,1.69335 h 0.846661 v -0.84668 H 97.89584 v 0.84668 m 0,-4.23335 v 2.54 h 0.846661 v -2.54 z"
id="path1"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#fb4934;fill-opacity:1;stroke-width:0.431972;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: 3.0 KiB

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="31.999975"
viewBox="0 0 8.4666665 8.4666603"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="problems.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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="9.03125"
inkscape:cy="17.3125"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.275834,-148.48417)">
<title
id="title1">alert-octagram</title>
<title
id="title1-7">alert-octagram-outline</title>
<path
d="m 90.275834,154.47132 0.725714,-1.75381 -0.725714,-1.75381 1.75381,-0.72572 0.725715,-1.75381 1.753808,0.72571 1.753809,-0.72571 0.725714,1.75381 1.753811,0.72572 -0.725715,1.75381 0.725715,1.75381 -1.753811,0.72571 -0.725714,1.75382 -1.753809,-0.72572 -1.753808,0.72572 -0.725715,-1.75382 -1.75381,-0.72571 m 1.12745,-3.04973 0.535645,1.29592 -0.535645,1.29592 1.287278,0.52269 0.522687,1.28727 1.295918,-0.53565 1.295919,0.53565 0.522687,-1.28727 1.287278,-0.52269 -0.535646,-1.29592 0.535646,-1.29592 -1.287278,-0.52269 -0.522687,-1.28727 -1.295919,0.53564 -1.295918,-0.53564 -0.522687,1.28727 -1.287278,0.52269 m 2.673911,2.59184 h 0.863945 v 0.86395 h -0.863945 v -0.86395 m 0,-3.45579 h 0.863945 v 2.59184 h -0.863945 v -2.59184"
id="path1-5"
style="stroke-width:0.431972;fill:#777777;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="31.999975"
viewBox="0 0 8.4666665 8.4666603"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="triggers.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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.8284271"
inkscape:cx="-0.1767767"
inkscape:cy="15.202796"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.275834,-148.48417)">
<title
id="title1">alert-octagram</title>
<path
d="m 90.275834,154.47131 0.725714,-1.75382 -0.725714,-1.75379 1.753809,-0.72572 0.725714,-1.75381 1.75381,0.72571 1.75381,-0.72571 0.725715,1.75381 1.753809,0.72572 -0.725715,1.75379 0.725715,1.75382 -1.753809,0.72571 -0.725715,1.75381 -1.75381,-0.72571 -1.75381,0.72571 -0.725714,-1.75381 -1.753809,-0.72571 m 4.665306,0.40605 v -0.86395 h -0.863946 v 0.86395 h 0.863946 m 0,-1.72788 v -2.59184 h -0.863946 v 2.59184 z"
id="path1"
style="stroke-width:0.431972;fill:#fb4934;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32.000011"
viewBox="0 0 8.4666665 8.4666699"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="triggers.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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="7.5"
inkscape:cy="4"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-87.047917,-148.43125)">
<title
id="title1">script-text</title>
<title
id="title1-6">script-text-outline</title>
<path
d="m 92.551251,156.05126 a 0.42333338,0.42333371 0 0 0 0.423332,-0.42334 v -6.35001 h -3.386666 a 0.42333338,0.42333371 0 0 0 -0.423334,0.42334 v 4.65666 h -0.846666 v -4.65666 a 1.2700002,1.2700012 0 0 1 1.27,-1.27 h 4.656667 a 1.2700002,1.2700012 0 0 1 1.27,1.27 v 0.42333 h -0.846666 v -0.42333 a 0.42333338,0.42333371 0 0 0 -0.423334,-0.42334 0.42333338,0.42333371 0 0 0 -0.423333,0.42334 v 1.69333 4.23334 a 1.2700002,1.2700012 0 0 1 -1.27,1.27 h -4.233334 a 1.2700002,1.2700012 0 0 1 -1.27,-1.27 v -0.42333 h 4.656666 a 0.84666678,0.84666745 0 0 0 0.846668,0.84667 m -2.540001,-5.92668 h 2.116668 v 0.84667 H 90.01125 v -0.84667 m 0,1.69333 h 2.116668 v 0.84667 H 90.01125 v -0.84667 m 0,1.69334 h 2.116668 v 0.84666 H 90.01125 Z"
id="path1"
style="stroke-width:0.423333;fill:#777777;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32.000011"
viewBox="0 0 8.4666665 8.4666699"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="triggers_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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="7.5"
inkscape:cy="4"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-87.047917,-148.43125)">
<title
id="title1">script-text</title>
<path
d="m 93.736584,156.05125 c -0.169334,0.508 -0.635,0.84667 -1.185334,0.84667 h -4.233333 c -0.719667,0 -1.27,-0.55033 -1.27,-1.27 v -0.42334 h 1.27 3.894667 c 0.169333,0.508 0.635,0.84667 1.185333,0.84667 h 0.338667 m 0.508,-7.62 c 0.719666,0 1.27,0.55034 1.27,1.27 v 0.42333 h -0.846667 v -0.42333 c 0,-0.254 -0.169333,-0.42333 -0.423333,-0.42333 -0.254,0 -0.423334,0.16933 -0.423334,0.42333 v 5.50333 h -0.423333 c -0.254,0 -0.423333,-0.16933 -0.423333,-0.42333 v -0.42333 h -4.656667 v -4.65667 c 0,-0.71966 0.550333,-1.27 1.27,-1.27 h 4.656667 m -4.656667,1.69333 v 0.84667 h 2.963333 v -0.84667 h -2.963333 m 0,1.69334 v 0.84666 h 2.54 v -0.84666 z"
id="path1"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#fb4934;fill-opacity:1;stroke-width:0.691155;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.8 KiB

View File

@ -0,0 +1,53 @@
export class UI {
constructor() {
document.getElementById('button-run').
addEventListener('click', evt=>evt.preventDefault())
document.addEventListener('keydown', evt=>this.keyHandler(evt))
}
setTrigger(t) {
this.trigger = t
}
run() {
this.trigger.run()
}
keyHandler(evt) {
if (evt.altKey && evt.shiftKey && evt.key == 'R') {
evt.preventDefault()
evt.stopPropagation()
this.run()
}
}
}
export class Trigger {
constructor(id, name) {
this.id = id
this.name = name
}
run() {
const result = document.getElementById('run-result')
const classes = result.classList
const expr = document.getElementById('expr').value
fetch(`/trigger/run/${this.id}`, {
method: 'POST',
cache: 'no-cache',
body: expr,
})
.then(data => data.json())
.then(json => {
if (!json.OK) {
classes.remove('ok')
classes.add('error')
result.innerText = json.Error
return
}
classes.remove('error')
classes.add('ok')
result.innerText = json.Output
})
.catch(err => alert(err))
}
}

11
static/less/Makefile Normal file
View File

@ -0,0 +1,11 @@
less = $(wildcard *.less)
_css = $(less:.less=.css)
css = $(addprefix ../css/, $(_css) )
../css/%.css: %.less theme.less
lessc $< ../css/$@
all: $(css)
clean:
rm -vf $(css)

15
static/less/loop_make.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
while true
do
# inotifywait -q -e MOVE_SELF *less
inotifywait -q -e MODIFY *less
#sleep 0.5
clear
if make -j12; then
echo -e "\n\e[32;1mOK!\e[0m"
#curl -s http://notes.lan:1371/_ws/css_update
sleep 1
clear
fi
done

102
static/less/main.less Normal file
View File

@ -0,0 +1,102 @@
@import "theme.less";
#layout {
display: grid;
grid-template-areas: "menu content";
grid-template-columns: 64px 1fr;
grid-template-rows:
100% 100%;
height: 100vh;
}
#menu {
display: flex;
flex-flow: column wrap;
justify-content: flex-start;
gap: 24px;
grid-area: menu;
background: @bg2;
padding: 16px;
img {
display: block;
width: 32px;
}
}
#page {
grid-area: content;
padding: 32px;
.page-label {
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 12px;
align-items: center;
margin-bottom: 32px;
div {
font-size: 1.5em;
color: @color1;
}
img {
display: block;
}
}
}
#areas {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
.area {
background: @bg3;
border-radius: 4px;
&>.name {
background: @color1;
color: #fff;
font-weight: bold;
padding: 4px 16px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.section {
margin: 8px 16px;
&>.name {
font-weight: bold;
}
.triggers {
a {
color: inherit;
text-decoration: none;
}
.trigger {
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 8px;
align-items: center;
margin-top: 8px;
img {
height: 16px;
}
.label {
color: @text2;
}
}
}
}
}
}

82
static/less/theme.less Normal file
View File

@ -0,0 +1,82 @@
@bg1: #282828;
@bg2: #202020;
@bg3: #333;
@text1: #d5c4a1;
@text2: #f7edd7;
@error: #fb4934;
@color1: #fb4934;
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
*:focus {
outline: none;
}
[onClick] {
cursor: pointer;
}
html,
body {
margin: 0;
padding: 0;
}
body {
background: @bg1;
font-family: "Roboto", sans-serif;
color: @text1;
font-size: 11pt;
}
h1, h2 {
margin-top: 0px;
margin-bottom: 4px;
}
h1 {
font-size: 1.5em;
color: @color1;
}
h2 {
font-size: 1.25em;
}
.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 {
font-family: "Roboto Mono", monospace;
background: @bg2;
color: @text1;
padding: 4px 8px;
border: none;
font-size: 1em;
}
button {
background: @bg2;
color: @text1;
padding: 8px 32px;
border: 1px solid lighten(@bg2, 10%);
font-size: 1em;
height: 3em;
}

View File

@ -0,0 +1,45 @@
@import "theme.less";
.widgets {
display: grid;
grid-template-columns: min-content 1fr;
gap: 8px 16px;
.label {
margin-top: 4px;
}
input[type="text"], textarea {
width: 100%;
}
.datapoints {
font: "Roboto Mono", monospace;
display: grid;
grid-template-columns: min-content 1fr;
gap: 6px 8px;
margin-bottom: 8px;
}
.action {
display: grid;
grid-template-columns: min-content min-content 1fr;
grid-gap: 8px;
#run-result {
font-family: 'Roboto Mono', monospace;
margin-left: 16px;
padding: 16px;
background: @bg2;
min-height: 8em;
&.ok {
color: @text1;
}
&.error {
color: @error;
}
}
}
}

143
trigger.go Normal file
View File

@ -0,0 +1,143 @@
package main
import (
// External
we "git.gibonuddevalla.se/go/wrappederror"
"github.com/expr-lang/expr"
// Standard
"encoding/json"
"fmt"
"strings"
)
type Trigger struct {
ID int
Name string
SectionID int `db:"section_id"`
Expression string
Datapoints []string
}
func Foo() {
}
func TriggersRetrieve() (areas []Area, err error) { // {{{
areas = []Area{}
row := service.Db.Conn.QueryRow(`
WITH section_triggers AS (
SELECT
s.id AS id,
s.area_id,
s.name AS name,
jsonb_agg(
to_jsonb(t.*)
) AS triggers
FROM section s
LEFT JOIN "trigger" t ON t.section_id = s.id
GROUP BY
s.id, s.name)
SELECT
jsonb_agg(jsonsections)
FROM (
SELECT
a.id,
a.name,
jsonb_agg(
to_jsonb(
s.*
)
) AS sections
FROM area a
LEFT JOIN section_triggers s ON s.area_id = a.id
GROUP BY
a.id, a.name
) jsonsections
`,
)
var jsonData []byte
err = row.Scan(&jsonData)
if err != nil {
err = we.Wrap(err)
return
}
err = json.Unmarshal(jsonData, &areas)
if err != nil {
err = we.Wrap(err)
return
}
return
} // }}}
func TriggerRetrieve(id int) (trigger Trigger, err error) { // {{{
row := service.Db.Conn.QueryRow(`SELECT to_jsonb(t.*) FROM "trigger" t WHERE id=$1`, id)
var jsonData []byte
err = row.Scan(&jsonData)
if err != nil {
err = we.Wrap(err)
return
}
err = json.Unmarshal(jsonData, &trigger)
return
} // }}}
func (t *Trigger) Validate() (ok bool, err error) {
if strings.TrimSpace(t.Name) == "" {
err = fmt.Errorf("Name can't be empty")
return
}
if strings.TrimSpace(t.Expression) == "" {
err = fmt.Errorf("Expression can't be empty")
return
}
return true, nil
}
func (t *Trigger) Update() (err error) {
var ok bool
if ok, err = t.Validate(); !ok {
return
}
logger.Info("FOO", "trigger", t)
_, err = service.Db.Conn.Exec(`
UPDATE "trigger"
SET
name=$2,
expression=$3
WHERE
id=$1
`,
t.ID,
t.Name,
t.Expression,
)
if err != nil {
err = we.Wrap(err)
}
return
}
func (t *Trigger) Run(datapoints map[string]Datapoint) (output any, err error) {
env := make(map[string]any)
for dpName, dp := range datapoints {
env[dpName] = dp.LastDatapointValue.Value()
}
program, err := expr.Compile(t.Expression)
if err != nil {
return
}
output, err = expr.Run(program, env)
if err != nil {
return
}
return
}

View File

@ -0,0 +1,6 @@
{{ define "fonts" }}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
{{ end }}

View File

@ -0,0 +1,8 @@
{{ define "menu" }}
<div id="menu">
<a href="/"><img src="/images/{{ .VERSION }}/logo{{ if eq .MENU "index" }}_selected{{ end }}.svg"></a>
<a href="/problems"><img src="/images/{{ .VERSION }}/problems{{ if eq .MENU "problems" }}_selected{{ end }}.svg"></a>
<a href="/triggers"><img src="/images/{{ .VERSION }}/triggers{{ if eq .MENU "triggers" }}_selected{{ end }}.svg"></a>
<a href="/configuration"><img src="/images/{{ .VERSION }}/configuration{{ if eq .MENU "configuration" }}_selected{{ end }}.svg"></a>
</div>
{{ end }}

View File

@ -0,0 +1,6 @@
{{ define "page_label" }}
<div class="page-label">
<img src="/images/{{ .VERSION }}/{{ .Icon }}.svg">
<div>{{ .Label }}</div>
</div>
{{ end }}

17
views/layouts/main.gotmpl Normal file
View File

@ -0,0 +1,17 @@
<!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 }}/main.css">
</head>
<body>
<div id="layout">
{{ block "menu" . }}{{ end }}
<div id="page">
{{ block "page" . }}{{ end }}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,3 @@
{{ define "page" }}
area new
{{ end }}

View File

@ -0,0 +1,20 @@
{{ define "page" }}
{{ block "page_label" . }}{{end}}
<h1>Areas</h1>
<div id="areas">
{{ range .Data.Areas }}
<div class="area">
<div class="name">{{ .Name }}</div>
{{ range .SortedSections }}
<div class="section">
<div class="name">{{ .Name }}</div>
</div>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}

12
views/pages/index.gotmpl Normal file
View File

@ -0,0 +1,12 @@
{{ define "page" }}
<div style="float: left;">
<img src="/images/{{ .VERSION }}/logo_selected.svg" style="width: 64px; margin-right: 32px;">
</div>
<div style="float: left;">
<h1>SMon</h1>
<h2>{{ .VERSION }}</h2>
</div>
{{ end }}

View File

@ -0,0 +1,17 @@
{{ define "page" }}
{{ block "page_label" . }}{{end}}
<div id="areas">
{{ range .Data.Areas }}
<div class="area">
<div class="name">{{ .Name }}</div>
{{ range .SortedSections }}
<div class="section">
<div class="name">{{ .Name }}</div>
</div>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}

View File

@ -0,0 +1,40 @@
{{ define "page" }}
<script type="module" defer>
import {UI, Trigger} from "/js/{{ .VERSION }}/trigger_edit.mjs"
window._ui = new UI()
let trigger = new Trigger(
{{ .Data.Trigger.ID }},
'{{ .Data.Trigger.Name }}',
)
_ui.setTrigger(trigger)
</script>
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/trigger_edit.css">
{{ block "page_label" . }}{{end}}
<form action="/trigger/update/{{ .Data.Trigger.ID }}" method="post">
<div id="widgets" class="widgets">
<div class="label">Name</div>
<div><input type="text" name="name" value="{{ .Data.Trigger.Name }}"></div>
<div class="label">Datapoints</div>
<div class="datapoints" style="margin-top: 4px">
{{ range .Data.Datapoints }}
<div class="datapoint name">{{ .Name }}</div>
<div class="datapoint value">{{ .LastDatapointValue.Value }}</div>
{{ end }}
</div>
<div class="label">Expression</div>
<div><textarea id="expr" name="expression" rows=8>{{ .Data.Trigger.Expression }}</textarea></div>
<div></div>
<div class="action">
<button>Update</button>
<button id="button-run" onclick="window._ui.run(); return false">Test</button>
<div id="run-result"></div>
</div>
</form>
{{ end }}

View File

@ -0,0 +1,33 @@
{{ define "page" }}
{{ block "page_label" . }}{{end}}
{{ $version := .VERSION }}
<div id="areas">
{{ range .Data.Areas }}
<div class="area">
<div class="name">{{ .Name }}</div>
{{ range .SortedSections }}
<div class="section">
<div class="name">{{ .Name }}</div>
<div class="triggers">
{{ range .SortedTriggers }}
{{ if eq .Name "" }}
{{ continue }}
{{ end }}
<a href="/trigger/edit/{{ .ID }}">
<div class="trigger">
<img src="/images/{{ $version }}/triggers.svg">
<div class="label">{{ .Name }}</div>
</div>
</a>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}