content,internal: add experiments and excluded to worker homepage

Information about experiments and excluded prefixes are added to the
worker homepage.

Change-Id: I7bb7fd1eece434bd4da12e1af384b141c8a0ed41
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/239181
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/content/static/html/worker/index.tmpl b/content/static/html/worker/index.tmpl
index 162b4dc..a0a8866 100644
--- a/content/static/html/worker/index.tmpl
+++ b/content/static/html/worker/index.tmpl
@@ -1,90 +1,91 @@
 <!--
-	Copyright 2019 The Go Authors. All rights reserved.
-	Use of this source code is governed by a BSD-style
-	license that can be found in the LICENSE file.
+  Copyright 2019 The Go Authors. All rights reserved.
+  Use of this source code is governed by a BSD-style
+  license that can be found in the LICENSE file.
 -->
 
 {{define "versionTable"}}
-	<table>
-	<thead>
-	{{if .}}
-		<tr>
-			<th>Module Version</th>
-			<th>Index Timestamp</th>
-			<th>Status</th>
-			<th>Error</th>
-			<th>Attempts</th>
-			<th>LastAttempt</th>
-			<th>NextAttempt</th>
-		</tr>
-		</thead>
-		<tbody>
-		{{range .}}
-		<tr>
-			<td>{{.ModulePath}}/@v/{{.Version}}</td>
-			<td>{{.IndexTimestamp | timefmt}}</td>
-			<td>{{.Status}}</td>
-			<td>{{.Error | truncate 500}}</td>
-			<td>{{.TryCount}}</td>
-			<td>{{.LastProcessedAt | timefmt}}</td>
-			<td>{{.NextProcessedAfter | timefmt}}</td>
-		</tr>
-		{{end}}
-	{{else}}
-	<p>No versions.</p>
-	{{end}}
-	</tbody>
-	</table>
+  {{if .}}
+    <table>
+      <thead>
+        <tr>
+          <th>Module Version</th>
+          <th>Index Timestamp</th>
+          <th>Status</th>
+          <th>Error</th>
+          <th>Attempts</th>
+          <th>LastAttempt</th>
+          <th>NextAttempt</th>
+        </tr>
+      </thead>
+      <tbody>
+        {{range .}}
+          <tr>
+            <td>{{.ModulePath}}/@v/{{.Version}}</td>
+            <td>{{.IndexTimestamp | timefmt}}</td>
+            <td>{{.Status}}</td>
+            <td>{{.Error | truncate 500}}</td>
+            <td>{{.TryCount}}</td>
+            <td>{{.LastProcessedAt | timefmt}}</td>
+            <td>{{.NextProcessedAfter | timefmt}}</td>
+          </tr>
+        {{end}}
+      </tbody>
+    </table>
+  {{else}}
+    <p>No versions.</p>
+  {{end}}
 {{end -}}
 
 <!DOCTYPE html>
 <script>
 function submitForm(formName, reload) {
-	let form = document[formName];
-	form.result.value = "request pending...";
-	let xhr = new XMLHttpRequest();
-	xhr.onreadystatechange = function() {
-	  if (this.readyState == 4) {
-			if (this.status >= 200 && this.status < 300) {
-        if (reload) {
-          location.reload();
-        } else {
-          form.result.value = "Success."
-        }
-			} else {
-				form.result.value = "ERROR: " + this.responseText;
-			}
-		}
-	}
-	xhr.open(form.method, form.action);
-	xhr.send(new FormData(form));
+  let form = document[formName];
+  form.result.value = "request pending...";
+  let xhr = new XMLHttpRequest();
+  xhr.onreadystatechange = function() {
+    if (this.readyState == 4) {
+      if (this.status >= 200 && this.status < 300) {
+    if (reload) {
+      location.reload();
+    } else {
+      form.result.value = "Success."
+    }
+      } else {
+        form.result.value = "ERROR: " + this.responseText;
+      }
+    }
+  }
+  xhr.open(form.method, form.action);
+  xhr.send(new FormData(form));
 }
 </script>
 <style>
 body {
-	font-family: Verdana, Arial, sans-serif;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
+  "Roboto", "Oxygen", "Ubuntu", "Helvetica Neue", Arial, sans-serif;
 }
 label {
-	display: inline-block;
-	text-align: right;
-	width: 200px;
+  display: inline-block;
+  text-align: right;
+  width: 12.5rem;
 }
 input {
-	width: 200px;
+  width: 12.5rem;
 }
 button {
-	width: 200px;
-	background-color: #eee;
-	border-radius: 2px;
-	border: 1px solid #ccc;
+  width: 12.5rem;
+  background-color: #eee;
+  border-radius: 0.125rem;
+  border: 0.0625rem solid #ccc;
 }
 table {
-	border-spacing: 10px 2px;
-	padding: 3px 0 2px 0;
-	font-size: 12px;
+  border-spacing: 0.625rem 0.125rem;
+  padding: 0.1875rem 0 0.125rem 0;
+  font-size: 0.75rem;
 }
 td {
-	border-top: 1px solid #ddd;
+  border-top: 0.0625rem solid #ddd;
 }
 </style>
 <title>{{.Env}} Worker</title>
