diff options
Diffstat (limited to 'cmd/monfront/main.go')
-rw-r--r-- | cmd/monfront/main.go | 874 |
1 files changed, 0 insertions, 874 deletions
diff --git a/cmd/monfront/main.go b/cmd/monfront/main.go deleted file mode 100644 index 57904a5..0000000 --- a/cmd/monfront/main.go +++ /dev/null @@ -1,874 +0,0 @@ -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 - Unhandled bool // set this flag when unhandled was called - } - - 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)) - } - for k, val := range AdminTemplates { - template.Must(tmpl.New("admin-" + k).Parse(val)) - } - Tmpl = tmpl - - http.HandleFunc("/", showChecks) - http.HandleFunc("/admin/", showAdmin) - http.HandleFunc("/admin/action", adminAction) - 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) { - con := Context{} - 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{} - idx := 0 - params := []interface{}{} - if id, found := r.URL.Query()["group_id"]; found { - query += ` join nodes_groups ng on n.id = ng.node_id` - idx += 1 - where = append(where, fmt.Sprintf(`ng.group_id = $%d::int`, idx)) - params = append(params, id[0]) - } - if strings.HasPrefix(r.URL.Path, "/unhandled") { - where = append(where, `ac.states[1] > 0 and ac.acknowledged = false and ac.enabled = true`) - con.Unhandled = true - } - 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.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) { - con := Context{} - query := `select - group_id, - group_name, - node_id, - node_name, - mapping_id, - state -from ( - select - g.id group_id, - g.name group_name, - n.id node_id, - n.name node_name, - ac.states[1] state, - ac.mapping_id, - ac.acknowledged, - row_number() over (partition by c.node_id order by ac.states[1] desc) 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 - %s - order by g.name, n.name -) groups -where maxstate = 1` - if strings.HasPrefix(r.URL.Path, "/unhandled") { - query = fmt.Sprintf(query, `where ac.states[1] != 0 and acknowledged = false`) - con.Unhandled = true - } else { - query = fmt.Sprintf(query, "") - } - - 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.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 interface{}, 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; } - .error { padding: 0.5em; background: #ffc6c6; border: 1px solid red; } - /* 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 class="submenu"> - <span class="header">main</span> - <ul> - <li><a href="/">home</a></li> - <li><a href="/admin">admin</a></li> - </ul> - </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 }}<a href="{{ if $.Unhandled }}/unhandled{{ end }}/checks?group_id={{ .GroupId }}">{{ .Name }}</a>{{ 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) }, - } -) |