Gibheer
80b7d7cbe4
Because Firefox has no attribute path for an event to represent the element chain of the target, we need to iterate through the parents.
818 lines
25 KiB
Go
818 lines
25 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"html"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
var (
|
|
configPath = flag.String("config", "monfront.conf", "path to the config file")
|
|
DB *sql.DB
|
|
Tmpl *template.Template
|
|
)
|
|
|
|
type (
|
|
Config struct {
|
|
DB string `json:"db"`
|
|
Listen string `json:"listen"`
|
|
}
|
|
|
|
Context struct {
|
|
Title string
|
|
CurrentPath string
|
|
Error string
|
|
Mappings map[int]map[int]MapEntry
|
|
Checks []check
|
|
CheckDetails *checkDetails
|
|
Groups []group
|
|
}
|
|
|
|
MapEntry struct {
|
|
Title string
|
|
Color string
|
|
}
|
|
|
|
check struct {
|
|
NodeId int
|
|
NodeName string
|
|
CommandName string
|
|
CheckID int64
|
|
MappingId int
|
|
State int
|
|
Enabled bool
|
|
Notify bool
|
|
Notice sql.NullString
|
|
NextTime time.Time
|
|
Msg string
|
|
}
|
|
|
|
checkDetails struct {
|
|
Id int64
|
|
Message string
|
|
Enabled bool
|
|
Updated time.Time
|
|
LastRefresh time.Time
|
|
NextTime time.Time
|
|
MappingId int
|
|
MappingName string
|
|
NodeId int
|
|
NodeName string
|
|
NodeMessage string
|
|
CommandId int
|
|
CommandName string
|
|
CommandLine []string
|
|
CommandMessage string
|
|
States []int64
|
|
Notice sql.NullString
|
|
Notifiers []notifier
|
|
Notifications []notification
|
|
}
|
|
|
|
notifier struct {
|
|
Id int
|
|
Name string
|
|
Enabled bool
|
|
}
|
|
|
|
notification struct {
|
|
Id int64
|
|
State int
|
|
Output string
|
|
Inserted time.Time
|
|
Sent pq.NullTime
|
|
NotifierName string
|
|
MappingId int
|
|
}
|
|
|
|
group struct {
|
|
GroupId int
|
|
Name string
|
|
NodeId int
|
|
NodeName string
|
|
State int
|
|
MappingId int
|
|
}
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
raw, err := ioutil.ReadFile(*configPath)
|
|
if err != nil {
|
|
log.Fatalf("could not read config: %s", err)
|
|
}
|
|
config := Config{}
|
|
if err := json.Unmarshal(raw, &config); err != nil {
|
|
log.Fatalf("could not parse config: %s", err)
|
|
}
|
|
|
|
db, err := sql.Open("postgres", config.DB)
|
|
if err != nil {
|
|
log.Fatalf("could not open database connection: %s", err)
|
|
}
|
|
DB = db
|
|
|
|
tmpl := template.New("main")
|
|
tmpl.Funcs(Funcs)
|
|
for k, val := range Templates {
|
|
template.Must(tmpl.New(k).Parse(val))
|
|
}
|
|
Tmpl = tmpl
|
|
|
|
http.HandleFunc("/", showChecks)
|
|
http.HandleFunc("/static/", showStatic)
|
|
http.HandleFunc("/check", showCheck)
|
|
http.HandleFunc("/checks", showChecks)
|
|
http.HandleFunc("/groups", showGroups)
|
|
http.HandleFunc("/action", checkAction)
|
|
http.HandleFunc("/unhandled/checks", showChecks)
|
|
http.HandleFunc("/unhandled/groups", showGroups)
|
|
http.ListenAndServe(config.Listen, nil)
|
|
}
|
|
|
|
func checkAction(w http.ResponseWriter, r *http.Request) {
|
|
con := Context{}
|
|
if r.Method != "POST" {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
w.Write([]byte("method is not supported"))
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintf(w, "could not parse parameters: %s", err)
|
|
return
|
|
}
|
|
checks := r.PostForm["checks"]
|
|
action := r.PostForm["action"]
|
|
if len(action) == 0 || action[0] == "" || len(checks) == 0 {
|
|
w.Header()["Location"] = []string{"/"}
|
|
w.WriteHeader(http.StatusSeeOther)
|
|
return
|
|
}
|
|
setTable := "checks"
|
|
setClause := ""
|
|
switch action[0] {
|
|
case "mute":
|
|
setTable = "checks_notify"
|
|
setClause = "enabled = false"
|
|
case "unmute":
|
|
setTable = "checks_notify"
|
|
setClause = "enabled = true"
|
|
case "enable":
|
|
setClause = "enabled = true, updated = now()"
|
|
case "disable":
|
|
setClause = "enabled = false, updated = now()"
|
|
case "reschedule":
|
|
setClause = "next_time = now()"
|
|
setTable = "active_checks"
|
|
case "ack":
|
|
setClause = "acknowledged = true"
|
|
setTable = "active_checks"
|
|
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
log.Printf("could not resolve hostname: %s", err)
|
|
con.Error = "could not resolve hostname"
|
|
returnError(http.StatusInternalServerError, con, w)
|
|
return
|
|
}
|
|
if _, err := DB.Exec(`insert into notifications(check_id, states, output, mapping_id, notifier_id, check_host)
|
|
select ac.check_id, 0 || states[1:array_length(states, 1)], 'check acknowledged', ac.mapping_id,
|
|
cn.notifier_id, $2
|
|
from checks_notify cn
|
|
join active_checks ac on cn.check_id = ac.check_id
|
|
where cn.check_id = any ($1::int[])`, pq.Array(&checks), &hostname); err != nil {
|
|
log.Printf("could not acknowledge check: %s", err)
|
|
con.Error = "could not acknowledge check"
|
|
returnError(http.StatusInternalServerError, con, w)
|
|
return
|
|
}
|
|
case "comment":
|
|
if len(r.PostForm["comment"]) == 0 {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintf(w, "no comment sent")
|
|
return
|
|
}
|
|
comment := r.PostForm["comment"][0]
|
|
_, err := DB.Exec(
|
|
"update active_checks set notice = $2 where check_id = any ($1::int[]);",
|
|
pq.Array(&checks),
|
|
html.EscapeString(comment))
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprintf(w, "could not store changes")
|
|
log.Printf("could not adjust checks %#v: %s", checks, err)
|
|
return
|
|
}
|
|
w.Header()["Location"] = []string{"/"}
|
|
w.WriteHeader(http.StatusSeeOther)
|
|
return
|
|
default:
|
|
con.Error = fmt.Sprintf("requested action '%s' does not exist", action[0])
|
|
returnError(http.StatusNotFound, con, w)
|
|
return
|
|
}
|
|
whereColumn := "id"
|
|
if setTable == "active_checks" || setTable == "checks_notify" {
|
|
whereColumn = "check_id"
|
|
}
|
|
|
|
_, err := DB.Exec("update "+setTable+" set "+setClause+" where "+whereColumn+" = any ($1::int[]);", pq.Array(&checks))
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
fmt.Fprintf(w, "could not store changes")
|
|
log.Printf("could not adjust checks %#v: %s", checks, err)
|
|
return
|
|
}
|
|
ref, found := r.Header["Referer"]
|
|
if found {
|
|
w.Header()["Location"] = ref
|
|
} else {
|
|
w.Header()["Location"] = []string{"/"}
|
|
}
|
|
w.WriteHeader(http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
func showChecks(w http.ResponseWriter, r *http.Request) {
|
|
query := `select c.id, n.id, n.name, co.name, ac.mapping_id, ac.states[1] as state,
|
|
ac.enabled, ac.notice, ac.next_time, ac.msg,
|
|
case when cn.check_id is null then false else true end as notify_enabled
|
|
from active_checks ac
|
|
join checks c on ac.check_id = c.id
|
|
join nodes n on c.node_id = n.id
|
|
join commands co on c.command_id = co.id
|
|
left join ( select distinct check_id from checks_notify where enabled = true) cn on c.id = cn.check_id`
|
|
where := []string{}
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
where = append(where, `ac.states[1] > 0 and ac.acknowledged = false and ac.enabled = true`)
|
|
}
|
|
idx := 0
|
|
params := []interface{}{}
|
|
if search, found := r.URL.Query()["search"]; found {
|
|
idx += 1
|
|
// Add the search for nodes. As hostnames or FQDNs are really weird, the
|
|
// string needs to be split up by some characters. The input string needs
|
|
// to be split up too, so all is done here.
|
|
// TODO move this into a proper index and add more to search.
|
|
where = append(where, fmt.Sprintf(
|
|
`to_tsvector('english', regexp_replace(n.name, '[.-/]', ' ', 'g')) @@
|
|
to_tsquery('english', regexp_replace($%d, '[.-/]', ' & ', 'g'))`, idx))
|
|
params = append(params, search[0])
|
|
}
|
|
if id, found := r.URL.Query()["node_id"]; found {
|
|
idx += 1
|
|
where = append(where, fmt.Sprintf("n.id = $%d::int", idx))
|
|
params = append(params, id[0])
|
|
}
|
|
if id, found := r.URL.Query()["command_id"]; found {
|
|
idx += 1
|
|
where = append(where, fmt.Sprintf("co.id = $%d::int", idx))
|
|
params = append(params, id[0])
|
|
}
|
|
if id, found := r.URL.Query()["check_id"]; found {
|
|
idx += 1
|
|
where = append(where, fmt.Sprintf("c.id = $%d::int", idx))
|
|
params = append(params, id[0])
|
|
}
|
|
if len(where) > 0 {
|
|
query += " where " + strings.Join(where, " and ")
|
|
}
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
query += ` order by ac.states[1] desc, n.name, co.name`
|
|
} else {
|
|
query += ` order by n.name, co.name`
|
|
}
|
|
rows, err := DB.Query(query, params...)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problems with the database"))
|
|
log.Printf("could not get check list: %s", err)
|
|
return
|
|
}
|
|
|
|
checks := []check{}
|
|
for rows.Next() {
|
|
c := check{}
|
|
err := rows.Scan(&c.CheckID, &c.NodeId, &c.NodeName, &c.CommandName, &c.MappingId, &c.State, &c.Enabled, &c.Notice, &c.NextTime, &c.Msg, &c.Notify)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problems with the database"))
|
|
log.Printf("could not get check list: %s", err)
|
|
return
|
|
}
|
|
checks = append(checks, c)
|
|
}
|
|
con := Context{
|
|
Checks: checks,
|
|
}
|
|
if err := loadMappings(&con); err != nil {
|
|
con.Error = "could not load mapping data"
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problem with the mappings"))
|
|
log.Printf("could not load mappings: %s", err)
|
|
return
|
|
}
|
|
w.Header()["Content-Type"] = []string{"text/html"}
|
|
if err := Tmpl.ExecuteTemplate(w, "checklist", con); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problem with a template"))
|
|
log.Printf("could not execute template: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func showGroups(w http.ResponseWriter, r *http.Request) {
|
|
query := `select groupid, groupname, nodeid, nodename, mapping_id, state
|
|
from (
|
|
select g.id groupid, g.name groupname, n.id nodeid, n.name nodename, ac.mapping_id,
|
|
ac.states[1] state, max(ac.states[1]) over (partition by c.node_id) maxstate
|
|
from groups g
|
|
join nodes_groups ng on g.id = ng.group_id
|
|
join nodes n on ng.node_id = n.id
|
|
join checks c on n.id = c.node_id
|
|
join active_checks ac on c.id = ac.check_id
|
|
join mapping_level ml on ac.mapping_id = ml.mapping_id and ac.states[1] = ml.target
|
|
) s
|
|
where state = maxstate`
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
query += ` and state > 0`
|
|
}
|
|
|
|
if strings.HasPrefix(r.URL.Path, "/unhandled") {
|
|
query += ` order by state desc, groupname, nodename`
|
|
} else {
|
|
query += ` order by groupname, nodename`
|
|
}
|
|
|
|
rows, err := DB.Query(query)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problems with the database"))
|
|
log.Printf("could not get check list: %s", err)
|
|
return
|
|
}
|
|
|
|
groups := []group{}
|
|
for rows.Next() {
|
|
g := group{}
|
|
err := rows.Scan(&g.GroupId, &g.Name, &g.NodeId, &g.NodeName, &g.MappingId, &g.State)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problems with the database"))
|
|
log.Printf("could not get check list: %s", err)
|
|
return
|
|
}
|
|
groups = append(groups, g)
|
|
}
|
|
con := Context{
|
|
Groups: groups,
|
|
}
|
|
if err := loadMappings(&con); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problem with the mappings"))
|
|
log.Printf("could not load mappings: %s", err)
|
|
return
|
|
}
|
|
w.Header()["Content-Type"] = []string{"text/html"}
|
|
if err := Tmpl.ExecuteTemplate(w, "grouplist", con); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problem with a template"))
|
|
log.Printf("could not execute template: %s", err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// showCheck loads shows the notifications for a specific check.
|
|
func showCheck(w http.ResponseWriter, r *http.Request) {
|
|
cd := checkDetails{}
|
|
con := Context{CheckDetails: &cd}
|
|
id, found := r.URL.Query()["check_id"]
|
|
if !found {
|
|
con.Error = "no check given to view"
|
|
returnError(http.StatusNotFound, con, w)
|
|
return
|
|
}
|
|
query := `select c.id, c.message, c.enabled, c.updated, c.last_refresh,
|
|
m.id, m.name, n.id, n.name, n.message, co.id, co.Name, co.message,
|
|
ac.cmdline, ac.states, ac.msg, ac.next_time
|
|
from checks c
|
|
join active_checks ac on c.id = ac.check_id
|
|
join nodes n on c.node_id = n.id
|
|
join commands co on c.command_id = co.id
|
|
join mappings m on ac.mapping_id = m.id
|
|
where c.id = $1::bigint`
|
|
err := DB.QueryRow(query, id[0]).Scan(&cd.Id, &cd.Message, &cd.Enabled, &cd.Updated, &cd.LastRefresh,
|
|
&cd.MappingId, &cd.MappingName, &cd.NodeId, &cd.NodeName, &cd.NodeMessage,
|
|
&cd.CommandId, &cd.CommandName, &cd.CommandMessage,
|
|
pq.Array(&cd.CommandLine), pq.Array(&cd.States), &cd.Notice, &cd.NextTime)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problems with the database"))
|
|
log.Printf("could not get check details for check id %s: %s", id[0], err)
|
|
return
|
|
}
|
|
|
|
query = `select n.id, states[1], output, inserted, sent, no.name, n.mapping_id
|
|
from notifications n
|
|
join notifier no on n.notifier_id = no.id
|
|
where check_id = $1::bigint
|
|
order by inserted desc
|
|
limit 500`
|
|
rows, err := DB.Query(query, cd.Id)
|
|
if err != nil {
|
|
log.Printf("could not load notifications: %s", err)
|
|
con.Error = "could not load notification information"
|
|
returnError(http.StatusInternalServerError, con, w)
|
|
return
|
|
}
|
|
cd.Notifications = []notification{}
|
|
for rows.Next() {
|
|
if err := rows.Err(); err != nil {
|
|
log.Printf("could not load notifications: %s", err)
|
|
con.Error = "could not load notification information"
|
|
returnError(http.StatusInternalServerError, con, w)
|
|
return
|
|
}
|
|
no := notification{}
|
|
if err := rows.Scan(&no.Id, &no.State, &no.Output, &no.Inserted,
|
|
&no.Sent, &no.NotifierName, &no.MappingId); err != nil {
|
|
log.Printf("could not scan notifications: %s", err)
|
|
con.Error = "could not load notification information"
|
|
returnError(http.StatusInternalServerError, con, w)
|
|
return
|
|
}
|
|
cd.Notifications = append(cd.Notifications, no)
|
|
}
|
|
|
|
if err := loadMappings(&con); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problem with the mappings"))
|
|
log.Printf("could not load mappings: %s", err)
|
|
return
|
|
}
|
|
|
|
w.Header()["Content-Type"] = []string{"text/html"}
|
|
if err := Tmpl.ExecuteTemplate(w, "check", con); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problem with a template"))
|
|
log.Printf("could not execute template: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func returnError(status int, con Context, w http.ResponseWriter) {
|
|
w.Header()["Content-Type"] = []string{"text/html"}
|
|
w.WriteHeader(status)
|
|
if err := Tmpl.ExecuteTemplate(w, "error", con); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("problem with a template"))
|
|
log.Printf("could not execute template: %s", err)
|
|
}
|
|
|
|
}
|
|
|
|
func loadMappings(c *Context) error {
|
|
c.Mappings = map[int]map[int]MapEntry{}
|
|
rows, err := DB.Query(SQLShowMappings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for rows.Next() {
|
|
if rows.Err() != nil {
|
|
return rows.Err()
|
|
}
|
|
var (
|
|
mapId int
|
|
target int
|
|
title string
|
|
color string
|
|
)
|
|
if err := rows.Scan(&mapId, &target, &title, &color); err != nil {
|
|
return err
|
|
}
|
|
ma, found := c.Mappings[mapId]
|
|
if !found {
|
|
ma = map[int]MapEntry{}
|
|
c.Mappings[mapId] = ma
|
|
}
|
|
ma[target] = MapEntry{Title: title, Color: color}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func showStatic(w http.ResponseWriter, r *http.Request) {
|
|
file := strings.TrimPrefix(r.URL.Path, "/static/")
|
|
raw, found := Static[file]
|
|
if !found {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Write([]byte("file does not exist"))
|
|
return
|
|
}
|
|
w.Header()["Content-Type"] = []string{"image/svg+xml"}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(raw))
|
|
return
|
|
}
|
|
|
|
var (
|
|
SQLShowMappings = `select mapping_id, target, title, color
|
|
from mapping_level`
|
|
)
|
|
|
|
var (
|
|
Templates = map[string]string{
|
|
"header": `<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>{{ .Title }}</title>
|
|
<meta name="referrer" content="same-origin">
|
|
<link rel="shortcut icon" href="/static/favicon" />
|
|
<style type="text/css">
|
|
* { font-size: 100%; }
|
|
body { display: flex; flex-direction: column; padding: 0; margin: 0; }
|
|
#mainmenu { background: #3a5f78; }
|
|
#mainmenu ul {
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: stretch;
|
|
align-content: center; }
|
|
#mainmenu .submenu { border-left: 0.1em solid black; }
|
|
#mainmenu li { list-style-type: none; }
|
|
.submenu .header {
|
|
text-align: center;
|
|
font-weight: bold;
|
|
color: #ff9000;
|
|
padding: 0.5em 0.5em;
|
|
display: block; }
|
|
#mainmenu a, #mainmenu a:visited, #mainmenu a:active, #mainmenu a:hover {
|
|
color: #ff9000;
|
|
padding: 0.25em 0.5em;
|
|
display: block; }
|
|
#mainmenu a:hover, #mainmenu a:active { color: #eeeeee; }
|
|
#mainmenu ul ul a { margin-left: 0.5em; }
|
|
#mainmenu form * { display: block; margin: 0.25em 0.5em; }
|
|
form section {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
align-items: flex-start;
|
|
}
|
|
form nav { order: 1; }
|
|
form content { order: 2; flex-grow: 1; border-left: 0.15em solid #dddddd; }
|
|
form nav { display: flex; flex-direction: column; }
|
|
form nav > * { margin: 0.5em; }
|
|
table { width: 100%; }
|
|
table tr:nth-child(odd) { background: #eeeeee; }
|
|
table tr.selected:nth-child(odd) { background: #afbfd4; }
|
|
table tr.selected:nth-child(even) { background: #cddbec; }
|
|
table tr:hover { background: #dfdfdf; }
|
|
table th { background: #cccccc; color: #3a5f78; }
|
|
table td, table th { text-align: center; }
|
|
table code { font-size: 75%; }
|
|
table td.disabled { text-decoration: line-through; }
|
|
.icon {
|
|
display: inline-block;
|
|
height: 1em;
|
|
margin: 0;
|
|
width: 1em;
|
|
vertical-align: bottom;
|
|
margin-right: 0.5em;
|
|
background-size: contain;
|
|
}
|
|
.mute { background-image: url(/static/icon-mute); }
|
|
.detail > div { display: grid; grid-template-columns: 25% auto; }
|
|
.detail > div:hover { background: #dfdfdf; }
|
|
/* state background colors */
|
|
{{ range $mapId, $mapping := .Mappings -}}
|
|
{{ range $target, $val := $mapping -}}
|
|
.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; }
|
|
{{ end -}}
|
|
{{ end -}}
|
|
</style>
|
|
<script>
|
|
setTimeout(function() { if (document.activeElement.tagName == "BODY") { location.reload(true) } }, 30000)
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<nav id="mainmenu">
|
|
<ul>
|
|
<li><a href="/">home</a></li>
|
|
<li class="submenu">
|
|
<span class="header">all</span>
|
|
<ul>
|
|
<li><a href="/checks">checks</a></li>
|
|
<li><a href="/groups">groups</a></li>
|
|
</ul>
|
|
</li>
|
|
<li class="submenu">
|
|
<span class="header">unhandled</span>
|
|
<ul>
|
|
<li><a href="/unhandled/checks">checks</a></li>
|
|
<li><a href="/unhandled/groups">groups</a></li>
|
|
</ul>
|
|
</li>
|
|
<li class="submenu">
|
|
<form action="/checks" method="get">
|
|
<input name="search" placeholder="search" />
|
|
<button type="submit">search</button>
|
|
</form>
|
|
</li>
|
|
<li class="submenu"><span class="header">{{ now.Format "2006.01.02 15:04:05" }}</span></li>
|
|
</ul>
|
|
</nav>
|
|
{{ if .Error }}<div class="error">{{ .Error }}</div>{{ end }}`,
|
|
"footer": `<script>
|
|
function select_row(event) {
|
|
if (event.target.nodeName == 'INPUT') {
|
|
return;
|
|
}
|
|
current = event.target;
|
|
while (current = current.parentNode) {
|
|
if (current,nodeName == 'BODY') {
|
|
break;
|
|
}
|
|
if (current.nodeName != 'TR') {
|
|
continue;
|
|
}
|
|
e = current.children[0].children[0];
|
|
if (e != event.target) {
|
|
e.checked = !e.checked;
|
|
}
|
|
if (e.checked) {
|
|
current.classList.add("selected");
|
|
} else {
|
|
current.classList.remove("selected");
|
|
}
|
|
e.focus();
|
|
break;
|
|
}
|
|
}
|
|
|
|
els = document.getElementsByTagName('tr');
|
|
for (i = 0; i < els.length; i++) {
|
|
els[i].addEventListener('click', {handleEvent: select_row});
|
|
}</script></body></html>`,
|
|
"checklist": `{{ template "header" . }}
|
|
<form method="post" action="/action">
|
|
<section>
|
|
<nav>
|
|
<div class="option">
|
|
<label for="action">Action</label>
|
|
<select name="action">
|
|
<option value="reschedule">run now</option>
|
|
<option value="mute">mute</option>
|
|
<option value="unmute">unmute</option>
|
|
<option value="ack">acknowledge</option>
|
|
<option value="disable">disable</option>
|
|
<option value="enable">enable</option>
|
|
<option value="comment">comment</option>
|
|
</select>
|
|
</div>
|
|
<div class="option">
|
|
<label for="comment">comment</label>
|
|
<input name="comment" />
|
|
</div>
|
|
<button type="submit">submit</button>
|
|
</nav>
|
|
<content>
|
|
<table>
|
|
<thead><tr><th></th><th>host</th><th>status</th><th>next check</th><th>message</th></tr></thead>
|
|
<tbody>
|
|
{{ $current := "" }}
|
|
{{ $mapping := .Mappings }}
|
|
{{ range .Checks }}
|
|
<tr>
|
|
<td><input type="checkbox" name="checks" value="{{ .CheckID }}" /></td>
|
|
<td>{{ if ne $current .NodeName }}{{ $current = .NodeName }}<a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a>{{ end }}</td>
|
|
<td class="state-{{ .MappingId }}-{{ .State }}">{{ if ne .Notify true }}<span class="icon mute"></span>{{ end }}<a href="/check?check_id={{ .CheckID }}">{{ .CommandName }}</a> - {{ (index $mapping .MappingId .State).Title }}</td>
|
|
<td {{ if ne .Enabled true }}title="This check is disabled." class="disabled"{{ end }}>{{ .NextTime.Format "2006.01.02 15:04:05" }} - in {{ in .NextTime }}</td>
|
|
<td><code>{{ .Msg }}</code></td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</content>
|
|
</form>
|
|
{{ template "footer" . }}`,
|
|
"checkformheader": `<form method="post" action="/action">
|
|
<section>
|
|
<nav>
|
|
<div class="option">
|
|
<label for="action">Action</label>
|
|
<select name="action">
|
|
<option value="reschedule">run now</option>
|
|
<option value="mute">mute</option>
|
|
<option value="unmute">unmute</option>
|
|
<option value="ack">acknowledge</option>
|
|
<option value="disable">disable</option>
|
|
<option value="enable">enable</option>
|
|
<option value="comment">comment</option>
|
|
</select>
|
|
</div>
|
|
<div class="option">
|
|
<label for="comment">comment</label>
|
|
<input name="comment" />
|
|
</div>
|
|
<button type="submit">submit</button>
|
|
</nav>`,
|
|
"checkformfooter": `</section></form>`,
|
|
"grouplist": `{{ template "header" . }}
|
|
{{ template "checkformheader" . }}
|
|
<content>
|
|
<table>
|
|
<thead><tr><th></th><th>group</th><th>host</th><th>worst state</th></tr></thead>
|
|
<tbody>
|
|
{{ $current := "" }}
|
|
{{ $mapping := .Mappings }}
|
|
{{ range .Groups }}
|
|
<tr>
|
|
<td><input type="checkbox" name="nodes" value="{{ .NodeId }}" /></td>
|
|
<td>{{ if ne $current .Name }}{{ $current = .Name }}{{ .Name }}{{ end }}</td>
|
|
<td><a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a></td>
|
|
<td class="state-{{ .MappingId }}-{{ .State }}">{{ (index $mapping .MappingId .State).Title }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</content>
|
|
{{ template "checkformfooter" . }}
|
|
{{ template "footer" . }}`,
|
|
"check": `{{ template "header" . }}
|
|
{{ template "checkformheader" . }}
|
|
<content class="details">
|
|
{{ $mapping := .Mappings }}
|
|
{{ with .CheckDetails }}
|
|
<input type="hidden" name="check_id" value="{{ .Id }}" />
|
|
<article class="detail">
|
|
<h1>check</h1>
|
|
<div><span class="label">current state</span><span class="value state-{{ .MappingId }}-{{ index .States 0 }}">{{ (index $mapping .MappingId (index .States 0) ).Title }}</span></div>
|
|
<div><span class="label">current notice</span><span class="value">{{ if .Notice }}{{ .Notice.String }}{{ end }}</span></div>
|
|
<div><span class="label">Message</span><span class="value">{{ .Message }}</span></div>
|
|
<div><span class="label">enabled</span><span class="value">{{ .Enabled }}</span></div>
|
|
<div><span class="label">updated</span><span class="value">{{ .Updated.Format "2006.01.02 15:04:05" }}</span></div>
|
|
<div><span class="label">next check</span><span class="value">{{ .NextTime.Format "2006.01.02 15:04:05" }}</span></div>
|
|
<div><span class="label">last refresh</span><span class="value">{{ .LastRefresh.Format "2006.01.02 15:04:05" }}</span></div>
|
|
<div><span class="label">mapping</span><span class="value">{{ .MappingId }}</span></div>
|
|
</article>
|
|
<article class="detail">
|
|
<h1>node <a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a></h1>
|
|
<div><span class="label">Message</span><span class="value">{{ .NodeMessage }}</span></div>
|
|
</article>
|
|
<article class="detail">
|
|
<h1>command {{ .CommandName }}</h1>
|
|
<div><span class="label">Message</span><span class="value">{{ .CommandMessage }}</span></div>
|
|
<div><span class="label">command line</span><span class="value"><code>{{ join .CommandLine " " }}</code></span></div>
|
|
</article>
|
|
<article>
|
|
<h1>notifications</h1>
|
|
<table>
|
|
<thead><tr><th>notifier</th><th>state</th><th>created</th><th>sent</th><th>output</th></thead>
|
|
<tbody>
|
|
{{ range .Notifications -}}
|
|
<tr>
|
|
<td>{{ .NotifierName }}</td>
|
|
<td class="state-{{ .MappingId }}-{{ .State }}">{{ (index $mapping .MappingId .State).Title }}</td>
|
|
<td>{{ .Inserted.Format "2006.01.02 15:04:05" }}</td>
|
|
<td>{{ if .Sent.Valid }}{{ .Sent.Time.Format "2006.01.02 15:04:05" }}{{ end }}</td>
|
|
<td>{{ .Output }}</td>
|
|
</tr>
|
|
{{ end -}}
|
|
</tbody>
|
|
</table>
|
|
</article>
|
|
{{ end }}
|
|
</content>
|
|
{{ template "checkformfooter" . }}
|
|
{{ template "footer" . }}`,
|
|
}
|
|
Static = map[string]string{
|
|
"icon-mute": `<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35.3 35.3" version="1.1"><title>Check is muted</title><style>.s0{fill:#191919;}</style><g transform="translate(0,-261.72223)"><path d="m17.6 261.7v35.3L5.3 284.7H0v-10.6l5.3 0zM30.2 273.1l-3.7 3.7-3.7-3.7-2.5 2.5 3.7 3.7-3.7 3.7 2.5 2.5 3.7-3.7 3.7 3.7 2.5-2.5-3.7-3.7 3.7-3.7z" fill="#191919"/></g></svg>`,
|
|
"error": `{{ template "header" . }}{{ template "footer" . }}`,
|
|
}
|
|
TmplUnhandledGroups = `TODO`
|
|
Funcs = template.FuncMap{
|
|
"int": func(in int64) int { return int(in) },
|
|
"sub": func(base, amount int) int { return base - amount },
|
|
"in": func(t time.Time) time.Duration { return t.Sub(time.Now()).Round(1 * time.Second) },
|
|
"now": func() time.Time { return time.Now() },
|
|
"join": func(args []string, c string) string { return strings.Join(args, c) },
|
|
}
|
|
)
|