@@ -94,67 +95,93 @@
 
 <p>
   <a href="https://cloud.google.com/console/cloudtasks/queue/{{.ResourcePrefix}}fetch-tasks?project={{.Config.ProjectID}}"
-	target="_blank" rel="noreferrer">
-     Task Queue
+  target="_blank" rel="noreferrer">
+   Task Queue
   </a> |
   <a href="https://cloud.google.com/console/cloudscheduler?project={{.Config.ProjectID}}"
-	target="_blank" rel="noreferrer">
-     Scheduler
+  target="_blank" rel="noreferrer">
+   Scheduler
   </a> |
   <a href="https://cloud.google.com/console/logs/viewer?project={{.Config.ProjectID}}&resource=gae_app%2Fmodule_id%2F{{.Config.ServiceID}}"
-	target="_blank" rel="noreferrer">
-     Logs (switch to "All Logs" when you get there)
+  target="_blank" rel="noreferrer">
+   Logs (switch to "All Logs" when you get there)
   </a>
 </p>
 
 <div class="actions">
-	<form action="/poll-and-queue" method="post" name="queueForm">
-		<button title="Poll the module index for up to 2000 new versions, and enqueue them for processing."
-      onclick="submitForm('queueForm', false); return false">Enqueue From Module Index</button>
-		<input type="number" name="limit" value="10"></input>
-		<output name="result"></output>
-	</form>
-	<form action="/requeue" method="post" name="requeueForm">
-		<button title="Query the discovery database for failed versions, and re-queue them for processing."
-      onclick="submitForm('requeueForm', true); return false">Requeue Failed Versions</button>
-		<input type="number" name="limit" value="10">
-		<output name="result"></output>
-	</form>
-	<form action="/reprocess" method="post" name="reprocessForm">
-		<button title="Mark all versions created before the specified app_version to be reprocessed."
-      onclick="submitForm('reprocessForm', true); return false">Reprocess Versions</button>
-		<input type="text" name="app_version">
-		<output name="result"></output>
-	</form>
-	<form action="/populate-stdlib" method="post" name="populateStdlibForm">
-		<button title="Populates the database with all supported versions of the Go standard library."
-      onclick="submitForm('populateStdlibForm', false); return false">Populate Standard Library</button>
-		<output name="result"></output>
-	</form>
+  <form action="/poll-and-queue" method="post" name="queueForm">
+    <button title="Poll the module index for up to 2000 new versions, and enqueue them for processing."
+    onclick="submitForm('queueForm', false); return false">Enqueue From Module Index</button>
+    <input type="number" name="limit" value="10"></input>
+    <output name="result"></output>
+  </form>
+  <form action="/requeue" method="post" name="requeueForm">
+    <button title="Query the discovery database for failed versions, and re-queue them for processing."
+    onclick="submitForm('requeueForm', true); return false">Requeue Failed Versions</button>
+    <input type="number" name="limit" value="10">
+    <output name="result"></output>
+  </form>
+  <form action="/reprocess" method="post" name="reprocessForm">
+    <button title="Mark all versions created before the specified app_version to be reprocessed."
+    onclick="submitForm('reprocessForm', true); return false">Reprocess Versions</button>
+    <input type="text" name="app_version">
+    <output name="result"></output>
+  </form>
+  <form action="/populate-stdlib" method="post" name="populateStdlibForm">
+    <button title="Populates the database with all supported versions of the Go standard library."
+    onclick="submitForm('populateStdlibForm', false); return false">Populate Standard Library</button>
+    <output name="result"></output>
+  </form>
 </div>
 
 <div class="config">
 <h3>Config</h3>
   <table>
