dashboard/app: add admin page

Add /admin page and move logs, jobs, manager onto it.
The main page is too overloaded and takes too long to load.
We need to start splitting it. This is a first step.
diff --git a/dashboard/app/admin.html b/dashboard/app/admin.html
new file mode 100644
index 0000000..09fd675
--- /dev/null
+++ b/dashboard/app/admin.html
@@ -0,0 +1,80 @@
+{{/*
+Copyright 2019 syzkaller project authors. All rights reserved.
+Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+Main page.
+*/}}
+
+<!doctype html>
+<html>
+<head>
+	{{template "head" .Header}}
+	<title>syzbot</title>
+</head>
+<body>
+	{{template "header" .Header}}
+
+	<a class="plain" href="#log"><div id="log"><b>Error log:</b></div></a>
+	<textarea id="log_textarea" readonly rows="20" wrap=off>{{printf "%s" .Log}}</textarea>
+	<script>
+		var textarea = document.getElementById("log_textarea");
+		textarea.scrollTop = textarea.scrollHeight;
+	</script>
+	<br><br>
+
+	{{template "manager_list" $.Managers}}
+
+	<table class="list_table">
+		<caption id="jobs"><a class="plain" href="#jobs">Recent jobs:</a></caption>
+		<tr>
+			<th>Bug</th>
+			<th>Created</th>
+			<th>Duration</th>
+			<th>User</th>
+			<th>Patch</th>
+			<th>Repo</th>
+			<th>Manager</th>
+			<th>Result</th>
+		</tr>
+		{{range $job := $.Jobs}}
+			<tr>
+				<td class="title"><a href="{{$job.BugLink}}">{{$job.BugTitle}}</a></td>
+				<td class="time">{{link $job.ExternalLink (formatTime $job.Created)}}</td>
+				<td class="time" title="started: {{formatTime $job.Started}}&#013;finished: {{formatTime $job.Finished}}">
+					{{formatDuration $job.Duration}}{{if gt $job.Attempts 1}} ({{$job.Attempts}}){{end}}
+				</td>
+				<td>
+					{{if eq $job.Type 0}}
+						{{$job.User}}
+					{{else if eq $job.Type 1}}
+						bisect
+					{{else if eq $job.Type 2}}
+						bisect fix
+					{{end}}
+				</td>
+				<td>{{optlink $job.PatchLink "patch"}}</td>
+				<td class="kernel" title="{{$job.KernelAlias}}">{{$job.KernelAlias}}</td>
+				<td title="{{$job.Namespace}}/{{$job.Reporting}}">{{$job.Manager}}</td>
+				<td class="result">
+					{{if $job.ErrorLink}}
+						{{link $job.ErrorLink "error"}}
+					{{else if $job.LogLink}}
+						{{link $job.LogLink "log"}}
+						({{if $job.Commit}}1{{else}}{{len $job.Commits}}{{end}})
+					{{else if $job.CrashTitle}}
+						{{optlink $job.CrashReportLink "report"}}
+						{{optlink $job.CrashLogLink "log"}}
+					{{else if formatTime $job.Finished}}
+						OK
+					{{else if formatTime $job.Started}}
+						running
+					{{else}}
+						pending
+					{{end}}
+				</td>
+			</tr>
+		{{end}}
+	</table>
+	<br><br>
+</body>
+</html>
diff --git a/dashboard/app/app.yaml b/dashboard/app/app.yaml
index e3300bf..7c8d90a 100644
--- a/dashboard/app/app.yaml
+++ b/dashboard/app/app.yaml
@@ -25,7 +25,7 @@
 - url: /(api)
   script: _go_app
   secure: always
-- url: /(email_poll)
+- url: /(admin|email_poll)
   script: _go_app
   login: admin
   secure: always
