From 709ccf6c223734e4e09a9fd8048b4eefcbe9a839 Mon Sep 17 00:00:00 2001
From: str4d <str4d@mail.i2p>
Date: Sun, 1 May 2016 01:02:17 +0000
Subject: [PATCH] Use tables for /netdb

---
 .../src/net/i2p/router/web/NetDbRenderer.java | 169 +++++++++++-------
 .../src/net/i2p/router/web/SybilRenderer.java |  81 ++++-----
 2 files changed, 141 insertions(+), 109 deletions(-)

diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
index 199c076053..2d33188239 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
@@ -142,9 +142,50 @@ class NetDbRenderer {
         int rapCount = 0;
         BigInteger median = null;
         int c = 0;
+
+
+        // Summary
+        FloodfillNetworkDatabaseFacade netdb = (FloodfillNetworkDatabaseFacade)_context.netDb();
+        if (debug) {
+            buf.append("<table id=\"leasesetdebug\">\n");
+        } else {
+            buf.append("<table id=\"leasesetsummary\">\n");
+        }
+        buf.append("<tr><th colspan=\"3\">Leaseset Summary</th>")
+           .append("<th><a href=\"/configadvanced\">Configure Floodfill Participation</a></th></tr>\n")
+           .append("<tr><td><b>Total Leasesets:</b></td><td colspan=\"3\">").append(leases.size()).append("</td></tr>\n");
+        if (debug) {
+            buf.append("<tr><td><b>Published (RAP) Leasesets:</b></td><td colspan=\"3\">").append(netdb.getKnownLeaseSets()).append("</td></tr>\n")
+               .append("<tr><td><b>Mod Data:</b></td><td>").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData())).append("</td>")
+               .append("<td><b>Last Changed:</b></td><td>").append(new Date(_context.routerKeyGenerator().getLastChanged())).append("</td></tr>\n")
+               .append("<tr><td><b>Next Mod Data:</b></td><td>").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData())).append("</td>")
+               .append("<td><b>Change in:</b></td><td>").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight())).append("</td></tr>\n");
+        }
+        int ff = _context.peerManager().getPeersByCapability(FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL).size();
+        buf.append("<tr><td><b>Known Floodfills:</b></td><td colspan=\"3\">").append(ff).append("</td></tr>\n")
+           .append("<tr><td><b>Currently Floodfill?</b></td><td colspan=\"3\">").append(netdb.floodfillEnabled() ? "yes" : "no").append("</td></tr>\n");
+        if (debug) {
+            buf.append("<tr><td><b>Network data (only valid if floodfill):</b></td><td colspan=\"3\">");
+            //buf.append("</b></p><p><b>Center of Key Space (router hash): " + ourRKey.toBase64());
+            if (median != null) {
+                double log2 = biLog2(median);
+                buf.append("</td></tr>")
+                   .append("<tr><td><b>Median distance (bits):</b></td><td colspan=\"3\">").append(fmt.format(log2)).append("</td></tr>\n");
+                // 2 for 4 floodfills... -1 for median
+                // this can be way off for unknown reasons
+                int total = (int) Math.round(Math.pow(2, 2 + 256 - 1 - log2));
+                buf.append("<tr><td><b>Estimated total floodfills:</b></td><td colspan=\"3\">").append(total).append("</td></tr>\n");
+                buf.append("<tr><td><b>Estimated total leasesets:</b></td><td colspan=\"3\">").append(total * rapCount / 4);
+            } else {
+                buf.append("<i>Not floodfill or no data.</i>");
+            }
+            buf.append("</td></tr>\n");
+        }
+        buf.append("</table>\n");
+
         if (leases.isEmpty()) {
           if (!debug)
-              buf.append("<i>").append(_t("none")).append("</i>");
+              buf.append("<div id=\"noleasesets\"><i>").append(_t("No Leasesets currently active.")).append("</i></div>");
         } else {
           if (debug) {
             // Find the center of the RAP leasesets
@@ -154,109 +195,91 @@ class NetDbRenderer {
             }
             medianCount = rapCount / 2;
           }
+
           long now = _context.clock().now();
           for (LeaseSet ls : leases) {
             Destination dest = ls.getDestination();
             Hash key = dest.calculateHash();
-            buf.append("<b>").append(_t("LeaseSet")).append(": ").append(key.toBase64()).append("</b>\n");
+            buf.append("<table id=\"leaseset\">\n")
+               .append("<tr><th><b>").append(_t("LeaseSet")).append(":</b> ").append(key.toBase64()).append("</th>");
             if (_context.clientManager().isLocal(dest)) {
-                buf.append(" (<a href=\"tunnels#" + key.toBase64().substring(0,4) + "\">" + _t("Local") + "</a> ");
+                buf.append("<th><b><a href=\"tunnels#" + key.toBase64().substring(0,4) + "\">" + _t("Local") + "</a> ");
                 if (! _context.clientManager().shouldPublishLeaseSet(key))
                     buf.append(_t("Unpublished") + ' ');
-                buf.append(_t("Destination") + ' ');
+                buf.append("<b>").append(_t("Destination")).append(":</b> ");
                 TunnelPoolSettings in = _context.tunnelManager().getInboundSettings(key);
                 if (in != null && in.getDestinationNickname() != null)
                     buf.append(in.getDestinationNickname());
                 else
                     buf.append(dest.toBase64().substring(0, 6));
-                buf.append(")<br>\n");
+                buf.append("</th></tr>\n<tr><td>");
                 String b32 = dest.toBase32();
-                buf.append("<a href=\"http://").append(b32).append("\">").append(b32).append("</a><br>\n");
+                buf.append("<a href=\"http://").append(b32).append("\">").append(b32).append("</a></td>");
                 String host = _context.namingService().reverseLookup(dest);
                 if (host == null) {
-                    buf.append("<a href=\"/susidns/addressbook.jsp?book=private&amp;destination=")
-                       .append(dest.toBase64()).append("#add\">").append(_t("Add to local addressbook")).append("</a><br>\n");    
+                    buf.append("<td>").append("<a href=\"/susidns/addressbook.jsp?book=private&amp;destination=")
+                       .append(dest.toBase64()).append("#add\">").append(_t("Add to local addressbook")).append("</a></td>");
                 }
             } else {
-                buf.append(" (").append(_t("Destination")).append(' ');
+                buf.append("<th><b>").append(_t("Destination")).append(":</b> ");
                 String host = _context.namingService().reverseLookup(dest);
                 if (host != null) {
-                    buf.append("<a href=\"http://").append(host).append("/\">").append(host).append("</a>)<br>\n");
+                    buf.append("<a href=\"http://").append(host).append("/\">").append(host).append("</a></th>");
                 } else {
                     String b32 = dest.toBase32();
-                    buf.append(dest.toBase64().substring(0, 6)).append(")<br>\n" +
-                               "<a href=\"http://").append(b32).append("\">").append(b32).append("</a><br>\n" +
-                               "<a href=\"/susidns/addressbook.jsp?book=private&amp;destination=")
-                       .append(dest.toBase64()).append("#add\">").append(_t("Add to local addressbook")).append("</a><br>\n");    
+                    buf.append("<code>").append(dest.toBase64().substring(0, 6)).append("</code></th>")
+                       .append("</tr>\n<tr>")
+                       .append("<td><a href=\"http://").append(b32).append("\">").append(b32).append("</a></td>\n")
+                       .append("<td><a href=\"/susidns/addressbook.jsp?book=private&amp;destination=")
+                       .append(dest.toBase64()).append("#add\">").append(_t("Add to local addressbook")).append("</a></td>");
                 }
             }
+            buf.append("</tr>\n<tr><td colspan=\"2\">\n");
             long exp = ls.getLatestLeaseDate()-now;
             if (exp > 0)
-                buf.append(_t("Expires in {0}", DataHelper.formatDuration2(exp)));
+                buf.append("<b>").append(_t("Expires in {0}", DataHelper.formatDuration2(exp))).append("</b>");
             else
-                buf.append(_t("Expired {0} ago", DataHelper.formatDuration2(0-exp)));
-            buf.append("<br>\n");
+                buf.append("<b>").append(_t("Expired {0} ago", DataHelper.formatDuration2(0-exp))).append("</b>");
+            buf.append("</td></tr>\n");
             if (debug) {
-                buf.append("RAP? " + ls.getReceivedAsPublished());
-                buf.append(" RAR? " + ls.getReceivedAsReply());
+                buf.append("<tr><td colspan=\"2\">");
+                buf.append("<b>RAP?</b> ").append(ls.getReceivedAsPublished());
+                buf.append(" <b>RAR?</b> ").append(ls.getReceivedAsReply());
                 BigInteger dist = HashDistance.getDistance(ourRKey, ls.getRoutingKey());
                 if (ls.getReceivedAsPublished()) {
                     if (c++ == medianCount)
                         median = dist;
                 }
-                buf.append(" Dist: <b>").append(fmt.format(biLog2(dist))).append("</b><br>");
+                buf.append(" <b>Distance: </b><span id=\"distance\">").append(fmt.format(biLog2(dist))).append("</span></b>");
+                buf.append("</td></tr>\n<tr><td colspan=\"2\">");
                 //buf.append(dest.toBase32()).append("<br>");
-                buf.append("Sig type: ").append(dest.getSigningPublicKey().getType()).append("<br>");
-                buf.append("Routing Key: ").append(ls.getRoutingKey().toBase64());
-                buf.append("<br>");
-                buf.append("Encryption Key: ").append(ls.getEncryptionKey().toBase64().substring(0, 20)).append("...<br>");
+                buf.append("<b>Signature type:</b> ").append(dest.getSigningPublicKey().getType());
+                buf.append(" <b>Encryption Key:</b> ").append(ls.getEncryptionKey().toBase64().substring(0, 20)).append("&hellip;");
+                buf.append("</td></tr>\n<tr><td colspan=\"2\">");
+                buf.append("<b>Routing Key:</b> ").append(ls.getRoutingKey().toBase64());
+                buf.append("</td></tr>");
+
             }
             for (int i = 0; i < ls.getLeaseCount(); i++) {
                 Lease lease = ls.getLease(i);
-                buf.append(_t("Lease")).append(' ').append(i + 1).append(": ").append(_t("Gateway")).append(' ');
+                buf.append("<tr><td colspan=\"2\">");
+                buf.append("<b>").append(_t("Lease")).append(' ').append(i + 1).append(":</b> ").append(_t("Gateway")).append(' ');
                 buf.append(_context.commSystem().renderPeerHTML(lease.getGateway()));
                 buf.append(' ').append(_t("Tunnel")).append(' ').append(lease.getTunnelId().getTunnelId()).append(' ');
                 if (debug) {
                     long exl = lease.getEndDate().getTime() - now;
                     if (exl > 0)
-                        buf.append(_t("Expires in {0}", DataHelper.formatDuration2(exl)));
+                        buf.append("<b>").append(_t("Expires in {0}", DataHelper.formatDuration2(exl))).append("</b>");
                     else
-                        buf.append(_t("Expired {0} ago", DataHelper.formatDuration2(0-exl)));
+                        buf.append("<b>").append(_t("Expired {0} ago", DataHelper.formatDuration2(0-exl))).append("</b>");
                 }
-                buf.append("<br>\n");
+                buf.append("</td></tr>\n");
             }
-            buf.append("<hr>\n");
+            buf.append("</table>\n");
             out.write(buf.toString());
             buf.setLength(0);
           } // for each
         }  // !empty
