monzero/cmd/monfront/main.go
Gibheer c9a990513a monfront - final fix for select all
There was an issue with the select all in that clicking an input field
didn't trigger the marking as the event was not pushed through to the
underlying elements.
2019-01-24 18:09:20 +01:00

846 lines
26 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
}
ref, found := r.Header["Referer"]
if found {
w.Header()["Location"] = ref
} else {
w.Header()["Location"] = []string{"/"}
}
checks := r.PostForm["checks"]
action := r.PostForm["action"]
if len(action) == 0 || action[0] == "" || len(checks) == 0 {
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.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
}
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 row_head_click_event(event) {
check = false;
current = event.target;
while (current != null) {
if (current.nodeName == 'TABLE') {
break;
}
if (current.nodeName == 'TR') {
check = !current.children[0].children[0].checked;
current.children[0].children[0].checked = check;
}
current = current.parentNode;
}
lines = current.children[1].children
for (i = 0; i < lines.length; i++) {
select_row(event, lines[i], lines[i].children[0].children[0], check);
}
}
function row_click_event(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];
check = !e.checked;
select_row(event, current, e, check);
break;
}
}
function select_row(event, row, input, check) {
if (input != event.target) {
input.checked = check;
}
if (input.checked) {
row.classList.add("selected");
} else {
row.classList.remove("selected");
}
input.focus();
}
for (selector of ['thead > tr', 'thead input']) {
els = document.querySelectorAll(selector);
for (i = 0; i < els.length; i++) {
els[i].addEventListener('click', {handleEvent: row_head_click_event});
}
}
for (selector of ['tbody > tr', 'tbody input']) {
els = document.querySelectorAll(selector);
for (i = 0; i < els.length; i++) {
els[i].addEventListener('click', {handleEvent: row_click_event});
}
}</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><input type="checkbox" title="select all" /></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="checks" 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) },
}
)