Compare commits

..

4 Commits

Author SHA1 Message Date
Magnus Åhall
42b1a20531 #3, added datapoint comment 2024-06-24 11:18:51 +02:00
Magnus Åhall
f29693a066 Added script notification 2024-06-02 10:59:06 +02:00
Magnus Åhall
4a52c319c4 Clean up notification type enum and add SCRIPT 2024-06-02 10:10:14 +02:00
Magnus Åhall
b83adad7c8 Added area deletion 2024-06-02 09:17:50 +02:00
21 changed files with 666 additions and 50 deletions

61
area.go
View File

@ -2,9 +2,10 @@ package main
import ( import (
// External // External
re "git.gibonuddevalla.se/go/wrappederror" werr "git.gibonuddevalla.se/go/wrappederror"
// Standard // Standard
"database/sql"
"encoding/json" "encoding/json"
"sort" "sort"
) )
@ -41,7 +42,7 @@ func AreaRetrieve() (areas []Area, err error) { // {{{
var jsonData []byte var jsonData []byte
err = row.Scan(&jsonData) err = row.Scan(&jsonData)
if err != nil { if err != nil {
err = re.Wrap(err) err = werr.Wrap(err)
return return
} }
@ -51,7 +52,7 @@ func AreaRetrieve() (areas []Area, err error) { // {{{
err = json.Unmarshal(jsonData, &areas) err = json.Unmarshal(jsonData, &areas)
if err != nil { if err != nil {
err = re.Wrap(err) err = werr.Wrap(err)
return return
} }
@ -65,6 +66,60 @@ func AreaRename(id int, name string) (err error) {// {{{
_, err = service.Db.Conn.Exec(`UPDATE area SET name=$2 WHERE id=$1`, id, name) _, err = service.Db.Conn.Exec(`UPDATE area SET name=$2 WHERE id=$1`, id, name)
return return
} // }}} } // }}}
func AreaDelete(id int) (err error) { // {{{
var trx *sql.Tx
trx, err = service.Db.Conn.Begin()
if err != nil {
err = werr.Wrap(err).WithData(id)
}
_, err = trx.Exec(`
DELETE
FROM trigger t
USING section s
WHERE
t.section_id = s.id AND
s.area_id = $1
`,
id,
)
if err != nil {
err2 := trx.Rollback()
if err2 != nil {
return werr.Wrap(err2).WithData(err)
}
return werr.Wrap(err).WithData(id)
}
_, err = trx.Exec(`DELETE FROM public.section WHERE area_id = $1`, id)
if err != nil {
err2 := trx.Rollback()
if err2 != nil {
return werr.Wrap(err2).WithData(err)
}
return werr.Wrap(err).WithData(id)
}
_, err = trx.Exec(`DELETE FROM public.area WHERE id = $1`, id)
if err != nil {
err2 := trx.Rollback()
if err2 != nil {
return werr.Wrap(err2).WithData(err)
}
return werr.Wrap(err).WithData(id)
}
err = trx.Commit()
if err != nil {
err2 := trx.Rollback()
if err2 != nil {
return werr.Wrap(err2).WithData(err)
}
return werr.Wrap(err).WithData(id)
}
return nil
} // }}}
func (a Area) SortedSections() []Section { // {{{ func (a Area) SortedSections() []Section { // {{{
sort.SliceStable(a.Sections, func(i, j int) bool { sort.SliceStable(a.Sections, func(i, j int) bool {

View File

@ -25,6 +25,7 @@ type Datapoint struct {
Group string Group string
Name string Name string
Datatype DatapointType Datatype DatapointType
Comment string
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
@ -73,11 +74,12 @@ 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, nodata_problem_seconds) VALUES($1, $2, $3, $4)`, `INSERT INTO datapoint("group", name, datatype, nodata_problem_seconds, comment) VALUES($1, $2, $3, $4, $5)`,
dp.Group, dp.Group,
name, name,
dp.Datatype, dp.Datatype,
dp.NodataProblemSeconds, dp.NodataProblemSeconds,
dp.Comment,
) )
} else { } else {
/* Keep nodata_is_problem as is unless the nodata_problem_seconds is changed. /* Keep nodata_is_problem as is unless the nodata_problem_seconds is changed.
@ -90,10 +92,11 @@ func (dp Datapoint) Update() (err error) { // {{{
"group"=$2, "group"=$2,
name=$3, name=$3,
datatype=$4, datatype=$4,
nodata_problem_seconds=$5, comment=$5,
nodata_problem_seconds=$6,
nodata_is_problem = ( nodata_is_problem = (
CASE CASE
WHEN $5 != nodata_problem_seconds THEN false WHEN $6 != nodata_problem_seconds THEN false
ELSE ELSE
nodata_is_problem nodata_is_problem
END END
@ -105,6 +108,7 @@ func (dp Datapoint) Update() (err error) { // {{{
dp.Group, dp.Group,
name, name,
dp.Datatype, dp.Datatype,
dp.Comment,
dp.NodataProblemSeconds, dp.NodataProblemSeconds,
) )
} }
@ -161,6 +165,7 @@ func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{
dp.datatype, dp.datatype,
dp.last_value, dp.last_value,
dp.group, dp.group,
dp.comment,
dp.nodata_problem_seconds, dp.nodata_problem_seconds,
dpv.id AS v_id, dpv.id AS v_id,
@ -190,6 +195,7 @@ func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{
Group string Group string
Name string Name string
Datatype DatapointType Datatype DatapointType
Comment string
LastValue time.Time `db:"last_value"` LastValue time.Time `db:"last_value"`
NodataProblemSeconds int `db:"nodata_problem_seconds"` NodataProblemSeconds int `db:"nodata_problem_seconds"`
@ -214,6 +220,7 @@ func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{
dp.Name = res.Name dp.Name = res.Name
dp.Group = res.Group dp.Group = res.Group
dp.Datatype = res.Datatype dp.Datatype = res.Datatype
dp.Comment = res.Comment
dp.LastValue = res.LastValue dp.LastValue = res.LastValue
dp.Found = true dp.Found = true
dp.NodataProblemSeconds = res.NodataProblemSeconds dp.NodataProblemSeconds = res.NodataProblemSeconds

91
main.go
View File

@ -120,33 +120,35 @@ func main() { // {{{
service.Register("/", false, false, staticHandler) service.Register("/", false, false, staticHandler)
service.Register("/area/new/{name}", false, false, areaNew) service.Register("/area/new/{name}", false, false, actionAreaNew)
service.Register("/area/rename/{id}/{name}", false, false, areaRename) service.Register("/area/rename/{id}/{name}", false, false, actionAreaRename)
service.Register("/area/delete/{id}", false, false, actionAreaDelete)
service.Register("/section/new/{areaID}/{name}", false, false, sectionNew) service.Register("/section/new/{areaID}/{name}", false, false, actionSectionNew)
service.Register("/section/rename/{id}/{name}", false, false, sectionRename) service.Register("/section/rename/{id}/{name}", false, false, actionSectionRename)
service.Register("/section/delete/{id}", false, false, actionSectionDelete)
service.Register("/problems", false, false, pageProblems) service.Register("/problems", false, false, pageProblems)
service.Register("/problem/acknowledge/{id}", false, false, pageProblemAcknowledge) service.Register("/problem/acknowledge/{id}", false, false, actionProblemAcknowledge)
service.Register("/problem/unacknowledge/{id}", false, false, pageProblemUnacknowledge) service.Register("/problem/unacknowledge/{id}", false, false, actionProblemUnacknowledge)
service.Register("/datapoints", false, false, pageDatapoints) service.Register("/datapoints", false, false, pageDatapoints)
service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit) service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit)
service.Register("/datapoint/update/{id}", false, false, pageDatapointUpdate) service.Register("/datapoint/update/{id}", false, false, actionDatapointUpdate)
service.Register("/datapoint/delete/{id}", false, false, pageDatapointDelete) service.Register("/datapoint/delete/{id}", false, false, actionDatapointDelete)
service.Register("/datapoint/values/{id}", false, false, pageDatapointValues) service.Register("/datapoint/values/{id}", false, false, pageDatapointValues)
service.Register("/triggers", false, false, pageTriggers) service.Register("/triggers", false, false, pageTriggers)
service.Register("/trigger/create/{sectionID}/{name}", false, false, triggerCreate) service.Register("/trigger/create/{sectionID}/{name}", false, false, actionTriggerCreate)
service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit) service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit)
service.Register("/trigger/edit/{id}/{sectionID}", false, false, pageTriggerEdit) service.Register("/trigger/edit/{id}/{sectionID}", false, false, pageTriggerEdit)
service.Register("/trigger/addDatapoint/{id}/{datapointName}", false, false, pageTriggerDatapointAdd) service.Register("/trigger/addDatapoint/{id}/{datapointName}", false, false, actionTriggerDatapointAdd)
service.Register("/trigger/update/{id}", false, false, pageTriggerUpdate) service.Register("/trigger/update/{id}", false, false, actionTriggerUpdate)
service.Register("/trigger/run/{id}", false, false, pageTriggerRun) service.Register("/trigger/run/{id}", false, false, actionTriggerRun)
service.Register("/trigger/delete/{id}", false, false, actionTriggerDelete) service.Register("/trigger/delete/{id}", false, false, actionTriggerDelete)
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, actionEntryDatapoint)
go nodataLoop() go nodataLoop()
@ -197,7 +199,7 @@ func staticHandler(w http.ResponseWriter, r *http.Request, sess *session.T) { //
service.StaticHandler(w, r, sess) service.StaticHandler(w, r, sess)
} // }}} } // }}}
func entryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ func actionEntryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
dpoint := r.PathValue("datapoint") dpoint := r.PathValue("datapoint")
value, _ := io.ReadAll(r.Body) value, _ := io.ReadAll(r.Body)
@ -368,7 +370,7 @@ func pageIndex(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page.Render(w) page.Render(w)
} // }}} } // }}}
func areaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionAreaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
name := r.PathValue("name") name := r.PathValue("name")
err := AreaCreate(name) err := AreaCreate(name)
if err != nil { if err != nil {
@ -380,7 +382,7 @@ func areaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
w.WriteHeader(302) w.WriteHeader(302)
return return
} // }}} } // }}}
func areaRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionAreaRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id") idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
@ -399,8 +401,26 @@ func areaRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
w.WriteHeader(302) w.WriteHeader(302)
return return
} // }}} } // }}}
func actionAreaDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
httpError(w, werr.Wrap(err).WithData(idStr).Log())
return
}
func sectionNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ err = AreaDelete(id)
if err != nil {
httpError(w, werr.Wrap(err).WithData(id).Log())
return
}
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
return
} // }}}
func actionSectionNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("areaID") idStr := r.PathValue("areaID")
areaID, err := strconv.Atoi(idStr) areaID, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
@ -419,7 +439,7 @@ func sectionNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
w.WriteHeader(302) w.WriteHeader(302)
return return
} // }}} } // }}}
func sectionRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionSectionRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id") idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
@ -438,6 +458,24 @@ func sectionRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{
w.WriteHeader(302) w.WriteHeader(302)
return return
} // }}} } // }}}
func actionSectionDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
httpError(w, werr.Wrap(err).WithData(idStr).Log())
return
}
err = SectionDelete(id)
if err != nil {
httpError(w, werr.Wrap(err).WithData(id).Log())
return
}
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
return
} // }}}
func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page := Page{ page := Page{
@ -474,7 +512,7 @@ func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page.Render(w) page.Render(w)
return return
} // }}} } // }}}
func pageProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id") idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
@ -493,7 +531,7 @@ func pageProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T
return return
} // }}} } // }}}
func pageProblemUnacknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionProblemUnacknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id") idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
@ -572,7 +610,7 @@ func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { /
page.Render(w) page.Render(w)
return return
} // }}} } // }}}
func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id") idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
@ -588,6 +626,7 @@ func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) {
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.Comment = r.FormValue("comment")
dp.NodataProblemSeconds = nodataSeconds dp.NodataProblemSeconds = nodataSeconds
err = dp.Update() err = dp.Update()
if err != nil { if err != nil {
@ -598,7 +637,7 @@ func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) {
w.Header().Add("Location", "/datapoints") w.Header().Add("Location", "/datapoints")
w.WriteHeader(302) w.WriteHeader(302)
} // }}} } // }}}
func pageDatapointDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionDatapointDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id") idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
@ -674,7 +713,7 @@ func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page.Render(w) page.Render(w)
} // }}} } // }}}
func triggerCreate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionTriggerCreate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
name := r.PathValue("name") name := r.PathValue("name")
sectionIDStr := r.PathValue("sectionID") sectionIDStr := r.PathValue("sectionID")
sectionID, err := strconv.Atoi(sectionIDStr) sectionID, err := strconv.Atoi(sectionIDStr)
@ -761,7 +800,7 @@ func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { //
page.Render(w) page.Render(w)
} // }}} } // }}}
func pageTriggerDatapointAdd(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionTriggerDatapointAdd(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
triggerID := r.PathValue("id") triggerID := r.PathValue("id")
dpName := r.PathValue("datapointName") dpName := r.PathValue("datapointName")
@ -793,7 +832,7 @@ func pageTriggerDatapointAdd(w http.ResponseWriter, r *http.Request, _ *session.
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
w.Write(j) w.Write(j)
} // }}} } // }}}
func pageTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id") idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
@ -828,7 +867,7 @@ func pageTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { /
w.Header().Add("Location", "/triggers") w.Header().Add("Location", "/triggers")
w.WriteHeader(302) w.WriteHeader(302)
} // }}} } // }}}
func pageTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func actionTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id") idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {

View File

@ -16,7 +16,16 @@ func ServiceFactory(t string, config []byte, prio int, ackURL string, logger *sl
err = werr.Wrap(err).WithData(config) err = werr.Wrap(err).WithData(config)
return nil, err return nil, err
} }
ntfy.SetLogger(logger)
return ntfy, nil return ntfy, nil
case "SCRIPT":
script, err := NewScript(config, prio, ackURL)
if err != nil {
err = werr.Wrap(err).WithData(config)
return nil, err
}
script.SetLogger(logger)
return script, nil
} }
return nil, werr.New("Unknown notification service, '%s'", t).WithCode("002-0000") return nil, werr.New("Unknown notification service, '%s'", t).WithCode("002-0000")

70
notification/script.go Normal file
View File

@ -0,0 +1,70 @@
package notification
import (
// External
werr "git.gibonuddevalla.se/go/wrappederror"
// Standard
"encoding/json"
"log/slog"
"os/exec"
"strconv"
"strings"
)
type Script struct {
Filename string
Prio int
AcknowledgeURL string
logger *slog.Logger
}
func NewScript(config []byte, prio int, ackURL string) (instance *Script, err error) {
instance = new(Script)
err = json.Unmarshal(config, &instance)
if err != nil {
err = werr.Wrap(err).WithCode("002-0001").WithData(config)
return
}
instance.Prio = prio
instance.AcknowledgeURL = ackURL
return instance, nil
}
func (script *Script) SetLogger(l *slog.Logger) {
script.logger = l
}
func (script *Script) GetType() string {
return "SCRIPT"
}
func (script *Script) GetPrio() int {
return script.Prio
}
func (script Script) Send(problemID int, msg []byte) (err error) {
var errbuf strings.Builder
cmd := exec.Command(script.Filename, strconv.Itoa(problemID), script.AcknowledgeURL, string(msg))
cmd.Stderr = &errbuf
err = cmd.Run()
if err != nil {
script.logger.Error("notification", "type", "script", "error", err)
err = werr.Wrap(err).WithData(
struct {
Filename string
ProblemID int
Msg string
StdErr string
}{
script.Filename,
problemID,
string(msg),
errbuf.String(),
},
).Log()
}
return
}

View File

@ -1,6 +1,9 @@
package main package main
import ( import (
// External
werr "git.gibonuddevalla.se/go/wrappederror"
// Standard // Standard
"sort" "sort"
) )
@ -30,3 +33,15 @@ func SectionRename(id int, name string) (err error) {// {{{
_, err = service.Db.Conn.Exec(`UPDATE section SET name=$2 WHERE id=$1`, id, name) _, err = service.Db.Conn.Exec(`UPDATE section SET name=$2 WHERE id=$1`, id, name)
return return
} // }}} } // }}}
func SectionDelete(id int) (err error) { // {{{
_, err = service.Db.Conn.Exec(`DELETE FROM public.trigger WHERE section_id = $1`, id)
if err != nil {
return werr.Wrap(err).WithData(id)
}
_, err = service.Db.Conn.Exec(`DELETE FROM public.section WHERE id = $1`, id)
if err != nil {
return werr.Wrap(err).WithData(id)
}
return
} // }}}

9
sql/00016.sql Normal file
View File

@ -0,0 +1,9 @@
ALTER TYPE notification_type RENAME TO _notification_type;
CREATE TYPE notification_type AS ENUM ('NTFY', 'SCRIPT');
ALTER TABLE notification RENAME COLUMN service TO _service;
ALTER TABLE notification ADD service notification_type NOT NULL DEFAULT 'NTFY';
UPDATE notification SET service = _service::text::notification_type;
ALTER TABLE notification DROP COLUMN _service;
DROP TYPE _notification_type;

1
sql/00017.sql Normal file
View File

@ -0,0 +1 @@
ALTER TABLE public.datapoint ADD COLUMN comment VARCHAR DEFAULT '' NOT NULL;

View File

@ -0,0 +1,130 @@
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: sans-serif;
font-weight: 300;
color: #d5c4a1;
font-size: 11pt;
}
h1,
h2 {
margin-bottom: 4px;
}
h1:first-child,
h2:first-child {
margin-top: 0px;
}
h1 {
font-size: 1.5em;
color: #fb4934;
font-weight: 800;
}
h2 {
font-size: 1.25em;
color: #b8bb26;
font-weight: 800;
}
a {
color: #3f9da1;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
b {
font-weight: 800;
}
input[type="text"],
textarea,
select {
font-family: monospace;
background: #202020;
color: #d5c4a1;
padding: 4px 8px;
border: none;
font-size: 1em;
line-height: 1.5em;
}
button {
background: #202020;
color: #d5c4a1;
padding: 8px 32px;
border: 1px solid #535353;
font-size: 1em;
height: 3em;
}
button:focus {
background: #333;
}
.line {
grid-column: 1 / -1;
border-bottom: 1px solid #4e4e4e;
}
span.date {
color: #d5c4a1;
font-weight: 800;
}
span.time {
font-size: 0.9em;
color: #d5c4a1;
}
span.seconds {
display: none;
}
label {
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;
}
#areas .area > .name {
display: grid;
grid-template-columns: 1fr min-content;
grid-gap: 0px 16px;
align-items: center;
padding-left: 16px;
padding-right: 8px;
}
#areas .area > .name img {
margin-top: 3px;
margin-bottom: 4px;
height: 16px;
}
#areas .area .section.configuration {
display: grid;
grid-template-columns: 1fr min-content;
grid-gap: 0 16px;
margin-top: 8px;
margin-bottom: 8px;
}
#areas .area .section.configuration:last-child {
margin-bottom: 16px;
}
#areas .area .section.configuration img {
height: 16px;
}

View File

@ -125,12 +125,16 @@ label {
} }
#datapoints div { #datapoints div {
white-space: nowrap; white-space: nowrap;
align-self: center;
} }
#datapoints .icons { #datapoints .icons {
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
} }
#datapoints img.info {
height: 20px;
}
#values { #values {
display: grid; display: grid;
grid-template-columns: repeat(2, min-content); grid-template-columns: repeat(2, min-content);

View File

@ -182,10 +182,6 @@ label {
margin-top: 12px; margin-top: 12px;
margin-bottom: 20px; margin-bottom: 20px;
} }
#areas .area .section.configuration {
margin-top: 8px;
margin-bottom: 8px;
}
#areas .area .section:last-child { #areas .area .section:last-child {
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16.000025"
height="18"
viewBox="0 0 4.2333398 4.7625001"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="delete_white.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="32"
inkscape:cx="10.203125"
inkscape:cy="7.03125"
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(-214.57708,-116.15208)">
<title
id="title1">trash-can-outline</title>
<path
d="m 215.9,116.15208 v 0.26459 h -1.32292 v 0.52916 h 0.26459 v 3.43959 a 0.52916667,0.52916667 0 0 0 0.52916,0.52916 h 2.64584 a 0.52916667,0.52916667 0 0 0 0.52916,-0.52916 v -3.43959 h 0.26459 v -0.52916 h -1.32292 v -0.26459 H 215.9 m -0.52917,0.79375 h 2.64584 v 3.43959 h -2.64584 v -3.43959 M 215.9,117.475 v 2.38125 h 0.52917 V 117.475 H 215.9 m 1.05833,0 v 2.38125 h 0.52917 V 117.475 Z"
id="path1"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="17.999989"
height="18"
viewBox="0 0 4.7624969 4.7625001"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="info-filled.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="6.84375"
inkscape:cy="0.6875"
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.222917,-148.43125)">
<title
id="title1">information-slab-circle</title>
<title
id="title1-6">information</title>
<path
d="m 92.842292,150.09812 h -0.47625 v -0.47625 h 0.47625 m 0,2.38126 h -0.47625 v -1.42875 h 0.47625 m -0.238125,-2.14313 a 2.3812503,2.3812503 0 0 0 -2.38125,2.38125 2.3812503,2.3812503 0 0 0 2.38125,2.38124 2.3812503,2.3812503 0 0 0 2.381246,-2.38124 2.3812503,2.3812503 0 0 0 -2.381246,-2.38125 z"
id="path1-2"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#fb4934;fill-opacity:1;stroke-width:0.311724;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="17.999989"
height="18"
viewBox="0 0 4.7624969 4.7625001"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="info-outline.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="11.313708"
inkscape:cx="21.610951"
inkscape:cy="4.5961941"
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.222917,-148.43125)">
<title
id="title1">information-slab-circle</title>
<title
id="title1-6">information-slab-circle-outline</title>
<title
id="title1-5">information-outline</title>
<path
d="m 92.366045,150.09812 h 0.47625 v -0.47625 h -0.47625 m 0.238125,3.09564 c -1.050132,0 -1.905003,-0.85488 -1.905003,-1.90501 0,-1.05013 0.854871,-1.90501 1.905003,-1.90501 1.050133,0 1.905006,0.85488 1.905006,1.90501 0,1.05013 -0.854873,1.90501 -1.905006,1.90501 m 0,-4.28626 a 2.381253,2.381253 0 0 0 -2.381253,2.38125 2.381253,2.381253 0 0 0 2.381253,2.38125 2.381253,2.381253 0 0 0 2.381256,-2.38125 2.381253,2.381253 0 0 0 -2.381256,-2.38125 m -0.238125,3.57188 h 0.47625 v -1.42875 h -0.47625 z"
id="path1-3"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#666666;fill-opacity:1;stroke-width:0.238125;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,38 @@
@import 'theme.less';
#areas {
.area {
& > .name {
display: grid;
grid-template-columns: 1fr min-content;
grid-gap: 0px 16px;
align-items: center;
padding-left: 16px;
padding-right: 8px;
img {
margin-top: 3px;
margin-bottom: 4px;
height: 16px;
}
}
.section.configuration {
display: grid;
grid-template-columns: 1fr min-content;
grid-gap: 0 16px;
margin-top: 8px;
margin-bottom: 8px;
&:last-child {
margin-bottom: 16px;
}
img {
height: 16px;
}
}
}
}

View File

@ -13,6 +13,7 @@
margin-top: 1.5em; margin-top: 1.5em;
padding-bottom: 4px; padding-bottom: 4px;
} }
h2 { h2 {
border-bottom: unset; border-bottom: unset;
} }
@ -26,6 +27,7 @@
div { div {
white-space: nowrap; white-space: nowrap;
align-self: center;
} }
.icons { .icons {
@ -33,6 +35,10 @@
gap: 12px; gap: 12px;
align-items: center; align-items: center;
} }
img.info {
height: 20px;
}
} }
#values { #values {

View File

@ -94,11 +94,6 @@
margin-top: 12px; margin-top: 12px;
margin-bottom: 20px; margin-bottom: 20px;
&.configuration {
margin-top: 8px;
margin-bottom: 8px;
}
&:last-child { &:last-child {
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@ -1,4 +1,6 @@
{{ define "page" }} {{ define "page" }}
{{ $version := .VERSION }}
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/configuration.css">
<script type="text/javascript"> <script type="text/javascript">
function newArea() { function newArea() {
let name = prompt("Area name") let name = prompt("Area name")
@ -47,6 +49,18 @@
} }
location.href = `/section/rename/${id}/${newName.trim()}` location.href = `/section/rename/${id}/${newName.trim()}`
} }
function deleteArea(id, name) {
if (!confirm(`Are you sure you want to delete '${name}'?\nEverything in it will be deleted!`))
return
location.href = `/area/delete/${id}`
}
function deleteSection(id, name) {
if (!confirm(`Are you sure you want to delete '${name}'?\nEverything in it will be deleted!`))
return
location.href = `/section/delete/${id}`
}
</script> </script>
{{ block "page_label" . }}{{end}} {{ block "page_label" . }}{{end}}
@ -58,14 +72,21 @@
<div id="areas"> <div id="areas">
{{ range .Data.Areas }} {{ range .Data.Areas }}
<div class="area"> <div class="area">
<div class="name" onclick="renameArea({{ .ID }}, {{ .Name }})">{{ .Name }}</div> <div class="name">
<div onclick="renameArea({{ .ID }}, '{{ .Name }}')">{{ .Name }}</div>
<img class="delete" src="/images/{{ $version }}/delete_white.svg" onclick="deleteArea({{ .ID }}, '{{ .Name }}')">
</div>
<div style="margin: 8px 16px"> <div style="margin: 8px 16px">
<a href="#" onclick="newSection({{ .ID }})">Create</a> <a href="#" onclick="newSection({{ .ID }})">Create</a>
</div> </div>
{{ range .SortedSections }} {{ range .SortedSections }}
{{ if eq .ID 0 }}
{{ continue }}
{{ end }}
<div class="section configuration"> <div class="section configuration">
<div class="name" onclick="renameSection({{ .ID }}, {{ .Name }})">{{ .Name }}</div> <div class="name" onclick="renameSection({{ .ID }}, {{ .Name }})">{{ .Name }}</div>
<img src="/images/{{ $version }}/delete.svg" onclick="deleteSection({{ .ID }}, '{{ .Name }}')">
</div> </div>
{{ end }} {{ end }}
</div> </div>

View File

@ -32,6 +32,12 @@
<div class="description">Set to 0 to disable.</div> <div class="description">Set to 0 to disable.</div>
</div> </div>
<div class="label">Comment</div>
<div>
<textarea name="comment" rows=4>{{ .Data.Datapoint.Comment }}</textarea>
</div>
<div></div> <div></div>
<div class="action"> <div class="action">
{{ if eq .Data.Datapoint.ID 0 }} {{ if eq .Data.Datapoint.ID 0 }}

View File

@ -31,6 +31,11 @@
<div class="value">{{ .LastDatapointValue.Value }}</div> <div class="value">{{ .LastDatapointValue.Value }}</div>
{{ end }} {{ end }}
<div class="icons"> <div class="icons">
{{ if eq .Comment "" }}
<div class="values"><img class="info" src="/images/{{ $version }}/info-outline.svg"></div>
{{ else }}
<div class="values"><img class="info" src="/images/{{ $version }}/info-filled.svg" title="{{ .Comment }}"></div>
{{ end }}
<div class="values"><a href="/datapoint/values/{{ .ID }}"><img src="/images/{{ $version }}/values.svg"></a></div> <div class="values"><a href="/datapoint/values/{{ .ID }}"><img src="/images/{{ $version }}/values.svg"></a></div>
<div class="delete"><a href="/datapoint/delete/{{ .ID }}" onclick="confirm(`Are you sure you want to delete '{{ .Name }}'?`)"><img src="/images/{{ $version }}/delete.svg"></a></div> <div class="delete"><a href="/datapoint/delete/{{ .ID }}" onclick="confirm(`Are you sure you want to delete '{{ .Name }}'?`)"><img src="/images/{{ $version }}/delete.svg"></a></div>
</div> </div>

View File

@ -38,6 +38,9 @@
<div class="area"> <div class="area">
<div class="name">{{ .Name }}</div> <div class="name">{{ .Name }}</div>
{{ range .SortedSections }} {{ range .SortedSections }}
{{ if eq .ID 0 }}
{{ continue }}
{{ end }}
<div class="section"> <div class="section">
<div class="create"> <div class="create">
<div class="name">{{ .Name }}</div> <div class="name">{{ .Name }}</div>