-        if (debug) {
-            FloodfillNetworkDatabaseFacade netdb = (FloodfillNetworkDatabaseFacade)_context.netDb();
-            buf.append("<p><b>Total Leasesets: ").append(leases.size());
-            buf.append("</b></p><p><b>Published (RAP) Leasesets: ").append(netdb.getKnownLeaseSets());
-            buf.append("</b></p><p><b>Mod Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData()))
-               .append("\" Last Changed: ").append(new Date(_context.routerKeyGenerator().getLastChanged()));
-            buf.append("</b></p><p><b>Next Mod Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData()))
-               .append("\" Change in: ").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight()));
-            int ff = _context.peerManager().getPeersByCapability(FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL).size();
-            buf.append("</b></p><p><b>Known Floodfills: ").append(ff);
-            buf.append("</b></p><p><b>Currently Floodfill? ");
-            buf.append(netdb.floodfillEnabled() ? "yes" : "no");
-            buf.append("</b></p><p><b>Network data (only valid if floodfill):");
-            //buf.append("</b></p><p><b>Center of Key Space (router hash): " + ourRKey.toBase64());
-            if (median != null) {
-                double log2 = biLog2(median);
-                buf.append("</b></p><p><b>Median distance (bits): ").append(fmt.format(log2));
-                // 2 for 4 floodfills... -1 for median
-                // this can be way off for unknown reasons
-                int total = (int) Math.round(Math.pow(2, 2 + 256 - 1 - log2));
-                buf.append("</b></p><p><b>Estimated total floodfills: ").append(total);
-                buf.append("</b></p><p><b>Estimated total leasesets: ").append(total * rapCount / 4);
-            } else {
-                buf.append("</b></p><p><b>Not floodfill or no data");
-            }
-            buf.append("</b></p>");
-        }
         out.write(buf.toString());
         out.flush();
     }
