Initial commit
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
smon
|
||||||
|
smon.exe
|
62
area.go
Normal 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
@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type SmonConfiguration struct {
|
||||||
|
LogFile string
|
||||||
|
}
|
112
datapoint.go
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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;
|
||||||
|
}
|
69
static/images/configuration.svg
Normal 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 |
67
static/images/configuration_selected.svg
Normal 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
@ -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 |
71
static/images/logo_selected.svg
Normal 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 |
69
static/images/problems.svg
Normal 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 |
67
static/images/problems_selected.svg
Normal 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 |
69
static/images/triggers.svg
Normal 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 |
67
static/images/triggers_selected.svg
Normal 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 |
53
static/js/trigger_edit.mjs
Normal 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
@ -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
@ -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
@ -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
@ -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;
|
||||||
|
}
|
45
static/less/trigger_edit.less
Normal 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
@ -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
|
||||||
|
}
|
6
views/components/head_fonts.gotmpl
Normal 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 }}
|
8
views/components/menu.gotmpl
Normal 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 }}
|
6
views/components/page_label.gotmpl
Normal 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
@ -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>
|
3
views/pages/area_new.gotmpl
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{{ define "page" }}
|
||||||
|
area new
|
||||||
|
{{ end }}
|
20
views/pages/configuration.gotmpl
Normal 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
@ -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 }}
|
17
views/pages/problems.gotmpl
Normal 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 }}
|
40
views/pages/trigger_edit.gotmpl
Normal 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 }}
|
33
views/pages/triggers.gotmpl
Normal 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 }}
|