-    <tr><td>App Version</td><td>{{.Config.VersionID}}</td></tr>
-    <tr><td>Zone</td><td>{{.Config.ZoneID}}</td></tr>
-    <tr><td>DB Host</td><td>{{.Config.DBHost}}</td></tr>
-    <tr><td>Redis Cache Host</td><td>{{.Config.RedisCacheHost}}</td></tr>
-    <tr><td>Redis HA Host</td><td>{{.Config.RedisHAHost}}</td></tr>
+  <tr><td>App Version</td><td>{{.Config.VersionID}}</td></tr>
+  <tr><td>Zone</td><td>{{.Config.ZoneID}}</td></tr>
+  <tr><td>DB Host</td><td>{{.Config.DBHost}}</td></tr>
+  <tr><td>Redis Cache Host</td><td>{{.Config.RedisCacheHost}}</td></tr>
+  <tr><td>Redis HA Host</td><td>{{.Config.RedisHAHost}}</td></tr>
   </table>
 </div>
 
+<div>
+  <h3>Experiments</h3>
+  {{if .Experiments}}
+    <table>
+      <thead>
+        <tr>
+          <th>Name</th>
+          <th>Rollout</th>
+          <th>Description</th>
+        </tr>
+      </thead>
+      <tbody>
+      {{range .Experiments}}
+        <tr>
+          <td>{{.Name}}</td>
+          <td>{{.Rollout}}</td>
+          <td>{{.Description}}</td>
+        </tr>
+      {{end}}
+      </tbody>
+    </table>
+  {{else}}
+    <p>No experiments.</p>
+  {{end}}
+</div>
+
 <div class="stats">
   <h3>Statistics</h3>
   <p>Latest timestamp from the module index: {{.LatestTimestamp | timefmt}}</p>
   <table>
-    <caption>Results by status:</caption>
-    <thead><tr><th>Code</th><th>Status</th><th>Count</th></tr></thead>
-    <tbody>
-      {{range .Counts}}
-        <tr><td>{{.Code}}</td><td>{{.Desc}}</td><td>{{.Count}}</td></tr>
-      {{end}}
-    </tbody>
+  <caption>Results by status:</caption>
+  <thead><tr><th>Code</th><th>Status</th><th>Count</th></tr></thead>
+  <tbody>
+    {{range .Counts}}
+    <tr><td>{{.Code}}</td><td>{{.Desc}}</td><td>{{.Count}}</td></tr>
+    {{end}}
+  </tbody>
   </table>
 </div>
 
@@ -166,3 +193,21 @@
 
 <h3>Recent failed attempts:</h3>
 {{template "versionTable" .RecentFailures}}
+
+<div>
+  <h3>Excluded Prefixes</h3>
+  {{if .Excluded}}
+    <table>
+      <thead>
+        <tr><th>Prefix</th></tr>
+      </thead>
+      <tbody>
+      {{range .Excluded}}
+        <tr><td>{{.}}</td></tr>
+      {{end}}
+      </tbody>
+    </table>
+  {{else}}
+    <p>No excluded prefixes.</p>
+  {{end}}
+</div>
diff --git a/internal/postgres/excluded.go b/internal/postgres/excluded.go
index 9010f2c..4f5e5b6 100644
--- a/internal/postgres/excluded.go
+++ b/internal/postgres/excluded.go
@@ -76,9 +76,10 @@
 	if time.Since(lastFetched) < excludedPrefixesExpiration {
 		return
 	}
-	prefixes, err := db.readExcludedPrefixes(ctx)
+	prefixes, err := db.GetExcludedPrefixes(ctx)
 	excludedPrefixes.mu.Lock()
 	defer excludedPrefixes.mu.Unlock()
