diff --git a/api.go b/api.go
index 9e56c8c4687c41a0c28c52488750899f129ed7af..3dd6f8a76a27fa9e387d38c3db8a18d4f2870033 100644
--- a/api.go
+++ b/api.go
@@ -64,6 +64,27 @@ func NewEncodingParams() *EncodingParams {
 	}
 }
 
+func (p *EncodingParams) String() string {
+	var out []string
+	out = append(out, p.Format)
+	if p.BitRate > 0 {
+		out = append(out, fmt.Sprintf("%dkBps", p.BitRate))
+	}
+	if p.Quality > -1 {
+		out = append(out, fmt.Sprintf("q=%g", p.Quality))
+	}
+	switch p.Channels {
+	case 1:
+		out = append(out, "mono")
+	case 2:
+		out = append(out, "stereo")
+	}
+	if p.SampleRate > 0 {
+		out = append(out, fmt.Sprintf("%gkHz", p.SampleRate/1000))
+	}
+	return strings.Join(out, ", ")
+}
+
 func (p *EncodingParams) Valid() error {
 	switch p.Format {
 	case "mp3", "mp3.cbr", "mp3.abr", "vorbis.cbr", "vorbis.abr":
diff --git a/fe/http.go b/fe/http.go
index 89cd8fceaa58b77551dcc2acf13ecc3ad568770a..11cf3565c7ed3db2e710546cdfb05e9199b5f11a 100644
--- a/fe/http.go
+++ b/fe/http.go
@@ -12,6 +12,7 @@ import (
 	"net/url"
 	"path"
 	"path/filepath"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -281,18 +282,81 @@ func (h *HttpRedirector) serveSource(mount *autoradio.Mount, w http.ResponseWrit
 	proxy.ServeHTTP(w, r)
 }
 
+type mountStatus struct {
+	Mount       *autoradio.Mount
+	Listeners   int
+	TransMounts []*mountStatus
+}
+
+func newMountStatus(m *autoradio.Mount, nodes []*autoradio.NodeStatus) *mountStatus {
+	var listeners int
+	for _, n := range nodes {
+		for _, ims := range n.Mounts {
+			if ims.Name == m.Name {
+				listeners += ims.Listeners
+				break
+			}
+		}
+	}
+	return &mountStatus{
+		Mount:     m,
+		Listeners: listeners,
+	}
+}
+
+type mountStatusList []*mountStatus
+
+func (l mountStatusList) Len() int      { return len(l) }
+func (l mountStatusList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
+func (l mountStatusList) Less(i, j int) bool {
+	return l[i].Mount.Name < l[j].Mount.Name
+}
+
 // Serve our cluster status page.
 func (h *HttpRedirector) serveStatusPage(w http.ResponseWriter, r *http.Request) {
 	nodes, _ := h.client.GetNodes()
 	mounts, _ := h.client.ListMounts()
+
+	// Aggregate stats, and create a tree of transcoding mounts.
+	ms := make(map[string]*mountStatus)
+	for _, m := range mounts {
+		if m.HasTranscoder() {
+			continue
+		}
+		ms[m.Name] = newMountStatus(m, nodes)
+	}
+	for _, m := range mounts {
+		if !m.HasTranscoder() {
+			continue
+		}
+		src := ms[m.Transcoding.SourceName]
+		if src == nil {
+			continue
+		}
+		src.TransMounts = append(src.TransMounts, newMountStatus(m, nodes))
+	}
+	msl := make([]*mountStatus, 0, len(ms))
+	for _, m := range ms {
+		msl = append(msl, m)
+	}
+
+	// Sort everything.
+	sort.Sort(mountStatusList(msl))
+	for _, s := range msl {
+		if s.TransMounts != nil {
+			sort.Sort(mountStatusList(s.TransMounts))
+		}
+	}
+
 	ctx := struct {
 		Domain string
 		Nodes  []*autoradio.NodeStatus
-		Mounts []*autoradio.Mount
-	}{h.domain, nodes, mounts}
+		Mounts []*mountStatus
+	}{h.domain, nodes, msl}
 
 	var buf bytes.Buffer
 	if err := h.template.ExecuteTemplate(&buf, "index.html", ctx); err != nil {
+		log.Printf("error rendering template: %v", err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
diff --git a/fe/http_test.go b/fe/http_test.go
index d944540d8c10699404b9dce018963662340df09d..28d6b7379c0305d271606816e599ec2202900617 100644
--- a/fe/http_test.go
+++ b/fe/http_test.go
@@ -40,6 +40,10 @@ func createTestHttpRedirector(t *testing.T) *HttpRedirector {
 	etcd.Set(autoradio.MountPrefix+"test.ogg",
 		`{"Name": "/test.ogg", "Username": "source1", "Password": "foo"}`,
 		86400)
+	etcd.Set(autoradio.MountPrefix+"test.mp3",
+		`{"Name": "/test.mp3", "Username": "source2", "Password": "foo",
+                  "Transcoding": {"SourceName": "/test.ogg", "Format": "mp3", "BitRate": 32}}`,
+		86400)
 	etcd.Set(autoradio.MasterElectionPath,
 		`{"Name": "node1", "IP": ["127.0.0.1"]}`,
 		86400)
@@ -88,8 +92,17 @@ func TestHttpRedirector_StatusPage(t *testing.T) {
 
 	// Retrieve the status page.
 	data := doHttpRequest(t, "GET", srv.URL, 200)
-	if !strings.Contains(data, "<div class=\"container\">") {
-		t.Errorf("Bad response:\n%s", data)
+
+	// Search for some expected content.
+	needles := []string{
+		"<div class=\"container\">",
+		"/test.ogg",
+		"/test.mp3",
+	}
+	for _, s := range needles {
+		if !strings.Contains(data, s) {
+			t.Errorf("Bad response ('%s' not found):\n%s", s, data)
+		}
 	}
 }
 
diff --git a/fe/static/style.css b/fe/static/style.css
index 681b3c5257bfeb271a2808425190813a66bbf3d0..3626800e3aafe97440506a71011fd462c9117706 100644
--- a/fe/static/style.css
+++ b/fe/static/style.css
@@ -77,7 +77,3 @@ body {
   z-index: -100;
   opacity: 0.3;
 }
-
-.error {
-  color: red
-}
\ No newline at end of file
diff --git a/fe/templates/index.html b/fe/templates/index.html
index 6bc3fab4ba67c8ee05edda592fa81a26ce5c5eab..71ae9b2e9af2aaa1a74c4a1baaa973350a7ab22c 100644
--- a/fe/templates/index.html
+++ b/fe/templates/index.html
@@ -3,7 +3,7 @@
   <head>
     <title>stream.{{.Domain}}</title>
     <link rel="stylesheet"
-          href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">
+          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
     <link rel="stylesheet" href="/static/style.css">
     <link rel="shortcut icon" href="/static/radio52.png">
   </head>
@@ -21,22 +21,45 @@
       <div class="row mainitems">
         <div class="col-lg-6">
           <h4>Streams</h4>
-          {{$domain := .Domain}}
-          {{range .Mounts}}
-          <p>
-            <a href="http://stream.{{$domain}}{{.Name}}">{{.Name}}</a>
-            <a href="http://stream.{{$domain}}{{.Name}}.m3u">(m3u)</a>
-          </p>
-          {{end}}
+          <ul>
+            {{$domain := .Domain}}
+            {{range .Mounts}}
+            <li>
+              <a href="http://stream.{{$domain}}{{.Mount.Name}}"
+                 {{if .Mount.RelayUrl}}
+                 data-toggle="tooltip" data-delay="300" title="relay of {{.Mount.RelayUrl}}"
+                 {{end}}
+                 >{{.Mount.Name}}</a>
+              <a href="http://stream.{{$domain}}{{.Mount.Name}}.m3u">(m3u)</a>
+              <span class="badge">{{.Listeners}}</span>
+              {{if .TransMounts}}
+              <ul>
+                {{range .TransMounts}}
+                <li>
+                  <a href="http://stream.{{$domain}}{{.Mount.Name}}"
+                     data-toggle="tooltip" data-delay="300" title="{{.Mount.Transcoding.String}}"
+                     >{{.Mount.Name}}</a>
+                  <a href="http://stream.{{$domain}}{{.Mount.Name}}.m3u">(m3u)</a>
+                  <span class="badge">{{.Listeners}}</span>
+                </li>
+                {{end}}
+              </ul>
+              {{end}}
+            </li>
+            {{end}}
+          </ul>
         </div>
 
         <div class="col-lg-6">
           <h4>Nodes</h4>
-          {{range .Nodes}}
-          <p>{{.IP}} ({{.NumListeners}})
-            {{if not .IcecastUp}}<span class="error">(IC_DOWN)</span>{{end}}
-          </p>
-          {{end}}
+          <ul>
+            {{range .Nodes}}
+            <li>
+              {{.Name}} <span class="badge">{{.NumListeners}}</span>
+              {{if not .IcecastUp}}<span class="label label-danger">IC_DOWN</span>{{end}}
+            </li>
+            {{end}}
+          </ul>
         </div>
       </div>
 
@@ -48,5 +71,12 @@
       </div>
     </div>
 
+    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
+    <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
+    <script type="text/javascript">
+      $(function() {
+        $('[data-toggle="tooltip"]').tooltip();
+      });
+    </script>
   </body>
 </html>