diff --git a/api.go b/api.go
index 68b8596ac47c39ec5f154b99ba71f616a1299517..57372bdc3ee8cd58cd806549e2fcf39724a67068 100644
--- a/api.go
+++ b/api.go
@@ -7,6 +7,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"hash/crc32"
 	"net"
 	"strings"
 	"time"
@@ -403,3 +404,9 @@ func GeneratePassword() string {
 	rand.Read(b)
 	return base64.StdEncoding.EncodeToString(b)
 }
+
+// GenerateUsername returns a username somehow related to the name of
+// the mount, possibly unique (but not actually guaranteed to be so).
+func GenerateUsername(path string) string {
+	return fmt.Sprintf("source%d", crc32.ChecksumIEEE([]byte(path)))
+}
diff --git a/cmd/radioctl/radioctl.go b/cmd/radioctl/radioctl.go
index a30a16d3e01b60080becc980c2f6a1f5c041389b..2ec35a829554ae2bf33d0a406273f97ed15f33df 100644
--- a/cmd/radioctl/radioctl.go
+++ b/cmd/radioctl/radioctl.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"flag"
 	"fmt"
-	"hash/crc32"
 	"log"
 	"net/url"
 	"os"
@@ -140,14 +139,10 @@ func getClient() *autoradio.Client {
 	return autoradio.NewClient(autoradio.NewEtcdClient(false))
 }
 
-func generateUsername(path string) string {
-	return fmt.Sprintf("source%d", crc32.ChecksumIEEE([]byte(path)))
-}
-
 func setRelay(m *autoradio.Mount, relayUrl string) {
 	if relayUrl == "" {
 		// Randomly generate source credentials.
-		m.Username = generateUsername(m.Name)
+		m.Username = autoradio.GenerateUsername(m.Name)
 		m.Password = autoradio.GeneratePassword()
 	} else {
 		// Validate the given relay URL.
diff --git a/debug.go b/debug.go
new file mode 100644
index 0000000000000000000000000000000000000000..13fe66df5f2077e022846e83446ef35388222c86
--- /dev/null
+++ b/debug.go
@@ -0,0 +1,73 @@
+package autoradio
+
+import "sort"
+
+// MountStatus reports the configuration and status of a mount,
+// including eventual transcoded mounts that source it.
+type MountStatus struct {
+	Mount       *Mount
+	Listeners   int
+	TransMounts []*MountStatus
+}
+
+func newMountStatus(m *Mount, nodes []*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
+}
+
+// MountsToStatus converts a list of mounts (and eventually the
+// current list of nodes) to a nicely sorted and tree-aggregated list
+// of MountStatus objects. The list of nodes can be nil, in which case
+// listener statistics will be omitted.
+func MountsToStatus(mounts []*Mount, nodes []*NodeStatus) []*MountStatus {
+	// 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 (including transcoded mounts).
+	sort.Sort(mountStatusList(msl))
+	for _, s := range msl {
+		if s.TransMounts != nil {
+			sort.Sort(mountStatusList(s.TransMounts))
+		}
+	}
+	return msl
+}
diff --git a/fe/http.go b/fe/http.go
index 596f1482e09a95de54a2a96694d5be226c662773..a37a4fe9bad6475b0ac623fadc643d536e430e93 100644
--- a/fe/http.go
+++ b/fe/http.go
@@ -11,7 +11,6 @@ import (
 	"net/http"
 	"net/url"
 	"path/filepath"
-	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -266,76 +265,16 @@ 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))
-		}
-	}
-
+	msl := autoradio.MountsToStatus(mounts, nodes)
 	ctx := struct {
 		Domain string
 		Nodes  []*autoradio.NodeStatus
-		Mounts []*mountStatus
+		Mounts []*autoradio.MountStatus
 	}{h.domain, nodes, msl}
 
 	var buf bytes.Buffer
diff --git a/node/bwmonitor/bwmonitor.go b/node/bwmonitor/bwmonitor.go
index baeeaff980522b685be131ba0db9632283e582cc..25f915be549ca7eb0e812e862a773fc024623ba9 100644
--- a/node/bwmonitor/bwmonitor.go
+++ b/node/bwmonitor/bwmonitor.go
@@ -68,9 +68,8 @@ func (bw *BandwidthMonitor) Run(stop chan bool) {
 	t := time.NewTicker(bw.period)
 	for {
 		select {
-		case <-t.C:
+		case now := <-t.C:
 			if c, err := getBytesSentForDevice(bw.device); err == nil {
-				now := time.Now()
 				bw.lock.Lock()
 				bw.rate = float64(c-bw.counter) / now.Sub(bw.stamp).Seconds()
 				bw.counter = c