Compare commits
3 Commits
6629b834fd
...
fe9571c466
Author | SHA1 | Date | |
---|---|---|---|
|
fe9571c466 | ||
|
1185ebd030 | ||
|
b22e99a072 |
64
datapoint.go
64
datapoint.go
@ -21,14 +21,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Datapoint struct {
|
type Datapoint struct {
|
||||||
ID int
|
ID int
|
||||||
Group string
|
Group string
|
||||||
Name string
|
Name string
|
||||||
Datatype DatapointType
|
Datatype DatapointType
|
||||||
LastValue time.Time `db:"last_value"`
|
LastValue time.Time `db:"last_value"`
|
||||||
DatapointValueJSON []byte `db:"datapoint_value_json"`
|
DatapointValueJSON []byte `db:"datapoint_value_json"`
|
||||||
LastDatapointValue DatapointValue
|
LastDatapointValue DatapointValue
|
||||||
Found bool
|
Found bool
|
||||||
|
NodataProblemSeconds int `db:"nodata_problem_seconds"`
|
||||||
|
NodataIsProblem bool `db:"nodata_is_problem"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatapointValue struct {
|
type DatapointValue struct {
|
||||||
@ -56,13 +58,13 @@ func (dp DatapointValue) Value() any { // {{{
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
} // }}}
|
} // }}}
|
||||||
func (dp DatapointValue) FormattedTime() string {// {{{
|
func (dp DatapointValue) FormattedTime() string { // {{{
|
||||||
if dp.ValueDateTime.Valid {
|
if dp.ValueDateTime.Valid {
|
||||||
return dp.ValueDateTime.Time.Format("2006-01-02 15:04:05")
|
return dp.ValueDateTime.Time.Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
return "invalid time"
|
return "invalid time"
|
||||||
}// }}}
|
} // }}}
|
||||||
func (dp Datapoint) Update() (err error) {// {{{
|
func (dp Datapoint) Update() (err error) { // {{{
|
||||||
name := strings.TrimSpace(dp.Name)
|
name := strings.TrimSpace(dp.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
err = errors.New("Name can't be empty")
|
err = errors.New("Name can't be empty")
|
||||||
@ -71,23 +73,47 @@ func (dp Datapoint) Update() (err error) {// {{{
|
|||||||
|
|
||||||
if dp.ID == 0 {
|
if dp.ID == 0 {
|
||||||
_, err = service.Db.Conn.Exec(
|
_, err = service.Db.Conn.Exec(
|
||||||
`INSERT INTO datapoint("group", name, datatype) VALUES($1, $2, $3)`,
|
`INSERT INTO datapoint("group", name, datatype) VALUES($1, $2, $3, $4)`,
|
||||||
dp.Group,
|
dp.Group,
|
||||||
name,
|
name,
|
||||||
dp.Datatype,
|
dp.Datatype,
|
||||||
|
dp.NodataProblemSeconds,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
/* Keep nodata_is_problem as is unless the nodata_problem_seconds is changed.
|
||||||
|
* Otherwise unnecessary nodata problems could be notified when updating unrelated
|
||||||
|
* datapoint properties. */
|
||||||
_, err = service.Db.Conn.Exec(
|
_, err = service.Db.Conn.Exec(
|
||||||
`UPDATE datapoint SET "group"=$2, name=$3, datatype=$4 WHERE id=$1`,
|
`
|
||||||
|
UPDATE datapoint
|
||||||
|
SET
|
||||||
|
"group"=$2,
|
||||||
|
name=$3,
|
||||||
|
datatype=$4,
|
||||||
|
nodata_problem_seconds=$5,
|
||||||
|
nodata_is_problem = (
|
||||||
|
CASE
|
||||||
|
WHEN $5 != nodata_problem_seconds THEN false
|
||||||
|
ELSE
|
||||||
|
nodata_is_problem
|
||||||
|
END
|
||||||
|
)
|
||||||
|
WHERE
|
||||||
|
id=$1
|
||||||
|
`,
|
||||||
dp.ID,
|
dp.ID,
|
||||||
dp.Group,
|
dp.Group,
|
||||||
name,
|
name,
|
||||||
dp.Datatype,
|
dp.Datatype,
|
||||||
|
dp.NodataProblemSeconds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
err = werr.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}// }}}
|
} // }}}
|
||||||
|
|
||||||
func DatapointAdd[T any](name string, value T) (err error) { // {{{
|
func DatapointAdd[T any](name string, value T) (err error) { // {{{
|
||||||
row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name)
|
row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name)
|
||||||
@ -120,7 +146,7 @@ func DatapointAdd[T any](name string, value T) (err error) { // {{{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
service.Db.Conn.Exec(`UPDATE datapoint SET last_value = NOW() WHERE name=$1`, name)
|
service.Db.Conn.Exec(`UPDATE datapoint SET last_value = NOW(), nodata_is_problem = false WHERE id=$1`, dpID)
|
||||||
|
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
@ -253,14 +279,14 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{
|
|||||||
|
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
func DatapointDelete(id int) (err error) {// {{{
|
func DatapointDelete(id int) (err error) { // {{{
|
||||||
_, err = service.Db.Conn.Exec(`DELETE FROM datapoint WHERE id=$1`, id)
|
_, err = service.Db.Conn.Exec(`DELETE FROM datapoint WHERE id=$1`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = werr.Wrap(err).WithData(id)
|
err = werr.Wrap(err).WithData(id)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}// }}}
|
} // }}}
|
||||||
func DatapointValues(id int) (values []DatapointValue, err error) {// {{{
|
func DatapointValues(id int) (values []DatapointValue, err error) { // {{{
|
||||||
rows, err := service.Db.Conn.Queryx(`SELECT * FROM datapoint_value WHERE datapoint_id=$1 ORDER BY ts DESC LIMIT 500`, id)
|
rows, err := service.Db.Conn.Queryx(`SELECT * FROM datapoint_value WHERE datapoint_id=$1 ORDER BY ts DESC LIMIT 500`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = werr.Wrap(err).WithData(id)
|
err = werr.Wrap(err).WithData(id)
|
||||||
@ -278,4 +304,4 @@ func DatapointValues(id int) (values []DatapointValue, err error) {// {{{
|
|||||||
values = append(values, dpv)
|
values = append(values, dpv)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}// }}}
|
} // }}}
|
||||||
|
4
go.mod
4
go.mod
@ -6,14 +6,14 @@ require (
|
|||||||
git.gibonuddevalla.se/go/webservice v0.2.15
|
git.gibonuddevalla.se/go/webservice v0.2.15
|
||||||
git.gibonuddevalla.se/go/wrappederror v0.3.4
|
git.gibonuddevalla.se/go/wrappederror v0.3.4
|
||||||
github.com/expr-lang/expr v1.16.5
|
github.com/expr-lang/expr v1.16.5
|
||||||
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.gibonuddevalla.se/go/dbschema v1.3.0 // indirect
|
git.gibonuddevalla.se/go/dbschema v1.3.0 // indirect
|
||||||
github.com/google/uuid v1.5.0 // indirect
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.1 // 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
|
golang.org/x/net v0.17.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@ -1,7 +1,7 @@
|
|||||||
git.gibonuddevalla.se/go/dbschema v1.3.0 h1:HzFMR29tWfy/ibIjltTbIMI4inVktj/rh8bESALibgM=
|
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/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.15 h1:ECe63fRDSrg3RJcgYV2pG+WsAQLVG8wvfHennz7aHsY=
|
||||||
git.gibonuddevalla.se/go/webservice v0.2.12/go.mod h1:3uBS6nLbK9qbuGzDls8MZD5Xr9ORY1Srbj6v06BIhws=
|
git.gibonuddevalla.se/go/webservice v0.2.15/go.mod h1:3uBS6nLbK9qbuGzDls8MZD5Xr9ORY1Srbj6v06BIhws=
|
||||||
git.gibonuddevalla.se/go/wrappederror v0.3.4 h1:dcKp9/+QrZSO3S4fVnq7yG2p7DUZVmlztBAb/OzoZNY=
|
git.gibonuddevalla.se/go/wrappederror v0.3.4 h1:dcKp9/+QrZSO3S4fVnq7yG2p7DUZVmlztBAb/OzoZNY=
|
||||||
git.gibonuddevalla.se/go/wrappederror v0.3.4/go.mod h1:j4w320Hk1wvhOPjUaK4GgLvmtnjUUM5yVu6JFO1OCSc=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
6
main.go
6
main.go
@ -140,6 +140,8 @@ func main() { // {{{
|
|||||||
service.Register("/configuration", false, false, pageConfiguration)
|
service.Register("/configuration", false, false, pageConfiguration)
|
||||||
service.Register("/entry/{datapoint}", false, false, entryDatapoint)
|
service.Register("/entry/{datapoint}", false, false, entryDatapoint)
|
||||||
|
|
||||||
|
go nodataLoop()
|
||||||
|
|
||||||
err = service.Start()
|
err = service.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("webserver", "error", werr.Wrap(err))
|
logger.Error("webserver", "error", werr.Wrap(err))
|
||||||
@ -553,11 +555,15 @@ func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nodataSeconds int
|
||||||
|
nodataSeconds, _ = strconv.Atoi(r.FormValue("nodata_seconds"))
|
||||||
|
|
||||||
var dp Datapoint
|
var dp Datapoint
|
||||||
dp.ID = id
|
dp.ID = id
|
||||||
dp.Group = r.FormValue("group")
|
dp.Group = r.FormValue("group")
|
||||||
dp.Name = r.FormValue("name")
|
dp.Name = r.FormValue("name")
|
||||||
dp.Datatype = DatapointType(r.FormValue("datatype"))
|
dp.Datatype = DatapointType(r.FormValue("datatype"))
|
||||||
|
dp.NodataProblemSeconds = nodataSeconds
|
||||||
err = dp.Update()
|
err = dp.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, werr.Wrap(err).Log())
|
httpError(w, werr.Wrap(err).Log())
|
||||||
|
75
nodata.go
Normal file
75
nodata.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nodataLoop checks if datapoint last_value is larger than the nodata_problem_seconds period and
|
||||||
|
// marks them as problems. They are then notified.
|
||||||
|
func nodataLoop() {
|
||||||
|
var ids []int
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// TODO - should be configurable
|
||||||
|
ticker := time.NewTicker(time.Second * 5)
|
||||||
|
for {
|
||||||
|
<-ticker.C
|
||||||
|
ids, err = nodataDatapointIDs()
|
||||||
|
if err != nil {
|
||||||
|
err = werr.Wrap(err).Log()
|
||||||
|
logger.Error("nodata", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ids) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("nodata", "problem_ids", ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodataDatapointIDs() (ids []int, err error) {
|
||||||
|
ids = []int{}
|
||||||
|
|
||||||
|
var rows *sql.Rows
|
||||||
|
rows, err = service.Db.Conn.Query(`
|
||||||
|
UPDATE datapoint
|
||||||
|
SET
|
||||||
|
nodata_is_problem = true
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id
|
||||||
|
FROM
|
||||||
|
datapoint
|
||||||
|
WHERE
|
||||||
|
NOT nodata_is_problem AND
|
||||||
|
extract(EPOCH from (NOW() - last_value))::int > nodata_problem_seconds
|
||||||
|
) AS subquery
|
||||||
|
WHERE
|
||||||
|
datapoint.id = subquery.id
|
||||||
|
RETURNING
|
||||||
|
datapoint.id
|
||||||
|
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
err = werr.Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var id int
|
||||||
|
for rows.Next() {
|
||||||
|
if err = rows.Scan(&id); err != nil {
|
||||||
|
err = werr.Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
4
sql/00014.sql
Normal file
4
sql/00014.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE datapoint ADD COLUMN nodata_problem_seconds INT4 NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE datapoint ADD COLUMN nodata_is_problem BOOL NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
CREATE INDEX datapoint_last_value_idx ON public.datapoint ("last_value");
|
@ -19,7 +19,7 @@ body {
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
background: #282828;
|
background: #282828;
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
@ -48,25 +48,16 @@ a:hover {
|
|||||||
b {
|
b {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.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"],
|
input[type="text"],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font-family: "Roboto Mono", monospace;
|
font-family: monospace;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background: #202020;
|
background: #202020;
|
||||||
@ -97,6 +88,16 @@ span.seconds {
|
|||||||
label {
|
label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.description {
|
||||||
|
border: 1px solid #737373;
|
||||||
|
color: #3f9da1;
|
||||||
|
background: #202020;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: min-content;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
#datapoints {
|
#datapoints {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, min-content);
|
grid-template-columns: repeat(5, min-content);
|
||||||
@ -137,6 +138,7 @@ label {
|
|||||||
}
|
}
|
||||||
.widgets .label {
|
.widgets .label {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.widgets input[type="text"],
|
.widgets input[type="text"],
|
||||||
.widgets textarea {
|
.widgets textarea {
|
||||||
|
@ -19,7 +19,7 @@ body {
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
background: #282828;
|
background: #282828;
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
@ -48,25 +48,16 @@ a:hover {
|
|||||||
b {
|
b {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.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"],
|
input[type="text"],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font-family: "Roboto Mono", monospace;
|
font-family: monospace;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background: #202020;
|
background: #202020;
|
||||||
@ -97,6 +88,16 @@ span.seconds {
|
|||||||
label {
|
label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.description {
|
||||||
|
border: 1px solid #737373;
|
||||||
|
color: #3f9da1;
|
||||||
|
background: #202020;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: min-content;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
#layout {
|
#layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "menu content";
|
grid-template-areas: "menu content";
|
||||||
|
@ -19,7 +19,7 @@ body {
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
background: #282828;
|
background: #282828;
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
@ -48,25 +48,16 @@ a:hover {
|
|||||||
b {
|
b {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.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"],
|
input[type="text"],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font-family: "Roboto Mono", monospace;
|
font-family: monospace;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background: #202020;
|
background: #202020;
|
||||||
@ -97,6 +88,16 @@ span.seconds {
|
|||||||
label {
|
label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.description {
|
||||||
|
border: 1px solid #737373;
|
||||||
|
color: #3f9da1;
|
||||||
|
background: #202020;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: min-content;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
#problems-list,
|
#problems-list,
|
||||||
#acknowledged-list {
|
#acknowledged-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -19,7 +19,7 @@ body {
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
background: #282828;
|
background: #282828;
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
@ -48,25 +48,16 @@ a:hover {
|
|||||||
b {
|
b {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.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"],
|
input[type="text"],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font-family: "Roboto Mono", monospace;
|
font-family: monospace;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background: #202020;
|
background: #202020;
|
||||||
@ -97,3 +88,13 @@ span.seconds {
|
|||||||
label {
|
label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.description {
|
||||||
|
border: 1px solid #737373;
|
||||||
|
color: #3f9da1;
|
||||||
|
background: #202020;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: min-content;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@ body {
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
background: #282828;
|
background: #282828;
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
@ -48,25 +48,16 @@ a:hover {
|
|||||||
b {
|
b {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.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"],
|
input[type="text"],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font-family: "Roboto Mono", monospace;
|
font-family: monospace;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
color: #d5c4a1;
|
color: #d5c4a1;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background: #202020;
|
background: #202020;
|
||||||
@ -97,6 +88,16 @@ span.seconds {
|
|||||||
label {
|
label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.description {
|
||||||
|
border: 1px solid #737373;
|
||||||
|
color: #3f9da1;
|
||||||
|
background: #202020;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: min-content;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
.widgets {
|
.widgets {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: min-content 1fr;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export class UI {
|
export class UI {
|
||||||
constructor() {
|
constructor() {
|
||||||
document.addEventListener('keydown', evt=>this.keyHandler(evt))
|
document.addEventListener('keydown', evt=>this.keyHandler(evt))
|
||||||
document.querySelector('input[name="name"]').focus()
|
document.querySelector('input[name="group"]').focus()
|
||||||
}
|
}
|
||||||
keyHandler(evt) {
|
keyHandler(evt) {
|
||||||
if (!(evt.altKey && evt.shiftKey))
|
if (!(evt.altKey && evt.shiftKey))
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"], textarea {
|
input[type="text"], textarea {
|
||||||
|
@ -44,7 +44,7 @@ body {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background: @bg1;
|
background: @bg1;
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: @text1;
|
color: @text1;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
@ -80,27 +80,16 @@ b {
|
|||||||
font-weight: @bold;
|
font-weight: @bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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"],
|
input[type="text"],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font-family: "Roboto Mono", monospace;
|
font-family: monospace;
|
||||||
background: @bg2;
|
background: @bg2;
|
||||||
color: @text1;
|
color: @text1;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
line-height: 1.5em; // fix for chrome hiding underscores
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -138,3 +127,14 @@ span.seconds {
|
|||||||
label {
|
label {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
border: 1px solid .lighterOrDarker(@bg3, 25%)[@result];
|
||||||
|
color: @color4;
|
||||||
|
background: @bg2;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: min-content;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
@ -25,6 +25,13 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="label">No data<br>problem time<br>(seconds)</div>
|
||||||
|
<div>
|
||||||
|
<input type="text" name="nodata_seconds" value="{{ .Data.Datapoint.NodataProblemSeconds }}">
|
||||||
|
<div class="description">A problem is raised and notified if an entry isn't made within this time.</div>
|
||||||
|
<div class="description">Set to 0 to disable.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
{{ if eq .Data.Datapoint.ID 0 }}
|
{{ if eq .Data.Datapoint.ID 0 }}
|
||||||
|
@ -2,19 +2,8 @@
|
|||||||
{{ $version := .VERSION }}
|
{{ $version := .VERSION }}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/datapoints.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/datapoints.css">
|
||||||
|
|
||||||
<script type="module" defer>
|
|
||||||
import { DataGraph } from "/js/{{ .VERSION }}/datapoint_graph.mjs"
|
|
||||||
const graph = new DataGraph({{ .Data.Datapoint }})
|
|
||||||
graph.setData({{ .Data.Values }})
|
|
||||||
graph.render('chart')
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{ block "page_label" . }}{{end}}
|
{{ block "page_label" . }}{{end}}
|
||||||
|
|
||||||
<div>
|
|
||||||
<canvas id="chart"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="values">
|
<div id="values">
|
||||||
{{ range .Data.Values }}
|
{{ range .Data.Values }}
|
||||||
<div class="value">{{ format_time .Ts }}</div>
|
<div class="value">{{ format_time .Ts }}</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user