diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
index 36d6943a4c48ab0db24a8230430d6c50dfb21d9e..6efe33003629f0e5c1cce647a5eadc53cf422bb1 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
@@ -105,7 +105,7 @@ class SybilRenderer {
     /**
      *  A total score and a List of reason Strings
      */
-    private static class Points implements Comparable<Points> {
+    public static class Points implements Comparable<Points> {
          private double points;
          private final List<String> reasons;
 
@@ -156,6 +156,24 @@ class SybilRenderer {
         }
     }
 
+    /**
+     *  Merge points1 into points2.
+     *  points1 is unmodified.
+     */
+    private void mergePoints(Map<Hash, Points> points1, Map<Hash, Points> points2) {
+        for (Map.Entry<Hash, Points> e : points1.entrySet()) {
+             Hash h = e.getKey();
+             Points p1 = e.getValue();
+             Points p2 = points2.get(h);
+             if (p2 != null) {
+                 p2.points += p1.points;
+                 p2.reasons.addAll(p1.reasons);
+             } else {
+                 points2.put(h, p1);
+             }
+        }
+    }
+
     private void addPoints(Map<Hash, Points> points, Hash h, double d, String reason) {
         String rsn = "<b>" + fmt.format(d) + ":</b> " + reason;
         Points dd = points.get(h);
@@ -168,15 +186,12 @@ class SybilRenderer {
     }
 
     /**
-     *  The whole thing
-     *
-     *  @param routerPrefix ignored
+     *  All the floodfills, not including us
+     *  @since 0.9.38 split out from renderRouterInfoHTML
      */
-    private void renderRouterInfoHTML(Writer out, String routerPrefix) throws IOException {
+    private List<RouterInfo> getFloodfills(Hash us) {
         Set<Hash> ffs = _context.peerManager().getPeersByCapability('f');
         List<RouterInfo> ris = new ArrayList<RouterInfo>(ffs.size());
-        Hash us = _context.routerHash();
-        Hash ourRKey = _context.router().getRouterInfo().getRoutingKey();
         for (Hash ff : ffs) {
              if (ff.equals(us))
                  continue;
@@ -184,6 +199,32 @@ class SybilRenderer {
              if (ri != null)
                  ris.add(ri);
         }
+        return ris;
+    }
+
+    private double getAvgMinDist(List<RouterInfo> ris) {
+        double tot = 0;
+        int count = 200;
+        byte[] b = new byte[32];
+        for (int i = 0; i < count; i++) {
+            _context.random().nextBytes(b);
+            Hash h = new Hash(b);
+            double d = closestDistance(h, ris);
+            tot += d;
+        }
+        double avgMinDist = tot / count;
+        return avgMinDist;
+    }
+
+    /**
+     *  The whole thing
+     *
+     *  @param routerPrefix ignored
+     */
+    private void renderRouterInfoHTML(Writer out, String routerPrefix) throws IOException {
+        Hash us = _context.routerHash();
+        Hash ourRKey = _context.router().getRouterInfo().getRoutingKey();
+        List<RouterInfo> ris = getFloodfills(us);
         if (ris.isEmpty()) {
             out.write("<h3 class=\"sybils\">No known floodfills</h3>");
             return;
@@ -210,16 +251,7 @@ class SybilRenderer {
         renderRouterInfo(buf, _context.router().getRouterInfo(), null, true, false);
         buf.append("<h3 id=\"known\" class=\"sybils\">Known Floodfills: ").append(ris.size()).append("</h3>");
 
-        double tot = 0;
-        int count = 200;
-        byte[] b = new byte[32];
-        for (int i = 0; i < count; i++) {
-            _context.random().nextBytes(b);
-            Hash h = new Hash(b);
-            double d = closestDistance(h, ris);
-            tot += d;
-        }
-        double avgMinDist = tot / count;
+        double avgMinDist = getAvgMinDist(ris);
         buf.append("<div id=\"sybils_summary\">\n" +
                    "<b>Average closest floodfill distance:</b> ").append(fmt.format(avgMinDist)).append("<br>\n" +
                    "<b>Routing Data:</b> \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData()))
@@ -243,15 +275,15 @@ class SybilRenderer {
         // Distance to our router analysis
         buf.append("<h3 id=\"ritoday\" class=\"sybils\">Closest Floodfills to Our Routing Key (Where we Store our RI)</h3>");
         buf.append("<p class=\"sybil_info\"><a href=\"/netdb?caps=f&amp;sybil\">See all</a></p>");
-        renderRouterInfoHTML(out, buf, ourRKey, avgMinDist, ris, points);
+        renderRouterInfoHTML(out, buf, ourRKey, "our rkey", avgMinDist, ris, points);
         RouterKeyGenerator rkgen = _context.routerKeyGenerator();
         Hash nkey = rkgen.getNextRoutingKey(us);
         buf.append("<h3 id=\"ritmrw\" class=\"sybils\">Closest Floodfills to Tomorrow's Routing Key (Where we will Store our RI)</h3>");
         buf.append("<p class=\"sybil_info\"><a href=\"/netdb?caps=f&amp;sybil\">See all</a></p>");
-        renderRouterInfoHTML(out, buf, nkey, avgMinDist, ris, points);
+        renderRouterInfoHTML(out, buf, nkey, "our rkey (tomorrow)", avgMinDist, ris, points);
 
         buf.append("<h3 id=\"dht\" class=\"sybils\">Closest Floodfills to Our Router Hash (DHT Neighbors if we are Floodfill)</h3>");
-        renderRouterInfoHTML(out, buf, us, avgMinDist, ris, points);
+        renderRouterInfoHTML(out, buf, us, "our router", avgMinDist, ris, points);
 
         // Distance to our published destinations analysis
         buf.append("<h3 id=\"dest\" class=\"sybils\">Floodfills Close to Our Destinations</h3>");
@@ -269,14 +301,14 @@ class SybilRenderer {
                 continue;
             Hash rkey = ls.getRoutingKey();
             TunnelPool in = clientInboundPools.get(client);
-            String name = (in != null) ? in.getSettings().getDestinationNickname() : client.toBase64().substring(0,4);
-            buf.append("<h3 class=\"sybils\">Closest floodfills to the Routing Key for " + DataHelper.escapeHTML(name) + " (where we store our LS)</h3>");
+            String name = (in != null) ? DataHelper.escapeHTML(in.getSettings().getDestinationNickname()) : client.toBase64().substring(0,4);
+            buf.append("<h3 class=\"sybils\">Closest floodfills to the Routing Key for " + name + " (where we store our LS)</h3>");
             buf.append("<p class=\"sybil_info\"><a href=\"/netdb?caps=f&amp;sybil=" + ls.getHash().toBase64() + "\">See all</a></p>");
-            renderRouterInfoHTML(out, buf, rkey, avgMinDist, ris, points);
+            renderRouterInfoHTML(out, buf, rkey, name, avgMinDist, ris, points);
             nkey = rkgen.getNextRoutingKey(ls.getHash());
-            buf.append("<h3 class=\"sybils\">Closest floodfills to Tomorrow's Routing Key for " + DataHelper.escapeHTML(name) + " (where we will store our LS)</h3>");
+            buf.append("<h3 class=\"sybils\">Closest floodfills to Tomorrow's Routing Key for " + name + " (where we will store our LS)</h3>");
             buf.append("<p class=\"sybil_info\"><a href=\"/netdb?caps=f&amp;sybil=" + ls.getHash().toBase64() + "\">See all</a></p>");
-            renderRouterInfoHTML(out, buf, nkey, avgMinDist, ris, points);
+            renderRouterInfoHTML(out, buf, nkey, name + " (tomorrow)", avgMinDist, ris, points);
         }
 
         // Profile analysis
@@ -311,6 +343,67 @@ class SybilRenderer {
         buf.setLength(0);
     }
 
+    /**
+     *  Analyze threats. No output.
+     *  Return separate maps for each cause instead?
+     *  @since 0.9.38
+     */
+    public Map<Hash, Points> backgroundAnalysis() throws IOException {
+        Hash us = _context.routerHash();
+        List<RouterInfo> ris = getFloodfills(us);
+
+        double avgMinDist = getAvgMinDist(ris);
+        Map<Hash, Points> points = new HashMap<Hash, Points>(64);
+
+        // IP analysis
+        renderIPGroupsFamily(null, null, ris, points);
+        renderIPGroupsUs(null, null, ris, points);
+        renderIPGroups32(null, null, ris, points);
+        renderIPGroups24(null, null, ris, points);
+        renderIPGroups16(null, null, ris, points);
+
+        // Pairwise distance analysis
+        renderPairDistance(null, null, ris, points);
+
+        // Distance to our router analysis
+        // closest to our routing key today
+        Hash ourRKey = _context.router().getRouterInfo().getRoutingKey();
+        renderRouterInfoHTML(null, null, ourRKey, "our rkey", avgMinDist, ris, points);
+        // closest to our routing key tomorrow
+        RouterKeyGenerator rkgen = _context.routerKeyGenerator();
+        Hash nkey = rkgen.getNextRoutingKey(us);
+        renderRouterInfoHTML(null, null, nkey, "our rkey (tomorrow)", avgMinDist, ris, points);
+        // closest to us
+        renderRouterInfoHTML(null, null, us, "our router", avgMinDist, ris, points);
+
+        // Distance to our published destinations analysis
+        Map<Hash, TunnelPool> clientInboundPools = _context.tunnelManager().getInboundClientPools();
+        List<Hash> destinations = new ArrayList<Hash>(clientInboundPools.keySet());
+        for (Hash client : destinations) {
+            boolean isLocal = _context.clientManager().isLocal(client);
+            if (!isLocal)
+                continue;
+            if (! _context.clientManager().shouldPublishLeaseSet(client))
+                continue;
+            LeaseSet ls = _context.netDb().lookupLeaseSetLocally(client);
+            if (ls == null)
+                continue;
+            Hash rkey = ls.getRoutingKey();
+            TunnelPool in = clientInboundPools.get(client);
+            String name = (in != null) ? DataHelper.escapeHTML(in.getSettings().getDestinationNickname()) : client.toBase64().substring(0,4);
+            // closest to routing key today
+            renderRouterInfoHTML(null, null, rkey, name, avgMinDist, ris, points);
+            // closest to routing key tomorrow
+            nkey = rkgen.getNextRoutingKey(ls.getHash());
+            renderRouterInfoHTML(null, null, nkey, name + " (tomorrow)", avgMinDist, ris, points);
+        }
+
+        // Profile analysis
+        addProfilePoints(ris, points);
+        addVersionPoints(ris, points);
+        return points;
+    }
+
     private static class Pair implements Comparable<Pair> {
         public final RouterInfo r1, r2;
         public final BigInteger dist;
@@ -380,7 +473,7 @@ class SybilRenderer {
         }
     }
 
-    private double closestDistance(Hash h, List<RouterInfo> ris) throws IOException {
+    private double closestDistance(Hash h, List<RouterInfo> ris) {
         BigInteger min = (new BigInteger("2")).pow(256);
         for (RouterInfo info : ris) {
             BigInteger dist = HashDistance.getDistance(h, info.getHash());
@@ -799,8 +892,9 @@ class SybilRenderer {
     /**
      *  @param out null for background analysis
      *  @param buf null for background analysis
+     *  @param usName HTML escaped
      */
-    private void renderRouterInfoHTML(Writer out, StringBuilder buf, Hash us, double avgMinDist,
+    private void renderRouterInfoHTML(Writer out, StringBuilder buf, Hash us, String usName, double avgMinDist,
                                       List<RouterInfo> ris, Map<Hash, Points> points) throws IOException {
         Collections.sort(ris, new RouterInfoRoutingKeyComparator(us));
         double min = 256;
@@ -843,7 +937,7 @@ class SybilRenderer {
             double point = MIN_CLOSE - dist;
             if (point > 0) {
                 point *= OUR_KEY_FACTOR;
-                addPoints(points, ri.getHash(), point, "Very close (" + fmt.format(dist) + ") to our key " + us.toBase64());
+                addPoints(points, ri.getHash(), point, "Very close (" + fmt.format(dist) + ") to our key " + usName + ": " + us.toBase64());
             }
             if (i >= MAX - 1)
                 break;