+	excludedPrefixes.lastFetched = time.Now()
 	excludedPrefixes.prefixes = prefixes
 	excludedPrefixes.err = err
 	if err != nil {
@@ -86,8 +87,8 @@
 	}
 }
 
-// readExcludedPrefixes reads all the excluded prefixes from the database.
-func (db *DB) readExcludedPrefixes(ctx context.Context) ([]string, error) {
+// GetExcludedPrefixes reads all the excluded prefixes from the database.
+func (db *DB) GetExcludedPrefixes(ctx context.Context) ([]string, error) {
 	var eps []string
 	err := db.db.RunQuery(ctx, `SELECT prefix FROM excluded_prefixes`, func(rows *sql.Rows) error {
 		var ep string
@@ -100,6 +101,5 @@
 	if err != nil {
 		return nil, err
 	}
-	setExcludedPrefixesLastFetched(time.Now())
 	return eps, nil
 }
diff --git a/internal/worker/server.go b/internal/worker/server.go
index a7effce..7097291 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -69,7 +69,7 @@
 func NewServer(cfg *config.Config, scfg ServerConfig) (_ *Server, err error) {
 	defer derrors.Wrap(&err, "NewServer(db, %+v)", scfg)
 
-	indexTemplate, err := parseTemplate(scfg.StaticPath)
+	indexTemplate, err := parseTemplate(scfg.StaticPath, "index.tmpl")
 	if err != nil {
 		return nil, err
 	}
@@ -84,8 +84,8 @@
 		redisCacheClient:     scfg.RedisCacheClient,
 		queue:                scfg.Queue,
 		reportingClient:      scfg.ReportingClient,
-		indexTemplate:        indexTemplate,
 		taskIDChangeInterval: scfg.TaskIDChangeInterval,
+		indexTemplate:        indexTemplate,
 	}, nil
 }
 
@@ -347,6 +347,8 @@
 	var (
 		next, failures, recents []*internal.ModuleVersionState
 		stats                   *postgres.VersionStats
+		experiments             []*internal.Experiment
+		excluded                []string
 	)
 	type annotation struct {
 		error
@@ -385,6 +387,22 @@
 		}
 		return nil
 	})
+	g.Go(func() error {
+		var err error
+		experiments, err = s.db.GetExperiments(ctx)
+		if err != nil {
+			return annotation{err, "error fetching experiments"}
+		}
+		return nil
+	})
+	g.Go(func() error {
+		var err error
+		excluded, err = s.db.GetExcludedPrefixes(ctx)
+		if err != nil {
+			return annotation{err, "error fetching excluded"}
+		}
+		return nil
+	})
 	if err := g.Wait(); err != nil {
 		var e annotation
 		if errors.As(err, &e) {
@@ -426,6 +444,8 @@
 		LatestTimestamp              *time.Time
 		Counts                       []*count
 		Next, Recent, RecentFailures []*internal.ModuleVersionState
+		Experiments                  []*internal.Experiment
+		Excluded                     []string
 	}{
 		Config:          s.cfg,
 		Env:             env,
@@ -435,14 +455,16 @@
 		Next:            next,
 		Recent:          recents,
 		RecentFailures:  failures,
+		Experiments:     experiments,
+		Excluded:        excluded,
 	}
 	var buf bytes.Buffer
 	if err := s.indexTemplate.Execute(&buf, page); err != nil {
 		return "error rendering template", err
 	}
 	if _, err := io.Copy(w, &buf); err != nil {
-		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 		log.Errorf(ctx, "Error copying buffer to ResponseWriter: %v", err)
+		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 	}
 	return "", nil
 }
@@ -511,12 +533,12 @@
 }
 
 // Parse the template for the status page.
-func parseTemplate(staticPath string) (*template.Template, error) {
+func parseTemplate(staticPath, filename string) (*template.Template, error) {
 	if staticPath == "" {
 		return nil, nil
 	}
-	templatePath := filepath.Join(staticPath, "html/worker/index.tmpl")
-	return template.New("index.tmpl").Funcs(template.FuncMap{
+	templatePath := filepath.Join(staticPath, "html/worker/"+filename)
+	return template.New(filename).Funcs(template.FuncMap{
 		"truncate": truncate,
 		"timefmt":  formatTime,
 	}).ParseFiles(templatePath)