@@ -448,31 +471,39 @@ class NetDbRenderer {
      */
     private void renderRouterInfo(StringBuilder buf, RouterInfo info, boolean isUs, boolean full) {
         String hash = info.getIdentity().getHash().toBase64();
-        buf.append("<table><tr><th><a name=\"").append(hash.substring(0, 6)).append("\" ></a>");
+        buf.append("<table id=\"netdbentry\">")
+           .append("<tr><th colspan=\"2\"><a name=\"").append(hash.substring(0, 6)).append("\" ></a>");
         if (isUs) {
-            buf.append("<a name=\"our-info\" ></a><b>" + _t("Our info") + ": ").append(hash).append("</b></th></tr><tr><td>\n");
+            buf.append("<a name=\"our-info\" ></a><b>" + _t("Our info") + ":</b>&nbsp;<code>").append(hash).append("</code></th><th>");
         } else {
-            buf.append("<b>" + _t("Peer info for") + ":</b> ").append(hash).append("\n");
+            buf.append("<b>" + _t("Peer info for") + ":</b>&nbsp;<code>").append(hash).append("</code></th><th id=\"viewfullentry\">");
             if (!full) {
-                buf.append("[<a href=\"netdb?r=").append(hash.substring(0, 6)).append("\" >").append(_t("Full entry")).append("</a>]");
+                buf.append("<a href=\"netdb?r=").append(hash.substring(0, 6)).append("\" >").append(_t("Full entry")).append("</a>");
             }
-            buf.append("</th></tr><tr><td>\n");
         }
-        
+        buf.append("</th></tr>\n<tr>");
         long age = _context.clock().now() - info.getPublished();
         if (isUs && _context.router().isHidden()) {
-            buf.append("<b>").append(_t("Hidden")).append(", ").append(_t("Updated")).append(":</b> ")
-               .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+            buf.append("<td><b>").append(_t("Hidden")).append(", ").append(_t("Updated")).append(":</b></td>")
+               .append("<td colspan=\"2\">")
+               .append(_t("{0} ago", DataHelper.formatDuration2(age)))
+               .append("</td>");
         } else if (age > 0) {
-            buf.append("<b>").append(_t("Published")).append(":</b> ")
-               .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+            buf.append("<td><b>").append(_t("Published")).append(":</b></td>")
+               .append("<td colspan=\"2\">")
+               .append(_t("{0} ago", DataHelper.formatDuration2(age)))
+               .append("</td>");
         } else {
             // shouldnt happen
-            buf.append("<b>" + _t("Published") + ":</b> in ").append(DataHelper.formatDuration2(0-age)).append("???<br>\n");
+            buf.append("<td colspan=\"2\"><b>").append(_t("Published")).append(":</b> in ").append(DataHelper.formatDuration2(0-age)).append("???</td>");
         }
+        buf.append("</tr>\n<tr><td>");
         buf.append("<b>").append(_t("Signing Key")).append(":</b> ")
+           .append("</td><td colspan=\"2\">")
            .append(info.getIdentity().getSigningPublicKey().getType().toString());
-        buf.append("<br>\n<b>" + _t("Address(es)") + ":</b> ");
+        buf.append("</td></tr>\n<tr>")
+           .append("<td><b>" + _t("Address(es)") + ":</b></td>")
+           .append("<td colspan=\"2\">");
         String country = _context.commSystem().getCountry(info.getIdentity().getHash());
         if(country != null) {
             buf.append("<img height=\"11\" width=\"16\" alt=\"").append(country.toUpperCase(Locale.US)).append('\"');
@@ -494,7 +525,7 @@ class NetDbRenderer {
         }
         buf.append("</td></tr>\n");
         if (full) {
-            buf.append("<tr><td>" + _t("Stats") + ": <br><code>");
+            buf.append("<tr><td><b>" + _t("Stats") + ":</b><td colspan=\"2\"><code>");
             Map<Object, Object> p = info.getOptionsMap();
             for (Map.Entry<Object, Object> e : p.entrySet()) {
                 String key = (String) e.getKey();
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java
index abc818f119..a9879448fa 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java
@@ -189,12 +189,13 @@ class SybilRenderer {
             tot += d;
         }
         double avgMinDist = tot / count;
-        buf.append("<p>Average closest floodfill distance: " + fmt.format(avgMinDist) + "</p>");
-        buf.append("<p>Routing Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData()))
-           .append("\" Last Changed: ").append(new Date(_context.routerKeyGenerator().getLastChanged()));
-        buf.append("</p><p>Next Routing Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData()))
-           .append("\" Rotates in: ").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight()));
-        buf.append("</p>");
+        buf.append("<div id=\"sybils_summary\">\n");
+        buf.append("<b>Average closest floodfill distance:</b> ").append(fmt.format(avgMinDist)).append("<br>\n");
+        buf.append("<b>Routing Data:</b> \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData()))
+           .append("\" <b>Last Changed:</b> ").append(new Date(_context.routerKeyGenerator().getLastChanged())).append("<br>\n");
+        buf.append("<b>Next Routing Data:</b> \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData()))
+           .append("\" <b>Rotates in:</b> ").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight())).append("\n");
+        buf.append("</div>\n");
 
         Map<Hash, Points> points = new HashMap<Hash, Points>(64);
 
@@ -760,35 +761,35 @@ class SybilRenderer {
      */
     private double renderRouterInfo(StringBuilder buf, RouterInfo info, Hash us, boolean isUs, boolean full) {
         String hash = info.getIdentity().getHash().toBase64();
-        buf.append("<table><tr><th><a name=\"").append(hash.substring(0, 6)).append("\" ></a>");
+        buf.append("<table class=\"sybil_routerinfo\"><a name=\"").append(hash.substring(0, 6)).append("\" ></a><tr>");
         double distance = 0;
         if (isUs) {
-            buf.append("<a name=\"our-info\" ></a><b>" + _t("Our info") + ": ").append(hash).append("</b></th></tr><tr><td>\n");
+            buf.append("<th colspan=\"4\"><a name=\"our-info\" ></a><b>" + _t("Our info") + ":</b> <code>").append(hash).append("</code></th></tr>\n");
         } else {
-            buf.append("<b>" + _t("Router") + ":</b> ").append(hash).append("\n");
+            buf.append("<th colspan=\"2\"><b>" + _t("Router") + ":</b> <code>").append(hash).append("</code>\n");
             if (!full) {
-                buf.append("[<a href=\"netdb?r=").append(hash.substring(0, 6)).append("\" >").append(_t("Full entry")).append("</a>]");
+                buf.append("</th><th><a href=\"netdb?r=").append(hash.substring(0, 6)).append("\" >").append(_t("Full entry")).append("</a></th><th>");
             }
-            buf.append("</th><th><img src=\"/imagegen/id?s=32&amp;c=" + hash.replace("=", "%3d") + "\" height=\"32\" width=\"32\"> ");
-            buf.append("</th></tr><tr><td colspan=\"2\">\n");
+            buf.append("<img src=\"/imagegen/id?s=32&amp;c=" + hash.replace("=", "%3d") + "\" height=\"32\" width=\"32\"> ");
+            buf.append("</th></tr>\n");
             if (us != null) {
                 BigInteger dist = HashDistance.getDistance(us, info.getHash());
                 distance = biLog2(dist);
-                buf.append("<b>Hash Distance: ").append(fmt.format(distance)).append("</b><br>");
+                buf.append("<tr><td><b>Hash Distance:</b></td><td colspan=\"3\">").append(fmt.format(distance)).append("</td></tr>\n");
             }
         }
-        buf.append("<b>Routing Key: </b>").append(info.getRoutingKey().toBase64()).append("<br>\n");
-        buf.append("<b>Version: </b>").append(DataHelper.stripHTML(info.getVersion())).append("<br>\n");
-        buf.append("<b>Caps: </b>").append(DataHelper.stripHTML(info.getCapabilities())).append("<br>\n");
+        buf.append("<tr><td><b>Routing Key:</b></td><td colspan=\"3\">").append(info.getRoutingKey().toBase64()).append("</td></tr>\n");
+        buf.append("<tr><td><b>Version:</b></td><td colspan=\"3\">").append(DataHelper.stripHTML(info.getVersion())).append("</td></tr>\n");
+        buf.append("<tr><td><b>Caps:</b></td><td colspan=\"3\">").append(DataHelper.stripHTML(info.getCapabilities())).append("</td></tr>\n");
         String fam = info.getOption("family");
         if (fam != null)
-            buf.append("<b>Family: ").append(DataHelper.escapeHTML(fam)).append("</b><br>\n");
+            buf.append("<tr><td><b>Family:</b></td><td colspan=\"3\">").append(DataHelper.escapeHTML(fam)).append("</td></tr>\n");
         String kls = info.getOption("netdb.knownLeaseSets");
         if (kls != null)
-            buf.append("<b>Lease Sets: </b>").append(DataHelper.stripHTML(kls)).append("<br>\n");
+            buf.append("<tr><td><b>Lease Sets:</b></td><td colspan=\"3\">").append(DataHelper.stripHTML(kls)).append("</td></tr>\n");
         String kr = info.getOption("netdb.knownRouters");
         if (kr != null)
-            buf.append("<b>Routers: </b>").append(DataHelper.stripHTML(kr)).append("<br>\n");
+            buf.append("<tr><td><b>Routers:</b></td><td colspan=\"3\">").append(DataHelper.stripHTML(kr)).append("</td></tr>\n");
         
         long now = _context.clock().now();
         if (!isUs) {
@@ -797,46 +798,46 @@ class SybilRenderer {
                 long heard = prof.getFirstHeardAbout();
                 if (heard > 0) {
                     long age = Math.max(now - heard, 1);
-                    buf.append("<b>First heard about:</b> ")
-                       .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+                    buf.append("<tr><td><b>First heard about:</b></td><td colspan=\"3\">")
+                       .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
                 }
                 heard = prof.getLastHeardAbout();
                 if (heard > 0) {
                     long age = Math.max(now - heard, 1);
-                    buf.append("<b>Last heard about:</b> ")
-                       .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+                    buf.append("<tr><td><b>Last heard about:</b></td><td colspan=\"3\">")
+                       .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
                 }
                 heard = prof.getLastHeardFrom();
                 if (heard > 0) {
                     long age = Math.max(now - heard, 1);
-                    buf.append("<b>Last heard from:</b> ")
-                       .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+                    buf.append("<tr><td><b>Last heard from:</b></td><td colspan=\"3\">")
+                       .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
                 }
                 DBHistory dbh = prof.getDBHistory();
                 if (dbh != null) {
                     heard = dbh.getLastLookupSuccessful();
                     if (heard > 0) {
                         long age = Math.max(now - heard, 1);
-                        buf.append("<b>Last lookup successful:</b> ")
-                           .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+                        buf.append("<tr><td><b>Last lookup successful:</b></td><td colspan=\"3\">")
+                           .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
                     }
                     heard = dbh.getLastLookupFailed();
                     if (heard > 0) {
                         long age = Math.max(now - heard, 1);
-                        buf.append("<b>Last lookup failed:</b> ")
-                           .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+                        buf.append("<tr><td><b>Last lookup failed:</b></td><td colspan=\"3\"> ")
+                           .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
                     }
                     heard = dbh.getLastStoreSuccessful();
                     if (heard > 0) {
                         long age = Math.max(now - heard, 1);
-                        buf.append("<b>Last store successful:</b> ")
-                           .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+                        buf.append("<tr><td><b>Last store successful:</b></td><td colspan=\"3\">")
+                           .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
                     }
                     heard = dbh.getLastStoreFailed();
                     if (heard > 0) {
                         long age = Math.max(now - heard, 1);
-                        buf.append("<b>Last store failed:</b> ")
-                           .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+                        buf.append("<tr><td><b>Last store failed:</b></td><td colspan=\"3\">")
+                           .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
                     }
                 }
                 // any other profile stuff?
@@ -844,15 +845,15 @@ class SybilRenderer {
         }
         long age = Math.max(now - info.getPublished(), 1);
         if (isUs && _context.router().isHidden()) {
-            buf.append("<b>").append(_t("Hidden")).append(", ").append(_t("Updated")).append(":</b> ")
-               .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+            buf.append("<tr><td><b>").append(_t("Hidden")).append(", ").append(_t("Updated")).append(":</b></td><td colspan=\"3\">")
+               .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
         } else {
-            buf.append("<b>").append(_t("Published")).append(":</b> ")
-               .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("<br>\n");
+            buf.append("<tr><td><b>").append(_t("Published")).append(":</b></td><td colspan=\"3\">")
+               .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("</td></tr>\n");
         }
-        buf.append("<b>").append(_t("Signing Key")).append(":</b> ")
-           .append(info.getIdentity().getSigningPublicKey().getType().toString());
-        buf.append("<br>\n<b>" + _t("Addresses") + ":</b> ");
+        buf.append("<tr><td><b>").append(_t("Signing Key")).append(":</b></td><td colspan=\"3\">")
+           .append(info.getIdentity().getSigningPublicKey().getType().toString()).append("</td></tr>\n");
+        buf.append("<tr><td><b>" + _t("Addresses") + ":</b></td><td colspan=\"3\">");
         String country = _context.commSystem().getCountry(info.getIdentity().getHash());
         if(country != null) {
             buf.append("<img height=\"11\" width=\"16\" alt=\"").append(country.toUpperCase(Locale.US)).append('\"');
-- 
GitLab