aboutsummaryrefslogblamecommitdiff
path: root/cmd/monfront/main.go
blob: 1fccb95c73d4966745ae74e9394a9498697fb5b3 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12











                       
            
                 







                                                                                      
                                     






                                             

                        






                                                     







                            
                               




                                  
                                
                                



                                          
 




























                                             






                                        

         







                                



















                                                                         






                                                     
                                        
                                               
                                            
                                              
                                              
                                               
                                                        
                                                        



                                                          
                        




















                                                                     

                                             
                      

                                            
                      
                                                             
                       
                                                              





                                                 


















                                                                                                                              




















                                                                                                 

                                                                                          


                           
                                                                       









                                                                                                                              
                                         




                                                      




                                                         
                                                                                           

                                                                                


                                           

                                                                                                               
                           
                                                        
                                                                                                           
         

                                 










                                                                                               

















                                                                           




                                                                       
                                               






                                                               


                            
                                                                                                                                                                   







                                                                       



                                                  
                                                         




                                                              
                                                          
                                                                         







                                                                 















                                                                                                   






                                                                    
                                    






                                                               
                           
                         

                                                                                                     





                                                                       
                                          
         



                                                  
                                                             

                                                              


                                                          
                                                                         







                                                                 











                                                                                  
                                                           








                                                                                                             
                                                                                          






                                                                                         






























                                                                                      















                                                                     











                                                                     





























                                                                                  













                                                              
     

                                                                  


     
                                      
                                          
      

                                           
                                                            
                                                                   

                                              






















                                                                                                 





                                                        
                                              
                                                                                                   

                                                                           

                                                                        

                                                                                  


                                                                         
                                                      
                                                                            








                                                         

                                                                                         
                                                                    





                                                                                           
                        
                        
                                                                                                                                 
                         

               


                                                             



















                                                                                               
                                                                                                                           
                             

                                                                               










                                                                                  




                                                                                           








                                                                                            
                                                       

                                                     
                                     
















                                                                                        
                                      



                                                                                                                                                
                                                                    

                                                                           

                                                                                                                               
                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                        
                                                                                        





                                                         
                                           
                                                                         


















                                                                                        





















                                                                                                                                                   
                                           

                                                   
                                         


                                                                                         
                                                        









                                                                                                                                                                                                                             
                                                        
                                                                                                                 

                                                                                                                                  
                                                        

                                                                                                                                     
                                                                                                                                                             
                                          
















                                                                                                                                                                           







                                                                                                                                                                                                                                                                                                                                                                                                                                            
         
                                    
                                               
                                                              



                                                                                                            
         
 
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;
				}
				for (i = 0; i < event.path.length; i++) {
					if (event.path[i].nodeName != 'TR') {
						continue;
					}
					e = event.path[i].children[0].children[0];
					e.checked = !e.checked;
					if (e.checked) {
						event.path[i].classList.add("selected");
					} else {
						event.path[i].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) },
	}
)