diff --git a/dashboard/app/handler.go b/dashboard/app/handler.go
index 167ca58..7fc133b 100644
--- a/dashboard/app/handler.go
+++ b/dashboard/app/handler.go
@@ -73,12 +73,14 @@
 }
 
 type uiHeader struct {
+	Admin               bool
 	LoginLink           string
 	AnalyticsTrackingID string
 }
 
 func commonHeader(c context.Context, r *http.Request) *uiHeader {
 	h := &uiHeader{
+		Admin:               accessLevel(c, r) == AccessAdmin,
 		AnalyticsTrackingID: config.AnalyticsTrackingID,
 	}
 	if user.Current(c) == nil {
diff --git a/dashboard/app/main.go b/dashboard/app/main.go
index 01e2f68..fdf3904 100644
--- a/dashboard/app/main.go
+++ b/dashboard/app/main.go
@@ -27,6 +27,7 @@
 	http.Handle("/", handlerWrapper(handleMain))
 	http.Handle("/bug", handlerWrapper(handleBug))
 	http.Handle("/text", handlerWrapper(handleText))
+	http.Handle("/admin", handlerWrapper(handleAdmin))
 	http.Handle("/x/.config", handlerWrapper(handleTextX(textKernelConfig)))
 	http.Handle("/x/log.txt", handlerWrapper(handleTextX(textCrashLog)))
 	http.Handle("/x/report.txt", handlerWrapper(handleTextX(textCrashReport)))
@@ -40,12 +41,16 @@
 type uiMain struct {
 	Header        *uiHeader
 	Now           time.Time
-	Log           []byte
-	Managers      []*uiManager
-	Jobs          []*uiJob
 	BugNamespaces []*uiBugNamespace
 }
 
+type uiAdminPage struct {
+	Header   *uiHeader
+	Log      []byte
+	Managers []*uiManager
+	Jobs     []*uiJob
+}
+
 type uiManager struct {
 	Now                   time.Time
 	Namespace             string
@@ -186,27 +191,14 @@
 
 // handleMain serves main page.
 func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error {
-	var errorLog []byte
-	var managers []*uiManager
-	var jobs []*uiJob
 	accessLevel := accessLevel(c, r)
-
+	var managers []*uiManager
 	if r.FormValue("fixed") == "" {
 		var err error
 		managers, err = loadManagers(c, accessLevel)
 		if err != nil {
 			return err
 		}
-		if accessLevel == AccessAdmin {
-			errorLog, err = fetchErrorLogs(c)
-			if err != nil {
-				return err
-			}
-			jobs, err = loadRecentJobs(c)
-			if err != nil {
-				return err
-			}
-		}
 	}
 	bugNamespaces, err := fetchBugs(c, r)
 	if err != nil {
@@ -222,16 +214,37 @@
 	data := &uiMain{
 		Header:        commonHeader(c, r),
 		Now:           timeNow(c),
-		Log:           errorLog,
-		Jobs:          jobs,
 		BugNamespaces: bugNamespaces,
 	}
-	if accessLevel == AccessAdmin {
-		data.Managers = managers
-	}
 	return serveTemplate(w, "main.html", data)
 }
 
+func handleAdmin(c context.Context, w http.ResponseWriter, r *http.Request) error {
+	accessLevel := accessLevel(c, r)
+	if accessLevel != AccessAdmin {
+		return ErrAccess
+	}
+	managers, err := loadManagers(c, accessLevel)
+	if err != nil {
+		return err
+	}
+	errorLog, err := fetchErrorLogs(c)
+	if err != nil {
+		return err
+	}
+	jobs, err := loadRecentJobs(c)
+	if err != nil {
+		return err
+	}
+	data := &uiAdminPage{
+		Header:   commonHeader(c, r),
+		Log:      errorLog,
+		Managers: managers,
+		Jobs:     jobs,
+	}
+	return serveTemplate(w, "admin.html", data)
+}
+
 // handleBug serves page about a single bug (which is passed in id argument).
 func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error {
 	bug := new(Bug)
diff --git a/dashboard/app/main.html b/dashboard/app/main.html
index f0f2db9..e1fc5a5 100644
--- a/dashboard/app/main.html
+++ b/dashboard/app/main.html
@@ -14,73 +14,6 @@
 <body>
 	{{template "header" .Header}}
 
-	{{if .Log}}
-	<a class="plain" href="#log"><div id="log"><b>Error log:</b></div></a>
-	<textarea id="log_textarea" readonly rows="20" wrap=off>{{printf "%s" .Log}}</textarea>
-	<script>
-		var textarea = document.getElementById("log_textarea");
-		textarea.scrollTop = textarea.scrollHeight;
-	</script>
-	<br><br>
-	{{end}}
-
-	{{template "manager_list" $.Managers}}
-
-	{{if $.Jobs}}
-	<table class="list_table">
-		<caption id="jobs"><a class="plain" href="#jobs">Recent jobs:</a></caption>
-		<tr>
-			<th>Bug</th>
-			<th>Created</th>
-			<th>Duration</th>
-			<th>User</th>
-			<th>Patch</th>
-			<th>Repo</th>
-			<th>Manager</th>
-			<th>Result</th>
-		</tr>
-		{{range $job := $.Jobs}}
-			<tr>
-				<td class="title"><a href="{{$job.BugLink}}">{{$job.BugTitle}}</a></td>
-				<td class="time">{{link $job.ExternalLink (formatTime $job.Created)}}</td>
-				<td class="time" title="started: {{formatTime $job.Started}}&#013;finished: {{formatTime $job.Finished}}">
-					{{formatDuration $job.Duration}}{{if gt $job.Attempts 1}} ({{$job.Attempts}}){{end}}
-				</td>
-				<td>
-					{{if eq $job.Type 0}}
-						{{$job.User}}
-					{{else if eq $job.Type 1}}
-						bisect
-					{{else if eq $job.Type 2}}
-						bisect fix
-					{{end}}
-				</td>
-				<td>{{optlink $job.PatchLink "patch"}}</td>
-				<td class="kernel" title="{{$job.KernelAlias}}">{{$job.KernelAlias}}</td>
-				<td title="{{$job.Namespace}}/{{$job.Reporting}}">{{$job.Manager}}</td>
-				<td class="result">
-					{{if $job.ErrorLink}}
-						{{link $job.ErrorLink "error"}}
-					{{else if $job.LogLink}}
-						{{link $job.LogLink "log"}}
-						({{if $job.Commit}}1{{else}}{{len $job.Commits}}{{end}})
-					{{else if $job.CrashTitle}}
-						{{optlink $job.CrashReportLink "report"}}
-						{{optlink $job.CrashLogLink "log"}}
-					{{else if formatTime $job.Finished}}
-						OK
-					{{else if formatTime $job.Started}}
-						running
-					{{else}}
-						pending
-					{{end}}
-				</td>
-			</tr>
-		{{end}}
-	</table>
-	<br><br>
-	{{end}}
-
 	{{range $ns := $.BugNamespaces}}
 		<br>
 		<a class="plain" href="#{{$ns.Name}}"><h2 id="{{$ns.Name}}">{{$ns.Caption}}</h2></a>
diff --git a/dashboard/app/templates.html b/dashboard/app/templates.html
index 3d7bba4..5747aa7 100644
--- a/dashboard/app/templates.html
+++ b/dashboard/app/templates.html
@@ -27,6 +27,9 @@
 					<h1><a href="/">syzbot</a></h1>
 				</td>
 				<td class="search">
+					{{if .Admin}}
+						<a href="/admin">admin</a> |
+					{{end}}
 					{{if .LoginLink}}
 						<a href="{{.LoginLink}}">sign-in</a> |
 					{{end}}
diff --git a/dashboard/app/util_test.go b/dashboard/app/util_test.go
index a12b9ec..73d1986 100644
--- a/dashboard/app/util_test.go
+++ b/dashboard/app/util_test.go
@@ -140,6 +140,7 @@
 	if !c.t.Failed() {
 		// Ensure that we can render main page and all bugs in the final test state.
 		c.expectOK(c.GET("/"))
+		c.expectOK(c.GET("/admin"))
 		var bugs []*Bug
 		keys, err := db.NewQuery("Bug").GetAll(c.ctx, &bugs)
 		if err != nil {